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!