消费电子固件加密编程:通过唯一芯片ID实现固件的防拷贝绑定
消费电子防拷贝最常被写成“读UID→比对→死循环”,这只能挡住小白烧录工。真量产环境里,固件.bin一旦被扒出来,逆向者搜UID基址、改跳转指令就能跳过校验。 正经做法是把UID当成“信任根因子”:不在代码里硬编码母片UID,而是让BootLoader用UID派生密钥、校验绑定值或解密App段,再叠RDP读保护。
一、UID绑定的正确位置:BootLoader
App里做if(uid!=xxx) while(1)最危险——校验分支就在固件明文里,特征好定位。 应该放在上电最先跑的BootLoader:
读芯片UID
拼salt做SHA256/AES派生
和Flash参数区存的binding值比对
通过才解密App或跳转
不通过进安全模式(限功能/清关键数据)
以STM32为例UID在0x1FFFF7E8(96bit),读出来别直接比字符串。
二、绑定值别存明文UID
出厂烧录时由上位机读当前片UID,算绑定值写进参数扇区:
#define UID_LEN 12
#define BIND_ADDR 0x0801F000 // 独立参数扇区
static const uint8_t SALT[16] = {
0x9A,0x23,0xC7,0x04,0x5E,0x88,0x1B,0xD2,
0x66,0x3F,0x90,0xAB,0x12,0x77,0xE5,0x40
};
/* 简化示意:真实项目用SHA256/AES,别用xor+srand */
void derive_bindkey(const uint8_t*uid, uint8_t*out){
for(int i=0;i<UID_LEN;i++){
out[i] = uid[i] ^ SALT[i] ^ SALT[i+4];
out[i] = (out[i]<<3) | (out[i]>>5); // 轮换
}
}
int check_uid_binding(void){
uint8_t uid[UID_LEN], k[UID_LEN], store[UID_LEN];
read_chip_uid(uid);
derive_bindkey(uid, k);
flash_read(BIND_ADDR, store, UID_LEN);
volatile uint8_t diff = 0;
for(int i=0;i<UID_LEN;i++)
diff |= k[i]^store[i]; // 防时序短路
return diff==0;
}
要点:
绑定值存在写保护扇区(WRP)
校验用异或累加密文比较,别写if(k!=store) return 0让逆向好改
派生算法别用srand/rand,序列可复现
Release版本关掉printf调试残留
三、更进一步:UID派生解密密钥
只校验UID,攻击者把校验跳了仍能跑。把UID融进“解密密钥”才更难绕:
/* BootLoader解密App前,先由UID参与派生根密钥 */
void build_dev_key(const uint8_t*uid, uint8_t*key128){
uint8_t seed[32];
memcpy(seed, SALT, 16);
memcpy(seed+16, uid, UID_LEN);
sha256(seed, 32, key128); // 前16字节当AES-128密钥
}
PC端加密固件时也用同样UID绑定值(产线脚本逐片算),烧进去的固件镜像就是“一芯一密”。 换到别片UID不对→解密出来是垃圾指令→App起不来。
四、安全启动闭环
推荐三级:
BootROM/ROM ─▶ BootLoader ─▶ App
(芯片固化) (验UID+解密) (业务)
BootLoader里顺序:
开RDP Level1(最终品再考虑Level2)
读UID→派生key
从Flash读密文App到SRAM
AES-CBC解密
SHA256校验完整性
UID binding比对
全部过→跳转;失败→擦参数/进安全模式
void bootloader_main(void){
rdp_check();
uint8_t uid[UID_LEN];
read_chip_uid(uid);
uint8_t key[16];
build_dev_key(uid, key);
aes_load_key(key);
aes_decrypt_app(); // 密文区→明文SRAM
if(sha256_verify() && check_uid_binding()){
jump_to_app();
}else{
enter_safe_mode(); // 限功能,不一定要死循环
}
}
ESP32类带硬件解密引擎的,密钥放eFuse,CPU执行前自动解密,固件Dump出来也是密文。
五、量产烧录流程
一份固件适配万片,不能手工改UID:
空白片先烧BootLoader(不含UID逻辑差异)
上位机连ST-Link/烧录器读UID
脚本算binding值+按UID加密App镜像
写参数扇区+写App密文
设RDP1、WRP锁参数区
断开调试口
这样同份源码出不同片固件,拷贝工只能复制出“绑定原片”的镜像。
六、防拷贝的天花板
UID绑定不是银弹,明确防不住:
读保护没开→Flash被dump→逆向算法
电压毛刺/故障注入跳过校验
把校验函数整体NOP掉重烧
高端芯片用PUF/OTP密钥才更抗
价值在“提高抄袭成本”:抄板客要逆向算法+改二进制+逐片算key,不如自己写。
七、落坑清单
App里明文字符串比UID→特征好搜
硬编码母片UID进固件→换片就废但易改跳转
只用xor不哈希→逆向还原算法
校验失败纯死循环→好定位好绕过
没开RDP→Flash直接读
绑定值放可写区→被覆盖
srand/rand做密钥派生→可复现
Release留printf UID→串口泄中间值
BootLoader不验签名→密文被换
八、落地顺序
BootLoader独立工程,先跑通跳转App
读UID+派生binding值,参数扇区存绑定
明文段改密文:PC AES加密、BootLoader解密
SHA256完整性校验
UID校验失败进安全模式(限功能/清校准)
开RDP1+WRP
产线脚本按UID逐片烧
高安全场景换eFuse/PUF+签名验签
UID防拷贝的本质不是“固件锁死某颗芯片”这么简单,是把UID变成密钥派生因子,让拷贝出去的bin在新片上解不出正确代码。明文比对只是入门玩具;BootLoader里做UID→key→解密→校验闭环,再配RDP和写保护,才算消费电子量产级。别承诺“不可破解”,靠谱说法是“抄袭成本高于重写成本”——这才是UID方案的真实工程价值。





