This CTF challenge was a series of 6 challenges pertaining to AArch64 privilege escalation, which is similar to ARM64. The main differences are removal of Thumb instructions and doubling the general purpose registers. There are four exception levels for this: EL0, EL1, EL2 and EL3. EL0 is user mode, EL1 is supervisor, EL2 is hypervisor and EL3 is the firmware. On top of this, everything has a secure and non-secure world as well using ARM TrustZone.
To run this, the particpiants had to use QEMU (with patches) with a BIOS image. The first stage is a statically linked ELF binary with an arbitrary call vulnerability via a back of index checking. To get code execution within this stage, an attacker can call the gets() with stdin to overwrite the function pointers within the .bss section.
The LIBC is a modified version (of course), since it runs a custom minified OS. To attack the kernel, we're going to need the ability to execute arbitrary code easily. To do this, they wrote some shellcode then used the overflow to call mprotect() to make a section of memory executable. With this, we have escaped EL0 and can start attacking EL1.
They detail their process of reversing the EL1 code, including an IDAPython extension that comments about MSRs and very low level instructions. Once they understood the memory mapping they began hunting for SVC handler (syscall) vulnerabilities. There is a classic vulnerability: missing validation on the destination address. This is similar to copy_to_user in Linux. using this, it's an easy write-what-where primitive within the kernel without any KASLR.
Unfortunately, the read() syscall can only write a single byte at a time. This means we can't simply corrupt the return address on the stack to get code execution; we'll need to do something else. Eventually, they found a gadget that allows them to corrupt a single byte but jump to an arbitrary location on the PC. Before this, they need to write their shellcode into the kernel using the syscalls.
Right now, there is only a limited payload that can be run. So, the author wanted to go from having a flag to full on code execution. Modern kernels have Supervisor Mode Execution Prevention (SMEP) which prevents the kernel from executing code on userland pages. While stepping in GDB, they found that this setting was not turned on but got a page fault. Why is this?
When a processor receives a request to go from a virtual address to a physical address it does a page walk with multiple lookups. As a result, there is a Translation Lookaside Buffer (TLB) to speed up this process. These page tables have attributes like access permissions, execution permissions and more. After learning all of this, the author realized that their userland memory had bad permissions. So, they simply used their arbitrary write primitive to corrupt these bit to make the page executable from EL1. Nice!
To communicate with EL2, there are hypervisor calls (hvc). The only functions into EL2 were for memory allocation is via mmap(). This takes in two parameters: an address and attributes. While looking at this functionality, there is a check to ensure that the hypervisor memory region cannot be written. However, the check is performed on the physical address and attributes separately before oring the bytes together. So, we can put swap the inputs to bypass the validation but map the intended address!
This creates a window between the EL2 and EL1 address spaces. By calling a function in EL1 with the already mapped address from EL2, we can write into EL1 from EL2! They wrote EL2 code byte-by-byte into the RESET vector of the hypervisor. Then, once they triggered the RESET, their shellcode would execute.
This is a fire post on exploiting very low level systems. This is just part 1; we need to go into the secure world now. Overall, loved this post and the challenges.