AIS3 2025 - 助教 & KoH 出題心得
Author: 堇姬Naup
目前已經 open source 了 KoH,各位可以載下來玩
https://github.com/Naupjjin/2025-AIS3-KoH
AD & KoH 直播
https://www.youtube.com/watch?v=y3PbmYq9bcQ
https://www.youtube.com/live/nNU9_lUObsM
https://www.youtube.com/live/vXpJXz2uPZA
起源
這次是擔任 CTF 組 (進階資安攻防競技) 的助教,那時候時間是 7/25 號,突然的 @curious 就 tag 我和 @Itiscaleb 說

沒錯,所以這個作為 CTF-9 場外加開組的黑克松就開始了
仔細看一下,現在時間離開賽的 8/1,好像剩一個禮拜
總之最終結論是到時候會出一題 A&D 或 KoH 來給學員玩
發想
前陣子打了 2025 Google CTF,當時有打了一題叫做 fishmez 的題目
詳細可以 參考
他需要通過玩家上傳一份 player_kernel
每 turn 都會去執行同一個 player_kernel 內的同一段程式碼,所以可以通過寫分支來讓他做出不一樣的行為
這時候就發想出了,可以讓總共八組的學員 (第九組是助教、第十組是 NPC)上傳自己的 player_kernel
邏輯
丟到同一個地圖上,然後去打架XD
不過,如果說要讓他們上傳 player_kernel
並且想達到想要的效果,就一定要寫個完善的 python sandbox,因此我們決定寫一台 VM
另外還有 caleb 頂級繪師


題目
先來介紹一下題目的架構
首先是我們有 AD 跟 KoH,所以需要一個總控制的地方,可以自動去計算時間,並且去戳 AD、KoH 的 API,來控制 round 數的開始與結束
而 KoH 會有以下組件
- dashboard: API、與 DB 互動、與 player 互動
- simulator: 整個遊戲邏輯
- VM: 跑 player 上傳的 assembly,每 turn 執行他後回傳最終執行動作給 simulator
- db: 存放玩家上傳、分數等
目前題目是已經開源了
https://github.com/Naupjjin/2025-AIS3-KoH
可以直接看 code
玩法介紹
這題 KoH 叫做 Mortis GO!!!!!
背景是
小睦因為太久沒上學所以跟不上進度,只好分裂出多重人格找小祥補進度
小祥會給小睦的不同人格題目,解出來就能獲得分數
或是也可以選擇幹掉其他的人格,在多重人格大逃殺中吃雞

玩法
隊伍通過上傳 assembly (詳見指令集) 來遊玩遊戲,1 round 遊戲共有 200 turn,每 turn 都會去執行同一段 assembly,並回傳一個指令 (詳見指令)
每五分鐘為 1 round,這裡稱作 active,並且 round 與 round 中間會有時長不一的 pending
若無上傳新腳本,則會沿用之前提交的最新腳本
模擬後會根據遊戲分數進行排名轉換成實際分數 (詳見計分方式)
ex:
round 31 active 時上傳腳本 –> round 31 pending 等待進入 round 32 –> round 32 active 模擬該 round 前上傳的最新腳本並更新分數 (模擬時間長短不一,請耐心等候)
Scoreboard 分數
每 round 會去跑玩家上傳的 assembly,並統計該輪遊戲獲得的分數,排名後會轉換成會記錄在 scoreboard 上的分數
轉換方是如下:
Rank | Score (scorebboard) |
---|---|
1 | 30 |
2 | 20 |
3 | 15 |
4、5 | 8 |
6、7 | 3 |
8、9、10 | 1 |
若該 round 沒有拿到任何分數以 0 分計算
同分狀況則如下轉換:
team 8、4 遊戲分數 52 分
team 9 遊戲分數 48 分
team 1、2、7 遊戲分數 6 分
則
team 8、4 並列第一名拿到 30 分
跳過第二名
team 9 第三名拿到 20 分
team 1、2、7 第四名拿到 8 分
遊戲分數
通過編寫 assembly,可以操控玩家並互動地圖上的物件
可以通過以下方式來獲取分數
- 移動(1 分): 若該 turn 有移動玩家 (本體、分身)
- 殺人:
- 擊殺本體 (70 分): 本體共有 3 滴血,若你成功擊殺別支隊伍本體可得分
- 擊殺分身 (40 分): 分身共有 2 滴血,若你成功擊殺別支隊伍分身可得分
- 箱子(藍色章魚): 通過互動可以拿到任務,會將任務相關資訊放置在記憶體上,之後將任務需求放置到對應記憶體上,再次互動,若正確即可得分,共有以下幾種任務,分別有不同分數
- 1 反轉數字 (
30 分60分): 7 個參數,7 個答案 (ex: 1234567 (七個) 反轉成 7654321) - 2 排序 (
40 分80分): 7 個參數,7 個答案 (ex: 2、5、4、1、6、3 排序成 1、2、3、4、5、6) - 3 RSA 解密 (
50 分100分): 4 個參數 (p、q、e、c),1 個答案 (m) - 4 ECC 點加法 (
60 分120分): 7 個參數(a、b、p、P_x、P_y、Q_x、Q_y),2 個答案 (R_X、R_y),給定曲線 y^2=x^3+ax+b (mod p),計算曲線上兩個 P、Q 之 P+Q = R 為多少
- 1 反轉數字 (
buf[50] 題目編號
buf[51] ~ buf[57] 按順序放置參數及答案
記憶體
玩家有 100 格可以存 unsigned int 的記憶體
- buf[0] ~ buf[49] 玩家與分身共用,會繼承
- buf[50] ~ buf[57] 玩家及分身自己用,會繼承
- buf[58] ~ buf[99] 玩家及分身自己用,不會繼承
地圖
每 5 round 會刷新地圖 (ex: 1-5 round 為第一張、6-10 為第二張,以此類推)
當模擬完畢後,可以在 game
的地方看到該輪模擬結果
- 按
1
可以追蹤一個玩家 - 按
2
回到全地圖 - 按
+
可以看到下一 turn (本地測試提供) - 按
-
可以看到上一 turn (本地測試提供) - 按
shift
和+
可以加速進行 (本地測試提供)
地圖的每一格可能會是 path = 0、wall = 1、chest = 2、player = 4,並且 or 起來,
ex: 若 buf[3] = 0, buf[4] = 0,且在 map[0][0] 的位置同時存在玩家跟箱子
則 load_map 0 3 4 的結果會是 2 | 4 = 6
玩家可以透過不同的 Instruction 來操控記憶體或是控制程式流程
並在最後透過 ret 回傳不同的數字來控制 Mortis
number | 指令 | 描述 |
---|---|---|
0 | stop | 無動作 |
1 | up | 往上走一格 |
2 | down | 往下走一格 |
3 | left | 往左走一格 |
4 | right | 往右走一格 |
5 | interact | 跟周圍 8 格寶箱互動 (不包含自身所在位置) |
6 | attack | 攻擊周圍 8 格 (不包含自身所在位置) |
7 | fork | 在原地分身 |
備註: assembly 每 turn timeout 上限為 250 ms,若該 turn timeout 則會直接回傳 0
角色
玩家本體會有 3 滴血,而分身會有 2 滴血
本體死後會在隨機位置重生,分身則會消失
fork
當執行 fork 時,玩家可以在原地產生分身
基礎消耗是 70
一個玩家可以最多有 3 個分身,並且每次 fork 時,所需的分數就會變成 1.2 倍,
fork 會執行跟一開始的角色一樣的 assembly
指令集
- memory: 以 memX 表示,例如 mem0, mem1,撰寫時使用 index,ex:
1
、83
- constant: 以前綴
#
標示,ex:#1
、#83
- label: 任意 label 名稱,並在結尾加上
:
,ex:avemujica:
- 可以使用
//
來當註解 (15:53 update)
指令 | 說明 |
---|---|
mov <mem1> <mem2> |
mem1 = mem2 |
mov <mem1> #<constant> |
mem1 = constant |
movi <mem1> <mem2> |
*mem1 = *mem2 |
add <mem1> <mem2> |
mem1 += mem2 |
add <mem1> #<constant> |
mem1 += constant |
shr <mem1> <mem2> |
mem1 >>= mem2 |
shr <mem1> #<constant> |
mem1 >>= constant |
shl <mem1> <mem2> |
mem1 <<= mem2 |
shl <mem1> #<constant> |
mem1 <<= constant |
mul <mem1> <mem2> |
mem1 *= mem2 |
mul <mem1> #<constant> |
mem1 *= constant |
div <mem1> <mem2> |
mem1 /= mem2 |
div <mem1> #<constant> |
mem1 /= constant |
je <mem1> <mem2> <label> |
若 mem1 == mem2 則跳轉至 <label> |
je <mem1> #<constant> <label> |
若 mem1 == constant 則跳轉至 <label> |
jg <mem1> <mem2> <label> |
若 mem1 > mem2 則跳轉至 <label> |
jg <mem1> #<constant> <label> |
若 mem1 > constant 則跳轉至 <label> |
inc <mem1> |
mem1++ |
dec <mem1> |
mem1-- |
and <mem1> <mem2> |
mem1 &= mem2 |
and <mem1> #<constant> |
mem1 &= constant |
or <mem1> <mem2> |
mem1 |= mem2 |
or <mem1> #<constant> |
mem1 |= constant |
ng <mem1> |
mem1 = ~mem1 |
ret <mem1> |
結束並返回 mem1 的值 (返回值就是指令) |
ret #<constant> |
結束並返回常數 (返回值就是指令) |
load_score <mem1> |
將當前分數載入 mem1 |
load_loc <mem1> |
載入當前 (x,y) 的至 mem1[0] 及 mem1[1] |
load_map <mem1> <mem2> <mem3> |
將 map(mem2,mem3) 載入 mem1 |
get_id <mem1> |
將角色 ID 載入 mem1 ,如果是 0 就是分身 |
locate_nearest_k_chest <mem1> <mem2> |
從 0 開始,將距離第 mem2 個最近寶箱的座標 (x,y) 存入 mem1[0] 與 mem1[1] |
locate_nearest_k_chest <mem1> #<constant> |
從 0 開始,將距離第 constant 個最近寶箱的座標 (x,y) 存入 mem1[0] 與 mem1[1] ,不會找到自己的角色(本體跟分身) |
locate_nearest_k_character <mem1> <mem2> |
從 0 開始,將距離第 mem2 個最近角色的資訊存入 mem1[0..2] ,分別是 is_fork 、 (x,y) ,不會找到自己的角色(本體跟分身) |
locate_nearest_k_character <mem1> #<constant> |
從 0 開始,將距離第 constant 個最近角色的資訊存入 mem1[0..2] ,分別是 is_fork 、 (x,y) |
整個遊戲大致就是這樣了
Day1

經歷了五天趕工,當然也寫出了不少垃圾 code
開賽後直接

當我們遊戲開賽不到幾 round
突然

直接 502 bad gateway
去後台 log 看了一下

難道我們要撿到 python zeroday 了嗎(X
後來想一想應該是 VM 那邊有問題
看了一下發現,這裡有個裸的 oob 耶,直接變成 VM pwn 101

python 101 第一章
眾所皆知,call function 記得要加 ()

python 101 第二章
再來看看這份 code 有甚麼問題呢
1 | match opcode: |
沒錯答案就是 match-case 不用加 break

python 101 第三章
跑一跑突然看到 game 頁面上面渲染的卡住了


戰況
第一天的地圖是有牆壁的,導致在沒有寫好尋路的狀況下,大家都在轉圈圈

不過第一天似乎只有一隊想到怎麼打贏 NPC
我 NPC 的策略是,除了轉圈圈以外,也分身一次
這樣就可以拿到比 200 分還要更高的分數
1 | load_score 2 |
Day2
第二天的時候,我們就決定把牆壁刪掉
這樣更方便大家去寫抓人或開寶箱的邏輯

第二天 KoH 整體就趨於穩定了,基本上 patch 了兩個錯誤
第一個是只需要刪除一次血量為 0 的玩家或分身,但不小心把他寫到迴圈會重複刪除,導致報錯

還有 movi 實作有誤,不小心多加了第三個參數

那基本上就很明顯可以使用 load_loc
+ locate_nearest_k_character
來去找到最近的敵人
並去檢測是否攻擊範圍有人,有的話就攻擊
基本上第二天所有人的策略都是這樣
第二天開賽這就是第九組策略,然後第十組 NPC 替換成比較沒策略的亂走,不然第一天 NPC 拿太多分了XD
1 | start: |
不過發現大家都沒在跟小祥互動,所以說把完成任務的分數調高至兩倍,但還是沒人完成就是了,大多數人貌似都卡在寫模逆元
全部人都在放 explosion

最終有寫出自動抓人攻擊腳本的人,都成功逆襲到前五名

最後來提幾個可能是策略的方法
- 先生成一般 x86/64 的 shellcode,在拿 assembly 去改
- 寫個簡易的展開,像是 sub、模逆元等,或是簡易 coompiler
- 若只能完成幾個任務,那就可以去尋找最近的小祥或人,若有人就去攻擊,若拿到的任務,若是拿到的是不能解的,就去所有第 2 近的寶箱,並預先壓到記憶體上走過去
- 最終能寫出完成任務邏輯應該是最佳解,因為小祥在地圖上的密度是相當高的
最後我們的各種 king
- 👑of Hill (拿到最多次該 round 第一)
- Team3 35次
- 👑of score
- Team3 1651 分
- 👑of script (不換 script)
- Team1 30 round 裸奔 script
最後也恭喜各位完賽!!!
除了出 KoH
除了 KoH 外,這幾天學員也會跑來找我們討論專題
看到有人以前沒學過 heap exploitaion 然後這幾天在努力把它學會真的強XD
也有看到許多蠻酷的
像是 @chilin 的與憑證相關的題目或是 @p23 的 kernel brainfuck
另外 reverse 題目整體水準都相當高,印象最深的還是他有對 windows api 通過 hash 來做混淆,總之最後可以從記憶體拉出一張圖片,並且繼續逆向可以看到類似於 key logger 的東西
那張圖片是個跟碰到牆壁會轉彎的圖片
鍵盤打出正確的方向就可以拿到 flag
還有 @ian 那題 prototype pollution 也蠻酷的
前端很棒XD
after all
感謝所有 AIS3 不管是 staff、講師或是其他助教和學員
這幾天雖然都蠻晚睡的,但也很好玩

也感謝 @curious @Itiscaleb 一起場外加開黑客松
跟 @pwn2ooown @Vincent55 救火幫忙視覺化
還有特別感謝
@chatGPT @claude @copilot