social



Hackover CTF 2016 - tiny_backdoor writeup

This challenge was split in two parts; in both of them we get a tiny binary (680 and 720 bytes long, respectively). Let's look at the first one with radare2:

(ctf)tr@karabut.com:~/work/hackover16/tiny$ radare2 tiny_backdoor_v1 
[0x004000b0]> pd 70
   ;      [0] va=0x004000b0 pa=0x000000b0 sz=134 vsz=134 rwx=-r-x .text
            ;-- section..text:
            0x004000b0    55           push rbp
            0x004000b1    4889e5       mov rbp, rsp
            0x004000b4    e828000000   call 0x4000e1
               0x004000e1(unk) ; entry0
            0x004000b9    488d3c253f0. lea rdi, [0x60013f]
            0x004000c1    488b3425870. mov rsi, [0x600187]
            0x004000c9    488d1425360. lea rdx, [0x600136]
            0x004000d1    31c9         xor ecx, ecx
            0x004000d3    b109         mov cl, 0x9
            0x004000d5    e81f000000   call 0x4000f9
               0x004000f9() ; entry0
            0x004000da    89c7         mov edi, eax
            0x004000dc    6a3c         push 0x3c ; 0x0000003c 
            0x004000de    58           pop rax
            0x004000df    0f05         syscall
               read (0x0,0x0,0x0) ; str.RhZ
            0x004000e1    55           push rbp
            0x004000e2    4889e5       mov rbp, rsp
            0x004000e5    31c0         xor eax, eax
            0x004000e7    31ff         xor edi, edi
            0x004000e9    488d3425360. lea rsi, [0x600136]
            0x004000f1    31d2         xor edx, edx
            0x004000f3    b209         mov dl, 0x9
      |     0x004000f5    0f05         syscall
      |        read (0x0,0x0,0x0) ; str.RhZ
      |     0x004000f7    c9           leave
      |     0x004000f8    c3           ret
      |     0x004000f9    55           push rbp
      |     0x004000fa    4889e5       mov rbp, rsp
      |     0x004000fd    4883ec20     sub rsp, 0x20
      |     0x00400101    4989c9       mov r9, rcx
      |     0x00400104    4989f2       mov r10, rsi
      |     0x00400107    4889d6       mov rsi, rdx
      |     0x0040010a    4d31c0       xor r8, r8
      |     0x0040010d    31c9         xor ecx, ecx
      | ,=< 0x0040010f    eb1c         jmp 0x40012d
      |.--> 0x00400111    428a0406     mov al, [rsi+r8]
      |||   0x00400115    8a140f       mov dl, [rdi+rcx]
      |||   0x00400118    30c2         xor dl, al
      |||   0x0040011a    88140f       mov [rdi+rcx], dl
      |||   0x0040011d    ffc1         inc ecx
      |||   0x0040011f    49ffc0       inc r8
      |||   0x00400122    31d2         xor edx, edx
      |||   0x00400124    4c89c0       mov rax, r8
      |||   0x00400127    49f7f1       div r9
      |||   0x0040012a    4989d0       mov r8, rdx
      ||`-> 0x0040012d    4c39d1       cmp rcx, r10
      |`==< 0x00400130    75df         jnz 0x400111
      |     0x00400132    ffd7         call rdi
      |        0x00000000(unk)        
      |     0x00400134    c9           leave
      |     0x00400135    c3           ret
   ;      [1] va=0x00400136 pa=0x00000136 sz=89 vsz=89 rwx=-rw- .data
      |     ;-- section..data:
      |     0x00400136    0000         add [rax], al
      |     0x00400138    0000         add [rax], al
      |     0x0040013a    0000         add [rax], al
      |     0x0040013c    0000         add [rax], al
    ... some data

[0x004000b0]> px 1 @ 0x400187
- offset -   0 1  2 3  4 5  6 7  8 9  A B  C D  E F  0123456789ABCDEF
0x00400187  48                                       H 

What it does is really very simple: it reads 9 bytes from the stdin, stores them in memory, then xors with them the data at 0x40013f-0x400186 (0x60013f-0x600186 at runtime), then calls 0x60013f. So we either have to invent some really short shellcode (we of course can control the first 9 bytes completely) or there's already something useful there and we need to find a correct key. Let's explore the latter possibilty.

First, we know that the 0x60013f address gets called and not jumped in, and that it's probably supposed to return safely, because there's an exit syscall present after it gets called. So we can assume with good deal of certainty the code will start with push rbp; mov rsp, rbp; and end with leave; ret;. Quick glance at Defuse's online assembler gives us the bytes we long for: 55 48 89 e5 and c9 c3. Let's test them; if we're right, that's six of nine needed!

Time to bring capstone in! Let's look at the disassembly starting with all the places we should get six bytes right consecutively:

from capstone import *

md = Cs(CS_ARCH_X86, CS_MODE_64)

def xor(a,b):
        result = ''
        for i in range(len(a)):
                result += chr(ord(a[i])^ord(b[i%len(b)]))
        return result


probable_start = '\x55\x48\x89\xe5'
probable_end = '\xc9\xc3'

f = open("tiny_backdoor_v1","rb")
f.seek(0x13f)
data = f.read(0x48)

probable_key = xor(data[:4], probable_start) + '\x00'*3 + xor(data[-2:], probable_end)
test = xor(data,probable_key)

for i in range(0,0x48,9):
        print "starting with %x:" % (i+7)
        for inst in md.disasm(test[i+7:],i+7):
                print "0x%x:\t%s\t%s" % (inst.address, inst.mnemonic, inst.op_str)

We get the following output:

(ctf)tr@karabut.com:~/work/hackover16/tiny$ python test.py
starting with 7:
0x7:    50      push    rax
0x8:    53      push    rbx
0x9:    6a02    push    2
0xb:    58      pop     rax
0xc:    48a7    cmpsq   qword ptr [rsi], qword ptr [rdi]
starting with 10:
0x10:   0000    add     byte ptr [rax], al
0x12:   00eb    add     bl, ch
0x14:   096646  or      dword ptr [rsi + 0x46], esp
starting with 19:
0x19:   2e7478  je      0x94
0x1c:   7400    je      0x1e
0x1e:   31dc    xor     esp, ebx
0x20:   332f    xor     ebp, dword ptr [rdi]
0x22:   0f05    syscall
0x24:   89c3    mov     ebx, eax
0x26:   89c7    mov     edi, eax
0x28:   1bc2    sbb     eax, edx
0x2a:   b58d    mov     ch, 0x8d
0x2c:   7424    je      0x52
0x2e:   0883c2100df8    or      byte ptr [rbx - 0x7f2ef3e], al
0x34:   6a01    push    1
0x36:   58      pop     rax
0x37:   89c7    mov     edi, eax
0x39:   0f2f68fe        comiss  xmm5, xmmword ptr [rax - 2]
0x3d:   58      pop     rax
0x3e:   89df    mov     edi, ebx
0x40:   0f05    syscall
0x42:   6a68    push    0x68
0x44:   5a      pop     rdx
0x45:   a6      cmpsb   byte ptr [rsi], byte ptr [rdi]
0x46:   c9      leave
0x47:   c3      ret
starting with 22:
0x22:   0f05    syscall
0x24:   89c3    mov     ebx, eax
0x26:   89c7    mov     edi, eax
0x28:   1bc2    sbb     eax, edx
0x2a:   b58d    mov     ch, 0x8d
0x2c:   7424    je      0x52
0x2e:   0883c2100df8    or      byte ptr [rbx - 0x7f2ef3e], al
0x34:   6a01    push    1
0x36:   58      pop     rax
0x37:   89c7    mov     edi, eax
0x39:   0f2f68fe        comiss  xmm5, xmmword ptr [rax - 2]
0x3d:   58      pop     rax
0x3e:   89df    mov     edi, ebx
0x40:   0f05    syscall
0x42:   6a68    push    0x68
0x44:   5a      pop     rdx
0x45:   a6      cmpsb   byte ptr [rsi], byte ptr [rdi]
0x46:   c9      leave
0x47:   c3      ret
starting with 2b:
0x2b:   8d742408        lea     esi, dword ptr [rsp + 8]
0x2f:   83c210  add     edx, 0x10
0x32:   0df86a0158      or      eax, 0x58016af8
0x37:   89c7    mov     edi, eax
0x39:   0f2f68fe        comiss  xmm5, xmmword ptr [rax - 2]
0x3d:   58      pop     rax
0x3e:   89df    mov     edi, ebx
0x40:   0f05    syscall
0x42:   6a68    push    0x68
0x44:   5a      pop     rdx
0x45:   a6      cmpsb   byte ptr [rsi], byte ptr [rdi]
0x46:   c9      leave
0x47:   c3      ret
starting with 34:
0x34:   6a01    push    1
0x36:   58      pop     rax
0x37:   89c7    mov     edi, eax
0x39:   0f2f68fe        comiss  xmm5, xmmword ptr [rax - 2]
0x3d:   58      pop     rax
0x3e:   89df    mov     edi, ebx
0x40:   0f05    syscall
0x42:   6a68    push    0x68
0x44:   5a      pop     rdx
0x45:   a6      cmpsb   byte ptr [rsi], byte ptr [rdi]
0x46:   c9      leave
0x47:   c3      ret
starting with 3d:
0x3d:   58      pop     rax
0x3e:   89df    mov     edi, ebx
0x40:   0f05    syscall
0x42:   6a68    push    0x68
0x44:   5a      pop     rdx
0x45:   a6      cmpsb   byte ptr [rsi], byte ptr [rdi]
0x46:   c9      leave
0x47:   c3      ret
starting with 46:
0x46:   c9      leave
0x47:   c3      ret

Those pushes, pops and syscalls sure do bolster certainty in our six bytes! Let's see now if we can glean anything from the parts that aren't quite right. Say, at offset 0x34, we have push 1; pop rax; mov edi, eax; and then go into some strange territory for an opcode before returning to sanity with pop rax; mov edi, ebx; syscall. Well well, isn't something supposed to go between? comiss opcode starts with 0f, which of course lends itself to assuming the byte at 0x3a should be 05 to make it 0f 05, or a syscall opcode. So we add it to our probable_key construction:

...

probable_key = xor(data[:4], probable_start) + xor(data[0x3a], '\x05') + '\x00'*2 + xor(data[-2:], probable_end)

...
(ctf)tr@karabut.com:~/work/hackover16/tiny$ python test.py
...
starting with 34:
0x34:   6a01    push    1
0x36:   58      pop     rax
0x37:   89c7    mov     edi, eax
0x39:   0f05    syscall
0x3b:   68fe5889df      push    -0x2076a702
0x40:   0f05    syscall
0x42:   6a42    push    0x42
0x44:   5a      pop     rdx
0x45:   a6      cmpsb   byte ptr [rsi], byte ptr [rdi]
0x46:   c9      leave
0x47:   c3      ret
...

Alright! As there's still a couple bytes wrong, the code flow got a little mangled, but we can easily see what's supposed to happen here (and here's a Linux syscall reference by Ryan Chapman). First it writes something to stdout (file descriptor 1 in edi, rsi contents unknown at the time), and, as we know from the previous output, then it does a syscall with unknown rax popped from stack and edi copied from ebx. So the two remaining bytes should be a push byte opcode, hence the byte at 0x3b is 6a.

...

probable_key = xor(data[:4], probable_start) + xor(data[0x3a], '\x05') + xor(data[0x3b], '\x6a') + '\x00' + xor(data[-2:], probable_end)

...
(ctf)tr@karabut.com:~/work/hackover16/tiny$ python test.py
...
starting with 7:
0x7:    50      push    rax
0x8:    53      push    rbx
0x9:    6a02    push    2
0xb:    58      pop     rax
0xc:    488d3dff000000  lea     rdi, qword ptr [rip + 0xff]
0x13:   eb09    jmp     0x1e
0x15:   666c    insb    byte ptr [rdi], dx
...

At the very start now we notice what seems to be a prelude to an open syscall. lea rdi, qword ptr [rip + 0xff] has only one byte wrong, and it's 0xff; looking at the jump right after it, it's supposed to load rdi with the address at some offset from the next instruction (rip holds its address), to be accepted as the filename in the syscall, then hop over them and continue execution. So what's supposed to be at that offset?..

...

print test[0x15:0x1e]
(ctf)tr@karabut.com:~/work/hackover16/tiny$ python test.py
...
fla�.txt

Well hello there!

Now we know that the byte at 0x18 is supposed to be 'g', or 67. Let's finish it up:

...

probable_key = xor(data[:4], probable_start) + xor(data[0x3a], '\x05') + xor(data[0x3b], '\x6a') + xor(data[0x18], '\x67') + xor(data[-2:], probable_end)

test = xor(data,probable_key)

for inst in md.disasm(test[:0x15],0x0):
        print "0x%x:\t%s\t%s" % (inst.address, inst.mnemonic, inst.op_str)

for inst in md.disasm(test[0x1e:],0x1e):
        print "0x%x:\t%s\t%s" % (inst.address, inst.mnemonic, inst.op_str)
(ctf)tr@karabut.com:~/work/hackover16/tiny$ python test.py
0x0:    push    rbp
0x1:    mov     rbp, rsp
0x4:    sub     rsp, 0x50
0x8:    push    rbx
0x9:    push    2
0xb:    pop     rax
0xc:    lea     rdi, qword ptr [rip + 2]
0x13:   jmp     0x1e
0x1e:   xor     esi, esi
0x20:   xor     edx, edx
0x22:   syscall
0x24:   mov     ebx, eax
0x26:   mov     edi, eax
0x28:   xor     eax, eax
0x2a:   lea     rsi, qword ptr [rsp + 8]
0x2f:   add     edx, 0x3a
0x32:   syscall
0x34:   push    1
0x36:   pop     rax
0x37:   mov     edi, eax
0x39:   syscall
0x3b:   push    3
0x3d:   pop     rax
0x3e:   mov     edi, ebx
0x40:   syscall
0x42:   push    0x42
0x44:   pop     rax
0x45:   pop     rbx
0x46:   leave
0x47:   ret

Now it looks all nice and tidy!

All that's left is sending the key we've found to the server:

import socket

key = 'e6d9f6382a02fd3ac3'.decode('hex')

s = socket.socket()
s.connect(('challenges.hackover.h4q.it', 4747))
s.send(key)
print s.recv(1024)
(ctf)tr@karabut.com:~/work/hackover16/tiny$ python crack.py
hackover16{do0_d0O_dOo_l00k1n_0uT_7h15_b4ckd00r,_dO0_d0O}

Yes! But what about the second binary? A cursory look reveals that all that it does differently is it calls mprotect at the memory region where the xorred code is located, preventing writes, but does it after the xorring is done, so if we used some custom shellcode, we wouldn't be able to rewrite anything there on the go. But we don't need it at all, and what's more, the xorred code stays exactly the same, so the challenge is already solved. All that's left is to add an extra 4 to the port number:

...
s.connect(('challenges.hackover.h4q.it', 47474))
...
(ctf)tr@karabut.com:~/work/hackover16/tiny$ python crack.py
hackover16{7h3_64ckd0Or_M4n_1s_kn0ck1ng_47_y0uR_b4ckdO0r}

Two flags for the price of one. Nice!