diff options
Diffstat (limited to 'trees')
| -rw-r--r-- | trees/googlectf-2025.typst | 363 | ||||
| -rw-r--r-- | trees/index.typst | 23 |
2 files changed, 386 insertions, 0 deletions
diff --git a/trees/googlectf-2025.typst b/trees/googlectf-2025.typst new file mode 100644 index 0000000..5d3bc67 --- /dev/null +++ b/trees/googlectf-2025.typst @@ -0,0 +1,363 @@ +#import "_lib/kodama.typ": * +#show: kodama + +#metadata(("title": "googlectf-2025")) + +// +++ +// title = 'Googlectf 2025' +// date = 2025-06-30T17:45:28-03:00 +// draft = false +// tags = ['rev', 'ctf'] +// +++ + + + += rev-multiarch-1 (126 points / 99 solves) + +#linebreak() + +#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: + + + +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. diff --git a/trees/index.typst b/trees/index.typst index 97aada1..48570bd 100644 --- a/trees/index.typst +++ b/trees/index.typst @@ -6,4 +6,27 @@ hello! +Hi, I'm Gabriel Schneider #sym.dash `gbrls`. My areas of interest are: +- #smallcaps[reverse engineering]. +- #smallcaps[exploit development]. +- #smallcaps[vulnerability research]. +- #smallcaps[programming]. +- #smallcaps[tool development]. +- CTFs at #link("https://epicleet.team/")[ELT]. + +My hobbies include, in no particular order: +- Painting and Drawing. +- Writing. +- Playing the drums, keyboard, acoustic guitar, electric bass and harmonica. +- FPV Drones +- Learning modern hebrew. +- Observing and being observed by my four cats: Nyx, Flor, Piolha and Bilbo. +- Sci-fi. +- Studying philosophy and religion. +- Coffee. +- Generative visual and auditory art. + +== Blog posts + +- #local("googlectf-2025") |
