你在分析一份二进制文件(固件、可执行文件、归档包),写了个脚本扫描里面的特征字节串。跑完出来的结果让你心跳加速:某个模式出现了 474 次。数字这么整齐、次数这么多,看着像个重大发现。但冷静下来一看,全是假的。
现象
- 脚本在二进制里扫出大量重复的短字节串,比如
AAAA出现 474 次; - 也可能是一段有规律的模板片段,反复出现几百次;
- 数量之整齐、之巨大,第一眼像是"发现了某种魔数"或者"找到了一个反复出现的结构";
- 但拿其中几处去人工核对,发现上下文完全对不上,根本不是什么结构标记。
根因
根因是:你扫到的不是二进制结构,而是 base64 编码的媒体数据。
关键在于 base64 的编码方式。base64 用 64 个字符(A–Z、a–z、0–9、+、/)来表示任意二进制数据,每 3 个字节编码成 4 个字符。这就带来一个特点:连续的零字节会被编码成连续的 A。
举几个具体的例子:
- 三个零字节
00 00 00→ base64 是AAAA; - 一段较长的零填充区,编码出来就是很长的一串
A; - 而图片、字体、图标这类资源经常大段填充零(对齐填充、透明像素等),base64 之后就成了满屏的
AAAA...。
更直接的证据:PNG 文件的魔数是 89 50 4E 47 0D 0A 1A 0A,把它 base64 编码后,开头正是 iVBORw0KGgo 这一类固定前缀——而一段 PNG 数据块中间大量出现的 AAAAABJRU 之类的串,就是那些固定字节序列的 base64 形态。你扫到的 AAAA,大概率就是这么来的。
所以结论很清楚:这些是编码数据里的巧合重复,不是结构信号。
解决
扫二进制特征时,要有意识地排除编码/媒体数据的干扰:
1. 先建立"已知噪声"清单,扫之前就排除
- base64 特征:连续的 A/Z、结尾的 = 填充、base64 字母表里高位为 0 的模式
- 压缩数据:zlib/gzip 流(往往带着固定的头字节)
- 资源区:图片、字体、图标所在的段
2. 对命中的位置做上下文检查
- 打印命中点前后几十个字节
- 如果前后是明显的 base64 字符表内容(全是 [A-Za-z0-9+/])→ 大概率是编码数据
- 如果前后是零填充、或者能识别出图片/资源的头 → 排除
3. 用"结构性证据"而不是"出现次数"来确认发现
- 真的结构标记通常伴随着长度字段、类型字段、偏移字段
- 单纯的高频重复,几乎总能在编码数据里找到解释
一个更根本的做法:扫描前先"还原"数据。 如果怀疑某段是 base64,就把它解码出来再看内容;如果怀疑是压缩流,就先解压。在解码/解压之后的明文里再扫特征,误报会少得多——因为那时你面对的是真实的结构,而不是它的编码形态。
延伸与预防
这条坑的通用教训是:高频重复的字节串,先怀疑是编码数据而不是结构。
这是一个更一般的思维习惯:"数量惊人"本身不是证据,它更可能是"你没意识到数据被编码过"。
- 锚定在结构性证据上。 真正有意义的发现,通常能讲出"它为什么在这里"——它跟哪些字段相邻、它的长度字段是否自洽、它出现在哪个段里。只有"出现了 N 次"这一个事实,撑不起任何结论。
- 先问"这段数据是什么形态"。 任何二进制分析,第一件事都应该是"这段是什么":可执行代码、压缩数据、base64 编码的文本、还是结构化资源?搞清形态再谈特征,比一头扎进字节匹配有效得多。
- 注意编码的副作用。 base64 会把三个字节摊成四个字符,于是"重复的字节"变成"重复的字符";反过来,压缩会把高频模式压掉、低频模式放大,还会引入新的重复。凡是对"编码/压缩后的数据"做统计,得到的结果都要打一个折扣——你统计的对象不是原始数据。
- 可疑的"重大发现",先做一次人工核对。 从命中的几百处里随便抽三五处,打印上下文看一眼。如果抽出来的都和结论对不上,那就是方向错了。这个动作花不了一分钟,却能挡住最常见的"数量错觉"。