2025 AIS3 考試前準備 / MyFirstCTF 寫作

Hi 大家好,我是梅森,今年五月底我參加了對於學生來說頗為重要的 CTF 競賽 — 由 AIS3 舉辦的 Pre-Exam 和 MyFirstCTF。這次競賽除了是進入 AIS3 暑期課程的重要門票之外,也是對於去年暑假有參加過 AIS3 Junior 2024(沒看過可以去看一下)的我來說,算是一個很好的機會可以測試一下自己這段時間的進步幅度。說實話這次比賽難度對我而言有點難度,雖然有些題目卡了很久,但最後還是順利解出來了,特別是 Rev 和 Crypto 的部分讓我學到不少新東西,所以這次想透過 writeup 的方式記錄一下解題過程,順便分享給同樣在學習資安的朋友們參考~

比賽過程中我發現自己在 Web 跟 OSINT 類型的題目比較快就能解出來,但在逆向和 PWN 的部分還是需要花比較多時間去理解和分析,特別是 WebAssembly 逆向工程對我來說算是比較新的領域。整體來說這次比賽讓我對自己的能力有更清楚的認知,也知道接下來要加強的方向。

雜項

歡迎

  • 題目上就寫出 FLAG 但不能直接複製
  • 直接手打出來
  • 獲取 FLAGAIS3{Welcome_And_Enjoy_The_CTF_!}

Windows CTF

  • 題目 FLAG 格式為 AIS3{Google Maps上的店名:拉麵名稱}
  • 題目給了一張圖片,看來是隱寫術

  • 將圖片用 exiftool 或 Aperi’Solve 分析,你會發現什麼都拿不到
  • 發現唯一的線索是右上角的發票,根本是明明的暗示
  • 分析發票資訊,拉麵店店名有”平和”,賣方為 3478592?,有一個完整的 QRCode

  • 如果上網 Google “平和 拉麵” 只會出現香港麵店
  • 眾所皆知,賣方編號為八碼,且與統一編號相同,因此可以在 財政部 稅籍登記資料公示查詢網 從 34785920 往 34785929 開始查詢猜測店名
  • 查詢到 34785923 會發現是一家叫做”平和溫泉拉麵店”的店家,地址在”宜蘭縣礁溪鄉德陽村5鄰礁溪路5段108巷1號”

  • 在 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)

  • 按照本地端測試結果,回到上一層 http://chals1.ais3.org:20127/,可以發現此目錄下只有 index.html

  • 嘗試發現 http://chals1.ais3.org:20127/index.html/../ 一樣會跳到上一層,推斷可以進行 目錄遍歷(Directory traversal)攻擊

  • 我在這邊卡了很久,想說到底是哪個底下的資料夾可能存在 flag

  • 後來在某次偶然打錯成 http://chals1.ais3.org:20902/..%2F..%2F..%2F..%2F..%2F,發現原來只要執行路徑遍歷就能看到flag檔案

  • 接著進入 http://chals1.ais3.org:20902/..%2F..%2F..%2F..%2F..%2F/readable_flag_BjSvx3Dj72Xd2GlSNQueTNxhE7PTUgSS 就能得到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

  • 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

  • 接著進入 2fa

  • 把網址修改成 /dashboard.php
  • 將封包點右鍵送到 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}")
  • 輸出結果

  • 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}

心得

這次的 AIS3 Pre-Exam / MyFirstCTF 對我來說算是一個很好的練習機會,整體難度雖然對我來說仍然有挑戰,但能夠讓我可以測試自己這段時間學習資安的成果。比較有趣的是題目類型很多元,從基礎的 MISC 到需要深入分析的 Rev 和 Crypto 都有涵蓋到,而且可以看出 author 很絞盡腦汁且用心的出題。

其中讓我比較有信心的是 Ramen CTF 這題,雖然是 OSINT 類型,但我很快就從發票資訊推敲出店名,透過財政部稅籍查詢系統和 Google Maps 找到正確答案,感覺平常練習 Google 搜尋技巧還是有用的。

Web 類的題目也很實用,像是 Tomorin db 這題讓我學到了更多關於 Go 語言 HTTP 服務的漏洞利用技巧,用 ffuf 進行目錄爆破也是很經典的手法。Login Screen 1 能夠考驗對於 Burp Suite 是否足夠熟悉,利用 repeater 功能繞過 2FA,這種解法還蠻巧妙的。

Rev 的部分是我覺得最有挑戰性的,WebAssembly 逆向工程對我來說算是比較新的領域,需要分析 WASM 程式碼找出加密演算法,然後寫 Python 腳本進行反向解密。這題讓我對位元運算和旋轉加密更加了解,也學到了如何處理大整數和 ASCII 轉換。不過說實話我在這部分花了很多時間,發現自己在逆向工程這塊還需要多加練習。

Crypto 的 Stream 題目設計得很聰明,前面 80 行都是用來混淆視聽的雜訊,真正的 flag 藏在最後一行。利用已知的 flag 格式 AIS3{ 來推算 key 的平方值,然後進行暴力破解,這種攻擊手法在現實中也很常見。

整體來說,這次比賽讓我對自己的能力有了更清楚的認知,也發現了一些需要加強的地方,特別是 Pwn 和 Rev 的部分。雖然這次比賽沒有解出 Pwn 題目,但我知道這是我接下來必須要重點加強的領域,畢竟類似得題目在 CTF 中佔了很重要的份量。Rev 的部分雖然有解出來,但過程中還是有些吃力,需要多練習不同類型的逆向工程題目。這次的名次雖然處於一個比上不足,比下有餘的位置,但幸好最後有成功進入 AIS3 暑期課程,讓我更加期待。

希望我的文章能給各位讀者們幫助,歡迎幫我按個拍手跟追蹤,您就能收到新文章的通知,也歡迎把本文分享給您的親朋好友,讓更多人知道喔!

若如有任何疑問及合作歡迎來信:ohmygodmason@gmail.com

By 那個成天泡在電腦裡的梅森 on August 22, 2025.

Canonical link