网鼎杯-Qualifier-2022/青龙组/Crypto/crypto405 writeup

1. challenge

下载附件得到源码和密文

from Crypto.Util.number import *
from random import randrange
# from grassfield import flag
flag=b"ffffffff"
p = getPrime(16)
print(p)
k = [randrange(1,p) for i in range(5)]
print(k)
for i in range(len(flag)):
    grasshopper = flag[i]
    for j in range(5):
        k[j] = grasshopper = grasshopper * k[j] % p
    print('Grasshopper#'+str(i).zfill(2)+':'+hex(grasshopper)[2:].zfill(4))
点击查看密文

Grasshopper#00:2066 Grasshopper#01:a222 Grasshopper#02:cbb1 Grasshopper#03:dbb4 Grasshopper#04:deb4 Grasshopper#05:b1c5 Grasshopper#06:33a4 Grasshopper#07:c051 Grasshopper#08:3b79 Grasshopper#09:6bf8 Grasshopper#10:2131 Grasshopper#11:2c40 Grasshopper#12:91ba Grasshopper#13:7b44 Grasshopper#14:5f25 Grasshopper#15:0208 Grasshopper#16:7edb Grasshopper#17:62b5 Grasshopper#18:cec5 Grasshopper#19:5ab3 Grasshopper#20:3c46 Grasshopper#21:c272 Grasshopper#22:714b Grasshopper#23:9e0b Grasshopper#24:48ee Grasshopper#25:44cc Grasshopper#26:05a0 Grasshopper#27:3da3 Grasshopper#28:11b1 Grasshopper#29:259f Grasshopper#30:899d Grasshopper#31:a130 Grasshopper#32:e58f Grasshopper#33:23f3 Grasshopper#34:5829 Grasshopper#35:6beb Grasshopper#36:3681 Grasshopper#37:0054 Grasshopper#38:a189 Grasshopper#39:2765 Grasshopper#40:c63d Grasshopper#41:bc68

分析

只要能看懂题目,就不难解题。关键在于每一轮的k都会变化,加密过程转换成数学公式为:

$$ \begin{align} & k_i^0 = m_i*k_{i-1}^0 \notag \newline & k_i^j = k_{i-1}^j*k_i^j \notag \newline & c_i = k_i^4 \notag \end{align} $$

方法一

阅读代码可知,每一轮的k,都与上一轮的k相关。又已知flag格式为flag{*},即已知明文前五位,可以列出5个模p的方程式:

from Crypto.Util.number import *

def generate_equations(m, k):
    k = k.copy()
    equations = []
    for i in range(5):
        grasshopper = m[i]
        for j in range(5):
            k[j] = grasshopper = grasshopper * k[j]
        equations.append(grasshopper)
    return equations

def show_equations(equations, c):
    for i in range(len(equations)):
        show(equations[i]-c[i])
        
p = 2^16-1
p = previous_prime(p)
R.<k0,k1,k2,k3,k4,m0,m1,m2,m3,m4,c0,c1,c2,c3,c4> = GF(p)[]

m = [m0,m1,m2,m3,m4]
k = [k0,k1,k2,k3,k4]
c = [c0,c1,c2,c3,c4]

equations = generate_equations(m, k)
show_equations(equations, c)

$$ \begin{align} & k_{0} k_{1} k_{2} k_{3} k_{4} m_{0} - c_{0} \notag \newline & k_{0}^{5} k_{1}^{4} k_{2}^{3} k_{3}^{2} k_{4} m_{0}^{5} m_{1} - c_{1} \notag \newline & k_{0}^{15} k_{1}^{10} k_{2}^{6} k_{3}^{3} k_{4} m_{0}^{15} m_{1}^{5} m_{2} - c_{2} \notag \newline & k_{0}^{35} k_{1}^{20} k_{2}^{10} k_{3}^{4} k_{4} m_{0}^{35} m_{1}^{15} m_{2}^{5} m_{3} - c_{3} \notag \newline & k_{0}^{70} k_{1}^{35} k_{2}^{15} k_{3}^{5} k_{4} m_{0}^{70} m_{1}^{35} m_{2}^{15} m_{3}^{5} m_{4} - c_{4} \notag \end{align} $$

5个未知数,5个同余方程,可解。但我不会自动解,考虑手动消元。

观察发现$k_4$的幂都是1, 每个方程式除以前式,可得到关于$k_0,k_1,k_2,k_3$的4个同余方程式:

def temp_k4_elimination(equations, c):
    equations = [equations[i+1]/equations[i] for i in range(4)]
    c = [c[i+1]/c[i] for i in range(4)]
    show_equations(equations, c) 

temp_k4_elimination(equations, c)

$$ \begin{align} & \frac{k_{0}^{4} k_{1}^{3} k_{2}^{2} k_{3} m_{0}^{4} m_{1} c_{0} - c_{1}}{c_{0}} \notag \newline & \frac{k_{0}^{10} k_{1}^{6} k_{2}^{3} k_{3} m_{0}^{10} m_{1}^{4} m_{2} c_{1} - c_{2}}{c_{1}} \notag \newline & \frac{k_{0}^{20} k_{1}^{10} k_{2}^{4} k_{3} m_{0}^{20} m_{1}^{10} m_{2}^{4} m_{3} c_{2} - c_{3}}{c_{2}} \notag \newline & \frac{k_{0}^{35} k_{1}^{15} k_{2}^{5} k_{3} m_{0}^{35} m_{1}^{20} m_{2}^{10} m_{3}^{4} m_{4} c_{3} - c_{4}}{c_{3}} \notag \end{align} $$

观察发现$k_3$的幂都是1, 同理,重复上述动作,可逐个消元,将方程组化简。

def simplify_equations(equations, c):
    c = c.copy()
    simplified_equations = [equations[0]-c[0]]
    for _ in range(4):
        equations =[equations[i+1]/equations[i] for i in range(len(equations)-1)]
        c = [c[i+1]/c[i] for i in range(len(c)-1)]
        simplified_equations.append(equations[0]-c[0])
    for i in range(len(simplified_equations)):
        show(simplified_equations[i])
    return simplified_equations

simplified_equations = simplify_equations(equations, c)

$$ \begin{align} & k_{0} k_{1} k_{2} k_{3} k_{4} m_{0} - c_{0} \notag \newline & \frac{k_{0}^{4} k_{1}^{3} k_{2}^{2} k_{3} m_{0}^{4} m_{1} c_{0} - c_{1}}{c_{0}} \notag \newline & \frac{k_{0}^{6} k_{1}^{3} k_{2} m_{0}^{6} m_{1}^{3} m_{2} c_{1}^{2} - c_{0} c_{2}}{c_{1}^{2}} \notag \newline & \frac{k_{0}^{4} k_{1} m_{0}^{4} m_{1}^{3} m_{2}^{2} m_{3} c_{0} c_{2}^{3} - c_{1}^{3} c_{3}}{c_{0} c_{2}^{3}} \notag \newline & \frac{k_{0} m_{0} m_{1} m_{2} m_{3} m_{4} c_{1}^{4} c_{3}^{4} - c_{0} c_{2}^{6} c_{4}}{c_{1}^{4} c_{3}^{4}} \notag \end{align} $$

def solve_equations(equations, kwargs, k):
    k = k.copy()
    for i in range(5):
        equation = equations[5-i-1]
        for j in range(i+1):
            kwargs[f'k{j}'] = k[j]
        k[i] = int(equation(**kwargs).univariate_polynomial().roots()[0][0])
#         print(f"k[{i}]=", k[i])
    return k

output = [0x2066,0xa222,0xcbb1,0xdbb4,0xdeb4,0xb1c5,0x33a4,0xc051,0x3b79,0x6bf8,0x2131,0x2c40,0x91ba,0x7b44,0x5f25,0x0208,0x7edb,0x62b5,0xcec5,0x5ab3,0x3c46,0xc272,0x714b,0x9e0b,0x48ee,0x44cc,0x05a0,0x3da3,0x11b1,0x259f,0x899d,0xa130,0xe58f,0x23f3,0x5829,0x6beb,0x3681,0x0054,0xa189,0x2765,0xc63d,0xbc68,]
known_plain = 'flag{'
known_m = [ord(i) for i in known_plain]
kwargs = {}
for i in range(5):
    kwargs[f'm{i}'] = known_m[i]
    kwargs[f'c{i}'] = output[i]
    kwargs[f'k{i}'] = 1

k_init = solve_equations(simplified_equations, kwargs, k)

求出k,则可解出明文,$m_ik_0k_1k_2k_3k_4\equiv c_i\mod p \Rightarrow m_i\equiv c_i*{k_0k_1k_2k_3k_4}^{-1}\mod p$

完整exp如下:

from tqdm import tqdm

def generate_equations(m, k):
    k = k.copy()
    equations = []
    for i in range(5):
        grasshopper = m[i]
        for j in range(5):
            k[j] = grasshopper = grasshopper * k[j]
        equations.append(grasshopper)
    return equations

def simplify_equations(equations, c):
    c = c.copy()
    simplified_equations = [equations[0]-c[0]]
    for _ in range(4):
        equations =[equations[i+1]/equations[i] for i in range(len(equations)-1)]
        c = [c[i+1]/c[i] for i in range(len(c)-1)]
        simplified_equations.append(equations[0]-c[0])
    return simplified_equations

def solve_equations(equations, kwargs, k):
    k = k.copy()
    for i in range(5):
        equation = equations[5-i-1]
        for j in range(i+1):
            kwargs[f'k{j}'] = k[j]
        k[i] = int(equation(**kwargs).univariate_polynomial().roots()[0][0])
#         print(f"k[{i}]=", k[i])
    return k

def update_k(m, k):
    grasshopper = m
    for j in range(5):
        k[j] = grasshopper = grasshopper * k[j] %p
    return k

def decrypt(k):
    m = known_m.copy()
    for i in range(5, len(output)):
        mi = output[i]*inverse_mod(k[0]*k[1]*k[2]*k[3]*k[4], p)%p
        if mi > 128:
            break
        m.append(mi)
        k = update_k(mi, k)
    return m

known_plain = 'flag{'
known_m = [ord(i) for i in known_plain]
output = [0x2066,0xa222,0xcbb1,0xdbb4,0xdeb4,0xb1c5,0x33a4,0xc051,0x3b79,0x6bf8,0x2131,0x2c40,0x91ba,0x7b44,0x5f25,0x0208,0x7edb,0x62b5,0xcec5,0x5ab3,0x3c46,0xc272,0x714b,0x9e0b,0x48ee,0x44cc,0x05a0,0x3da3,0x11b1,0x259f,0x899d,0xa130,0xe58f,0x23f3,0x5829,0x6beb,0x3681,0x0054,0xa189,0x2765,0xc63d,0xbc68,]
c = output[:5]

p = 2^16-1
pbar = tqdm(total=int(prime_pi(2^16)-prime_pi(2^15)))
while p.nbits()<=16:
    pbar.update()
    p = previous_prime(p)
    R.<k0,k1,k2,k3,k4,m0,m1,m2,m3,m4,c0,c1,c2,c3,c4> = GF(p)[]
    k = [k0,k1,k2,k3,k4]

    equations = generate_equations(known_m, k)
    simplified_equations = simplify_equations(equations, c)
    
    kwargs = {}
    for i in range(5):
        kwargs[f'k{i}'] = 1

    k_init = solve_equations(simplified_equations, kwargs, k)    
    k_iterate = k_init.copy()
    for i in known_m:
        k_iterate = update_k(i, k_iterate)

    m = decrypt(k_iterate)
    if len(m)==42:
        flag = "".join([chr(i) for i in m])
        print(flag)
        print("k:", k_init)
        print("p:", p)
        break

pbar.close()
 18% 534/3030 [00:02<00:10, 231.04it/s]

flag{749d39d4-78db-4c55-b4ff-bca873d0f18e}
k: [28358, 39970, 28105, 42009, 46211]
p: 59441

方法二

for i in range(len(flag)):
    grasshopper = flag[i]
    for j in range(5):
        k[j] = grasshopper = grasshopper * k[j] % p

根据代码知道,每一轮的密文,就是每一轮的$k_4$。每一轮的$k_4$为该轮的$k_3$乘以上一轮的$k_4$。

即,记第i轮的$k_j$为$k^j_i$,密文为$c_i$,有:

$$ \begin{align} & c_i = k_i^4 \notag \newline & k_i^j \equiv k_i^{j-1} k_{i-1}^j \mod p \notag \newline \Rightarrow & k_i^{j-1}\equiv k_i^j*{k_{i-1}^j}^{-1} \mod p \notag \end{align} \notag $$

已知$k_i^4$,则可以求得$k_i^3$,同理可求得$k_i^0$。

另有: $$ \begin{align} \notag & k_i^0\equiv k_{i-1}^0*m_i\mod p \notag \newline \Rightarrow & m_i \equiv k_i^0*{k_{i-1}^0}^{-1}\mod p \notag \end{align} $$

因为p是16位的素数,已知flag格式为flag{*},则可以爆破p,解密出明文,根据明文格式判断是否为正确的flag。

from tqdm import tqdm
output = [0x2066,0xa222,0xcbb1,0xdbb4,0xdeb4,0xb1c5,0x33a4,0xc051,0x3b79,0x6bf8,0x2131,0x2c40,0x91ba,0x7b44,0x5f25,0x0208,0x7edb,0x62b5,0xcec5,0x5ab3,0x3c46,0xc272,0x714b,0x9e0b,0x48ee,0x44cc,0x05a0,0x3da3,0x11b1,0x259f,0x899d,0xa130,0xe58f,0x23f3,0x5829,0x6beb,0x3681,0x0054,0xa189,0x2765,0xc63d,0xbc68]

p = 2^15-1
pbar = tqdm(total=int(prime_pi(2^16)-prime_pi(2^15)))

while p.nbits()<=16:
    pbar.update()
    p = next_prime(p)
    k = output.copy()
    for _ in range(5):
        if p in k:
            break
        k = [k[i]*inverse_mod(k[i-1], p)%p for i in range(1, len(k))]

    if k[-1] == ord('}'):
        flag = "flag{"+"".join([chr(i) for i in k])
        print(flag)
        break
pbar.close()
 77% 2322/3030 [00:00<00:00, 3877.36it/s]

flag{749d39d4-78db-4c55-b4ff-bca873d0f18e}