N1CTF 2022

ezdlp

题目:

ezdlp.py
from Crypto.Util.number import *
from math import prod
from secret import flag

def keygen(pbits,kbits,k):
    p = getPrime(pbits)
    x = [getPrime(kbits + 1) for i in range(k)]
    y = prod(x)
    while 1:
        r = getPrime(pbits - kbits * k)
        q = 2 * y * r + 1
        if isPrime(q):
            return p*q, (p, q, r, x)

def encrypt(key, message):
    return pow(0x10001, message, key)

key = keygen(512, 24, 20)
flag = bytes_to_long(flag)
messages = [getPrime(flag.bit_length()) for i in range(47)] + [flag]
enc = [encrypt(key[0], message) for message in messages]

print(messages[:-1])
print(enc)

分析:
1.加密和困难性分析
题目的keygen是生成光滑素数p和q的函数,而encrypt是进行乘方模的函数,其中message作为指数;整体的加密流程很简单,反复随机生成若干个和flag的bit长度相同的数进行encrypt,但是没给n;一旦我们拿到n就可以通过p-1算法分解n拿到p,并且在模p下计算离散对数,即flag。所以难点有两个,一个是求n,另一个是p-1算法的时间复杂度较大,需要另辟蹊径。
2.利用格基规约恢复n
这一步可以参考maple大佬出的一个题,我这里还是来分析一下。
我们假设每一组已知的message为\(e_i\),输出的密文记为\(c_i\),我们去找一组系数\(a_i\),使得满足如下关系:

\[\large \prod_{i=0}^{n-1} c_i^{a_i}=m^{e_i a_i}=m^0=1\;mod\;n \]

为了找到这样一组系数,我们构造如下的格:

image
其实非常类似背包的构造,但是对第一列乘了一个较大的数字K,这是为了限制规约后的最短向量的第一维是0;关于这里我测试了不乘k,结果发现第一组最小向量的第一维是54,乘上k的话这个54会扩大很多倍,自然就不满足最短向量这一条件了,也就是排除非0的结果。当然即使不是0的最短向量系数也是能做的但是会麻烦一点,就不讨论了。
这里规约出来得到的系数就是\(a_i\)了,并且是有正有负的,接下来计算每一组\(c_i^{a_i}\)并把n组连续相乘起来,记为s,则\(s-1\equiv0\;mod\;n\),注意这里的s由于\(a_i\)有负数所以是个分数,那么记为\(\frac{x}{y}-1\;\equiv0\;mod\;n\)。通分以后得到\(x-y\;\equiv0\;mod\;n\),那么用两组x和y,计算\(gcd(x_0-y_0,x_1-y_1)\)就是n了。
3.根据keygen可知,其中的小素数最大的是r,bit为512-24.20=32,其余的都是25bit,那么改写p-1算法,即使只把所有的素数相乘,也有一个32bit的需要去遍历,这个时间复杂度是相当大的,用python写的话太慢了,需要用c实现或者利用现有的工具计算。github有一个大数分解的工具gmp-ecm,p-1算法的使用方法见项目readme:

image
在未知p-1的任何一个因子的情况下,我们需要找到一个上界B2,即所有因子都小于它,这里就是\(\small 2^{32}=4294967296\);次上界B1,只有一个因子大于它,这里是\(\small 2^{25}=33554432\),那么使用命令
echo 131158523227880830085100826212925738665356578827561846263073537503153187073136528966506785633847097997799377037969243883439723340886038624250936927221630287086602285835045356221763554989140952262353930420392663280482277832613695689454662506372252641564106136178637816827646124189347219273164844809807934422046441 | ecm -pm1 33554432 4294967296
能在30s以内分解成功。
拿到p和q以后在模p下求离散对数即可。
exp:

from Crypto.Util.number import *

print(2^32)
print(2^25)
with open('ezdlp.txt') as f:
    msgs = eval(f.readline())
    enc = eval(f.readline())
# T是转置,augment是把两个矩阵左右相连扩展,identity是单位矩阵
M = matrix(ZZ, msgs).T.augment(matrix.identity(len(msgs))) 
# 每一行的第0列乘上一个较大的k,其他列不变
M[:,0] *= 2^100
M = M.LLL()
print(M[0])
print(M[1])
aa = product([ZZ(x)^y for x,y in zip(enc, M[0][1:])])
bb = product([ZZ(x)^y for x,y in zip(enc, M[1][1:])])
# numer是分子 denom是分母
n = gcd(aa.numer() -  aa.denom(), bb.numer() - bb.denom())
print(n)

n = 131158523227880830085100826212925738665356578827561846263073537503153187073136528966506785633847097997799377037969243883439723340886038624250936927221630287086602285835045356221763554989140952262353930420392663280482277832613695689454662506372252641564106136178637816827646124189347219273164844809807934422046441
q = 12980311456459934558628309999285260982188754011593109633858685687007370476504059552729490523256867881534711749584157463076269599380216374688443704196597025947
p = n // q

m = GF(q)(enc[-1]).log(0x10001)
flag = long_to_bytes(int(m))
print(flag)

brand_new_checkin

题目:

brand_new_checkin.py
from Crypto.Util.number import *
from random import getrandbits
from secret import flag

def keygen():
    p = getPrime(512)
    q = getPrime(512)

    n = p * q
    phi = (p-1)*(q-1)

    while True:
        a = getrandbits(1024)
        b = phi + 1 - a

        s = getrandbits(1024)
        t = -s*a * inverse(b, phi) % phi

        if GCD(b, phi) == 1:
            break
    return (s, t, n), (a, b, n)


def enc(m, k):
    s, t, n = k
    r = getrandbits(1024)

    return m * pow(r, s, n) % n, m * pow(r, t, n) % n


pubkey, privkey = keygen()

flag = pow(bytes_to_long(flag), 0x10001, pubkey[2])

c = []
for m in long_to_bytes(flag):
    c1, c2 = enc(m, pubkey)
    c.append((c1, c2))

print(pubkey)
print(c)

分析:
1.加密和困难性分析
题目有两个难点,1是利用MT19937还原a,而是通过a和其他已知信息推导phi求d解密。
2.关于MT19937
这里的enc函数m * pow(r, s, n) % n, m * pow(r, t, n) % n实际上存在共模攻击漏洞,而m是flag的rsa密文的每一个字节,这里可以通过遍历1-255来确认正确的m。然后可以恢复pow(r, s, n)pow(r, t, n),利用共模攻击求\(r\;mod\;n\),实际上r的比特是小于1024,由于n为1023bit长,所以可能大于n,这里就需要爆破一下,可以找到距离\(2^{1024}\)最近的20个连续的\(r\;mod\;n\)进行爆破以缩小时间复杂度。爆破恢复随机数序列以后可以预测几个数来判断是否正确。然后反推a就可以了。
2.关于求phi
已知\(a+b \equiv 1\;mod\;\phi\;\;as+bt\equiv0\;mod\;\phi\)
那么联立解就能得到\(at-as\equiv \; t\;mod\; \phi\),用k.phi求d即可,脚本又长又丑,就不放了。

babyecc

题目:

babyecc.sage
from Crypto.Util.number import *
from secret import flag

m = Integer(int.from_bytes(flag, 'big'))

for _ in range(7):
    p = getPrime(512)
    q = getPrime(512)
    n = p * q
    while 1:
        try:
            a = randint(0,n)
            b = randint(0,n)
            Ep = EllipticCurve(GF(p), [a,b])
            Gp = Ep.lift_x(m) * 2
            Eq = EllipticCurve(GF(q), [a,b])
            Gq = Eq.lift_x(m) * 2
            y = crt([int(Gp[1]),int(Gq[1])],[p,q])
            break
        except Exception as err:
            pass
    print(n, a, b, y)

分析:
因为是赛后复现,做法基本上参考的NeSE战队的思路。
1.加密和困难性分析
题目给了七轮的加密,每次随机选取大素数p q,产生两条椭圆曲线Ep和Eq,然后在两条曲线上各取一点,计算它们的倍点,利用crt计算倍点的y值,这里相当于生成了一条新的mod n的椭圆曲线。这种类型的题目基本都是要用椭圆曲线的坐标计算公式来建立方程,这题特殊的地方在于用coppersmith求解的时候需要用crt组一下。
2.首先明确一下倍点公式,也就是:

\[s = \frac{3x_p^2 + a}{2y_p} \]

\[x_r=s^2 - 2x_p \quad y_r=y_p + s(x_r-x_p) \qquad(1) \]

由(1)得\(x_r=(\frac{3m^2 +a}{2y_p})^2 -2m\rightarrow \frac{(3m^2+a)^2}{4(m^3+am+b)}-2m \qquad (2)\)
再联立ecc方程(3)

\[ ecc:y^2 = x^3 +ax+b\qquad(3) \]

可以设\(k = 4(m^3+am+b),c=(3m^2+a)\),带入计算得:

\[y_r^2=(\frac{c}{k} -2m)^3+a(\frac{c}{k} -2m)+b\rightarrow k^3y_r^2-(c-2mk)^3-a(ck^2-2mk^3)-bk^3=0 \]

3.由2我们已经建立了模n下的方程式,接下来就是求解的问题。
其实这就是一个广播攻击而已,很多论文中都有,就是利用crt将模数N的比特位提高,然后使其满足coppersmith的界。
image
增大N,copper的界\(N^{\frac{1}{d}-\epsilon}\)也随之增大了,为此我们在求解的时候还应该尽量减小\(\epsilon\)的值,进一步扩大界。进行估算以验证copper的有效性,维数是12,crt以后的N是7 * 1024bit,\(N^{\frac{1}{12}}\)大约600bit,flag格式是uuid所以比特大概为300-400,所以可行。后面就是写exp了。
exp:

from Crypto.Util.number import *

lines = open("babyecc.txt","r").readlines()
fs = []
ns = []

def Function(n,a,b,y):
    P.<m> = PolynomialRing(Zmod(n))
    k = 4*(m^3+a*m+b)
    c = (3*m^2+a)^2
    f = k^3*y^2 - (c-2*m*k)^3 - a*(k^2*c-2*m*k^3) - b*k^3
    return f

for line in lines:
    n,a,b,y = [ZZ(i) for i in line.strip().split(" ")]
    f = Function(n,a,b,y).monic().change_ring(ZZ)
    fs.append(f)
    ns.append(n)

F = crt(fs,ns)
N = prod(ns)
FF = F.change_ring(Zmod(N))
roots = FF.small_roots(epsilon = 0.03)
print(roots)
print(long_to_bytes(int(roots[0])))
# n1ctf{7140f171-5fb5-484d-92f4-9f7ba02c33d0}
posted @ 2022-11-09 14:10  ZimaB1ue  阅读(659)  评论(1编辑  收藏  举报