单片机程序加密与破解实战详解:从理论到实践
前言
作为单片机开发者,我们经常会忽略一个重要问题:程序加密。一款辛苦开发的产品,如果被轻易破解复制,不仅会造成经济损失,还会削弱产品竞争力。本文将深入探讨单片机程序加密的重要性,详细分析UID加密原理,并通过一个STM32实例,直观展示如何破解一个带有加密的程序,以及如何加强程序安全性。
目录
为什么需要加密读保护机制及其局限性UID加密原理详解破解UID加密的常见方法实战演示:用记事本破解STM32程序
示例程序解析HEX文件结构分析定位与修改UID地址验证破解结果 防破解策略与最佳实践总结与思考
为什么需要加密
我之前在公司工作时,从未考虑过加密问题。完成开发后,直接通过编程器下载程序,连读保护都没有设置。直到遇见一位老板,他的"商业模式"是:
从市场上寻找销量高的产品 → 购买样品 → 破解程序 → 批量复制生产 → 低价竞争
这种经历让我深刻认识到:不做加密保护,等于把辛苦开发的产品拱手让人。
读保护机制及其局限性
读保护的基本概念
单片机读保护(Read Protection)是最基础的保护措施。开启后,可以防止通过标准调试接口(如JTAG、SWD)直接读取Flash内容。
// STM32 读保护设置示例代码
void EnableReadProtection(void)
{
FLASH_OBProgramInitTypeDef OBInit;
HAL_FLASH_Unlock();
HAL_FLASH_OB_Unlock();
// 设置读保护等级
OBInit.OptionType = OPTIONBYTE_RDP;
OBInit.RDPLevel = OB_RDP_LEVEL_1; // 读保护等级1
HAL_FLASHEx_OBProgram(&OBInit);
HAL_FLASH_OB_Launch();
HAL_FLASH_OB_Lock();
HAL_FLASH_Lock();
}
读保护的局限性
但只依靠读保护是远远不够的。对于专业破解者而言,他们有更多手段:
芯片开盖技术:直接物理接触芯片内部,绕过读保护侧信道攻击:通过分析功耗、电磁辐射等信息推测程序内容固件转储:通过特殊设备提取加密固件
目前淘宝上就有提供STM32F系列芯片开盖服务,价格约1000元左右,就能完整提取程序数据。这一事实告诉我们:仅依靠读保护是不够的。
UID加密原理详解
既然读保护可被绕过,我们需要更深层次的保护措施。这就用到了UID加密技术。
什么是UID?
UID(Unique Identifier)是大多数单片机都具有的唯一识别码,类似于"芯片身份证"。例如STM32F系列的UID是96位(12字节),分为低、中、高三个部分,每部分32位。
// STM32 UID读取示例
uint32_t UID_Low = *(uint32_t*)(0x1FFFF7E8); // 低32位
uint32_t UID_Mid = *(uint32_t*)(0x1FFFF7EC); // 中32位
uint32_t UID_High = *(uint32_t*)(0x1FFFF7F0); // 高32位
UID加密流程
UID加密的核心思路是:将程序与特定芯片绑定。其工作流程如下:
加密阶段:程序内置加密算法,计算芯片UID生成验证值K验证阶段:运行时读取当前芯片UID,与预存K值匹配验证结果处理:匹配成功则正常运行,失败则进入异常处理
这样,即便破解者提取了完整程序并复制到新芯片,由于新芯片UID与原芯片不同,验证必然失败,程序无法正常运行。
破解UID加密的常见方法
聪明的破解者往往能找到绕过UID验证的方法。最常见的是UID重定向攻击:
在程序中定位读取UID的指令(通常是固定地址)修改该指令,将读取地址重定向到自定义区域在自定义区域填入原始芯片的UID值程序运行时,读取的是伪造UID而非真实UID
这种攻击有效的前提是:程序中明文存在UID读取地址(如STM32的0x1FFFF7E8)。
实战演示:用记事本破解STM32程序
下面通过一个实例,演示如何破解一个使用UID加密的STM32程序。
示例程序解析
首先,我编写了一个简单的示例程序:
// 示例代码:带UID验证的LED闪烁程序
#include "stm32f10x.h"
// 母片UID值(预先读取)
#define MOTHER_UID_LOW 0x0672FF31 // 示例值,请替换为实际值
int main(void)
{
// 初始化LED GPIO
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOC, &GPIO_InitStructure);
// 读取当前芯片UID
uint32_t current_uid = *(uint32_t*)(0x1FFFF7E8);
// UID验证
if (current_uid != MOTHER_UID_LOW)
{
// 验证失败:LED快闪(200ms周期)
while(1)
{
GPIO_SetBits(GPIOC, GPIO_Pin_13);
for(volatile uint32_t i=0; i<100000; i++);
GPIO_ResetBits(GPIOC, GPIO_Pin_13);
for(volatile uint32_t i=0; i<100000; i++);
}
}
// 验证通过:LED慢闪(1s周期)
while(1)
{
GPIO_SetBits(GPIOC, GPIO_Pin_13);
for(volatile uint32_t i=0; i<500000; i++);
GPIO_ResetBits(GPIOC, GPIO_Pin_13);
for(volatile uint32_t i=0; i<500000; i++);
}
}
该程序通过比较当前芯片UID与预设的母片UID值来决定LED闪烁方式:
验证通过:LED慢闪(1秒1次)验证失败:LED快闪(200ms一次)
HEX文件结构分析
编译生成HEX文件后,我们需要了解HEX文件的基本结构。Intel HEX格式的每行包含以下部分:
:10 0020 00 4FF4FC552069B14BF0F3884301210170 44
↑ ↑ ↑ ↑ ↑
| | | | └── 校验和
| | | └── 数据(16字节)
| | └── 记录类型(00=数据记录)
| └── 地址(本行数据起始地址)
└── 数据长度(16字节,用16进制表示)
校验和计算方法:256减去前面所有字节之和的最低字节。例如:
0x10 + 0x00 + 0x20 + 0x00 + 数据各字节之和 = 0xBC
0x100 - 0xBC = 0x44(校验和)
定位与修改UID地址
破解的关键是找到程序中读取UID的指令,即访问0x1FFFF7E8地址的代码。由于STM32采用小端模式,我们需要搜索E8F7FFFF1F这个地址序列。
在HEX文件中搜索后,我们找到了这个地址:
:10 05B0 00 2068094600F0BDE8E8F7FFFF1F782346 A9
破解步骤:
修改UID读取地址:将E8F7FFFF1F替换为自定义地址,比如00010008
:10 05B0 00 2068094600F0BDE800010008782346 XX
在自定义地址存放母片UID:在程序未使用的区域(如0x08000100)放置母片UID值
:04 0100 00 31FF7206 XX
注意UID要按小端模式存储:0x0672FF31 → 31FF7206
重新计算校验和:确保每行校验和正确
验证破解结果
将修改后的HEX文件下载到目标芯片(复制品)中,观察LED闪烁情况:
修改前:LED快闪(验证失败)修改后:LED慢闪(验证通过)
这证明我们成功绕过了UID验证机制。
防破解策略与最佳实践
既然UID加密可以被破解,那么如何提高程序安全性呢?以下是一些最佳实践:
1. 避免明文读取UID
不要直接通过固定地址读取UID,而应该使用间接计算方式:
// 不安全:直接读取
uint32_t uid = *(uint32_t*)(0x1FFFF7E8);
// 更安全:间接计算地址
volatile uint32_t addr_base = 0x1FFFF000;
volatile uint32_t offset = 0x7E8;
uint32_t uid = *(uint32_t*)(addr_base + offset);
甚至可以使用更复杂的地址计算方式,增加破解难度。
2. 避免使用while(1)死循环
验证失败时,不要使用明显的死循环,这容易被逆向工程定位并跳过:
// 不安全:明显的死循环
if (verification_failed) {
while(1); // 容易被定位和跳过
}
// 更安全:修改关键参数
if (verification_failed) {
important_parameter *= 0.8; // 使程序继续运行但性能降低
timing_constant = 500; // 修改时序参数
}
3. 随机化验证时机
不要在固定时间或固定位置验证UID,而应该随机触发验证:
// 随机验证示例
void randomized_verification(void) {
static uint32_t counter = 0;
counter++;
// 使用随机条件触发验证
if ((counter % 17 == 0) ||
(HAL_GetTick() % 1000 > 950) ||
(adc_value > threshold)) {
verify_uid();
}
}
4. 多重验证机制
除了UID验证外,还可以结合其他方式:
外部器件UID:使用DS18B20等带唯一ID的外部器件作为辅助验证通信加密:在PCB上增加辅助MCU,通过加密通信验证身份专用加密芯片:如ATSHA204A等安全认证芯片,提供硬件级安全性
// 多重验证示例
bool verify_system(void) {
bool uid_valid = verify_mcu_uid();
bool ds18b20_valid = verify_ds18b20_id();
bool eeprom_valid = verify_eeprom_signature();
// 需要全部验证通过
return uid_valid && ds18b20_valid && eeprom_valid;
}
总结与思考
安全永远是攻防博弈,没有绝对安全的系统。我们能做的是:提高破解难度,增加破解成本。
对于普通产品,基本的UID加密+一些反调试手段,已能抵御大部分破解尝试。对于高价值产品,则应考虑多重验证机制和专业安全方案。
记住一点:安全设计应该从项目初期就考虑进去,而不是事后补救。
最后,希望本文能帮助开发者们提高对单片机程序安全的重视,设计出更安全可靠的产品。如果你有其他好的加密经验和方法,欢迎在评论区分享交流!
参考资料
STM32参考手册 - UID和Flash保护章节《嵌入式系统安全》- 清华大学出版社正点原子STM32开发实战指南 - 安全编程章节