Yes, you heard that right, 10 years after its release, I managed to hack and unlock the first MediaTek based Amazon tablet that went on sale, the Amazon Fire HD6 / HD7 2014 (codenamed ariel
).
是的,你听到的权利,10年后发布,我设法破解并解锁第一个联发科的亚马逊平板电脑,去销售,亚马逊消防HD 6/HD 7 2014(代号 ariel
).
In this article, I’ll explain my journey in detail without making it too long. If you prefer to skip ahead and see the source code directly (no judgment, I don’t like to read or write much either :P), you can find it here!
在这篇文章中,我将详细解释我的旅程,而不会让它太长。如果你更喜欢直接跳到源代码(没有判断,我也不喜欢读或写太多:P),你可以在这里找到它!
Introduction 介绍
You might be wondering why I decided to tinker with such an old device, especially after so much time has passed since its release. The reason is simple: its SoC. While MediaTek devices are quite common, this tablet features a unique SoC, the MT8135.
你可能想知道为什么我决定修补这样一个旧设备,特别是在它发布以来已经过去了这么长时间。原因很简单:它的SoC。虽然联发科设备很常见,但这款平板电脑采用了独特的SoC,MT 8135。
So, what’s so special about it? Well, nothing much, really. It feels like a tablet version of the MT6595, which was used in phones like the Meizu MX4. The real interest lies in the fact that no one has managed to unlock this device due to its unique quirks which we’ll see as soon as the article develops.
有什么特别的?其实也没什么它感觉就像是平板电脑版的MT 6595,这是在像魅族MX4手机中使用。真实的兴趣在于这样一个事实,即没有人设法解锁这个设备,由于其独特的怪癖,我们将看到尽快的文章发展。
Getting the device 获取设备
Although it may sound stupid, the first problem I encountered was finding the device, as it was never sold in Spain. In total, throughout my journey, I acquired two HD7s and two HD6s (one of which eventually died).
虽然这听起来很愚蠢,但我遇到的第一个问题是找到该设备,因为它从未在西班牙出售。总之,在我的整个旅程中,我获得了两个HD7和两个HD6(其中一个最终死亡)。
The first unit was purchased from Wallapop, a popular platform for buying and selling second-hand products in Spain. The two HD7s were bought from eBay and imported directly from the U.S., which cost me quite a bit. The former HD6 was generously donated by kip_dynamite, to whom I owe a huge thanks.
第一个单位是从Wallapop购买的,这是一个在西班牙买卖二手产品的流行平台。这两台HD7是从eBay购买的,直接从美国进口,花了我不少钱前HD6是由kip_dynamite慷慨捐赠的,我欠他一个巨大的感谢。
Analyzing the firmware 分析固件
As seen on other Amazon devices, this tablet runs a heavily modified version of Android called FireOS. To my surprise, it was released with FireOS 4 (based on Android 4) but received an update to FireOS 5 (based on Android 5.1.1).
与亚马逊的其他设备一样,这款平板电脑运行的是一个经过大量修改的Android版本,名为FireOS。令我惊讶的是,它与FireOS 4(基于Android 4)一起发布,但收到了FireOS 5(基于Android 5.1.1)的更新。
Given that this is such an old device, I assumed its firmware would be similar to the 2015 Fire 7. So, I proceeded to download the latest stock firmware available for this device and extracted it. The result surprised me because something very important seemed to be missing… or at least that was my initial impression.
考虑到这是一款如此古老的设备,我认为它的固件将与2015年的Fire 7类似。所以,我继续下载最新的股票固件可用于此设备并提取它.结果令我惊讶,因为一些非常重要的东西似乎失踪.或者至少这是我最初的印象。
r0rt1z2@r0rt1z2-pc:~/Downloads/update$ tree -L 2
.
├── boot.img
├── file_contexts
├── images
│ ├── lk.bin
│ └── tz.img
├── META-INF
│ ├── CERT.RSA
│ ├── CERT.SF
│ ├── com
│ └── MANIFEST.MF
├── ota.prop
├── system
│ └── build.prop
├── system.new.dat
├── system.patch.dat
└── system.transfer.list
5 directories, 12 files
r0rt1z2@r0rt1z2-pc:~/Downloads/update$
In case you haven’t noticed, the Preloader image is missing. As I mentioned before, this device has quite a few special quirks, and this is one of them.
如果你还没有注意到,预加载器的形象是失踪。正如我之前提到的,这个设备有很多特殊的怪癖,这就是其中之一。
After realizing the Preloader was missing, I decided to do some research and came across an XDA thread that provided the location of TX and included a few UART logs from an HD6. Fortunately, one of the log links was still working, allowing me to understand how the boot chain worked on this device.
在意识到预加载器丢失后,我决定做一些研究,并遇到了一个XDA线程,它提供了TX的位置,并包括一些来自HD6的日志。幸运的是,其中一个日志链接还在工作,让我了解了靴子链在这个设备上是如何工作的。
[PL0] Build Time: 20140829-000812
...
That is the very first line of the log. It looks like the PreLoader printing its build time, but what does that 0
stand for? If we read a few more lines, we can find the answer to that question:
这是日志的第一行。看起来PreLoader打印了它的构建时间,但是 0
代表什么呢?如果我们再多读几行,我们就能找到这个问题的答案:
[PL0] loading partition 'TEE1' offset=00300000 at address=12001000
...
[PART] load "2" from 0x0000000001800200 (dev) to 0x12001000 (mem) [SUCCESS]
[PART] load speed: 9999KB/s, 112636 bytes, 11ms
...
[PL0]image verification passed for PL1[key0]
[PL0] PL1 Load OK from TEE1: err=0
[PL0] jump to 12001000
Apparently, Preloader is divided into two different stages:
显然,Preloader分为两个不同的阶段:
-
PL0
: This stage initializes the eMMC, sets up the clock and bus width, and parses the GPT to identify partitions. It then loads and verifiesPL1
from theTEE1
partition before jumping to execute it.
PL0
:此阶段会对eMMC进行调试,设置时钟和总线宽度,并解析GPT以识别分区。然后,它从TEE1
分区加载并验证PL1
,然后跳转到执行它。 -
PL1
: This stage initializes the PMIC, I2C, performs hardware checks, sets up the RTC, DRAM, and initializes the boot device. It then verifies and loadsLK
andTEE
images, performs cryptographic checks, and sets up the boot arguments. Finally, it jumps to theTEE
image to continue the boot process.
PL1
:此阶段将调试PMIC、I2C,执行硬件检查,设置RTC、DRAM,并调试靴子设备。然后,它验证并加载LK
和TEE
映像,执行加密检查,并设置靴子参数。最后,它跳转到TEE
映像以继续靴子过程。
With this information, I extracted the latest PL1 image from the tz.img
we previously downloaded. Knowing its offset is 0x00300000
(as seen in the UART log), I used UNIX dd
to cut the image:
有了这些信息,我从之前下载的 tz.img
中提取了最新的PL1映像。知道它的偏移量是 0x00300000
(如图1中所示),我使用UNIX dd
来剪切图像:
r0rt1z2@r0rt1z2-pc:~/Downloads/update/images$ dd if=tz.img of=PL1.img bs=1 skip=$((0x00300000))
113328+0 records in
113328+0 records out
113328 bytes (113 kB, 111 KiB) copied, 0.234714 s, 483 kB/s
r0rt1z2@r0rt1z2-pc:~/Downloads/update/images$ hexdump -C PL1.img | head -n5
00000000 88 16 88 58 b0 b8 01 00 50 4c 31 00 00 00 00 00 |...X....PL1.....|
00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00000020 00 00 00 00 00 00 00 00 ff ff ff ff ff ff ff ff |................|
00000030 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................|
*
r0rt1z2@r0rt1z2-pc:~/Downloads/update/images$
Success! We’ve obtained a clean dump of the second Preloader stage image. Regarding the other parts of firmware, everything was similar, if not identical, to the Fire 7 2015. Both LK and the rest of the TZ function the same way, and FireOS has the same structure. For those interested, I have uploaded a full dump on my dumpyard.
成功了!我们已经得到了第二个预加载阶段图像的干净转储。至于固件的其他部分,一切都与Fire 7 2015相似,如果不是相同的话。LK和TZ的其余部分都以相同的方式工作,FireOS具有相同的结构。对于那些感兴趣的,我已经上传了一个完整的转储我的垃圾场。
Rooting the device 使设备生根
To play it safe, I thought it would be best to root the device. Before acquiring it, I read XDA and informed myself about the available options.
为了安全起见,我想最好还是把设备的根拔出来。在购买它之前,我阅读了XDA并了解了可用的选项。
The latest versions of FireOS 5 are not rootable, but it’s always possible to downgrade (without bricking) to FireOS 4.5.3 and root from there using KingoRoot (yes, I also hate these one-click root solutions too; they are the worst).
最新版本的FireOS 5不是可根的,但它总是可以降级(没有砖)到FireOS 4.5.3和根从那里使用KingoRoot(是的,我也讨厌这些一键根解决方案;他们是最糟糕的)。
The problem with this method is that KingoRoot requires an internet connection. If you connect to some Wi-Fi while on 4.5.3, Amazon will automatically (and instantly) download a software update and subsequently install it, causing a hard brick on the device (I’m speaking from experience :D).
这种方法的问题是KingoRoot需要互联网连接。如果你在4.5.3上连接到一些Wi-Fi,亚马逊会自动(立即)下载软件更新并随后安装,导致设备上出现硬砖(我是根据经验说的:D)。
To avoid this, I decided to sniff out and extract whatever KingoRoot’s black magic is, and put everything together into a ZIP file to create a safe offline rooting method. This method directly installs SuperSU instead of the usual Chinese bloatware! I won’t go into details here, but if you want to see how it works, check out this XDA thread.
为了避免这种情况,我决定嗅出并提取KingoRoot的黑魔法,并将所有内容放在一个ZIP文件中,以创建一个安全的离线生根方法。这种方法直接安装SuperSU而不是通常的中文bloatware!我不会在这里详细介绍,但如果你想看看它是如何工作的,请查看这个XDA线程。
shell@ariel:/data/local/tmp $ su
root@ariel:/data/local/tmp # id
uid=0(root) gid=0(root) context=u:r:init:s0
root@ariel:/data/local/tmp #
(g0t r00t!) (g0t r00t!)
Accessing UART however,
As I mentioned earlier, an XDA user had posted the TX location on the HD6 board a few years ago, which made my life easier.
正如我前面提到的,一个XDA用户几年前在HD 6板上发布了TX位置,这让我的生活更轻松。
I decided to open up my HD7 to try to solder the TX connection, allowing me to more easily debug amonet, as UART is usually necessary for this process. I opened the back of the device and the first thing I found was a completely different PCB layout, which scared the hell out of me. Did this mean that finding the TX would not be as easy as I had hoped?
我决定打开我的HD7来尝试焊接TX连接,让我更容易地调试amonet,因为这个过程通常需要调试。我打开设备的背面,首先发现的是一个完全不同的PCB布局,这把我吓坏了。这是否意味着找到TX不会像我希望的那样容易?
Thankfully, my fears were unfounded. In the picture posted by the XDA user, you could see that the TX was part of what looked like a JTAG test point labeled JDEBUG1
. After a quick inspection of my HD7 board, I noticed the same label was present so my partner helped me to solder the pin in the same position as shown in the XDA image.
幸运的是,我的恐惧是没有根据的。在XDA用户发布的图片中,您可以看到TX是看起来像JTAG测试点的一部分,标记为 JDEBUG1
。在对我的HD7板进行快速检查后,我注意到相同的标签存在,所以我的合作伙伴帮助我将引脚焊接在XDA图像中所示的相同位置。
To be able to close the back of the tablet, we made a hole in the right side of the chassis and carefully passed both cables through it. The result was pretty solid, and it still holds up just fine as I’m writing this article!
I plugged in the device and… voila! UART was working just fine, I was able to read the output of PL0
, PL1
and the rest of bootloader images:
[21:50:51.343] Warning: Could not open tty device (No such file or directory)
[21:50:51.343] Waiting for tty device..
[21:50:56.839] Connected to /dev/ttyUSB0
[PL0] Build Time: 20140925-030705
[SD0] Bus Width: 1
...
Accessing bootROM mode
Typically, on such devices, we’d use the first stage of amonet, which exploits a vulnerability in bootROM to upload and execute custom payloads. However, to do this, we’d need to access USDBL mode first, which is something nobody has been able to achieve on this particular device.
Volume keys
I decided to run strings
on the previously extracted PL1
image to check for any references to USBDL mode, and I found the following:
r0rt1z2@r0rt1z2-pc:~/Downloads/update/images$ strings PL1.img | grep -e "emergency" -e "download"
...
%s exit emergency dl mode due to time-out (%d ms, %d ms)
download keys are pressed
[RTC] clear emergency dl mode flag in rtc register
[RTC] emergency dl mode flag in rtc register is detected
%s emergency download mode(timeout: %ds).
[RTC] use pl dl mode for emergency dl mode
r0rt1z2@r0rt1z2-pc:~/Downloads/update/images$
Technically, if the image wasn’t lying, this mode should be accessible through the volume rocker, similar to the first versions of the Fire 7 2015’s Preloader. So, I decided to give it a try:
r0rt1z2@r0rt1z2-pc:~$ lsusb | grep MT
Bus 003 Device 007: ID 0e8d:3000 MediaTek Inc. MT65xx Preloader
r0rt1z2@r0rt1z2-pc:~$ lsusb
After a few tries, I concluded no MT6627
(which is what bootrom identifies with) showed up at all, so this probably got patched by Amazon 🙁
经过几次尝试,我得出的结论是没有 MT6627
(这是bootrom识别)出现在所有,所以这可能是由亚马逊修补:(
Erasing Preloader from the eMMC
从eMMC中删除预加载程序
The next thing I tried was quite risky, but as we say in Spanish, “quien tenga miedo a morir, que no nazca” (those who fear death should not be born). It involves erasing /dev/block/mmcblk0boot0
so that bootROM fails to load the Preloader and falls back to USBDL mode:
接下来我尝试了一件相当冒险的事情,但正如我们在西班牙语中所说,“quien tenga miedo a morir,que no nazca”(那些害怕死亡的人不应该出生)。它涉及到擦除 /dev/block/mmcblk0boot0
,以便bootROM无法加载预加载程序,并且福尔斯返回到USBDL模式:
root@ariel:/ $ echo 0 > /sys/block/mmcblk0boot0/force_ro
root@ariel:/ $ dd if=/dev/zero of=/dev/block/mmcblk0boot0 bs=512 count=8
8+0 records in
8+0 records out
4096 bytes transferred in 0.001 secs (4096000 bytes/sec)
root@ariel:/ $ echo -n EMMC_BOOT > /dev/block/mmcblk0boot0
root@ariel:/ $ reboot -p
… and it booted back to the OS, as if nothing happened! I double checked mmcblk0boot
and it remained intact so… what’s exactly going on here?
…然后它又回到了操作系统,就像什么都没发生一样我仔细检查了 mmcblk0boot
,它仍然完好无损,所以……这到底是怎么回事
After hours of research, I discovered that the persisbackup
partition seemed to contain factory logs from when the device was first programmed and this is what I found:
经过几个小时的研究,我发现 persisbackup
分区似乎包含设备首次编程时的工厂日志,这是我发现的:
Boot Area Write protection [BOOT_WP]: 0x04
Power ro locking: possible
Permanent ro locking: possible
partition 0 ro lock status: locked permanently
partition 1 ro lock status: not locked
Looks like Amazon locked down the first stage of Preloader on purpose… but why? That’s something I discovered after hard bricking my Fire HD6.
看起来亚马逊故意锁定了Preloader的第一阶段。但为什么呢这是我发现后,硬砖我的消防HD6。
Shorting the eMMC 缩短eMMC
I really didn’t want to go to this extreme, as I only had my first HD6 at the time, but I decided to be brave and disassemble the device. Considering this method is meant to work in 100% of cases unless USBDL mode was disabled, I wondered: this is a 2014 device—did Amazon really disable it, like on newer models?
我真的不想走到这个极端,因为当时我只有我的第一台HD6,但我决定勇敢地拆开设备。考虑到这种方法是为了在100%的情况下工作,除非USBDL模式被禁用,我想知道:这是一个2014年的设备亚马逊真的禁用它,像在新的型号?
As seen in the picture (courtesy of iFixit), everything is protected (or covered) by a soldered metal shield, so I had to use my soldering iron. Since I’m not very skilled at soldering, I asked my partner, who has excellent soldering skills, to help me with this.
如图所示(由iFixit提供),所有东西都被焊接的金属屏蔽保护(或覆盖),所以我不得不使用我的烙铁。因为我不是很熟练的焊接,我问我的合作伙伴,谁拥有出色的焊接技术,帮助我与此。
The result was fairly good, except for the fact that we accidentally ripped off what seemed to be a capacitor related to the screen.
结果相当不错,除了我们不小心扯掉了似乎与屏幕有关的电容器。
After that, I started playing the lottery (a very bad mistake—DON’T ever try this at home) with what I thought could be CLK
, CMD
, or even DAT0
. Unfortunately, after a few shorts, I ended up killing the device to the point where it wouldn’t even try to boot. So, there goes my first unit 😀
UART. What’s going on?
Since we had already found TX (which is enough to read UART logs), I decided to see what was happening when trying to access USBDL mode, either by shorting or using the volume rocker. Here’s what I discovered:
key 1 is pressed
[LIB] invalid susbdl config '0xEA000007'
<ASSERT> seclib_dl.c:line 62 0
[PLFM] preloader fatal error...
That’s what happens when you press the volume down key while connecting the device to the PC. Apparently, the Preloader detects the key press and triggers an assert, which should cause a reboot to bootROM mode. Unfortunately, in my case, it rebooted normally 🙁
这就是当您在将设备连接到PC时按下音量向下键时会发生的情况。显然,预加载器检测到按键并触发断言,这应该会导致重新引导到bootROM模式。不幸的是,在我的情况下,它重新启动正常:(
Exploiting the Preloader
利用Preloader
Having concluded that USBDL mode was not accessible, I decided to focus on exploiting the Preloader to gain arbitrary code execution and subsequently upload my own payloads.
在得出USBDL模式不可访问的结论后,我决定专注于利用Preloader来获得任意代码执行,并随后上传我自己的有效负载。
Since we know that both the Preloader and bootROM support the same commands, I decided to use the same method as the one employed for the Fire HD8 2018, which exploited the GCPU to read and write memory addresses arbitrarily.
由于我们知道Preloader和bootROM都支持相同的命令,因此我决定使用与Fire HD8 2018相同的方法,该方法利用GCPU任意读取和写入内存地址。
My initial goal was to dump the bootROM, but as you’ll see later, I failed miserably. However, I did manage to achieve code execution in the Preloader, which is a significant result nonetheless. 🙂
我最初的目标是转储bootROM,但正如您稍后将看到的,我失败得很惨。然而,我确实设法在预加载器中实现了代码执行,这仍然是一个重要的结果。:)
What’s bootROM? bootROM是什么?
After the CPU initializes, the internal SRAM controller pushes a jump instruction to the bootROM address. This is the first code that runs on the device, and it can’t be modified. The bootROM takes care of initializing basic hardware such as flash storage, UART1 (the first serial port), loading the Preloader into the On-Chip SRAM, and jumping to it.
CPU复位后,内部SRAM控制器将跳转指令推送到bootROM地址。这是在设备上运行的第一个代码,不能修改。bootROM负责初始化基本硬件,如闪存,UART 1(第一个串行端口),将预加载程序加载到片上SRAM中,并跳转到它。
While bootROM is usually located at 0x0
, there are certain cases where that address contains a direct jump to either 0x00400000
or 0x48000000
, as seen in bypass_payloads
.
虽然bootROM通常位于 0x0
,但在某些情况下,该地址包含到 0x00400000
或 0x48000000
的直接跳转,如 bypass_payloads
所示。
r0rt1z2@r0rt1z2-pc:~ $ hexdump -C 6572_0x0.bin | head -n 1
00000000 04 f0 1f e5 00 00 40 00 00 00 00 00 00 00 00 00 |......@.........|
r0rt1z2@r0rt1z2-pc:~ $
As seen above, the MT6572 contains the instruction 04 f0 1f e5
, which translates (HEX -> ARM) to LDR pc, [pc, #-4]
. This instruction loads the value from address 0x4
into the program counter (PC). Since this value is 0x00400000
, the instruction effectively redirects execution to the actual bootROM code.
如上所述,MT 6572包含指令 04 f0 1f e5
,它将(HEX -> ARM)转换为 LDR pc, [pc, #-4]
。此指令将地址 0x4
中的值加载到程序计数器(PC)中。由于该值为 0x00400000
,因此该指令有效地将执行重定向到实际的bootROM代码。
Dumping bootROM is no easy task, as it requires you to do so within a privileged context. To understand what I mean, let’s take a look at the ARM developer documentation:
转储bootROM并不是一件容易的事情,因为它需要您在特权上下文中执行此操作。为了理解我的意思,让我们来看看ARM开发人员文档:
In the ARMv7 architecture, the processor mode can change under privileged software control or automatically when taking an exception. When an exception occurs, the core saves the current execution state and the return address, enters the required mode, and possibly disables hardware interrupts.
在ARMv7架构中,处理器模式可以在特权软件控制下改变,或者在发生异常时自动改变。当异常发生时,内核保存当前执行状态和返回地址,进入所需模式,并可能禁用硬件中断。Applications operate at the lowest level of privilege, PL0, previously unprivileged mode. Operating systems run at PL1, and the Hypervisor in a system with the Virtualization extensions at PL2. The Secure monitor, which acts as a gateway for moving between the Secure and Non-secure (Normal) worlds, also operates at PL1.
应用程序在最低特权级别PL0(以前为非特权模式)下运行。操作系统在PL1上运行,Hypervisor在PL2上运行在具有虚拟化扩展的系统中。安全监视器作为在安全和非安全(正常)世界之间移动的网关,也在PL 1处运行。
To make things easier to understand, let’s just say that on the MT8135, which is ARMv7-based, both the Preloader and the TEE run at the most privileged state. Meanwhile, the Little Kernel (second bootloader) and the kernel operate at a lower privilege level.
Understanding the GCPU exploit
The GCPU is a SoC peripheral designed for decrypting encrypted media, featuring a microcontroller core (Control CPU or CCPU) equipped with ROM, SRAM, and hardware accelerators for various cryptographic algorithms including AES, SHA, MD5, RC4, DES, CRC32, and DMA.
The Control CPU (its microcontroller core) operates with a 22-bit instruction set and includes 32 general-purpose 32-bit registers, instruction ROM, instruction RAM, and data RAM.
Direct interaction with the GCPU is achieved by writing to its memory-mapped registers within the SoC’s address space. During the boot process, at least on Amazon devices, both the Preloader and the LK (bootloader) use the GCPU to verify the integrity of the images before loading them into memory. As usual, further reverse engineering of this process can provide deeper insights into the GCPU’s functionality 🙂
In my first attempts to dump bootROM, I didn’t have arbitrary code execution capabilities in the LK. Thus, my access to the GCPU was solely through the Preloader.
For my device, an older Preloader version exposed two commands to read and write memory addresses within a predefined range; CMD_READ32
and CMD_WRITE32
As seen in the amonet source code, these can be used to read from and write to the GCPU’s registers, and thus trigger cryptographic operations.
To grasp how bootROM data was successfully dumped back then, we need to delve into the intricacies of AES-CBC (Cipher Block Chaining) mode utilized during the decryption processes.
为了掌握当时bootROM数据是如何成功转储的,我们需要深入研究解密过程中使用的AES-CBC(密码块链接)模式的复杂性。
Reading data 阅读数据
AES-CBC mode is a common encryption technique where data is encrypted block by block. Each block of data is XORed with the ciphertext of the previous block before it is encrypted.
AES-CBC模式是一种常见的加密技术,其中数据逐块加密。每个数据块在加密之前都与前一个块的密文进行异或。
During decryption, each block of ciphertext is decrypted and then XORed with the previous block’s ciphertext to reconstruct the plaintext. The very first block, however, uses an Initialization Vector (IV) in place of previous ciphertext, setting the stage for the encryption or decryption sequence.
在解密过程中,每个密文块都被解密,然后与前一个块的密文进行异或以重建明文。然而,第一个块使用一个加密向量(IV)代替先前的密文,为加密或解密序列设置阶段。
In this scenario, the attacker sets the IV to zero. This is basically done so when the first block is decrypted, the absence of a previous ciphertext block means the plaintext is directly revealed and gets decrypted without any alterations, making it plainly visible.
在这种情况下,攻击者将IV设置为零。这基本上是这样做的,当第一个块被解密时,不存在先前的密文块意味着明文直接被揭示并在没有任何改变的情况下被解密,使其清晰可见。
For example, let’s consider a situation where at address 0x0
the data looks like this:
例如,让我们考虑一种情况,在地址 0x0
处,数据看起来像这样:
0xDEADBEEFCAFEBABE13371337DEADBEEFCAFEBABE13371337
This data represents two blocks of encrypted information (16 bytes each, given typical AES block size). If we set the IV to zero, the decryption would proceed as follows:
该数据表示两个加密信息块(每个16字节,给定典型的AES块大小)。如果我们将IV设置为零,解密将如下进行:
- Decryption of the first block:
第一个区块的解密:- Ciphertext Block (C1):
DEADBEEFCAFEBABE13371337DEADBEEF
密文块(C1):DEADBEEFCAFEBABE13371337DEADBEEF
- IV:
00000000000000000000000000000000
(set to zero)
IV:00000000000000000000000000000000
(设置为零) - Assume the AES decryption of C1 produces a block we’ll call D1
假设C1的AES解密产生了一个我们称之为D1的块 - Plaintext Result (P1): Since the IV is zero, P1 is equal to D1
明文结果(P1):由于IV为零,因此P1等于D1- (Normally, you’d see an XOR step here, but with an IV of zero, it simply doesn’t alter the output)
(通常情况下,您会在这里看到XOR步骤,但如果IV为零,则它不会改变输出)
- (Normally, you’d see an XOR step here, but with an IV of zero, it simply doesn’t alter the output)
- Ciphertext Block (C1):
- Decryption of the second block:
第二个区块的解密:- Ciphertext Block (C2):
CAFEBABE13371337DEADBEEFCAFEBABE
密文块(C2):CAFEBABE13371337DEADBEEFCAFEBABE
- IV:
DEADBEEFCAFEBABE13371337DEADBEEF
(previous ciphertext block)
IV:DEADBEEFCAFEBABE13371337DEADBEEF
(前一密文块) - Assume the AES decryption of C2 produces a block we’ll call D2
假设C2的AES解密产生了一个我们称之为D2的块 - Plaintext Result (P2): The decrypted output D2 is XORed with the previous ciphertext C1
明文结果(P2):解密输出D2与先前密文C1进行异或- (The XOR operation mixes D2 with the first block’s ciphertext, revealing the plaintext for this block)
(The异或操作将D2与第一块的密文混合,从而揭示该块的明文)
- (The XOR operation mixes D2 with the first block’s ciphertext, revealing the plaintext for this block)
- Ciphertext Block (C2):
With the IV set to zero, the first block’s plaintext is directly revealed, and each subsequent block’s decryption is influenced by the ciphertext of the block before it. This is essentially what allowed xyz to dump the bootROM in chunks of 16 bytes at a time, since one could read out the generated plaintext after the first block was decrypted.
当IV设置为0时,第一个块的明文直接被显示,而每个后续块的解密都受到前一个块的密文的影响。这基本上允许xyz一次以16字节的块转储bootROM,因为可以在第一个块被解密后读出生成的明文。
Writing data 写入数据
A similar process can be used to arbitrarily write data to memory in chunks of 16 bytes using AES-CBC mode. In order to archieve this, a fixed pattern is defined for XOR operations:
类似的过程可以用于使用AES-CBC模式将数据以16字节的块任意写入内存。为了实现这一点,为XOR操作定义了一个固定模式:
pattern = bytes.fromhex("4dd12bdf0ec7d26c482490b3482a1b1f").
This pattern is used to manipulate the data before it’s actually processed by the AES decryption function. Following that, the 16 bytes of data are split into four 4-byte (32-bit) words.
此模式用于在AES解密函数实际处理数据之前操作数据。之后,16个字节的数据被分成4个4字节(32位)字。
Each word is XORed with the corresponding word from the pattern. This XOR operation prepares the data in such a way that, when decrypted, it will result in the desired plaintext.
每个单词都与模式中相应的单词进行异或。这个XOR操作以这样一种方式准备数据,即当解密时,它将产生所需的明文。
In addition to that, the source address for the operation is set to 0, which has to be a valid address containing all zeroes. The destination address is set to the target address addr
, where the data should be written to. Lastly, another AES decryption gets triggered, which writes the manipulated data to the target address.
除此之外,操作的源地址被设置为0,这必须是一个包含所有零的有效地址。目的地地址被设置为目标地址 addr
,数据应该被写入到该目标地址。最后,触发另一个AES解密,将操作的数据写入目标地址。
For example, let’s assume the attacker wants to write the following 16 bytes of data to the target address 0x1000
:
例如,假设攻击者想要将以下16个字节的数据写入目标地址 0x1000
:
0xCAFEBABE13371337DEADBEEFCAFEBABE
This data represents 16 bytes of information. If we follow the process outlined above, the data is manipulated as follows:
该数据表示16字节的信息。如果我们遵循上述过程,则数据操作如下:
- Split the data into four 4-byte words:
将数据拆分为四个4字节字:0xCAFEBABE
0x13371337
0xDEADBEEF
0xCAFEBABE
- Each word is XORed with the corresponding word from the pattern:
每个单词都与模式中的相应单词进行XOR:0xCAFEBABE
^0x4dd12bdf
=0x872f9161
0x13371337
^0x0ec7d26c
=0x1df0c15b
0xDEADBEEF
^0x482490b3
=0x96892e5c
0xCAFEBABE
^0x482a1b1f
=0x82d4a1a1
- The source address for the AES operation is set to 0, and the destination address is set to
0x1000
.
AES操作的源地址设置为0,目的地址设置为0x1000
。 - By triggering the AES decryption, the XORed data gets transformed back to the original plaintext and written to the target address.
通过触发AES解密,XOR数据被转换回原始明文并写入目标地址。
My failed attempt to dump bootROM
我试图转储bootROM的失败
I tried to replicate the same method on my device, but by using the Preloader’s CMD_READ32
command to read the GCPU’s registers. While I was able to read and write GCPU registers, and I could even execute cryptographic operations, every time I tried to read 0x0
, the IV came out as zero 🙁
我尝试在我的设备上复制相同的方法,但使用Preloader的 CMD_READ32
命令读取GCPU的寄存器。虽然我能够读取和写入GCPU寄存器,甚至可以执行加密操作,但每次我试图读取 0x0
时,IV都显示为零:(
After realizing I couldn’t read anything from 0x0
, I started to think that the second stage of the Preloader, which I’m targeting, might be running under an insufficiently privileged context and the GCPU somehow knew that.
在意识到我无法从 0x0
中读取任何内容后,我开始认为我的目标Preloader的第二阶段可能在权限不足的上下文下运行,GCPU不知何故知道这一点。
In any case, I created a simple script to loop over the memory in chunks and try read operations everywhere until I hit something that is only zeros:
在任何情况下,我创建了一个简单的脚本来循环内存块,并尝试读取操作,直到我遇到只有零的东西:
with open("test.txt", 'w') as f:
address = 0x0
step_size = 0x1000
try:
while True:
data = dev.aes_read16(address)
if data.hex() != '00000000000000000000000000000000':
output = f'aes16_read @ 0x{address:08x} = {data.hex()}'
print(output)
f.write(output + '\n')
address += step_size
except KeyboardInterrupt:
print(f'Last address: 0x{address:08x} (data: {data.hex()})')
I left the script dumping memory overnight and when I woke up, I found out that it had crashed at 0xffff0000
:
我让脚本转储内存过夜,当我醒来时,我发现它在 0xffff0000
崩溃了:
Current address: 0xffff0000, Block data: 00000000000000000000000000000000
Traceback (most recent call last):
File "/home/r0rt1z2/amonet/modules/main.py", line 204, in <module>
main(dev, args)
File "/home/r0rt1z2/amonet/modules/main.py", line 39, in main
data = dev.aes_read16(address)
^^^^^^^^^^^^^^^^^^^^^^^
File "/home/r0rt1z2/amonet/modules/common.py", line 222, in aes_read16
self.write32(CRYPTO_BASE + 0xC04, addr)
File "/home/r0rt1z2/amonet/modules/common.py", line 171, in write32
self.dev.write(struct.pack(">I", word))
^^^^^^^^^^^^^^^^^^^^^^^
struct.error: 'I' format requires 0 <= number <= 4294967295
However, I open the file and found out it actually dumped a lot of memory! Apparently, it started dumping from 0x80000000
and stopped when it reached 0xffff0000
.
然而,我打开文件,发现它实际上转储了很多内存!显然,它从0#开始倾倒,当它到达1#时停止。
Uploading my own payload
把我自己的载荷
After failing to dump bootROM, I decided to try to upload my own payload to the device. The first thing I did was to reverse engineer the Preloader to see how it handled Download Agents, since that was the only way to jump to something from the Preloader.
在转储bootROM失败后,我决定尝试将自己的有效载荷上传到设备。我做的第一件事是对预加载器进行反向工程,看看它是如何处理下载代理的,因为这是从预加载器跳转到某些内容的唯一方法。
To understand how it works, let’s take a look at the usbdl_handler
function, which manages USB communication.
为了理解它是如何工作的,让我们来看看 usbdl_handler
函数,它管理USB通信。
The device waits for a specific magic sequence from the host to stay in Preloader mode and accept instructions. If the magic sequence isn’t received within a set timeout, the device continues with the normal boot process.
设备等待来自主机的特定魔术序列以保持在预加载器模式并接受指令。如果在设置的超时时间内未接收到魔术序列,设备将继续正常的靴子过程。
int usbdl_handler(bldr_comport *comport, uint32_t hshk_tmo_ms) {
memcpy(startcmd_, startcmd, 4);
start = get_timer(0);
comm = comport->ops;
uVar6 = 0;
len32 = len32 & 0xffffff00;
/*
* handshake process begins here, the host has a few
* seconds to send the magic sequence so the device
* stays in Preloader mode and listens for commands.
*/
while (true) {
platform_wdt_kick();
usbdl_get_byte(&cmd);
if (cmd != 0xfe) {
usbdl_put_byte(cmd); // echo back
}
// ...
If the magic sequence is received, the device enters a loop, continuously listening for instructions from the host. If we take a look at some of the leaked BSPs, we’ll find a list of commands that the Preloader supports.
如果接收到幻数序列,则设备进入循环,持续监听来自主机的指令。如果我们看一下一些泄露的BSP,我们会发现Preloader支持的命令列表。
#define CMD_GET_HW_SW_VER 0xfc
#define CMD_GET_HW_CODE 0xfd
#define CMD_GET_BL_VER 0xfe
#define CMD_LEGACY_WRITE 0xa1
#define CMD_LEGACY_READ 0xa2
#define CMD_I2C_INIT 0xB0
#define CMD_I2C_DEINIT 0xB1
#define CMD_I2C_WRITE8 0xB2
#define CMD_I2C_READ8 0xB3
#define CMD_I2C_SET_SPEED 0xB4
#define CMD_PWR_INIT 0xC4
#define CMD_PWR_DEINIT 0xC5
#define CMD_PWR_READ16 0xC6
#define CMD_PWR_WRITE16 0xC7
#define CMD_READ16 0xD0
#define CMD_READ32 0xD1
#define CMD_WRITE16 0xD2
#define CMD_WRITE16_NO_ECHO 0xD3
#define CMD_WRITE32 0xD4
#define CMD_JUMP_DA 0xD5
#define CMD_JUMP_BL 0xD6
#define CMD_SEND_DA 0xD7
#define CMD_GET_TARGET_CONFIG 0xD8
#define CMD_UART1_LOG_EN 0xDB
Since I used CMD_WRITE32
in a lot of places, it’s worth explaining how it works. This command is used to write a 32-bit value to a specific memory address. The host sends the command, the address, and the data to be written, and the device echoes back the address and data to confirm the operation.
因为我在很多地方都使用了 CMD_WRITE32
,所以有必要解释一下它是如何工作的。此命令用于将32位值写入特定的内存地址。主机发送命令、地址和要写入的数据,设备回送地址和数据以确认操作。
It’s worth noting that there’s a range check to ensure the address is within a valid range; otherwise, we wouldn’t have to abuse the crypto engine at all.
值得注意的是,有一个范围检查来确保地址在有效范围内;否则,我们根本不必滥用加密引擎。
/*
* there are quite a few cmds, but I skipped
* their handlers to focus on CMD_WRITE32 which
* is what we'll use.
*/
uint32_t addr = 0;
uint32_t data = 0;
uint32_t len32 = 0;
// receive the parameters from the host.
usbdl_get_dword((uint32_t *)&base_addr);
usbdl_put_dword((uint32_t)base_addr);
usbdl_get_dword(&len32);
usbdl_put_dword(len32);
// check the alignment of the address.
if (((uint)addr & 3) != 0) goto err_and_ret;
// make sure the size is actually valid.
if (len32 == 0) goto err_and_ret;
// prevent overflow attacks.
if (len32 << 2 <= len32) goto err_and_ret;
// check if the address range is valid.
sec_region_check((uint32_t)addr, len32 << 2);
// ...
Next, the device enters a loop to receive data from the host, writing each data packet to the specified memory address. This process continues until all data is written. Once complete, the function handles any additional instructions or finalizes the Preloader operations.
接下来,设备进入一个循环,从主机接收数据,将每个数据包写入指定的内存地址。这个过程一直持续到所有数据都被写入。完成后,该函数将处理任何附加指令或完成预加载器操作。
/*
* if we reach this point, all the checks have passed
* and we can notify the host about it so he can start
* sending us the data we need to write.
*/
usbdl_put_word(0);
for (index = 0; index < len32; index = index + 1) {
usbdl_get_dword(&data);
usbdl_put_dword((uint32_t)data);
*(uint32_t**)(base_addr + index * 4) = data;
}
}
/*
* the rest of the command handler would follow here, I
* decided to omit it to keep this portion more simple.
*/
return 0;
}
The next interesting command is CMD_JUMP_DA
, which is used to jump to a Download Agent (DA) located at a (fixed) memory address. Naturally, the DA downloaded by the host has to be signed for this command to actually work and not crash.
下一个有趣的命令是 CMD_JUMP_DA
,它用于跳转到位于(固定)内存地址的下载代理(DA)。当然,主机下载的DA必须进行签名,才能使此命令真正工作而不会崩溃。
if (local_41 == 0xd5) {
usbdl_get_dword(&da_addr);
usbdl_put_dword((uint32_t)da_addr);
if (g_da_verified == 1) {
status = 0;
}
else {
status = 0x2001; // DA_IMAGE_SIG_VERIFY_FAIL
}
usbdl_put_word((uint16_t)status);
if (status != 0) {
dprintf("%s usbdl_jump_da: %x\n","[USBDL]",status);
ASSERT("download.c",0x282,"0"); // crash and reboot
}
da_addr = &DAT_80001000;
_da_arg->magic = 0x58885168;
_da_arg->ver = 1;
_da_arg->flags = 3;
// ...
g_boot_mode = 100;
bldr_jump((uint32_t)da_addr,0x80000ff4,0xc);
// ...
}
So, as we can see, if g_da_verified
is set to 1, the device will jump to the fixed address 0x80001000
and execute the DA. If the DA is not verified, the device will crash and reboot.
因此,正如我们所看到的,如果 g_da_verified
被设置为1,设备将跳转到固定地址 0x80001000
并执行DA。如果DA未被验证,设备将崩溃并重新启动。
We know that aes_write16
and aes_read16
can be used starting from address 0x80000000
, so we can technically upload the payload in chunks of 16 bytes and then call it a day!
我们知道 aes_write16
和 aes_read16
可以从地址 0x80000000
开始使用,所以我们可以在技术上以16字节的块上传负载,然后收工!
Oh, but there’s a catch. As we’ve seen, g_da_verified
is only set to 1 if the DA is signed. After countless hours of trying to bypass this restriction, out of mere desperation, I tried to write to that global variable with CMD_WRITE32
and… it worked! I was able to set it to 1 and jump to my own payload.
但有个条件正如我们所看到的,如果DA是签名的,则 g_da_verified
仅设置为1。在无数个小时试图绕过这个限制之后,出于绝望,我试图用 CMD_WRITE32
和……写入全局变量。成功了!我可以将其设置为1并跳转到我自己的有效载荷。
[2024-07-21 02:02:21.020854] Waiting for Preloader
[2024-07-21 02:02:40.487613] Found port = /dev/ttyACM0
[2024-07-21 02:02:40.527277] Handshake
[2024-07-21 02:02:40.549432] Disable watchdog
[2024-07-21 02:02:40.549937] Init crypto engine
[2024-07-21 02:02:40.565977] Disable DA verification check
[2024-07-21 02:02:40.566455] Load payload from ../brom-payload/pl/pl.bin = 0x3BB2 bytes
[2024-07-21 02:02:48.838860] Let's rock
[2024-07-21 02:02:48.839156] Wait for the payload to come online...
[2024-07-21 02:02:50.813918] all good
[PLFM] USB cable in
[TOOL] USB enum timeout (Yes), handshake timeout(Yes)
[USBD] USB Full Speed
[TOOL] Enumeration(Start)
[USBD] USB High Speed
[USBD] USB High Speed
[TOOL] Enumeration(End): OK 537ms
[TOOL] sync time 277ms
...
[BLDR] jump to 0x80001000
[BLDR] <0x80001000>=0xFA000025
[BLDR] <0x80001004>=0xB5072300
[R0rt1z2] Hello from the other side!
At this point I was able to upload my own payload to the device and execute it. I simply based it on k4y0z’s Preloader based payload for mantis and called it a day.
在这一点上,我能够上传我自己的有效载荷到设备和执行它。我只是基于k4 y 0 z的预加载器为螳螂的有效载荷,并称之为一天。
As for bootROM, when I tried to dump 0x0
(with the payload, that is), I got the following output:
至于bootROM,当我试图转储 0x0
(也就是说,使用有效负载)时,我得到了以下输出:
00000000 04 f0 1f e5 00 10 00 12 04 f0 1f e5 00 10 00 12 |................|
As we’ve seen before, this is a jump instruction, which in this case redirects execution to 0x12001000
. Doesn’t this sound familiar? Yes, it’s where PL1 gets loaded as well. So when I tried to dump that address, I was greeted by PL1 instead of bootROM.
正如我们之前看到的,这是一条跳转指令,在本例中,它将执行重定向到 0x12001000
。听起来是不是很熟悉?是的,这也是PL 1加载的地方。因此,当我试图转储该地址时,迎接我的是PL 1而不是bootROM。
I haven’t bothered further with this, as it seemed like I hit a dead end. The only way to dump bootROM at this point was to gain arbitrary code execution before PL1 gets loaded, which is fairly complicated.
我没有再继续纠缠下去,因为我似乎走到了死胡同。此时转储bootROM的唯一方法是在加载PL 1之前获得任意代码执行,这相当复杂。
Unlocking the bootloader
解锁bootloader
After successfully gaining direct read/write access to the eMMC, the next step was to find a way to exploit the LK to permanently unlock the bootloader.
在成功获得对eMMC的直接读/写访问后,下一步是找到一种方法来利用LK永久解锁引导加载程序。
The first idea that came to mind was to use amonet’s microloader, porting it from ford to ariel (considering they’re very similar). However, I wondered, is it going to be that easy? That’s what I was about to find out…
我想到的第一个想法是使用amonet的微型加载器,将其从福特移植到ariel(考虑到它们非常相似)。然而,我想知道,这会那么容易吗?我也想知道…
As explained in my previous blog, microloader works by crafting a malicious boot image with a user-controlled kernel load address. This allows it to overwrite a portion of the LK (the one loaded into RAM and running at runtime) with a ROP chain, which is then executed by pivoting the stack.
正如我在之前的博客中所解释的,微加载器的工作原理是使用用户控制的内核加载地址制作恶意靴子映像。这允许它用ROP链覆盖LK的一部分(加载到RAM中并在运行时运行的LK),然后通过旋转堆栈来执行。
This would be perfect… if only it had worked. I did a quick test on my device and was greeted with an image verification failure.
这将是完美的…如果真的有用就好了我在我的设备上做了一个快速测试,结果发现图像验证失败。
[1250] > page count of kernel image = 2
[1260] Verifying kernel...
[1260] [HW CRYPTO LK] AXI = 0x0000885b
[1260] [HW CRYPTO LK] AXI = 0x0000885b
[1270] Error: fail to check 0xBC for pkcs_1_pss_decode_sha256 operation
[1270] [VERIFY_BOOTIMG] Error: fail to do pss decode for boot data.
[1280] [MBOOT] Load 'Android Boot Image' partition Error
[1290]
[1290] *******************************************************
[1290] *ERROR.ERROR.ERROR.ERROR.ERROR.ERROR.ERROR.ERROR.ERROR*
[1300] *******************************************************
[1300] > Please check kernel and rootfs in Android Boot Image are both correct.
[1310] *******************************************************
[1310] *ERROR.ERROR.ERROR.ERROR.ERROR.ERROR.ERROR.ERROR.ERROR*
[1320] *******************************************************
So, what’s going on? Why doesn’t it crash LK instead, considering we’re technically overwriting it? To get to the bottom of this, I decided to reverse engineer my LK image.
所以,怎么回事?为什么不把LK撞了,考虑到我们在技术上是不可能的?为了弄清这一点,我决定对我的LK图像进行逆向工程。
While doing so, I discovered that the load address specified in the header of the boot image is actually ignored. Regardless of what address you choose, the bootloader will always use 0x80208000
as the kernel load address.
在这样做时,我发现在靴子图像的标题中指定的加载地址实际上被忽略了。无论您选择什么地址,引导程序都将始终使用 0x80208000
作为内核加载地址。
// ... this is app(). After performing some initializations,
// LK proceeds to load either a boot image, a recovery image,
// or a factory image. Under all circumstances, it hardcodes
// the load address.
ret = mboot_android_load_bootimg_hdr("boot", 0x80208000); // header
if (-1 < ret) {
iVar1 = mboot_android_load_bootimg("boot", 0x80208000); // image
if (ret == -1) {
msg_img_error("Android Boot Image"); // error and trigger assert()
}
}
msg_header_error("Android Boot Image"); // error and trigger assert()
// ...
As you can see, the second parameter of mboot_android_load_bootimg
(which is the function amonet exploits) is a hardcoded load address. This address is then used to load the kernel into memory:
正如您所看到的, mboot_android_load_bootimg
的第二个参数(这是amonet利用的函数)是一个硬编码的加载地址。然后使用此地址将内核加载到内存中:
int mboot_android_load_bootimg(char *part_name, ulong addr) {
part_dev_t *dev;
part_t *part;
int ret;
uint64_t offset;
dev = mt_part_get_device();
if (dev == NULL) {
dprintf("mboot_android_load_bootimg , dev = NULL\n");
return -0x13;
}
part = mt_part_get_partition(part_name);
if (part == (part_t *)0xffffffff) {
dprintf("mboot_android_load_bootimg , part = NULL\n");
return -2;
}
offset = partition_get_offset((int)part);
// load whatever the data is to 0x80208000
ret = dev->read(dev, addr, (uchar *)addr, (int)offset);
if (verify_image(1, addr, _DAT_81e6c420, 0) == 0) {
if (is_prod_device()) {
FUN_81e3f6a0("console=tty0 console=ttyMT3,115200n1 root=/dev/ram", "%s androidboot.prod=1", "console=tty0 console=ttyMT3,115200n1 root=/dev/ram");
} else {
FUN_81e3f6a0("console=tty0 console=ttyMT3,115200n1 root=/dev/ram", "%s androidboot.prod=0", "console=tty0 console=ttyMT3,115200n1 root=/dev/ram");
}
} else {
dprintf("failed to verify boot image. size :0x%x", _DAT_81e6c420);
return -5;
}
return ret;
}
If you’re quick enough (unlike me :P), you might have noticed that although the address is hardcoded, the verification of the data is carried out AFTER the image is loaded into memory.
如果你足够快(不像我:P),你可能已经注意到,虽然地址是硬编码的,但数据的验证是在图像加载到内存之后进行的。
Great, so how is this helpful to us? Well, this is where math comes in handy. We know that:
太好了,这对我们有什么帮助?这就是数学派上用场的地方了我们知道:
- LK’s load address in memory is
0x81E00000
.
LK在内存中的加载地址是0x81E00000
。 - The kernel load address in memory is
0x80208000
.
内存中的内核加载地址是0x80208000
。
Notice that LK is placed AFTER (sometimes I wonder if MediaTek engineers are just plain stupid) the kernel in the memory stack. This means that, technically, flashing a huge boot image could overwrite the loaded LK data, giving us the ability to execute arbitrary code.
请注意,LK被放置在内存堆栈中的内核之后(有时我怀疑联发科的工程师是不是很愚蠢)。这意味着,从技术上讲,闪烁一个巨大的靴子映像可能会覆盖加载的LK数据,从而使我们能够执行任意代码。
The difference between the two addresses is 0x81E00000 - 0x80208000 = 0x1BF8000
, which is roughly 30 MB. Do you see where I’m going with this?
两个地址之间的差异是 0x81E00000 - 0x80208000 = 0x1BF8000
,大约为30 MB。你明白我的意思了吗?
Modifying the GPT 修改GPT
This step was quite easy, considering that we did it before in sloane to exploit LK in the same way. In this case, we just had to rename the original recovery
and boot
partitions to recovery_x
and boot_x
, then shrink userdata
and create two 30 MB partitions called boot
and recovery
(which are what LK will pick up).
这一步很容易,考虑到我们以前在斯隆以同样的方式利用LK。在这种情况下,我们只需要将原始的 recovery
和 boot
分区重命名为 recovery_x
和 boot_x
,然后收缩 userdata
并创建两个30 MB的分区,称为 boot
和 recovery
(这是LK将拾取的)。
Since we don’t have much internal memory on ariel (it’s either 8GB – for the HD7 – or 16 GB – for the HD6), I decided to shrink the cache
partition instead, which has a total size of 1GB ~. In any case, the resulting GPT looked like this:
由于ariel上没有太多的内部内存(HD7的内存是8GB,HD6的内存是16GB),我决定缩小 cache
分区,它的总大小为1GB。在任何情况下,产生的GPT看起来像这样:
[2024-07-21 18:15:51.917937] Sector size (logical): 512 bytes
[2024-07-21 18:15:51.917975] Disk identifier (GUID): 6F3736EC-8AB2-40E8-838D-F184B1848C1B
[2024-07-21 18:15:51.918002] Partition table holds up to 128 entries
[2024-07-21 18:15:51.918027] This partition table begins at sector 2 and ends at sector 33
[2024-07-21 18:15:51.918051] First usable sector is 34, last usable sector is 30527454
[2024-07-21 18:15:51.918073] Other partition table is at sector 30527487
[2024-07-21 18:15:51.918096]
[2024-07-21 18:15:51.918157] Number Start (sector) End (sector) Size Name
[2024-07-21 18:15:51.918515] 1 64 319 128.00 KiB PRO_INFO
[2024-07-21 18:15:51.918619] 2 2048 10239 4.00 MiB PMT
[2024-07-21 18:15:51.918711] 3 10240 20479 5.00 MiB TEE1
[2024-07-21 18:15:51.918800] 4 20480 30719 5.00 MiB TEE2
[2024-07-21 18:15:51.918956] 5 30720 31743 512.00 KiB UBOOT
[2024-07-21 18:15:51.919047] 6 31744 48127 8.00 MiB boot_x
[2024-07-21 18:15:51.919134] 7 48128 64511 8.00 MiB recovery_x
[2024-07-21 18:15:51.919234] 8 64512 66559 1024.00 KiB KB
[2024-07-21 18:15:51.919322] 9 66560 68607 1024.00 KiB DKB
[2024-07-21 18:15:51.919408] 10 68608 69631 512.00 KiB MISC
[2024-07-21 18:15:51.919491] 11 69632 102399 16.00 MiB persisbackup
[2024-07-21 18:15:51.919573] 12 102400 2559999 1.17 GiB system
[2024-07-21 18:15:51.919656] 13 2560000 4239359 820.00 MiB cache
[2024-07-21 18:15:51.919741] 14 4239360 4300799 30.00 MiB boot
[2024-07-21 18:15:51.919824] 15 4300800 4362239 30.00 MiB recovery
[2024-07-21 18:15:51.919914] 16 4362240 30527454 12.48 GiB userdata
Crafting the malicious boot image
制作恶意靴子映像
Once we had the GPT ready, the next step was to craft a (big enough) boot image that would overwrite LK in memory. On older devices, a ROP chain was used to redirect execution to the payload, but with time we (k4y0z, t0x1cSH and I) realized it wasn’t necessary.
一旦我们准备好了GPT,下一步就是制作一个(足够大的)靴子映像,它将覆盖内存中的LK。在旧设备上,ROP链用于将执行重定向到有效负载,但随着时间的推移,我们(k4 y 0 z,t0 x1 cSH和我)意识到这是不必要的。
One could just overwrite a function that gets called before the verification process with a direct jump to the payload. In the case of ariel, I decided to overwrite 0x81e099e8
, which is the function used to verify the boot image:
人们可以直接跳转到有效负载,覆盖在验证过程之前调用的函数。在ariel的例子中,我决定覆盖 0x81e099e8
,这是用于验证靴子映像的函数:
int verify_img(int flag, void *img, uint p3, uint p4) {
char status = *(char *)(_DAT_81e81258 + 0x16);
int ret;
if (status == '\x01') {
dprintf("Device or user build unlocked, or non-user build on engineering device! Skip kernel verification.\n");
if (flag != 0) {
flag = 0;
sprintf("console=tty0 console=ttyMT3,115200n1 root=/dev/ram",
"%s androidboot.unlocked_kernel=true",
"console=tty0 console=ttyMT3,115200n1 root=/dev/ram");
}
} else if (status == '\x02') {
dprintf("Verifying kernel with engineering key...\n");
ret = verify_img_type(img, p3, 1);
if (ret != 0) {
ret = -5;
}
return ret;
} else {
if (!is_prod_dev()) {
dprintf("User build on engineering device. Skip verification.\n");
return 0;
}
dprintf("Verifying kernel...\n");
ret = verify_img_type(img, p3, 0);
if (ret == 0) {
if (flag != 0) {
flag = 0;
sprintf("console=tty0 console=ttyMT3,115200n1 root=/dev/ram",
"%s androidboot.unlocked_kernel=false",
"console=tty0 console=ttyMT3,115200n1 root=/dev/ram");
}
return 0;
} else {
flag = -5;
}
}
return flag;
}
I simply forked sloane’s amonet repository and modified the create_boot_img.py
script to suit my needs. The result can be found here, and it generated the following image:
我只是派生了Sloane的amonet存储库并修改了 create_boot_img.py
脚本以满足我的需要。结果可以在这里找到,它生成了以下图像:
Payload Address: 0x81dff000
Payload Block: 57271
Part Size: 29763948 (28.39 MiB / 58133 Blocks)
Writing ../bin/boot.hdr...
Writing ../bin/boot.payload...
Lastly, I modified the bootROM-based Python scripts to automatically patch the GPT, downgrade bootloader images, and flash the payload to the corresponding block.
最后,我修改了基于bootROM的Python脚本,以自动修补GPT,降级引导加载程序映像,并将有效负载闪存到相应的块。
It took me a few attempts, but after adjusting some minor details, like using BL instead of BLX (unlike the original sloane exploit), I managed to jump to the payload and unlock the bootloader!
我花了几次尝试,但在调整了一些小细节,如使用BL而不是BLX(不像原来的斯隆漏洞),我设法跳转到有效载荷和解锁引导程序!
The functionality of the payload itself is the same as the one explained in my previous article. I just had to modify some parts to make it compatible with such an old device, such as the dev-read/write operations. You can check the full code here.
有效负载本身的功能与我在上一篇文章中解释的功能相同。我只需要修改一些部分,使其与这样一个旧设备兼容,例如dev-read/write操作。你可以在这里查看完整的代码。
Bonus: LineageOS 12.1 奖励:LineageOS 12.1
Considering how slow FireOS is, I decided it would be a great idea to have a smooth AOSP-based ROM. Since my motivation to build ROMs has been waning over the past few years, I didn’t feel like bringing something newer than the latest stock version. Instead, I decided to build LineageOS 12.1 (formerly known as CyanogenMod 12.1) and make it as stable as possible.
考虑到FireOS有多慢,我决定拥有一个流畅的基于AOSP的ROM是一个好主意。由于我构建ROM的动机在过去几年中一直在减弱,我不想带来比最新股票版本更新的东西。相反,我决定构建LineageOS 12.1(以前称为CyanogenMod 12.1)并使其尽可能稳定。
After a few weeks of work, I managed to build a pretty solid ROM that runs 200 times faster than FireOS and is much more customizable. As of the time of writing, the only bug the ROM has is video recording, which crashes the Camera application. All the sources used to build both TWRP and LineageOS 12.1 can be found on this GitHub organization.
经过几个星期的工作,我成功地构建了一个相当可靠的ROM,它的运行速度比FireOS快200倍,而且更具可定制性。截至撰写本文时,ROM唯一的错误是视频录制,这会导致相机应用程序崩溃。用于构建TWRP和LineageOS 12.1的所有源代码都可以在这个GitHub组织中找到。
Conclusions 结论
This was a fun journey, and I’m glad I managed to unlock the bootloader and build a custom ROM for the device. This process has helped me acquire a lot of knowledge about MediaTek devices and how they work at a low-level scope.
这是一个有趣的旅程,我很高兴我设法解锁了引导程序,并为设备构建了一个自定义ROM。这个过程帮助我获得了很多关于联发科设备的知识,以及它们如何在低级别范围内工作。
I’d like to thank k4y0z, t0x1cSH, and AntiEngineer for helping me with this project, both software and hardware-wise. I’d also like to thank zeroepoch and xyz for their amazing work on MediaTek devices. Nothing would have been possible without their contributions.
我要感谢k4 y 0 z、t0 x1 cSH和AntiEngineer在软件和硬件方面对我的帮助。我还要感谢zeroepoch和xyz在联发科设备上的出色工作。没有他们的贡献,一切都不可能。
With this, I’m finishing this article. I hope you enjoyed reading it as much as I enjoyed writing it 🙂
就这样,我完成了这篇文章。我希望你喜欢阅读它,就像我喜欢写它一样:)
原文始发于Roger’s Blog:Hacking a 2014 tablet… in 2024!