GoogleCTF 2025 - writeup
Author: 堇姬 Naup


Pwn
multi-arch 2
一題 VM pwn
第一題是 Reverse 第二題是 pwn
這份 writeup 只有寫 pwn
analyze IDA
沒有 debug symbol,所以要修
這台 VM 自定義了一個 masm 的檔案格式
整個 masm 檔案格式就是
- 4 bytes MASM 作為 magic number
- 被 load 進去後共有 3 個 segments,code、data、stack
- 檔案 header 有三段,分別是 code、data、arch_table
- 每段的 header 有一個 offset 跟 size (2 bytes)
- 最後面就是放那三段了
整個 vm 的核心是這個結構體
1 | 00000000 struct __attribute__((packed)) masm_struct // sizeof=0x88 |
提供了四個 register、sp、pc 等
另外也有 heap,這部分會在之後利用使用到,到時候深入說明
正如題目名稱,這是一個 multi-arch 的 VM
共有兩個指令集架構
一個是基於 stack、另一個是 register
1 | __int64 __fastcall run_masm(__int64 masm) |
他會去 parse 你的 arch-table 來去識別目前的 opcode 是哪個架構的
parse 的方法就是 arch-table 1 bytes 就是對應8個 pc
以 bits 為單位,0 代表 stack arch、1 代表 register arch
1 | unsigned __int8 __fastcall caculate_arch(masm_struct *masm_struct) |
指令集的部分就不細說
不過我只有逆乾淨 stack-arch 跟一部分的 register-arch
我把我有用到的貼出來
stack-arch
- 0x10 push imm (byte)
- 0x30 push imm (dword)
- 0xA0 syscall
- syscall_num =
- 0 readint I; push I
- 1 not supported
- 2 pop s,n; fwrite(stdout,s,n)
- 3 pop seed; srand(seed)
- 4 push rand() + rand() << 16
- 5 call get_flag
- 6 calloc heap
- 0xff halt
register-arch
- 0x1 syscall syscall_num = register[0]
- 0 readint reg[1]
- 1 fread(stdin,[reg[2]],reg[3]) (terminate by newline)
- 2 fwrite(stdout,[reg[2]],reg[3])
- 3 srand(reg[2])
- 4 reg[1] = rand() + rand() << 16
- 5 no support
- 6 calloc heap
- 0x15 pop reg[0]
- 0x16 pop reg[1]
- 0x17 pop reg[2]
- 0x41 xor idx(2 bytes), imm(4 bytes) (xor regs[idx], imm)
analyze bug
bug 是出在 register-arch xor 的地方
1 | case 0x41u: |
稍微寫個腳本去測試一下輸入,就可以發現,有很明顯的越界
1 | def f(idx): |
原本應該要只能寫到 register 的範圍
現在可以向下越界到 heap 的地方
這邊來看 heap 的實作方式吧
可以通過 syscall 6 來去創建 heap 區段
1 | __int64 __fastcall calloc_0x200_heap_seg(masm_struct *masm, int a2, unsigned int *a3) |
總之會從 0xA000 開始,並且需要去對齊 0x1000
有空閒的記憶體後,他就會去 calloc 一塊 0x200 大小的 chunk
並把該 pointer 跟 vm 內的 address 都存到 heap_array 中
這段是可以被覆蓋到的
之後當想要去寫這塊記憶體的時候,他在 vm address 轉 實際的 address 時,就會去查這個 heap array
1 | __int64 __fastcall mapping_addr(masm_struct *masm, unsigned int a2, __int64 a3) |
既然可以越界去 xor,那是不是可以去 xor 原本 chunk address 成 masm_struct
因為該 chunk 也是存在 heap 上的
並且那塊 chunk 只有低 1.5 bytes 不一樣,並且這 1.5 bytes 相同
所以這兩塊需要 xor 的值是固定的
將這塊 xor 成 masm_struct
後,通過 syscall 2 的 fwrite
就可以去 leak 相當多資料,其中我們需要的資料是 data 存放的實際位置
這是一塊通過 mmap 出來的記憶體
1 | seg1_load_ptr = mmap(0LL, 0x1000uLL, 7, 33, 0, 0LL); |
這三塊都是 rwx 權限,如果能夠把 shellcode 寫在上面(直接把 shellcode 放在 masm file data 段)
並且跳上去就可以 RCE 了
其中他有提供 syscall 5 會去 call 一個 get_flag
的 function pointer
使用 register-arch 的 syscall 2 來去寫 pointer
把 register 1 設成 0xa000 (你第一次分配的位置,這塊已經在先前把實際位置改成了 masm_struct
) + 0x28
當你去寫入他時候,他把 0xa028 轉換成 masm_struct
+ 0x28
就可以去寫入了
把他寫成 data mmap address 後
去 syscall 他就可以跳上去 shellcode 拿 shell 了
VM 題目真的是逆向困難XD
exploit
1 | from pwn import * |
Misc
FishMaze
魚初始會在中間,要想辦法走出去這個藍色的圈圈
藍色是牆壁,剩下的是敵人,不能碰到

首先他沒有給 source code
不過可以發現,他每一 round 都會去執行你寫在 player_kernel
內的 code
並且在 @chumy 的測試下,發現他是個 JAX
https://docs.jax.dev/en/latest/quickstart.html
另外 player_kernel 本身有提供三個變數mapdata_ref
、auxdata_ref
、out_ref
第一個 mapdata
有idx 0~7
分別是以魚為中心周圍八格的狀況
1 | A0 A1 A2 |
out_ref 的 index 0 存放的是下一次要走的方向
- 0 : stay still
- 1 : move left
- 2 : move right
- 3 : move up
- 4 : move down
剩下的 out_ref 可以在這輪存入東西
他會在下一輪把資料繼承給 auxdata_ref
這裡的 aux 就充當了一個暫存器
到這邊其實就可以想到,寫一個計數器,去紀錄當前輪數
然後在之後的 index 就放之後每個輪數要去執行的方向,來去控制魚走出去
通過以下的 exploit 就可以走出去了
exploit
1 | def player_kernel(mapdata_ref, auxdata_ref, out_ref): |
get flag ~

after all
蠻好玩的~