CH55x 微处理器固件提取器
CH55x 固件提取器用于从 CH55x 集成电路中读取固件。这些器件本身并不支持通过引导加载程序直接读取固件。然而,它提供了一种功能,可以一次 8 字节地将固件内容与提供的数据进行验证。该功能容易受到教科书式的时间攻击,这正是本文所利用的方法,从而允许从这些设备中提取固件。引导加载程序可以通过 USB 或 UART 访问。此固件提取器仅适用于 UART,因为 USB 的较高延迟会使这种攻击变得困难。已测试的芯片为 CH552 和 CH554,引导加载程序版本为 2.4 和 2.5。固件提取器的硬件方案基于 STM32 Blue Pill,因为这些板子容易获得、价格低廉且性能足够。
引导加载程序已事先从设备中读取出来,并且其通信协议已被逆向工程。下面是一个大致正确的验证命令以及引导加载程序中使用的验证函数。存在一些防护措施:要求长度为 8 的倍数、地址对齐到 8 字节边界、地址小于 0x3800,并且之前没有验证错误。最后一个防护措施意味着每次验证失败后都需要重启 CH55x。
我们可以看到,验证函数在验证到某一字节失败时会立即返回。这意味着正确的字节越多,验证函数执行的时间就越长。这是教科书式的时间攻击示例,可以被利用。
unsigned char verifycmd[] = {
// 0x57, 0xab, // 未包含在验证函数中的 UART 魔数
0xa6, // 验证命令
5 + len, // 常数 5 加上待验证数据的长度
0, // 未使用
addr_low, // 地址低字节
addr_high, // 地址高字节
0, 0, 0, // 未使用
0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, // 用于验证的数据
checksum
}
unsigned char verify(unsignec char *cmdbuffer)
{
static char prev_verify_error;
unsigned char len = cmdbuffer[1]-5
unsigned short addr = cmdbuffer[3] + cmdbuffer[4] << 8;
if (len & 0x07 || addr & 0x07 || addr > 0x3800 || prev_verify_error) {
return 0xfe;
}
for (int i=0; i < len; i++) {
// 密钥可以通过引导加载程序设置,CBYTE[] 表示代码存储器
if(key[i & 0x07] ^ cmdbuffer[8+i] ^ CBYTE[addr+i]) {
prev_verify_error = 1;
return 0xf5;
}
}
return 0;
}
通过反复试验,我们发现每个正确字节会使验证函数的执行时间延长大约 4.2 µs。引导加载程序用于 UART 通信的波特率为 57600(无论你在别处读到什么),这意味着传输一个比特大约需要 17.4 µs。这两个时间之间的关系很重要,因为回复似乎也存在大约 17 µs 的抖动。我们试图区分验证函数响应时间相差 4.2 µs 的回复,但由于 UART 抖动(时钟定时),它们可能相差多达 17 µs。这似乎是一项艰巨的任务,但通过统计手段是可以实现的。
我们可以通过多次尝试验证一个字节并记录结果来确定该字节是否正确。例如,假设第一个字节错误时收到回复的最短可能时间为 30 µs。那么考虑到 UART 抖动,第一个字节无效时的最长回复时间预计为 30 µs + 17.4 µs = 47.4 µs。同时,如果第一个字节有效而第二个字节无效,则会将该“范围”推至 34.2 µs 到 51.6 µs。加上一些余量,我们可以得出结论:如果回复时间小于约 33 µs,则第一个字符无效。类似地,如果回复时间大于 48 µs,则第一个字符有效。这就是用于提取固件的时间攻击的基础。
这不是一个完全自动化的固件提取工具——需要修改源代码并重新编译(使用 VS Code 搭配 PlatformIO)。主要原因在于每个字节的精确时序特征因设置而异,需要进行调整。调整过程可以实现自动化,但这并非本项目的目的。主要的调整通过 prober_limits 变量完成。例如,prober_limits[0] 包含待验证的 8 个字节中第 0 个字节的限制条件。如果响应时间低于 .invalid_under_time,我们就知道该字节无效。如果高于 .valid_over_time,我们就知道该字节正确。还有一个 .min_delta 参数,允许在确定字节是否正确之前提前进入下一个字节的探测。
struct ProberByteLimits prober_limits[8] = {
{
// 字节 0
.invalid_under_time = 33,
.valid_over_time = 50,
.min_delta = 30
}, // ...
为了找到合适的值,建议将 .invalid_under_time 设为 0,.valid_over_time 设为较大的值(例如 100),.min_delta 保持为 30。这意味着探测程序将无法找到第一个正确的字节,但进度会通过主机 PC 的 UART 打印出来。你将会看到类似以下内容:
[0x0000]=0x01? min=31 max=47 tries=63
min=31 max=48 tries=127
min=31 max=48 tries=191
min=30 max=48 tries=255
min=30 max=48 tries=319
假设测试的第一个字符无效,我们可以使用这些最小/最大值,并偏移 1 或 2 来确定合适的限制值。例如,上面我们可以将 .invalid_under_time 设为 33,.valid_over_time 设为 50。探测程序随后会尝试不同的值,直到找到有效的值,如下所示:
...
[0x0000]=0x7d? min=31 max=39 tries= 7
[0x0000]=0x02? min=40 max=56 tries= 7
[0x0001]=0x01? min=35 max=52 tries=34
我们看到地址 0x0000 的最后一次尝试的最大响应时间为 56 µs,这意味着这是正确的字节。探测程序随后前进到下一个字节,并继续该过程。请注意,所有 8 个字节的限制条件必须单独调整,但一旦完成,它将适用于整个存储器。当找到 8 个正确的字节后,它会以 ihex 格式打印出来:
:0800000002002932ffffffff9f
将 UART 输出记录到文本文件中,可以通过 grep 搜索 ^: 来以 ihex 格式提取完整的存储器内容。
用于固件提取的示例电路如下所示。两个晶体管使得 Blue Pill 可以关闭 CH55x 的电源。这比仅使用软件复位更可取,因为软件复位仅在 CH55x 处于引导加载程序模式时有效(并且引导加载程序有一个超时,超时后会启动应用程序代码)。UART 到 CH55x 之间的电阻是因为我怀疑 CH55x 的内部上拉电阻可能会在电源关闭时通过 UART 反向供电。V33 与 P3.6 之间的 10k 电阻用于将 CH55x 置于引导加载程序模式。在 Blue Pill 上,PA11 连接到 UART RX 引脚,以便能够使用定时器 1 对响应进行计时。

通过调整并使用此工具,可以提取 CH55X 器件的固件。提取过程并不快,但可以在一天或两天内提取 14 kB 的固件。这在一定程度上取决于调整效果以及所使用的频率表与实际固件汇编的匹配程度。不幸的是,源代码有些混乱。我制作这个工具是因为我需要从 CH554 器件中提取固件,既然已经得到了它,就没有真正的理由再去完善这个工具本身了。尽管如此,如果有人需要从 CH55x 器件中提取固件,这个工具应该会很有用。