HITCON CTF 2022 SuperPrime

继续复现HITCON CTF 的赛题。争取近期全部复现完。

源码

chall.py

from Crypto.Util.number import getPrime, isPrime, bytes_to_long

def getSuperPrime(nbits):
    while True:
        p = getPrime(nbits)
        pp = bytes_to_long(str(p).encode())
        if isPrime(pp):
            return p, pp


p1, q1 = getSuperPrime(512)
p2, q2 = getSuperPrime(512)
p3, q3 = getSuperPrime(512)
p4, q4 = getSuperPrime(512)
p5, q5 = getSuperPrime(512)

n1 = p1 * q1
n2 = p2 * p3
n3 = q2 * q3
n4 = p4 * q5
n5 = p5 * q4

e = 65537
c = bytes_to_long(open("flag.txt", "rb").read().strip())
for n in sorted([n1, n2, n3, n4, n5]):
    c = pow(c, e, n)

print(f"{n1 = }")
print(f"{n2 = }")
print(f"{n3 = }")
print(f"{n4 = }")
print(f"{n5 = }")
print(f"{e = }")
print(f"{c = }")

思路

首先可以令

\[f(x)=bytes\_to\_long(str(x).encode()) \]

通过long_to_bytes源码,知道这这个函数就是把参数的每一个字节转成16进制然后拼起来。假设

\[\begin{aligned} x &= a_0 + a_1*10 + a_2*10^2 + \cdots \\ &= \sum_{i=0}^na_i*10^i \end{aligned} \]

那么

\[\begin{aligned} f(x) &= (a_0 + 48) + (a_1 + 48)<<8 + (a_2 + 48)<<16 + \cdots\\ &=(a_0 + 48) + (a_1 + 48)*2^{8} + (a_2+48)*2^{16}+\cdots\\ &=\sum_{i=0}^{n}(a_i+48)*256^{i} \end{aligned} \]

根据题目我们可以把题目分解成三个部分

# part 1
n1 = p1 * q1
# part 2
n2 = p2 * p3
n3 = q2 * q3
# part 3
n4 = p4 * q5
n5 = p5 * q4

Part 1

很容易知道\(f(x)\)是单调递增的,就可以用二分法搜索,时间复杂度为\(O(logN)\),最多查找512次,这个时间复杂度是非常低的。

def binary_search(n):
    L, R = 0, 2**512
    while L <= R:
        middle = (L+R) // 2
        v = middle*f(middle)
        if v > n:
            R = middle
        elif v < n:
            L = middle
        else:
            return middle

最后确实找了512次,只用了一秒不到。

Part 2

n2, n3的表达式得到

\[\begin{aligned} n_2 &= p_2*p_3 \\ &=(a_{01}+a_{11}*10+a_{21}*10^2+\cdots+)*(a_{02}+a_{12}*10+a_{22}*10^2+\cdots+) \end{aligned} \]

\[\begin{aligned} n_3&=q_2*q_3 \\ &= [(a_{01}+48) + (a_{11}+48)*256 + (a_{21}+48)*256^2+\cdots+]*[(a_{02}+48)+(a_{12}+48)*256+(a_{22}+48)*256^2+\cdots+] \end{aligned} \]

可以推出

\[\begin{aligned} n_2 &\equiv a_{01}*a_{02} \pmod{10} \\ n_2 &\equiv (a_{01} + a_{11}*10)*(a_{02}+a_{12}*10) \pmod{10^2} \\ &\vdots \\ n_2 &\equiv (a_{01} + \cdots+a_{n1}*10^n)*(a_{02} + \cdots+a_{n2}*10^n) \pmod{10^{n+1}} \end{aligned} \]

\[\begin{aligned} n_3 &\equiv (a_{01}+48)*(a_{02}+48) \pmod{256}\\ &\vdots\\ n_3&\equiv[(a_{01}+48)+\cdots+(a_{n1}+48)*256^n]*[(a_{02}+48)+\cdots+(a_{n2}+48)*256^n] \pmod{256^{n+1}} \end{aligned} \]

欸?是不是感觉似曾相识。很像BabySSS这道题的处理,不过这里有两个未知变量,但是对于每一个模10和256的幂,有两个方程,而且\(a_{i1}\)\(a_{i2}\)的取值都在\([0, 9]\)
这个区间内,可以爆破。具体的方法使用到了Prune and Search搜索算法。

#Prune and Search
#official writeup from maple3142

def factor2(n1, n2):
    n1p = None

    def test_digits(ps, qs):
        nonlocal n1p
        if n1p is not None:
            return False
        p = sum([pi * 10**i for i, pi in enumerate(ps)])
        pp = sum([(48 + pi) * 256**i for i, pi in enumerate(ps)])
        q = sum([pi * 10**i for i, pi in enumerate(qs)])
        qq = sum([(48 + pi) * 256**i for i, pi in enumerate(qs)])
        if p != 0 and p != 1 and n1 % p == 0:
            n1p = p
            return False
        m1 = 10 ** len(ps)
        m2 = 256 ** len(qs)
        return (p * q) % m1 == n1 % m1 and (pp * qq) % m2 == n2 % m2

    def find_ij(ps, qs):
        for i in range(10):
            for j in range(10):
                if test_digits(ps + [i], qs + [j]):
                    yield i, j

    def search(ps, qs):
        for i, j in find_ij(ps, qs):
            search(ps + [i], qs + [j])

    search([], [])
    n2p = bytes_to_long(str(n1p).encode())
    assert n2 % n2p == 0
    return (n1p, n1 // n1p), (n2p, n2 // n2p)

Part 3

官方wp的解决方法是把\(n_4\)看成

\[\begin{aligned} n4&=p_4*f(p_5) \\ &=p_4*f(\frac{n_5}{f(p4)}) \end{aligned} \]

然后在\(p_4\)的长度不变的时候,\(n_4\)也是一个单调递增的,就可以用二分搜索分解。
offical writeup

def factor3(n1, n2):
    def try_factor(l, r): #二分搜索
        while l < r:
            m = (l + r) // 2
            if m > 1 and n1 % m == 0:
                return m
            if m * f(n2 // f(m)) < n1:
                l = m + 1
            else:
                r = m - 1

    for i in range(16):
        # brute force top 4 bits of p1
        # because len(str(p1)) must be constant to have monotonic property
        l = i << 508
        r = l + (1 << 508)
        if p1 := try_factor(l, r):
            return (p1, n1 // p1), (f(p1), n2 // f(p1))

最后所有的n都分解了,解5次rsa就可以了。

总结

这道题并没有考到很多密码学上的知识,而是一些非常有用的搜索算法,以及数学上的变换。通过这道题,学习了很多,包括Prune and Search还有对于时间复杂度的估算的应用。

posted @ 2023-02-12 17:28  tr0uble  阅读(160)  评论(0编辑  收藏  举报