ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

STM32安全加固实战:从调试口封锁到安全启动与密钥管理

STM32安全加固实战:从调试口封锁到安全启动与密钥管理 STM32的安全方案很多工程师第一反应就是“烧录后把调试口锁上”但真正做产品安全加固时你会发现锁调试口只是最基础的一步。最近接手一个IoT设备的安全测评客户要求固件不能被抄板、密钥不能被提取、设备通信不能被伪造。我在STM32F407上用了一整套组合拳才真正把固件保护起来。这篇应用笔记就把这套经验拆开讲从威胁模型、调试接口、Flash读保护、安全启动到密钥管理最后给一份可以照着做的加固流程。无论你用F1/F4还是L5/H5这些思路都通用。1. STM32面临的安全威胁攻击者到底在打什么主意1.1 最常见的三个攻击目标做嵌入式安全先想清楚对手要抢什么。从实战来看STM32产品被攻击的主要目标集中在三个方面固件本身。这是最常见的攻击目标。很多产品的核心竞争力就是代码逻辑尤其是电机控制算法、传感器校准参数、通信协议栈这类花了大量时间调出来的东西。攻击者通过SWD或Bootloader把Flash内容读出来丢进IDA Pro逆向整个知识库就泄漏了。严重的还会直接克隆固件抄出一个一模一样的产品。密钥和敏感数据。物联网设备里往往藏着Wi-Fi密码、云平台密钥、AES加密密钥、证书私钥。这些数据如果明文存在Flash里一旦固件被读取就等于把大门钥匙交给了攻击者。我之前遇到一个项目设备用AES-CBC加密上行数据密钥就写死在代码里结果固件被提取后通信协议被完全破解后端服务器被迫下线重做。设备逻辑和功能。有些攻击者不想逆向只想篡改。比如通过调试接口修改某个变量值跳过付费校验或者直接修改Flash里的参数让设备进入工厂测试模式。更隐蔽的是固件降级攻击把新版本固件替换回有漏洞的旧版本从而利用旧版本的已知漏洞。1.2 攻击入口都有哪些攻击者不会只盯着一个口STM32的每个外部接口都可能成为攻击面。我习惯按物理接触程度把攻击路径分成四类攻击路径典型手段需要什么条件调试接口用ST-Link/J-Link连接SWD/JTAG读Flash、改内存物理接触PCB检出调试引脚启动接口启动引脚拉高/拉低进入系统Bootloader通过UART/USB读取Flash物理接触且Bootloader未被禁止外部存储拆下SPI NOR Flash用编程器读取或监听SPI总线使用外部Flash存储固件/密钥网络接口利用通信协议漏洞伪造报文、重放攻击、提权能接入同一网络这四条里调试接口是最容易被忽视的。很多板子为了维修方便JTAG/SWD座子一直留着出厂也不禁用。攻击者拿个几十块的ST-Link往上一怼整个内存空间就暴露了。1.3 STM32安全功能的“家底”好在STM32从硬件层面就内置了不少安全功能只是很多人不清楚这些功能到底覆盖了哪些威胁。拿主流系列来说调试保护RDPRead-out Protection读保护分Level 0/1/2控制调试端口对Flash的访问权限。存储保护选项字节里有Flash写保护WRP可以锁定某些扇区防止意外擦写。OTP区域一次性可编程存储用于放序列号、密钥、公钥哈希等不可变数据。TrustZoneL5/U5/H5等M33/M23内核实现安全世界和非安全世界隔离把密钥隔离在安全区内。硬件加密引擎AES、DES、HASH、RNG配合DMA可以做高效的加密通信。安全启动部分系列支持BootROM中的安全引导配合SBSFU等中间件实现签名校验。关键问题是这些能力不是开箱即用的需要根据威胁模型主动配置。下面逐个讲实操。2. 调试接口这道大门SWD/JTAG的滥用与锁定2.1 为什么说调试口是“后门”SWDSerial Wire Debug是ARM Cortex-M内核的标准调试接口只需要SWDIO和SWCLK两根线就能完成全内存读写、断点调试、Flash编程。对开发者来说这是神兵利器对攻击者来说这简直是自带钥匙的后门——只要物理接触到板子上的两三个测试点不需要任何密码。最经典的攻击流程是这样的用ST-Link连接目标板的SWD打开STM32CubeProgrammer点击“Connect”然后选择“Read”整片Flash。如果芯片处于Level 0默认无保护几秒钟就能把固件完整拖走。之后用binwalk、ghidra、IDA等工具定位字符串、函数、协议一次完整的固件逆向就这么开始了。更过分的是SWD不仅能读还能写。攻击者可以在运行时暂停CPU修改某个变量的值来触发隐藏功能比如跳过设备的激活码校验。这种攻击不需要理解整个固件只要会用调试器就能做到。2.2 禁用调试口的正确方法要封死这个后门最有效的手段是在烧录程序的最后阶段配置选项字节把调试口关掉或者加上读保护。我常用的方式是用STM32CubeProgrammer命令行版本叫STM32_Programmer_CLI在烧录完固件后执行一条命令STM32_Programmer_CLI -c portSWD modeUR -ob RDP0xBB0xBB对应RDP Level 1。执行后SWD连接会被禁止访问Flash后续再用工具连接时会报“Read protection is enabled”之类的错误。有些项目连SWD引脚都不想保留可以通过选项字节将调试端口完全关闭。在CubeProgrammer的“Option Bytes”界面里把DEBUG PORT设置为Disabled。这样芯片连SWD设备枚举都做不到只能通过自定义Bootloader或ISP接口进行后续维护。2.3 禁用调试口会带来哪些新麻烦锁定调试口前一定要想清楚售后问题。一旦禁用了SWD开发时用的“烧录后在线调试”、“读取当前固件版本”、“现场擦除重烧”这些便利操作全都没了。之后的固件升级只能走应用层通信比如UART Bootloader、CANOTA、Wi-Fi OTA。我有一次在生产阶段就把RDP设成了Level 2结果出货后客户反馈说需要修改某个参数我们又不想把整个设备拆回来。最后只能让客户通过串口命令行重新写入参数非常狼狈。所以现在我的原则是产品化初期用Level 1同时保留一个可控的OTA通道只有对安全性要求特别高的行业如支付、安防才用Level 2。另外还要注意禁用调试口不等于禁用所有对外接口。STM32的串口、USB、CAN等外设仍然可以正常通信攻击者如果找到应用层的漏洞依然可能通过固件漏洞远程读取Flash。所以调试口保护只是第一步后面要结合安全启动、通信加密来做纵深防御。3. Flash读保护不是玄学RDP级别的真实行为与选型3.1 理解RDP的三个级别读保护RDP是STM32安全体系里最核心的功能但很多人对它的理解停留在“开和关”。实际上STM32的RDP分三个级别行为差异很大选错了要么不安全要么把自己锁死。Level 0出厂默认状态无读保护。SWD/JTAG可以随意访问Flash和SRAM。Level 1使能读保护。此时通过调试接口访问Flash主存储区会被禁止但CPU本身访问Flash不受影响。也就是说程序正常运行没问题但调试器和外部设备读不到Flash内容。要注意的是Level 1下SRAM的访问权限仍然开放攻击者可以在程序运行期间通过调试器修改SRAM数据如果调试口没完全禁用的话。Level 2最高级别读保护也叫“永久保护”。一旦设置到Level 2读保护级别无法回退调试口被永久禁用连Level 1回退到Level 0的“擦除Flash”机制都不复存在。芯片的灵魂里写入了“我永远不会让你调试”。同时BootROM中的某些调试相关功能也会被限制。3.2 Level 1回退为什么会擦除Flash这是很多工程师踩坑的地方。在Level 1下如果你通过CubeProgrammer把RDP从Level 1改回Level 0软件操作是先擦除整个主Flash区再修改选项字节。擦除动作执行完你的代码和数据就全没了。为什么芯片要这么设计因为如果不擦除Flash攻击者可以这样做先把芯片设为Level 1然后声称自己是合法开发者执行“回退RDP”操作芯片在回退后变回Level 0攻击者再读Flash——固件就白拿了。为了堵住这个逻辑漏洞芯片设计上强制规定从Level 1回退到Level 0必须擦除用户代码。这个机制在ST的参考手册里写得明明白白但很多人没注意。我测试过F407和G070回退时CubeProgrammer会弹窗警告“The user Flash will be erased”确认后芯片先全片擦除再打开读保护整个过程大约一秒钟但数据已经没了。所以如果你想保留固件千万别在生产状态下随意做RDP降级操作。3.3 实战中怎么选RDP级别根据产品生命周期不同阶段我一般这样配置开发调试阶段保持Level 0方便随时用调试器看问题。功能验证阶段可以临时设Level 1测试保护效果但注意回退会擦除Flash需要先备份。量产阶段设置Level 1。理由Level 1已经能阻止绝大多数固件读取攻击且允许通过自定义Bootloader做在线升级Bootloader运行在CPU模式下CPU访问Flash是不受限制的。如果想进一步防抄板可以配合固件加密。特殊行业只有极少数对数据极其敏感的设备需要Level 2。比如某些金融终端的密钥存储区或者出口管制设备。一旦设置Level 2设备基本告别远程升级了。这里特别提醒一个容易混淆的点RDP Level 1只保护Flash不保护RAM和寄存器。如果你的密钥或者敏感计算逻辑是在运行时从Flash解密到RAM再使用攻击者仍然可以通过SWD如果SWD没禁读取RAM。所以真正的高安全场景需要同时做到“禁用调试口”或“使用TrustZone隔离”而不是单纯依赖RDP。4. 固件加密与安全启动让代码即使被读出也无法直接使用4.1 固件加密的思路对比如果RDP Level 1被攻击者用某种方式绕过比如利用Bootloader漏洞、芯片bug、物理探测明文固件依然可能泄露。为了增加逆向成本我们可以在固件层面再做一层加密。这里的思路有两种方案A外部Flash存储加密固件。有些产品代码太大内部Flash放不下只能外挂SPI NOR Flash。如果固件明文存在外部Flash攻击者用编程器直接读SPI Flash就能拿到完整代码。正确的做法是对外部Flash里存储的固件进行AES加密MCU启动时读回密文、用内部存储的密钥解密后执行或解密到RAM/内部Flash。这种方式适合F1/F4等没有TrustZone但带有AES硬件外设的芯片。方案B内部Flash 读保护。如果代码能装进内部Flash则靠RDP就基本足够此时固件加密不是必须的。内部Flash的读取保护已经比较难绕过了。实操中我见过很多产品把整个固件都放在外部SPI Flash里却不做任何加密相当于把源码摆在门口。相比之下哪怕只做“外部Flash密文 内部Bootloader简单解密”都能挡住90%的“会用编程器的小学生”。4.2 安全启动的信任链怎么搭安全启动的核心思想是确保只有经过签名校验的固件才能运行防止攻击者向设备写入篡改过的固件。对于支持TrustZone的STM32L5、U5、H5、L4等ST官方提供了X-CUBE-SBSFU例程它把BootROM和用户代码之间放了一个安全固件更新中心实现了安全启动和安全固件下载。对于老的F系列我们可以自己搭一个简化的安全Bootloader。我做过一个F407的简化版Bootloader放在0x08000000开始的16KB区域App从0x08004000开始。App的头部加了一个固定结构体包含App长度、版本号、SHA-256哈希、RSA签名。Bootloader启动后执行以下流程读取App Header检查魔数是否正确。计算App区域的SHA-256与Header中保存的哈希比对。用RSA公钥验证签名确认哈希没被人替换。通过校验后跳转App失败则停留在Bootloader等待固件更新。公钥存储在OTP区域写入后不可修改。这样攻击者即使拿到了固件也无法伪造一个能通过验证的新固件放入设备。签名验证如果全部用软件跑F407在168MHz下一次SHA-256大约几毫秒RSA-2048验证大约几十毫秒完全可接受。我就是用mbedTLS库实现的代码量不大。4.3 为什么还要做版本号防回滚安全启动校验通过不代表万事大吉。如果攻击者提取到某个老版本固件该版本存在已知漏洞然后想办法通过OTA把设备降级到那个老版本那么之前做的所有安全补丁就全白费了。这就是“降级攻击”。应对方法也简单在App Header里加一个单调递增的版本号Bootloader检查当前版本号不能低于存放在备份寄存器或OTP里的版本值。每次升级成功后就把新版本号写入一个非易失性的位置。F407可以放在备份寄存器如果VBAT有电或者RTC backup registers或者专门一个Flash扇区。我习惯用后一种简单可靠。这里要强调的是版本号不能放在App内部因为它会随着App一起被替换。要放在Bootloader维护的独立存储区里最好带写入计数和校验防止攻击者直接修改版本号存储区。5. 通信安全与密钥管理密钥泄漏等于一切白搭5.1 通信加密最常见的坑设备之间通信如果走明文攻击者在总线上抓个包就能分析出协议字段更不用说有些设备直接通过UART收发明文AT指令。我之前帮客户排查一个产物联网问题发现设备和服务端之间用的是自定义TCP协议每条报文里还带着明文设备ID没有任何校验字段。攻击者伪造一条“重启设备”的指令整条产线就能被关停。给STM32设备做通信安全有两个层次的要求第一层是加密保证数据内容不泄露第二层是认证和完整性保证报文没有被篡改或重放。AES-GCM或ChaCha20-Poly1305可以同时解决加密和完整性认证。5.2 用硬件加密引擎还是软件库STM32系列里的加密外设差异很大。F407自带AES、DES、HASH但没有真正的RSA加速L4/L5/H5/RM的加密外设更完整G0/G4部分型号也有AES。如果你的芯片有硬件AES强烈建议用硬件外设而不是纯软件加密。硬件加密引擎有两个好处一是速度更快F407的AES-128硬件加密大约能跑到几十MB/s二是密钥可以直接存放在硬件外设的独立寄存器区软件无法直接读出来至少没有现成指令能读相对更安全。mbedTLS是嵌入式里最常用的加密库支持软件和硬件Acceleration两种模式。如果芯片有硬件AES可以写一个基于ST HAL的加密回调函数交给mbedTLS调用这样上层直接使用mbedtls_aes_crypt_ctr等API底层自动走硬件。反过来如果芯片没有加密外设纯软件跑AES在72MHz的F103上也能用只是占CPU时间多一些适用低速控制场景没问题。5.3 密钥存在哪里才算安全这是整个安全方案里最核心、也最容易被忽视的问题。很多工程师嫌麻烦直接把AES密钥定义成一个const uint8_t key[16]数组写在代码里。这种做法等于把保险柜钥匙贴在保险柜外面。一旦固件被提取不管是外部Flash明文还是RDP被绕过密钥就暴露了。我整理了几种密钥存储方式按安全性从低到高排列存储方式说明适用场景代码内硬编码密钥随固件一起存储提取固件即可获得仅适合原型验证不适合量产独立OTP区域烧录时写入OTP不可修改但CPU可读密钥若被CPU读取仍可能被攻击者通过调试接口读取需结合调试保护TrustZone安全区密钥只存在于安全世界非安全代码无法访问L5/U5等带TrustZone的芯片安全性高外部安全芯片SE密钥存储在ATECC608A等安全芯片内MCU只能请求加解密不能直接读取安全要求最高的场景这里特别说明一下OTP。STM32的OTP是一个独立的一次性可编程区域写进去后就不能再改。对F407来说OTP前512字节可以自由写后部分是RoTRoot of Trust区域。可以把公钥哈希存在OTP里攻击者无法篡改。但OTP区域本身CPU可以读所以不能直接把私有密钥放进去。我的经验是私钥永远留在工厂或服务器端设备里只放公钥或由设备唯一密钥派生的加密密钥。对于需要设备拥有唯一密钥的场景比较好的做法是用芯片的UIDUnique ID作为种子通过硬件随机数密钥派生函数KDF在运行时动态生成密钥这样每台设备的密钥都不同而且不常驻Flash。攻击者即使拿到了固件也无法直接还原出另一台设备的密钥。6. 实战案例给STM32F407设备做一套基础安全加固6.1 加固目标和总体步骤这块板子是我自己做的一个数据采集器主控是STM32F407VET6外部挂了一个SPI NOR Flash存历史数据通过以太网口上传。加固目标定为三条固件不能被通过SWD读取防抄板固件被篡改后无法启动防破坏网络通信中的敏感命令不能被伪造防攻击整体加固流程分五步按顺序执行缺一不可生成RSA密钥对把公钥烧入OTP编写带签名校验的Bootloader编写AppApp编译时用私钥签名烧录Bootloader和App然后设置RDP Level 1验证攻击效果6.2 关键操作细节第1步生成密钥对并烧录公钥。在PC上用OpenSSL生成2048位RSA密钥对。公钥是DER格式的二进制长度是模块指数大约294字节。把公钥的内容用STM32_Programmer_CLI写入F407的OTP区域STM32_Programmer_CLI -c portSWD modeUR -otp 1 0x1FFF7800 public_key.bin注意这块地址是F407的OTP基地址具体到不同系列要查参考手册。写OTP是一次操作写错就废了最好先写一个小块测试。第2步Bootloader的签名校验。Bootloader编译时不启用任何加密库的话工程里加入mbedTLS或者用ST的加密库。我在Bootloader中只放了SHA-256和RSA验证代码总Flash占用约12KB可以接受。校验逻辑伪代码大致是这样bool verify_app(void) { const uint8_t *app_start (uint8_t*)0x08004000; uint8_t digest[32]; mbedtls_sha256(app_start, APP_MAX_SIZE, digest, 0); // 从AppHeader取签名用OTP中的公钥验证 if (rsa_verify(public_key, digest, app_header.sig) 0) { return true; } return false; }第3步App编译与签名。App编译生成bin后在PC上用Python脚本读取App bin追加一个头部包含长度、版本号、SHA-256、RSA签名重新打包成一个新的bin。然后用CubeProgrammer将Bootloader烧到0x08000000把处理后的App bin烧到0x08004000然后设置RDP Level 1STM32_Programmer_CLI -c portSWD modeUR -ob RDP0xBB第4步验证。断开SWD再用CubeProgrammer连接会报错无法读取Flash如果用命令行强制读会返回全0xFF或报保护错误。将SWD恢复Level 0但会擦除Flash后再读出来的固件已经没有了。能证明保护生效。第5步验证签名启动。重新上电Bootloader显示App校验通过程序正常运行。然后用一个其他私钥签名的App烧入在开发阶段做测试Bootloader应该拒绝启动停留等待固件。6.3 几条实测经验设置RDP Level 1以后CubeProgrammer仍然可以擦除并重新烧录代码但是擦除后你就没法再调试了因为你每次连接都会碰上RDP保护。所以建议烧录顺序是先烧Bootloader再烧App最后才设RDP。如果设完RDP后想重新开发只能执行“回退RDP”但Flash会被清空OTP不会被清OTP一次性清不了。如果公钥之前烧进去了回退后公钥还在这既是优点也是缺点因为你想换公钥就得换芯片。在F407上SHA-256计算1MB数据大约要100ms左右在Bootloader里做每次开机校验会略微增加启动时间但可接受。如果对启动时间敏感可以只校验App前64KB或者加一个“校验通过则设置标志下次快速跳过”的优化不过跳过会增加安全风险自己权衡。这整套流程做完之后我的设备至少能抵抗以下常见攻击用调试器读Flash、用编程器读外部SPI Flash如果内部固件不直接存放还需要对SPI Flash内容加密这个案例里历史数据是加密后写进的、通过OTA发送伪造固件、通过抓包重放网络命令。当然没有绝对的安全但这一步能让攻击成本远远高于产品本身价值对绝大多数商用场景已经够用。最后分享一个实操心得安全加固不是一次性的而是和产品迭代、密钥维护、Factory流程绑定在一起的。比如这批设备用了RSA密钥A下个批次如果换个密钥BBootloader里的公钥就要跟着换而OTP已经写死怎么办所以我在设计时做了兼容Bootloader里先查OTP中的公钥哈希如果为空再使用代码里内置的默认公钥仅用于开发板。产品化时工厂在烧录环节写入正式公钥并清零OTP标志这样既能保证开发便利又能让不同批次用不同的密钥对。如果你也准备给STM32产品做安全加固建议把这套流程提前设计进工厂生产流程而不是最后临时补。
返回列表