2025 AIS3 Pre-Exam 與 MyFirstCTF 解題紀錄
2025 AIS3 Pre-Exam 與 MyFirstCTF 的活動主視覺
今年五月底,我參加 AIS3 舉辦的 Pre-Exam 與 MyFirstCTF。這場競賽既是進入 AIS3 暑期課程的前測,也讓曾參與 AIS3 Junior 2024 的我,能檢視這段期間的學習成果。題目難度高於我原先預期,不過 Reverse 與 Crypto 題目的分析過程也帶來許多新的理解。
本文保留幾題具代表性的解題過程,從 OSINT、Web 到 WebAssembly 逆向與密碼分析,整理我如何縮小問題範圍並驗證假設。比賽中我也更清楚地看見自己的傾向,Web 與 OSINT 題型較能迅速建立方向,逆向與 Pwn 則需要更長時間熟悉程式執行流程。
雜項
歡迎
- 題目上就寫出 FLAG 但不能直接複製
直接手打出來
- 獲取 FLAG
AIS3{Welcome_And_Enjoy_The_CTF_!}
Windows CTF
- 題目 FLAG 格式為 AIS3{Google Maps上的店名:拉麵名稱}
- 題目給了一張圖片,看來是
隱寫術呢
Windows CTF 題目提供的原始圖片
- 將圖片用 exiftool 或 Aperi’Solve 分析,你會發現什麼都拿不到
- 發現唯一的線索是右上角的發票,根本是明明的暗示
- 分析發票資訊,拉麵店店名有”平和”,賣方為 3478592?,有一個完整的 QRCode
圖片中可辨識的發票資訊
- 如果上網 Google “平和 拉麵” 只會出現香港麵店
- 眾所皆知,賣方編號為八碼,且與統一編號相同,因此可以在 財政部 稅籍登記資料公示查詢網 從 34785920 往 34785929 開始查詢猜測店名
- 查詢到 34785923 會發現是一家叫做”平和溫泉拉麵店”的店家,地址在”宜蘭縣礁溪鄉德陽村5鄰礁溪路5段108巷1號”
稅籍查詢結果中的店家資料
- 在 Google Maps 貼上地址後會發現,這家店的正確店名是 “樂山溫泉拉麵”
Google Maps 顯示的正確店名
- 掃描左邊的 QRCode,會發現是以下內容:MF1687991111404137095000001f4000001f40000000034785923VG9sG89nFznfPnKYFRlsoA==:**********:2:2:1:蝦拉
- 搭配菜單猜測為蝦拉麵
- 獲取 FLAG
AIS3{樂山溫泉拉麵:蝦拉麵}
AIS3 微型伺服器 — Web/雜項
- 根據題目引用 GitHub 敘述會發現這是一個安全防護低、以 c 語言編寫的網頁伺服器
- 題目原始檔為可在 window kali linux 系統上執行之包裝好的伺服器檔案,在本地架設執行後發現會顯示同一資料夾下之檔案
- 根據題目敘述要求尋找名稱為
/readable_flag_隨機字串 之 FLAG 檔案
- 輸入 ctfd token 開啟 Instance
- 進入 Instance 發現進入
[http://chals1.ais3.org:20127/index.html](http://chals1.ais3.org:20127/index.html)
微型伺服器題目的 Instance 頁面
- 按照本地端測試結果,回到上一層
http://chals1.ais3.org:20127/,可以發現此目錄下只有 index.html
伺服器目錄列出的檔案
- 嘗試發現
http://chals1.ais3.org:20127/index.html/../ 一樣會跳到上一層,推斷可以進行 目錄遍歷(Directory traversal)攻擊
利用路徑遍歷返回上層目錄的結果
路徑遍歷後的系統目錄畫面
- 我在這邊卡了很久,想說到底是哪個底下的資料夾可能存在 flag
尋找 flag 檔案時的目錄內容
- 後來在某次偶然打錯成
http://chals1.ais3.org:20902/..%2F..%2F..%2F..%2F..%2F,發現原來只要執行路徑遍歷就能看到flag檔案
回到根目錄後顯示的 flag 檔案
- 接著進入
http://chals1.ais3.org:20902/..%2F..%2F..%2F..%2F..%2F/readable_flag_BjSvx3Dj72Xd2GlSNQueTNxhE7PTUgSS 就能得到flag
讀取 readable_flag 檔案的結果
- 獲取 FLAG
AIS3{tInY_We8_53RvER_w1tH_fILe_Br0Ws1nG_as_@_Fe4Tur3}
網路
托莫林
http .Handle ( "/" , http.FileServer ( http.Dir ( "/app/Tomorin" ))) http .HandleFunc ( "/flag" , func (w http.ResponseWriter, r *http.Request) { http .Redirect (w http.ResponseWriter, r *http.Request) { http .Redirect ( w, r, "httpshttps://lobe/ http.StatusFound) })
|
初步觀察:
/ 被設為靜態檔案伺服器,對應的是 /app/Tomorin
/flag 被設定為 redirect,到一個 YouTube 連結,顯然是誘餌
- 推測 flag 被藏在非公開目錄中
- 嘗試直接訪問
/flag,結果為一段 youtube 影片,確認是 redirect,不是 flag 本體
- 嘗試對
/ 進行靜態檔案掃描爆破,使用字典搭配 ffuf 掃描常見檔名,沒有特別發現
ffuf -w common.txt -u http://chals1.ais3.org:30000/FUZZ -mc all
|
- 後來我請 AI 生了一份常見的 flag 檔案名稱來用 ffuf 爆破
cat > strong-list.txt <<EOF .flag Flag fLaG flag.txt flag.TXT fl4g fl@g fl@g.txt flag.txt~ flag.txt.bak flag.bak .flag.swp flag.log flag.tmp %66lag %2e%2e/flag ..%2fflag ..%2f%66lag EOF
|
ffuf -w strong-list.txt -u http://chals1.ais3.org:30000/FUZZ -mc all
|
- 從 ffuf 的輸出來看,有三個 payload 成功回傳了 200 OK,且大小一致(Size: 95),成功發現三個 flag 位置:
..%2fflag [Status: 200, Size: 95] %2e%2e/flag [Status: 200, Size: 95] ..%2f%66lag [Status: 200, Size: 95]
|
透過 curl 得到三個頁面皆為正確flag
Tomorin 題目的目錄遍歷結果
- Get FLAG
AIS3{G01ang_H2v3_a_c0O1_way!!!_Us3ing_C0NN3ct_M3Th07_L0l@T0m0r1n_1s_cute_D0_yo7_L0ve_t0MoRIN?}
Login Screen 1
- 從題目原始碼發現會需要在 index.php 登入得到 user SESSION,接著進入 2fa.php 拿到 code SESSION 才能進入 dashboard.php 拿到 flag
- 根據頁面提示,透過 guest/guest 進入到 2fa 並輸入 000000,發現只能在 dashboard 讀到
Only admin and view the flag.,所以要設法拿到admin權限
- 原本在 index.php 時想說要如何才能取得 admin,用 sqlmap 爆後發現不可行,無意間嘗試輸入 admin/admin 結果就取得 admin 權限,成功進入 2fa
- 但目前困境在於不知道要如何獲得到正確 2fa code,想說嘗試爆破看看有沒有辦法爆出來,我試了不同速度的腳本,要不就太快被擋,要不就太慢
- 觀察原始程式會發現,如果沒有成功拿到 code SESSION 雖然在 2fa 想直接跳到 dashboard 一定會被重導向回 2fa,但是可以用 burp suite 的 repeater 來測試看看會回傳什麼
- 於是乎開啟 burp suite 登入 admin/admin
Login Screen 1 的管理員登入畫面
Login Screen 1 的二階段驗證頁面
- 把網址修改成
/dashboard.php
- 將封包點右鍵送到 repeater 中
將 dashboard 請求送入 Burp Suite Repeater
透過 Repeater 取得 flag 的回應
- Get FLAG
AIS3{1.Es55y_SQL_1nJ3ct10n_w1th_2fa_IuABDADGeP0}
Rev
web flag checker
- 此題是一個使用 WebAssembly 製作的的 flag 檢查器,需要輸入正確的 flag 才會顯示 “Success”
- 初步檢視 HTML 檔案,發現這是一個簡單的輸入介面:
- 有一個輸入框用於輸入 flag
- 有一個 “check” 按鈕用於驗證
- 結果會顯示在頁面上
- 從 index.js 可以看出這是用 Emscripten 編譯的 WebAssembly 程式
接著分析 WebAssembly:
查看 WASM 程式碼,找到關鍵函數 主要驗證函數 (func 9)
wasm(func (;9;) (type 3) (param i32) (result i32)
|
這個函數執行以下步驟:
- 檢查輸入長度是否為 40 字元 (i32.const 40)
- 初始化魔術常數 -39934163 (0xFD9EA72D)
- 設置 5 個 64 位元的預期值
- 將輸入分成 5 組,每組 8 字元
- 對每組進行位元左旋轉操作
- 將結果與預期值比較
從 WASM 程式碼中提取的期望值:
wasmi64.const 7577352992956835434 ; 0x692E506F4734536A i64.const 7148661717033493303 ; 0x63337237655F7337 i64.const -7081446828746089091 ; 0x9DB36F5F6876336D i64.const -7479441386887439825 ; 0x9830395F6837312F i64.const 8046961146294847270 ; 0x6FA63738726E6326
|
旋轉函數 (func 8) 實作 64 位元左旋轉操作:
wasm(func (;8;) (type 7) (param i64 i32) (result i64)
|
分析加密演算法:
- 發現程式使用魔術常數 -39934163 (0xFD9EA72D)
- 旋轉量計算公式:旋轉量 = (ROTATION_KEY >> (i * 6)) & 63
- 對每組 8 字元進行左旋轉後與預期值比較
撰寫 Python 解密腳本:
#!/usr/bin/env python3 """ 優化的解碼程序 - 反向旋轉解密 """
|
def decode_flag(): """解碼主函數""" # 常數定義 ROTATION_KEY = -39934163 EXPECTED_VALUES = [ 7577352992956835434, 7148661717033493303, -7081446828746089091, -7479441386887439825, 8046961146294847270 ] # 計算旋轉量(優化:使用列表推導式) rotation_amounts = [ (ROTATION_KEY >> (i * 6)) & 63 for i in range(5) ] print("旋轉量:", rotation_amounts) # 計算原始值並解碼 flag_parts = [] print("解碼結果:") for i, expected_value in enumerate(EXPECTED_VALUES): # 反向旋轉 original_value = reverse_rotation_64(expected_value, rotation_amounts[i]) # 轉換為ASCII ascii_text = bigint_to_ascii(original_value) flag_parts.append(ascii_text) print(f"位置 {i}: \"{ascii_text}\" (hex: 0x{original_value:016x})") # 輸出完整結果 complete_flag = ''.join(flag_parts) print(f"\n完整結果: \"{complete_flag}\"") print(f"長度: {len(complete_flag)} 字符") return complete_flag
|
def reverse_rotation_64(value, shift): """ 64位反向旋轉函數 優化:簡化負數處理和位運算 """ # 處理負數(轉換為無符號64位) if value < 0: value = (1 << 64) + value # 確保值在64位範圍內 value &= (1 << 64) - 1 # 執行反向旋轉(右旋轉 = 原來的左旋轉的逆操作) shift &= 63 # 確保shift在0-63範圍內 return ((value >> shift) | (value << (64 - shift))) & ((1 << 64) - 1)
|
def bigint_to_ascii(value): """ 將大整數轉換為ASCII字符串 優化:使用更簡潔的方式處理字節序 """ # 提取8個字節(小端序) bytes_list = [ (value >> (i * 8)) & 0xFF for i in range(8) ] # 轉換為字符並過濾無效字符 chars = [] for byte_val in bytes_list: if 32 <= byte_val <= 126: # 可打印ASCII範圍 chars.append(chr(byte_val)) elif byte_val == 0: # 空字節,停止處理 break else: chars.append('?') # 不可打印字符用?代替 return ''.join(chars)
|
def analyze_rotation_key(rotation_key): """分析旋轉密鑰的額外信息""" print(f"旋轉密鑰: {rotation_key}") print(f"二進制: {rotation_key & 0xFFFFFFFF:032b}") print("各位置旋轉量:") for i in range(5): shift = (rotation_key >> (i * 6)) & 63 print(f" 位置 {i}: {shift} 位")
|
if __name__ == "__main__": # 可選:分析旋轉密鑰 # analyze_rotation_key(-39934163) # print() # 執行解碼 result = decode_flag() print(f"\n=== 解碼完成 ===") print(f"最終結果: {result}")
|
WebAssembly flag checker 的解碼輸出
AIS3{W4SM_R3v3rsing_w17h_g0_4pp_39229dd}
Crypto
Stream
- 題目原始程式碼中提供了一段以 sha512(os.urandom(True)) 製造隨機噪音的迴圈,最後一行則使用如下方式將 flag 做加密處理並輸出:
def hexor(a: bytes, b: int): return hex(int.from_bytes(a)^b**2)
|
print(hexor(flag, getrandbits(256)))
|
- 發現程式中前 80 行的輸出皆為雜湊後亂數與隨機 key² 進行 XOR,意圖混淆視聽
- 最後一行才是重點,真正的 flag 被轉為 bytes 後與一個 256-bit 隨機整數的平方做 XOR,然後輸出為十六進位字串
- 加密公式還原
encrypted = int.from_bytes(flag, "big") ^ key**2
- 只要知道 encrypted 和猜中 flag 開頭,即可推得 key**2,進而爆破還原出完整 flag
- 利用 flag 格式 AIS3{…},我們假設開頭為 AIS3{,將其轉為 int 後與 encrypted XOR,得到 key 的平方附近值
- 開始嘗試爆破 key,取開平方後 ±10000 範圍暴力嘗試,解回來的結果若符合 AIS3{ 開頭與 } 結尾,即為正解
- 解出結果,成功還原出 flag
Payload:
enc_hex = "0x1a95888d32cd61925d40815f139aeb35d39d8e33f7e477bd020b88d3ca4adee68de5a0dee2922628da3f834c9ada0fa283e693f1deb61e888423fd64d5c3694" enc = int(enc_hex, 16)
|
known_prefix = b“AIS3{” guess_int = int.from_bytes(known_prefix, “big”) partial_key_sq = enc ^ guess_int approx_key = math.isqrt(partial_key_sq)
|
對於範圍內的增量(-10000,10000): key_candidate = approx_key + delta key_sq = key_candidate ** 2 flag_int = enc ^ key_sq 嘗試: flag_bytes = flag_int.to_byint(flag_int_int). 8,「big」) 如果flag_bytes.startswith(b「AIS3 {」)和flag_bytes.endswith(b「}」): 列印(flag_bytes.decode()) 中斷 除外: 繼續
|
AIS3{no_more_junks...plz}
心得
這次競賽讓我以實際題目檢驗所學。Windows CTF 的發票資訊、Tomorin 的路徑處理,以及 Login Screen 1 的 session 流程,都必須從題目留下的細節建立可驗證的假設。這些題目也提醒我,工具本身並不能取代判斷,搜尋、掃描與攔截封包都需要先有清楚的目的。
WebAssembly 逆向是本次最耗時的部分。我需要從 WASM 程式碼還原旋轉運算,再以 Python 驗證結果,因此更熟悉位元運算、整數表示方式與資料轉換。Stream 題則讓我練習從大量干擾輸出中辨認真正參與加密的資料。
比賽結果讓我更清楚自己的基礎與不足。Pwn 和 Reverse 仍是需要持續投入的方向,而成功進入 AIS3 暑期課程,也讓我有機會在後續課程中延伸這些練習。
By 那個成天泡在電腦裡的梅森 on August 22, 2025.
Canonical link