tl;dr
- Buffer overflow in AArch64
- Bypass pointer authentication to leak libc and get shell
Challenge Points: 392
Solves: 32
Solved by: d4rk_kn1gh7, sherl0ck, Cyb0rG, 3agl3
Initial analysis
The challenge handout contained a challenge file, along with libc and loader files. The given binary was of aarch64
architecture, and we were able to setup a debugging environment using qemu
and gdb-multiarch
.
The mitigations enabled on the binary were as follows:
1 | Arch: aarch64-64-little |
The challenge first asked for a name as input, and then a menu driver, which contained 4 options, namely add
, lock
, show
, auth
, and exit
.
1 | input your name: abcd |
Reversing
The name was inputted into the address 0x412030
, on bss, with a max size of 0x20
.
Add:
This function allowed us to input a string identity
, which was converted into an integer value using atoi
, and stored in an array starting from the address 0x412050
, with each consecutive input being stored in index*2
of the array, and allowed a maximum of 5 such inputs.
1 | for ( i = 0LL; ; ++i ) |
Lock:
This function took an integer (say idx
) as input, and called a function encode
on the array at address 0x412050
, with the index being equal to 2*idx
, and stored 1L at index 2*idx+1
, only if there was a value present at that index, and the value at the next index was 0.
1 | printf("idx: "); |
Show:
This function first printed the name entered earlier (value stored at 0x412030
), and then printed each value of the array if they existed, and if the value was not locked (encoded by lock function). If the value was locked, it would print **censored**
.
1 | result = printf("name: %s\n", byte_412030); |
Auth:
This function took an integer (say idx
) as input, and if there was a value present at the array 0x412050
of the entered index, and the value at the next index was 1 (i.e if lock had been called on that index), it compared the value with the result of encode(0x10A9FC70042)
, and if they were the same, it called a function which gave us a 0x100
byte read, and an obvious buffer overflow.
1 | printf("idx: "); |
Encode:
This was a bit of a complicated function, it took a value a1
as input and performed a number of bit-shift and xor operations on the input.
1 | return a1 ^ (a1 << 7) ^ ((a1 ^ (a1 << 7)) >> 11) ^ ((a1 ^ (a1 << 7) ^ ((a1 ^ (a1 << 7)) >> 11)) << 31) ^ ((a1 ^ (a1 << 7) ^ ((a1 ^ (a1 << 7)) >> 11) ^ ((a1 ^ (a1 << 7) ^ ((a1 ^ (a1 << 7)) >> 11)) << 31)) >> 13); |
Vulnerability & bypassing PAC
After reversing, it was pretty clear that we needed to encode a value equal to 0x10A9FC70042
to trigger the buffer overflow, however atoi
only allowed us to input a 4-byte value, so it wasnt possible to enter this value in the add
function. However, there was no check for negative indices in the lock
and auth
functions, so this caused an integer overflow, allowing us to input a negative index (-1 or -2) to access the name
buffer, where we could store this value (0x10A9FC70042
), lock(encode) it and therefore bypass the auth and access the function which gave us a buffer overflow.
Following this, we got PC control, or so we initially thought. The RET
instruction for this overflow function wasn’t a normal RET
, it was instead a RETAA
instruction, which checked whether the pointer was properly PAC-encoded or not, and if it was, the pc value was set to the pointer, and if it was not, the second most significant byte of the pointer was set to 0x20
, making it an invalid address and hence causing a segmentation fault. On further research, we found that this PAC encoding was done using multiple factors - namely the pointer itself, the stack base, and a key which could not be viewed in userspace. Since it wasnt possible to access the key, it wasnt possible to predict this encryption directly.
1 | 0x0000000000400e84 -> Normal pointer to main |
However, we soon realized that the input to the encode
function in lock
was the PAC-encoded version of the original pointer, and we could use show
to leak the result of this (at a negative index only, namely the name buffer). So sherl0ck reversed the encode function, and wrote a function that would return the PAC-encoded pointer, given the original pointer that was passed to lock
and the result of encode
. This way we could get a single PAC-encoded pointer by passing the original into name
, leaking the result of lock(-2)
using show
, and then using the aforementioned function to get the PAC-encoded version of the pointer, thus allowing us to bypass RETAA
.
Exploitation
At this point, we had RIP control, but we could only PAC-encode a single gadget. This meant that all the other return instructions would need to be normal RET
instead of RETAA
instructions. It is important to note that for aarch64
architecture, the first 3 arguments are passed through the registers x0
, x1
and x2
, and the RET
instruction operates in a slightly different way. Instead of popping a value off the stack, it moves the value of x30
into the PC, and continues with program flow.
So to leak libc, we would need a gadget that sets x0
based on a value on the stack, and sets x30
based on a value on the stack. Unlike x32
or x64
ROP, we cannot always link gadgets using RET
, as we may not always have control over the x30
register. And looking through all the gadgets present in the binary, we didn’t find a single gadget that gives us control over both x0
and x30
registers.
Luckily, we found these couple of interesting gadgets:
1 | 0x400ff8 : ldp x19, x20, [sp, #0x10] ; ldp x21, x22, [sp, #0x20] ; ldp x23, x24, [sp, #0x30] ; ldp x29, x30, [sp], #0x40 ; ret |
The first gadget sets the value of the registers x19
, x20
, x21
, x22
, x23
, x24
, x29
and x30
based on values at specific stack offsets, which we had control over, and then calls RET
, which is usable because we have control over x30
. The second gadget however is more interesting. The first instruction is ldr x3, [x21, x19, lsl #3]
, which sets x3
to the values pointed to be x21
, with an index of $x19 * 3
. Since we had control over x21
and x19
, we could just set x19
to 0, and x3
would become the value pointed to by x21
. The following instructions transfer the value of x24
into x2
, x23
into x1
, and x22
into x0
, which allows us to set upto 3 arguments for any function, as we have control over those registers. The next instruction is blr x3
, which essentially calls the subroutine at x3
, and sets x30
to pc + 4
.
So using these two registers, we can essentially set arguments and call any function, as long as we have a pointer to that function. So the plan was to initially call puts@plt
, with a GOT
address as its first argument. Now we needed a pointer to puts@plt
. For this, we can use the add function, as puts@plt
is less than 4 bytes, atoi will return the same value and it will get stored on bss. So then 0x412050
contained a pointer to puts@plt
, and we were able to leak libc addresses.
For the final part of the exploit, we had to be able to return to main, and repeat the same process to call system("/bin/sh")
, as we had libc leaks. I looked up the instructions following blr x3
, and I saw the following:
1 | blr x3 ; cmp x20, x19 ; b.ne #0x400ff4 ; ldp x19, x20, [sp, #0x10] ; ldp x21, x22, [sp, #0x20] ; ldp x23, x24, [sp, #0x30] ; ldp x29, x30, [sp], #0x40 ; ret |
This essentially meant that if x20
and x19
were equal, it would skip the jump, and then we had control over x30
, which would subsequently give us control over PC and allow us to return to main. After puts
was called, the value of x19
was 1, so I set x20
to 1 with the gadgets used earlier, and we got PC control!
THe rest of the exploit was straightforward. As we couldnt get a pointer to system(or any other libc gadget), as they were greater than 4 bytes, we PAC-encoded then returned back to the same gadgets used earlier, and instead of returning to main, we used the following gadget to pass a pointer to /bin/sh
into x0
, and then call system
:
1 | 0x63c0c : ldr x0, [sp, #0x18] ; ldp x29, x30, [sp], #0x20 ; ret |
Following this, we got a shell!
Exploit script
1 | #!/usr/bin/python |
Flag
1 | d4rk_kn1gh7 @ BatMobile python exp.py |