rva3/CVE-2026-40003
GitHub: rva3/CVE-2026-40003
针对 ZXIC/Sanechips ZX297520V3 SoC BootROM 任意内存写入漏洞(CVE-2026-40003)的 PoC 利用代码,通过 USB 下载模式绕过安全启动实现任意代码执行。
Stars: 11 | Forks: 1
CVE-2026-40003(又名 Joselito)是在 ZXIC/Sanechips ZX297520V3 SoC BootROM 中发现的任意内存写入漏洞。
## 状态
该漏洞应已于 2026 年 3 月在较新的 SoC 版本中修复。目前尚无已知产品使用了打过补丁的芯片。所有现有设备均受此漏洞影响。
## 工作原理
*TL;DR: 查看 [加载器代码](./loader/src/main.rs)。*
BootROM 从 0x4 地址开始执行,此时栈指针位于 `0x81FF0`。代码根据特定的测试点,或从闪存加载镜像的能力来执行模式选择。如果 BootROM 无法加载或验证闪存上的镜像,它将回退到 USB 下载模式。
我们可以在这里观察到此行为:
```
v0 = MEMORY[0x13B004] & 7; // hardware pin
switch ( (char)v0 )
{
case 0:
if ( nand_init() )
goto LABEL_12;
parse_header((Header *)0x8A000);
v4 = "\nMODE:NAND\n";
goto LABEL_11;
case 1:
if ( !nand_init() )
{
parse_header((Header *)0x8A000);
uart_puts("\nMODE:USB-N\n");
}
usbdl_init(0x1500000);
v5 = "TURN TO NAND\n";
goto LABEL_16;
/* ... */
LABEL_16:
uart_puts(v5);
LABEL_7:
verify_and_jump((Header *)0x8A000, 0, 1);
break;
```
`usbdl_init` 函数初始化 USB IP 并进入下载循环:
```
void __fastcall usbdl_init(int usb_base)
{
char *v1; // r0
if ( usb_base == 0x1500000 )
{
alt_usb_base = 0;
usb_init();
}
else
{
alt_usb_base = 1;
noidea();
}
if ( usb_setup() )
{
try_sync_with_host();
v1 = "FAILED\n";
}
else
{
v1 = "\nNOLINK\n";
}
uart_puts(v1);
}
```
反汇编输出:
```
ROM:00001F68 ; void __fastcall usbdl_init(int usb_base)
ROM:00001F68 usbdl_init ; CODE XREF: entry+5E↑p
ROM:00001F68 ; entry+7C↑p ...
ROM:00001F68 PUSH {R4,LR}
ROM:00001F6A MOVS R2, #0x1500000
ROM:00001F6E LDR R1, =dword_8014C
ROM:00001F70 MOV R4, R0
ROM:00001F72 CMP R0, R2
ROM:00001F74 BNE loc_1F80
ROM:00001F76 MOVS R0, #0
ROM:00001F78 STRB R0, [R1,#(alt_usb_base - 0x8014C)]
ROM:00001F7A BL usb_init
ROM:00001F7E B loc_1F88
ROM:00001F80 ; ---------------------------------------------------------------------------
ROM:00001F80
ROM:00001F80 loc_1F80 ; CODE XREF: usbdl_init+C↑j
ROM:00001F80 MOVS R0, #1
ROM:00001F82 STRB R0, [R1,#(alt_usb_base - 0x8014C)]
ROM:00001F84 BL noidea
ROM:00001F88
ROM:00001F88 loc_1F88 ; CODE XREF: usbdl_init+16↑j
ROM:00001F88 MOV R0, R4
ROM:00001F8A BL usb_setup
ROM:00001F8E CMP R0, #0
ROM:00001F90 BEQ loc_1F9E
ROM:00001F92 BL try_sync_with_host
ROM:00001F96 ADR R0, aFailed ; "FAILED\n"
ROM:00001F98
ROM:00001F98 loc_1F98 ; CODE XREF: usbdl_init+38↓j
ROM:00001F98 BL uart_puts
ROM:00001F9C POP {R4,PC}
ROM:00001F9E ; ---------------------------------------------------------------------------
ROM:00001F9E
ROM:00001F9E loc_1F9E ; CODE XREF: usbdl_init+28↑j
ROM:00001F9E ADR R0, aNolink ; "\nNOLINK\n"
ROM:00001FA0 B loc_1F98
ROM:00001FA0 ; End of function usbdl_init
```
`try_sync_with_host` 等待同步字节(`0x5A`)并进入主循环。
```
void try_sync_with_host()
{
int v0; // r4
unsigned int i; // r0
int v2; // r5
int v3; // r2
int v4; // r3
int v5; // r2
int v6; // r3
int v7; // r0
v0 = 0x805A0;
time_to_boot = 0;
for ( i = 0; i < 0x40; ++i )
*(_BYTE *)(i + 0x805A0) = 0;
g_state = 0;
v2 = 0x200;
if ( usb_recv(byte_805A0, 0x200, 0x5A) == 1 ) // 0x5a = ack sent by the host
{
uart_puts("TIMEOUT\n");
}
else
{
resp[0] = 0xA5;
usb_send((int)resp, 1, v3, v4);
g_state = 1;
while ( !time_to_boot )
{
v7 = sub_11CA(v0, v2, v5, v6);
handler(v0, v7);
if ( g_state == 4 )
{
v2 = size;
v0 = start_addr;
}
else
{
v0 = 0x805A0;
v2 = 0x200;
}
}
verify_and_jump(unk_80160, 0, 1);
}
}
```
握手完成后,BootROM 进入主循环,等待阶段 1 二进制文件,该文件通常用于恢复变砖的设备。然而,BootROM 并未验证目标地址,这允许通过 `0x7A` 命令进行任意写入。`0xA1` 状态表示镜像地址和大小已被接受。
```
usb_send:
resp[0] = status;
v5 = 1;
v6 = resp;
return usb_send((int)v6, v5, state, v3);
case 3u: // 0x7A
if ( *(int *)&resp[4] >= 4 )
{
if ( *(int *)&resp[4] < 8 )
*(_DWORD *)&resp[12] = (*(_DWORD *)&resp[12] << 8) | result;
}
else
{
*(_DWORD *)&resp[8] = (*(_DWORD *)&resp[8] << 8) | result;
}
v3 = *(_DWORD *)&resp[4] + 1;
*(_DWORD *)&resp[4] = v3;
if ( v3 != 8 )
continue;
*(_DWORD *)&resp[4] = 0;
start_addr = *(_DWORD *)&resp[8];
size = *(_DWORD *)&resp[12];
g_state = 4;
status = 0xA1;
goto usb_send;
```
如果设备已熔丝(fused),`verify_and_jump` 函数将执行镜像验证。
```
int __fastcall verify_and_jump(Header *a1, int zero, int must_verify)
{
int *p_sp; // r6
int result; // r0
p_sp = (int *)&a1->sp;
result = memcmp((int)a1->magic, 0x80008, 2u);
if ( !result )
{
if ( !zero || (result = a1->usbdl_en, result == 0x5A) )
{
if ( !must_verify )
return jump(p_sp);
result = verify(a1);
if ( !result )
return jump(p_sp);
}
}
return result;
}
```
`verify` 函数将使用硬件 MD5 和 RSA 引擎来验证镜像的完整性,而 `jump` 会将栈指针设置为镜像头中的地址,并跳转到入口点。
在 ARM/Thumb 调用约定中,函数调用通常以 `PUSH {regs, LR}` 开始,并以尾调用 `BX LR` 或 `POP {regs, PC}` 结束。`usbdl_init` 没有被编译器优化为尾调用,因此包含了 `PUSH` 和 `POP` 指令。
***您已经明白其中的原理了吗?***
由于 BootROM 没有针对一次性下载的标志,我们可以发送任意数量的镜像。该漏洞利用了这一特性(尽管这本身不是一个漏洞)结合任意内存写入来实施攻击。
1. 将 payload 发送到指定地址
2. 计算栈偏移量:6(`entry` 函数:`PUSH {R3-R7,LR}`)+ 2(`usbdl_init` 函数:`PUSH {R4,LR}`)。乘以寄存器大小,即 8 * 4 = 32
3. 将跳转地址发送到栈上保存的 `LR` 寄存器(为简单起见,PoC 对所有保存的寄存器重复使用相同的地址)
4. 发送跳转命令
5. 让镜像验证失败(缺少 header 魔数)
6. `verify_and_jump` 函数返回到 `usbdl_init`
7. `usbdl_init` 打印 `FAILED` 并执行 `POP {R4, PC}` 指令,从而运行 payload
## 从源代码构建
- 安装 curl、llvm-objcopy(通常是 llvm 包的一部分)和 gcc。Ubuntu 22.04 示例:`sudo apt update && sudo apt install curl llvm gcc`
- 使用 `curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh` 或特定发行版的包管理器安装 rustup
- 确保 rust 命令可用:`. "$HOME/.cargo/env"` (bash) 或 `source "$HOME/.cargo/env.fish"` (fish)
- 构建 payload:`cd payload && ./build.sh && cd ..`
- 运行主机工具:`sudo -E cargo r -p loader -- target/thumbv6m-none-eabi/release/payload`
现在连接已触发 USB 下载模式(通过特定测试点或损坏的闪存)的设备,并查看 UART 输出。
## 时间线
- 2 月 5 日 - 提交初始报告
- 3 月 18 日 - 被 ZTE PSIRT 确认
- 5 月 7 日 - 公开发布
## 外部致谢
- 大量的 SoC 逆向工程:[Stefan Dösinger](https://github.com/stefand)
- 在已熔丝设备上测试:[Mio Naganohara](https://github.com/Mio-sha512)
- 在另一台已熔丝设备上进行预发布测试:[exp-3](https://github.com/exp-3)
## 许可证
[AGPLv3](./LICENSE)
标签:BootROM漏洞, CISA项目, CVE-2026-40003, IoT安全, Rust语言, Sanechips, SoC安全, USB下载模式, ZX297520V3, ZXIC, 中兴微电子, 二进制分析, 云安全运维, 云资产清单, 任意内存写, 可视化界面, 固件安全, 嵌入式安全, 提示词注入, 文档安全, 硬件黑客, 逆向工程, 通知系统