Preface
This post documents the process of restoring the NAND Flash filesystem from an in-vehicle head unit.
Recovery process
The head unit is equipped with an MXIC B162711 NAND Flash storage chip and a Qualcomm CPU. Raw NAND Flash is typically not controlled directly by the host processor unless the vendor, such as Qualcomm, is highly confident in their controller/SoC implementation. During hardware analysis, I observed no anti-tamper measures on the chip, so I proceeded to desolder it using a hot-air rework station.

The chip details are shown below; it is a 512MB SLC NAND.

The chip utilizes a BGA63 package with the following pinout:

The extraction process was performed using a Proman programmer:


I read the chip multiple times and compared the resulting dumps. To mitigate the impact of bit flips and ensure data integrity, I applied a majority-voting algorithm, retaining the most frequent byte values.

The head unit is powered by a Qualcomm CSR3703 SoC.

The datasheet for this chip is not publicly available. Despite exhaustive searches, I was unable to obtain it, as the documentation is kept strictly confidential.
Even after a manual teardown, the specific chip variant could not be identified solely from the BGA layout.

Through black-box analysis, I confirmed that the SoC’s memory mapping table resides within the NAND controller. Consequently, the logical block order cannot be directly determined from the raw firmware dump.

As a result, I had to analyze the extracted firmware without knowledge of the mapping. By examining the raw binary data from the NAND dump, I was able to carve out some low-level system components, including U-Boot.

While U-Boot can be reverse-engineered, the critical information lies within the system partition.
CSR Visor analysis (128KB):

The Device Tree indicates that the console is ttySiRF1 running at 115200 baud.

NAND controller configuration details:

Since the erase block distribution in the first head unit’s NAND dump appeared overly random, I decided to desolder and read the NAND Flash from a second, identical head unit.

To restore the head unit to working condition, I reballed the chip and reattached it using an optical rework station for precise alignment.

After further research, I suspected the Wear Leveling (WL) mechanism might be using a hybrid mode. I reviewed publicly available FTL (Flash Translation Layer) implementations but could not find matching mapping logic. I hypothesized that the FTL table might be stored in the NAND controller’s ROM.

By analyzing U-Boot, I determined it utilizes a proprietary NANDDisk driver which uses ioctl for NAND read/write operations. The mapping algorithm, however, was not implemented within the driver itself.

Consequently, I could only recover fragmented files rather than a complete filesystem. To verify if the mapping table was stored inside the SoC, I performed a controlled experiment using two identical head units. I swapped their NAND chips (reballing and resoldering each time). Neither head unit would boot with the other’s NAND Flash. However, when the original chips were returned to their respective units, both booted successfully. This confirmed that the mapping logic or table is tied to the specific SoC.

After multiple comparisons, I managed to extract a RAMdisk with over 50% integrity, allowing me to successfully read the first half of the data.


I confirmed the use of a Linux EXT filesystem. The MBR information indicated three partitions:

Partition 1: 30MB
Partition 2: 400MB
Partition 3: 61.75MB
The remaining files were primarily resources and fragmented ELF binaries. Due to the fragmentation, proper reverse engineering of the ELF files was not possible.

I attempted to manually deduce the erase-block ordering logic by splitting and analyzing each block. However, inconsistencies between the initial and subsequent OOB areas misled my initial analysis. I only managed to recover the page order, which allowed reconstruction of the smaller zImage and ramdisk, but the overall mapping logic remained elusive.

I also failed to locate a map table within the NAND dump. Quick filesystem recovery was impossible; I needed to analyze the block patterns to derive the underlying mapping logic.
I decided to remove and read the NAND Flash from the second head unit. Unfortunately, the two units were running different system versions, which hindered further comparative analysis.
XXXXX_IHU_LOW_A7_LINUX_18.0F40
XXXXX_IHU_LOW_A7_LINUX_18.0F43
The kernel and ramdisk build timestamps and page layouts were inconsistent, making it impossible to infer the mapping algorithm through comparison.

Suspecting a hybrid mapping scheme, I obtained a firmware image of the same version via a USB HID “GetShell” exploit. Analyzing this known-good firmware allowed me to derive the mapping algorithm.

Below is the script I used to reconstruct the NAND dump. Looking back, I don’t recall the specific details of the logic, but it was effective at the time.
#!/usr/bin/env python3
import sys
import binascii
import struct
if len(sys.argv) < 3:
print("Usage: fuckftl.py ftl.bin raw.bin")
sys.exit(1)
def bin2hex(bin):
x = str(binascii.b2a_hex(bin), "utf-8")
return x
def hex2int(hex):
x = int('0x' + hex, 16)
return x
def get_hex_tens_place(num):
x = int((num / 0x10) % 0x10) * 0x10 + int((num / 1) % 0x10)
return x
def get_le_int16(be):
return struct.unpack("<H", be)[0]
proman_file_path = sys.argv[1]
raw_file_path = sys.argv[2]
nanddisk_path = sys.argv[3]
readable_block_addr = {}
try:
with open("sp_.bin", 'wb') as sp:
with open(proman_file_path, 'rb') as proman_file:
promanbin = proman_file.read()
proman_file.close()
with open(raw_file_path, 'wb') as raw_file:
for x in range(0, len(promanbin), 0x840):
pbuffer = promanbin[x:x+0x840]
page_a = pbuffer[:0x400]
page_b = pbuffer[0x415:0x800]
page_c = pbuffer[0x816:0x82B]
sparea_b = pbuffer[0x800:0x816]
if sparea_b[0:4] == b'\xFF\x42\x00\x00' or (sparea_b[0:4] == b'\xFF\x41\xFF\xFF' and sparea_b[6:8] == b'\x00\x00'):
x_addr = get_le_int16(sparea_b[4:6])
if x_addr in readable_block_addr:
cur_wl_version = get_le_int16(sparea_b[6:8])
if readable_block_addr[x_addr][1] > cur_wl_version:
readable_block_addr[x_addr] = [int(x / 0x840 * 0x800), get_le_int16(sparea_b[6:8])]
else:
readable_block_addr[x_addr] = [int(x / 0x840 * 0x800), get_le_int16(sparea_b[6:8])]
pbuffer = page_a + page_b + page_c
raw_file.write(pbuffer)
sp.write(sparea_b[:10])
raw_file.close()
sp.close()
except Exception as e:
print(e)
readable_block_addr_sorted = sorted(readable_block_addr.items(),key=lambda x:x[0])
with open(raw_file_path, 'rb') as raw_file:
rawbin = raw_file.read()
raw_file.close()
cur_index = -1
with open(nanddisk_path, 'wb') as nand_file:
for (k,v) in readable_block_addr_sorted:
if k<0xfff:
print("fix block {:x}, off {:x}".format(k, v[0]))
# print(hex(k))
skip = k-cur_index
if (skip) == 1:
nand_file.write(rawbin[v[0]:v[0]+0x20000])
cur_index = cur_index + skip
nand_file.close()
Ultimately, I successfully reconstructed the firmware. Given the bit-flip characteristics of NAND flash, I applied Hamming ECC for error correction. Since the system had undergone many reboot cycles, some data remained inconsistent, resulting in the significant differences shown in the comparison below.

The file sizes and types were correct.

The size of the reconstructed firmware matched the 496.6MiB reported by the live system.

Upon mounting the second partition, the directory structure was displayed correctly.

However, some files were likely corrupted due to uncorrectable bit flips. Due to time constraints, I did not pursue further analysis.