4、固件加密技术:固件整体加密、关键代码段加密、数据段加密、加密密钥的存储策略

加密这事儿,说白了就是给固件穿上一件防弹衣。我做了这么多年逆向分析,见过太多裸奔的固件被直接 dump 出来,反编译一下,算法逻辑一览无余。嗯,加密不是万能的,但不加密是万万不能的。

今天咱们聊聊固件加密的几种常见手法。我个人习惯把加密分成三个层次:整体加密、局部加密、数据加密。每个层次都有它的适用场景和坑。

4.1 固件整体加密

整体加密是最粗暴的方式。把整个固件镜像当成一个数据块,用对称算法(比如 AES-256)一把锁死。芯片上电后,BootROM 先解密,再跳转到明文区执行。

我在项目中遇到过一种情况:客户把整个 2MB 的固件用 AES 加密了,看起来很安全。结果呢?攻击者直接抓了 SPI Flash 的读时序,把解密后的明文从数据总线上捞走了。你想想看,加密算法再强,密钥在芯片内部被解密后,总线上的数据还是裸奔的。

核心要点:整体加密只能防离线分析,防不住在线攻击。如果芯片没有总线加密或内存加密机制,整体加密的意义就大打折扣。

整体加密的典型流程是这样的:

// 伪代码:整体加密流程
1. 编译生成原始固件 firmware.bin
2. 使用 AES-256-CBC 模式加密
   - 密钥:从芯片 OTP 中读取
   - IV:使用芯片唯一 ID 派生
3. 加密后的固件烧录到外部 Flash
4. 上电后 BootROM 执行:
   - 读取加密固件到内部 SRAM
   - 用 OTP 密钥解密
   - 校验 CRC 或签名
   - 跳转到解密后的入口地址

注意:整体加密的密钥不能硬编码在固件里!我曾经见过一个产品,密钥就写在固件头部偏移 0x100 的位置,加密了个寂寞。

4.2 关键代码段加密

整体加密太笨重了,而且影响启动速度。我更推荐的做法是——只加密关键代码段。

什么叫关键代码段?就是那些核心算法、许可证校验、防调试逻辑。这些代码通常只占固件总量的 5%-10%。加密它们,攻击者就算 dump 了整个固件,看到的也是一堆乱码。

我习惯的做法是:

  • 函数级加密:把关键函数单独加密,运行时按需解密到 RAM 中执行
  • 页面级加密:针对某些特定的代码页面,设置 MPU/MMU 权限,只有解密后才能访问
  • 动态解密:函数调用前解密,调用后立即擦除 RAM 中的明文

举个例子,我曾经帮客户设计过一个方案:

// 关键函数加密示例
// 加密前:license_check() 函数体是乱码
// 运行时动态解密

void execute_encrypted_func(uint32_t *encrypted_data, uint32_t len) {
    uint8_t key[16];
    uint8_t temp_buf[256];
    
    // 从安全存储中读取密钥
    read_secure_key(key);
    
    // 解密到临时缓冲区
    aes_decrypt(encrypted_data, temp_buf, len, key);
    
    // 将临时缓冲区标记为可执行
    mpu_set_region(temp_buf, len, EXECUTABLE);
    
    // 调用解密后的函数
    void (*func)(void) = (void (*)(void))temp_buf;
    func();
    
    // 执行完毕后立即擦除
    memset(temp_buf, 0, len);
    mpu_clear_region(temp_buf);
}

避坑指南:我曾经犯过一个错误——解密后的函数放在 RAM 中,但忘了清空。攻击者通过调试接口读 RAM,直接拿到了明文代码。记住:用完即焚,别留痕迹。

4.3 数据段加密

代码加密很重要,但数据段往往被忽视。你想想看,固件里的常量字符串、查找表、配置参数,这些信息泄露了也很要命。

数据段加密我一般分两类处理:

数据类型 加密方式 解密时机 注意事项
静态常量(字符串、魔数) XOR 或简单异或 编译时加密,运行时解密 别用固定 key,用地址或函数名做种子
查找表(S-Box、系数表) AES-128 加密 首次使用时解密并缓存 缓存区要加访问保护
配置参数(序列号、校准值) 带 MAC 的加密 每次读取时解密验证 防止篡改,要加完整性校验
运行时变量(密钥、中间结果) 内存加密(硬件支持) CPU 自动处理 没有硬件支持就别存敏感数据

我个人习惯在编译阶段就做数据加密。写个 Python 脚本,扫描 ELF 文件中的特定段,自动加密并替换。这样源码里看到的还是明文,编译出来的固件已经是加密的了。

# 编译时数据加密脚本片段
import struct
from Crypto.Cipher import AES

def encrypt_data_section(input_file, output_file, key):
    with open(input_file, 'rb') as f:
        data = f.read()
    
    # 找到 .rodata 段中标记为 ENCRYPT 的部分
    encrypted_data = bytearray(data)
    # ... 解析 ELF 头,定位加密段 ...
    
    cipher = AES.new(key, AES.MODE_CBC, iv=derive_iv())
    encrypted_block = cipher.encrypt(pad(target_data))
    
    # 替换原始数据
    encrypted_data[offset:offset+len(encrypted_block)] = encrypted_block
    
    with open(output_file, 'wb') as f:
        f.write(encrypted_data)

警告:数据段加密的密钥绝对不能和代码段加密用同一个!我曾经见过一个产品,所有加密都用同一个 128 位密钥,破解了一个就等于破解了全部。

4.4 加密密钥的存储策略

聊完加密方式,最关键的问题来了——密钥放哪儿?

这个问题我研究了很久。说白了,密钥存储就三个原则:

  1. 别放 Flash 里——Flash 太容易被读出来了
  2. 别硬编码——反编译一看就露馅
  3. 别用固定值——所有设备用同一个 key,一破全破

我推荐几种靠谱的存储方式:

  • OTP(一次性可编程存储器):芯片出厂时烧录,只能写一次,读受控。这是最常用的方式。
  • eFuse:类似 OTP,但可以按位烧录。适合存储根密钥。
  • PUF(物理不可克隆函数):利用芯片制造差异生成唯一密钥。我最近的项目就在用这个,效果不错。
  • 密钥派生:不存密钥本身,存种子。运行时用芯片唯一 ID + 种子派生密钥。

举个例子,密钥派生怎么做:

// 密钥派生示例
// 不存储最终密钥,只存储派生因子

#define KEY_DERIVATION_SALT 0xA5B6C7D8

void derive_device_key(uint8_t *output_key) {
    uint8_t chip_id[16];
    uint8_t seed[16];
    
    // 从 OTP 读取种子(每个芯片不同)
    read_otp(OTP_SEED_ADDR, seed, 16);
    
    // 读取芯片唯一 ID
    read_chip_unique_id(chip_id);
    
    // 使用 HMAC-SHA256 派生
    hmac_sha256(seed, 16, chip_id, 16, output_key);
    
    // 再用一次 XOR 混淆
    for (int i = 0; i < 16; i++) {
        output_key[i] ^= (KEY_DERIVATION_SALT >> (i * 8)) & 0xFF;
    }
}

我的经验:密钥存储最怕的就是「一把钥匙开所有锁」。每个设备用不同的密钥,就算被破解一个,也不会影响整个产品线。另外,密钥在运行时要用完即焚,别留在寄存器或栈里。

嗯,加密这事儿,说到底就是增加攻击者的成本。没有绝对的安全,只有相对的成本。你让攻击者花一个月破解一个设备,他可能就放弃了;但如果花一天就能破解,那你的加密就跟没做一样。

最后说一句:加密算法选 AES-256,别用 DES 或 3DES 了。我见过有人还在用 RC4,那玩意儿早被破解得底裤都不剩了。