On Sat, May 7, 2022, 3:02 AM Undiscussed Horrific Abuse, One Victim of Many <[1]gmkarl@gmail.com> wrote: here's load_payload it calls at the start of the main function: #!/usr/bin/env python3 import sys import time from common import Device from logger import log from functions import UserInputThread, check_modemmanager import usb.core import usb.util import struct import os def p32(x): return struct.pack(">I", x) def load_payload_file(path): with open(path, "rb") as fin: payload = fin.read() log("Load payload from {} = 0x{:X} bytes".format(path, len(payload))) while len(payload) % 4 != 0: payload += b"\x00" return payload def attempt2(d): d.write(b"\xE0") result = d.read(1) d.write(p32(0xA00)) result = d.read(4) payload = load_payload_file("../brom-payload/stage1/stage1.bin") if len(payload) >= 0xA00: raise RuntimeError("payload too large") d.write(payload) attempt2() appears to be where the payload is uploaded. it's strange the command is hardcoded here, rather than encapsulated into the Device class, and it implies the dev published their work soon after it functioned, without fully cleaning it up. E0 appears to be a special command that takes a size and data. We can infer the data is then executed somehow. def noop(*args, **kwargs): pass def load_payload(dev): log("Handshake") dev.handshake() This is the same handshake I was doing. log("Disable watchdog") dev.write32(0x10007000, 0x22000000) It turns out arbitrary RAM reads and writes can be made with normal commands. These high addresses were registers, I believe. thread = UserInputThread() thread.start() while not thread.done: dev.write32(0x10007008, 0x1971) # low-level watchdog kick time.sleep(1) This process is at [2]https://github.com/amonet-kamakiri/kamakiri/blob/master/modules/f unctions.py . It tells the user that if they have shorted their hardware, it is time to remove the short, and waits for them to press enter to continue. The thread is likely because the wait is done by a blocking read on stdin. d = [3]dev.dev addr = 0x10007050 result = dev.read32(addr) dev.write32(addr, [0xA1000]) # 00 10 0A 00 result = dev.read32(addr) Writes to a 32 bit register. The trailing two bytes are the length of the payload, could be a coincidence. readl = 0x24 result = dev.read32(addr - 0x20, readl//4) Maybe debugging cruft to read registers. Or, the reading could trigger something. dev.write32(addr, 0) Clears the register after writing it. attempt2(d) Uploads the brom payload using a normal-looking command. The upload did not verify that the length did not underrun, which seems strange to me. udev = usb.core.find(idVendor=0x0e8d, idProduct=0x3) udev._ctx.managed_claim_interface = noop log("Let's rock") try: udev.ctrl_transfer(0xA1, 0, 0, 10, 0) Okay, so it's sending control data to the USB device here. I think the device was usb_acm? i'm not immediately finding information on request 0xa1, if that is request field. a next step here might be to look at the interface for ctrl_transfer and see what the parameters are, then either check a usb log or look for information on the mediatek usb interface. except usb.core.USBError as e: print(e) # clear 2 more bytes d.read(2) This skips 2 bytes in the serial stream. The protocol appears to sometimes send 2 byte status confirmations. log("Waiting for stage 1 to come online...") data = d.read(4) if data != b"\xA1\xA2\xA3\xA4": raise RuntimeError("received {} instead of expected pattern".format(data)) This is now interfacing with the boot rom payload. I'm guessing the purpose of these payloads is to read and write data without checks that are present in the factory firmware, but I don't know. dev.kick_watchdog() log("All good") log("Load 2nd stage payload") stage2=load_payload_file("../brom-payload/stage2/stage2.bin") log("Send 2nd stage payload") # magic d.write(p32(0xf00dd00d)) # cmd d.write(p32(0x4000)) # address to write d.write(p32(0x201000)) # length d.write(p32(len(stage2))) # data d.write(stage2) code = d.read(4) if code != b"\xd0\xd0\xd0\xd0": raise RuntimeError("device failure") dev.kick_watchdog() All it did was write the data into ram at address 0x201000 . These functions not being encapsulated into a class possibly shows how small and quick this part is considered, maybe not helpful information, unsure. log("Party time") # magic d.write(p32(0xf00dd00d)) # cmd d.write(p32(0x4001)) # address to write d.write(p32(0x201000)) This jumps to the address. It seems the reason for two payloads is that there is a size limitation on the code used to upload the first one. The dev wanted more features than fit within that size. log("Waiting for stage 2 to come online...") data = d.read(4) if data != b"\xB1\xB2\xB3\xB4": raise RuntimeError("received {} instead of expected pattern".format(data)) log("All good") dev.kick_watchdog() That's that. The purpose of this code is to replace the behavior of the device with new behavior: to provide a custom download agent for further steps to use. It uses a command code E0 to do this, which is followed by a short control transfer to the usb device, the nature of which I haven't identified. It's somewhat reasonable to summarise the E0 + control transfer as a hack that executes up to 0xa00 bytes of uploaded code. The user interfacing implies the device may be booted with some pins shorted to facilitate this. This may be the kamakiri hack, but it seems worthwhile glancing in other areas to understand better. if __name__ == "__main__": check_modemmanager() if len(sys.argv) > 1: dev = Device(sys.argv[1]) else: dev = Device() dev.find_device() load_payload(dev) References 1. mailto:gmkarl@gmail.com 2. https://github.com/amonet-kamakiri/kamakiri/blob/master/modules/functions.py 3. http://dev.dev/