EVTX 文件格式详解:chunk、模板与 BinXML 内部结构
从字节层面看一个 .evtx 文件如何布局 —— 文件头、64 KB 的 chunk、模板表,以及引用这些模板的 BinXML 记录流。
Windows 事件日志格式 —— .evtx —— 随 Windows Vista 引入,用以替代面向行的 .evt。它是一种二进制、追加写、按块组织的容器,设计上由单进程(EventLog 服务)写入,写满后轮转或封装。理解它的内部布局会让取证恢复场景 —— 部分文件、脏块、雕刻 —— 容易得多。
文件头
每个 .evtx 以 4 KB 的文件头开头(ElfFile\0\0 魔数、版本、chunk 数量、最旧/当前 chunk 索引,以及一个 CRC32)。每次文件轮转或某个 chunk 封装时,文件头会被原地改写,这让其中的 Dirty 和 Full 标志变成有用的信号:Dirty 置位的文件说明在主机崩溃或镜像在线采集时它处于打开状态。
文件头之后是一系列固定大小的 chunk。
Chunk(64 KB)
每个 chunk 恰好 64 KB,自带 512 字节的头部(ElfChnk\0 魔数、本 chunk 中首尾记录的 log record ID、文件内偏移、两个 CRC32 —— 一个用于头部,一个用于记录数据)。chunk 之间相互独立:你可以从未分配空间里雕出一个 chunk,不需要文件其它部分就能解析。这正是 EVTX 能从磁盘碎片中恢复的原因。
chunk 内部:
- 字符串表 —— 在本 chunk 内部去重的字符串,按偏移引用。
- 模板表 —— 本 chunk 中各条记录所用的 XML 模板,同样按偏移索引。
- 记录 —— BinXML 记录流,每条引用一个模板加上各自的替换值。
BinXML 与模板
EVTX 记录并不是以 XML 文本形式存储的,而是以 BinXML —— 一种 XML 文档的令牌化二进制表示 —— 存储。为了节省空间,结构骨架(元素名、属性名、树形结构)被抽取成一个 模板,在 chunk 的模板表里只存一份。每条记录只需说"使用模板 ID 5,带值 [alice, S-1-5-21-..., 3, 0xc000006a]"。
要从记录还原出 XML,解析器需要:
- 读取记录的令牌流。
- 在 chunk 的模板表里按 ID 查找模板。
- 把各条记录的值替换进模板里的占位符。
- 输出最终的 XML。
这就是为什么解析器(包括驱动本页面的 omerbenamram/evtx)必须按 chunk 维护本地上下文 —— 模板 ID 在文件范围内并不全局唯一。
已封装 vs 脏 chunk
当 EventLog 服务写完一个 chunk 并切到下一个时,它会计算并写入该 chunk 的 CRC32,并把 chunk 头部标记为 Full。干净的文件除最后一个外,每个 chunk 都应处于这种状态。
Dirty chunk —— 最后修改时间晚于文件头最后更新时间 —— 是活动的尾部。它通常仍可解析,但有些工具会拒绝读取,因为记录流可能在某个令牌中途截断。这一点对取证很关键:在写入过程中采集主机的人会看到尾部出现脏 chunk,你的解析器对这种 chunk 的行为必须事先掌握(是跳过、报错,还是尽可能恢复?)。
对解析的实际影响
- 被截断的
.evtx—— 从在线主机采集时很常见 —— 通常大部分仍可恢复,因为每个完整的 chunk 都是独立的。 - 从未分配空间雕出来的 chunk 可以套一个合成的文件头然后解析。
- 某个 chunk 解析失败并不意味着整个文件失败 —— 健壮的解析器会跳到下一个 chunk。
- chunk 的 CRC32 是篡改的检测点:被改过却没重新计算 CRC 的记录是可识别的。