SUSCTF 2022 Crypto Writeup

发布于 2022-04-02  111 次阅读






SUSCTF crypto

SUSCTF-Writeup(Crypto)

large case

这题没有提供e,给了p、q、r,并且条件里说了e由三个素因子组成,所以不难想到分解p-1,q-1,r-1,从而对e的可能值进行组合。不过就算组合过了,也不能用常规方法解题,因为本题e phi不互素,所以考虑对其开根,又考虑到这题的e会相对较大,所以用AMM算法对其开根。AMM算法详见 Intended Solution to Crypto Problems in NCTF 2019 | Soreat_u's Blog (soreatu.com)

在这之前可以对e的因子进行猜测,由于需要使用到CRT组合,所以因子不会太大,并且因子不会是共有的因数,于是可以猜测e使用p-1中的757,q-1中的66553,r-1中的5156273(如果不对,再进行调整,可供调整的选择不多,一些小的因数比如3、7,可以直接跑,很快就可以知道不满足)。

这里e值的选择,在看了NepNep师傅的wp之后学到了一手k次剩余的判断方法(这波是信安数基学的不扎实) 。由RSA加密,c是模p的e次剩余,那么c必然也是模p的ep次剩余(因为ep是e的因⼦)。同理c是模q 的eq次剩余,c是模r的er次剩余。⽤这个条件筛⼀下,得到 。然后我们知道因子不能是公有的因数,所以e的值只有一种可能。

def is_kth_residue(a, k, p):
	k = k % (p-1)
	a = a % p
	return True if pow(a, (p-1)//k, p) == 1 else False

for i in range(len(p_minus_1_factor)):
	if is_kth_residue(c, p_minus_1_factor[i], p):
		print(f"e_p: {p_minus_1_factor[i]}")
for i in range(len(q_minus_1_factor)):
	if is_kth_residue(c, q_minus_1_factor[i], q):
		print(f"e_q: {q_minus_1_factor[i]}")
for i in range(len(r_minus_1_factor)):
	if is_kth_residue(c, r_minus_1_factor[i], r):
		print(f"e_q: {r_minus_1_factor[i]}")

可以注意到flag的长度在1025-2048比特之间,所以我们不需要考虑r的部分,只考虑p、q。有了猜测的e,我们就可以把多于2048的pad去掉,留下一部分\x00

之后分别计算模p和模q的情况

对上面的式子使用AMM算法,就可以得到mp、mq的列表,然后使用CRT对其组合,用SUSCTF校验即可,整个过程大约十几分钟。

from Crypto.Util.number import *
import time
import random
c = 2832775557487418816663494645849097066925967799754895979829784499040437385450603537732862576495758207240632734290947928291961063611897822688909447511260639429367768479378599532712621774918733304857247099714044615691877995534173849302353620399896455615474093581673774297730056975663792651743809514320379189748228186812362112753688073161375690508818356712739795492736743994105438575736577194329751372142329306630950863097761601196849158280502041616545429586870751042908365507050717385205371671658706357669408813112610215766159761927196639404951251535622349916877296956767883165696947955379829079278948514755758174884809479690995427980775293393456403529481055942899970158049070109142310832516606657100119207595631431023336544432679282722485978175459551109374822024850128128796213791820270973849303929674648894135672365776376696816104314090776423931007123128977218361110636927878232444348690591774581974226318856099862175526133892
p = 127846753573603084140032502367311687577517286192893830888210505400863747960458410091624928485398237221748639465569360357083610343901195273740653100259873512668015324620239720302434418836556626441491996755736644886234427063508445212117628827393696641594389475794455769831224080974098671804484986257952189021223
q = 145855456487495382044171198958191111759614682359121667762539436558951453420409098978730659224765186993202647878416602503196995715156477020462357271957894750950465766809623184979464111968346235929375202282811814079958258215558862385475337911665725569669510022344713444067774094112542265293776098223712339100693
r = 165967627827619421909025667485886197280531070386062799707570138462960892786375448755168117226002965841166040777799690060003514218907279202146293715568618421507166624010447447835500614000601643150187327886055136468260391127675012777934049855029499330117864969171026445847229725440665179150874362143944727374907
# fp = [2, 7, 757, 1709, 85015583 , 339028665499, 149105250954771885483776047]
# fq = [2, 3, 66553,81768440203, 84405986771, 38037107558208320033, 16137718604846030589135490851713]
# fr = [2, 5156273, 10012111, 11607389, 68872137169799749, 9691125310820433463]
# e_list = []
# for i in fp:
#     for j in fq:
#         for k in fr:
#             e_list.append(i*j*k)
# print(e)
e = 757 * 66553 *5156273
def AMM(o, r, q):
    start = time.time()
    print('\n----------------------------------------------------------------------------------')
    print('Start to run Adleman-Manders-Miller Root Extraction Method')
    print('Try to find one {:#x}th root of {} modulo {}'.format(r, o, q))
    g = GF(q)
    o = g(o)
    p = g(random.randint(1, q))
    while p ^ ((q-1) // r) == 1:
        p = g(random.randint(1, q))
    t = 0
    s = q - 1
    while s % r == 0:
        t += 1
        s = s // r
    k = 1
    while (k * s + 1) % r != 0:
        k += 1
    alp = (k * s + 1) // r
    a = p ^ (r**(t-1) * s)
    b = o ^ (r*alp - 1)
    c = p ^ s
    h = 1
    for i in range(1, t):
        d = b ^ (r^(t-1-i))
        if d == 1:
            j = 0
        else:
            j = - discrete_log(d, a)
        b = b * (c^r)^j
        h = h * c^j
        c = c^r
    result = o^alp * h
    end = time.time()
    print("Finished in {} seconds.".format(end - start))
    return result

def findAllPRoot(p, e):
    start = time.time()
    proot = set()
    while len(proot) < e:
        proot.add(pow(random.randint(2, p-1), (p-1)//e, p))
    end = time.time()
    return proot
def findAllSolutions(mp, proot, cp, p):
    print("Start to find all the {:#x}th root of {} modulo {}.".format(e, cp, p))
    start = time.time()
    all_mp = set()
    for root in proot:
        mp2 = mp * root % p
        all_mp.add(mp2)
    end = time.time()
    print("Finished in {} seconds.".format(end - start))
    return all_mp
n=p*q*r
# tmp = pow(int(1<<1024),int(e),n)
tmp = 2794203162952680694875426764547234241112236710433123774476303768639242351566319207612773628883250609134659396011750795113351981408956541443986997306105869563120160376511155383832592237253107342927670770094240045412123787825031261131955433947396826167219004667304636335427373984885928591487526163086731412269053200262557436796244981351739716063496342017497841802350338687039580076905949641402173299773716271511639533823663740291331408514189072329517625171120136705677613965360814741037067953490597466636290290725085921897578223035738383932219334876192857916281288629471625406358741715112812225419217748429901501082216480552255233899368763011103720695984389763743356559299690991047172680728215625377751497287365075742929734077041112027582350488222091280835389084342367259161290929159777553525296385682720359499417021854388648183468514374707670903349123145819166174953312125050648613697527164777642887701658900202492759171557250
realc=inverse_mod(tmp,n)*c%n
er=5156273
der = inverse_mod(er,(p-1)*(q-1))
c = pow(int(realc), der,p*q)
dp = inverse_mod(66553, p-1)
dq = inverse_mod(757, q-1)
cp = pow(int(c),int(dp),int(p))
cq = pow(int(c),int(dq),int(q))
mp = AMM(cp, 757, p)
mq = AMM(cq, 66553, q)
p_proot = findAllPRoot(p, 757)
q_proot = findAllPRoot(q, 66553)
mps = findAllSolutions(mp, p_proot, cp, p)
mqs = findAllSolutions(mq, q_proot, cq, q)

start = time.time()
print('Start CRT...')
for mpp in mps:
    print(mpp)
    for mqq in mqs:
        solution = crt([int(mpp), int(mqq)], [p, q])
        if b'SUSCTF' in long_to_bytes(solution):
            print(long_to_bytes(solution))
            end = time.time()
            print("Finished in {} seconds.".format(end - start))
#For RSA, the wrong key generation method can also reveal information. You recover my secret message, and here is the flag:
#SUSCTF{N0n_c0prime_RSA_c1pher_cAn_a1s0_recover_me33age!!!}

InverseProblem

题目名字是反问题,一开始被名字带偏了,还去找了矩阵反问题,找到了病态矩阵求解的一些知识,用一些相关正规化方法也调不出结果。后来想了想,这题就因为python浮点数的精度是52位,所以方程组很敏感,给它每个数都扩大的话,敏感的那部分误差就相当于error,那就直接用LWE或者CVP去解就行了。
这里就通过Embedded Technique的构造,把CVP转化成SVP

然后规约之后的第一行就是近似解,所以只要解近似解的方程就可以了。

exp如下

import numpy as np
f=open('b.txt','r')
b=[]
for line in f:
    tmp = line[:-5].replace('.','')
    b.append(int(tmp))

def gravity(n,d=0.25):
    A=np.zeros([n,n])
    for i in range(n):
        for j in range(n):
            A[i,j]=d/n*(d**2+((i-j)/n)**2)**(-1.5)
    return A

A = gravity(85)
A = [[int(j*10^18) for j in i] for i in A]

M = []
for i in range(85):
    M.append(A[i] + [0])

M.append(b + [1])
M = Matrix(ZZ, M)
ans = M.LLL()[0]
flag=M.solve_left(ans)
print(bytes(flag[:-1]))

Ez_Pager_Tiper

这道题是一个lfsr的题目,当时比赛的时候没注意到给了data这个文件夹,当时想了好久怎么通过最后的结果逆回去,然后就做不出来了。赛后看师傅们的wp才发现有这么个文件夹,里面的文件名base64过了,而且每个文件开头都是类似的格式。因此我们只需要通过解加密文件名,获得文件开头的明文,格式为 Date: yyyy-mm-dd。

再对magic_box里的函数进行审计,其中第一部分是常见的LFSR,第二部分中confusion 经过测试可以知道,当mask中有奇数个1,输出LFSR2;当mask中有偶数个1,输出LFSR1^LFSR2

>>> def malicious_magic(magic):
    now = (-magic & magic)
    magic ^= now
    return int(now), int(magic)

>>> def confusion(magicy,c1, c2):
    magic = magicy
    output, cnt = magic, 0
    output ^= c1 ^ c2
    while magic:
        now, magic = malicious_magic(magic)
        cnt ^= now >> (now.bit_length() - 1)
        output ^= now
    output ^= cnt * c1
    return int(output)

>>> print(confusion(0b1001,3,4))
7
>>> print(confusion(0b10001,3,4))
7
>>> print(confusion(0b1000,3,4))
4
>>> print(confusion(0b10000,3,4))
4
>>> print(confusion(0b100010,3,4))
7
>>> print(confusion(0b100000010,3,4))
7
>>> print(confusion(0b100000010,3,4))
7
>>> print(confusion(0b1000110,3,4))
4
>>> print(confusion(0b101110,3,4))
7

所以对于problem1,可以知道他只使用了lfsr2。我们就可以用已知的明密文对进行异或,得到输出。然后用矩阵求出对应mask。

from Crypto.Util.number import *
from Crypto.Util.strxor import *

n1, n2 = 64, 12
plain1 = b'Date: 1984-04-01'
ciper1 = open('MTk4NC0wNC0wMQ==_6d30.enc', 'rb').read()
output1 = strxor(plain1, ciper1[:len(plain1)])
output1 = bytes_to_long(output1)
s = [int(x) for x in bin(output1)[2:]]

M = matrix(GF(2), 12, 12)
T = vector(GF(2), 12)
for i in range(12):
        M[i] = s[i : i + 12]
        T[i] = s[i+12]
mask = M.inverse() * T
print(mask[::-1])
#(1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1)
#mask2 =0b101000000001

有了mask2,我们就可以对problem2进行求解。通过运算可以知道这里的magic有偶数个1,所以输出是LFSR1^LFSR2。考虑对LFSR2的12bit的seed3爆破,然后通过已知LFSR2的输出以及开头明文,得到LFSR1的输出,之后求出mask1和seed1,就可以顺利求解。

from Crypto.Util.number import *
from Crypto.Util.strxor import *
class lfsr():
    def __init__(self, seed, mask, length):
        self.length_mask = 2 ** length - 1
        self.mask = mask & self.length_mask
        self.state = seed & self.length_mask

    def next(self):
        next_state = (self.state << 1) & self.length_mask
        i = self.state & self.mask & self.length_mask
        output = 0
        while i != 0:
            output ^= (i & 1)
            i = i >> 1
        next_state ^= output
        self.state = next_state
        return output

    def getrandbit(self, nbit):
        output = 0
        for _ in range(nbit):
            output = (output << 1) ^ self.next()
        return output
    
    def recover(self, output):
        lastbit = output & 1
        output >>= 1
        i = (output & self.mask) & self.length_mask
        while i != 0:
            lastbit ^= (i & 1)
            i >>= 1
        seed = (lastbit << (self.length_mask.bit_length()-1)) | output
        return seed
mask2=0b101000000001
plain2 = b'Date: 1984-12-25'
n1, n2 = 64, 12
x = (1<<12)-1

cipher2 = open('MTk4NC0xMi0yNQ==_76ff.enc', 'rb').read()
unknown_c = cipher2[16:]
output2 = strxor(plain2, cipher2[:16])
for seed3 in range(x):
    lfsr2 = lfsr(seed3, mask2, n2)
    outlfsr2 = long_to_bytes(lfsr2.getrandbit(128)).rjust(16,b"\x00")
    outlfsr1 = strxor(output2,outlfsr2)
    m = bin(bytes_to_long(outlfsr1))[2:].zfill(128)
    A = matrix(GF(2),n1,n1)    
    B = vector(GF(2), 64)
    for i in range(n1):
        A[i]=m[i:i+n1]
        B[i]=m[i+n1]
        if det(A)!=0 and det(B)!=0:
            mask1=int("".join([str(int(o)) for o in (A.inverse()*B)][::-1]),2)
            lfsr1=lfsr(int(m,2),mask1,n1)
            l2=lfsr2.getrandbit(((len(cipher2)-16)*8)).rjust((len(cipher2)-16),b"\x00")
            l1=lfsr1.getrandbit(((len(cipher2)-16)*8)).rjust((len(cipher2)-16),b"\x00")
            message = strxor(unknown_c,strxor(l1,l2))
            if b'SUSCTF' in message:
                print(message)