# AIS3 EOF 2025 這裡記錄兩題crypto jeopardy題的解法,我覺得兩題都滿有趣的 ## prsa :::spoiler 題目 ```python from Crypto.Util.number import getPrime, getRandomRange, bytes_to_long import os def keygen(sz): p = getPrime(sz // 2) q = getPrime(sz // 2) n = p * q phi = (p - 1) * (q - 1) e = 0x10001 d = pow(e, -1, phi) g = 1 + n #pq+1 mu = pow(phi, -1, n) pk = (n, e, g) sk = (n, d, phi, mu) return pk, sk def rsa_encrypt(pk, m): n, e, g = pk return pow(m, e, n) def rsa_decrypt(sk, c): n, d, phi, mu = sk return pow(c, d, n) def paillier_encrypt(pk, m): n, e, g = pk r = getRandomRange(1, n) n2 = n * n return (pow(g, m, n2) * pow(r, n, n2)) % n2 def paillier_decrypt(sk, c): n, d, phi, mu = sk cl = pow(c, phi, n * n) return ((cl - 1) // n) * mu % n def rsa_to_paillier(pk, sk, c): return paillier_encrypt(pk, rsa_decrypt(sk, c)) def paillier_to_rsa(pk, sk, c): return rsa_encrypt(pk, paillier_decrypt(sk, c)) def pad(m, ln): pad_ln = ln - len(m) pre = pad_ln // 2 post = pad_ln - pre return os.urandom(pre) + m + os.urandom(post) def main(): pk, sk = keygen(1024) flag = os.environ.get("FLAG", "flag{fake_flag}") m = bytes_to_long(pad(flag.encode(), 1024 // 8 - 1)) c = rsa_encrypt(pk, m) print("RSA Encrypted Flag:") print(f"{c = }") for _ in range(48763): print("1. RSA to Paillier") print("2. Paillier to RSA") print("3. Exit") choice = int(input("> ")) if choice == 1: c = int(input("c = ")) c = rsa_to_paillier(pk, sk, c) print(f"{c = }") elif choice == 2: c = int(input("c = ")) c = paillier_to_rsa(pk, sk, c) print(f"{c = }") elif choice == 3: break else: print("Invalid choice") if __name__ == "__main__": main() ``` ::: 這題目給了一個可以將RSA的密文和[paillier](https://en.wikipedia.org/wiki/Paillier_cryptosystem)的密文相互轉換的功能 其實在遇見這題之前我並不知道Paillier cryptosystem是在幹嘛的,上網查了一下發現它的加密有同態的性質 $$ E_{paillier}(m_1) \cdot E_{paillier}(m_2) = E_{paillier}(m_1 + m_2) \mod n^2 $$ 另外RSA我們本來就知道(?也具有同態加密的性質 $$ E_{RSA}(m_1) \cdot E_{RSA}(m_2) = E_{RSA}(m_1 \cdot m_2) \mod n $$ 我們便可以利用這些酷酷的同態性質做出酷酷的操作,但遇到的第一個問題是我們沒有n,而求n常見的方法是利用 $$ (x \hspace{0.2cm} mod \hspace{0.2cm} n) \hspace{0.3cm} \cdot \hspace{0.3cm} (y \hspace{0.2cm} mod \hspace{0.2cm} n) \hspace{0.3cm} - \hspace{0.3cm} (xy \hspace{0.2cm} mod \hspace{0.2cm} n) $$ 得到的數會有n這個因數 因此在這裡我們使用以下式子來求n $$n = gcd( E_{RSA}(m)^2 - E_{RSA}(m^2), E_{RSA}(m)^3 - E_{RSA}(m^3) )$$ 再來就是用酷酷的同態性質做出酷酷的操作的時刻了! $$ \begin{flalign} \\ & E_{RSA}(m) \xrightarrow{decrypt_{RSA}} m\xrightarrow{encrypt_{paillier}} E_{paillier}(m) \\ \\ & E_{paillier}(m) \cdot E_{paillier}(m) = E_{paillier}(m+m)\xrightarrow{decrypt_{paillier}} 2m \xrightarrow{encrypt_{RSA}} E_{RSA}(2m) \\ \\ & E_{RSA}(2) = E_{RSA}(2m) \cdot E_{RSA}(m)^{-1} \hspace{0.2cm} mod \hspace{0.2cm} n \\ \\ & E_{RSA}(2) \xrightarrow{decrypt_{RSA}} 2\xrightarrow{encrypt_{paillier}} E_{paillier}(2) \\ \\ & E_{paillier}(m) \cdot E_{paillier}(2) = E_{paillier}(m+2)\xrightarrow{decrypt_{paillier}} m+2 \xrightarrow{encrypt_{RSA}} E_{RSA}(m+2) \\ \\ \\ \end{flalign} $$ 至此我們有了$E_{RSA}(m)$和$E_{RSA}(m+2)$就可以使用[Franklin–Reiter related-message attack](https://github.com/ashutosh1206/Crypton/blob/master/RSA-encryption/Attack-Franklin-Reiter/README.md)了!然而一般的gcd速度太慢,因此我直接用了[這篇](https://github.com/death-of-rats/CTF/tree/master/Dice2021/plagiarism)裡面提到的更快計算gcd的方法 <br/> 以下為完整的payload(抱歉變數名稱取得很爛:p ) :::spoiler solution ```python from pwn import * from Crypto.Util.number import GCD r = remote('10.102.100.30', 21337) r.recvline() c = eval(r.recvline()[3:]) print(f'c = {c}') e = 65537 def ans(choice, number): r.sendlineafter(b'> ',str(choice).encode()) r.sendlineafter(b'= ',str(number).encode()) return eval(r.recvline()[3:]) p = ans(1,c) # p(m) c_2 = ans(2,p*p) #2^e * m^e p_2 = ans(1,c_2) #p(2m) c_e = c # c = m^e p_m_square = ans(1, c_e*c_e) #p(m^2) c_e_square = ans(2, p_m_square) p_m_cube = ans(1, c_e*c_e*c_e) #p(m^2) c_e_cube = ans(2, p_m_cube) n = GCD(c_e_square - c_e*c_e, c_e_cube - c_e*c_e*c_e) # get n!! print(n) c_2_e = c_2 * pow(c,-1,n) % n #2^e p_2_constant = ans(1,c_2_e) c_m_plus_2 = ans(2,p*p_2_constant) print( c_m_plus_2) ``` 將得到的$E_{RSA}(m)$和$E_{RSA}(m+2)$丟入更快的GCD ```python #!/usr/local/bin/sage -python import binascii from sage.all import * import numpy as np import sys def franklin(n, e, delta, c1, c2): R = PolynomialRing(Zmod(n), 'X') X = R.gen() f1 = (X)**e - c1 f2 = (X + delta)**e - c2 r = gcd(f1, f2) co = r.coefficients() return -co[0]//co[1] def hgcd(a0,a1): if a1.degree() <= (a0.degree()//2): return np.array([[1,0],[0,1]]) m = a0.degree()//2 X = a0.variables()[0] b0 = a0 // X**m b1 = a1 // X**m R = hgcd(b0,b1) [d,e] = (R.dot(np.array([a0,a1]).transpose())).transpose() ff = d % e m = m // 2 g0 = e // X**m g1 = ff // X**m S = hgcd(g0,g1) q = d // e return S.dot(np.array([[0,1],[1,-q]])).dot(R) def gcd(a0,a1): while True: print(a0.degree(), end=", ", flush=True) if a0 % a1 == 0: return a1 if a0.degree() == a1.degree(): a1 = a0%a1 #print(a0.degree()) R = hgcd(a0,a1) [b0,b1] = R.dot(np.array([a0,a1]).transpose()).transpose() if b0%b1==0: return b1 c = b0 % b1 a0 = b1 a1 = c def main(): c2 = 66834555546603916608333653016401878776688916551592143035167253275230240766945198849834436542517296027329563059685319968537731358565394668263762674040400032669021171984639375055846753356780038323808260737561179083071093870132320531102232585884330838200333219121130652393831449046975064423525272905434682119641 # RSA(m + 2) c1 = 74309692513998681300874362475679715926141823491721960009423424837642075969042788863193700950542384139500474947327567396515929070514702368856717418697542499654732990670025110358696551237462617652678952298208884627721522533882340862877607645156704396035860036493500429926181474230249481785324891749678676998795 # RSA(m) N = 110651064591193823655829340724584396245037917386997729630017486070807378214452780923854687746762264814933821458635154464755788173581406384989704032601333549693981377505193175063386380186949929980764866648996740397660096071335187937305844353276920699700989045357881610175517571094873215069218393380692395876769 e = 65537 diff = 2 flag = franklin(N, e, diff, c1, c2) print(flag) h = hex(int(flag))[2:].rstrip("L") h = "0" * (len(h) % 2) + h print (binascii.unhexlify(h)) main() ``` ::: :::success AIS3{this_is_what_could_happen_to_rsa_if_it_is_an_additively_homomorphic_encryption} ::: ## zkdlp :::spoiler 題目 ```python from Crypto.Util.number import bytes_to_long from hashlib import shake_128 import os, random p = 0x2CDE2997126F706BD27498A9FA07E93F321B4932982BC455910FF694160DB5484257D0886EC66E5D7BE59ECFD16AAFF6B5BD57E600FAB97E7CF75D76A7F12BC4619A036ED8787F4508CC7C1FB35689575E007B7DC6B1EECC4B9BC2E91AA31FBE027C62BFF3E2065912591ECC1C361CEEAE75B382F1BD7D967633FD91476A3ABC4AD22CD3372C3FC40C2841B3BC70DAC11E3A6631AB3BE49AB9F748AE9093FBAB15B5457244363F444D146C0ADE84CC1AB0D0CFB2AD329483E957235EDD0085BD2F5CDAFCD77D00622A9DFCD3C0098DCB42C7EF1DEE808E8216F0F0638F51D26614B0C61352A13565098FE60146FF7E46FAEBCB75629DAF517880E36AEEE617B9F q = (p - 1) // 2 g = 2 assert pow(g, q, p) == 1 def main(): flag = os.environ.get("FLAG", "flag{fake_flag}") hflag = shake_128(flag.encode()).digest(q.bit_length() // 8) #strong hash x = bytes_to_long(hflag) y = pow(g, x, p) print("Do you know the flag?") print(f"{y = }") wins = 0 while wins < 10: t = int(input("t = ")) % p if t == 0: print("Nope") return c = random.randrange(q) print(f"{c = }") s = int(input("s = ")) % q if pow(g, s, p) == t * pow(y, c, p) % p: print("Hmm, you probably know the flag!") wins += 1 else: print("No, you don't know the flag :(") wins = 0 print("Okay, I am finally convinced that you know the flag:") print(flag) if __name__ == "__main__": main() ``` ::: 這題的code非常簡潔,就是在實作[Schnorr protocol](https://cs.au.dk/~ivan/Sigma.pdf),把q丟到[factordb](https://factordb.com/) 沒辦法分解,所以這個離散對數問題應該很安全沒辦法解,另外步驟完全是照著Schnorr protocol所以也沒有問題 這時候敏銳的我們會注意到問題必定就是出現在```c = random.randrange(q)```了!眾所解知python 的random函式庫式使用名為MT19937的pseudo random number generator(PRNG),因此我們只要試著找出如何用randrange吐出的數字還原出原本的PRNG狀態即可預測之後的c,便可以答對這題的提問 但實作比想像中的麻煩呢:) 上網搜尋了一下關於之前有沒有過類似的題目,找到了2019年BalsnCTF的[題目](https://github.com/sasdf/ctf/tree/master/tasks/2019/BalsnCTF/crypto/unpredictable),可以知道randrange的實作方法如下 ```python def _randbelow_with_getrandbits(self, n): "Return a random int in the range [0,n). Raises ValueError if n==0." getrandbits = self.getrandbits k = n.bit_length() # don't use (n-1) here because n can be 1 r = getrandbits(k) # 0 <= r < 2**k while r >= n: r = getrandbits(k) return r ``` 這題的q為2049 bits,q/(1<<2049) ~= 0.701,表示每次使用randrange時會直接輸出當前getrandbits(2049)不重抽的機率約為0.7,我花了兩三個小時試圖看懂BalsnCTF那題writeup在幹嘛,發現實在是看不懂裡面的演算法QQ於是晚上回飯店後自己再想想 getrandbits(2049)在random中的作法會是一路從LSB往MSB的方向一直取getrandbits(32),直到有2049個bits(嗯對沒錯最後會有31個bits直接丟棄不用) 故getrandbits(2049)會需要65個getrandbits(32),由於第65個getrandbits(32)我們只能知道1 bit,資訊太少我就乾脆不用了。因此每次getrandbits(2049),我就取後面的2048bits,然後即可以得到連續的64個getrandbits(32)的值,還原MT19937的狀態會需要624個getrandbits(32)的值,所以我們就假設連續10次都成功的話,那我們便可以拿到640個getrandbits(32)那麼應該就可以成功了,算一下機率大概是0.7^10^ ~= 0.03,嗯還在可接受的範圍耶! 啊我就直接抄[這裡](https://github.com/icemonster/symbolic_mersenne_cracker/blob/main/main.py)的來解 結果發現沒有成功笑死嗚嗚嗚,但預測的數字會對一半,想了想會不會是條件不夠多因此沒有唯一解(z3的SAT solver只會吐出解出來的第一個解,不會給所有解),因此我就不斷上調得到的randrange(2049)的數量,最後在17個的時候成功,但相對的17次都是直接拿當前getrandbits(2049)而不重抽的機率為0.7^17^ ~= 0.0023,但沒關係我們時間很多可以慢慢等:)) 有耐心慢慢等不著急~ ![image](https://hackmd.io/_uploads/ryYgVGaF1g.png) 我們的目標是給s和t要滿足 $$ g^s \mod p \hspace{0.2cm} = \hspace{0.2cm} (t \cdot y^c) \mod p $$ 既然c可以預測了,我們就設 $$ s=1 \hspace{0.2cm}, \hspace{0.2cm} t = g \cdot \left( y^{-c} \right) \mod p $$ 以下為完整的payload :::spoiler solution ```python from z3 import * from random import Random from itertools import count from time import time import logging logging.basicConfig(format='STT> %(message)s') logger = logging.getLogger() logger.setLevel(logging.DEBUG) SYMBOLIC_COUNTER = count() p = 0x2CDE2997126F706BD27498A9FA07E93F321B4932982BC455910FF694160DB5484257D0886EC66E5D7BE59ECFD16AAFF6B5BD57E600FAB97E7CF75D76A7F12BC4619A036ED8787F4508CC7C1FB35689575E007B7DC6B1EECC4B9BC2E91AA31FBE027C62BFF3E2065912591ECC1C361CEEAE75B382F1BD7D967633FD91476A3ABC4AD22CD3372C3FC40C2841B3BC70DAC11E3A6631AB3BE49AB9F748AE9093FBAB15B5457244363F444D146C0ADE84CC1AB0D0CFB2AD329483E957235EDD0085BD2F5CDAFCD77D00622A9DFCD3C0098DCB42C7EF1DEE808E8216F0F0638F51D26614B0C61352A13565098FE60146FF7E46FAEBCB75629DAF517880E36AEEE617B9F q = (p - 1) // 2 class Untwister: def __init__(self): name = next(SYMBOLIC_COUNTER) self.MT = [BitVec(f'MT_{i}_{name}', 32) for i in range(624)] self.index = 0 self.solver = Solver() #This particular method was adapted from https://www.schutzwerk.com/en/43/posts/attacking_a_random_number_generator/ def symbolic_untamper(self, solver, y): name = next(SYMBOLIC_COUNTER) y1 = BitVec(f'y1_{name}', 32) y2 = BitVec(f'y2_{name}' , 32) y3 = BitVec(f'y3_{name}', 32) y4 = BitVec(f'y4_{name}', 32) equations = [ y2 == y1 ^ (LShR(y1, 11)), y3 == y2 ^ ((y2 << 7) & 0x9D2C5680), y4 == y3 ^ ((y3 << 15) & 0xEFC60000), y == y4 ^ (LShR(y4, 18)) ] solver.add(equations) return y1 def symbolic_twist(self, MT, n=624, upper_mask=0x80000000, lower_mask=0x7FFFFFFF, a=0x9908B0DF, m=397): ''' This method models MT19937 function as a Z3 program ''' MT = [i for i in MT] #Just a shallow copy of the state for i in range(n): x = (MT[i] & upper_mask) + (MT[(i+1) % n] & lower_mask) xA = LShR(x, 1) xB = If(x & 1 == 0, xA, xA ^ a) #Possible Z3 optimization here by declaring auxiliary symbolic variables MT[i] = MT[(i + m) % n] ^ xB return MT def get_symbolic(self, guess): name = next(SYMBOLIC_COUNTER) ERROR = 'Must pass a string like "?1100???1001000??0?100?10??10010" where ? represents an unknown bit' assert type(guess) == str, ERROR assert all(map(lambda x: x in '01?', guess)), ERROR assert len(guess) <= 32, "One 32-bit number at a time please" guess = guess.zfill(32) self.symbolic_guess = BitVec(f'symbolic_guess_{name}', 32) guess = guess[::-1] for i, bit in enumerate(guess): if bit != '?': self.solver.add(Extract(i, i, self.symbolic_guess) == bit) return self.symbolic_guess def submit(self, guess): ''' You need 624 numbers to completely clone the state. You can input less than that though and this will give you the best guess for the state ''' if self.index >= 624: name = next(SYMBOLIC_COUNTER) next_mt = self.symbolic_twist(self.MT) self.MT = [BitVec(f'MT_{i}_{name}', 32) for i in range(624)] for i in range(624): self.solver.add(self.MT[i] == next_mt[i]) self.index = 0 symbolic_guess = self.get_symbolic(guess) symbolic_guess = self.symbolic_untamper(self.solver, symbolic_guess) self.solver.add(self.MT[self.index] == symbolic_guess) self.index += 1 def get_random(self): ''' This will give you a random.Random() instance with the cloned state. ''' logger.debug('Solving...') start = time() self.solver.check() model = self.solver.model() end = time() logger.debug(f'Solved! (in {round(end-start,3)}s)') #Compute best guess for state state = list(map(lambda x: model[x].as_long(), self.MT)) result_state = (3, tuple(state+[self.index]), None) rr = Random() rr.setstate(result_state) return rr #以上只是用來還原MT19937的工具而已 from pwn import remote p = 0x2CDE2997126F706BD27498A9FA07E93F321B4932982BC455910FF694160DB5484257D0886EC66E5D7BE59ECFD16AAFF6B5BD57E600FAB97E7CF75D76A7F12BC4619A036ED8787F4508CC7C1FB35689575E007B7DC6B1EECC4B9BC2E91AA31FBE027C62BFF3E2065912591ECC1C361CEEAE75B382F1BD7D967633FD91476A3ABC4AD22CD3372C3FC40C2841B3BC70DAC11E3A6631AB3BE49AB9F748AE9093FBAB15B5457244363F444D146C0ADE84CC1AB0D0CFB2AD329483E957235EDD0085BD2F5CDAFCD77D00622A9DFCD3C0098DCB42C7EF1DEE808E8216F0F0638F51D26614B0C61352A13565098FE60146FF7E46FAEBCB75629DAF517880E36AEEE617B9F q = (p - 1) // 2 g = 2 r = remote('10.102.100.30', 11337) r.recvline() xx = r.recvline() print(xx) y = int(xx[3:].strip().decode()) print(y) record = [] def split_into_32bit_chunks(num): chunks = [] for _ in range(64): chunks.append(num & 0xFFFFFFFF) # 取最低 32 位 num >>= 32 # 右移 32 位,繼續取下一組 chunks.append('?'*32) return chunks def test(): record = [] for _ in range(17): r.sendlineafter(b't = ',str(1).encode()) c = int(r.recvline()[3:].strip().decode()) chunks = split_into_32bit_chunks(c) record.extend(chunks) r.sendlineafter(b's = ',str(1).encode()) r.recvline() assert len(record) == 65*17 return record for _ in range(10000): record = test() #print(record) ut = Untwister() for i in range(65*17): if i % 65 !=64: ut.submit(bin(record[i])[2:]) else: ut.submit(record[i]) try: r2 = ut.get_random() for _ in range(10): c = r2.randrange(q) s = 1 # g == t * y^c mod p tmp = pow(y,c,p) t = g * pow(tmp,-1,p) % p print(c,s,t) r.sendlineafter(b't = ',str(t).encode()) print(r.recvline()) r.sendlineafter(b's = ',str(s).encode()) print(r.recvline()) print(r.recvline().decode()) print(r.recvline().decode()) except: pass ``` ::: :::success AIS3{if_you_know_the_flag......?_you_know_the_flag!_btw,你願意和我玩一輩子ZKP嗎🥺} :::