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 加密密钥的存储策略
聊完加密方式,最关键的问题来了——密钥放哪儿?
这个问题我研究了很久。说白了,密钥存储就三个原则:
- 别放 Flash 里——Flash 太容易被读出来了
- 别硬编码——反编译一看就露馅
- 别用固定值——所有设备用同一个 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,那玩意儿早被破解得底裤都不剩了。