大学实验室里有块STM32开发板,连着电机驱动和WiFi模块,挂在校园网上跑了三年。没人碰过它,代码是研究生一届传一届的,密码没改过。一个学弟在毕业设计的最后一晚,用nmap扫了一下网段,发现端口23开着——Telnet,没加密,默认密码。他连上去看了一眼,说"噢,这板子能控制整栋楼的门禁"。

这不是段子。这是真实发生在某高校的事。所以今天想聊聊嵌入式安全这件事,它不是"加个密码防君子"那么简单。

攻击面比你想象的大

嵌入式系统和PC最大的区别在于——它被物理暴露在环境中。一个联网的门禁控制器、一台工业PLC、一个智能水表,攻击者可能摸得到它,可能用逻辑分析仪夹它的走线,也可能在它附近用电磁手段干扰。

先看软件层面的入口。

最常见的漏洞入口是未加密的通信通道。很多设备用UART、SPI、I2C这些总线传数据,裸数据直接走线。攻击者在板子上焊几根杜邦线,接个Saleae逻辑分析仪,就能读到你在总线上传什么。比如你用一个stm32通过UART给WiFi模块发AT指令:"AT+CIPSEND=xxx"——攻击者看到的就是明文,连协议都不用逆向。

一个有意思的地方是,很多设备出厂时调试接口没封。JTAG/SWD接口用Tag-Connect或者排针引出来了,上面印着"SWDIO""SWCLK"——等于把门钥匙挂在了门上。用JLink或者ST-Link连上去,OpenOCD一把读Flash,固件就出来了。

然后是固件本身。Cortex-M系列芯片很多没有开启读保护(RDP),或者只是开了Level 1——对应着stm32的RDP选项字节设成0x00或者0xBB。level 1还能用全局擦除或者某些侧信道手段攻破。真正需要的是Level 2,永久锁定调试口。当然代价是厂内返修也读不了,所以要权衡。

一个简单的内存越界,能让整个系统沦陷

这个问题值得单独拿出来说。

嵌入式开发里最常见的错误是什么?大概率是缓冲区溢出。malloc之后忘了检查返回是不是NULL、memcpy时src比dst长、sprintf往固定数组里塞可变长度的字符串——这些写法在嵌入式代码里太常见了。

我们来看一个典型的场景。

void process_command(char *cmd) {
    char buf[64];
    strcpy(buf, cmd);
    // 解析buf,执行操作...
}

如果cmd是从网络包或者串口数据里解析出来的,攻击者可以构造一个超过64字节的payload——strcpy没有长度限制,多出来的内容直接覆盖栈上的返回地址。这在Cortex-M上意味着什么?攻击者可以控制PC指针,把执行流redirect到他自己构造的payload上。如果MCU开了XIP(eXecute In Place),甚至可以在外部Flash里藏一段shellcode。

防止这个问题的标准做法是用strncpy或者snprintf,限定目标缓冲区的大小。但光这样还不够,更好的方案是配合MPU(Memory Protection Unit)做区域隔离。Cortex-M3/M4/M7多半都有MPU,只是很多人不用。设定好region属性,把栈区标记为不可执行,把代码区标记为只读——即使攻击者覆写了栈,也没法执行上面放的东西。

密码和密钥管理,不应该是草草了事

很多嵌入式设备出厂时用的密码是"admin/123456"、"root/root"、"user/password"这种组合。更糟的是有些设备的密码hardcode在固件里,还不会过期,用户也改不了。之前在某个品牌的IP摄像头固件里找到过后门账户,那是一行硬编码的comparison——"if (!strcmp(input, \"Zte521\"))"——搜一下互联网,这密码十年前就在GitHub上公开了。

密钥存储也是个问题。把AES密钥放在const数组里:

const uint8_t aes_key[16] = {0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6,
                             0xab, 0xf7, 0x15, 0x88, 0x09, 0xcf, 0x4f, 0x3c};

这段代码在固件bin里就是连续16个字节,用hexdump或者binwalk -E一搜就找到了。稍微有点经验的逆向工程师会用sigfind去找这类模式。

解决这个问题,可以考虑用MCU内置的OTP(One-Time Programmable)区域或者eFuse来存密钥。STM32L5/H7系列带有CRYP硬件加密和HASH模块,密钥存到内部的BSEC区域,软件层面读不到裸密钥。另一种方案是使用ATECC608A这类独立安全芯片来做密钥协商和签名,密钥永远不离开芯片本身。

安全启动链,从reset vector就开始了

Secure Boot的思路是芯片上电后,ROM里的一段不可更改的bootloader先校验Flash里第一级boot的签名。通过之后一级boot校验二级boot或者app的签名。每一级都信任上一级签过名的下一级,直到整个固件完成自验。这样只要ROM没被篡改,整个信任链是完整的。

ST、NXP、Microchip这些厂商的中高端MCU现在都带这个功能。以STM32H7为例,它的SFI(Secure Firmware Install)方案支持在产线上用加密的方式把固件灌进去,芯片内部解密后再写进Flash——代码在产线工人手里已经是密文了。

但Secure Boot也不是万能的。如果bootloader本身有漏洞——比如前几年的"CVE-2021-xxxx"系列,某厂商的bootloader在处理签名校验时有一个off-by-one漏洞,攻击者可以构造一个能通过checksum但实际内容已被篡改的固件镜像。所以安全是一个持续对抗的过程,不是装一个功能就完事了。

几条可以现在就动手的建议

回到开头那个故事。现在那栋楼的门禁系统换成了带安全启动和加密通信的新方案。而如果你也想让自己手上的嵌入式设备更安全,可以从这几步开始:

关闭不用的接口。 出货前把JTAG/SWD锁住(RDP Level 2),把UART除debug口外都disable掉,没用的GPIO设成analog输入省电也省事。

通信加个壳。 UART也好SPI也罢,在应用层加一个简单的加密隧道。不一定要用TLS(很多MCU扛不住),一个轻量级的XTS-AES或者ChaCha20就够了。STM32的CRYP外设可以硬件加速AES,几乎不占CPU。

开MPU。 哪怕只配两三个region,把关键段保护起来,都能拦下大部分简单的栈溢出攻击。

审计你的密码。 不要让密码硬编码在一个const char里。至少要做到支持每个设备生成不同的默认凭据,或者强制用户在首次上电时修改。

最后——永远假设攻击者能拿到你的硬件。 如果有人能物理接触你的设备,他迟早能读你的Flash。所以尽量让"即使读到了Flash也干不了什么"——密钥单独存、代码签名校验、敏感操作需要Challenge-Response认证。这是一种深度防御的思路,每一层都不完美,但合起来能挡住绝大多数攻击。

这是个很有意思的领域,安全设计和功能开发往往有冲突——加了MPU调度延迟会变,加密通信会占带宽和功耗,Secure Boot增加了产线复杂度。你在自己的项目里有没有遇到过这种取舍?

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐