2025 AIS3 Pre-Exam 與 MyFirstCTF 解題紀錄

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 但不能直接複製
  • 直接手打出來
  • 獲取 FLAGAIS3{Welcome_And_Enjoy_The_CTF_!}

Windows CTF

  • 題目 FLAG 格式為 AIS3{Google Maps上的店名:拉麵名稱}
  • 題目給了一張圖片,看來是隱寫術呢
Windows CTF 題目提供的原始圖片
Windows CTF 題目提供的原始圖片
  • 將圖片用 exiftool 或 Aperi’Solve 分析,你會發現什麼都拿不到
  • 發現唯一的線索是右上角的發票,根本是明明的暗示
  • 分析發票資訊,拉麵店店名有”平和”,賣方為 3478592?,有一個完整的 QRCode
圖片中可辨識的發票資訊
圖片中可辨識的發票資訊
  • 如果上網 Google “平和 拉麵” 只會出現香港麵店
  • 眾所皆知,賣方編號為八碼,且與統一編號相同,因此可以在 財政部 稅籍登記資料公示查詢網 從 34785920 往 34785929 開始查詢猜測店名
  • 查詢到 34785923 會發現是一家叫做”平和溫泉拉麵店”的店家,地址在”宜蘭縣礁溪鄉德陽村5鄰礁溪路5段108巷1號”
稅籍查詢結果中的店家資料
稅籍查詢結果中的店家資料
  • 在 Google Maps 貼上地址後會發現,這家店的正確店名是 “樂山溫泉拉麵”
Google Maps 顯示的正確店名
Google Maps 顯示的正確店名
  • 掃描左邊的 QRCode,會發現是以下內容:MF1687991111404137095000001f4000001f40000000034785923VG9sG89nFznfPnKYFRlsoA==:**********:2:2:1:蝦拉
  • 搭配菜單猜測為蝦拉麵
  • 獲取 FLAGAIS3{樂山溫泉拉麵:蝦拉麵}

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 頁面
微型伺服器題目的 Instance 頁面
  • 按照本地端測試結果,回到上一層 http://chals1.ais3.org:20127/,可以發現此目錄下只有 index.html
伺服器目錄列出的檔案
伺服器目錄列出的檔案
  • 嘗試發現 http://chals1.ais3.org:20127/index.html/../ 一樣會跳到上一層,推斷可以進行 目錄遍歷(Directory traversal)攻擊
利用路徑遍歷返回上層目錄的結果
利用路徑遍歷返回上層目錄的結果
路徑遍歷後的系統目錄畫面
路徑遍歷後的系統目錄畫面
  • 我在這邊卡了很久,想說到底是哪個底下的資料夾可能存在 flag
尋找 flag 檔案時的目錄內容
尋找 flag 檔案時的目錄內容
  • 後來在某次偶然打錯成 http://chals1.ais3.org:20902/..%2F..%2F..%2F..%2F..%2F,發現原來只要執行路徑遍歷就能看到flag檔案
回到根目錄後顯示的 flag 檔案
回到根目錄後顯示的 flag 檔案
  • 接著進入 http://chals1.ais3.org:20902/..%2F..%2F..%2F..%2F..%2F/readable_flag_BjSvx3Dj72Xd2GlSNQueTNxhE7PTUgSS 就能得到flag
讀取 readable_flag 檔案的結果
讀取 readable_flag 檔案的結果
  • 獲取 FLAGAIS3{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 爆破
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 題目的目錄遍歷結果
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 的管理員登入畫面
  • 接著進入 2fa
Login Screen 1 的二階段驗證頁面
Login Screen 1 的二階段驗證頁面
  • 把網址修改成 /dashboard.php
  • 將封包點右鍵送到 repeater 中
將 dashboard 請求送入 Burp Suite Repeater
將 dashboard 請求送入 Burp Suite Repeater
  • 成功獲得 flag
透過 Repeater 取得 flag 的回應
透過 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 的解碼輸出
WebAssembly flag checker 的解碼輸出
  • Get FLAG

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:

import math
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())
中斷
除外:
繼續
  • 獲取 FLAG

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