喂你们那设备上电之后随便拿个串口工具就能读固件客户这句话是下午三点发过来的。当时我正在调试另一块板子看了一眼消息心想不至于吧。结果试了一下——好家伙Reset拉低、BOOT0拉高、上电OpenOCD连上去二话不说就把整个flash读出来了。客户说得客气但意思很明确你们这东西跟裸奔没区别。说实话之前不是没想过安全问题。但像大多数做单片机开发的团队一样总觉得我们这玩意儿又不连公网、谁会闲得蛋疼来读你这破固件——直到这东西真的被客户部署到别人家的机房里你才发现信任边界一旦超出你控制的范围一切假设都不成立。secure boot 这东西很多低端MCU压根不支持。但如果你用的芯片好歹有个OTP区域或者efuse那就能做点文章。拿STM32举例子它的RDPRead Protection等级分三级Level 0无保护你随便读Level 1禁止调试接口读flash但SRAM还能访问可以通过SRAM里的代码间接读flashLevel 2彻底锁死调试接口永久失效不可逆我当时第一个反应是直接上Level 2。但一查手册——Level 2一旦设置连你自己都没办法通过调试器更新固件了只能走ISP或者OTA。如果bootloader崩了这板子就只能当砖头扔了。Level 2这玩意儿就是个回不了头的选择。后来折中了一下选了Level 1 自定义校验。Level 1意味着SWD/JTAG直接读flash读不出来但还是要防SRAM攻击。STM32有一个叫RDP Regression的功能——如果你设置了Level 1然后又想降级回Level 0芯片会自动擦除整个flash。这是个好机制。但问题在于光靠硬件读保护还不够。/* 设置RDP Level 1的代码很简短 */ HAL_FLASH_OB_Unlock(); FLASH_OBProgramInitTypeDef ob {0}; ob.OptionType OPTIONBYTE_RDP; ob.RDPLevel OB_RDP_LEVEL_1; HAL_FLASHEx_OBProgram(ob); HAL_FLASH_OB_Launch();这段代码跑完重新上电读保护就生效了。但你知道我踩了什么坑吗它只保护了flash的静态数据。如果攻击者把你板子上的flash芯片如果是外部spi flash直接拆下来用编程器读呢如果你用的是外部QSPI Flash存代码呢那就不是RDP能管的事了。去年有个项目用了W25Q64做代码存储结果生产的时候发现烧录效率太低就把flash座子喷了——结果客户现场维修工直接把座子上的flash拆下来插到自己的烧录器上读走了固件。物理访问 完全控制这个基本假设你得接受。所以后来做了这么几件事第一bootloader里加了一层签名验证。在固件bin文件的末尾附上RSA签名bootloader启动时用公钥验签。公钥放在芯片内部flash的OTP区域改不了。这样就算固件被读走了它也没法篡改后刷回去。/* bootloader验签简化版 */ bool verify_firmware(const uint8_t *fw, uint32_t len) { /* 假设fw的最后256字节是RSA-2048签名 */ uint32_t sig_len 256; const uint8_t *signature fw len - sig_len; uint8_t hash[32]; mbedtls_sha256(fw, len - sig_len, hash, 0); return mbedtls_rsa_pkcs1_verify( public_key, MBEDTLS_MD_SHA256, hash, signature) 0; }别笑就这一行验签函数省了后面多少破事。第二外部flash的数据做了AES加密。密钥存在MCU内部的OTP里bootloader启动时用内部flash的OTP key解密外部flash的代码。这样就算别人拆了flash芯片拿到的也是密文根本用不了。/* AES解密外部flash内容到内部SRAM */ void decrypt_firmware(uint32_t ext_addr, uint32_t *dest, uint32_t size) { uint8_t key[32]; read_otp_key(key); /* 从OTP读密钥 */ mbedtls_aes_context aes; mbedtls_aes_setkey_dec(aes, key, 256); uint8_t iv[16] {0}; /* 注意这里的iv不能全0生产环境下要从eFuse/UID派生 */ for (uint32_t i 0; i size; i 16) { uint8_t buf[16]; /* 从外部flash读 */ spi_flash_read(ext_addr i, buf, 16); mbedtls_aes_crypt_cbc(aes, MBEDTLS_AES_DECRYPT, 16, iv, buf, dest i/4); } }当然AES-CBC模式有个问题如果攻击者能多次上电观察不同iv下的解密行为理论上可以进行padding oracle attack。虽然对嵌入式场景来说攻击成本很高但如果你的产品要过FIPS或者等保测评建议换成GCM或者CTR。第三加了一道防篡改检测虽然不是软件层面的——在PCB上埋了几条细走线一旦外壳被打开走线被切断立即触发DMAC死亡中断调用RTC backup register里存的标志位下次启动直接进recovery模式。硬件层级的防护比软件更不好绕过。那OTA升级的安全性呢一旦开启了OTA攻击面就从一个物理接触口扩大到了网络。最开始我图省事OTA固件包直接用明文传。直到有次做渗透测试对方通过抓包拿到了固件镜像逆向分析找到了里面硬编码的阿里云key……那个云账号下的所有设备数据全等于白送。那一版之后OTA包至少要做签名防篡改 加密防窥探 版本号校验防降级攻击。缺一个都不行。OTA包的格式大概是这样的| 固件头 (64B) | 固件数据 (加密) | RSA签名 (256B) | |- magic: 4B | | | |- version: 4B | | | |- length: 4B | | | |- iv: 16B | | | |- reserved | | |bootloader收到OTA包后先验签名确认版本不低于当前运行的版本防止rollback attack然后解密写入flash。说到防降级攻击我见过一个真实案例某厂商的智能门锁攻击者拿到了旧版本的固件包旧版本有个硬编码的管理员口令然后模拟服务器伪装成OTA下发把门锁降级到了那个版本直接拿管理员权限开门。所以版本号校验是硬性要求而且版本号要跟签名一起验证不能被篡改。最后说一句安全这种事没有银弹。你加了读保护、上了签名验证、做了加密存储——还是会有人拿探针去刮开芯片封装用FIB聚焦离子束修改变压器电路来绕过OTP熔丝。但问题是你的攻击者愿意花多少钱来搞你对于大部分IoT设备来说把防护成本做到比你的产品售价高就已经赢了。几千块钱买回来的设备攻击者不太可能花几万美金去做芯片级逆向。除非你的设备控制的是核电站阀门或者几百万人的支付数据——那另当别论。对了后来我跟客户说已经加了读保护和签名验证。他没回消息。第二天他发了一张照片过来——一台用钢锯锯开的设备壳子旁边丢着个逻辑分析仪。底下配文这个怎么防