TUM CTF 2016: zwiebel

zwiebel (the German word for onion) was a reversing challenge for 55 points in this year's TUM CTF.

I found this onion in my kitchen, may I ask you to dissect it?

https://www.youtube.com/watch?v=LowwCyZHBBk

download

Executing the binary it asks us to enter a key. After entering some string the program responds with a sad face. Our input was not correct, so we have to find find out a valid key.

$ ./zwiebel
Input key: foobar
:(

Disassembling the main function in radare2, we can see, what's going on.

            ;-- main:
╒ (fcn) sym.main 128
│           ; DATA XREF from 0x004006ed (entry0)
│           0x00400800      4157           push r15
│           0x00400802      4156           push r14
│           0x00400804      53             push rbx
│           0x00400805      bf07094000     mov edi, str.Input_key:     ; "Input key: " @ 0x400907
│           0x0040080a      31c0           xor eax, eax
│           0x0040080c      e84ffeffff     call sym.imp.printf
│           0x00400811      488b3d885722.  mov rdi, qword [rip + 0x225788] ; [0x625fa0:8]=0x616962654428203a LEA obj.__TMC_END__ ; ": (Debian 6.1.1-11) 6.1.1 20160802" @ 0x625fa0
│           0x00400818      e883feffff     call sym.imp.fflush
│           0x0040081d      488b158c5722.  mov rdx, qword [rip + 0x22578c] ; [0x625fb0:8]=0x2e312e3620293131 LEA obj.stdin__GLIBC_2.2.5 ; "11) 6.1.1 20160802" @ 0x625fb0
│           0x00400824      41bf80126000   mov r15d, str.hxp_th15_15_c3rt41nly_n0t_th3_fl4g_ ; "hxp{th15_15_c3rt41nly_n0t_th3_fl4g}" @ 0x601280
│           0x0040082a      bf80126000     mov edi, str.hxp_th15_15_c3rt41nly_n0t_th3_fl4g_ ; "hxp{th15_15_c3rt41nly_n0t_th3_fl4g}" @ 0x601280
│           0x0040082f      be90000000     mov esi, 0x90
│           0x00400834      e847feffff     call sym.imp.fgets
│           0x00400839      bf00000000     mov edi, 0
│           0x0040083e      be8d4c0200     mov esi, 0x24c8d
│           0x00400843      ba07000000     mov edx, 7
│           0x00400848      b922000000     mov ecx, 0x22               ; '"'
│           0x0040084d      41b8ffffffff   mov r8d, 0xffffffff         ; -1 ; -1
│           0x00400853      4531c9         xor r9d, r9d
│           0x00400856      e8f5fdffff     call sym.imp.mmap
│           0x0040085b      4989c6         mov r14, rax
│           0x0040085e      be10136000     mov esi, obj.shc            ; obj.shc
│           0x00400863      ba8d4c0200     mov edx, 0x24c8d
│           0x00400868      4c89f7         mov rdi, r14
│           0x0040086b      e820feffff     call sym.imp.memcpy
│           0x00400870      4c89fb         mov rbx, r15
│           0x00400873      31c0           xor eax, eax
│           0x00400875      41ffd6         call r14
│           0x00400878      31c0           xor eax, eax
│           0x0040087a      5b             pop rbx
│           0x0040087b      415e           pop r14
│           0x0040087d      415f           pop r15
╘           0x0040087f      c3             ret

It reads 144 byte from standard input and writes the result in a buffer filled with a fake flag. Then it uses mmap to map a new region of memory, copies an amount of data from the symbol shc to that region and calls it.

      ;-- shc:
      ; DATA XREF from 0x0040085e (sym.main)
      0x00601310      8db600000000   lea esi, dword [rsi]
      0x00601316      8d742600       lea esi, dword [rsi]
      0x0060131a      8db426000000.  lea esi, dword [rsi]
      0x00601321      8dbc27000000.  lea edi, dword [rdi]
      0x00601328      4889d8         mov rax, rbx
      0x0060132b      8a4000         mov al, byte [rax]
      0x0060132e      2440           and al, 0x40
 ┌──< 0x00601330      7416           je 0x601348
 ││   0x00601332      488d35340000.  lea rsi, qword [rip + 0x34] ; 0x60136d
 ││   0x00601339      ad             lodsd eax, dword [rsi]
 ││   0x0060133a      4889c1         mov rcx, rax
 ││   0x0060133d      ad             lodsd eax, dword [rsi]
┌───> 0x0060133e      3106           xor dword [rsi], eax
│││   0x00601340      4883c604       add rsi, 4
└───< 0x00601344      e2f8           loop 0x60133e
┌───< 0x00601346  ~   eb2d           jmp 0x601375
│││   ;-- str._h:__n:
│││   0x00601347     .string "-h:(\\n" ; len=6
│ │   0x0060134d      b801000000     mov eax, 1
│ │   0x00601352      bf00000000     mov edi, 0
│ │   0x00601357      4889e6         mov rsi, rsp
│ │   0x0060135a      ba03000000     mov edx, 3
│ │   0x0060135f      0f05           syscall
│ │   0x00601361      b83c000000     mov eax, 0x3c               ; '<'
│ │   0x00601366      bf01000000     mov edi, 1
│ │   0x0060136b      0f05           syscall
│ │   0x0060136d  ~   0a930000eb28   or dl, byte [rbx + 0x28eb0000]
│ │   ;-- str._F_f___:
│ │   0x00601372     .string "(F<f\\`<" ; len=8
  │   0x0060137a      a19eb6ab3562.  movabs eax, dword [0x3e9f3e6235abb69e] ; [0x3e9f3e6235abb69e:4]=-1
  │   0x00601383      0e             invalid                     ; [0x3e9f3e6235abb69e:4]=-1
  │   0x00601384      b1de           mov cl, 0xde
  │   0x00601386      1c46           sbb al, 0x46
      ...

The address of our input buffer is stored in ebx. The code loads the first byte of our input and checks if the bit 0x40 is set.

If that is not the case an error message is printed and the program exits. Otherwise it loads two words relative to the instruction pointer (qword [rip + 0x34]). The first is used as a counter and the second is xored to the following words in a loop. When the counter reaches zero a jump to the decrypted code is executed.

If we try to step through the binary with GDB, the program exits before reaching the main function. The reason is hidden in a function named __printf which is called during startup.

╒ (fcn) sym.__printf 45
│           0x004007d0      50             push rax
│           0x004007d1      31ff           xor edi, edi
│           0x004007d3      31f6           xor esi, esi
│           0x004007d5      31d2           xor edx, edx
│           0x004007d7      31c9           xor ecx, ecx
│           0x004007d9      31c0           xor eax, eax
│           0x004007db      e8d0feffff     call sym.imp.ptrace
│           0x004007e0      4885c0         test rax, rax
│       ┌─< 0x004007e3      7504           jne 0x4007e9
│       │   0x004007e5      31c0           xor eax, eax
│       │   0x004007e7      5a             pop rdx
│       │   0x004007e8      c3             ret
│       └─> 0x004007e9      bf04094000     mov edi, 0x400904
│           0x004007ee      e84dfeffff     call sym.imp.puts
│           0x004007f3      bfffffffff     mov edi, 0xffffffff         ; -1 ; -1
╘           0x004007f8      e833feffff     call sym.imp._exit

It checks whether any process is attached to it using ptrace. If that is the case the program exits with an error message. This is an anti-debugging method, but we can replace the jump with NOPs, such that the exit is never executed.

In each iteration one bit of the input is checked and the code of the next iteration gets decrypted. Since there seemed to be a lot of iterations, we have written a script (see below) so solve this challenge. It uses the capstone engine to disassemble the code, extracts offset and value of the checked bit and decrypts the next section. After 1605 iterations the code exits successfully with ":)" and we have recovered all bits of the flag:

hxp{1_h0p3_y0u_d1dnt_p33l_th3_0ni0n_by_h4nd}

Appendix

The full solution:

#/usr/bin/env python3

from binascii import hexlify, unhexlify
from struct import pack, unpack
import IPython
from capstone import *

addr_data = 0x601270
len_data = 0x24d2d
off_data = 0x1270
addr_shc = 0x601310
len_shc = 0x24c8d
addr_buf = 0x601280

off_shc = addr_shc - addr_data

with open('zwiebel', 'rb') as f:
    f.seek(off_data)
    data = f.read(len_data)


shc = bytearray(data[off_shc:])
mov_rax_rbx_bytes = unhexlify('4889d8')

print(len(shc))


md = Cs(CS_ARCH_X86, CS_MODE_64)
md.detail = True

constraints = []


def xor(offset, size, bites):
    print("[+] XOR 0x{:x}, 0x{:x}, {}".format(offset, size, hexlify(bites)))
    for i in range(size * 4):
        shc[offset+i] ^= bites[i%len(bites)]

def print_inst(i):
    print("0x{:04x}:  {:14s}  {}  {}".format(i.address, hexlify(i.bytes).decode(), i.mnemonic, i.op_str))


def analyze_block(offset):
    global cnt
    b1 = bytes(shc[offset:])
    insts = list(md.disasm(b1, offset))
    try:
        while insts[0].bytes != mov_rax_rbx_bytes:
            insts.pop(0)
    except IndexError:
        for i in md.disasm(b1, offset):
            print_inst(i)
        raise
    i_indx = insts[1]
    print("[+] index instruction")
    print_inst(i_indx)
    indx = i_indx.operands[1].mem.disp
    i_mask = insts[2]
    print("[+] mask instruction")
    print_inst(i_mask)
    mask = i_mask.operands[1].imm
    i_bit_set = insts[3]
    print("[+] bit set instruction")
    print_inst(i_bit_set)
    bit_set = i_bit_set.insn_name() == 'je'
    constraints.append((indx, mask, bit_set))
    i_addr = insts[4]
    print("[+] next block instruction")
    print_inst(i_addr)
    off = insts[5].address + i_addr.operands[1].mem.disp
    off_next = off + 0x8
    xor_ctr = unpack('<I', shc[off:off+4])[0]
    xor_byte = shc[off+4:off+8]
    xor(off_next, xor_ctr, xor_byte)
    print("[+] next block at offset 0x{:x}".format(off_next))
    return off_next


def print_block(offset):
    sys_cnt = 0
    for i in md.disasm(bytes(shc[offset:]), offset):
        print("0x{:04x}:  {:14s}  {}  {}".format(i.address, hexlify(i.bytes).decode(), i.mnemonic, i.op_str))
        if i.insn_name() == 'syscall':
            sys_cnt += 1
        if sys_cnt == 2:
            pass
    print()


blocks = [0x11]

cnt = 1
while True:
    print("[+] block {}".format(cnt))
    try:
        nxt = analyze_block(blocks[-1])
    except:
        break
    blocks.append(nxt)
    cnt += 1

print_block(blocks[-1])

inp = bytearray(0x90)
for off, mask, bit_set in constraints:
    if bit_set:
        inp[off] |= mask
print(inp)