Lean And Emulate Pixel bootloader for Finding Bugs

缘起

大约一两个月前,在某黑底小白X的时候看到了一篇eshard关于pixel6 bootloader研究的推文. 里面提到的bootloader Emulation的部分引起了我的兴趣.

关于pixel6 bootloader的研究是一套系列文章,作者从“找到已经被修复的bootloader exploit”出发,一步步的介绍了如何快速定位漏洞位置、如何通过模拟执行或是真机调试的方式来调试exploit、如何从任意地址读写最终转化到任意命令执行。

文章里面并没有公开bootloader的漏洞位置以及exploit代码,但实际上文章中的不少细节也足够定位到漏洞的位置,并且作者公开的使用unicorn模拟执行等一些列代码已经让我受益匪浅了。因此本次我并没有执着于一定要找到这个已经被修复的漏洞位置并利用成功,本文也会更加关注于eshard系列文章作者对于漏洞挖掘思路以及工具使用的记录与思考。

BootUp & SecureBoot

首先了解下Pixel 6的启动流程能够帮助更好的找到并理解分析目标的功能作用。高通公开文档 中介绍了SecureBoot的启动以及image之间的认证过程。

设备厂商会持有一组私钥(private key)并将其公钥存写入bootROM中,并将这些公钥的hash烧写到硬件fuse中确保bootROM中的公钥不会被篡改,这些公钥即可作为设备的信任根。SecureBoot Chain由存放在bootROM中的信任根开始,每个阶段都会遵循镜像加载、RSA认证、执行到下一阶段,以次来保证整个启动流程执行流是没有被篡改的。

SecureBootChain.png

quarkslab总结了总结了高通SecureBoot的启动过程,如下图:

SecureBoot.png

  • bootROM中包含了PBL(Primary Boot Loader)部分的流程,是CPU最先加载和执行的代码; 在这里启动错误或是设备重启过程中的选项, 可以进入EDL, 在这个模式下可以刷机(也就是传说中的9008刷机)

  • 启动的第二个阶段是SBL(Secondary Boot Loader) 或者叫XBL(eXtended Boot Loader),在这个阶段会在EL3中加载、验证并执行Secure Monitor, QSEE (QTEE),成功后会进入EL1中加载ABL(Android Boot Loader)

  • ABL 会按照AVB(Android Verify Boot)的流程验证并执行Android相关的镜像分区,也可以进入fastboot模式,在这个模式下也可以刷机(线刷)

ABL 的逆向分析与定制fastboot

本次的漏洞是可以通过fastboot完成利用的,因此分析的目标是abl。解压ROM包中的bootloader.img可以使用imjtool

./imjtool path/to/bootloader.img extract

解压后可以获取到adl文件,通过IDA可以分析,分析时注意选择ARM Little-endian,并配置好ROM 起始地址, 确认ROM的其实地址可以通过先设置成0用IDA解析一下,在起始位置附近找一下,很快就可以确定这个地址。

IDA_config.png

(插句题外话,在Eshard的第一篇文章Pixel 6 BootUp中对Android 12版本rom的abl有非常详细的解析过程,为了不完全照搬Eshard文章的成果本节后续对abl的解析对应的是Android 13版本ROM)

据说如果使用8.0以上版本的IDA加载abl完成后会自动解析出大部分函数。我手上的IDA并没有那么高的版本,需要搞一段IDA scripts自动生成下function就可以:

1
2
3
4
5
6
7
8
9
10
11
12
13
import idc 
import idautils
import idaapi

addr = 0xFFFF0000F887C800
end = 0xFFFF0000F8920000
while addr < end and addr != BADADDR:
if isUnknown(GetFlags(addr)):
add_func(addr)
addr = NextHead(addr, BADADDR)
else:
addr = NextAddr(addr)
print("Done.")

通过fastboot的命令与abl交互,可以获取abl返回输出的字符串信息,通过这些字符串就可以定位到abl中处理对应命令的函数中。再通过全局搜索处理函数的入口地址就可以找到abl中处理fastboot命令的函数映射表:

command_struct.png

ABL_cmd.png

继续回溯和分析这些命令的解析过程,可以找到abl与fasboot之间命令交互的循环函数,以及不断的深入分析挖掘更多功能的函数

comman_entry.png

为了更好的利用漏洞,需要对abl的代码做更多深入的分析,找到一些地址读写的原语(primitives)

READ/WRITE Primitives

(以下的代码,适用于Android 12的abl,因为需要逆向不少东西,笔者犯懒不想在Android13 的abl上慢慢逆代码了…)

首先是写原语(write primitives)

write_prim.png

这段代码在fastboot与abl的交互命令列表中,通过download命令就可以直接触发到。观察上面的代码,当download_ptr1为空的,写入地址会被设置成0xffff000090700000。也就是说通过fastboot命令可以上传一段代码到这个地址中,在构造ROP chain的时候,劫持PC到这个地址即可执行代码(当然首先需要这段内存空间是可执行的)

下面是读原语(read primitives)

read_prim.png

这段代码在fastboot与abl的交互命令列表中,通过upload命令就可以直接触发到。观察下这段代码,当能够控制upload_src以及upload_size就可以将对应的内存数据通过usb传输回来

至此,对于ABL的静态分析暂时就告一段落了,想要快速找到已经被修复的Nday可以使用bindiff去对比修复前后版本之间的差异,找到修复位置,并以此推理可能存在的Nday利用方法。但实际上,我对着Android 12&13版本的abl对比好久也没能高效的找到修复函数,也许姿势还是略有问题吧….

定制 fastboot

在逆向分析到abl处理的fastboot命令中,会有一些不常用的命令(比如download). 这些很可能是一些底层的命令,fastboot在处理用户传入的部分命令会使用到这些接口与abl交互,但这些接口并没有以fastboot命令的形式提供给用户使用,因此,需要定制下fastboot扩展出对应的命令接口。

fastboot的代码是开源的,可以和AOSP一起编译或者在AOSP环境中独立编译模块:

mkdir pixel6

repo init -u https://aosp.tuna.tsinghua.edu.cn/platform/manifest -b android-12.0.0_r32

repo sync -c –optimized-fetch -j16 #(or more or less)

source build/envsetup.sh

godir fastboot # system/core/fastboot

mma

当然,就算是独立模块编译也是非常耗时的,况且AOSP已经不再支持在MacOS系统上编译,也给编译过程带来不小的麻烦。

不想麻烦的话,也可以尝试下使用Google提供的python-adb来执行fastboot命令, 从代码上看这个库在python上实现了fastboot/adb协议通信,也就更方便的定制需要的fastboot命令了。

ABL Emulation

一般在找到漏洞位置之后,搭建好一套用于调试的环境能够更加方便的编写exploit。这也是系列文章里面最吸引我的部分,使用unicorn, keystone, capstone共同模拟运行起abl核心代码。

keystone & capstone

这两个工具是很好用的快速指令编译与反编译的python库。

通过capstone可以实现快捷的指令反汇编:

1
2
3
4
5
6
import capstone

disassembler = capstone.Cs(capstone.CS_ARCH_ARM64, capstone.CS_MODE_ARM)
def disas(code, addr):
for insn in disassembler.disasm(code, addr):
print(f"addr = 0x{insn.address}\t opstr = {insn.op_str}")

通过keystone可以实现汇编指令的快速编译生成shellcode. (貌似用pwntools的子模块也可以实现类似的功能)

1
2
3
4
5
6
import keystone

def gen_shellcode(data, address):
ks = keystone.Ks(keystone.KS_ARCH_ARM64, keystone.KS_MODE_LITTLE_ENDIAN)
ret = ks.asm(data, address)
return bytes(ret[0])

Unicorn

一些非常好用的函数

  • 打印调用栈:
1
2
3
4
5
6
7
8
9
10
def print_stack(uc:Uc, num=20):
sp = uc.reg_read(UC_ARM64_REG_SP)
print(f"## STACK DUMP ##\nSP: {sp:x}")
for idx, stack in enumerate(range(sp,sp+num*8,8)):
if sp != 0:
v = struct.unpack('Q', uc.mem_read(stack, 8))[0]
print(f"@{stack:x} {v:x} - #{idx*8:x}")

if idx >=num:
break
  • 打印寄存器信息(for ARM64)
1
2
3
4
5
def print_regs(uc:Uc):
for reg in ["X0","X1","X2","X3","X8","X19","X20","X21","X22","X23","X24","X28","X29","X30","SP","PC"]:
val = eval(f"uc.reg_read(UC_ARM64_REG_{reg})")
print(f"{reg} - {val:8X}",end=" ")
print("")
  • 自动修复缺页异常
1
2
3
4
5
6
7
8
9
def hook_mem_invalid(uc:Uc, uc_mem_type, addr,size, value, user_data):
PAGE_SIZE=10*1024*1024
pc = uc.reg_read(UC_ARM64_REG_PC)
start = addr & ~(PAGE_SIZE - 1)
uc.mem_map(start, PAGE_SIZE)
return True


uc.hook_add(UC_HOOK_MEM_INVALID, hook_mem_invalid)

逆向ABL获取的一些列地址

1
2
3
4
5
6
7
8
9
10
11
12
fastboot_read = 0xFFFF0000F8871408
download_buffer = 0xffff000090700000
__debug_stdio_write = 0xFFFF0000F88A6D5C
fastboot_write = 0xFFFF0000F8871484
pixel_loader_entry_run = 0xFFFF0000F8813D74
stop_fastboot = 0xFFFF0000F8ACBD20

ABL_LOAD_ADDRESS = 0xFFFF0000F8800000
MEMORY_START = 0xFFFF0000F8000000
MEMORY_SIZE = 200*1024*1024
STACK_START = MEMORY_START + MEMORY_SIZE - 0x1000
PAGE_SIZE = 10*1024*1024

利用地址启动Unicorn

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
try:
mu = Uc(UC_ARCH_ARM64, UC_MODE_ARM)

mu.mem_map(MEMORY_START, MEMORY_SIZE)
mu.mem_map(0xd8000000, PAGE_SIZE)
mu.mem_map(0xf8200000, PAGE_SIZE)
mu.mem_map(0xffffffff19200000, PAGE_SIZE)
mu.mem_map(0xfffffffff8200000, PAGE_SIZE)
mu.mem_map(0xffff000080000000, PAGE_SIZE)
mu.mem_map(0xffff000002000000, PAGE_SIZE)
mu.mem_map(0xffffffff10000000, PAGE_SIZE)
mu.mem_map(download_buffer,1024*1024*5) #download buffer


        mu.hook_add(UC_HOOK_MEM_INVALID, hook_mem_invalid)
mu.hook_add(UC_HOOK_CODE, hook_fastboot_read, begin=fastboot_read,end=fastboot_read)
mu.hook_add(UC_HOOK_CODE, hook_stdio_write, begin=__debug_stdio_write,end=__debug_stdio_write)
mu.hook_add(UC_HOOK_CODE, hook_fastboot_write, begin=fastboot_write,end=fastboot_write)
except UcError as e:
print(f"UcError ==> {e}")

总结体验

相比于qemu,通过unicorn实际上只能从某些函数为入口,运行部分代码功能。如果这部分代码的执行对整体的运行时环境没有太多的依赖的话,是个不错的代码调试方式。但是这需要对整体的代码功能理解有更高的要求。

个人感觉unicorn比较适合于一些简单执行流的调试或是一些小固件的模拟执行,通过unicorn也可以在设计exploit chain过程中,对每个模块独立调试。后续有机会的话,善用起来会是很不错的脚手架工具。

参考文档