Before all
Rank: 23
Team: fewer
這是我第一次跟 fewer 一起打比賽,解了一題有趣的 Crypto 題
明顯感覺的到近年為了抗衡 AI Agents(事實上,這已經幾乎不可能),題目都走向了更難、code base 更龐大的階段。畢竟 dice ctf 我去年也有解一題 crypto,難度根本三級跳
這是一道關於 ZKP / SNARG, 可以學到 DPP (dot product proof?) 結構的題目
題目過一陣子應該在 https://github.com/dicegang 有
Writeup
dot
Server 會走 SNARG 和 DPP 的組合來確認是否對於提供的 a, b,你輸入的 c 和 proof 是可以證明 a + b = c 的。
要偽證 20 次才可以拿到 flag
先來看 dpp.py:
1 | import random |
Circuits 先不管,簡言之就是把想要的操作透過 gates 和 wires 扭回去線性乘法
首先,對於 bits b1, b2 而言,xor 就可以定義是 $b_1 + b_2 - 2b_1b_2$ ,同理對於其他位元操作,我們會知道其實他們都可以變成兩變數 與 兩變數乘積的一種線性組合
所以事實上今天可以給定一個向量:
$\pi = {x_1, x_2, x_3, \cdots, x_n, x_1\times x_1, x_1 \times x_2 \cdots x_n \times x_n }$
然後透過事先構造好的向量跟$\pi$內積(dot)來完成每個 bit 組合後是否滿足特定運算的結果的檢查
為此,在 dpp 裡面作了以下定義:
- $q1$ : 一個長度為 n 的隨機向量
- $q2$ : $q1||q1’$,其中 $q1’$ 就是類似上面 $\pi$ 後半段的構造方法,不過不同項相乘要改成兩倍因為 $q2$ 最後是要做一個平方項
- $q3$ : 某些很大的數字用以驗證條件的,在這題是用來透過對剛剛 “事先構造好的向量” $\pi$ 驗證是否滿足加法相等條件的,如果滿足的話那他每次產出的值都會相同 (我們叫它 val)
最後
$q = q1 + B \cdot (q2 - q3)$
其中 B 是一個大數 Bound,當一個滿足的 proof $\pi$ 輸入進來的時候,我們會可以有效預測 $\langle q, \pi \rangle$ 的值(他對 p3 的 dot value 永遠固定,那不固定的就剩下 q1, q2 的部分了,但是根據我們 dot $q$ 出來的結果會等於 $k-B \cdot (k^2-val)$ 其中 $k$ 是 $\langle q1, \pi \rangle$)。
於是就可以建立答案池,根據 k 和固定的 val 值去推算所有結果。
這樣做的方法是透過前面加入一個 k 以及一個大 Bound 讓攻擊者如果想透過線性補償滿足 $(q2 - q3)$ 部分的計算依然需要考慮 q1 導致無解。
snarg.py
1 | import hashlib |
在 snarg.py 中定義了整個 SNARG 協議,我們可以特別注意到是基於橢圓曲線的,一開始有個生成元 $G$ 以及一個 HASH 函數把 int 值 HASH 後對上曲線上的點,定義它叫做 $H(i)$。
公開出來的公鑰(crs.bin 檔案內容)包含這些點:
$C_i = q_i \cdot G + sk \cdot H_i$
整個 $q$ 向量是私鑰,還有我們的 $sk$
Prover 每次要提供的就是兩個點:
$h_1 = \sum_{i=n}^{M} \pi_i \cdot H_i$
$h_2 = \sum_{i=n}^{M} \pi_i \cdot C_i$
M 就是剛剛那個 $\pi$ 的長度
根據前面講 dpp 的方法他這樣就可以還原點的結果並跟答案池對答案:
$P = h_2 - sk \cdot h_1 + (\langle \pi, p1 \rangle \cdot G)$
最後是這題的 server.py (dist file 並未提供私鑰 vk.bin)
1 | #!/usr/local/bin/python3 |
最後,這題的漏洞是對於 BOUND1 (snarg.py 中)的定義過小
code trace 注意到 dpp.py 這一段
1 | def tensor_queries(circuit: Circuit, bound: int) -> tuple[Vector, Vector]: |
這邊對於 q2 向量的構造其實在 BOUND1 是 2**8 範圍下的時候,假設攻擊者翻 LSB 讓值偏離一下(這樣就可以滿足 a+b 不等於 c),因為 LSB 相關的只會有第 128 bit 的 q 以及後續相乘的時候在 wire 裡面的 l 值,他們都在我們小小的 BOUND1 範圍內,所以分批枚舉就好。
而這剛好是可以枚舉出來的,注意到對於 $h_1$ 點的值因為 hash 是公開地所以很好 fake。
$h_2$ 的部分,則是因為只有偏移 LSB,爆破兩個參數的時候再最後面加上本次猜測的值會跟原本的正確答案偏移多少倍的點 $G$ 給它補回去試試看就知道了。
其他我覺得看 exploit 更快(?) 一樣是用 Gemini 修好的
exp.py
1 | #!/usr/bin/env python3 |