🎀 文章作者:二土电子
🌸 关注公众号获取更多资料!
🐸 期待大家一起学习交流!
文章目录
相信大家对于为什么要防止烧录文件被读取应该是比较清除的,但是这里还是想先罗嗦几句,让大家能更深刻地体会一下防止烧录文件被窃取的重要性。
试想一下,你辛苦编写调试了很久,最后终于设计完成了一个非常满意的程序,你兴致勃勃地正准备开始大卖产品,结果刚卖出去几个就发现市场上已经出现了无论是UI还是功能都和你完全一样的东西……
一、防止烧录文件被窃取使用的几种方法
首先我们先来介绍一下本文下面要介绍的计中防止烧录文件被窃取使用的方法
- 禁用下载调试引脚;
- 启用读保护;
- UID加密
当然除了上面的三种方法,还有很多,博主这里只介绍博主亲自实践测试过的方法,下面我们一起以STM32F103C8T6芯片为例,介绍一下防止自己的可烧录文件被窃取使用的几种方法和实现思路。
二、方法详细介绍
2.1 禁用下载调试引脚
首先我们来介绍一下第一种方法——禁用下载调试引脚。用下载调试引脚是亿中非常基础的一种保护方法,我们来介绍一下实现方法
我们先来看一下芯片手册中列举出来的几个下载调试引脚
在标准库中给出了禁用和启用下载调试引脚的方法,我们只需要使能AFIO时钟,之后利用库函数设置一下要禁用或者启用的功能即可,共有一下几种选择
GPIO_Remap_SWJ_NoJTRST // 完全SWJ(恢复引脚的默认功能)
GPIO_Remap_SWJ_JTAGDisable // 关闭JTAG,启用SW-DP
GPIO_Remap_SWJ_Disable // 关闭JTAG-DP,关闭SW-DP
具体的配置方法如下
// 开启AFIO时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);
// 关闭JTAG-DP,关闭SW-DP
GPIO_PinRemapConfig(GPIO_Remap_SWJ_Disable,ENABLE);
如此一来,下载和调试引脚就会被禁止,后续将无法通过下载器下载程序,所以在配置关闭下载功能的时候一定要确保可以通过某些操作恢复下载和调试功能,比如发送一些特定指令,进行某种特定操作等等,否则后续将无法下载调试,当我们在Keil中点击下载按钮时会提示没找到目标
或者使用STM32 ST-LINK Utility连接,也会出现错误提示
如果禁用了下载调试功能,而且程序中没有留出可以重新使能的方法怎么办?
如果我们不小心在程序中禁用了下载和调试功能,但是我们想要下载程序,这是我们可以将ST-Link插好,按住复位按键,在点击下载的瞬间松开复位按键,是有可能拯救回来的,如果失败可以多尝试几次。
2.2 启用读保护
除了最基本的禁用下载调试引脚之外,还可以启用读保护,启用读保护之后我们的Flash将不允许被读取进一步防止我们的可烧录文件被窃取,如果想要关闭读保护,芯片会自动将Flash中的全部内容清除,从而保护我们的程序不会被读取。库函数中提供了可以启用读保护的方法
FLASH_Unlock(); // 解锁FLASH控制寄存器
FLASH_ReadOutProtection(ENABLE); // 启用读保护
FLASH_Lock(); // 锁定FLASH控制寄存器
博主测试了一下,开启读保护之后,使用ST-Link下载调试也无法正常使用,
在Keil中点击下载按钮会弹出下面的错误提示
或者使用STM32 ST-LINK Utility连接,也会出现警告提示
回到和上面类似的情况,如果我们开启了读保护功能,想要解除读保护,但是程序中没有留出解除的方法,该怎么办?
很简单,我们打开STM32 ST-LINK Utility
,点击“Target”中的“Connect”,先连接芯片,连接成功时虽然会弹出一个提示窗口,提示“无法读取内存,请禁用读取保护并重试”,但是我们可以通过查看右上侧的设备信息中是否显示了设备信息来判断是否连接成功
连接成功之后,我们还是点击“Target”,找到“Option bytes…”点进去,关闭读保护之后点击应用,如下图所示
如此一来,我们就关闭了读保护,但是同时芯片的Flash内容也被全部清除,需要重新下载程序,当然也可以在程序中增加解除读保护的代码。
2.3 UID加密
我们上面介绍的两种方法都是介绍如何保护我们的烧录文件被读取,俗话说道高一尺,魔高一丈,即使用再多的方法来防止我们的烧录文件被读取,也会有专业人士能够通过某些方法拿到你的烧录文件,那么最后,让我们来介绍一种防止我们的烧录文件被别人使用的方法,即使别人拿到了你的烧录文件,也无法在自己的芯片上运行。
众所周知,每一个芯片都有一个唯一的ID,我们可以利用这个唯一的UID做文章,也就相当于给我们的烧录文件加上了一个独一无二的ID,一个芯片绑定一个烧录文件,一旦烧录文件被烧录到别的芯片,程序就无法正常运行,或是直接出发异常中断,或是直接死循环。比较常见的方法是基于芯片的UID,使用哈希算法计算出一个密钥,实现UID加密。
三、程序设计
方法和思路上面都已经介绍完了,下面我们来介绍如何编写对应的程序,不仅仅是实现,还要做到方便移植何使用,下面我们来看看实现思路和程序设计。
3.1 下载和调试引脚控制
首先是下载和调试引脚控制函数,能够控制下载和调试功能的开启和关闭
/*
*==============================================================================
*函数名称:Encrypt_SetProgram
*函数功能:配置程序下载功能
*输入参数:ENABLE:开启;DISABLE:关闭
*返回值:无
*备 注:无
*==============================================================================
*/
void Encrypt_SetSetProgram (u8 state)
{
switch (state)
{
case ENABLE:
// 禁用JTAG,启用SWD功能
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE); // 开启AFIO时钟
GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable,ENABLE); // 禁用JTAG,启用SWD功能
break;
case DISABLE:
// 禁用JTAG,禁用SWD功能
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE); // 开启AFIO时钟
GPIO_PinRemapConfig(GPIO_Remap_SWJ_Disable,ENABLE); // 关闭JTAG-DP,关闭SW-DP
break;
}
}
3.2 读保护控制
下面是读保护控制函数,能够控制读保护功能的开启和关闭
/*
*==============================================================================
*函数名称:Encrypt_SetProtect
*函数功能:配置读保护
*输入参数:ENABLE:开启;DISABLE:关闭
*返回值:无
*备 注:无
*==============================================================================
*/
void Encrypt_SetProtect (u8 state)
{
switch (state)
{
case ENABLE:
if (FLASH_GetReadOutProtectionStatus() != SET)
{
FLASH_Unlock(); // 解锁FLASH控制寄存器
FLASH_ReadOutProtection(ENABLE); // 启用读保护
FLASH_Lock(); // 锁定FLASH控制寄存器
}
break;
case DISABLE:
if (FLASH_GetReadOutProtectionStatus() != RESET)
{
FLASH_Unlock(); // 解锁FLASH控制寄存器
FLASH_ReadOutProtection(DISABLE); // 禁用读保护
FLASH_Lock(); // 锁定FLASH控制寄存器
}
break;
}
}
3.3 UID加密
最后我们介绍UID加密的程序实现,我们这里并没有使用哈希算法,而是在获取到芯片的UID之后,使用CRC16算法,基于芯片的UID计算出两字节的校验码,以此来作为密钥,在第一次烧录程序时将计算出的密钥存储到一个固定的位置,后续使用过程中会根据当前芯片的UID重新计算一个密钥,将新计算的密钥与从对应位置读取出来的密钥作比较,如果密钥不同,则不会正常运行程序,计算密钥的参考函数如下
/*
*==============================================================================
*函数名称:Encrypt_KeyCalculate
*函数功能:根据UID计算密钥
*输入参数:无
*返回值:计算出的密钥值
*备 注:无
*==============================================================================
*/
u16 Encrypt_KeyCalculate (void)
{
u16 key = 0;
u8 uid[12] = {0};
Encrypt_GetUID(uid); // 读取UID
key = Encrypt_Crc16Calculate(uid,12); // 计算密钥值
return key;
}
四、使用方法
最后我们来介绍一下上面的一些函数的使用方法,仅供参考,考虑到我们在开发阶段调试程序时如果开启了上面的功能不利于我们的调试工作,所以博主的思路是使用条件编译,在需要开启加密功能时开启,调试阶段关闭加密功能,实现方法如下
#define ENCTYPTENFLAG 0x01U // 加密启用控制标志位,0:不启用加密,1:启用加密,在调试时不要启用加密功能,出货时必须启用
#define KEYSAVEFLAG 0x00U // 密钥计算存储标志位,0:不计算存储,1:计算存储,在第一次下载程序时要开启计算存储一下,后续关闭
#define DISSWDFLAG 0x01U // 禁用下载引脚标志位,0:不禁用;1:禁用,必须启用加密之后才有效
#define ENPROTECT 0x01U // 开启读保护标志位,0:不开启;1:开启,必须启用加密之后才有效
#define KEYADDR 0x0801FFF0 // 密钥存储地址
在初始化系统前加上这段代码
#if ENCTYPTENFLAG // 判断加密是否启用
u16 calculateKey = 0;
u16 readKey = 0;
#endif
delay_init(); //延时函数初始化
#if ENCTYPTENFLAG // 判断加密是否启用
calculateKey = Encrypt_KeyCalculate(); // 计算密钥
#if KEYSAVEFLAG // 判断是否需要存储
Med_Flash_Write(KEYADDR,&calculateKey,1); // 将密钥存储到Flash
delay_ms(10);
#endif
Med_Flash_Read(KEYADDR,&readKey,1); // 读取之前存储的密钥
delay_ms(10);
if (readKey != calculateKey)
{
while(1); // 校验失败
}
#if DISSWDFLAG // 判断是否禁用下载引脚
Encrypt_SetSetProgram(DISABLE); // 关闭程序下载功能
#endif
#if ENPROTECT // 判断是否开启读保护
Encrypt_SetProtect(ENABLE); // 开启读保护
#endif
#endif
博主在程序中增加了使用通信方式启用下载调试功能和关闭读保护的代码,如果你像博主这么设计,那么需要注意在调试阶段关闭整个加密的宏,产品设计完成后先开启加密宏和密钥存储的宏,下载程序,之后关闭密钥存储的宏,开启禁用下载和启用读保护的宏
,如此完成出货前的加密操作。
五、注意事项
即使是使用程序中预留的关闭读保护代码关闭了读保护,芯片也会对整个Flash进行擦除,因此之前存储的密钥也会被擦除,所以无论是通过STM32 ST-LINK Utility关闭了读保护还是通过预留代码关闭了读保护,后续都需要重新按照第四节介绍的操作重新操作一遍。