TL;DR. 首个 u32 = 头部大小(通常为 0x30–0x36)。在该偏移处,每条条目以四字节魔术 10ts 开头,随后是 12 字节的头部与可变长度的正文:UTF-16LE 路径(带长度前缀,不以空字符终止)、FILETIME mtime 和尾部数据块。执行标志在 Windows 10 中已被移除。
自 XP 起,Windows 的每一个版本都提供了不同的 ShimCache 布局。Windows 10 / 11 这一变体是今天调查人员最常遇到的,也是相对最好解析的 —— 前提是你知道规则。本文从头到尾走一遍这个格式。
你要解析的 Blob
起点是下列位置的二进制值:
HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\AppCompatCache\AppCompatCache
如果你已经从 SYSTEM 配置单元提取出了这个值(见 ShimCache 存储在哪里),你手里就是一个 blob,通常几十到几百 KB。其中包含一个小头部,后面是一系列条目,每条以已知的 magic 开头。
头部
blob 开头四字节(little-endian)给出 第一条条目的偏移。在 Windows 10 / 11 上,这通常是 0x30、0x34、0x35 或 0x36,依 build 而定。某些配置单元会写入 0x80,那是仍使用 10ts / 00ts 条目签名的 Windows 8.x 布局。
写得稳健一些的解析器不会把头部大小写死:先读首个 u32,再校验该偏移处的字节确实是已识别条目的开头。如果声明的偏移既没有 10ts 也没有 00ts,则向前扫描查找 10ts 并从那里继续 —— 这能容忍现实系统偶尔产生的略微不规范的配置单元。
一条 Windows 10 / 11 条目,逐字节
每条现代条目都以四字节签名 10ts(0x31 0x30 0x74 0x73)开头。完整布局如下:
| 偏移 | 大小 | 字段 |
|---|---|---|
| 0x00 | 4 | Magic —— "10ts" |
| 0x04 | 4 | 序号 / 未知(解析器通常忽略) |
| 0x08 | 4 | 条目数据大小 —— 此字段之后的字节数 |
| 0x0C | 2 | 路径大小(字节,UTF-16LE) |
| 0x0E | N | 路径字节(UTF-16LE,不含终止符) |
| 0x0E + N | 8 | 最后修改时间 —— FILETIME(从 1601-01-01 UTC 起的 100 ns 计数) |
| 0x16 + N | 4 | 尾部数据大小 |
| 0x1A + N | M | 尾部数据(通常为空或很小) |
整条条目长度因此是 0x0C + entry_data_size。下一条紧跟其后。
两点务必注意:
- 路径为 UTF-16LE,且 不以空字节结尾。请使用路径大小字段;不要读到 null。
FILETIME是文件的$STANDARD_INFORMATIONmtime,并非程序运行时刻。原因详见 关于执行证明的文章。
Windows 10 里被拿掉的东西
更早的 Windows 在每条条目里塞了一个 Executed 标志。在 Windows 10 / 11 中,这个标志被彻底移除。缓存内已没有任何方法告诉你二进制是否真的运行过 —— 这件事得交给 Prefetch、AmCache 或事件日志 去回答。
实际操作
一个健壮的解析器会:
- 在头部声明的偏移处校验四字节 magic。
- 通过读取条目数据大小并步进来遍历条目。
- 把路径作为 UTF-16LE 解码。
- 把
FILETIME转换为下游工具偏好的时间戳格式(Unix 毫秒就很方便)。 - 把操作系统格式标签(Windows 10/11、8.x、8.0)暴露出来供下游过滤。
如果想看一个工作中的参考实现,开源的 Shimcache Parser 正是用 Rust 实现了上述算法,并以 WebAssembly 形式送到你的浏览器 —— 没有服务器,没有上传。
参考实现
- Eric Zimmerman 的 AppCompatCacheParser —— 覆盖每一个 Windows 变体的 C# 黄金标准实现。
- Mandiant 的 ShimCacheParser —— 最早的 Python proof-of-concept,可作为格式交叉参考。
- Velociraptor
Windows.Registry.AppCompatCache—— 用于现场采集的 VQL 工件。 - Windows 10/11 AppCompatCache deep dive (Ø Security) —— 关于现代布局最完整的公开梳理。