适用于 ARM Cortex-M0/M3/M4 等资源受限 MCU | 附 C 代码示例
本方案仅供技术参考,无法保证完全防止破解。硬件安全需结合软件防护,必要时请咨询专业安全公司。
一、背景与威胁模型
嵌入式产品面临的常见攻击手段包括:
- 电压/时钟 glitching(故障注入) — 通过短暂干扰芯片供电或时钟,使加密验证跳过执行
- DPA / CPA(差分功率分析) — 通过分析芯片运行时的功耗曲线推测密钥
- 芯片 decap + 固件提取 — 物理方法去除封装,用微探针直接读取 flash 内容
- 调试接口攻击 — 通过 SWD/JTAG 接口读取内存或控制执行
二、芯片级安全特性(选型建议)
如果预算允许,优先选择带硬件安全特性的 MCU:
三、软件层面加密方案(含代码示例)
1代码混淆
将关键算法拆分、跳转嵌套,使反汇编难以理解。攻击者看到的是一堆跳转和"垃圾"代码,无法直接看出算法逻辑。
正常代码是线性的:A → B → C → 结果。混淆后变成:A → 随机跳转 → 垃圾代码 → 函数指针表 → 间接调用 → 解密结果。攻击者需要手动追踪每一条跳转,耗时增加 10-100 倍。
// 方法1:函数指针间接调用(增加分析难度)
typedef int (*CalcFunc)(int a, int b);
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int mul(int a, int b) { return a * b; }
// 函数指针数组,但索引不直接使用
static CalcFunc funcs[] = {sub, mul, add}; // 顺序打乱
// 通过计算得到索引,而不是直接写 add[2]
int secure_calc(int type, int a, int b) {
int idx = (type * 7) % 3; // 攻击者不知道这个算法
return funcs[idx](a, b);
}
// 方法2:插入无效代码干扰分析
void process_key(uint8_t *key) {
uint8_t dummy1 = key[0] ^ 0x55; // 垃圾代码
uint8_t dummy2 = key[1] + 0xAA; // 垃圾代码
// 真正的处理
for(int i = 0; i < 16; i++) {
key[i] = key[i] ^ 0x42;
}
// 更多垃圾代码
volatile uint8_t waste = dummy1 + dummy2;
(void)waste; // 防止优化
}
2运行时动态解密
关键数据不以明文存储在 Flash 中,运行时才解密。即使攻击者读取了 Flash,看到的也只是密文。
Flash 中存的是 0xA3 0x7F 0x12...,攻击者不知道密钥就无法还原。即使提取了固件,没有正确的解密 key 也无法得到原始算法。
// 加密的密钥存储在 Flash(攻击者读不到明文)
// 这段数据是用 AES 加密后的结果,密钥通过 OTP 或外部芯片提供
const uint8_t encrypted_key[16] __attribute__((section(".encrypted"))) = {
0x8A, 0x7B, 0x3C, 0xD1, 0x5E, 0x9F, 0x22, 0x77,
0x44, 0xB1, 0x6E, 0xC3, 0x9A, 0x08, 0xF5, 0x6B
};
// 运行时解密(密钥从安全区域读取)
void get_decrypted_key(uint8_t *out_key) {
// 从 OTP 或安全引导区读取原始密钥
uint8_t master_key[16];
read_from_otp_secure_region(master_key); // 这是一个"不可能被读"的区域
// AES 解密
AES_decrypt(encrypted_key, out_key, master_key);
// 使用后立即清除(防止内存泄漏)
memset(master_key, 0, 16);
}
// 验证流程:每次需要用密钥时临时解密
int verify_license(uint8_t *license_key) {
uint8_t real_key[16];
get_decrypted_key(real_key); // 临时解密
int result = memcmp(license_key, real_key, 16);
// 关键:使用后立即清除!
memset(real_key, 0, 16);
return result;
}
3校验和反调试
检测调试器连接或代码被篡改,主动破坏数据让攻击者即使破解也无法使用。
攻击者想调试代码来理解逻辑?但一旦检测到调试器连接,芯片会主动擦除关键数据或跳转到死循环,等于白忙一场。
// 方法1:检测调试器(检查 CPU 状态寄存器)
int check_debugger(void) {
// Cortex-M 的 DHCSR 调试寄存器
// 如果 bit0 = 1,说明调试器已连接
volatile uint32_t *DHCSR = (uint32_t *)0xE000EDF0;
if (*DHCSR & 0x00000001) {
return 1; // 检测到调试器
}
return 0;
}
// 方法2:代码段 CRC 校验(检测被篡改)
uint32_t calculate_crc(uint32_t start, uint32_t length) {
uint32_t crc = 0xFFFFFFFF;
for (uint32_t i = 0; i < length; i++) {
crc ^= *(uint8_t *)(start + i);
for (int j = 0; j < 8; j++) {
crc = (crc >> 1) ^ (0xEDB88320 & -(crc & 1));
}
}
return ~crc;
}
// 启动时和关键操作前校验
void secure_verify(void) {
// 预期值:编译时计算好并固化
const uint32_t expected_crc = 0xDEADBEEF;
uint32_t actual_crc = calculate_crc(0x08003000, 0x1000); // 校验关键代码段
if (actual_crc != expected_crc) {
// 代码被篡改!自毁
erase_flash_secure_region(); // 擦除加密密钥
memset((void*)0x20000000, 0, 4096); // 清 RAM
while(1); // 死循环
}
}
// 方法3:蜜罐函数(诱杀攻击者)
void secret_decrypt(uint8_t *data, int len) {
// 这是一个假函数,攻击者破解后会调用它
// 但它会触发自毁
trigger_self_destruct();
}
// 在某处埋下陷阱
void maybe_call_honeypot(void) {
// 5% 概率调用蜜罐(随机爆炸)
if ((RTC->TR & 0xFF) % 20 == 0) {
secret_decrypt(NULL, 0);
}
}
4时间随机化
关键操作加入随机延时,使 glitching 攻击难以找到正确的时序窗口。
Glitching 攻击依赖精确的时序。加入了随机延时后,攻击者需要尝试 10^6-10^9 次才可能成功一次,成本极高。
// 伪随机数生成(基于 ADC 噪声,更难预测)
uint32_t get_random_delay(uint32_t min_us, uint32_t max_us) {
uint32_t noise = 0;
// 读取 ADC 噪声作为随机种子
ADC1->CR |= ADC_CR_ADSTART;
while(!(ADC1->ISR & ADC_ISR_EOC));
noise = ADC1->DR; // ADC 噪声值
uint32_t range = max_us - min_us;
return min_us + (noise % range);
}
// 关键操作前加入随机延时
void secure_critical_operation(void) {
// 验证通过后,不要立刻执行
// 加入 100-500us 随机延时
uint32_t delay_us = get_random_delay(100, 500);
delay_us(delay_us); // 硬件延时
// 再执行真正的操作
execute_critical_task();
}
// 加密操作加入随机填充(打乱功耗曲线)
void random_delay_aes_encrypt(uint8_t *input, uint8_t *output, uint8_t *key) {
// 随机延时使 DPA 攻击失效
uint32_t delay1 = get_random_delay(50, 200);
delay_us(delay1);
// 执行 AES
AES_encrypt(input, output, key);
// 再加一段随机延时
uint32_t delay2 = get_random_delay(30, 150);
delay_us(delay2);
}
5多级验证机制
分散验证点,攻击者必须全部突破才能破解,任何一处失败都会触发保护。
传统方案只有一个验证点,破解就全部搞定。多级验证把安全分散到多个位置,攻击者需要同时突破所有关卡,难度指数级上升。
// 三级验证机制
// 第一级:启动验证(bootloader 阶段)
int level1_boot_verify(void) {
uint32_t stored_crc = *((uint32_t *)FLASH_SIGNATURE_ADDR);
return check_crc(stored_crc); // 验证固件完整性
}
// 第二级:功能级验证(每次关键操作前)
int level2_feature_verify(uint8_t *license) {
// 验证 license 格式 + 签名
return verify_license_signature(license);
}
// 第三级:运行时随机验证(定时触发)
int level3_runtime_verify(void) {
// 随机抽取某个内存区域验证完整性
uint32_t check_addr = get_random_ram_region();
return verify_ram_integrity(check_addr);
}
// 组合调用:全部通过才算成功
int secure_activate(uint8_t *license_key) {
if (level1_boot_verify() != 0) return -1; // 第一级失败
if (level2_feature_verify(license_key) != 0) return -2; // 第二级失败
// 每 10 次调用,随机触发第三级验证
static int call_count = 0;
if (++call_count % 10 == 0) {
if (level3_runtime_verify() != 0) return -3; // 第三级失败
}
return 0; // 全部通过
}
6密钥分片管理
密钥不存放在单一位置,攻击者即使找到一部分也无法拼出完整密钥。
密钥被拆成 3-5 份,分别存放在 Flash 的不同位置,有的甚至通过算法临时计算。攻击者必须同时获取所有分片并知道拼接算法。
// 密钥分 3 片存储
// 分片1:直接存放在 Flash 某处
const uint8_t key_part1[4] __attribute__((section(".key1"))) = {0x12, 0x34, 0x56, 0x78};
// 分片2:通过计算得到(基于 UID)
// 攻击者即使读到 key_part1,不知道这个算法也拼不出来
void get_key_part2(uint8_t *out) {
uint32_t uid = get_chip_uid(); // 芯片唯一 ID
out[0] = (uid >> 24) ^ 0xAA;
out[1] = (uid >> 16) ^ 0x55;
out[2] = (uid >> 8) ^ 0xCC;
out[3] = uid ^ 0x33;
}
// 分片3:存在外部 EEPROM 或通过通信获取
void get_key_part3(uint8_t *out) {
// 从外部安全芯片读取
external_secure_chip_read(0x10, out, 4);
}
// 运行时动态拼接完整密钥
void assemble_full_key(uint8_t *full_key) {
// 拼接 3 个分片
memcpy(full_key, key_part1, 4); // 直接拷贝
get_key_part2(full_key + 4); // 动态计算
get_key_part3(full_key + 8); // 外部读取
// 可选:再做一个 XOR 混淆
for (int i = 0; i < 12; i++) {
full_key[i] ^= 0x5A;
}
}
7轮换密钥机制
每次固件升级更换密钥,旧版固件无法在新硬件上运行,防止历史漏洞被利用。
即使攻击者破解了 v1.0 版本并提取了密钥,v1.1 升级后旧密钥立即失效。攻击者必须重新破解,但每次升级都换密钥,成本太高。
// 版本号存储在 OTP 或专用区域
#define CURRENT_VERSION 3
// 版本密钥表(每个版本对应一个密钥)
const uint8_t version_keys[4][16] __attribute__((section(".version_keys"))) = {
// v0(已废弃,密钥可公开让旧固件失效)
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
// v1
{0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00},
// v2
{0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00, 0x11},
// v3(当前版本)
{0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00, 0x11, 0x22}
};
// 升级时调用:擦除旧版本,使用新版本
void upgrade_version(uint8_t new_version) {
// 从服务器获取新密钥(通过安全通道)
uint8_t new_key[16];
server_fetch_new_key(new_version, new_key);
// 写入新密钥(写入 OTP 区域)
write_to_otp(CURRENT_VERSION_ADDR, new_version);
write_key_to_otp(new_version, new_key);
// 旧版本密钥立即失效
invalidate_old_version();
}
// 运行时检查版本密钥
int verify_version_key(uint8_t *input_key) {
uint8_t current_ver = read_from_otp(CURRENT_VERSION_ADDR);
const uint8_t *valid_key = version_keys[current_ver];
return memcmp(input_key, valid_key, 16);
}
8通讯层加密
产品与服务器/APP 通信必须加密,防止中间人攻击和重放攻击。
即使攻击者抓到了通信包,没有密钥也看不懂内容。加上时间戳和序列号,即使重放旧包也会被识别为无效。
// 轻量级通信加密(适用于资源受限的 M0)
// 使用 XOR + 时间戳 + 简单哈希
typedef struct {
uint8_t data[32]; // 实际数据
uint8_t hash[4]; // 简化 HMAC
uint32_t timestamp; // 时间戳
uint16_t sequence; // 序列号防重放
} SecurePacket;
// 简化版 HMAC(资源受限时使用)
void simple_hmac(uint8_t *key, uint8_t *data, int len, uint8_t *out) {
uint32_t h = 0x12345678; // 初始化值
for (int i = 0; i < len; i++) {
h = h ^ (data[i] * 31 + key[i % 16]);
h = (h << 5) | (h >> 27); // 循环左移
}
memcpy(out, &h, 4);
}
// 发送加密数据
void send_secure(uint8_t *payload, int len) {
static uint16_t seq = 0;
SecurePacket pkt;
memcpy(pkt.data, payload, len > 32 ? 32 : len);
pkt.timestamp = get_rtc_time(); // 获取当前时间
pkt.sequence = seq++;
// 生成 HMAC(简单版)
uint8_t session_key[16];
get_session_key(session_key); // 动态生成的会话密钥
simple_hmac(session_key, (uint8_t*)&pkt, sizeof(pkt) - 4, pkt.hash);
// 发送(可能是明文,但有 hash 保护完整性)
uart_send((uint8_t*)&pkt, sizeof(pkt));
}
// 接收并验证
int recv_verify(uint8_t *packet, int len) {
SecurePacket *pkt = (SecurePacket*)packet;
// 1. 检查序列号(防重放)
if (!is_sequence_valid(pkt->sequence)) {
return -1; // 可能是重放攻击
}
// 2. 检查时间戳(5分钟内的包才有效)
uint32_t now = get_rtc_time();
if (abs(now - pkt->timestamp) > 300) {
return -2; // 包太旧了
}
// 3. 验证 HMAC
uint8_t session_key[16];
get_session_key(session_key);
uint8_t calc_hash[4];
simple_hmac(session_key, (uint8_t*)pkt, sizeof(*pkt) - 4, calc_hash);
if (memcmp(calc_hash, pkt->hash, 4) != 0) {
return -3; // 数据被篡改
}
return 0; // 验证通过
}
四、相关诉讼案例参考
📋 案例一:以色列公司 vs 深圳某科技公司(MAC 软件侵权案)
案情:以色列某公司拥有 WHDI 无线高清视频传输芯片的 MAC 软件著作权。深圳某公司从正规渠道购买了芯片后,将配套软件用于自己的产品,被原公司起诉侵权。
结果:一审原告胜诉,被告赔偿 30 万元 + 11 万维权费用。二审翻案,被告胜诉。
关键点:
- 芯片和软件"一并销售"被视为发行权用尽,购买者有权合理使用
- 销售合同中对第三方的限制,不能约束合同外第三方
- 软件与芯片"一对一绑定"关系,购买者获得的是所有权而非许可权
单纯买芯片送软件的方式很难阻止二次销售和合理使用。需要在软件层面做加密绑定,使软件与产品唯一ID绑定才能运行。
📋 案例二:艾为电子 vs 芯海科技(IPO 期间专利战)
案情:2021 年艾为电子 IPO 关键期,竞争对手芯海科技突然发起专利诉讼,指控其压力触控芯片侵权,向客户发律师函。
结果:影响 IPO 进程,最终双方和解。
关键点:
- 专利诉讼成为商业竞争武器,尤其瞄准 IPO 节点
- 诉讼本身即使不赢,也能恶心对手
📋 案例三:台积电 vs 中芯国际(商业秘密)
案情:2003 年台积电指控中芯国际通过跳槽员工窃取商业秘密,侵犯多项专利。
结果:2005 年和解,中芯国际支付 1.75 亿美金。2006 年台积电再次起诉,中芯国际最终赔偿 2 亿美金 + 转让 8% 股份。
关键点:
- 商业秘密侵权证据难取证,但一旦认定赔偿极高
- 员工竞业协议和保密协议是关键证据
📋 案例四:芯片抄板侵权(行业潜规则)
现状:国内存在完整产业链,抄板破解费用从几百到几千元不等。
维权难点:
- 证明"实质性相似"很难,需要专业鉴定
- 证据保全困难,对方可以随时删除
- 跨境维权成本极高
如果发现被抄袭:
1. 第一时间公证购买对方产品
2. 申请法院证据保全
3. 委托专业机构进行技术鉴定
4. 同时从专利、著作权、商业秘密多条路径维权
五、总结
在资源受限的 Cortex-M0/M3 芯片上,无法使用复杂的加密方案,但可以通过多层叠加的方式提高破解成本:
- 基础层:读保护 + 加密存储 + 密钥分片
- 进阶层:代码混淆 + 反调试 + 时间随机化
- 高级层:动态解密 + 多级验证 + 轮换密钥
目标是让破解成本高于产品利润,使抄袭者无利可图。