关键词:Chromium、Firefox、Internet Explorer、Edge Legacy、os_crypt、DPAPI、AES-256-GCM、App-Bound Encryption、NSS、places.sqlite、WebCacheV01.dat
难度:进阶
前置知识:SQLite 查询、注册表取证、DPAPI 主密钥机制、事件日志时间线分析
一、概述
个人设备上信息密度最高的单一应用,就是浏览器。它同时记下了访问过什么、搜索过什么、下载过什么、登录过什么、填过什么,以及什么时候发生。
外泄案件里,攻击者几乎必然要在浏览器里留下一段访问记录。内部调查里那些「他到底访问了哪个云盘后台、什么时候导出的报表」的问题,答案九成落在浏览器工件里。
比 LNK、Prefetch 这类要解析二进制格式的工件,浏览器有个明显优势:数据以明文语义存储。URL、标题、表单值、搜索词都是可直接查询的文本,分析效率极高,误判率也极低。这让浏览器数据库成为时间线构建的第一优先级工件。
短板同样突出:所有敏感字段都被加密,加密方案随浏览器品牌、版本、平台反复变化。 Login Data 里明明有 password_value 字段却解不开——这是新手最常见的误判。解不开不等于没有。 误判的代价不是「少一条线索」,而是把一条可用的证据写成不可用。
这一块覆盖的是 Windows 平台上的主流桌面浏览器。用户级痕迹(UserAssist、TypedPaths)见用户行为痕迹,卷的定位与提取见Windows 取证工件地图。
浏览器工件属于分析环节的高频入口,位置在「工件已提取、开始逐应用判读」这一步。
上游依赖三件事。镜像接入方式是前提——浏览器数据库文件在被打开时会写入临时文件与锁文件,直接挂载分析等于修改证据,见只读挂载与安全接入。DPAPI 主密钥必须按用户上下文取用,取不到则凭据类字段全部不可解。用户身份确认决定取哪个 Profile。
它自己要先做两件判断:每个浏览器的用户数据目录有多少个 Profile(Chromium 系有 Default、Profile 1、Profile 2……,只看 Default 会漏掉大量数据);以及密文前缀属于三代方案里的哪一代(决定能不能解、怎么解)。
下游支撑三件事。访问时间是时间线的第一优先级输入;下载记录要与系统层的下载痕迹(Zone.Identifier、SRUM 的网络用量)交叉;凭据的存在性结论(而非明文本身)可以支撑「该机登录过某服务」这类判断。
浏览器应用内痕迹、SRUM 数据库分析的系统级资源使用统计、Windows 11 与 Recall的系统级屏幕快照,三者是并列关系。
落到具体文件上:
| 浏览器族 |
用户数据目录 |
关键文件 |
| Chrome |
%LOCALAPPDATA%\Google\Chrome\User Data |
Local State、Default\History、Login Data、Web Data、Network\Cookies、Preferences、Secure Preferences |
| Edge(Chromium) |
%LOCALAPPDATA%\Microsoft\Edge\User Data |
结构同上,仅目录名不同 |
| Brave / Opera |
各自的 User Data / Opera Stable 目录 |
结构同上 |
| Firefox |
%APPDATA%\Mozilla\Firefox\Profiles\*.default-release |
profiles.ini、places.sqlite、cookies.sqlite、formhistory.sqlite、logins.json、key4.db、prefs.js |
| IE / Edge Legacy |
%LOCALAPPDATA%\Microsoft\Windows\History |
index.dat、WebCacheV01.dat |
| 密钥载体 |
Local State 里的 os_crypt.encrypted_key / os_crypt.app_bound_encrypted_key |
前者 Base64 解码后前 5 字节固定为 ASCII DPAPI;后者前缀 ASCII APPB |
| DPAPI 主密钥 |
%APPDATA%\Microsoft\Protect\<SID>\ |
解密前提是「在正确的用户上下文里」,不是「知道密码」 |
三个判定要点:Chromium 密文前缀分三代(无前缀 / v10 / v20)、DPAPI 解密靠用户上下文而非口令、v20 的双层包装使离线解密不具通用性。
据此能回答的是:访问过哪些 URL 及时间、搜索过什么、下载过什么文件、登录过哪些站点、表单自动填充过什么、Cookie 与站点设置的当前状态、以及某个凭据条目是否存在。
回答不了的是这些。下面几个误解必须先纠正:
- 「密文解不开」不等于「没有这条记录」。
password_value 字段存在而解不开,是正常的凭据可解性限制,不是数据缺失。写成「无凭据」是事实性错误。
v20 密文解不开是预期内的。 它是两层 DPAPI 包装,外层在 SYSTEM 上下文、内层在用户上下文,服务还会校验调用进程的可执行文件路径。成功率取决于浏览器版本、安装路径与系统状态,不应向委托人承诺「一定解得开」。
- 密文方案的版本差异不能套用。 Windows 上的 Chromium 密文前缀是
v10,其它平台会因密钥管理方式不同出现其他前缀,跨平台样本时把非 v10 前缀套进 GCM 结构解析会得到错误结论。
- 非 Chromium 浏览器的方法不能套用。 Firefox 是 NSS 加密(
key4.db),IE / Edge Legacy 多数字段明文但结构完全不同,三者各有解析路径。
- 仅看
Default 目录的完整性。 Chromium 系的多个 Profile 各自独立,漏掉 Profile 就是漏掉数据。
- 访问时间与访问行为的区别。 数据库记录的是时间戳与 URL,不代表用户实际阅读了页面内容。
- Profile 已删除但数据仍在盘上的情形。 这需要卷层面的搜索能力,浏览器数据库本身回答不了。
读到这里你应该已经掌握:SQLite 查询,能从数据库里定位表、字段并处理时间基准转换,Safari / Chromium 都有各自的纪元偏移;DPAPI 的工作前提,主密钥与用户 SID 绑定,解密需要在对应用户上下文执行,见注册表取证中 hive 的归属判定;Windows 目录结构与用户数据路径,能定位 %LOCALAPPDATA% 与 %APPDATA% 下的多个 Profile;AES-GCM 的报文结构(密文与 tag 分离,取 tag 时截断会导致解密失败或校验不过)。
还有一条边界要立住:凭据材料只做离线可解性判断与元数据分析,不做在线使用、不记录口令明文。
二、核心原理
2.1 工件矩阵
| 浏览器族 |
用户数据目录 |
关键文件 |
访问凭据存储 |
| Chrome |
%LOCALAPPDATA%\Google\Chrome\User Data |
Local State、Default\History、Login Data、Web Data、Network\Cookies、Preferences、Secure Preferences |
v10 / v20 密文 |
| Edge (Chromium) |
%LOCALAPPDATA%\Microsoft\Edge\User Data |
同上(目录名不同) |
v10 / v20 密文 |
| Brave |
%LOCALAPPDATA%\BraveSoftware\Brave-Browser\User Data |
同上 |
v10 / v20 密文 |
| Opera |
%APPDATA%\Opera Software\Opera Stable |
同上 |
v10 / v20 密文 |
| Firefox |
%APPDATA%\Mozilla\Firefox\Profiles\*.default-release |
profiles.ini、places.sqlite、cookies.sqlite、formhistory.sqlite、logins.json、key4.db、prefs.js |
NSS 加密 |
| IE / Edge Legacy |
%LOCALAPPDATA%\Microsoft\Windows\History、...\WebCache |
index.dat、WebCacheV01.dat |
多数字段明文 |
Chromium 系的用户数据目录下有多个配置文件目录(Default、Profile 1、Profile 2……),必须逐个遍历,只看 Default 会漏掉大量数据。
2.2 Chromium 核心数据库
| 数据库 |
关键表 |
取证价值 |
History |
urls、visits、keyword_search_terms、downloads、downloads_url_chains |
完整访问与下载时间线、搜索词、下载来源 |
Login Data |
logins |
站点、用户名、密码密文 |
Web Data |
autofill、credit_cards、keywords |
表单自动填充值、信用卡信息 |
Network\Cookies |
meta、cookies |
会话 Cookie,可用于会话劫持与溯源 |
urls 与 visits 是 1:N 关系。
urls 保存 URL 字符串与累计统计。visits 每次打开写一行,是时间线的真正来源。
keyword_search_terms 通过 url_id 关联搜索产生的访问记录。
visits.transition 的低位(transition & 0xFF)标注访问方式,判定「用户是主动输入还是点链接进来的」:
| transition |
含义 |
取证意义 |
0x01000000 |
TYPED |
用户手工键入 URL 或从地址栏搜索,意图明确 |
0x30000000 |
LINK |
从链接点击 |
0x00020000 |
KEYWORD |
通过搜索关键词到达 |
0x00080000 |
FORM_SUBMIT |
表单提交跳转 |
0x02000000 |
RELOAD |
刷新 |
0x10000000 |
FORWARD_BACK |
前进/后退 |
0x40000001 |
CHAIN_END |
一次跳转链的结束点 |
2.3 Chromium 加密:三代方案并存
Chromium 的凭据加密不是一套方案,而是三代并存。
判定入口是密文前 3 字节前缀:
| 前缀 |
密钥载体 |
算法 |
绑定范围 |
出现版本 |
| 无前缀 |
直接 DPAPI |
DPAPI |
用户 + 机器 |
Chrome 80 之前 |
v10 |
Local State 的 os_crypt.encrypted_key |
AES-256-GCM |
用户 + 机器 |
Chrome 80 起(Windows) |
v20 |
Local State 的 os_crypt.app_bound_encrypted_key |
AES-256-GCM |
应用路径 + 服务 + 用户 |
Chrome 127 起(Windows) |
平台差异:Windows 上的 Chromium 密文前缀是 v10。其他平台会因密钥管理方式不同出现其他前缀,跨平台样本时不要把非 v10 前缀套进下文的 GCM 结构。
os_crypt.encrypted_key 是 Base64 字符串,解码后前 5 字节固定为 ASCII DPAPI,其后才是 DPAPI 密文;用对应用户上下文的 CryptUnprotectData 解开后,得到 32 字节 AES 密钥。
os_crypt.app_bound_encrypted_key 是 Base64 字符串,前缀为 ASCII APPB,其内部是两层 DPAPI 包装:外层在 SYSTEM(或服务自身)上下文加密,内层在用户上下文加密。
此外服务还会校验调用进程的可执行文件路径是否与加密时规范化后的安装路径一致。
因此 v20 的离线解密并不具备通用性,成功率取决于浏览器版本、安装路径、系统状态与工具实现,不应向委托人承诺「一定解得开」。
2.4 DPAPI 主密钥与用户配置文件的绑定
CryptUnprotectData 成功的前提不是「知道密码」,而是能在正确的用户上下文里拿到主密钥并解开被 DPAPI 加密的用户密钥。
主密钥文件位于:
C:\Users\<user>\AppData\Roaming\Microsoft\Protect\<SID>\
C:\Windows\System32\Microsoft\Protect\<SID>\
每个用户 SID 一套,主密钥的有效性受登录口令、域凭据、机器身份、系统时间、策略共同约束。
由此产生两个必须写进报告的判断:
- DPAPI 解密失败通常表示用户配置文件、机器、口令状态或策略不匹配,不表示字段为空或未加密。
Login Data 中 password_value 为非空 BLOB 而解不开,正确表述是「存在密文,密钥恢复失败」。
- 用户没登录过就解不开。未交互登录过的账户只有 STATIC 域密钥,主密钥不完整,离线还原难度显著上升。
主密钥还需按 DPAPI 版本(v1 / v2)派生用户密钥,两者算法与迭代次数不同,由系统与域策略决定,具体以实际检材为准。
2.5 AES-256-GCM 报文结构与 tag 语义
v10 密文的物理结构是:
| 偏移 |
长度 |
内容 |
| 0 |
3 |
固定前缀 v10 |
| 3 |
12 |
GCM nonce |
| 15 |
可变 |
密文 |
| 末尾 |
16 |
GCM 认证 tag |
判定要点:
- tag 校验失败通常意味着密钥、nonce、密文或附加认证数据之一不正确,不表示该数据没有被加密。它是「参数错了」的信号,不是「格式判断错了」的信号。
- 去掉前缀后的最小有意义长度为
12 + 0 + 16 = 28 字节,短于此的字段不是标准 v10 结构。
- 部分较新版本会在明文前加 32 字节随机填充,解析结果需要按实际检材验证后再裁剪,不能默认首字节即有效字符。
2.6 Firefox 与 IE / Edge Legacy
Firefox 的 places.sqlite 保存访问历史(moz_places、moz_historyvisits、moz_bookmarks),cookies.sqlite 的 value 列通常为明文,这是它相对 Chromium 的最大便利。
logins.json 中的 encryptedUsername / encryptedPassword 是 Base64 的 ASN.1 派生结果,真正的主密钥材料在 key4.db(BerkeleyDB,含全局盐与校验值)中。
logins.json 与 key4.db 必须配套,缺一不可判定。旧版本 Firefox 还可能存在 signons.sqlite 与 key3.db。
注意 Firefox 的 moz_historyvisits.visit_date 是 Unix 纪元起的微秒数,而 Chromium 的对应字段是 1601-01-01 起的微秒数。两者在时间线中混用会得到 1601 年附近的荒谬时间,这是合并两族浏览器排序时最典型的错误。
IE 的历史在 Windows 7 及以后集中在 %LOCALAPPDATA%\Microsoft\Windows\History\index.dat(早期版本在 History.IE5\ 目录);IE10 与 Edge Legacy 还需检查 %LOCALAPPDATA%\Microsoft\Windows\WebCache\WebCacheV01.dat,其中包含缓存、Cookie 与下载记录。
二者均为复合文档/自定义二进制结构,需要专门解析器。
三、操作步骤
3.1 定位、拷贝与只读挂载
先确定卷偏移(mmls / fsstat),再按目录模式批量抽取,注意 -p 输出中 inode 在第 2 列:
mmls /work/DigiForensics.dd
fsstat -o 2048 /work/DigiForensics.dd
fls -o 2048 -r -p /work/DigiForensics.dd \
| grep -Ei '/(Chrome|Edge|Brave-Browser)/User Data/.*(History|Login Data|Web Data|Cookies|Local State)$' \
| grep -E '^r/r' \
| awk '{print $2}' \
| while read -r ino; do
icat -o 2048 /work/DigiForensics.dd "$ino" > "/evidence/browser/$(printf '%s' "${ino%%:*}").bin"
done
SQLite 的 WAL 内容可能不在主库文件里,必须把 -wal 与 -shm 一起拷贝。
它们是独立的普通文件(不是主库的数据流),因此要用 fls 分别定位各自的 inode:
# 逐个定位主库与其 -wal / -shm 同伴文件
fls -o 2048 -r -p /work/DigiForensics.dd \
| grep -E 'User Data/Default/History(-wal|-shm)?$' | grep -E '^r/r'
# 逐个导出(文件名保留后缀,便于后续按同名组装)
fls -o 2048 -r -p /work/DigiForensics.dd \
| grep -E 'User Data/Default/History(-wal|-shm)?$' | grep -E '^r/r' | awk '{print $2}' \
| while read -r ino; do
icat -o 2048 /work/DigiForensics.dd "$ino" > "/evidence/browser/$(echo "$ino" | sed 's/.*-//')"
done
# 以 immutable 模式只读打开,sqlite 不会回写证据文件
sqlite3 "file:/evidence/browser/History.db?immutable=1" ".tables"
若检材中不存在 -wal/-shm(如已正常关闭或已 checkpoint),说明主库是自洽的;不要伪造这两个文件,也不要因为缺失就断定「有未落盘记录」。
3.2 构建 Chromium 访问时间线
WebKit 时间戳(1601 起微秒)到 UTC 的换算必须用 116444736000000000 这个偏移量:
SELECT
datetime((v.visit_time/1000000) - 11644473600, 'unixepoch') AS visit_utc,
u.url, u.title, v.visit_duration,
printf('0x%08X', v.transition & 255) AS transition
FROM visits v JOIN urls u ON u.id = v.url
ORDER BY v.visit_time;
把访问与搜索词、下载关联起来:
-- 搜索词
SELECT k.term, u.url,
datetime((v.visit_time/1000000) - 11644473600, 'unixepoch') AS visit_utc
FROM keyword_search_terms k
JOIN urls u ON u.id = k.url_id
JOIN visits v ON v.url = u.id
ORDER BY v.visit_time;
-- 下载及其来源链
SELECT d.target_path, d.total_bytes, d.tab_url, d.referrer,
datetime((d.start_time/1000000) - 11644473600, 'unixepoch') AS start_utc
FROM downloads d ORDER BY d.start_time;
3.3 提取凭据密文并判断可解性
SELECT origin_url, action_url, username_value,
length(password_value) AS pw_len,
hex(substr(password_value,1,3)) AS prefix,
date_created
FROM logins ORDER BY date_created;
prefix 为空表示旧式 DPAPI 直加密;763130 即 v10;763230 即 v20。
只要 pw_len 非零,就说明密码条目确实存在,后续能否还原是另一层问题,报告里两者必须分开写。
在原始用户上下文中解出 v10 主密钥: