summaryrefslogtreecommitdiff
path: root/trees/blog/googlectf-2025.typ
blob: 716f1b7e7844984fd11cc54625871087edb3226a (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
#import "./html_elements.typ": post

#show: post

// +++
// title = 'Googlectf 2025'
// date = 2025-06-30T17:45:28-03:00
// draft = false
// tags = ['rev', 'ctf']
// +++



= rev-multiarch-1 (126 points / 99 solves)

#box()[#html.frame()[#text(size: 8em, fill: rgb("#74c7ec").darken(10%))[T]]]
his challenge was solved by me and #link("https://lobisomem.gay")[Matt].

The challenge files were a Linux executable and a misterius `crackme.masm` file.

We worked together by him reversing the actual VM runtime and I was reversing
the binary file format and was writing some python code to interact with it.

In the first day we were actually sidetracked trying to solve `pwn-multiarch-2`,
it wasn't until the end of the day that then we realized that there's the
`rev-multiarch-1` and they're based on the same vm binary. It didn't change our
methodology to solve it, since we are still reversing it at that time, but this
time we had a valid program with the `crackme.masm`.

Below is the reversing of the main function, as you can see it's pretty straight
forward:

```c
__int64 __fastcall main(int a1, char **f, char **a3)
{
  char *segments; // rax
  __int64 v4; // rbp
  char *stuff; // rbx

  setbuf(stdin, 0);
  setbuf(stdout, 0);
  setbuf(stderr, 0);
  if ( a1 <= 1 )
  {
    fprintf(stderr, "[E] usage: %s [path to .masm file]\n", *f);
    return 2;
  }
  else
  {
    fwrite("[I] initializing multiarch emulator\n", 1u, 0x24u, stderr);
    segments = (char *)parse_bin(f[1]);
    v4 = (__int64)segments;
    if ( segments )
    {
      stuff = build_vm_state(segments);
      fwrite("[I] executing program\n", 1u, 0x16u, stderr);
      while ( (unsigned __int8)run_vm((__int64)stuff) )
        ;
      if ( stuff[48] )
      {
        fwrite("[E] execution failed\n", 1u, 0x15u, stderr);
        debug_print_vm((__int64)stuff, 1);
      }
      else
      {
        fwrite("[I] done!\n", 1u, 0xAu, stderr);
      }
      sub_555555555427(stuff);
      call_free(v4);
      return 0;
    }
    else
    {
      fwrite("[E] couldn't load multiarch program\n", 1u, 0x24u, stderr);
      return 1;
    }
  }
}
```

Note the `debug_print_vm` function, it was pretty useful to undestand the vm
state struct since it prints the stack and all four registers.

Below is the assembly code that prepares the arguments for the printf call,
note that all arguments are being read relative to `rdi`, which holds the
address of the vm state struct.

```asm
mov     ecx, [rdi+3Bh]
mov     edx, [rdi+37h]
mov     esi, [rdi+33h]
mov     eax, [rdi+47h]
push    rax
mov     r9d, [rdi+43h]
mov     r8d, [rdi+3Fh]
lea     rdi, debug_fmt_string ; "  ---[ PC=0x%08x SP=0x%08x | A=0x%08x B"...
mov     eax, 0
call    _printf
add     rsp, 10h
test    bpl, bpl
jnz     short loc_555555556A8F
```

At this point we had a good idea about the how the program worked, but we wanted
to understand what the `crackme.masm` does.

This is the hexdump of the whole file:

![image](/hexdump.svg)

Just seeing the strings we can get a pretty good idea of what it does:

- It has four bytes at the start, being the magic bytes for the MASM file.
- It seems like it consists of three seperate challenges, seeing the strings
near the end.


At this step what I'd do would be to debug it in `gdb` to see what's happening,
i.e. where it reads input we control, and how it's used when deciding if a
challenge step is correct or not. The issue is that since this is a VM, for
every instruction executed by it, in `gdb` we see a lot of code and function
calls related to the implementation of the VM itself, doing the fetch, decode
and execute cycle.

> Given the amount of solves this challenge had, I believe most
> people just solved this manually on `gdb`, but I *really* wanted to try a
> new tool...

= libdebug

This is where #link("https://github.com/libdebug/libdebug")[libdebug] comes in, it's
basically a python library to automate `ptrace` debugging, it's pretty cool and
since I discovered it, I hadn't had the opportunity to use it yet.

With the script below I was able to debug the `crackme.masm` with an interactive
debugger, that I could even add breakpoints and single step instructions.


```python
from libdebug import debugger, libcontext
from pwn import u64, u32, u8

cur_vm = 'stackvm'
cur_cycle = 0

hit_vm_bp = False
vm_breakpoints = [
    #0
    #18, # challenge 1 ends
    #49
]
vm_ip_breakpoints = [
    # 0x131, # regvm loop cmp
    #0x5a, # chal1 cmp
    #0x7c,
    0x88, # chal2 cmp
    #0xcd, # chal3 cmp?
    0xdd, # chal3 actual cmp?
    0x10b, # chal3 actual actual idk?
    #0x12d, # xor r1 r3
]
prev_input_dbg = ''
vm_stop = []

STACKVM_MNEMONICS = {
    0xa0: 'S.SYSCALL',
    0x10: 'S.LDB',
    0x20: 'S.LDW',
    0x30: 'S.LDD',
    0x40: 'S.LDP',
    0x50: 'S.POP',
    0x60: 'S.ADD',
    0x61: 'S.SUB',
    0x62: 'S.XOR',
    0x63: 'S.AND',
    0x70: 'S.JMPI',
    0x71: 'S.JMPI.EQ?',
    0x72: 'S.JMPI.NE?',
    0x80: 'S.SCMP',
    0xff: 'S.HLT',
}


def stackvm_mnemonic(ins, is_ip=False):
    opcode = ins & 0xff
    dat = ins >> 8
    mnemonic = hex(opcode)
    if opcode in STACKVM_MNEMONICS:
        mnemonic = STACKVM_MNEMONICS[opcode]

    if (0x70 <= opcode <= 0x72) and is_ip:
        print('DIDJMP!!!!!!!!!!!')

    return f'{mnemonic} {hex(dat)}'

d = debugger(['./multiarch', './crackme.masm'], aslr=False)
io = d.run()

mem_access = d.breakpoint(0x00005555555554B3)
cycle_tick = d.breakpoint(0x0000555555556FFF)

d.cont()
io.sendline(b'2405061754')
io.sendline(b'\x46\x91')
io.sendline(f'{0x2b6043c}'.encode())

d.wait()

should_stop = False

def regvm_disasm(code_adr, ip):
  # a lot of boring code...
  # you can use your imagination for this function.

def stackvm_disasm(code_adr, ip):
    for i in range(ip, ip + (8 * 5), 5):
        ins = u64(d.memory.read(i + code_adr, 5).ljust(8, b'\x00'))
        if i == ip:
            print('>', end='')
        print(f'{i:08x}\t{stackvm_mnemonic(ins, i==ip)}\t{ins:05x}')

def disasm(off, ip, code_adr):
    if cur_vm == 'stackvm':
        stackvm_disasm(code_adr, ip)
    else:
        raw_instr = u64(d.memory.read(code_adr+ip, 8))
        print(f'raw: {raw_instr:016x}\n')
        acc = ip
        for i in range(0, 8):
            s, bytes_read = regvm_disasm(code_adr, acc)
            print(f'{acc:08x}\t{s}')
            acc += bytes_read
        print('')

def print_stack(sp, stack_adr):
    print('-------stack sp: ', hex(sp), hex(stack_adr))

    for i in range(sp - (4 * 5), sp + (4 * 5), 4):
        cur = u32(d.memory.read(stack_adr + i, 4))
        if i == sp:
            print('>', end='')
        print(f'{cur:08x}')

    print('')

def calc_mode(ip):
    i = ip >> 3
    mode_ptr = u64(d.memory.read(d.regs.rbx + 0x18, 8))
    mask = u8(d.memory.read(mode_ptr+i, 1))
    bit_idx = ip & 7
    return (mask >> bit_idx) & 1

while not should_stop:
    if hit_vm_bp:
        i = input('masmdbg> c/n/q: ')
        match i:
            case 'c':
                hit_vm_bp = False
            case 'n':
                vm_breakpoints.append(cur_cycle)
                hit_vm_bp = False
            case '':
                vm_breakpoints.append(cur_cycle)
                hit_vm_bp = False
            case 'q':
                vm_stop.append(cur_cycle)
                hit_vm_bp = False
            case _:
                continue
        prev_input_dbg = i

    if cycle_tick.hit_on(d):
        ip = u32(d.memory.read(d.regs.rbx + 0x33, 4)) - 0x1000
        if cur_cycle in vm_breakpoints or ip in vm_ip_breakpoints:
            mode = calc_mode(ip)
            if mode:
                cur_vm = 'regvm'
            else:
                cur_vm = 'stackvm'
            print(f'\n\n========masm debugger=== {cur_vm} {cur_cycle}\nip -> {ip:08x}')

            r0 = u32(d.memory.read(d.regs.rbx + 0x3b, 4))
            r1 = u32(d.memory.read(d.regs.rbx + 0x3f, 4))
            r2 = u32(d.memory.read(d.regs.rbx + 0x43, 4))
            r3 = u32(d.memory.read(d.regs.rbx + 0x47, 4))

            sp = u32(d.memory.read(d.regs.rbx + 0x37, 4)) - 0x8000

            code_adr_ptr = u64(d.memory.read(d.regs.rbx, 8))
            stack_adr_ptr = u64(d.memory.read(d.regs.rbx+0x10, 8))

            print(f'~~~ REGS\n\t{r0:08x}\n\t{r1:08x}\n\t{r2:08x}\n\t{r3:08x}\n~~~~~')
            disasm(0, ip, code_adr_ptr)
            print_stack(sp, stack_adr_ptr)
            print('========cycle ended=====\n\n')
            hit_vm_bp = True

        if cur_cycle in vm_stop:
            should_stop = True
            continue

        cur_cycle += 1
        d.cont()
        d.wait()

    elif mem_access.hit_on(d):
        pos = d.regs.rsi
        sz = d.regs.rdx
        d.cont()
        d.wait()

    elif not d.running:
        io.interactive()
        should_stop = True

```

Below is how it looks like in action:

```
masmdbg> c/n/q:


========masm debugger=== regvm 139
ip -> 000000bd
~~~ REGS
        88c0ffee
        7a213a1c
        00000000
        00000000
~~~~~
raw: 00631100ffffff10

000000bd        PUSHI 0x00ffffff
000000c2        PUSH REG(0)
000000c3        JMPI.NE 0x00000000
000000c8        SUB REG(13), REG(13)
000000ca        UNKNOWN(0xff)
000000cb        UNKNOWN(0xc0)
000000cc        NOP
000000cd        CMPI REG(0), 0x00000000

-------stack sp:  0xee0 0x7ffff7fbc000
00000000
00000000
f2f2f2f2
88c0ffee
000010bd
>00009146
00000000
00000000
00000000
00000000

========cycle ended=====

```

As you can see the disassembler wasn't 100% complete, but it was enough to
manually reverse the `masm` file and solve it.