0x9a.com

保持简单,保持好奇

二进制与熵:从 01 混沌中看见信息的本质

2026-01-22

在《黑客帝国》里,Neo 看到了世界的本质——绿色的代码雨。但在现实的计算机科学中,如果我们剥去文件的扩展名,直接去读硬盘上的 0 和 1,我们会看到什么?

是整齐排列的士兵,还是无序的混沌?

今天我们要聊一个信息论中的核心概念:熵 (Entropy)。它不仅能帮我们理解压缩软件(如 ZIP 和 7z)是如何工作的,甚至能让我们在不打开文件的情况下,一眼识别出它是文本、图片,还是经过压缩或加壳的程序。

一、 什么是“熵”?

“熵”这个词最早来自热力学,用来描述系统的混乱程度。而在信息论中,香农(Claude Shannon)用它来度量信息的不确定性

简单来说:

在计算机中,我们通常以 比特每字节 (bits per byte) 来衡量熵。
一个字节有 8 位 ($2^8=256$ 种可能)。

二、 文本的浪费与压缩的魔法

为什么我们需要压缩文件?本质上是因为大多数人类产生的数据,熵都太低了。

纯文本的熵

以一段英文小说为例。虽然它由 ASCII 码组成(0-255),但你绝不会看到乱码。你会频繁看到 eta空格,但很难看到不可见字符。

统计规律意味着文本充满了“冗余”:

一段标准的英文文本,其熵通常只有 3.5 ~ 4.5 bits/byte 左右。

这意味着,虽然我们用 8 个比特存一个字母,但其中有一半的空间其实是“可以被预测”的,也就是被浪费了。

压缩的作用:榨干冗余

压缩软件(ZIP, 7z, RAR)的工作原理,本质上就是一套寻找并消除冗余的组合拳。它主要依靠两大流派的协作:

  1. 字典编码 (以 LZ 系列算法为代表)
    像 LZ77 或 LZMA 算法,它们在文件里维护一个“滑动窗口”。当它发现后面出现了一段以前出现过的内容(比如一个长单词或重复的网页模板),它不会重复存储这段内容,而是存一个“指路牌”:“去前面第 X 字节处,拷贝 Y 个字节回来”。这消除了结构上的重复
  2. 统计编码 (以哈夫曼编码 Huffman 为代表)
    这是基于概率的魔法。它会统计每个符号出现的频率,给出现频率高的(如字母 e)分配极短的比特序列,给冷门的分配长的序列。这消除了概率上的冗余

压缩的结果,就是熵的增加。
根据香农的第一信源编码定理,平均码长可以无限接近信源的熵。当你把所有的“规律”和“冗余”全部剔除,并用最优的长度重新编码后,剩下的数据看起来就像是纯粹的随机噪声。

一个完美的压缩算法,应该能把文件的熵提升到无限接近 8.0 bits/byte。这时候,文件中再也没有任何可预测的模式——因为所有的规律都被提取出来变成了“压缩字典”或“频率表”。

为什么不能“无限压缩”?

很多人都有过一个大胆的想法:如果我把一个 1GB 的电影压缩成 500MB,再把这 500MB 压缩成 250MB……反复套娃,最后是不是能把它缩到一个字节?

遗憾的是,这在逻辑上是行不通的:

  1. 熵的饱和:正如前文所述,压缩的本质是“脱水”(去除冗余)。一旦文件的熵达到了 8.0 bits/byte 左右,它就已经是纯净的“信息干货”了,没有规律可循。对一个已经是随机噪声的文件再次压缩,算法找不到任何重复模式,结果往往不仅不会变小,反而会因为增加了压缩包的文件头而变大
  2. 鸽巢原理 (Pigeonhole Principle):从数学上看,如果你想把所有可能的 100 字节文件都无损压缩到 99 字节,这是不可能的。因为 100 字节的文件有 $2^{800}$ 种,而 99 字节的文件只有 $2^{792}$ 种。要把大集合塞进小集合,必然会导致两个不同的原文件对应同一个压缩文件,从而无法还原。

三、 动手实战:计算文件的熵

我们不需要复杂的工具,用 Python 就可以轻松计算任意文件的熵。

$$ H(X) = - \sum_{i=1}^{n} p(x_i) \log_2 p(x_i) $$

import math
import sys

def calculate_entropy(file_path):
    with open(file_path, 'rb') as f:
        data = f.read()

    if not data:
        return 0.0

    # 统计每个字节出现的频率
    frequency = [0] * 256
    for byte in data:
        frequency[byte] += 1

    file_length = len(data)
    entropy = 0.0

    for count in frequency:
        if count == 0:
            continue
        # 计算概率 p(x)
        p_x = count / file_length
        # 套用香农公式
        entropy -= p_x * math.log2(p_x)

    return entropy

# 随便找个文件试试
print(f"Entropy: {calculate_entropy('test.txt'):.4f} bits/byte")

注意,熵计算对小文件不准(<1KB 时统计偏差大)

四、 频率分析:直方图里的破绽

如果说熵是衡量文件的“混乱总量”,那么直方图(Histogram)就是观察混乱的“精细结构”。

直方图统计了 0 到 255 这 256 种字节在文件中各自出现了多少次。即使两个文件的熵很接近,它们的直方图也可能完全不同。

1. 寻找“沉默”的 0x00

在绝大多数未经处理的二进制文件(如 .exe, .elf, .dat)中,0x00 (Null Byte) 往往是出现频次最高的字节。

如果你看到一个文件的直方图在 0 处有一个高耸入云的尖峰,这通常意味着这是一个“松散”的、未经压缩的原始二进制文件。

2. XOR 的“自报家门”

这是逆向工程中最基础也最有趣的技巧。

假设一个黑客用一个固定字节(比如 0xAA)对一个二进制文件进行了全文件异或(XOR)加密。这时候,文件的熵会显著升高,肉眼看起来全是乱码。

但只要你画出它的直方图,你就会发现:原本属于 0x00 的那个“最高峰”,现在平移到了 0xAA 的位置。

原理极其简单
$$ 0x00 \oplus Key = Key $$
因为原始文件中 0x00 最多,那么异或后的文件中 Key 就会最多。你只需要看直方图里哪个字节出现的次数最夸张,那个字节极大概率就是加密密钥。

类似的,如果是纯英文文本文件(如 .txt, .json, .xml),原本出现最多的可能是空格 (0x20)。加密后,你只需要找直方图的峰值,然后将其与 0x20 做异或,就能瞬间推导出密钥。

五、 案例分析:从 ZIP 到 EXE 加壳

现在我们来做几个有趣的对比实验,看看不同文件的“含金量”。

1. ZIP vs 7z:谁更极致?

我有一个 10MB 的日志文件(纯文本,大量重复内容)。我们分别用 ZIP(Deflate 算法)和 7z(LZMA 算法)对其进行压缩。

解读:虽然肉眼看不出区别,但 7z 的熵更接近完美的 8.0,说明 LZMA 算法比 Deflate 更彻底地榨干了数据中的冗余。这也是为什么 7z 通常比 ZIP 压缩率高的原因。

2. 黑客侦探:识别加壳 EXE

这是逆向分析中非常经典的一招。给你两个 EXE 程序,如何不运行就判断哪个可能有问题?

结论:如果你扫描到一个 EXE 文件,它的熵 ≥ 7.8–7.9 通常是强信号,提示很可能经过压缩、加壳或加密处理。它可能是一个小巧的 UPX 自解压工具,也可能是一个被保护的商业软件。

六、 推荐工具

如果你想深入探索二进制文件的“本质”,以下工具是你的绝佳助手:

尾声

在这个数字世界里,如果你想隐藏秘密,最好的办法不是把它锁进保险箱,而是把它变成“随机的噪声”。

当我们凝视那些看似毫无意义的乱码时,请记住:混乱,有时候是最高级的秩序。

← 返回首页