HackTM 2020 - mama bear (499pt)

jit reversing + z3

February 5, 2020
reversing

mama bear (499pt)

Reversing

Description: Author: trupples

Despite Goldilocks' bad encounters with the bear family, mama bear seems to like her, and they’ve been talking using cryptography. Papa bear remembers hearing her say: “Haha more like 8ba409960881fbab676e7e4a47447770b365d57c186169286b2f064d0b434bf6”

Can you find out what she actually told Goldilocks?

P.S. to make your search easier, baby bear has noticed Goldilocks always starts her passwords with a capital X and ends them with a capital W.

Files:

Overview:

We are given an ELF mama_bear that takes an 8-byte password and a 32-byte “secret” and spits out a new 32-byte message:

mama_bear

Our goal is to find the correct password and secret so that we see the output: 8ba409960881fbab676e7e4a47447770b365d57c186169286b2f064d0b434bf6

Reversing:

The program is pretty small and our _start function consists of some string printing calls, a call to read user input and several calls to this function:

mama_bear

Additionally, we find this following suspicious data in memory:

mama_bear

This pattern of a switch statement inside a loop usually indicates some sort of virtual machine. The suspicious memory string is likely our encoded program.

After some reversing, we see that rsi is acting as our program counter in this function. However the opcode cases look strange: there are weird constants and it’s hard to understand what the function is. For example here is the case for the - opcode:

mama_bear

So we are writing some strange constant to [rdi] loading another byte from rsi and writing more data to rdi. We can also see that at the start of this function, rdi is set to the address of a subroutine:

mama_bear

Hmmm… so maybe we should interpret these constants as machine code…

When we do that it makes a lot more sense. For the - opcode, we have the pattern:

0:  8a 04 24                mov    al,BYTE PTR [rsp]
3:  ff c4                   inc    esp
5:  a2 5e 15 60 00 00 00    movabs ds:0x60155e+<x>,al

where <x> is the value encoded in the second byte of the opcode (ascii value minus 0x30).

So this opcode is pushing x86 instructions that implement a POP operation. Specifically, we are using the native stack pointer (rsp) and it looks like 0x60155e is some sort of memory buffer.

As a whole, this function is implementing a sort of JIT-compilation step to translate the program into x86. The final opcode ! adds the function prologue and executes the instruction.

Following this same pattern we can decode all the operations:

#<x>   :: M[x:x+2] >>= (pass & 7)  :: rotate two bytes at a 
                                   :: time by the current pass byte

<x>    :: PUSH M[x]                :: push M[x] to the stack

-<x>   :: POP M[x]                 :: pop into M[x]

&      :: MUT                      :: pop the top two values,
                                   :: mutate them with a crypto
                                   :: func and push them back

!      :: EXEC                     :: execute the JIT function

<x> is an integer in the range [0,0x2d) that is encoded by the ascii value x + 0x30.

After some more reversing we see that this VM function is called 9 times, the first time is actually before we enter any input and we see that the memory buffer is set to: https://twitter.com/_trupples <3

Then the vm function is called 8 more times (on 8 subsequent program strings) and each time the next byte of our password input is set in a global variable which influences the operation of the MUT and ROTATE operations (& and #).

Solution:

To solve this problem, I implemented the program execution in z3 at a relatively high level and symbolically solved for the original password and secret.

If you are not familiar, z3 is a constraint solver that allows you to define a set of constraints and obtain a solution. Basically it’s black magic and it comes in very handy for certain types of reversing problems.

I’ll walk through my solution script in this section:

We start with the import:

from z3 import *

Now we can initialize a Solver and create the symbolic variables to hold our initial password and secret:

# init solver
s = Solver()

# add password
password = [BitVec('p%d' % i, 8) for i in range(8)]

# known bytes
s.add(password[0] == ord('X'))
s.add(password[7] == ord('W'))

# add mem
mem = [BitVec('m%d' % i, 8) for i in range(32)]

# keep a reference to the original memory (as we will modify the list above)
orig_mem = [k for k in mem]

Next we can constrain the bytes of the password we know (from the description) and constrain both values to ascii:

# known bytes
s.add(password[0] == ord('X'))
s.add(password[7] == ord('W'))

# constrain password and secret to ascii
for o in orig_mem:
    s.add(And(ULE(o,0x7f), o>0x1f))
    
for p in password:
    s.add(And(ULE(p, 0x7f), p>0x1f))

During the MUT function, we have a relatively complex operation that pops two bytes from the stack, does some bit manipulation and pushes them back:

mama_bear

Rather than try to figure out what this does at a high level, I just implemented the x86 instructions directly in z3. The address at 0x601b8f points to a 4 element array that is computed as a function of the current password character. Since there are only 256 values, I decided to just dump all the possible lookup arrays and index by the current password byte. Using a simple gdb script, I dumped all the possible values:

# dumped with gdb script
BOX = [[1, 2, 3, 0], [2, 1, 3, 0], [3, 1, 2, 0], [0, 1, 2, 3], [1, 3, 2, 0], [2, 3, 1, 0], [3, 2, 1, 0], [0, 2, 1, 3], [1, 0, 2, 3], [2, 0, 1, 3], [3, 0, 1, 2], [0, 3, 1, 2], [1, 2, 0, 3], [2, 1, 0, 3], [3, 1, 0, 2], [0, 1, 3, 2], [1, 3, 0, 2], [2, 3, 0, 1], [3, 2, 0, 1], [0, 2, 3, 1], [1, 0, 3, 2], [2, 0, 3, 1], [3, 0, 2, 1], [0, 3, 2, 1], [1, 2, 3, 0], [2, 1, 3, 0], [3, 1, 2, 0], [0, 1, 2, 3], [1, 3, 2, 0], [2, 3, 1, 0], [3, 2, 1, 0], [0, 2, 1, 3], [1, 0, 2, 3], [2, 0, 1, 3], [3, 0, 1, 2], [0, 3, 1, 2], [1, 2, 0, 3], [2, 1, 0, 3], [3, 1, 0, 2], [0, 1, 3, 2], [1, 3, 0, 2], [2, 3, 0, 1], [3, 2, 0, 1], [0, 2, 3, 1], [1, 0, 3, 2], [2, 0, 3, 1], [3, 0, 2, 1], [0, 3, 2, 1], [1, 2, 3, 0], [2, 1, 3, 0], [3, 1, 2, 0], [0, 1, 2, 3], [1, 3, 2, 0], [2, 3, 1, 0], [3, 2, 1, 0], [0, 2, 1, 3], [1, 0, 2, 3], [2, 0, 1, 3], [3, 0, 1, 2], [0, 3, 1, 2], [1, 2, 0, 3], [2, 1, 0, 3], [3, 1, 0, 2], [0, 1, 3, 2], [1, 3, 0, 2], [2, 3, 0, 1], [3, 2, 0, 1], [0, 2, 3, 1], [1, 0, 3, 2], [2, 0, 3, 1], [3, 0, 2, 1], [0, 3, 2, 1], [1, 2, 3, 0], [2, 1, 3, 0], [3, 1, 2, 0], [0, 1, 2, 3], [1, 3, 2, 0], [2, 3, 1, 0], [3, 2, 1, 0], [0, 2, 1, 3], [1, 0, 2, 3], [2, 0, 1, 3], [3, 0, 1, 2], [0, 3, 1, 2], [1, 2, 0, 3], [2, 1, 0, 3], [3, 1, 0, 2], [0, 1, 3, 2], [1, 3, 0, 2], [2, 3, 0, 1], [3, 2, 0, 1], [0, 2, 3, 1], [1, 0, 3, 2], [2, 0, 3, 1], [3, 0, 2, 1], [0, 3, 2, 1], [1, 2, 3, 0], [2, 1, 3, 0], [3, 1, 2, 0], [0, 1, 2, 3], [1, 3, 2, 0], [2, 3, 1, 0], [3, 2, 1, 0], [0, 2, 1, 3], [1, 0, 2, 3], [2, 0, 1, 3], [3, 0, 1, 2], [0, 3, 1, 2], [1, 2, 0, 3], [2, 1, 0, 3], [3, 1, 0, 2], [0, 1, 3, 2], [1, 3, 0, 2], [2, 3, 0, 1], [3, 2, 0, 1], [0, 2, 3, 1], [1, 0, 3, 2], [2, 0, 3, 1], [3, 0, 2, 1], [0, 3, 2, 1], [1, 2, 3, 0], [2, 1, 3, 0], [3, 1, 2, 0], [0, 1, 2, 3], [1, 3, 2, 0], [2, 3, 1, 0], [3, 2, 1, 0], [0, 2, 1, 3], [1, 0, 2, 3], [2, 0, 1, 3], [3, 0, 1, 2], [0, 3, 1, 2], [1, 2, 0, 3], [2, 1, 0, 3], [3, 1, 0, 2], [0, 1, 3, 2], [1, 3, 0, 2], [2, 3, 0, 1], [3, 2, 0, 1], [0, 2, 3, 1], [1, 0, 3, 2], [2, 0, 3, 1], [3, 0, 2, 1], [0, 3, 2, 1], [1, 2, 3, 0], [2, 1, 3, 0], [3, 1, 2, 0], [0, 1, 2, 3], [1, 3, 2, 0], [2, 3, 1, 0], [3, 2, 1, 0], [0, 2, 1, 3], [1, 0, 2, 3], [2, 0, 1, 3], [3, 0, 1, 2], [0, 3, 1, 2], [1, 2, 0, 3], [2, 1, 0, 3], [3, 1, 0, 2], [0, 1, 3, 2], [1, 3, 0, 2], [2, 3, 0, 1], [3, 2, 0, 1], [0, 2, 3, 1], [1, 0, 3, 2], [2, 0, 3, 1], [3, 0, 2, 1], [0, 3, 2, 1], [1, 2, 3, 0], [2, 1, 3, 0], [3, 1, 2, 0], [0, 1, 2, 3], [1, 3, 2, 0], [2, 3, 1, 0], [3, 2, 1, 0], [0, 2, 1, 3], [1, 0, 2, 3], [2, 0, 1, 3], [3, 0, 1, 2], [0, 3, 1, 2], [1, 2, 0, 3], [2, 1, 0, 3], [3, 1, 0, 2], [0, 1, 3, 2], [1, 3, 0, 2], [2, 3, 0, 1], [3, 2, 0, 1], [0, 2, 3, 1], [1, 0, 3, 2], [2, 0, 3, 1], [3, 0, 2, 1], [0, 3, 2, 1], [1, 2, 3, 0], [2, 1, 3, 0], [3, 1, 2, 0], [0, 1, 2, 3], [1, 3, 2, 0], [2, 3, 1, 0], [3, 2, 1, 0], [0, 2, 1, 3], [1, 0, 2, 3], [2, 0, 1, 3], [3, 0, 1, 2], [0, 3, 1, 2], [1, 2, 0, 3], [2, 1, 0, 3], [3, 1, 0, 2], [0, 1, 3, 2], [1, 3, 0, 2], [2, 3, 0, 1], [3, 2, 0, 1], [0, 2, 3, 1], [1, 0, 3, 2], [2, 0, 3, 1], [3, 0, 2, 1], [0, 3, 2, 1], [1, 2, 3, 0], [2, 1, 3, 0], [3, 1, 2, 0], [0, 1, 2, 3], [1, 3, 2, 0], [2, 3, 1, 0], [3, 2, 1, 0], [0, 2, 1, 3], [1, 0, 2, 3], [2, 0, 1, 3], [3, 0, 1, 2], [0, 3, 1, 2], [1, 2, 0, 3], [2, 1, 0, 3], [3, 1, 0, 2], [0, 1, 3, 2], [1, 3, 0, 2], [2, 3, 0, 1], [3, 2, 0, 1], [0, 2, 3, 1], [1, 0, 3, 2], [2, 0, 3, 1], [3, 0, 2, 1], [0, 3, 2, 1], [1, 2, 3, 0], [2, 1, 3, 0], [3, 1, 2, 0], [0, 1, 2, 3], [1, 3, 2, 0], [2, 3, 1, 0], [3, 2, 1, 0], [0, 2, 1, 3], [1, 0, 2, 3], [2, 0, 1, 3], [3, 0, 1, 2], [0, 3, 1, 2], [1, 2, 0, 3], [2, 1, 0, 3], [3, 1, 0, 2], [0, 1, 3, 2]]

Originally I stored these values in a 1024 element matrix (256 * 2 * 2) and indexed by the current password byte and the two computed bits. However, I found it was more performant to use a 256 element array indexed by the password byte and pack the 4 2-bit values into a single 8-bit BitVec:

# concatenate to improve z3 performance
BOX_CONCAT = []
for b in BOX:
    v = b[0] + (b[1]<<2) + (b[2]<<4) + (b[3]<<6)
    BOX_CONCAT.append(v)

In order to use these result of an indexing operation in a z3 expression, we need to make this array symbolic:

# add the array
S_BOX_CONCAT = Array('box', BitVecSort(8), BitVecSort(8))
for j in range(len(BOX_CONCAT)):
    s.add(S_BOX_CONCAT[j] == BOX_CONCAT[j])

Essentially, we are initializing a symbolic array that maps an 8-bit BitVec to an 8-bit BitVec. Then we are constraining the values of this array to specific (pre-computed) static values.

Now we can implement the MUT function as follows:

# simulate this at a really low level so I don't fuck it up
def mut(pass_byte, a, b, box):
    bl = a
    bh = b
    
    al,bh = bh,bl

    for i in range(8):
        # print(hex(al),hex(bl))
        dl = al
        dh = bl
        al &= 1
        bl &= 1

        dil = LShR(LShR(box[pass_byte], al*4), bl*2) & 3
        sil = LShR(dil, 1)

        al = dl
        al &= 0xfe
        al |= sil

        dil &= 1
        bl = dh
        bl &= 0xfe
        bl |= dil

        al = ((al << 1) | LShR(al, 7)) & 0xff
        bl = ((bl << 1) | LShR(bl, 7)) & 0xff

    bh = al
    bl,bh = bh,bl

    return (bl, bh)

Next we define the function strings (obtained from program memory):

# functions
f1 = 'CBKMNLHOIAJ@DFGE?8:67=<>93;1452-4-:-8-2-F-6-B-L-H-J->-<-D-@-ND&-D-E@&-@-AB&-B-CF&-F-GN&-N-O:&-:-;L&-L-M<&-<-=0&-0-1>&->-?6&-6-72&-2-34&-4-58&-8-9J&-J-KH&-H-I'
f2 = '8:5>;=91?072634<CEM@IBJKLAGNFDH-A-9-=-M-?-3-I-G-E-5-C-1-K-;-77&-7-6G&-G-FI&-I-HC&-C-BK&-K-JA&-A-@O&-O-N1&-1-0M&-M-L=&-=-<5&-5-49&-9-83&-3-2E&-E-D;&-;-:?&-?->'
f3 = 'L7>-L->-76<H?N3-6-3-N-?-H-<K9B-K-B-9=J;FCI-=-I-C-F-;-J8@O124-8-4-2-1-O-@M5:DGA-M-A-G-D-:-5A@&-@-AIH&-H-I98&-8-9GF&-F-G;:&-:-;76&-6-7ON&-N-OKJ&-J-K54&-4-5ML&-L-M32&-2-3ED&-D-E10&-0-1?>&->-?=<&-<-=CB&-B-C'
f4 = 'C7@1LI-C-I-L-1-@-74F=-4-=-FKG?0NM-K-M-N-0-?-GHA3-H-3-A5D9<6B-5-B-6-<-9-DJE;8>2-J-2->-8-;-EFG&-G-FNO&-O-NBC&-C-BDE&-E-D@A&-A-@67&-7-689&-9-8<=&-=-<JK&-K-J01&-1-0HI&-I-H45&-5-423&-3-2LM&-M-L>?&-?->:;&-;-:'
f5 = 'JKFEH@IMCGANLDOB<9;5>728:=463?1-2-N-6-<-8-J-D-@-4->-L-:-F-B-HJ&-J-K0&-0-1F&-F-G6&-6-72&-2-3L&-L-M@&-@-AH&-H-I4&-4-5<&-<-=N&-N-O>&->-?D&-D-EB&-B-C8&-8-9:&-:-;'
f6 = '862:>1?3;=<47950JMFHENDC@ILABKG-?-G-5-3-I-C-1-7-9-M-;-A-=-K-EO&-O-NE&-E-D=&-=-<A&-A-@G&-G-F7&-7-65&-5-49&-9-8I&-I-H1&-1-0M&-M-L3&-3-2;&-;-:K&-K-JC&-C-B?&-?->'
f7 = 'L7>-L->-7I=J;FC-I-C-F-;-J-=9BK-9-K-B5:DGAM-5-M-A-G-D-:1248@O-1-O-@-8-4-236<H?N-3-N-?-H-<-610&-0-154&-4-5?>&->-?ON&-N-OGF&-F-GA@&-@-A32&-2-3ED&-D-E;:&-:-;CB&-B-CIH&-H-IKJ&-J-KML&-L-M=<&-<-=98&-8-976&-6-7'
f8 = '<6B5D9-<-9-D-5-B-6HA3-H-3-A4F=-4-=-F7@1LIC-7-C-I-L-1-@G?0NMK-G-K-M-N-0-?>2JE;8->-8-;-E-J-2HI&-I-HJK&-K-J01&-1-0<=&-=-<LM&-M-LFG&-G-F67&-7-645&-5-4@A&-A-@>?&-?->89&-9-8DE&-E-DBC&-C-BNO&-O-N23&-3-2:;&-;-:' # #2#B#F#4#0#8#>#D#@#H#L#N#:#J#6#<'
# (last one is truncated to remove rotation instructions)

Finally we are ready to symbolically execute the functions:

# symbolically execute a program
def process_func(prog, mem, pass_byte, box):
    STACK = [None] * 32
    stack_i = 0
    
    pc = 0
    while pc < len(prog):
        curr = prog[pc]
        pc += 1
        
        if curr == '#':
            # rotate
            x = ord(prog[pc]) - 0x30
            pc += 1

            ror = (pass_byte & 0x7)
            
            val = Concat(mem[x+1], mem[x])
            val = RotateRight(val, ror)

            mem[x+1] = Extract(15,8,val)
            mem[x] = Extract(7,0,val)
            
        elif curr == '-':
            # pop
            x = ord(prog[pc]) - 0x30
            pc += 1
            
            stack_i -= 1
            mem[x] = STACK[stack_i]
            
        elif curr == '&':
            # mut
            t0 = STACK[stack_i-1]
            t1 = STACK[stack_i-2]
            
            x,y = mut(pass_byte,t0,t1,box)
            
            STACK[stack_i-1] = x
            STACK[stack_i-2] = y
            
        else:
            # push
            x = ord(curr) - 0x30
            
            STACK[stack_i] = mem[x]
            stack_i += 1

This function takes a program string, memory array, current password byte and the lookup table we created earlier. Stack manipulation is done by moving pointers between the STACK list and the mem list. When we compute mut for example, it returns two symbolic expressions that are stored back on the stack.

Now we can use this in our original program:

# symbolically execute the functions
process_func(f1, mem, password[0], S_BOX_CONCAT)
process_func(f2, mem, password[1], S_BOX_CONCAT)
process_func(f3, mem, password[2], S_BOX_CONCAT)
process_func(f4, mem, password[3], S_BOX_CONCAT)
process_func(f5, mem, password[4], S_BOX_CONCAT)
process_func(f6, mem, password[5], S_BOX_CONCAT)
process_func(f7, mem, password[6], S_BOX_CONCAT)
process_func(f8, mem, password[7], S_BOX_CONCAT)

Finally, we tell z3 what the target output should be and also constrain a few bytes of the input to conform to flag format:

# (this is the target pre-rotation)
target = 'd2 45 cb 04 40 84 d5 fd b7 33 25 3f a2 23 b8 3b b2 d9 be 6a 30 8c 94 34 97 b5 26 83 a1 85 fb 25'
target = [int(x,16) for x in target.split(' ')]

# constrain mutated secret to the target
for i in range(len(target)):
    s.add(mem[i] == target[i])

# constrain original secret to flag format
s.add(orig_mem[0] == ord('H'))
s.add(orig_mem[1] == ord('a'))
s.add(orig_mem[2] == ord('c'))
s.add(orig_mem[3] == ord('k'))

Now all we need to do is let it run:

# may take a few minutes
s.check()

# print flag
m = s.model()
print(''.join([chr(m[x].as_long()) for x in orig_mem]))

HackTM{By3_bYE_G0Ld!l0cKS_~mama}

For a full solution script see here

comments powered by Disqus