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计算功能。操作步骤很简单:
- 打开ST-Link Utility,连接目标芯片
- 点击菜单 Target → Checksum
- 选择计算范围(通常是整个Flash,或者从0x08000000到固件末尾)
- 记录显示的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 | 需要读取特定地址 |
怎么定位?我一般用这个方法:
- 用Python算出整个固件的CRC值(假设是
0xAABBCCDD) - 在十六进制编辑器里搜索
DD CC BB AA(小端序)或AA BB CC DD(大端序) - 如果找到了,那个位置就是校验码存储地址
- 如果没找到,说明校验码不在固件文件里,可能在芯片的独立存储区
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校验分析流程
我把整个分析流程画成了图,方便你对照操作:
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对比、定位校验码存储位置。核心就三件事:参数要对、范围要对、位置要找对。