Unbricking an LG G4 Stylus (MT6592M): Reverse-Engineering MediaTek's DA Write Protocol & Disassembling Little Kernel

One of the boards in my Phone Revival Lab is an LG G4 Stylus (LG-H540) built around the octa-core MediaTek MT6592M (8× Cortex-A7 @ 1.4 GHz). When I first powered it on, the screen flashed the LG logo with:

Security Error: Authentication Fail #9

and immediately shut off. LG's Download Mode (laf) partition was unreachable because the bootloader halted before loading boot or recovery, leaving only the raw MediaTek Preloader USB interface.

What looked like a routine firmware reflash turned into two deep reverse-engineering sessions:

  1. Reconstructing MediaTek's legacy DA Stage 2 (0x61) write protocol after upstream mtkclient failed to write to the MT6592 eMMC.
  2. Disassembling the ARM Thumb-2 Little Kernel (lk.bin) bootloader in radare2 to understand why the unbricked board still powered off after 3 seconds when running on bench power without a battery.

1. Catching LG's Custom MediaTek Preloader (1004:61f1)

Standard MediaTek devices expose USB VID/PID 0e8d:2000 in Preloader mode. LG customized the Preloader on the LG-H540 to enumerate as 1004:61f1 (LG Electronics, Inc. G2 / Optimus Android Phone) for roughly 2 seconds upon USB insertion before timing out.

To prevent Linux's ModemManager from probing the serial port with AT commands during that 2-second window and to bind the custom VID/PID to the USB serial driver, I added /etc/udev/rules.d/99-lg-mtk.rules:

SUBSYSTEM=="usb", ATTR{idVendor}=="1004", ATTR{idProduct}=="61f1", MODE="0666", ENV{ID_MM_DEVICE_IGNORE}="1"
ACTION=="add", SUBSYSTEM=="usb", ATTR{idVendor}=="1004", ATTR{idProduct}=="61f1", RUN+="/sbin/modprobe option", RUN+="/bin/sh -c 'echo 1004 61f1 > /sys/bus/usb-serial/drivers/option1/new_id'"

With the udev rule active, mtkclient caught the Preloader handshake (HW code: 0x6592), disabled the hardware watchdog (0x10007000), and uploaded the MT6592 Download Agent (MTK_AllInOne_DA.bin).

Before attempting any writes, I backed up the board's unique calibration and security partitions (nvram, pro_info, protect_f, protect_s, seccfg) using mtk r.


2. Why mtkclient Failed to Flash MT6592—and Reversing the 0x61 DA Protocol

Reading partitions with mtk r worked flawlessly. However, every attempt to write a partition with mtk w failed immediately (Error on writing ...).

Inspecting mtkclient/Library/DA/mtk_dalegacy.py showed why: for legacy MediaTek chips, mtkclient's writeflash() calls sdmmc_write_data(), which sends opcode 0x60 followed by a parameter structure intended for newer MT67xx Download Agents. On the MT6592 Stage 2 DA inside MTK_AllInOne_DA.bin, opcode 0x60 is rejected or misinterpreted.

Disassembling the MT6592 DA Stage 2 Command Loop

I extracted the MT6592 Stage 2 binary from MTK_AllInOne_DA.bin and traced its main command dispatch loop in radare2. Right next to the working read handler (0x65), opcode 0x61 (SDMMC_WRITE_IMAGE_CMD) and opcode 0x62 (SDMMC_WRITE_DATA_CMD) handled eMMC writes.

Tracing every uart_read / usb_read and usb_write call inside the 0x61 handler revealed the exact wire framing expected by the MT6592 DA:

  1. Command & 36-Byte Header (0x61):
    • Host sends 0x61 (1 byte).
    • Host sends a 36-byte header:
      • storage_type: 0x00 (1 byte, eMMC)
      • part_type: 0x08 (1 byte, EMMC_PART_USER)
      • addr: 64-bit big-endian byte offset on eMMC (8 bytes)
      • length: 64-bit big-endian total byte length (8 bytes)
      • packet_length: 32-bit big-endian chunk size = 0x00100000 (1 MiB, 4 bytes)
      • padding: 14 zero bytes (bringing the header to 36 bytes)
    • DA responds with 0x5A (ACK).
  2. Data Transfer Initiation (0x62):
    • Host sends 0x62 (SDMMC_WRITE_DATA_CMD, 1 byte) + 0x00 (1 byte).
  3. Chunk Loop (per 1 MiB block):
    • DA sends 0x69 (CONT_CHAR) when its SRAM/DRAM buffer is ready.
    • Host streams up to 1,048,576 bytes in 4 KB USB bulk writes.
    • Host computes a 16-bit word sum over the chunk:
      # Sum every 16-bit little-endian word, masked to 16 bits, sent big-endian
      words = struct.unpack(f"<{len(chunk)//2}H", chunk)
      checksum = sum(words) & 0xFFFF
      usb_write(struct.pack(">H", checksum))
      
    • DA verifies the checksum, writes the 1 MiB block to eMMC, and replies with 0x69.
  4. Completion Sync:
    • After the final chunk, DA sends 0x5A (ACK), flushes the eMMC cache, and sends a final 0x5A.

Flashing All 16 Partitions with unbrick_flash.py

I unpacked the official H54010d_00.kdz → H54010d_00.dz firmware (merging 81 sparse chunks into a 2.08 GB raw system.img) and wrote unbrick_flash.py implementing the 0x61/0x62 protocol on top of mtkclient's DA session bootstrap.

All 16 partitions flashed cleanly at ~4.1 MB/s:

  • MBR, EBR1, EBR2 (partition tables)
  • uboot (lk.bin @ 0x02fc0000)
  • boot.img (0x03080000), recovery.img (0x04080000)
  • secro.img (SEC_RO @ 0x04980000) — Restoring this partition resolved Security Error: Authentication Fail #9!
  • logo.bin, cust.img, cache.img, and system.img (0x09800000, 2,181,038,080 bytes)

3. Why Does the Unbricked Board Shut Down Without a Battery? Disassembling lk.bin

With Security Error #9 gone, the board vibrated, displayed the clean LG boot logo, and then powered down ~3 seconds later whenever powered solely over USB without a battery installed.

To find out whether this was a brownout or a deliberate software shutdown, I loaded uboot_393216.bin (strip off the 512-byte MediaTek header → lk.bin, loaded at base address 0x81E00000) into radare2 (r2 -a arm -b 16 -m 0x81e00000).

How LG's Thumb-2 Little Kernel Resolves Strings

Standard cross-reference searches (axt) in radare2 initially showed zero references to the error strings in lk.bin. Inspecting the Thumb-2 functions revealed why: the compiler uses PC-relative offset tables. A function loads a 32-bit signed offset from its literal pool (ldr r0, [pc, #imm]) and adds pc (add r0, pc) to compute the string address at runtime.

By scanning all Thumb-2 literal pools for (literal_value + add_pc_addr + 4) == target_string_addr, I mapped every boot-mode and battery check inside lk.bin.

Shutdown Trigger 1: KPOC (Kernel Power Off Charging) Mode

At 0x81E0DFD4 (boot_mode_select), when the board powers up from USB VBUS insertion:

  1. It checks whether a charger is attached and logs:

    Power Off Charging Mode, Check Power Key
    
  2. Unless the physical Power button is held continuously across the check window, it sets g_boot_mode = 8 (KERNEL_POWER_OFF_CHARGING_BOOT / chargerlogo).

  3. Inside mt65xx_bat_init() (0x81E0B6F8), before drawing the charging animation, it checks mtk_detect_key(8) (Power Key). If the power key was released, it hits 0x81E0B97C:

    PowerKey is released before display chargerlogo : Power Off!!
    

    and immediately calls mt6575_power_off() (0x81E02EB8), pulling the PMIC BBPU power-hold bit low.

Shutdown Trigger 2: [LGBM] Battery Temperature ADC Fault

Even if the power key is held, mt65xx_bat_init() calls 0x81E0B298 (lgbm_get_battery_temp_adc), which reads the battery NTC thermistor through the MediaTek PMIC AUXADC:

  • With no battery connected, the NTC line floats open-circuit.

  • The loop at 0x81E0B2C8 retries 5 times, printing:

    [LGBM] battery temperature ADC read fail,retry count = %d
    
  • After 5 failed reads, control returns to 0x81E0B83C, which prints:

    BATTERY OVER TEMPERATURE Or FAIL Reading Battery Temperature
    

    and executes mt6575_power_off().


Takeaway

Hardware recovery on older consumer boards rarely fails for just one reason. On the LG G4 Stylus (LG-H540), clearing the eMMC cryptographic state required reverse-engineering the MT6592 DA Stage 2 0x61 wire protocol, while diagnosing the post-unbrick shutdown required static binary analysis of the Thumb-2 bootloader (lk.bin) to separate PMIC brownouts from intentional LGBM NTC thermistor lockouts.

Related Posts