你的嵌入式设备有多不安全?看看攻击者是怎么进来的

你的嵌入式设备有多不安全?看看攻击者是怎么进来的
大学实验室里有块STM32开发板连着电机驱动和WiFi模块挂在校园网上跑了三年。没人碰过它代码是研究生一届传一届的密码没改过。一个学弟在毕业设计的最后一晚用nmap扫了一下网段发现端口23开着——Telnet没加密默认密码。他连上去看了一眼说噢这板子能控制整栋楼的门禁。这不是段子。这是真实发生在某高校的事。所以今天想聊聊嵌入式安全这件事它不是加个密码防君子那么简单。攻击面比你想象的大嵌入式系统和PC最大的区别在于——它被物理暴露在环境中。一个联网的门禁控制器、一台工业PLC、一个智能水表攻击者可能摸得到它可能用逻辑分析仪夹它的走线也可能在它附近用电磁手段干扰。先看软件层面的入口。最常见的漏洞入口是未加密的通信通道。很多设备用UART、SPI、I2C这些总线传数据裸数据直接走线。攻击者在板子上焊几根杜邦线接个Saleae逻辑分析仪就能读到你在总线上传什么。比如你用一个stm32通过UART给WiFi模块发AT指令ATCIPSENDxxx——攻击者看到的就是明文连协议都不用逆向。一个有意思的地方是很多设备出厂时调试接口没封。JTAG/SWD接口用Tag-Connect或者排针引出来了上面印着SWDIOSWCLK——等于把门钥匙挂在了门上。用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开了XIPeXecute In Place甚至可以在外部Flash里藏一段shellcode。防止这个问题的标准做法是用strncpy或者snprintf限定目标缓冲区的大小。但光这样还不够更好的方案是配合MPUMemory 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内置的OTPOne-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为例它的SFISecure 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增加了产线复杂度。你在自己的项目里有没有遇到过这种取舍