资讯详情

资讯详情

STM32固件保护:如何用选项字节禁掉SWD和ISP彻底锁死代码

做嵌入式开发最怕什么代码被抄、固件被扒、产品被复刻。很多开发者觉得写完程序、烧进芯片就万事大吉了但实际上芯片出厂默认状态就是个“透明保险柜”SWD调试口和ISP串口引导都开着谁拿个调试器往板子上一戳就能把Flash里的固件读出来、改掉、甚至整个擦掉重写。我经常被问到的一个问题就是怎么禁止STM32通过SWD和ISP升级来保护自己的产品这个问题背后的核心需求是防止别人通过调试接口或者串口下载接口重新写程序、读代码。这里要用到的关键手段就是操作STM32的选项字节Option Bytes而HAL库里的HAL_FLASH_Program一族函数正是干这个的——很多人以为它只能写应用代码其实它还能改写选项字节给芯片上锁。这篇内容我结合自己实际做产品的经验把从原理到代码、从避坑到量产的操作一次性写清楚。1. 问题本质SWD和ISP为什么是“泄密入口”1.1 SWD调试口出厂自带的“全权限通道”SWDSerial Wire Debug是ARM Cortex-M内核标配的调试接口只需要SWDIO、SWCLK两根线加上GND和复位就能通过调试器连接到芯片内部。它能干什么读Flash、读RAM、读寄存器、单步执行、打断点、改内存数据甚至还能通过串口窗口偷看你的日志输出和全局变量。STM32出厂时读保护RDP默认是Level 0也就是完全没有任何保护。这时候你手里的板子对调试器来说就是一个完全开放的芯片。任何人只要拿到你的板子用调试器连上SWD接口就能把固件整体读出来然后用IDA或者Ghidra逆向分析。更狠的是他都不用完整还原你的代码直接把自己写的固件烧进去你的硬件就成了他的“新作品”。我见过一个案例某开发者做了一款数据采集模块觉得自己代码写得还行结果客户拿到样机后找个人用调试器几天就把固件抄走了然后自己量产。他后来跟我说“我连Flash里存的参数表都被人家读出来了。”这就是SWD口不设防的代价。1.2 ISP串口引导BOOT引脚一拉固件随便刷ISPIn-System Programming是STM32出厂自带的系统存储器引导程序藏在芯片的ROM区用户没法改写它。它通过UART串口接收数据可以擦除和写入主Flash。启动它的条件很简单把BOOT0引脚拉高部分系列还要配合BOOT1然后复位芯片芯片就会进入系统存储器里的bootloader等待串口指令。这意味着什么哪怕你完全不接调试器别人只要打开板子找到BOOT0引脚用镊子或者杜邦线把BOOT0接到高电平再复位一次芯片就进入ISP模式了。然后通过串口工具连接USART1或USART2具体看型号发送原厂定义的协议命令就能读取芯片信息、擦除Flash、写入新固件。整个过程只需要一个USB转串口模块成本十几块钱。1.3 安全与便利的天平你到底要防谁讨论保护方案之前先想清楚一个核心问题你的产品要防什么人不同场景的取舍完全不同。防同行抄板需要防的是“能拿到实物、有调试器、会逆向”的人读保护和接口封锁必须做到硬件级。防普通用户误刷用户不会刻意破解但可能因为好奇按住复位键、掰BOOT引脚或者误操作导致固件损坏。这种情况做好BOOT引脚处理和读保护就够了。防维修/售后团队有时候返修回来的板子厂家自己也要刷程序那就不能锁太死需要预留受控的升级通道。另外还要想清楚一个矛盾你封死了SWD和ISP那自己工厂量产的时候怎么烧程序后面售后要升级固件怎么办这些问题如果不在设计阶段想明白等产品已经做了几百套再改麻烦就大了。所以我会在文章后半部分专门讲“量产顺序”和“OTA升级”的配合方案。2. 核心防线HAL_FLASH_Program一族函数改写选项字节2.1 先分清HAL库Flash编程三兄弟很多人一看到标题里的HAL_FLASH_Program就问“这函数不是写应用Flash的吗跟保护有什么关系”这里有一个很常见的误区。HAL库提供了多个Flash操作相关的函数各有分工函数名作用说明HAL_FLASH_Program编程主Flash区写应用程序代码、写参数数据HAL_FLASH_OBProgram编程选项字节区设置读保护、写保护、BOOT配置等HAL_FLASH_OB_Unlock / HAL_FLASH_Unlock解锁对应区域写入访问密钥防止误操作你可以在代码里调用HAL_FLASH_Program来写IAP升级程序但“禁止通过SWD和ISP升级”这件事真正干活的其实是HAL_FLASH_OBProgram——它专门用来改写选项字节。很多教程不区分这两个函数导致开发者以为烧录保护就是调用一下HAL_FLASH_Program。理解这对兄弟函数的区别是搞懂本篇内容的第一步。选项字节Option Bytes是STM32里一个独立的配置区域可以理解成芯片的“系统设置项”。它里面的RDPRead Protection字段就是控制代码保护级别的总开关。通过修改这个字段你可以让调试器和ISP都无法读取、写入主Flash。2.2 读保护RDP三级机制0、1、2的区别STM32的读保护分三个级别这是整个保护方案的核心基础。我直接用一张表说清楚RDP级别名称调试口访问ISP访问能否恢复典型场景Level 0无保护完全开放完全开放—开发调试阶段Level 1读保护无法读Flash/RAM只能连接做擦除无法读Flash但可执行全片擦除可恢复但恢复时自动全片擦除量产成品标准配置Level 2永久保护调试口彻底失效彻底失效不可恢复永久锁死高度保密产品/终极熔断这里有两个细节特别重要第一Level 1并不是“不能连接调试器”而是“连接后无法访问主存储区”。实际表现是调试器能识别到芯片但读内存出来的数据全是零或者无效烧录操作被拒绝。很多开发者上了Level 1之后发现还能连上调试器就以为保护没生效其实保护已经起作用了——只是“能看见”不代表“能拿到数据”。第二Level 1有一个设计上的“后门”就是允许把RDP从Level 1降回Level 0但代价是硬件会自动触发全片擦除Mass Erase。也就是说即使有人找到了降级的方法他得到的也只是一颗空片你的固件已经被硬件自动抹掉了。这个机制不是漏洞反而是防抄板的核心保障。想清楚这个逻辑你不是要防止别人“擦掉固件”而是要防止别人“读出固件”。擦掉不可怕读出来才是灾难。2.3 设置RDP Level 1的标准代码以STM32F4系列、HAL库为例设置读保护Level 1的完整流程如下/* 1. 解锁Flash和选项字节区域 */ HAL_FLASH_Unlock(); HAL_FLASH_OB_Unlock(); /* 2. 配置读保护级别 */ FLASH_OBProgramInitTypeDef obConfig; obConfig.OptionType OPTIONBYTE_ROP; obConfig.RDPLevel OB_RDP_LEVEL_1; /* 3. 写入选项字节 */ uint32_t status HAL_FLASH_OBProgram(obConfig); /* 4. 上锁并复位 */ HAL_FLASH_OB_Lock(); HAL_FLASH_Lock(); if (status HAL_OK) { /* 复位后选项字节生效 */ NVIC_SystemReset(); }注意不同系列的API写法有差异。比如在STM32F1系列上代码更接近FLASH_Unlock(); FLASH_OB_Unlock(); FLASH_OB_RDP_LevelConfig(OB_RDP_LEVEL_1); FLASH_OB_Lock(); FLASH_Lock();所以抄代码的时候一定要对应自己的芯片型号和HAL版本别直接拿F4的函数往F1上套会编译报错或者行为不符合预期。你可以在CubeMX生成的工程里找到对应的HAL头文件先确认有哪些OB相关的函数原型再决定怎么写。2.4 Level 2真正的“物理熔断”如果产品对保密要求极高可以在量产时直接设置RDP Level 2。这个级别下芯片的所有调试接口、ISP引导、选项字节读取/修改全部失效而且是永久性的没有任何恢复手段。可以说设了Level 2芯片就从一个“可编程设备”变成了“只读设备”连你自己都动不了它。设置Level 2的代码和Level 1基本一样只改一个宏obConfig.RDPLevel OB_RDP_LEVEL_2;但我要反复强调严禁在开发调试阶段设置Level 2。一旦设置这个芯片就废了你以后想改固件、想调bug、想复用这颗料做其他项目都无能为力。我身边就有朋友在测试读保护功能时手一抖写了Level 2结果整块板子变成砖头只能扔了换新片。量产的流程应该是所有固件下载、功能测试、参数校准都做完之后最后一步才写Level 2。而且要写一个独立的量产工具脚本不要让产线工人手动在调试界面里选Level 2免得选错。2.5 运行时动态上锁程序跑起来后自己锁死除了在烧录阶段用调试器设置RDP还有一种实用场景固件运行过程中检测到某个条件后主动调用HAL_FLASH_OBProgram把自己锁起来。举个例子你的产品有密码保护功能用户连续输错三次密码。这时候可以在代码里判断出错次数然后把RDP从Level 0直接改到Level 1if (password_error_count 3) { /* 输入错误次数过多锁死芯片防止暴力破解 */ HAL_FLASH_Unlock(); HAL_FLASH_OB_Unlock(); FLASH_OBProgramInitTypeDef lockConfig; lockConfig.OptionType OPTIONBYTE_ROP; lockConfig.RDPLevel OB_RDP_LEVEL_1; HAL_FLASH_OBProgram(lockConfig); HAL_FLASH_OB_Lock(); HAL_FLASH_Lock(); NVIC_SystemReset(); /* 复位后锁生效 */ }这种做法的意义在于它让“上锁”这件事不依赖外部烧录器而是产品自己掌握自己的命运。哪怕别人没有通过SWD入侵仅仅是想通过应用层的漏洞提取数据动态上锁也能打断他的后续操作。但这里也有一个坑程序一旦调用这个函数而且复位之前没有写好特殊标记后果就是板子从此以后不能再调试了。如果这是开发过程中的测试板你就等着全片擦除程序重来吧。所以动态上锁的代码一定要加条件编译只在正式版固件里启用开发版保持开放。3. 堵死SWD端口两种禁用方式的取舍3.1 方式A靠RDP硬件级封锁推荐在前面讲了RDP之后你应该能明白真正可靠地禁掉SWD不是把引脚配置成GPIO而是设置RDP Level 1或Level 2。RDP封锁是芯片硬件层面的行为跟你的单片机里跑什么代码没有关系。芯片一上电硬件就会自动检查RDP状态一旦检测到Level 1调试访问就被拦截Level 2更是直接把调试端口整个关掉。这里有个实操经验想分享很多人测试读保护效果时发现SWD还是“能连上”心里发虚。其实这不是保护失效而是调试器在握手阶段还是能识别芯片只有等你实际执行读内存、写Flash操作时才会被拒绝。判断保护是否生效最简单的办法是用烧录工具连接之后执行“读Flash”操作如果读取失败或者读到的全为空说明保护已经生效。3.2 方式B把SWD引脚复用成GPIO不推荐单独用网上有不少教程教你怎么“关闭SWD”方法是把PA13、PA14STM32F1上对应SWDIO、SWCLK复用为普通GPIO。代码也很简单GPIO_InitTypeDef gpio; gpio.Pin GPIO_PIN_13 | GPIO_PIN_14; gpio.Mode GPIO_MODE_ANALOG; HAL_GPIO_Init(GPIOA, gpio);但我要严正提醒这种方式只能让你的调试口“暂时失效”防不住真正想破解的人。为什么因为芯片复位后、用户程序还没跑起来的那个短暂窗口期SWD端口依然是默认的调试功能。破解者只要拉低复位引脚在复位释放的同时紧接着发起调试连接就能在用户代码运行之前抢到调试权限。这种“复位毛刺攻击”在嵌入式安全圈里是基本操作单纯靠复用引脚挡不住。另外把SWD引脚复用成GPIO意味着你自己也没法调试了。如果你的程序跑了几天之后出了bug需要查看现场结果发现没法连调试器只能靠日志输出一点点排查那种痛苦我不希望你来体验。所以我的结论是SWD复用引脚这个方案最多只能作为临时手段不能作为产品防护的主防线。真正要依赖的还是RDP的硬件封锁。3.3 为什么有人禁了SWD还是被破解这里说一个常见的认知落差。有人觉得“我禁了SWD所以固件安全了”这是错的。因为固件被破解的途径不止SWD一个ISP串口引导、应用层的串口/网络协议漏洞、外部Flash芯片数据泄露甚至直接通过JTAG边界扫描测试设备在某些测试模式下都有可能成为泄露口。SWD只是其中一条路你把路堵了别人还可以绕路。这也是为什么完整的防护方案必须组合拳SWD封锁、ISP封锁、数据加密、代码区域保护一起上。4. 封死ISP串口引导软硬件结合的方案4.1 硬件层面BOOT0引脚必须彻底趴地前面说了ISP模式的进入条件是BOOT01部分系列还要考虑BOOT1。那最直接的防御就是在硬件设计上让BOOT0永远处于低电平。常规做法是在BOOT0引脚上串联一个10k下拉电阻确保上电和复位时BOOT0被稳定拉低。注意这个电阻必须放在靠近引脚的地方而且下拉要足够强10k到100k都可以但不要用太大的阻值避免噪声干扰导致误判。更进一步如果你的产品PCB允许可以考虑把BOOT0的过孔或者焊盘隐藏起来比如放在BGA芯片底下、或者铺铜盖住增加别人飞线触碰的难度。我见过一个产品把BOOT0焊盘直接做成0欧电阻连接调试时焊上电阻可以进ISP出厂时把电阻去掉并涂上三防漆。这个思路比较实用量产和保密兼顾。4.2 软件层面选项字节强制主Flash启动在STM32F4等系列上BOOT0引脚的行为是可以被选项字节控制的。通过把nBOOT0这个选项位配置为1可以让芯片在选择启动方式时忽略BOOT0引脚的硬件电平强制从主Flash启动。这样即使别人把BOOT0引脚强行拉高芯片也不会进入ISP模式而是照常执行主Flash里的用户程序。配置nBOOT0的代码本质上还是写选项字节类似这样FLASH_OBProgramInitTypeDef bootConfig; bootConfig.OptionType OPTIONBYTE_USER; bootConfig.USERConfig OB_BOOT0_FROM_MAIN_FLASH; HAL_FLASH_OBProgram(bootConfig);需要说明的是不同系列的USER选项字节定义不一样一定要查对应型号的参考手册里关于BOOT配置的表格。比如有些系列是通过BOOT1或者nBOOT1来决定启动模式的逻辑不完全相同。我把这个功能列出来的目的是告诉你硬件拉低BOOT0不是唯一手段芯片给了你软件层面的控制权善用它能进一步提高防护强度。4.3 封锁ISP之后量产和售后怎么烧录这是很多人卡住的地方。你把SWD和ISP都封了工厂怎么烧固件售后怎么升级我的处理方式是这样权衡的生产阶段用烧录器烧录不封功能测试和校准之后再由专门的程序烧写进去设置RDP Level 1或者更高级别。RDP一旦设置为Level 1主Flash不可读调试连接也会拒绝访问但生产线上保留一个特殊工位用于需要返工时的全片擦除。售后升级走OTA或者IAP。具体来说在固件初始版本里内置一个IAP Bootloader通过约定的协议接收新的固件用HAL_FLASH_Program把新程序写入另一个Flash区域校验通过后跳转。这个通道完全在应用层控制不依赖SWD和ISP。这样既保证了产品正常生命周期内的可维护性又把外部的硬件调试通道彻底封死。5. 进阶组合防降级、防绕过、防拆芯片5.1 为什么“全片擦除”反而保护了你前面提到Level 1降到Level 0会触发全片擦除。这个机制我再说透一点它是硬件自动执行的不经过你的用户程序也不受外部调试器控制。也就是说破解者如果想通过“降级”的方式把RDP从Level 1改回Level 0芯片会先把整个主Flash区域清空然后才允许修改选项字节。这里有一个很微妙的点全片擦除对破解者来说是一种“杀敌一千自损八百”的行为。他想拿到的是你的代码但成功降级之后得到的是空片什么都没拿到。所以RDP Level 1的设计思路很清晰——保护的不是“代码永远不消失”而是“代码无法被完整读出”。哪怕最终被人擦掉了他也没得到任何可复用的东西。5.2 PCROP给核心算法单独上一把锁RDP保护的是整个主Flash的读取权限。但有些产品需求更精细我希望Flash大部分区域可以被读取比如为了日志导出、配置文件升级只有一小部分核心算法区域严防死守。这时候就轮到PCROPProprietary Code Read Out Protection上场了。PCROP允许你在Flash里指定一块区域设置后这块区域的内容既不能被调试器读取也不能被ISP读取连通过用户程序本身访问都受限制。它非常适合存放AES密钥、RSA私钥、自定义加密算法这类核心资产。配置方式同样是通过HAL_FLASH_OBProgram指定区域起始地址和结束地址然后调用带PCROP配置的选项字节写入函数。使用PCROP要注意的是这个区域一旦设置了保护固件里对这个区域的任何读取操作都会被硬件拦截返回零所以那些密钥数据一定要在受保护区域内但整个代码逻辑中又需要能访问到它。这里有个处理技巧可以在启动阶段把PCROP区域的数据拷贝到SRAM中然后程序后续就只跟SRAM里的副本打交道。这样既保证了静态存储时的安全性又不影响运行时的效率。5.3 固件校验与签名验证防止“改固件”式的二次入侵前面讲的是防止“读”还有一个维度是防止“改”。别人虽然没有读出你的代码但如果你预留了IAP升级通道而他逆向分析了通信协议伪造一个升级包把恶意固件刷进去后果一样严重。所以我在设计IAP方案时一定会加两层校验固件包内的CRC32校验和RSA签名验证。CRC32能防止传输损坏RSA签名能防止伪造升级包。升级流程是Bootloader收到完整固件包后先校验CRC再用内置公钥解签名确认升级包确实来自厂商才允许写入。签名验证通过之后调用HAL_FLASH_Program把数据写到应用区最后跳转执行。这里提醒一个细节签名校验用的公钥如果存放在主Flash里别人可以通过替换Bootloader的方式绕过签名验证。所以公钥要么放在PCROP保护区要么放在OTPOne-Time Programmable区域。哪怕Bootloader被篡改错误的公钥也会导致升级包验证失败从而保护整个升级链路。6. 实操避坑上锁过程常见问题和自救速查6.1 锁死后调试器连不上先分辨是哪一种“连不上”我遇到过不少新手设置读保护之后发现Keil报错整个人就慌了以为自己把芯片搞废了。其实大部分情况下芯片根本没废只是被“锁住”了。不同级别、不同工具的报错表现不一样我整理了一个速查表症状可能原因处理方案调试器完全找不到设备芯片已设置RDP Level 2无法挽救换新芯片能识别设备但读Flash失败/为空RDP Level 1生效属正常现象说明保护成功能连接但无法烧录烧录工具提示“Flash下载失败”RDP Level 1先用“全片擦除”功能恢复再烧录ISP连接超时无法握手芯片未进入ISP模式或RDP Level 1检查BOOT0电平、串口接线确认RDP状态升级后芯片无法运行启动方式配置错误或固件跳转地址错误检查选项字节BOOT配置、源程序启动地址6.2 不小心把自己锁死了怎么办万一你在开发阶段误触发了读保护而上锁的芯片里又存着还没备份出来的固件先别急着扔。绝大多数支持STM32的烧录工具都提供“解除保护”功能操作的本质就是对芯片执行全片擦除并清除RDP选项字节。代价是固件和所有数据都没了但芯片本身还能重新烧录。具体操作思路用调试器连接芯片在烧录软件里选择“Erase Chip”或者“Option Bytes”模式下的“Unprotect”软件会尝试将RDP写回Level 0。这个过程会触发芯片硬件全片擦除完成后芯片恢复为出厂状态重新烧录固件即可。如果是通过ISP模式解除注意BOOT0要拉到高电平让芯片进入系统存储器引导然后通过串口协议发送Mass Erase命令。同样数据全没了但芯片能救回来。6.3 量产顺序锁保护必须放在最后一步给产品做防护最忌讳的就是过早锁死。我见过一个团队在样机验证阶段就灌了RDP Level 1的代码结果后续开发中每次调试都要先全片擦除再重新烧录折腾到怀疑人生。合理的量产分工是这样的硬件贴片完成后先用烧录器下载Bootloader和应用程序进行功能测试、老化测试、参数校准这个阶段保持芯片完全开放所有测试通过后在产线最后一道工序执行“产品固化”操作内容包括烧写唯一的序列号、设置RDP Level 1或Level 2、配置启动选项字节固化完成后抽检几台验证一下调试口是否确实无法访问、ISP是否被堵死。这个流程经历了多轮的验证生产节拍的额外损耗也只有几秒钟但产品的安全级别完全是另一个层次。6.4 我实际操作中的几点体会做代码保护这些年踩过的坑不少说几个自己觉得有价值的经验。第一保护方案一定要分等级。不是所有产品都需要Level 2的“物理熔断”大多数产品做到Level 1就够了——防住了普通人也不想让自己售后无路可退。真正需要Level 2的产品一般是核心算法价值极高、且量产后不需要任何现场升级的才值得牺牲掉可维护性。第二别迷信“把所有口都封死”就是最安全。产品安全是一个系统工程SWD和ISP只是外部通道。如果你的系统里有外部Flash、有无线通信模块、有应用层软件漏洞一样可以泄露敏感信息。千万不要因为设了RDP就放松其他维度的安全设计。第三保存好每颗芯片唯一的标识信息。量产的时候把芯片的96位唯一ID、固件版本、烧录时间等数据记录到数据库里。后面如果发现某台设备被人逆向可以通过唯一ID追溯到批次和烧录环节这对追责和改进防护方案都非常有帮助。最后送给大家一个小技巧量产固化程序时写一个脚本通过命令行调用烧录工具把“烧录固件、写序列号、设置RDP、校验结果”这几个动作串起来一次性完成。手工在软件界面里点来点去容易出错尤其容易误选RDP Level 2导致整批芯片报废。脚本化之后这个环节的操作风险会降到最低。希望这篇内容能帮你在保护自己产品的同时少踩一些我当年踩过的坑。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →