diff --git a/README.md b/README.md index 2b5419f..f21a03a 100644 --- a/README.md +++ b/README.md @@ -6,33 +6,34 @@ When I tried to mainline MT6577, I've read tons of forum posts, chat rooms and a * [The state of MT65xx in mainline Linux kernel](#the-state-of-mt65xx-in-mainline-linux-kernel) * [mt83xx/mt65xx comparison](#mt83xxmt65xx-comparison) * [Extracting information from the running device](#extracting-information-from-the-running-device) - * [CPU Operating points](#cpu-operating-points) - * [GPIO Pins](#gpio-pins) - * [I2C](#i2c) - * [LCM (LCD panel / controller model)](#lcm-lcd-panel--controller-model) - * [PMIC](#pmic) + * [CPU Operating points](#cpu-operating-points) + * [GPIO Pins](#gpio-pins) + * [I2C](#i2c) + * [LCM (LCD panel / controller model)](#lcm-lcd-panel--controller-model) + * [PMIC](#pmic) * [Searching in the source code](#searching-in-the-source-code) - * [CPU Operating points](#cpu-operating-points-1) - * [MT6572](#mt6572) - * [MT6577 + MT6329 PMIC](#mt6577--mt6329-pmic) - * [Register addresses](#register-addresses) - * [IRQ (Interrupt request) IDs](#irq-interrupt-request-ids) + * [CPU Operating points](#cpu-operating-points-1) + * [MT6572](#mt6572) + * [MT6577 + MT6329 PMIC](#mt6577--mt6329-pmic) + * [Register addresses](#register-addresses) + * [IRQ (Interrupt request) IDs](#irq-interrupt-request-ids) * [Working with BootROM / Preloader / Download agents](#working-with-bootrom--preloader--download-agents) - * [Issues related to the use of virtual machines](#issues-related-to-the-use-of-virtual-machines) - * [USB devices](#usb-devices) - * [Booting into the BootROM mode](#booting-into-the-bootrom-mode) + * [Issues related to the use of virtual machines](#issues-related-to-the-use-of-virtual-machines) + * [USB devices](#usb-devices) + * [Booting into the BootROM mode](#booting-into-the-bootrom-mode) + * [Dumping BootROM](#dumping-bootrom) * [Debugging over UART](#debugging-over-uart) - * [1. Visual inspection](#1-visual-inspection) - * [2. Schematics](#2-schematics) - * [2.1 General schematics](#21-general-schematics) - * [2.2 Board schematic / board view file](#22-board-schematic--board-view-file) - * [3. Asking on the internet](#3-asking-on-the-internet) - * [Connecting to UART](#connecting-to-uart) - * [Hardware](#hardware) - * [Software](#software) - * [UART description](#uart-description) - * [Output typical to Boot ROM and Preloader (UART1)](#output-typical-to-boot-rom-and-preloader-uart1) - * [Output typical to U-Boot and Linux kernel (UART4)](#output-typical-to-u-boot-and-linux-kernel-uart4) + * [1. Visual inspection](#1-visual-inspection) + * [2. Schematics](#2-schematics) + * [2.1 General schematics](#21-general-schematics) + * [2.2 Board schematic / board view file](#22-board-schematic--board-view-file) + * [3. Asking on the internet](#3-asking-on-the-internet) + * [Connecting to UART](#connecting-to-uart) + * [Hardware](#hardware) + * [Software](#software) + * [UART description](#uart-description) + * [Output typical to Boot ROM and Preloader (UART1)](#output-typical-to-boot-rom-and-preloader-uart1) + * [Output typical to U-Boot and Linux kernel (UART4)](#output-typical-to-u-boot-and-linux-kernel-uart4) # The state of MT65xx in mainline Linux kernel @@ -265,6 +266,9 @@ For some reasons you might want to use BootROM mode instead Preloader mode. 2. Some devices boot into BootROM when some key is held. Usually it's one of the volume keys. 3. Some devices enter BootROM mode when connected to PC without a battery. +## Dumping BootROM +Read [brom-dump/README.md](brom-dump/README.md). + # Debugging over UART UART is one of the best tools for gathering information and even communicating with your device. Usually a single SoC has multiple UARTs for various purposes. For example, one of UARTs could be used to control the wireless hardware (Wi-Fi, Bluetooth, GPS, Radio...). Despite its advantages, there are several drawbacks. First, there's need to tear down the device to access UART. Second, you will need a soldering iron with thin tip and some good flux, _and_ skills to use them. Third, most Mediatek devices have UART pins exposed on the motherboard, however identifying them might not be the easiest task. I will go through some ways to find UART pads, Fly IQ430 (MT6577) will be used as an example. diff --git a/brom-dump/README.md b/brom-dump/README.md new file mode 100644 index 0000000..d4d51af --- /dev/null +++ b/brom-dump/README.md @@ -0,0 +1,158 @@ +# Dumping BootROM +*Some assembly required.* + +# Table of contents + +* [Dumping mt6589 BROM](#dumping-mt6589-brom) + * [Obtaining SP Flash Tool](#obtaining-sp-flash-tool) + * [Capturing USB traffic of SP Flash Tool and UART output](#capturing-usb-traffic-of-sp-flash-tool-and-uart-output) + * [Reverse engineering the Download Agent](#reverse-engineering-the-download-agent) + * [Patching Download Agent](#patching-download-agent) + * [Hello, world!](#hello-world) + + +# Dumping mt6589 BROM +Initially this part was meant to be more of a blog post than a clear and concise guide. I will eventually publish everything I used but please do not expect any common sense to be present here especially if you are actually experienced in reverse engineering and baremetal programming. + +Dumping BootROM on modern Mediatek family (mt67xx) SoCs is quite a trivial task because we have [mtkclient](https://github.com/bkerler/mtkclient) that works in nearly automatic mode. + +For slightly older devices we can always rely on modified generic payloads from the [bypass_payloads](https://github.com/chaosmaster/bypass_payloads) repository. + +However, for some reason even properly coded generic UART dump payload has never worked for me on mt6589. It felt like some hardware was either not initialized at all or initialized in some wrong way. Judging by Github commits no one has publicly shared mt6589 BROM dump at the time I started working on it so I decided to take a deeper look into what could I do. + +## Obtaining SP Flash Tool +As we know, Mediatek developed their proprietary flashing software called SP Flash Tool. Its workflow could be approximated to something like the following: +1. Establish a connection with a target device (either via UART or USB). Connection can be made with devices booted into BROM and Preloader modes. +2. Identify the device and perform some very basic hardware setup procedure that depends on the target SoC using a small set of commands. +3. Extract a Download Agent for target SoC from `MTK_AllInOne_DA.bin`. Download Agent is a program compiled for specific SoC that provides rich set of commands and allows SP Flash Tool to perform ROM/RAM init, flashing etc. +4. Push Download Agent to target's SRAM at specific offset. +5. Jump to Download Agent. +6. Wait till DA performs HW initialization and sends first data back to SP Flash Tool (DRAM info, partition table etc.). +7. Execute commands to performs user-defined tasks (Formatting / Flashing / Memory test etc.) + +My idea is to obtain the original DA for my SoC and make it execute my code right after initializing the on-board hardware. + +I started by searching the oldest available SP Flash Tool build for Linux that still supported mt6589. By the time first Linux support was added to SPFT its developers already started dropping code for older platforms. For example, mt6575 and mt6577 were among the first to get their support removed from SPFT though their DAs remained in a few later versions of `MTK_AllInOne_DA.bin`. The first search result led me to the [download page at spflashtool.com](https://spflashtool.com/download/) where I got the archive with the Linux variant of SP Flash Tool v5.1648. Worth mentioning the website is tricky because it doesn't want us to access archives via direct links. Instead, it runs a script to add an event listener that appends a special request header on clicking the link. If you access the direct link without this header you will get redirected to the main page. + +By the way, the Linux version is more useful than the Windows one because the `libflashtoolEx.so` has debug symbols unlike its Windows counterpart :) + +``` +libflashtoolEx.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=b570d3c0871606769140884647696f12d864c9b7, with debug_info, not stripped +``` + +## Capturing USB traffic of SP Flash Tool and UART output +After spending a few minutes on libpng-12 and udev errors I got SP Flash Tool to work on my computer. My next goal was to capture its USB traffic to be able to replay it later. In SPFT I loaded a scatter file for my device and added one Readback entry starting from 0x0 with length of 0x1000. *Here I omit the part about killing the PRELOADER partition on my device to get into the BROM mode*, though at this point it *should* work even with Preloader mode. I initialized usbmon with `modprobe usbmon` and fired up Wireshark. I also connected my device to UART to capture output on this bus, too. + +The captured data can be split into a few parts for better understanding: +1. USB endpoint configuration. +2. BROM handshake. +3. SoC identification. +4. Basic HW setup. +5. DA push and execution. +6. Readback flow. + +BROM commands are very well documented already by other folks so I won't describe them in detail and for steps 1-3 I will re-use code from [bypass_utility](https://github.com/MTK-bypass/bypass_utility). For step 4 I will analyze captured traffic and replay it programmatically verbatim *even if some commands make no sense* just to ensure absolute compatibility with SP Flash Tool flow. + +For step 5 I needed to carve out the mt6589 DA first. Looking at the traffic capture made it trivial to find same bytes in `MTK_AllInOne_DA.bin` and extract the needed binary. + +![mt6589 DA push in Wireshark](../images/brom-dump-001.png) + +``` +7z x -so "SP_Flash_Tool_v5.1648_Linux.zip" "SP_Flash_Tool_v5.1648_Linux/MTK_AllInOne_DA.bin" |\ + tail -c +767137 | head -c 141012 \ + > "mt6589-da-original.bin" +``` + +Of course I could have used [cyrozap's kaitai struct](https://github.com/cyrozap/mediatek-lte-baseband-re/blob/master/SoC/mediatek_download_agent.ksy) but: +1. I could not be arsed to fix Kaitai Web IDE issues in Firefox (it didn't open big files at the time of writing) +2. Back when I worked on mt6577 I remember `MTK_AllInOne_DA.bin` having older format that this kaitai struct did not support. As it turned out this struct should work with mt6589 but I didn't test it because see point 1. + +With the original DA for mt6589 I can now try replaying the traffic and pushing it myself. For this matter I came up with what later became the `spft-replay` program. I based it on [bypass_utility](https://github.com/MTK-bypass/bypass_utility) by Dinolek and chaosmaster and repurposed it to suit my needs. The first thing I removed was Windows support and its DLLs :P The next thing to go was Preloader mode handling as I am working only with BROM. I also took an attempt at refactoring and reformatting the code. I doubt I succeeded at this because I don't use any IDE and rely solely on `isort`, `ruff` and `black`. + +I decided to go the path of least resistance and did not implement traffic replay past the point after the device jumps to DA and sends some initial data (this part is highlighted on the picture below). + +![mt6589 DA init data exchange in Wireshark](../images/brom-dump-002.png) + +With UART hooked up to my device I pushed the original DA I carved out from Wireshark dump and watched for console output. Surprisingly it worked and printed the following lines: + +``` +Output Log To Uart 4 +InitLog: 10:54:54 26000000 [MT6589] +GetNandID(), m_nand_acccon=0, m_chip_select=0 +[SD0] Bus Width: 1 +[SD0] SET_CLK(260kHz): SCLK(259kHz) MODE(0) DDR(0) DIV(193) DS(0) RS(0) +1501004D, 38473157, 41022557, 1844608F, +``` + +In other words, before the DA requests more data from SP Flash Tool it successfully prints those lines. Now I need to find a place where I can patch in a jump to my code. + +## Reverse engineering the Download Agent +I loaded the original DA into Ghidra: + +![Original mt6589 DA info in Ghidra](../images/brom-dump-003.png) + +I searched for the usages of the string `Output Log To Uart 4` and Ghidra jumped to `FUN_12004088` which I renamed to `init_log` for conveniece. Then I jumped to `FUN_12003f7c` because it clearly is some `print` function. Looking at its contents in decompiler leaves no doubt it's `printf`. I renamed `FUN_12003f7c` to `printf_uart`. + +Analyzing outgoing call tree of `printf_uart` revealed a few useful functions. I mapped them on the picture below after giving them normal names. + +![Illustrated outgoing call tree of printf_uart](../images/brom-dump-004.png) + +Lets get back to `init_log`. The first XREF of this function seems to be a goldmine - it looks like a global init function (`FUN_12001be8` renamed to `init`) that runs many initialization routines before jumping into the command loop (`FUN_120016d6` renamed to `command_loop`). This could be super useful in the future. + +![init function in Ghidra](../images/brom-dump-005.png) + +The last line printed on UART console looks like it was a printf format string. Large amount of parentheses makes me think so and I confirmed it by looking at `FUN_12013426` which I renamed to `set_sd_clk`. I did not understand how to configure the Function Call Graph tool to show all possible call trees between `init` and `set_sd_clk` so I opened the "Incoming Calls" pane of the Function Call Trees tool and entered `init` in the filter field: + +![set_sd_clk incoming calls tree](../images/brom-dump-006.png) + +All `set_sd_clk` invokations stemming from `command_loop` execute way too late, and DA does not reach such code after receiving just the initial data after jumping to DA. + +Now there are just 4 different call trees originating from `init` left. I need to find the function after `init` that gets executed first. I opened `init` in disasm and looked at functions listed in the Incoming Calls tree: + +![Functions in init that eventually call set_sd_clk](../images/brom-dump-007.png) + +Looking at the addresses of the `BL` instructions it is obvious `FUN_12000ccc` is executed first and at some point this function causes the `[SD0] SET_CLK(260kHz): ...` line to be printed on UART. I opened `FUN_12000ccc` in decompiler and instantly noticed values similar to those in Wireshark. + +![DA data exchange - Ghidra and Wireshark](../images/brom-dump-008.png) + +At this point it's obvious that `FUN_12000c72` generates 5 separate transfers highlighted in green color. I will keep the `FUN_12000c72` call and patch the next one to jump to custom payload. The payload will be appended to the end of the original DA and it should be small enough to fit into SRAM. The original mt6589 DA is 141012 (0x226D4) bytes long and since we count bytes from 0 the last byte is 141011 (0x226D3) so our payload will start at 141012th byte. + +## Patching Download Agent +Ghidra's "Patch Instruction" is quite inconvenient to use so I fired up [Online ARM to HEX converter](https://armconverter.com/). Of course I could have used tools like r2 and others but why should I when I have the online converter... + +The instruction that jumps to custom payload will be located at `0x12000cda` and I pasted this value into the "Offset (hex)" field on webpage. The assembly code is trivial: `BL 0x120226d4`. I copied THUMB bytes (`12f0fbfc`) because the parent function is in Thumb mode. + +Right now I'm planning to compile my first custom payload completely in Thumb mode because I plan to use some functions from DA that are Thumb and they also return to Thumb mode. + +To not patch same instructions in other places (there are many byte sequences of `60688047` in the binary) I will extend to search pattern to include the previous instruction that will remain untouched. I don't really know how do professionals patch binary files and went with simple `xxd` and `sed` workflow: + +``` +xxd -c 256 -p "mt6589-da-original.bin" |\ + sed -e "s/fff7ccff60688047/fff7ccff21f0fbfc/" |\ + xxd -p -r \ + > "mt6589-da-patched.bin" +``` + +## Hello, world! +For starters, I decided to write a small payload that will use DA functions I found to print a few test values. There's nothing outstanding about it and you can check the source in the `payloads/hello-world-uart-da-api.s` . However there are few interesting points worth noting. + +First, right after jumping we must reset all registers and stack pointer to guarantee proper execution flow. This is done in `payloads/init.s` - this file is compiled in such a way that the code is put into a separate `.text.init` section. Later I will tell `ld` to always put this section at start of the custom payload. After registers are cleared the init routine jumps to the main code. + +Second is the `ld` script I wrote. My binary targets bare metal target and I use the `arm-none-eabi` toolchain [v11.2-2022.02 from ARM Developer](https://developer.arm.com/downloads/-/arm-gnu-toolchain-downloads) to build it. At the beginning of script I define the only available memory sector that starts at ( (DA memory offset)+(custom payload offset) ). This way `ld` understands there is no read-only memory. Since I'm building for bare-metal there is no crt0 to perform common basic initialization such as copying sections from RO memory to RWX memory and zero-filling the `.bss` section. In fact, DA has already initialized not only its own sections but also set up essential hardware. As there is no RO memory everything left to do is to initialize the `.bss` section with zeroes. Of course I could implement some algorithm in `.text.init` section but as I'm going the path of least resistance I limited the max payload size to `0x200` bytes and made `ld` fill unused space with zeroes thus imitating already initialized `.bss` section. Resulting piggyback binaries will always be `0x200` bytes long but it's always possible to adjust the size. + +The piggyback then is appended to patched DA file resulting in ready-to-use payload for `spft-replay`. + +``` +cat "mt6589-da-patched.bin" "mt6589-hello-world-uart-da-api-piggyback.bin" \ + > "mt6589-hello-world-uart-da-api-payload.bin" +``` + +I connected my device to UART console and pushed the payload: + +``` +spft-replay.py "mt6589-hello-world-uart-da-api-payload.bin" +``` + +![Hello world payload output](../images/brom-dump-009.png) + +Success! My next step is to implement proper code for dumping BROM. diff --git a/brom-dump/payloads/.gitignore b/brom-dump/payloads/.gitignore new file mode 100644 index 0000000..567609b --- /dev/null +++ b/brom-dump/payloads/.gitignore @@ -0,0 +1 @@ +build/ diff --git a/brom-dump/payloads/Makefile b/brom-dump/payloads/Makefile new file mode 100644 index 0000000..6f323a3 --- /dev/null +++ b/brom-dump/payloads/Makefile @@ -0,0 +1,97 @@ +# SPDX-License-Identifier: Unlicense + +CROSS_COMPILE ?= arm-none-eabi- +AS = $(CROSS_COMPILE)as +LD = $(CROSS_COMPILE)ld +OBJCOPY = $(CROSS_COMPILE)objcopy +ASFLAGS = -mthumb -march=armv7 -mcpu=cortex-a7 -I include/$(TARGET) +LDFLAGS = -T $(TARGET_LD_SCRIPT) + +# Root directory for build files +BUILD_DIR = $(CURDIR)/build +# Directory for storing auxiliry file such as SP Flash Tool distributions +AUX_DIR = $(BUILD_DIR)/aux +# Directory for temp build files +OUT_DIR = $(BUILD_DIR)/out + +# Supported devices +ALL_TARGETS = mt6589 +# This has to be set manually +TARGET ?= unsupported +TARGET_LD_SCRIPT = include/$(TARGET)/payload.ld +TARGET_INIT = $(OUT_DIR)/$(TARGET)-init.o +# Available payloads +PAYLOADS = hello-world-uart-da-api + +PAYLOAD_BINS = $(patsubst %,$(BUILD_DIR)/$(TARGET)-%-payload.bin,$(PAYLOADS)) +TARGET_DA_PATCHED = $(OUT_DIR)/$(TARGET)-da-patched.bin + +CHECKSUM_SPFT_V5_1648_LINUX := "9c9c57405ee35044e41d7958cfbd01232107101ec5cec03539d33438cbe38b4b" + +.PHONY: all target clean + +all: target $(PAYLOAD_BINS) + +target: +ifeq ($(TARGET),unsupported) + $(error "TARGET is not defined, supported values: $(ALL_TARGETS)") +endif + +clean: + rm -vf $(AUX_DIR)/*-da-original.bin + rm -vf $(OUT_DIR)/*-da-patched.bin + rm -vf $(OUT_DIR)/*.o + rm -vf $(BUILD_DIR)/*-payload.bin + + +$(TARGET_INIT): init.s + $(AS) $(ASFLAGS) -o "$@" "$<" + +$(OUT_DIR)/$(TARGET)-hello-world-uart-da-api-piggyback.o: hello-world-uart-da-api.s | $(OUT_DIR) + $(AS) $(ASFLAGS) -o "$@" "$<" + +%-piggyback.elf: %-piggyback.o $(TARGET_INIT) | $(OUT_DIR) + $(LD) $(LDFLAGS) -o "$@" $^ + +%-piggyback.bin: %-piggyback.elf | $(OUT_DIR) + $(OBJCOPY) -O binary -S -g "$<" "$@" + +$(BUILD_DIR)/$(TARGET)-%-payload.bin: $(TARGET_DA_PATCHED) $(OUT_DIR)/$(TARGET)-%-piggyback.bin | $(BUILD_DIR) $(OUT_DIR) + cat $^ > "$@" + +$(AUX_DIR)/mt6589-da-original.bin: $(AUX_DIR)/SP_Flash_Tool_v5.1648_Linux.zip | $(AUX_DIR) + 7z x -so "$<" "SP_Flash_Tool_v5.1648_Linux/MTK_AllInOne_DA.bin" |\ + tail -c +767137 | head -c 141012 \ + > "$@" + +# Patch the internal memory initialization routine to jump to our payload +# before Download Agent starts waiting for more data from SP Flash Tool. +# +# Before patch: +# ====================================================================== +# 12000cd6 ff f7 cc ff bl FUN_12000c72 +# 12000cda 60 68 ldr r0,[r4,#0x4]=>DAT_00102118 +# 12000cdc 80 47 blx r0 +# +# +# After patch: +# ====================================================================== +# 12000cd6 ff f7 cc ff bl FUN_12000c72 +# 12000cda 21 f0 fb bc bl FUN_120226d4 +$(OUT_DIR)/mt6589-da-patched.bin: $(AUX_DIR)/mt6589-da-original.bin | $(OUT_DIR) $(AUX_DIR) + xxd -c 256 -p "$<" |\ + sed -e "s/fff7ccff60688047/fff7ccff21f0fbfc/" |\ + xxd -p -r > "$@" + +$(AUX_DIR)/SP_Flash_Tool_v5.1648_Linux.zip: | $(AUX_DIR) + wget \ + --quiet \ + --header "referer: https://spflashtool.com/download/" \ + -O "temp-v5.1468-linux.zip" \ + "https://spflashtool.com/download/SP_Flash_Tool_v5.1648_Linux.zip" + @echo "Checking file integrity" + sha256sum "temp-v5.1468-linux.zip" | grep -q -F $(CHECKSUM_SPFT_V5_1648_LINUX) + mv "temp-v5.1468-linux.zip" "$@" + +$(BUILD_DIR) $(AUX_DIR) $(OUT_DIR): + mkdir -p "$@" diff --git a/brom-dump/payloads/hello-world-uart-da-api.s b/brom-dump/payloads/hello-world-uart-da-api.s new file mode 100644 index 0000000..ce3f402 --- /dev/null +++ b/brom-dump/payloads/hello-world-uart-da-api.s @@ -0,0 +1,71 @@ +@ SPDX-License-Identifier: GPL-3.0-only +@ SPDX-FileCopyrightText: 2023 arzamas-16 + + .include "da-api.s" + .include "hw-api.s" + + + .syntax unified + .global _main +_main: + LDR R0, =str_hello_world + LDR R1, =HW_reg_chip_id + LDR R1, [R1] + MOV R2, #0 + MOV R3, #0 + BL DA_printf_uart + + MOV R0, #0x12 + MOV R1, #2 + BL DA_print_hex_value + MOV R0, #'\n' + BL DA_putc_wrapper_uart + + LDR R0, =val_1 + LDR R0, [R0] + MOV R1, #4 + BL DA_print_hex_value + MOV R0, #'\n' + BL DA_putc_wrapper_uart + + LDR R0, =val_2 + LDR R0, [R0] + MOV R1, #8 + BL DA_print_hex_value + MOV R0, #'\n' + BL DA_putc_wrapper_uart + + MOV R4, #0 +sequence_test_loop: + MOV R0, R4 + MOV R1, #2 + BL DA_print_hex_value + MOV R0, #' ' + BL DA_putc_wrapper_uart + ADD R4, R4, #1 + CMP R4, #0x10 + BLT sequence_test_loop +sequence_test_loop_end: + MOV R0, #'\n' + BL DA_putc_wrapper_uart + + MOV R0, #':' + BL DA_putc_wrapper_uart + MOV R0, #')' + BL DA_putc_wrapper_uart + MOV R0, #'\n' + BL DA_putc_wrapper_uart +busy_wait: + NOP + B busy_wait @ do not go any further, must reset manually! + + + +.data +str_hello_world: + .asciz "\n\n\nHello from mt%x!\n" + .align 4 +val_1: + .word 0x3456 +val_2: + .word 0x789ABCDE diff --git a/brom-dump/payloads/include/mt6589/da-api.s b/brom-dump/payloads/include/mt6589/da-api.s new file mode 100644 index 0000000..4011cbf --- /dev/null +++ b/brom-dump/payloads/include/mt6589/da-api.s @@ -0,0 +1,6 @@ +@ SPDX-License-Identifier: GPL-3.0-only +@ SPDX-FileCopyrightText: 2023 arzamas-16 + +.equ DA_print_hex_value, 0x12003F50 @ void print_hex_value(uint value, uint width) +.equ DA_putc_wrapper_uart, 0x12003F3A @ void putc_uart_wrapper(uint chr) +.equ DA_printf_uart, 0x12003F7C @ void printf_uart(char* fmt, uint* val1, uint val2, uint val3) diff --git a/brom-dump/payloads/include/mt6589/hw-api.s b/brom-dump/payloads/include/mt6589/hw-api.s new file mode 100644 index 0000000..58f8acb --- /dev/null +++ b/brom-dump/payloads/include/mt6589/hw-api.s @@ -0,0 +1,7 @@ +@ SPDX-License-Identifier: GPL-3.0-only +@ SPDX-FileCopyrightText: 2023 arzamas-16 + +.equ HW_reg_chip_id, 0x08000000 + +@ derived from the original mt6589 DA, see 0x120000A0 +.equ MEM_stack_base, 0x10FFF0 diff --git a/brom-dump/payloads/include/mt6589/payload.ld b/brom-dump/payloads/include/mt6589/payload.ld new file mode 100644 index 0000000..96dba77 --- /dev/null +++ b/brom-dump/payloads/include/mt6589/payload.ld @@ -0,0 +1,49 @@ +/* SPDX-License-Identifier: GPL-3.0-only */ +/* SPDX-FileCopyrightText: 2023 arzamas-16 */ + +ENTRY(_init) + +MEMORY +{ + DA_RAM (IWX) : ORIGIN = 0x120226d4, LENGTH = 0x200 +} + +SECTIONS +{ + .text : + { + *(.text.init); + *(.text*); + } + + .rodata : + { + *(.rodata); + *(.rodata.*); + *(.gnu.linkonce.r.*); + } + + .data : + { + *(.data); + *(.data.*); + *(.gnu.linkonce.d.*); + } + + .bss : + { + + *(.bss); + *(.bss.*); + *(.gnu.linkonce.b.*); + + *(COMMON); + } + + .fill : + { + FILL(0); + . = ORIGIN(DA_RAM) + LENGTH(DA_RAM) - 1; + BYTE(0); + } +} diff --git a/brom-dump/payloads/init.s b/brom-dump/payloads/init.s new file mode 100644 index 0000000..8a39530 --- /dev/null +++ b/brom-dump/payloads/init.s @@ -0,0 +1,28 @@ +@ SPDX-License-Identifier: GPL-3.0-only +@ SPDX-FileCopyrightText: 2023 arzamas-16 + + .include "hw-api.s" + + + .syntax unified + .section .text.init + .global _init +_init: + LDR R0, =MEM_stack_base + CPY SP, R0 @ reset stack pointer + + MOVS R0, #0 + MOV R1, #0 + MOV R2, #0 + MOV R3, #0 + MOV R4, #0 + MOV R5, #0 + MOV R6, #0 + MOV R7, #0 + MOV R8, #0 + MOV R9, #0 + MOV R10, #0 + MOV R11, #0 + MOV R12, #0 + MOV R14, #0 @ reset link register + B _main diff --git a/brom-dump/spft-replay/.gitignore b/brom-dump/spft-replay/.gitignore new file mode 100644 index 0000000..b1cb160 --- /dev/null +++ b/brom-dump/spft-replay/.gitignore @@ -0,0 +1,161 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + diff --git a/brom-dump/spft-replay/README.md b/brom-dump/spft-replay/README.md new file mode 100644 index 0000000..b414dd2 --- /dev/null +++ b/brom-dump/spft-replay/README.md @@ -0,0 +1,16 @@ +### spft-replay: execute arbitrary payloads on Mediatek devices. + +spft-replay is based on the `bypass_utility` program originally made by **Dinolek** and **chaosmaster**. The idea of this program is to replay USB traffic generated by the proprietary Mediatek flashing software SP Flash Tool in order to push user-specified payload to device and execute it. + +Usually old Mediatek devices from the mt65xx family do not enforce digital signatures and link authorization so in theory it's possible to execute arbitrary code with the highest privileges. The original SP Flash Tool software pushes so-called Download Agents (DAs) that provide rich functionality for flashing devices. + +### Differences from the original bypass_utility: +* Works only with devices in BROM mode (VID/PID 0E8D:0003). +* No kamakiri support. +* No Windows support. + +### Supported platforms: +* mt6589 / mt8389 + +### License +MIT. diff --git a/brom-dump/spft-replay/spft-replay.py b/brom-dump/spft-replay/spft-replay.py new file mode 100755 index 0000000..1d65e1b --- /dev/null +++ b/brom-dump/spft-replay/spft-replay.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: MIT +# SPDX-FileCopyrightText: 2023 arzamas-16 + +import argparse +import logging +from functools import partial, partialmethod + +from src.common import as_0x +from src.device import Device +from src.replay import replay + +LOG_LEVEL_REPLAY = logging.INFO + 1 +LOG_LEVEL_BROM_CMD = logging.INFO - 1 +LOG_LEVEL_BROM_IO = logging.DEBUG - 1 + + +def main(): + parser = argparse.ArgumentParser( + prog="spft-replay", + description=""" +Replay SP Flash Tool traffic to run Download Agents that can execute arbitrary code. +This tool works only with devices booted into BROM mode as I could not be arsed +to implement crashing Preloader for old platforms. + """, + ) + parser.add_argument("da_file", help="File to use as a payload") + dbg_parser = parser.add_mutually_exclusive_group() + dbg_parser.add_argument( + "-v", + dest="log_level", + action="store_const", + const=LOG_LEVEL_BROM_CMD, + help="Verbose: print all executed commands", + ) + dbg_parser.add_argument( + "-vv", + dest="log_level", + action="store_const", + const=LOG_LEVEL_BROM_IO, + help="Super verbose: also print all read/write operations", + ) + args = parser.parse_args() + + init_logging(args) + + da = None + with open(args.da_file, "rb") as fis: + da = fis.read() + da_len = len(da) + logging.info(f"Payload size {da_len} bytes ({as_0x(da_len)})") + + device = Device().find() + if not device: + logging.critical("Could not find device") + exit(1) # Exit immediately + + try: + device.handshake() + replay(device, da) + except: + # Don't exit, try to close the device + logging.critical("Replay error!", exc_info=True) + + logging.info("Closing device") + device.close() + + +def init_logging(args): + # Add some logging levels. Source: https://stackoverflow.com/a/55276759 + logging.REPLAY = LOG_LEVEL_REPLAY + logging.addLevelName(logging.REPLAY, "REPLAY") + logging.Logger.replay = partialmethod(logging.Logger.log, logging.REPLAY) + logging.replay = partial(logging.log, logging.REPLAY) + + logging.BROM = LOG_LEVEL_BROM_CMD + logging.addLevelName(logging.BROM, "BROM CMD") + logging.Logger.brom = partialmethod(logging.Logger.log, logging.BROM) + logging.brom = partial(logging.log, logging.BROM) + + logging.BROM_IO = LOG_LEVEL_BROM_IO + logging.addLevelName(logging.BROM_IO, "BROM I/O") + logging.Logger.BROM_IO = partialmethod(logging.Logger.log, logging.BROM_IO) + logging.brom_io = partial(logging.log, logging.BROM_IO) + + # Apply program-wide configuration + log_level = args.log_level if args.log_level else logging.INFO + logging.basicConfig( + level=log_level, format="[%(asctime)s] <%(levelname)s> %(message)s" + ) + + +if __name__ == "__main__": + main() diff --git a/brom-dump/spft-replay/src/common.py b/brom-dump/spft-replay/src/common.py new file mode 100644 index 0000000..903357b --- /dev/null +++ b/brom-dump/spft-replay/src/common.py @@ -0,0 +1,72 @@ +# SPDX-License-Identifier: MIT +# SPDX-FileCopyrightText: 2021 Dinolek +# SPDX-FileContributor: chaosmaster +# SPDX-FileContributor: arzamas-16 + +import logging +import struct +import time + + +def raise_(ex): + raise ex + + +def to_bytes(value, size=1, endian=">"): + return { + 1: lambda: struct.pack(endian + "B", value), + 2: lambda: struct.pack(endian + "H", value), + 4: lambda: struct.pack(endian + "I", value), + }.get(size, lambda: raise_(RuntimeError("invalid size")))() + + +def from_bytes(value, size=1, endian=">"): + return { + 1: lambda: struct.unpack(endian + "B", value)[0], + 2: lambda: struct.unpack(endian + "H", value)[0], + 4: lambda: struct.unpack(endian + "I", value)[0], + }.get(size, lambda: raise_(RuntimeError("invalid size")))() + + +# Represent an object as hex string +def as_hex(obj, size=4): + if isinstance(obj, list) or isinstance(obj, tuple): + return ", ".join(as_hex(i) for i in obj) + elif isinstance(obj, bytes): + return obj.hex().upper() + elif isinstance(obj, int): + fmt = "{:0" + str(size * 2) + "X}" + return fmt.format(obj) + else: + return "?" * (size * 2) + + +# Same as above but prefixed with 0x +def as_0x(obj, size=4): + if isinstance(obj, list) or isinstance(obj, tuple): + return ", ".join("0x" + as_hex(i, size) for i in obj) + else: + return "0x" + as_hex(obj, size) + + +# Print progress every 300 ms or when reach 100% +last_progr_upd_at = 0 +last_progr_perc = -1 + + +def report_write_progress(off_start, off_end, data_sz): + global last_progr_upd_at, last_progr_perc + + THRESHOLD = 16 # Print progress only when writing more than 16 bytes + if data_sz < THRESHOLD: + return + + perc_progr = int(off_end / data_sz * 100) + if perc_progr < last_progr_perc: # occurs when the previous transfer is done + last_progr_perc = 0 + + time_delta = time.time() - last_progr_upd_at + if time_delta >= 0.3 or perc_progr == 0 or perc_progr == 100: + logging.info(f"Uploaded {off_end} out of {data_sz} bytes ({perc_progr}%)") + last_progr_upd_at = time.time() + last_progr_perc = perc_progr diff --git a/brom-dump/spft-replay/src/device.py b/brom-dump/spft-replay/src/device.py new file mode 100644 index 0000000..32e7679 --- /dev/null +++ b/brom-dump/spft-replay/src/device.py @@ -0,0 +1,475 @@ +# SPDX-License-Identifier: MIT +# SPDX-FileCopyrightText: 2021 Dinolek +# SPDX-FileContributor: chaosmaster +# SPDX-FileContributor: arzamas-16 + +import array +import logging +import time + +import usb +import usb.backend.libusb1 + +from src.common import as_0x, as_hex, from_bytes, report_write_progress, to_bytes + +BAUD = 115200 +TIMEOUT = 3 +VID = 0x0E8D +PID = 0x0003 + + +class Device: + def __init__(self, port=None): + self.udev = None + self.dev = None + self.rxbuffer = array.array("B") + self.timeout = TIMEOUT + + def find(self): + if self.dev: + raise Exception("Device already found!") + + self.backend = usb.backend.libusb1.get_backend() + + logging.info( + f"Waiting for device in BROM mode " f"({as_hex(VID, 2)}:{as_hex(PID, 2)})" + ) + self.udev = None + while not self.udev: + self.udev = usb.core.find(idVendor=VID, idProduct=PID, backend=self.backend) + if self.udev: + break + time.sleep(0.25) + + logging.info("Found device") + self.dev = self + + try: + if self.udev.is_kernel_driver_active(0): + self.udev.detach_kernel_driver(0) + if self.udev.is_kernel_driver_active(1): + self.udev.detach_kernel_driver(1) + except: + logging.exception("USB: Cannot detach kernel driver") + return None + + try: + self.configuration = self.udev.get_active_configuration() + except: + logging.exception("USB: Cannot request configuration") + return None + + try: + self.udev.set_configuration(1) + usb.util.claim_interface(self.udev, 0) + usb.util.claim_interface(self.udev, 1) + except: + logging.exception("USB: Cannot claim interface") + return None + + try: + cdc_if = usb.util.find_descriptor( + self.udev.get_active_configuration(), bInterfaceClass=0xA + ) + self.ep_in = usb.util.find_descriptor( + cdc_if, + custom_match=lambda x: usb.util.endpoint_direction(x.bEndpointAddress) + == usb.util.ENDPOINT_IN, + ) + self.ep_out = usb.util.find_descriptor( + cdc_if, + custom_match=lambda x: usb.util.endpoint_direction(x.bEndpointAddress) + == usb.util.ENDPOINT_OUT, + ) + except: + logging.exception("USB: Cannot configure endpoints") + return None + + try: + self.udev.ctrl_transfer( + 0x21, + 0x20, + 0, + 0, + array.array("B", to_bytes(BAUD, 4, "<") + b"\x00\x00\x08"), + ) + except: + logging.exception("USB: Cannot set baudrate") + return None + + return self + + @staticmethod + def check(test, gold): + if test != gold: + test = as_hex(test) + gold = as_hex(gold) + raise RuntimeError(f"Unexpected output, expected {gold} got {test}") + + def close(self): + self.dev = None + self.rxbuffer = array.array("B") + + try: + usb.util.release_interface(self.udev, 0) + usb.util.release_interface(self.udev, 1) + except Exception: + logging.debug("USB: Could not release interfaces") + + try: + self.udev.reset() + except Exception: + logging.debug("USB: Could not reset device") + + for i in range(0, 2): + try: + self.udev.attach_kernel_driver(i) + except Exception: + logging.debug(f"USB: Could not reattach kernel driver on interface {i}") + + try: + usb.util.dispose_resources(self.udev) + except Exception: + logging.debug("USB: Could not dispose resources") + + self.udev = None + time.sleep(1) + + def handshake(self): + sequence = b"\xA0\x0A\x50\x05" + i = 0 + while i < len(sequence): + self.write(sequence[i]) + reply = self.read(1) + if reply and reply[0] == ~sequence[i] & 0xFF: + i += 1 + else: + i = 0 + logging.info("Handshake completed!") + + def echo(self, words, size=1): + self.write(words, size) + self.check(from_bytes(self.read(size), size), words) + + def read(self, size=1): + while len(self.rxbuffer) < size: + try: + self.rxbuffer.extend( + self.ep_in.read(self.ep_in.wMaxPacketSize, self.timeout * 1000) + ) + except usb.core.USBError as e: + if e.errno == 110: + self.udev.reset() + break + if size <= len(self.rxbuffer): + result = self.rxbuffer[:size] + self.rxbuffer = self.rxbuffer[size:] + else: + result = self.rxbuffer + self.rxbuffer = array.array("B") + + result = bytes(result) + logging.brom_io(f"<- {as_hex(result)}") + return result + + def read_reg(self, reg_size, addr, amount=1, check_status=True): + result = [] + + # Fall back to 32-bit registers + read_command = 0xD1 + if reg_size == 16 and check_status: + read_command = 0xD0 + elif reg_size == 16 and not check_status: + read_command = 0xA2 + + self.echo(read_command) + self.echo(addr, 4) + self.echo(amount, 4) + + if check_status: + status = self.read(2) + if from_bytes(status, 2) > 0xFF: + raise RuntimeError(f"status is {as_hex(status, 2)}") + + sz = reg_size // 8 + for _ in range(amount): + data = from_bytes(self.read(sz), sz) + result.append(data) + + if check_status: + status = self.read(2) + if from_bytes(status, 2) > 0xFF: + raise RuntimeError(f"status is {as_hex(status, 2)}") + + # support scalar + if len(result) == 1: + return result[0] + else: + return result + + def read16(self, addr, amount=1, check_status=True): + logging.brom(f"read16({as_0x(addr)})") + return self.read_reg(16, addr, amount=amount, check_status=check_status) + + def read32(self, addr, amount=1): + logging.brom(f"read32({as_0x(addr)})") + return self.read_reg(32, addr, amount=amount) + + def write(self, data, size=1): + if type(data) != bytes: + data = to_bytes(data, size) + + data_sz = len(data) + # pkt_sz = self.ep_out.wMaxPacketSize + pkt_sz = 1024 # SP Flash Tool seems to ignore wMaxPacketSize + + off_start = 0 + while off_start < data_sz: + remaining = data_sz - off_start + off_end = off_start + (pkt_sz if remaining > pkt_sz else remaining) + chunk = data[off_start:off_end] + logging.brom_io(f"-> {as_hex(chunk)}") + self.ep_out.write(data[off_start:off_end], self.timeout * 1000) + report_write_progress(off_start, off_end, data_sz) + + off_start += pkt_sz + + def write_reg(self, reg_size, addr, words, check_status=True): + # support scalar + if not isinstance(words, list): + words = [words] + + # Fall back to 32-bit registers + write_command = 0xD2 if reg_size == 16 else 0xD4 + self.echo(write_command) + self.echo(addr, 4) + self.echo(len(words), 4) + + expected = 0 + self.check(self.read(2), to_bytes(expected, 2)) # arg check + + for word in words: + self.echo(word, reg_size // 8) + + if check_status: + self.check(self.read(2), to_bytes(expected, 2)) # status + + def write16(self, addr, words, check_status=True): + logging.brom(f"write16({as_hex(addr)}, [{as_hex(words, 2)}])") + self.write_reg(16, addr, words, check_status) + + def write32(self, addr, words, check_status=True): + logging.brom(f"write32({as_hex(addr)}, [{as_hex(words)}])") + self.write_reg(32, addr, words, check_status) + + def get_target_config(self): + logging.brom("Get target config") + self.echo(0xD8) + + target_config = self.read(4) + status = self.read(2) + + if from_bytes(status, 2) != 0: + raise RuntimeError(f"status is {as_hex(status, 2)}") + + target_config = from_bytes(target_config, 4) + + secure_boot = target_config & 1 + serial_link_authorization = target_config & 2 + download_agent_authorization = target_config & 4 + + return ( + bool(secure_boot), + bool(serial_link_authorization), + bool(download_agent_authorization), + ) + + def get_hw_code(self): + logging.brom("Get HW code") + self.echo(0xFD) + + hw_code = self.read(2) + status = self.read(2) + + if from_bytes(status, 2) != 0: + raise RuntimeError(f"status is {as_hex(status, 2)}") + return from_bytes(hw_code, 2) + + def get_hw_sw_ver(self): + logging.brom("Get HW/SW version") + self.echo(0xFC) + + hw_sub_code = self.read(2) + hw_ver = self.read(2) + sw_ver = self.read(2) + status = self.read(2) + + if from_bytes(status, 2) != 0: + raise RuntimeError(f"status is {as_hex(status, 2)}") + + return from_bytes(hw_sub_code, 2), from_bytes(hw_ver, 2), from_bytes(sw_ver, 2) + + def send_da(self, da_address, da_len, sig_len, da): + logging.brom( + f"Send Download Agent to {as_0x(da_address)} " + f"({da_len} bytes, {sig_len} byte signature)" + ) + self.echo(0xD7) + + self.echo(da_address, 4) + self.echo(da_len, 4) + self.echo(sig_len, 4) + + status = self.read(2) + + if from_bytes(status, 2) != 0: + raise RuntimeError(f"status is {as_hex(status, 2)}") + + self.write(da) + + checksum = from_bytes(self.read(2), 2) + status = from_bytes(self.read(2), 2) + + if status != 0: + raise RuntimeError(f"status is {as_hex(status, 2)}") + + return checksum + + def jump_da(self, da_address): + logging.brom(f"Jump to Download Agent at {as_0x(da_address)}") + self.echo(0xD5) + + self.echo(da_address, 4) + + status = self.read(2) + + if from_bytes(status, 2) != 0: + raise RuntimeError(f"status is {as_hex(status, 2)}") + + def cmd_da(self, direction, offset, length, data=None, check_status=True): + logging.brom(f"DA: {as_hex([direction, offset, length], 4)}") + self.echo(0xDA) + + self.echo(direction, 4) + self.echo(offset, 4) + self.echo(length, 4) + + status = self.read(2) + + if from_bytes(status, 2) != 0: + raise RuntimeError(f"status is {as_hex(status, 2)}") + + if (direction & 1) == 1: + self.write(data) + else: + data = self.read(length) + + if check_status: + status = self.read(2) + if from_bytes(status, 2) != 0: + raise RuntimeError(f"status is {as_hex(status, 2)}") + + return data + + def uart1_log_enable(self): + logging.brom("Enable UART1 logging") + self.echo(0xDB) + + status = self.read(2) + if from_bytes(status, 2) != 0: + raise RuntimeError(f"status is {as_hex(status, 2)}") + + def power_init(self, reg, val): + logging.brom(f"Init PMIC at {as_0x(reg)} ({as_hex(val)})") + self.echo(0xC4) + + self.echo(reg, 4) + self.echo(val, 4) + + status = self.read(2) + if from_bytes(status, 2) != 0: + raise RuntimeError(f"status is {as_hex(status, 2)}") + + def power_deinit(self): + logging.brom("Deinit PMIC") + self.echo(0xC5) + + status = self.read(2) + if from_bytes(status, 2) != 0: + raise RuntimeError(f"status is {as_hex(status, 2)}") + + def power_read16(self, reg): + logging.brom(f"PMIC read16({as_0x(reg, 2)})") + self.echo(0xC6) + self.echo(reg, 2) + + expected = 0 + self.check(self.read(2), to_bytes(expected, 2)) # recv ack + self.check(self.read(2), to_bytes(expected, 2)) # PMIC read status + + result = from_bytes(self.read(2), 2) + return result + + def power_write16(self, reg, val): + logging.brom(f"PMIC write16({as_hex(reg)}, {as_hex(val)})") + self.echo(0xC7) + self.echo(reg, 2) + self.echo(val, 2) + + expected = 0 + self.check(self.read(2), to_bytes(expected, 2)) # recv ack + self.check(self.read(2), to_bytes(expected, 2)) # PMIC write status + + def get_me_id(self): + logging.brom("Get ME ID") + self.echo(0xE1) + + length = from_bytes(self.read(4), 4) + if length == 0: + raise RuntimeError("bad ME ID length") + me_id = self.read(length) + + status = self.read(2) + if from_bytes(status, 2) != 0: + raise RuntimeError(f"status is {as_hex(status, 2)}") + + return me_id + + def get_preloader_version(self): + logging.brom("Get PRELOADER version") + self.write(0xFE) + + ver = from_bytes(self.read(1)) + if ver == 0xFE: + logging.warning("Cannot get PRELOADER version in BROM mode") + return ver + + def get_brom_version(self): + logging.brom("Get BROM version") + self.write(0xFF) + + ver = from_bytes(self.read(1)) + if ver == 0xFF: + logging.warning("Cannot get BROM version in PRELOADER mode") + return ver + + def set_power_reg(self, register, new_value, reference_value): + # Check current value + # reference_value - a value obtained from the Wireshark dump of my device + pwr = self.power_read16(register) + if pwr != reference_value: + logging.warning("power_read16 returned non-reference value") + if pwr == new_value: + logging.debug("power_read16 value is already set, setting anyway") + + # Write even if no change is needed + pwr = self.power_write16(register, new_value) + + # Check if new value has been set successfully + pwr = self.power_read16(register) + if pwr != new_value: + logging.error( + f"Could not set PMIC reg {as_0x(register, 2)} " + f"to {as_hex(new_value, 2)}, " + f"got {as_hex(pwr, 2)}" + ) diff --git a/brom-dump/spft-replay/src/replay.py b/brom-dump/spft-replay/src/replay.py new file mode 100644 index 0000000..13a8f42 --- /dev/null +++ b/brom-dump/spft-replay/src/replay.py @@ -0,0 +1,85 @@ +# SPDX-License-Identifier: MIT +# SPDX-FileCopyrightText: 2023 arzamas-16 + +import logging + +from src.common import as_0x, as_hex + + +# Request chip ID and replay its traffic +def replay(dev, payload): + hw_code = dev.get_hw_code() + logging.replay(f"HW code: {as_hex(hw_code, 2)}") + + if hw_code == 0x6583: + replay_mt6589(dev, payload) # The code is 0x6583 but the SoC is 6589 + else: + logging.critical("Unsupported hardware!") + exit(1) + + +def replay_mt6589(dev, payload): + hw_dict = dev.get_hw_sw_ver() + logging.replay(f"HW subcode: {as_hex(hw_dict[0], 2)}") + logging.replay(f"HW version: {as_hex(hw_dict[1], 2)}") + logging.replay(f"SW version: {as_hex(hw_dict[2], 2)}") + + dev.uart1_log_enable() + + logging.replay("Setting up PMIC") + dev.power_init(0x80000000, 0) + val = dev.power_read16(0x000E) + dev.set_power_reg(0x000E, 0x1001, 0x1001) + dev.set_power_reg(0x000C, 0x0049, 0x0041) + dev.set_power_reg(0x0008, 0x000C, 0x000F) + dev.set_power_reg(0x001A, 0x0000, 0x0010) + dev.set_power_reg(0x0000, 0x007B, 0x0063) + dev.set_power_reg(0x0020, 0x0009, 0x0001) + dev.power_deinit() + + # Disable watchdog timer and dump Reset Generator Unit registers + logging.replay("Disabling watchdog") + dev.write32(0x10000000, 0x22002224) + + # SP Flash Tool already knows the device is in BROM mode but still + # requests the preloader version + val = dev.get_preloader_version() + + for addr in range(0x10000000, 0x10000018 + 1, 4): + val = dev.read32(addr) + logging.replay(f"TOPRGU register {as_0x(addr)} == {as_hex(val)}") + + val = dev.get_brom_version() + logging.replay(f"BROM version: {as_hex(val, 1)}") + val = dev.get_preloader_version() # Again..? + + # External memory interface + MT6589_EMI_GENA = 0x10203070 + val = dev.read32(MT6589_EMI_GENA) # 00000000 on my device + dev.write32(MT6589_EMI_GENA, 0x00000002) + logging.replay( + f"EMI_GENA ({as_0x(MT6589_EMI_GENA)}) " + f"set to {as_hex(0x2)}, was {as_hex(val)}" + ) + + val = dev.send_da(0x12000000, len(payload), 0x100, payload) + logging.replay(f"Received DA checksum: {as_hex(val, 2)}") + + dev.uart1_log_enable() + + dev.jump_da(0x12000000) + + # Download agent is running and sends some data we have to receive + # and ACK in order to get everything initialized before it will + # jump to custom payload. + logging.replay("Waiting for device to send remaining data") + logging.replay(f"<- DA: (unknown) {as_hex(dev.read(1))}") + logging.replay(f"<- DA: (unknown) {as_hex(dev.read(4))}") + logging.replay(f"<- DA: (unknown) {as_hex(dev.read(2))}") + logging.replay(f"<- DA: (unknown) {as_hex(dev.read(10))}") + logging.replay(f"<- DA: (unknown) {as_hex(dev.read(4))}") + logging.replay(f"<- DA: (EMMC ID) {as_hex(dev.read(16))}") + + val = 0x5A + logging.replay(f"-> DA: (OK) {as_hex(val, 1)}") + dev.write(val) diff --git a/images/brom-dump-001.png b/images/brom-dump-001.png new file mode 100644 index 0000000..ffe8bc2 Binary files /dev/null and b/images/brom-dump-001.png differ diff --git a/images/brom-dump-002.png b/images/brom-dump-002.png new file mode 100644 index 0000000..4d207e2 Binary files /dev/null and b/images/brom-dump-002.png differ diff --git a/images/brom-dump-003.png b/images/brom-dump-003.png new file mode 100644 index 0000000..0d823ff Binary files /dev/null and b/images/brom-dump-003.png differ diff --git a/images/brom-dump-004.png b/images/brom-dump-004.png new file mode 100644 index 0000000..2fd6a84 Binary files /dev/null and b/images/brom-dump-004.png differ diff --git a/images/brom-dump-005.png b/images/brom-dump-005.png new file mode 100644 index 0000000..7581e85 Binary files /dev/null and b/images/brom-dump-005.png differ diff --git a/images/brom-dump-006.png b/images/brom-dump-006.png new file mode 100644 index 0000000..a0b6115 Binary files /dev/null and b/images/brom-dump-006.png differ diff --git a/images/brom-dump-007.png b/images/brom-dump-007.png new file mode 100644 index 0000000..e88e8ea Binary files /dev/null and b/images/brom-dump-007.png differ diff --git a/images/brom-dump-008.png b/images/brom-dump-008.png new file mode 100644 index 0000000..ef4bfd9 Binary files /dev/null and b/images/brom-dump-008.png differ diff --git a/images/brom-dump-009.png b/images/brom-dump-009.png new file mode 100644 index 0000000..e42121d Binary files /dev/null and b/images/brom-dump-009.png differ