一、概述
浏览器是单设备上凭据密度最高的取证目标。一个普通用户的 Chrome 里可能有上百个站点的账号密码、信用卡信息、Cookie 会话和长期有效的 API Token。在磁盘上能拿到的凭据里,它常常是最集中的一份。
没有这套方法,案件里最常见的一类问题就悬在半空:"这个人掌握了哪些系统的访问权"。
凭据清单能给出站点的完整覆盖面。而覆盖面在内部调查、账号泄露追源、数据外泄路径分析里,都是决定性事实。
浏览器凭据相比其它类型的加密存储,有三个特点让它在案件里格外重要。
- 多站点覆盖。一次提取往往就回答了"这个人掌握哪些系统的访问权",不需要逐个系统单独取证。
- Token 常常绕过口令。
Local Storage 与扩展存储里的 token 很多是明文,拿到就能直接访问账号,不需要任何解密。
这一点必须先讲清楚,否则你会把大量明文 target 当成"解不开的密文"而放弃。
- 防护机制正在变紧。Chrome 127 引入的 App-Bound Encryption(ABE)已经让大量流传的 DPAPI 解密教程失效——这是本篇必须写清的第一个重点,也是本章最容易被旧资料误导的地方。
本篇要写清的最重要一句:如果你在网上找到的教程只做了"读 Local State → CryptUnprotectData → AES-GCM 解 v10",它在 2024 年 7 月之后的 Chrome 上对部分数据(Cookies)会失败。这不是工具的 bug,是防护边界的设计变更。 原因见 2.5。
本篇处理的证据形态:
| 浏览器 |
关键文件 |
加密层 |
判据 |
| Chrome / Edge / Brave(Chromium 系) |
Login Data、Cookies、Web Data(SQLite),Local State(JSON),Local Storage/leveldb/ |
AES-GCM,密文前缀 v10 / v11 / v20 |
v20 = ABE 保护,传统 DPAPI 流程必然解不开 |
| Firefox |
logins.json、key4.db、cookies.sqlite |
NSS 体系,不是 DPAPI |
key4.db 里 NSS 私钥项的加密方式 |
| Safari(macOS) |
Keychains 文件 |
Keychain 钥匙串,受系统钥匙串 ACL 保护 |
与前两者机制完全不同 |
记住 Chromium 系和 Firefox 是两套完全独立的体系——加密层、密钥存放、破解前提都不通用。把 Chrome 的方法套到 Firefox 上(或者反过来)会一路碰壁。
而且同一个 Chromium 内部也有代次差异:Chrome 80 改过一次前缀(v10 → v11),Chrome 127 又引入了 v20。
密文前缀本身就是判据。 读任何一条记录之前先看前缀,再决定走哪条解密链。
本篇的操作主链是五步,每一步都有明确的产出与停手条件:
| 步骤 |
动作 |
产出 |
什么时候就该停 |
| 1 |
定位所有浏览器与所有 Profile |
浏览器清单 + Profile 路径清单 |
只找到一个 Profile 就动手 → 会漏读 |
| 2 |
读 Local State,拿主密钥并判 ABE 状态 |
是否有 app_bound_encrypted_key(解码后以 APPB 开头) |
出现 v20 前缀且无内存镜像 → 转"本阶段不可解" |
| 3 |
解密 Login Data 并导出 |
域名 / 用户名 / 口令(脱敏) |
遇到 v20 不换工具硬试 |
| 4 |
顺带取更高价值的目标 |
Cookies、支付数据、Local Storage 里的 token |
token 若是明文就到此为止 |
| 5 |
记录与脱敏 |
结构化记录 + 脱敏说明 |
完整明文不进报告正文 |
第 4 步是本篇性价比最高的一步。
很多人把全部精力耗在 Login Data 上,而 Local Storage 与扩展存储里的 token 大量是明文的——它们不需要任何解密就能读出来,却往往比口令更直接地指向访问权。
反过来,第 3 步的失败也不该被当成失败:v20 在关机镜像上解不开是符合预期的正确结果。写成"该数据受 App-Bound Encryption 保护,在现有检材范围内不可解"就是完整结论。
在取证流程中的位置:本篇属于分析环节,紧跟在采集之后。
上游依赖是镜像制作——浏览器 profile 分散在用户目录下,必须从镜像里取,不能在目标机现场翻;只读挂载与安全接入提供只读访问;SQLite 基础与结构是解 Login Data 与 Cookies 的前置。
DPAPI 机制的完整背景见EFS 文件系统加密取证。浏览器密码库用的就是同一套 DPAPI 主密钥,主密码一旦解开,EFS 与浏览器两条线同时打开。
下游支撑的是浏览器取证里的历史、下载与扩展痕迹,与本篇的凭据侧互为印证。涉及"这个人访问过什么"的问题,则延伸到时间线重建。
这台机器上装过哪些浏览器、有几个用户 Profile、每个 Profile 的凭据覆盖面(域名 + 用户名)、哪些字段是可读的明文、哪些受 ABE 或系统钥匙串保护、Cookie 会话与本地存储里有哪些 token、以及它们的可用期限。
v20 加密的数据在不改变检材状态的前提下解不开——这是 ABE 的设计目标,不是能力不足。Firefox 在不知道主密码且无内存镜像时同样受阻。
更不能回答的是**"这个人有没有实际使用过某个账号"**。凭据存在与使用是两条独立的证据链,后者要走浏览器取证的历史与下载记录。
本篇也明确不涉及任何在线验证行为。
读者前提:你需要先掌握 SQLite 的基本读写(SQLite 基础与结构)、DPAPI 的工作方式与主密钥存放(EFS 文件系统加密取证)、以及 AES-GCM 的密文结构(前缀 + 12 字节 IV + 密文 + 16 字节 tag)。
合规前提同样要先对齐:提取出的凭据默认只做固定与上报、不做使用,相关要求见合规授权与法律边界。
如果你的检材只有一台已关机设备、没有内存镜像,遇到 v20 密文就应当把结论写成"在现有检材范围内不可解"并列出未覆盖范围,而不是换工具继续试。
最后提醒一个范围问题:一个 Profile 里的凭据清单,只覆盖这台机器上这个浏览器。
同一个人可能有第二个浏览器、另一台设备、手机端的 App 登录态。
报告里写"发现 N 组凭据"时必须同时写明范围,否则"未发现的凭据"这个推论会在质证时被推翻——尤其当案件本身涉及多设备时。
二、核心原理
2.1 Chromium 系的文件布局
所有 Chromium 系浏览器结构完全相同,只是路径不同——理解一次即可通用于全部:
| 浏览器 |
User Data 根目录(%LOCALAPPDATA% 下,Opera 例外) |
| Chrome |
Google\Chrome\User Data |
| Edge |
Microsoft\Edge\User Data |
| Brave |
BraveSoftware\Brave-Browser\User Data |
| Vivaldi |
Vivaldi\User Data |
| Opera |
%APPDATA%\Opera Software\Opera Stable |
| 360 等国产套壳 |
\360Chrome\Chrome\User Data(结构同 Chrome) |
根目录下:
| 文件/目录 |
内容 |
取证价值 |
Local State |
JSON,存放主密钥 |
解密链的起点 |
<Profile>\Login Data |
SQLite,口令与自动填充用户名 |
最高 |
<Profile>\Web Data |
SQLite,自动填充、信用卡 |
高(含卡号) |
<Profile>\Cookies |
SQLite,会话 Cookie |
高(token 常在此) |
<Profile>\History |
SQLite,访问历史 |
高 |
<Profile>\Local Storage\leveldb\ |
LevelDB,token 常明文 |
最高之一 |
<Profile>\Session Storage |
LevelDB,会话态 |
中 |
<Profile>\Local Extension Settings\<id> |
扩展数据 LevelDB |
中(可含扩展保存的凭据) |
<Profile>\Preferences |
JSON,搜索词、主页、扩展列表 |
中(搜索历史是主观意图的直接证据) |
Profile 目录名不是 Default 就找不着。 实际是 Default、Profile 1、Profile 2……数量与用户数对应。必须遍历所有 Profile,只读 Default 会漏掉其他账号体系。
2.2 v10/v11 的解密链(Chrome 80+ 的传统路径)
Local State
└─ os_crypt.encrypted_key (Base64 字符串)
│ Base64 解码
├─ [0:5] = b"DPAPI" ← ASCII 前缀,不是密文,剥掉
└─ [5:] = DPAPI 密文 ← CryptUnprotectData → 32 字节 AES 主密钥
│
Login Data 的 logins.password_value (BLOB) │
└─ [0:3] = b"v10" 或 b"v11" │
└─ [3:15] = 12 字节 GCM nonce │
└─ [15:-16] = 密文 ├── AES-256-GCM 解密
└─ [-16:] = 16 字节 GCM tag ┘
需要纠正的常见说法:
- 主密钥是 32 字节,
v10 和 v11 都走 AES-256-GCM。 说"v10 是 AES-128、v11 才是 AES-256"是错的——密钥长度由 Local State 决定,与前缀无关。v11 与 v10 的差异在 Chromium 内部实现,解密方式与前缀无关。
- 前缀不是加密算法标识。 只需要判断"是 v10/v11 走 GCM"还是"无前缀走旧版 DPAPI"。
判定分支,实际写脚本时按这个顺序:
len(blob) >= 3 且 blob[0:3] in (b"v10", b"v11") → 取主密钥走 AES-256-GCM
blob[0:3] == b"v20" → ABE 保护的 Cookie,见 2.5
blob[0:8] == 01 00 00 00 D0 8C 9D D0 → Chrome 80 之前的旧版 DPAPI
2.3 Linux / macOS 上的 Chromium
同一份 Login Data,密钥来源不同:
| 平台 |
密钥来源 |
取证难度 |
| Windows |
DPAPI(用户凭据绑定) |
中,受 2.5 的 ABE 影响 |
| macOS |
Keychain,需该用户的 keychain 密码 |
高(详见 [07 之外:Keychain 解锁是独立课题) |
| Linux |
固定口令 peanuts(明文硬编码在源码里) |
极低——主密钥可直接算出 |
Linux 上的 peanuts 值得单独记住:Chromium 在 Linux 上用一个硬编码的固定口令加密主密钥。这在设计上是"依赖 kwallet/gnome-libsecret"的兜底路径,但只要文件可读,密钥就可复现。这是跨平台对比中差异最大的一项。
2.4 Firefox:用另一套体系(NSS)
Firefox 完全不走 DPAPI/AES-GCM,而是 Mozilla 的 NSS(Network Security Services):
| 文件 |
内容 |
%APPDATA%\Mozilla\Firefox\Profiles\<随机名>.default-release\logins.json |
凭据清单,字段名是明文,密码是 NSS 加密串 |
同目录 key4.db |
SQLite,存放 NSS 主密钥(PKCS#11 格式) |
cert9.db(老版本 cert8.db) |
证书库 |
global-magento-db.json |
老版本(<58)的密码库,已被 logins.json 取代 |
解密依赖关系:logins.json 里的 encryptedUsername/encryptedPassword 需要 key4.db 中的密钥,两者同目录。
若用户设置了 Primary Password(主密码),则还需该密码参与解锁,无密码时不可解。
取证顺序:先看 logins.json 里的 hostname 字段——这已经是明文的高价值证据("这台机器访问过哪些站点"),密码本身往往不是必需的。
2.5 ⚠️ App-Bound Encryption:为什么老教程失效了
这是本篇的技术核心,Chrome 127(2024 年 7 月)开始引入。
旧模型的弱点:DPAPI 只能证明"这个数据属于这个 Windows 用户"。
任何以该用户身份运行的程序——包括信息窃取木马——都能调用 CryptUnprotectData 把它解开。
防护边界在用户,不在程序。
ABE 把边界从用户移到了程序:
① Chrome 生成随机 32 字节 app_bound_key
② 把它交给一个以 SYSTEM 权限运行的 COM 服务(Elevation Service)
③ 该服务做信封加密:
- 把调用者进程的可执行文件路径作为身份数据编入(规范化后比对)
- 用硬编码在服务内的密钥加密 app_bound_key
- 内层 CryptProtectData(用户上下文)
- 外层 CryptProtectData(SYSTEM 上下文) ← 双层 DPAPI 包装
④ 结果写入 Local State 的 os_crypt.app_bound_encrypted_key
(Base64,解码后以 ASCII "APPB" 开头)
解密时:Chrome 通过 COM 接口(IElevator::DecryptData)把 APPB 块交给 Elevation Service,服务验证调用者确实是那个签名的 chrome.exe,才返回 32 字节密钥。
别的程序来问,一律被拒。
在 Local State 里怎么认出来:看是否存在 app_bound_encrypted_key 字段(值解码后以 ASCII APPB 开头),命令见第七节 2。
受影响范围——要精确,不要过度宣称:
| 数据 |
Chrome 127+ 状态 |
| Cookies |
首批迁移到 ABE,密文前缀为 v20 |
Login Data 口令 |
127 初始批次尚未全部迁移,多数仍为 v10/v11 + 传统 DPAPI 主密钥 |
| 支付数据等 |
设计上计划后续迁移 |
准确的表述是"部分失效"而非"全部失效":ABE 首发覆盖 Cookies,Login Data 的迁移在后续版本逐步推进。判据是数据本身的前缀——v20 就是 ABE 保护,传统 DPAPI 流程必然解不开。
三条应对路径,都要写进报告的方法论,说明各自代价:
| 路径 |
做法 |
代价与限制 |
| 运行中提取 |
在目标机器上让浏览器处于运行态,从其进程内存中取得已解密的密钥材料 |
改变了检材状态,需授权;破坏了"关机镜像"这一取证前提 |
| 受控启动参数 |
以 --disable-features=AppBoundEncryption 启动浏览器 |
需要用户侧配合或可控账户;改变了配置与可能的运行痕迹,必须记录 |
| 维持静态分析 |
关机镜像上只解 v10/v11 数据,对 v20 数据如实记录为"受 ABE 保护,本阶段未解出" |
最保守、零污染,v20 密文本身的存在即是证据 |
取证原则:"没解出来"是一个合法且应当如实写进报告的结论。 不要为了"拿到明文"而越过授权去改检材状态。密文存在、格式可辨、机制可述——这本身就是充分的证据描述。
一条有用的旁证:Chrome 在 ABE 校验失败时会在 Application 日志的 Chrome 源写 Event ID 257。
日志里出现这条事件,说明当时有程序试图解密该数据但被拒——这本身就是一条独立的攻击痕迹证据。
2.6 Safari 与 Edge Legacy
| 目标 |
位置与机制 |
要点 |
| Safari |
~/Library/Keychains/login.keychain-db、~/Library/Safari/History.db |
凭据在 Keychain,需该用户 keychain 密码解锁;iOS 备份另有一套 schema |
| Edge Legacy |
%LOCALAPPDATA%\Microsoft\Windows\WebCache\WebCacheV01.dat |
ESE 数据库格式,非 SQLite,必须用专用解析工具,直接读取只会得到乱码 |
不要把 Edge Legacy 和 Chromium 版 Edge 搞混:新版 Edge 是 Chromium,路径与 Chrome 一致;WebCacheV01.dat 属于 IE 时代的遗留组件,两者机制完全不同。
2.7 合规边界
| 允许 |
禁止 |
| 提取并脱敏记录"该设备曾登录过某站点" |
用提取的凭据登录任何网站 |
| 记录 Cookie/token 的存在、有效期、归属站点 |
使用 token 访问对方账号(包括"只看一下有没有") |
| 用 token 的格式与签发时间做时间线分析 |
把完整凭据写进报告正文 |
| 将凭据固定为证据并移交委托方 |
私存一份"以备后用" |
报告中的正确表述:"在 Chrome Profile 1 的 Local Storage/leveldb/ 中发现某站点会话 token(JWT 格式,签发时间 2024-03-11,有效期 7 天),表明该设备在该时段持有该站点的有效会话。本次分析未使用该 token 访问任何服务。"
错误表述:"恢复出该站点账号密码,可登录查看内容。"——这既超出了委托范围,也可能构成非法访问。
三、操作步骤
步骤 1:定位所有浏览器与所有 Profile