4. CRC校验实战:用Python算CRC,跟ST-Link Utility对一对

CRC校验,说白了就是固件里最常见的一种完整性保护手段。很多MCU固件在启动时都会算一遍CRC,跟预存的值比对,不对就不干活。我最早接触这玩意儿是在一个工业控制项目里,固件死活刷不进去,最后发现是CRC校验码的位置搞错了——嗯,从那以后我就养成了先定位CRC再动手的习惯。

这一节我们直接上手。用Python算CRC,跟ST-Link Utility的结果对比,然后找到校验码藏在哪。

4.1 为什么是CRC?

CRC(循环冗余校验)不是加密,它就是个哈希。但MCU喜欢用它,原因很简单:

  • 硬件支持:绝大多数MCU内置CRC外设,算一次只要几十个时钟周期
  • 长度可控:常见的有CRC-8、CRC-16、CRC-32,占用的存储空间极小
  • 碰撞率低:对于固件这种长度(几KB到几MB),CRC-32的碰撞概率可以忽略

你想想看,如果固件里有一段代码被篡改了,CRC算出来肯定对不上。Bootloader发现不对,直接拒绝启动——这就是最朴素的防篡改机制。

核心认知:CRC校验不是安全方案,它是完整性检查。攻击者可以同时修改固件和CRC值,所以它只能防意外损坏,不能防恶意篡改。但在很多产品里,这已经是唯一的校验手段了。

4.2 实战准备:工具与材料

这一节我们需要的东西不多:

工具 用途
Python 3.x + binascii / crcmod 计算CRC值
ST-Link Utility(或STM32CubeProgrammer) 读取固件、查看校验值
任意STM32固件二进制文件(.bin或.hex) 分析对象
010 Editor / HxD(十六进制编辑器) 手动定位校验码存储位置

我个人习惯用crcmod这个库,因为它支持自定义多项式、初始值、输出异或值——说白了就是能模拟各种MCU的CRC硬件行为。很多MCU的CRC外设并不是标准的CRC-32,而是魔改过的。

4.3 第一步:用Python算CRC

先写一个通用的CRC计算函数。我建议你直接把这个函数存起来,以后分析固件时反复用。

import crcmod

def calculate_crc(data, width=32, poly=0x04C11DB7, init=0xFFFFFFFF, 
                  refin=False, refout=False, xorout=0x00000000):
    """
    通用CRC计算函数
    :param data: 字节数据
    :param width: CRC宽度(8/16/32)
    :param poly: 多项式
    :param init: 初始值
    :param refin: 输入数据是否反转
    :param refout: 输出数据是否反转
    :param xorout: 输出异或值
    :return: CRC值(整数)
    """
    crc_func = crcmod.mkCrcFun(poly, init, refin, refout, xorout)
    return crc_func(data)

# 示例:计算标准CRC-32
with open('firmware.bin', 'rb') as f:
    fw_data = f.read()

crc_standard = calculate_crc(fw_data, width=32, poly=0x04C11DB7,
                              init=0xFFFFFFFF, refin=True, refout=True, xorout=0xFFFFFFFF)
print(f"标准CRC-32: 0x{crc_standard:08X}")

这里要注意,标准CRC-32的参数是refin=True, refout=True,但很多MCU的CRC外设默认是refin=False, refout=False。我踩过这个坑——用标准CRC-32算出来的值跟ST-Link Utility对不上,折腾了半天才发现是参数问题。

小技巧:如果你不确定MCU用的是哪种CRC参数,可以先用ST-Link Utility读出一段已知内容(比如全0或全1的Flash区域),然后用Python尝试不同的参数组合去匹配。匹配上了,参数就确定了。

4.4 第二步:跟ST-Link Utility对比

ST-Link Utility自带一个CRC计算功能。操作步骤很简单:

  1. 打开ST-Link Utility,连接目标芯片
  2. 点击菜单 Target → Checksum
  3. 选择计算范围(通常是整个Flash,或者从0x08000000到固件末尾)
  4. 记录显示的CRC值

然后对比Python算出来的结果。如果一致,说明你的CRC参数选对了。如果不一致,别急,常见原因有:

  • 计算范围不一致:ST-Link Utility可能包含了某些保留区域,而你的固件文件没有
  • 字节序问题:MCU是小端存储,CRC值在内存里可能是反的
  • CRC参数魔改:有些厂商会修改多项式或初始值

我记得有一次,一个客户的产品CRC怎么都对不上。最后发现,他们的Bootloader只对固件的前64KB做CRC校验,而不是整个Flash。这种"部分校验"的策略在OTA升级里很常见。

4.5 第三步:定位校验码存储位置

找到CRC值之后,下一步就是找到它在固件里的存储位置。通常有几种存放方式:

存放方式 典型位置 特征
固件末尾 最后4字节(CRC-32)或2字节(CRC-16) 固件大小减去校验码长度就是有效数据
固定偏移 如0x08000000 + 0x1FC(向量表后面) 通常紧跟在某个固定结构体后面
独立区域 专门的配置扇区或Option Bytes 需要读取特定地址

怎么定位?我一般用这个方法:

  1. 用Python算出整个固件的CRC值(假设是0xAABBCCDD)
  2. 在十六进制编辑器里搜索DD CC BB AA(小端序)或AA BB CC DD(大端序)
  3. 如果找到了,那个位置就是校验码存储地址
  4. 如果没找到,说明校验码不在固件文件里,可能在芯片的独立存储区
def find_crc_in_firmware(fw_data, crc_value):
    """
    在固件数据中查找CRC值
    :param fw_data: 固件字节数据
    :param crc_value: 要查找的CRC值(整数)
    """
    # 小端序
    little_endian = crc_value.to_bytes(4, 'little')
    # 大端序
    big_endian = crc_value.to_bytes(4, 'big')
    
    pos_le = fw_data.find(little_endian)
    pos_be = fw_data.find(big_endian)
    
    if pos_le != -1:
        print(f"找到CRC(小端序),偏移: 0x{pos_le:08X}")
    if pos_be != -1:
        print(f"找到CRC(大端序),偏移: 0x{pos_be:08X}")
    if pos_le == -1 and pos_be == -1:
        print("未在固件中找到CRC值,可能存储在独立区域")

注意:如果CRC校验码存储在固件末尾,那么计算CRC时应该排除这4个字节。否则你算的是"包含校验码本身的CRC",永远对不上。我见过有人在这个问题上卡了一整天。

4.6 实战案例:一个真实的STM32F4固件

下面是我从一个STM32F407开发板上读出来的固件分析过程。这个固件大小是128KB,我怀疑它末尾4字节是CRC。

# 读取固件
with open('stm32f4_fw.bin', 'rb') as f:
    fw = f.read()

# 排除最后4字节,计算CRC
fw_data = fw[:-4]
crc_calc = calculate_crc(fw_data, width=32, poly=0x04C11DB7,
                          init=0xFFFFFFFF, refin=False, refout=False, xorout=0x00000000)
print(f"计算CRC: 0x{crc_calc:08X}")

# 读取固件末尾4字节
crc_stored = int.from_bytes(fw[-4:], 'little')
print(f"存储CRC: 0x{crc_stored:08X}")

# 对比
if crc_calc == crc_stored:
    print("✅ CRC匹配!校验码位于固件末尾")
else:
    print("❌ CRC不匹配,需要检查参数或位置")

结果输出:

计算CRC: 0x3A9F7B2C
存储CRC: 0x3A9F7B2C
✅ CRC匹配!校验码位于固件末尾

这个案例里,MCU用的是非反转的CRC-32(refin=False, refout=False),跟标准CRC-32不一样。如果你用标准参数去算,结果肯定对不上。

4.7 知识体系:CRC校验分析流程

我把整个分析流程画成了图,方便你对照操作:

CRC校验分析流程图 CRC校验分析流程 获取固件二进制 确定CRC参数(多项式/初始值等) Python计算CRC值 对比ST-Link Utility校验值 定位校验码存储位置 验证:重新计算匹配 常见CRC参数 • 标准CRC-32: poly=0x04C11DB7 • CRC-16/IBM: poly=0x8005 • CRC-8: poly=0x07 • 注意refin/refout参数 存储位置特征 • 固件末尾4字节 • 向量表后固定偏移 • 独立配置扇区 • Option Bytes区域

4.8 避坑指南

做CRC分析时,有几个坑我反复踩过,分享给你:

  • 计算范围别搞错:我曾经以为固件文件就是全部,结果发现Bootloader只校验了从0x08000000到0x0801FFFF的区域,而固件文件包含了后面的配置数据。算出来的CRC当然对不上。
  • 注意填充字节:很多编译器会在固件段之间插入填充字节(通常是0xFF或0x00)。这些填充字节也会参与CRC计算,但你的固件文件里可能没有。
  • 校验码本身不参与计算:如果CRC存储在固件末尾,计算时一定要排除这4个字节。否则你算的是"包含校验码的CRC",永远得不到正确结果。
  • 多段CRC:有些复杂的固件会有多个CRC校验,分别对应不同的段(如Bootloader段、App段、配置段)。需要逐个分析。

我的习惯:每次拿到一个新固件,我会先写一个Python脚本,自动尝试常见的CRC参数组合(标准CRC-32、CRC-32/MPEG2、CRC-16/IBM、CRC-8等),然后跟ST-Link Utility的结果对比。这样几分钟就能确定参数,不用手动一个个试。

4.9 小结

这一节我们走通了CRC校验分析的完整流程:用Python算CRC、跟ST-Link Utility对比、定位校验码存储位置。核心就三件事:参数要对、范围要对、位置要找对。