关键词:魔数、magic number、文件签名、file 命令、xxd、binwalk、伪装文件、OOXML
难度:入门
前置知识:十六进制基础、字节序概念、文件恢复基础
相关文章:文件系统取证总览、文件雕刻与签名恢复
一、概述
用户和操作系统看到的是"这是一个 .jpg",但磁盘上真正决定它"是什么"的,是文件开头那几个字节。扩展名是文件名的属性,不是文件的属性。
这两个依据一旦分离,差异会直接变成结论差异:
| 场景 |
靠扩展名判断 |
靠文件头判断 |
| 检材是磁盘镜像 |
✗ 目录项里的名字可被任意伪造 |
✓ 头几字节是格式定义写死的 |
| 检材是 carving 产物 |
✗ 工具按签名分配扩展名 |
✓ 同一依据,循环论证 |
| 可疑文件定性 |
✗ 发票.pdf 实际是 PE |
✓ 头不同 = 结论不同 |
| 篡改后缀伪装 |
✗ 完全失效 |
✓ 可被识破 |
所以要处理的就是这个分离带来的判定问题:怎么读头、怎么交叉比对、怎么把"看起来是什么"变成"结构上是什么"。扩展名可以撒谎,魔数不会。
这件事落在分析环节的类型判定阶段,几乎是每个案件都会经过的通用动作,在流程里承担两个不同角色:
- 入口判据:从磁盘里提取出来的字节(
icat 结果、carving 产物、附件)首先需要类型判定,否则后续所有内容分析都用错工具
- 出口校验:认定某个文件"是什么"之后,还要反向验证它"没被改动过"——头尾成对、内部校验和自洽才是强依据
上游是提取动作(拿到字节)与文件系统元数据(拿到大小与时间戳);下游是内容解读、隐写与容器结构检查、以及元数据与内容的时间一致性比对。第五章案例里"文件系统 Created 早于文档内部创建时间 2 分 44 秒"这种发现,只有在类型判定正确之后才可能被发现。
落到具体证据上:
- 魔数(magic number) —— 格式开头的固定字节。三种形态:可打印字符串(
PK\x03\x04、%PDF、GIF89a)、固定十六进制(PNG \x89PNG\r\ \x1a\ 、ELF \x7fELF)、定长数值 + 二次跳转(PE 的 MZ + 偏移 0x3C 处的 PE\0\0,头不连续)
- 结束标记 —— JPEG 的
FFD9(EOI)、PNG 的 IEND、GIF 的 0x3B、PDF 的 %%EOF。头尾成对是结构完整性的最低门槛
- 内部校验和 —— ZIP 与 GZIP 的 CRC32、OOXML 的关系文件。
unzip -t 全通过比"能打开"强得多
- EOCD 与中央目录 —— ZIP 尾部的目录记录。中央目录偏移 + 大小 + 22 字节是否等于文件实际大小,是判断"尾部被追加数据"的关键算式
- 容器与成员的层级 ——
.docx / .xlsx / .pptx 实际是 ZIP,内含 [Content_Types].xml、_rels/.rels、xl/workbook.xml。这三个条目同时存在才是真 OOXML,仅有 PK 头只能证明是 ZIP
- 字节序标记 —— PE 的 PE32/PE32+、ELF 的 EI_CLASS、Mach-O 的 magic。同为 32 位/64 位可执行文件,字节序不同会让所有偏移解释反过来
- 声明尺寸与实际尺寸的差 —— BMP 头声明宽高小于文件实际大小、图片元数据声明尺寸与像素数据不匹配。这类"结构自相矛盾"是隐写与截断的常见指示
file / file -b --mime-type / file -r 的输出 —— 类型判定工具输出。注意 --mime-type 常给通用类型(application/zip),不是工具失灵而是它确实区分不了
xxd / od 的十六进制首 64 字节 —— 人工复核的最终依据
DigiForensics.dd 中提取出的可疑文件 —— 这里的输入。必须先固定哈希再看头,否则分析的不是证据而是副本
走完这一层能回答的是这些:这个文件在结构上是什么格式、它的头尾与内部校验是否自洽、扩展名与实际类型是否一致、以及某个容器里装了哪些成员。
回答不了的几件事要说清:
- 文件的来源与作者 —— 文档元数据里的作者字段可由生成方任意填写。
core.xml 的 dc:creator 不是身份证据
- 内容是否被改过 —— 校验和通过只证明"与写入时一致",不证明"写入时就是原始内容"。判定篡改要靠多源时间比对
- 是否存在任何隐藏内容 —— 签名扫描扫的是"已知形状",检测不到加密、变形、刻意规避签名的载荷。polyglot 文件就是签名扫描的盲区
- carving 产物的有效性 —— 头尾齐全的碎片和真实文件在签名层面无法区分。清洗与功能验证在 文件雕刻与签名恢复
- 文件的具体内容语义 —— 类型对了不等于内容可读(还可能是加密的,见 EFS 文件系统加密取证)
前提只有两条:会看十六进制、知道字节序这个概念。不需要提前读 NTFS 结构与 MFT 解析——对文件系统只提出"用 icat 把文件提取出来"这一个要求。但如果已经读了那篇,会更清楚"声明长度"与"簇大小"的差值如何影响提取结果,那是 NTFS 数据流与隐藏数据 里 slack 话题的入口。
各类格式的魔数与结构约束在核心原理,file / xxd / binwalk 的组合用法与验证流程在操作步骤。
最容易被误用的地方是把工具输出直接当结论,常见陷阱的 4.2 与 4.3 组专门处理这件事——application/zip 不是失灵、CRC 全通过不等于没被篡改。
走一遍完整判定流程见案例示例,常用签名的十六进制速查见命令速查。
二、核心原理
2.1 魔数:格式为什么要写死自己的名字
魔数(magic number) 是文件开头的固定字节序列,等同于格式的"身份证"。它存在的理由很朴素:读取方必须在读任何内容之前就知道这是什么格式,而文件名和扩展名都可以被改。魔数有三种形态:可打印字符串(PK\x03\x04、%PDF、GIF89a,直观易人工识别)、固定十六进制(PNG \x89PNG\r\ \x1a\ 、ELF \x7fELF,有时含不可打印字节)、定长数值 + 偏移(PE 的 MZ + 0x3C 处的 PE\0\0,头不是连续的,需二次跳转)。
一个必须知道的现实:魔数不是标准,是各格式的"事实约定"。 因此同一个字节序列可能被多种格式占用(见 2.4);有些格式根本没有魔数(纯文本、CSV、某些日志);有些格式的魔数在文件中间或尾部(PDF 的 %%EOF、ZIP 的中央目录与 EOCD);必须用工具判断,不要凭记忆。
而 file 报 "data" 时也不是"无价值",只是需要人工介入。
2.2 常用文件签名对照表
偏移列是相对文件开头的字节偏移;尾部标记另见 2.3。
| 格式 |
偏移 |
魔数(十六进制) |
格式 |
偏移 |
魔数(十六进制) |
| PNG |
0 |
89 50 4E 47 0D 0A 1A 0A |
OLE2 (Office 97-03) |
0 |
D0 CF 11 E0 A1 B1 1A E1 |
| JPEG |
0 |
FF D8 FF(第 3 字节随版本变) |
SQLite 3 |
0 |
53 51 4C 69 74 65 20 66 6F…33 00 |
| GIF87a / 89a |
0 |
47 49 46 38 37 61 / …38 39 61 |
Windows LNK |
0 |
4C 00 00 00(偏移 4 起是 GUID) |
| PDF |
0 |
25 50 44 46 2D |
PCAP / PCAPNG |
0 |
D4 C3 B2 A1(含字节序)/ 0A 0D 0D 0A |
| ZIP |
0 |
50 4B 03 04 |
RTF |
0 |
7B 5C 72 74 66 31({\rtf1) |
| RAR 4.x / 5.x |
0 |
52 61 72 21 1A 07 00 / …07 01 00 |
RIFF (AVI/WAV) |
0 |
52 49 46 46(偏移 8 是类型) |
| 7-Zip / GZIP |
0 |
37 7A BC AF 27 1C / 1F 8B 08 |
MP4/MOV (ftyp) |
4 |
66 74 79 70(moov/mdat 原子) |
| BZIP2 / XZ |
0 |
42 5A 68 / FD 37 7A 58 5A 00 |
PE/EXE (DOS 头) |
0 |
4D 5A |
| ELF |
0 |
7F 45 4C 46 |
PE/EXE (PE 头) |
0x3C 处指向 |
50 45 00 00(Mach-O 见 2.5) |
两个必须记住的例外:%PDF- 前面可能有一段垃圾字节(某些生成器会在文件前加 BOM 或空白),严格解析时应允许在前 1024 字节内搜索 %PDF,这也是 carving 工具对 PDF 采用宽松匹配的原因。PE 则是"两头"结构:MZ 在偏移 0,真正的 PE\0\0 在偏移 0x3C 指向的位置——只看 MZ 会把任何 DOS 可执行文件(含许多老程序)误判为现代 PE,解析器必须做二次跳转验证。
2.3 头尾成对:结构完整性的验证
只认头不认尾,等于只验证了一半。 常用配对:PNG 89 PNG… ↔ IEND….;JPEG FFD8FF ↔ FFD9;GIF GIF89a ↔ 003B;PDF %PDF ↔ %%EOF(可能多个);ZIP PK\x03\x04 ↔ PK\x05\x06(EOCD 存在且中央目录偏移正确才算可列目录);GZIP 1F8B08 ↔ 末 8 字节的 CRC32 + ISIZE。
但最严格的验证是"功能验证",不是"标记验证"——pngcheck 会走完全部 chunk CRC 校验,pdfinfo 能解析出页数,identify 能解码出尺寸。
头尾标记齐全但内部 CRC 失败的文件,必须在报告里记为"内容损坏":它在证据上是"存在但不可用",与"不存在"是完全不同的结论。GZIP 则相反——它的尾部 CRC32 + ISIZE 就是功能校验,能完整解压即证明通过。
2.4 ZIP 家族:一个魔数,多种格式
这是最容易误判的一类,因为 Office 2007+、OpenDocument、Java JAR、APK、EPub、Python wheel 全都是 ZIP。判定方法只有一个:列出内部条目,看目录结构。
它们魔数全是 PK 03 04,只能靠内部条目区分:OOXML 有 [Content_Types].xml + word/ xl/ ppt/;OpenDocument 有 mimetype(必须位于偏移 0);JAR/APK 有 META-INF/MANIFEST.MF / AndroidManifest.xml;EPUB 与 Python wheel 则是 mimetype+META-INF / *.dist-info/。
这些路径是规范强制的——一个真正的 docx 必然同时具备 [Content_Types].xml 与 word/document.xml,xlsx 则是 [Content_Types].xml + xl/workbook.xml;手工改扩展名造出的假 docx 内部不会有这些结构,unzip -l 一眼识破。
为什么"改扩展名"骗不过 file:.docx 与任意 ZIP 的头部字节完全相同,--mime-type 只会给出通用的 application/zip 而非 application/vnd.openxmlformats-officedocument.spreadsheetml.sheet。这是正常的,不是工具失灵——必须进容器内部才能定案。
OOXML 的三个取证优势:① 内容是明文 XML,可直接全文搜索(unzip -p report.docx word/document.xml,在 OLE2 的 .doc 上这样做无效);② 内部条目名与时间是独立元数据来源(unzip -l,但条目时间是写入时的本地时间,可被改,只能作旁证);③ 宏与外链可精确定位(vbaProject.bin、.rels 里的 Target)。
局限:XML 压缩存储,strings 未必命中,必须先 unzip -p 取出再搜;批注在 word/comments.xml,数字签名在 _xmlsignatures/。
ZIP 的结构性事实与一个取证要点:EOCD 记录中央目录的偏移与大小,中央目录又记录每个条目数据区的绝对偏移。"尾部追加数据"因此可被精确检出,见 3.2 的 zipcheck.py。
注意 zip -T 只做 CRC 测试、不报告尾随数据,报告尾随数据的是 unzip 的 extra bytes at beginning or within zipfile 告警。ZIP 格式本身允许在归档前后附加任意数据而不影响解压,所以"能正常打开"绝不等于"未被改过"。
2.5 可执行文件头:PE / ELF / Mach-O 的字节序陷阱
三类可执行格式是识别恶意样本、判断跨平台文件的基础,但字节序是最大的误判来源:
PE(Windows): [0x00] 4D 5A "MZ"
[0x3C] PE 偏移量(4字节, LE) → 该处必须是 50 45 00 00 "PE\0\0"
ELF(Linux): 7F 45 4C 46 "\x7fELF",第 5 字节 EI_CLASS 01=32位 02=64位
第 6 字节 EI_DATA 01=LE 02=BE ← 字节序标记
Mach-O 的四个变体,取决于"看到的字节"和"按什么顺序解释成整数":
| 磁盘上的前 4 字节 |
小端解释为 |
常量 |
含义 |
CE FA ED FE |
0xFEEDFACE |
MH_MAGIC |
32 位,本机序小端 |
FE ED FA CE |
0xCEFAEDFE |
MH_CIGAM |
32 位,本机序大端(字节序需交换) |
CF FA ED FE |
0xFEEDFACF |
MH_MAGIC_64 |
64 位,本机序小端 |
FE ED FA CF |
0xCFFAEDFE |
MH_CIGAM_64 |
64 位,本机序大端 |
为什么这张表反直觉:Mach-O 的魔数是一个按宿主字节序解释的整数。CF FA ED FE 按小端解释得 0xFEEDFACF,落在 MH_MAGIC_64 上,所以是 64 位小端;而看起来更整齐的 FE ED FA CF 解释出来是 MH_CIGAM_64,反而是大端。记不住就查表,不要凭字节排列的直觉判断。
另一个歧义是 CA FE BA BE,它同时是 Mach-O 通用二进制(fat)和 Java class 文件的头,区分靠偏移 4 之后:Java 存放大端版本号(00 00 00 34 = 52 → Java 8),fat 二进制则是架构描述符个数等结构,遇到它不能直接下结论。
file 是首选,它会直接给出架构;只有当 file 不可用或需要取证级精确时,才手工做 PE 的二次跳转。
三、操作步骤
3.1 第 1 步:查看文件头,用 file 判定并交叉比对
xxd -l 64 /evidence/mystery.bin
# 00000000: 8950 4e47 0d0a 1a0a 0000 000d 4948 4452 .PNG..........IHR
# 00000010: 0000 01f4 0000 03e8 08 02 00 00 00 8d 0a24 64 .......h..¤$d
xxd -s $((0x3C)) -l 4 /evidence/prog.exe # 先读 PE 的 e_lfanew
xxd -s $((0x80)) -l 64 /evidence/prog.exe # 再跳到 PE 头验证 50 45 00 00
xxd -l 64 -p /evidence/mystery.bin # 纯十六进制,无 ASCII 栏
file /evidence/mystery.bin # PNG image data, 1920 x 1080, 8-bit/color RGB, non-interlaced
file -b --mime-type /evidence/mystery.bin # 简要类型 / MIME(-i 会连 charset 一起给)
# 批量分类:carving 产物
file -r --mime-type /evidence/carved/ | cut -d: -f2- | sort | uniq -c | sort -rn | head
# 18432 image/jpeg 3210 image/png 847 application/zip ← 需再区分 docx/xlsx/jar
# 与扩展名交叉比对,找出"不符"的文件(核心步骤,见 3.3)
cd /evidence/carved && for f in *; do
ext="${f##*.}"; ft=$(file -b --mime-type "$f")
case "$ext:$ft" in
jpg:image/jpeg|png:image/png|pdf:application/pdf) ;;
jpg:*|png:*|pdf:*|docx:*|xlsx:*) echo "不符: $f 扩展名=$ext 实际=$ft" ;;
esac
done > /evidence/mismatch.txt
读十六进制的三个技巧:① 看前 8–16 字节,绝大多数格式的魔数都在这里(见 2.2);② 找 00 密集区——00 00 00 00 常是版本号或填充,连续 00 之后突然出现版本号,是"版本-时间"式结构的典型;③ 找不可打印字节——大量 FF、00、8D、9C 混杂 = 压缩或加密数据**,可打印字符连续则是文本。
3.2 第 2 步补:验证完整性(头尾成对 + 功能验证 + 尾随数据)