#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.