Solved by 4rbit3r
It took me a while to get the final exploit working for this challenge, but it was fun pwning this binary.
We’re given the executable, the libc (more CTF’s should do this. Saves a lot of time spent on unnecessary version hunting) and the source code too! Well, so no need to spend time on reversing.
Almost every protection has been enabled on the binary.
$ checksec bigpicture
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
FORTIFY: Enabled
The plus side is that the source code is small and really simple to understand.
The binary asks us for two integers to be passed as input to calloc
. It then stores the pointer returned by calloc in a global variable called buf. It then goes into a loop which breaks if we give “quit” as input. Inside the loop the binary asks for three inputs, two co-ordinates and a character. It then goes onto to call a function plot
with these three inputs as arguments. Plot checks if the co-ordinates are greater than the specified height and width. If not, it calls another function get
with the co-ordinates as argument. This get
basically returns a pointer to the character at the co-ordinates specified. The plot
function then checks if the byte at the pointer returned is 0. If not, it prints out the character that exists at the address. If 0, the character we gave as input is written at the address. There’s also a function draw
but I didn’t find it useful in any way.
So, now that we’ve covered what the binary does, we can start pwning this.
The vuln in the binary is that plot
only checks the upper bound of the co-ordinates. So we could give negative integers as co-ordinates which is an out of bounds access.
Now in order to actually do anything useful with this vuln, we need to be able to access either the libc or the bss segment. We can’t access the bss since PIE is enabled.
So the option left is the libc. Now what we can do is to force calloc to return a pointer which is in the libc’s bss. This can be done if the arguments passed to calloc is large enough.
A call to calloc() with arguments 20,20 returns a pointer to the heap.
A call to calloc() with arguments 1056,1056 however returns a pointer to the libc’s bss.
So once we’ve got that allocated, we can exploit the vulnerability to mess up something in the libc that lies at a lower address than our pointer, namely the glibc hooks.
So what I did was to find the offset to __realloc_hook and leak out its contents. Once I had gotten that done, I could then move on to overwrite the __free_hook with the one-shot-RCE gadget which will be invoked once free is called (which happens right after we enter “quit”). Easy enough. But there was a difference in the offsets to __realloc_hook when I tried my exploit remotely. So I kept leaking bytes from some random offsets. What I found was that the bytes being leaked out never changed. So either PPP forgot to turn on ASLR or I’m leaking out some constants. Of course it’s the latter.
I searched the process memory for the sequence of bytes that I had leaked out and found out that they were from the text section of the libc. From that, I could calculate the correct offset to the hooks. The only way I could verify my offset was to leak out a pointer and calculate libc’s base address and check if it was page aligned.
After all that, there was still another problem, the constraints for the one-shot-RCE gadget weren’t met. So I spent quite some time on trying to find some way to pivot the stack and do a ROP.
That was a failed attempt. What I noticed was that one of the one-shot-RCE gadgets would work if I could move the stack pointer 8 bytes towards a lower address. So I started to look for something to do that.
I found this call qword [rdi]
gadget which would do the trick. All I needed to do was to fill the address of the one-shot-RCE gadget in first 8 bytes of the buffer and overwrite the address of the call_rdi gadget in the __free_hook. So while executing free, the __free_hook would be invoked which would then execute the call_rdi gadget which then executes the one-shot-RCE gadget.
And well, that finally worked and landed a shell.
Here’s the script
from pwn import * import sys offset = 0xf1500 remote_offset = 0x10e500 call_rdi = 0x0007d8b0 def leak_stuff(target): addr = 0 for x in xrange(6): y = -1*(target-(5-x)) p.sendlineafter(">","{} , {} , c".format(0,y)) p.recvuntil("overwriting ") byte = p.recv(1) addr = (addr << 8) + ord(byte) return addr def write_stuff(target,payload): for x in xrange(6): y = -1*(target-x) byte = (payload >> (8*x)) & 0xff p.sendlineafter(">","{} , {} , {}".format(0,y,chr(byte)) if __name__ == "__main__": if sys.argv[1] == "local": p = process("./bigpicture") elif sys.argv[1] == "remote": p = remote("bigpicture.chal.pwning.xxx",420) offset = remote_offset p.sendlineafter("? ","1056 x 1056") free_offset = offset - 0x1c98 libc = leak_stuff(offset+8) - 0x84e50 log.success("Leaked libc @ {}".format(hex(libc))) one_gadget = libc + 0xf0567 call_rdi += libc write_stuff(free_offset,call_rdi) write_stuff(0,one_gadget) p.sendline("quit") p.recvline() p.sendline("cat /home/bigpicture/flag") log.success(p.recvline())
Cool, we did the same, but we didn’t need to worry about one shot: The `__free_hook` is called with rdi set to the address of the alloced buffer. That means if we just wrote “/bin//sh” at the very start, overwriting the hook with `system` is enough.
LikeLike
Nice. I’ll probably have to use that same method some other time. One shot gadgets aren’t reliable.
Thanks
LikeLike
Good writeup! The pointer returned by the large calloc() size request is not an address within libc’s BSS though. The address returned is actually an address within a newly mmap-ed rw-p section.
LikeLike
Oh, yea you’re right. My bad.
LikeLike