Before all
完整題目可以從: https://github.com/William957-web/My-CTF-Challenges/tree/main/AIS3-Pre-Exam/2026/crypto 下載
整體而言這次題目 code base 和解題腳本都偏大,要了解細節建議對照著 Github 看。
預期難度(由易到難)EasyWEB < EasyJWT < EasyZKP < EasyPAINT < EasyFAULT <= EasyRSA
🤔 其實這次真的沒有往死裡出,詳情請看接下來的 writeup!
Writeup
EasyWEB
Web Crypto 黑箱題目,可以輸入 username 和 note 內容
選擇使用 Cloudflare 五秒盾 + 後來失效的 Google Recaptcha 防 Agent 自動化解題,可以擋一下但認真唱一唱其實唱的過去,笑死
應該會注意到送出後會拿到一個 session cookie
也會注意到前端(後端也有)禁止 username 以 . 和 / 開頭,可以猜測是 Path Traversal 的 Patch
這時候如果在 name 輸入各種 input 像是 100 個 a 之類的會得到兩個資訊
- 輸入內容都是 16 的倍數 bytes 的 hex string
- 會有重複的 block 出現
ECB Mode!
進行 ECB Copy and Paste Attack 就可以做到 LFI
Payload 就如同 a*16 + /etc/passwd\x05\x05\x05\x05\x05,再把拿到的 session 去除前 16 Bytes 複製貼上拿去當新的 session 即可讀到 /etc/passwd (在這邊要特別去記得處理 )
最後就是讀取 /proc/self/cmdline 取得當前 process cmdline + 讀取 app.py 就可以拿到原始碼 (flag)
EasyJWT
一樣是一個看起來很像 Web 題的題目,目標是要透過 XSS 去戳到限制內網的網站 /flag URI 拿取 flag
XSS 觸發點在這:
1 |
|
如果在 verify_jwt 的時候發生 error,那就會回傳 500 然後把 jwt token 內容 payload (JSON) 完全送出
稍微介紹一下 RS256 的 JWT 是怎麼實作的,簡單說就是把 Token 內的資料取 SHA256 後,做 RSA Signing,也就是把加密的過程反過來做:利用私鑰 $d$ 把訊息取 $H(m)^d(mod\space N)$ 作為簽章值交出
仔細 code review 一下 jwt.py 會發現是固定了公鑰 $e$ 後使用輾轉相除法,針對 $e, \phi(N)$ 構造一個 $ed-\phi(N)x=1$ 的過程求模逆元(其中 $x$ 值不重要
1 | E = 677676677 |
欸,等等等,不對不對。
首先我們的 $e=673 * 1006949$ … 其實沒什麼大問題,但是在求取 inverse 的時候,如果 $e$ 與 $\phi(N)$ 不互質,那麼輾轉相除法解出來的是 $ed-\phi(N)x=gcd(e, \phi(N))$,而 673 其實不是一個大質數…不會難撞到。
如果撞到了,就會導致在驗證簽名的時候有 $\operatorname{sign}^e \equiv H(m)^{ed} \equiv H(m)^{gcd(e, \phi(N))} \not\equiv H(m) \space (mod \space N)$
進而產生 error,而 JWT 初始化的時候會吃一個 text 參數存進去,在 /login 路由是一個 GET 參數,可以用來把 XSS Payload 寫入 JWT Token
1 |
|
再來,雖然 673 不算大,但是要一次命中也不容易,儘管題目有開 challenge instancer,但重複去洗個 600 多次對選手或出題團隊其實也是個負擔 T^T
注意到這個 Flask 網站有開啟 debug mode,並且在 /upload 上傳路由中有個 t 參數可以指定檔案來做 os.utime 更新檔案時間,如果我們一直去更新網站程式檔(如 app.py 之類的),因為 debug 模式開啟所以會一直重啟整個服務,這時候就會生成新的 RSA 參數,直到 $\phi(N)$ 與 $e$ 不互質我們就贏了!
1 |
|
最後還有一點就是,在繳交 URL 的時候會驗證一個 PoW (Proof of Work) 的 Challenge,本來我設計的 challenge size 是 36 bits 並提供了 PoW Script,但這樣在 instancer 給的 15 分鐘內幾乎不可能跑得完。所以還有一點是要自己改寫腳本,去申請比較多 challenge (像是 $2^{12}$ 組),再離線去計算大約 $2^{24}$ 量級的 Challenge 會比較合理(就是做 Meet in the Middle)。
解題腳本有點長就請看我 Github Repo 吧 XD
EasyZKP
一個架構題,內部有個 HTTP Server 作為 Proover,外部跟你 nc 互動的則會作為 client 去請求新的 Proof 下來,整體偏向 Σ-protocol 的 ZKP?!(其實不是 lol,只是概念像而已但隨便
目標是能在題目給定 seed 和 server challenge 的情況下 fake 十六個 proof 出來 (注意到這邊還是可以給 user challenge)
整體的模式是,以 Flag 作為 Secret,每次可以提供一個 user challenge,而 client 會再生成一個隨機的 seed $s$ 與 server challenge,結合的 Challenge 是 user||server
最後會以 sha256(FLAG||Challenge) 生成一個 state (256 bits state)
最後要產出的 proof $p$ 初始是 0,如果現在 state 的 bit = 0
$p \equiv p+s \space (mod \space N)$
否則就是
$p \equiv p^s \space (mod \space N)$
這樣做 256 次後會產出 proof,而這個 proof 只有持有 FLAG 的人可以拿出,最後證明的方法就類似事後才有持有者給出該 Challenge 的 Hash value 再讓大家自己算算看(不過這 part 沒在題目中出現,題目設定中沒有直接的 Hash 值可以拿)
另外,這題還有加入一個 fault attack condition,玩家可以選擇一個 bit 位置然後每次 sha256 產出的結果那一個位元會被翻轉
而限制是只能 query 領 proof 128 次 + fault attack 128 次
參數範圍的 $N$ 在電腦上可以用 cado-nfs 幾分鐘完成因數分解拿到 p, q,也可以直接去查 factordb
第一個漏洞在這邊,在我們提供 Challenge 的時候:
1 | def fetch_proof(user_part_b64, server_part_b64, seed, bit_flip_indices=None): |
這邊是直接把 user_part_b64 變數放進去 URL 裡面,但是 seed 參數在它的位置後面,等於我們可以透過多注入一段 &s=... 去控制 seed value。
回顧剛剛的流程,接下來我們可以考慮可控的 seed value 可以怎麼攻擊,先把目標定為還原 sha256 計算結果(因為我們給定的 user challenge 就是部分後綴,一但有一筆計算結果,可以利用 Length Extension Attack 在未知秘密 FLAG 的情況下計算出 sha256 value)。
首先 seed 有個範圍限制 必須介於 $2 \sim N-1$,阻絕了直接把 seed 設為 1 做很簡單 oracle 的可能性。
這時候考慮到 高次剩餘 (link),我們可以把 seed 設成 $\phi(N)$ 的某個大因數,接著拿到數字結果後判斷它是不是一個 s 次剩餘,如果是的話那目前執行到的最後一個 bit 就很高機率是 1(大因數的原因是讓加法後產出的數字幾乎不可能剛好是個 s 次剩餘)
但是 s 次剩餘共有 s 個根,一旦我們為了攻擊穩定性把 s 調高,那逆推就顯得極為困難了,這時候如果當前測試的最後一個 bit 是 1 就把它 flip 成 0,後面解下一個 state 的時候記得先減去 s 即可。
如果測試出來 bit 是 0 那運氣挺好的,等等計算的時候只需直接把它減去就可以解下一個 state 了。
最後,因為 sha256 結果有 256 bits,我們可預期期望值大概有 128 個 1 和 0,query 和 fault attack 各 128 次操作有很高機率會讓我們能還原整個 sha256 算出來的結果。
結束後就是在申請 challenge 的時候使用 sha256 length extenstion attack 利用剛剛還原的 sha256 state + 我們控制後綴來解出 sha256 推算值,並依樣畫葫蘆算出 proof 去驗證就好,唯一的麻煩是 LEA 跟 salt(前綴,也就是 flag)長度有關,所以要去爆破一下。
EasyPAINT
我希望選手在畫畫的時候是開心的
原本在 64*64 的畫布上是長這樣的。 XD
這題其實是 Reverse + Crypto,用 Pygame 寫了支加密玩具後用 Nuitka 做包裝,其中 Nuitka 的原理其實就是把 Python source code parse 出來後 compile 成 pyc 包進去,最後再把需要的 lib 作為 dependencies 一起裝進去。
一開始打開是一個密碼輸入介面,如果有逆向的話就會知道密碼 charset 是這些 "abcdefghijklmnopqrstuvwxyz_0OABCDEFG",而密碼比對是去比對一個 sha256 value,而密碼大小是 512 chars。
接下來的加密流程是用 LWE,整個加密流程是透過 512 x 512 的畫布畫出來的黑白建立 0/1 的 Public Key Matrix $A$,把密碼的每一個字元都建取 sha256 排成一個 512 維的 secret vector $s$,$Q \sim 256 \space bits$ 而 $p=257$,會生成一組 mod p 的 error vector $e$,最後我們要加密的訊息(對稱式解密訊息用的密鑰)的 key vector 是 $k$,其中每一位都是 1 byte 的某個數字
LWE 的流程是:
$$A\times s + 256 e + k$$
只有持有 $A$ 以及私鑰 $s$ 的人可以 unmask,最後再把每一項 mod $p$ (256) 來提取 $k$
這題要解決的問題有幾個:
- 沒有公鑰 $A$
- 我們到底持有多少 $s$ 的資訊
注意到雖然我們沒有私鑰 $s$ 的內容,但我們知道它的每一項都是某個字母取 sha256 後的 hash value,所以 $A\times s$ 可以想像成產出的每一列都是 “該字母這次被 0/1 Vector 數了幾次”,而這個次數其實挺小的,是已知項的線性組合其中每個 entry (數幾次) 都很小
最後因為 $e$ 其實量級(16 bits) 相對於整個 mod 和前面的運算 (256 bits) 極小,所以其實 $A\times s \approx A\times s + 256 e + k$,可以直接做 CVP(link) 或者把 $256e$ 當作另一個小根利用 LLL(link) 解線性方程都是可以的! (不過這邊可能會需要調整配重)
至於 LLL 解小根方程的簡單介紹可以看我以前出的題目:
NoHackNoCTF 2024(link)
配種可以看這個
THJCC CTF 2025 (link)
EasyFAULT
LLL 大題,其實分成三個階段:
- 從
blob裡還原base - 用
base把data裡被 XOR mask 掉的 exponent fault 還原 - 利用很多組
$$
y_i \equiv c^{d \oplus r_i} \pmod n
$$
的關係,先還原 RSA modulus $n$,再還原明文 $m$
題目核心程式碼如下:
1 | c, n, d = gen() |
其中:
- $d$ 是 448 bits 的 RSA private exponent
- $r_i$ 是 448 bits random row
- 輸出的第一項是 $r_i \oplus f(\text{base}, i)$
- 輸出的第二項是
$$
y_i \equiv c^{d \oplus r_i} \pmod n
$$
如果可以知道 base,就可以算出所有 $f(\text{base}, i)$,進而還原所有 $r_i$。
第一步也是一種叫做 HSSP/AHSSP 的問題:
https://eprint.iacr.org/2020/461.pdf
Step 1: Recover base
先看 bundle(base):
1 | def bundle(base): |
對每一個 set,有 72 個方程:
$$
v_j \equiv \sum_{i=0}^{31} a_i x_{i,j} - \text{base} \cdot b_j \pmod p
$$
其中:
- $p$ 是
mod - $b_j$ 是
bias[j] - $v_j$ 是
vals[j] - $a_i$ 是未知的
coeffs[i] - $x_{i,j}$ 是第 $j$ 個 32-bit random row 的第 $i$ 個 bit
移項後:
$$
v_j + \text{base} \cdot b_j
\equiv
\sum_{i=0}^{31} a_i x_{i,j}
\pmod p
$$
右邊其實只落在一個由 32 個 column span 出來的低維線性空間裡。
如果我們找到一組係數 $\lambda_j$,使得:
$$
\sum_j \lambda_j x_{i,j} = 0
\quad
\text{for all } i = 0, 1, \dots, 31
$$
那麼把所有方程乘上 $\lambda_j$ 加起來,右邊會消失:
$$
\sum_j \lambda_j v_j
+
\text{base} \sum_j \lambda_j b_j
\equiv 0 \pmod p
$$
因此只要 $\sum_j \lambda_j b_j \not\equiv 0 \pmod p$,就可以推出:
$$
\text{base}
\equiv
-\frac{\sum_j \lambda_j v_j}{\sum_j \lambda_j b_j}
\pmod p
$$
這題有兩個 set,真正的 base 對兩邊都一樣,所以可以用兩個 modulus 算出來的結果交叉驗證。
Auxiliary lattice
實作上先對每個 set 建立模 $p$ 的 kernel:
$$
\sum_j \lambda_j v_j \equiv 0 \pmod p
$$
$$
\sum_j \lambda_j b_j \equiv 0 \pmod p
$$
也就是對矩陣:
$$
\begin{bmatrix}
v_0 & v_1 & \cdots & v_{71} \
b_0 & b_1 & \cdots & b_{71}
\end{bmatrix}
$$
找 kernel。
這個 kernel 裡會包含很多短向量,而真正有用的是來自於那些低維 bit column 結構的 relation。因為 72 個 samples 只由 32 個 bit columns 生成,所以理論上會有大約:
$$
72 - 32 - 1 = 39
$$
個短 relation。
把這些短 relation 找出來後,再取 right kernel,就可以回復出原本由 bit columns span 出來的 32 維 lattice。兩個 set 都做一次後,取交集,可以得到穩定的 auxiliary lattice。
最後對這個 lattice 再取 right kernel,就可以得到滿足:
$$
\sum_j \lambda_j x_{i,j} = 0
\quad
\forall i
$$
的 relation。
對這些 relation 逐一測試:
$$
\text{base}
\equiv
-\frac{\sum_j \lambda_j v_j}{\sum_j \lambda_j b_j}
\pmod p
$$
如果兩個 set 算出來的 base 一樣,就成功還原 base。
Step 2: Unmask all rows
題目輸出的 data 是:
1 | (row ^ f(base, i), pow(c, d ^ row, n)) |
還原 base 後,就可以算:
1 | row = masked ^ f(base, i) |
因此我們現在有很多組:
$$
(r_i, y_i)
$$
其中:
$$
y_i \equiv c^{d \oplus r_i} \pmod n
$$
接下來的目標是利用這些資料把 $n$ 和 $m$ 找回來。
注意這題沒有直接輸出 $n$。
Step 3: Rewrite the faulty exponent
令:
$$
r_i = \sum_{k=0}^{447} r_{i,k} 2^k
$$
$$
d = \sum_{k=0}^{447} d_k 2^k
$$
則:
$$
d \oplus r_i=r_i + d - 2(d & r_i)
$$
如果展開成 bit:
$$
d \oplus r_i=r_i+\sum_{k=0}^{447} d_k (1 - 2r_{i,k})2^k
$$
現在考慮一組整數係數 $\alpha_i$,如果它滿足:
$$
\sum_i \alpha_i = 0
$$
以及對每個 bit position $k$ 都有:
$$
\sum_i \alpha_i r_{i,k} = 0
$$
那麼:
$$
\sum_i \alpha_i r_i = 0
$$
且:
$$
\sum_i \alpha_i(d & r_i) = 0
$$
所以:
$$\sum_i \alpha_i(d \oplus r_i)=\sum_i \alpha_i r_i+d \sum_i \alpha_i-2\sum_i \alpha_i(d & r_i)=0$$
因此:
$$\prod_i y_i^{\alpha_i}\equiv\prod_i c^{\alpha_i(d \oplus r_i)}\equiv c^0\equiv 1\pmod n$$
也就是:
$$\prod_{\alpha_i > 0} y_i^{\alpha_i}-\prod_{\alpha_i < 0} y_i^{-\alpha_i}\equiv 0 \pmod n$$
這個差值會是 $n$ 的倍數。
所以只要找到很多這種 relation,對所有差值取 gcd,就能把 $n$ 找回來。
Recover modulus $n$
要找 relation,就把每個 $r_i$ 寫成 bit vector,並且額外加上一個常數 1:
$$(r_{i,0}, r_{i,1}, \dots, r_{i,447}, 1)$$
找係數 $\alpha_i$,使得:
$$\sum_i \alpha_i(r_{i,0}, r_{i,1}, \dots, r_{i,447}, 1)=0$$
這可以用 lattice reduction 找短 relation。
對每個 relation,計算:
$$\Delta=\prod_{\alpha_i > 0} y_i^{\alpha_i}-\prod_{\alpha_i < 0} y_i^{-\alpha_i}$$
理論上:
$$
n \mid \Delta
$$
所以:
$$
n = \gcd(\Delta_1, \Delta_2, \dots)
$$
實作時 gcd 可能會帶一些小因數或雜訊,所以可以把小質數除掉,再用所有 $y_i$ 跟目前的候選 $n$ 做 gcd 清理。
Step 4: Recover plaintext
目前已知:
$$
y_i \equiv c^{d \oplus r_i} \pmod n
$$
而 RSA 裡:
$$
c^d \equiv m \pmod n
$$
所以如果我們可以湊出:
$$
\sum_i \alpha_i(d \oplus r_i) = d
$$
那麼:
$$
\prod_i y_i^{\alpha_i}
\equiv
c^d
\equiv
m
\pmod n
$$
不過直接湊出 $d$ 很難。可以先湊出 $t d$。
這次 relation 條件稍微放寬,只要求:
$$
\sum_i \alpha_i r_{i,k} = 0
\quad
\forall k
$$
但不要求:
$$
\sum_i \alpha_i = 0
$$
令:
$$
t = \sum_i \alpha_i
$$
則:
$$\sum_i \alpha_i(d \oplus r_i)=\sum_i \alpha_i r_i+d\sum_i \alpha_i-2\sum_i \alpha_i(d & r_i)$$
因為每個 bit 的 weighted sum 都是 0,所以:
$$
\sum_i \alpha_i r_i = 0
$$
且:
$$
\sum_i \alpha_i(d & r_i) = 0
$$
因此:
$$\sum_i \alpha_i(d \oplus r_i)=td$$
所以:
$$
\prod_i y_i^{\alpha_i}
\equiv
c^{td}
\equiv
(c^d)^t
\equiv
m^t
\pmod n
$$
我們可以用 lattice 找到很多組 relation,得到很多個:
$$
m^{t_1}, m^{t_2}, m^{t_3}, \dots
$$
如果這些 $t_i$ 的 gcd 是 1,也就是:
$$
\gcd(t_1, t_2, \dots, t_s) = 1
$$
那就可以用 extended gcd 找到整數 $\beta_i$,使得:
$$
\sum_i \beta_i t_i = 1
$$
於是:
$$
\prod_i (m^{t_i})^{\beta_i}
\equiv
m^{\sum_i \beta_i t_i}
\equiv
m
\pmod n
$$
這樣就可以直接還原 flag。
Flag 是
1 | AIS3{lll_then_lll_then_lll_then_lll_owob} |
XDDD
EasyRSA
短短的程式碼 大大的鯨喜!
1 | from Crypto.Util.number import * |
首先最後的 N*getPrime(512) 根本不用管,假設我們能分解出 $p$,我們可以先想辦法用 $mod \space p*q$ 的量(或他的某個因數)去嘗試解密 flag,理論上 flag 通常不那麼長。
開胃小菜想完了,現在是主角。
我們考慮 $\mathbb{F}_p$ 下的多項式環,假設今天上面有個不可約多項式 $f_1(x)=ax^4+bx^3+cx^2+dx+e$
我們來考慮 $\mathbb{F}_p[x] / \langle f_1(x) \rangle$,我們知道它的 order 會是 $p^4-1$
注意到 $p^4-1=(p^2+1)(p^2-1)$,剛剛是不是哪裡有個 $p^2+1$?沒錯,就是我們 $N$ 裡面因數的 $q$
再來我們考慮某個 $\mathbb{F}_p[x] / \langle f_1(x) \rangle$ 上的三次多項式 $g_1(x)$,取 $g_1(x)^N = g_2(x)^{p^2+1}$ 會得到某個 order 剩下 $p^2-1$ 的生成元 $g_3(x)=g_2(x)^{p^2+1}$。
$p^2-1$ 代表的是某個二次多項式形成的多項式環的 order,也就是說在 $\mathbb{F}_p$ 下這個多項式的2, 3 次項退化成 0 了。
等等,那假設我們考慮 $\mathbb{Z}[x] / \langle f_1(x) \rangle$ 呢?
隨機挑選多項是 $f_1, g_1$ 一直做這些操作,再取計算後的生成元 $g_3$ 的 2, 3 次項,它很可能會跟 $N$ 產生最大公因數 $p$ (因為本來在 mod $p$ 的時候變成 $0$了)。
Solved!
Exploit 一樣看 GitHub XDD
P.S. 中間 Threads 上還有選手互動的 part xDD
After all
TL;DR 一下為什麼這次 Crypto 整個領域除了我還是我出的 :)
上次出 Pre Exam / EOF 都沒寫完整 writeup,一方面是沒空(現在其實更忙),另一方面是後面在討論區其實都跟大家聊的差不多了(?
怎麼說呢,但這次情況特殊好像值得寫一篇文章紀念一下 XDDDD
情況特殊不單單是指整場的密碼學被一個人包下,也想順手紀錄下這次的對抗 LLM 之旅
如大家所知,比賽的題目分數會在 100~500 分間隨解題人數即時動態調整…?!
P.S. 先附上其他領域的分數分布做個對照組,理想中我以為分數線會長得像 pwn 那樣,看起來大家真的都不喜歡動腦解 Crypto >:(
或者說動腦解 CTF?

其實在這次 Pre Exam 當完 Staff + 周末打完 DEFCON CTF Qual 後深深感覺到 CTF 應該要有所改變了(或者說慢慢在改變?