前言

  本文使用的工程环境为:CLion、CubeMX、STM32G431
  一个量产产品,出厂时必定会关闭调试接口如SWD、JTAG(如提升RDP等级)以避免重要的程序泄露或被注入漏洞,但出厂后在它的全生命周期中仍有升级程序的需要,这就要求做好bootloader与OTA的工作。

  • BL:固件的完整性校验(CRC32、SHA256哈希),验权(RSA)、A/B分区切换
  • OTA:通过标准通信通道,如UART、BLE、CAN/LIN刷写固件

  仓库地址:https://github.com/Dikle-OvO/BootLoaderTest

基础知识

XIP机制

  XIP(eXecute In Place,就地执行),是指CPU可以直接在存储介质如FLASH中取指的技术,这种技术在MCU中比较常见,通用Linux系统(包括内核、BL)几乎不会用到XIP,必须搬运到RAM,除非使用squashfs,XIP模式内核直接挂载读取,否则都要使用ram(Linux的页缓存机制,题外话:写入也有相关的机制,这也是sync命令的作用)

FLASH零等待

  指MCU储存访问的理想状态,特指CPU访问储存介质时无需插入等待周期,通过预取指缓冲实现,接近RAM的运行水平,是XIP模式的天花板。
  反之,若 Flash XIP 是 1 等待,意味着每读取一次指令要多等 1 个时钟周期,480MHz 主频的实际等效性能会降到 240MHz。

NVS

  NVS(Non-Volatile Storage,非易失性存储),是嵌入式设备中专门用于存储 “小量、高频读写、掉电不丢失” 数据的模块,代码体积极小,核心逻辑仅几KB、RAM占用通常小于1KB,实现磨损均衡和数据校验。

1
NVS 本质是基于 Flash/EEPROM/OTP 等非易失介质的 “轻量级键值对存储系统”,把零散的小数据按 “键 - 值” 形式组织,统一管理擦写、磨损均衡、数据校验,让开发者像用 “嵌入式版 Redis” 一样操作非易失数据

固件合并

  使用这种模式会产生多个固件,包括Bootloader、App以及签名段,但实际下载不可能一个个分开烧录,可以使用python库hexmerge.py进行合并。

固件大小分布

  Flash = Code + RO + RW;RAM = RW + ZI +Stack + Heap

  • Code:表示程序所占用FLASH的大小
  • RO-data:即Read Only-data,表示程序定义的常量
  • RW-data:即ReadWrite-data,表示已被初始化的变量
  • ZI-data:即Zero Init-data,表示未被初始化的变量
    堆区(heap)就是动态内存分配出来的,比如new、malloc,栈用于临时变量(自动内存),静态分配在编译时就确定了位置(全局数组,ZI/RW),而堆则有分配失败的风险,只是商业上为了降本增效让内存进行分时复用。

  典型优化:

1
2
3
4
5
6
7
int data[500]={1,1,1,1…};

int data[500];
for(int i=0;i<500;i++)
{
data[i]=1;
}

无校验跳转

  先从最简单的无校验跳转开始,新建一个led闪烁函数,BL程序函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//1.定义APP地址
uint32_t APP_ADDRESS=0x08008000;
typedef void (*pFunction) (void);
void JumpToApplication(void) {
if (((*(__IO uint32_t*)APP_ADDRESS) & 0x2FFE0000) == 0x20000000) {
//2.将中断向量长重定向到应用程序地址
SCB->VTOR=APP_ADDRESS;
//3.获取应用程序的栈顶地址和复位处理函数地址
pFunction app_reset_handler =(pFunction) (*(__IO uint32_t*)(APP_ADDRESS + 4));
__set_MSP(*(__IO uint32_t*)APP_ADDRESS);
//4.跳转到应用程序
app_reset_handler();
}
}

  将BL程序烧录后,修改闪灯频率作为app以表示运行程序的不一样,而后在工程的ld链接文件中,修改Flash的位置为0x08008000(即上述BL配置的APP_ADDRESS),最后修改Keil或者CubeMX Programmer的烧录地址也为APP_ADDRESS即可。

解析

  CPU上电后默认从0地址启动,存储器重映射会根据boot选择将特定的地址映射过去,如用户空间0x0800 0000,BL程序只是判断了一下栈顶地址是否有效(在RAM空间中),有效即跳转,这是个非常简陋的校验。

完整性校验

  确保固件并未受到篡改。

CRC

  CRC循环冗余校验是一种基于多项式除法的哈希函数

SHA-256

  SHA-256是 SHA-2 家族的 256 位密码散列函数,由 NSA 设计、NIST 于 2001 年标准化(FIPS PUB 180-2),用于替代存在安全缺陷的 SHA-1,能将任意长度输入转为 256 位(32 字节,64 个十六进制字符)的消息摘要。

鉴权

  通常使用开源库mbedtls,这是嵌入式领域最主流的轻量级加密库,最小可裁剪到仅占 10KB Flash + 2KB RAM(仅保留 SHA-256+ECDSA);

RSA

  RSA非对称加密,核心原理是一对密钥:私钥和公钥两者在数学上存在关联但无法相互推导。RSA并不是对固件进行加密,而是验证完整性+合法性,在开发端

  1. 计算哈希:对 App 固件的二进制数据,用 SHA-256 计算出一个 32 字节的 “指纹”(哈希值)—— 哪怕固件改 1 个字节,哈希值会完全不同;
  2. RSA 签名:用厂商的 RSA 私钥(比如 2048 位)对这个哈希值做 “签名运算”(本质是用私钥加密哈希值),生成一个 256 字节的签名;
  3. 附加签名:把签名、哈希算法、密钥信息等封装成 mcuboot 的 TLV 段,附加到固件文件末尾;
  4. 输出固件:最终生成带签名的固件(比如app_signed.hex)

而在MCU端

  1. 读取固件和签名:PBL 从 Flash 的 App 分区读取固件数据,同时读取固件末尾的 TLV 段(包含 RSA 签名);
  2. 重新计算哈希:PBL 用和厂商侧相同的 SHA-256 算法,重新计算 App 固件的哈希值;
  3. 加载公钥:PBL 从自身的 Flash 只读分区(不可篡改)读取预烧录的 RSA 公钥(比如编译时固化到 PBL 代码中);
  4. RSA 验签:调用 mbedtls 的mbedtls_rsa_verify()接口,用公钥解密 “签名”,得到厂商侧的原始哈希值;
  5. 对比哈希:把 “解密得到的厂商哈希值” 和 “PBL 重新计算的哈希值” 对比:
    一致:说明固件没被篡改,且是厂商签名的合法固件 → 启动 App;
    不一致:说明固件被篡改,或不是厂商签名的 → 拒绝启动。

    综合

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    static uint8_t sign_data[256];

    bool ota_check_download_signature(void)
    {
    int ret = 0;
    bool success = true;
    uint8_t hash[32] = {0};
    const uint8_t cnt = ota_ctx.segment_count;

    for (uint8_t i = 0; i < cnt; ++i) {
    if (BLOCK_TYPE_APP != ota_ctx.data_segments[i].belong_block_type) {
    continue;
    }

    uint32_t addr = ota_get_phy_addr(i);
    ret = mbedtls_sha256((const unsigned char *)addr, ota_ctx.data_segments[i].memory_size, hash, 0);
    if (0 != ret) {
    LOG_ERR("ota data-segment sha256 failed: %d, addr=0x%x, size=0x%x", ret,
    ota_ctx.data_segments[i].memory_addr, ota_ctx.data_segments[i].memory_size);
    success = false;
    break;
    }

    /* check signature block number is valid */
    if (i >= ota_ctx.sign_count) {
    LOG_ERR("ota sign block count failed: %d < %d", i, ota_ctx.sign_count);
    success = false;
    break;
    }

    if (!rsa_signature_verify(ota_ctx.signatures[i], SIGNATURE_DATA_SIZE, hash, sizeof(hash))) {
    LOG_ERR("ota data-segment sign failed: addr=0x%x, size=0x%x",
    ota_ctx.data_segments[i].memory_addr, ota_ctx.data_segments[i].memory_size);
    success = false;
    break;
    }
    }

    return success;
    }