---
# System prepended metadata

title: AIS3 EOF 2025 - B33F S50P  crypto writeup

---

# 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嗎🥺}
:::