NSSCTF -- [tangcuxiaojkuai] -- easy_copper

easy_copper1

task.py

from Crypto.Util.number import *
from random import *
from secret import flag

assert flag.startswith(b"NSSCTF{") and flag.endswith(b"}")
assert len(flag) == 37

def gen_data(p,length):
    coeff = [randint(-2**32,2**32) for i in range(length-1)] + [randint(1,2**32)]
    return sum([coeff[i]*p**i for i in range(length)])

b = getPrime(1400)
a = gen_data(b, 6)
p = getPrime(256 * 5)
q = getPrime(256)
n = p * q
m = bytes_to_long(flag[7:-1])

print("a =",a)
print("b =",b)
print("n =",n)
print("c =",a % (b-m) % p)


'''
a = ...
b = ...
n = ...
c = ...
'''

analysis

\[a=k_5b^5+k_4b^4+\cdots+k_0|k_5\in[1,2^{32}),k_4\cdots k_0\in[-2^{32},2^{32}) \]

  • 这道题目其实考察点和《密码工程》中的一个优化方法很像,为了加速椭圆曲线上点的计算,模数可以选用特殊的梅森素数,这种素数具有以下的特殊形式\(p=2^k-b\),这样子在进行模运算的时候不需要像\(a\%b\)一样进行耗时的整数除法来获取余数,可以直接在模运算时计算得到\(2^k\%p=b\),省去了耗时的除法运算。
  • 由同余的性质,可得

\[b\equiv m\pmod{b-m}\rightarrow a\equiv k_5 m^5+k_4 m^4+\cdots k_1\pmod{b-m} \]

\[\rightarrow c\equiv k_5 m^5+k_4 m^4+\cdots k_1\pmod{p} \]

  • 接下来的问题就是,未知数\(k_i\)本身的范围相对于\(b\)来说很小,如果我们能够准确获得\(k_i\),之后就能获得仅有未知数\(m\)的多项式,就可以用\(Coppersmith\)攻击解出\(m\),\(Coppersmith\)的界为\(|x_0|<N^{\frac{\beta^2}{\delta}}\)其中\(\delta\)是多项式的阶,而\(\beta\)是齐次多项式成立下的真实模数\(b\),即\(b≤n^{\beta}\)且\(b\mid n\)。

\[n(1536-bits)\rightarrow n^{({5\over6})^2\over 5}≈2^{213}<2^{29×8}=2^{230}<m \]

  • 我发现需要求解的根\(m\)与\(Coppersmith\)攻击的界并不满足该算法求解的范围,但是在实践过程中发现,如果仍按照正常的\(Coppersmith\)去求解的话,是可以求解出来准确的\(m\),这个问题就比较奇怪了。参考下面:
    exp.sage
from Crypto.Util.number import long_to_bytes
K = [-2199405606, -2479878525, 2139313240, 815370412, -1991983599, 2189833032]
n = 1698281899194715114165111012319277103359733674717346894156321734086384210027912893592223341535254583183189375047805019470712423207121454213625786296403115965465797639874678119529865412063714723513964182015925137173277042639307327631371055326412204990172328114832185024332076266014268385262996787504249248741895102659976146333218908476195041388126877183421158327681480273218635257896126985050902620483850502219753728842322424353423665711001628722961510508066740039
c = 53146904354859601599585110457067111012858829248246133531123405294986679458995718625053726629192021849150034273282207940006128865030953003797480171720673649060942787124637476440400908506795533118278613492356804275358218541297790334587524059713360959881377651255593428483947657195937373058321156413003347566693680573881827047037751088091600420762361729539354
R.<m> = PolynomialRing(Zmod(n), 'm') # ⭐⭐⭐
f = 0
for i in range(6):
    f = f + K[i] * m ** i
# print(f)
f = f - c
f = f.monic()
res = f.small_roots(X=2 ** 232, beta=5/6)
# print(res)
print(long_to_bytes(int(res[0])))
# b'F1nd_4_M3th0d_70_COPPER5m1th!'
  • 以下内容就是我后续反思以及测试得到的,这里我想了很多原因,但是\(|x|<N^{\beta^2\over\delta}\)这个界本身就是许多中提及的一个界,包括一些需要你爆破部分位的题目设计的思路都是来源于这个界,这里阅读以下原论文看一看发现\(Coppersmith\)算法的底层其实就是\(LLL\)算法的推广,但是依旧没有找到原因所在。后续想了很久,发现a % (b-m) % p这行代码中的% p并没有起到实质性的作用,因为\(b\)是\(1400-bits\),而\(p\)最多也就是个\(1280-bits\),而对于我们计算之后的a % (b-m)之后的结果我们可以断定\(\sum_{i=0}^5(k_i\cdot m^i)<p\),所以最终的结果其实就是在整数环上进行的,有了这个想法之后我进行了脚本的修改,发现依旧可以解取\(m\):
    exp.sage
from Crypto.Util.number import long_to_bytes
K = [-2199405606, -2479878525, 2139313240, 815370412, -1991983599, 2189833032]
n = 1698281899194715114165111012319277103359733674717346894156321734086384210027912893592223341535254583183189375047805019470712423207121454213625786296403115965465797639874678119529865412063714723513964182015925137173277042639307327631371055326412204990172328114832185024332076266014268385262996787504249248741895102659976146333218908476195041388126877183421158327681480273218635257896126985050902620483850502219753728842322424353423665711001628722961510508066740039
c = 53146904354859601599585110457067111012858829248246133531123405294986679458995718625053726629192021849150034273282207940006128865030953003797480171720673649060942787124637476440400908506795533118278613492356804275358218541297790334587524059713360959881377651255593428483947657195937373058321156413003347566693680573881827047037751088091600420762361729539354
R.<m> = PolynomialRing(ZZ, 'm') # ⭐⭐⭐
f = 0
for i in range(6):
    f = f + K[i] * m ** i
# print(f)
f = f - c
# f = f.monic()
res = f.roots()
# print(res)
print(long_to_bytes(int(res[0][0])))
# b'F1nd_4_M3th0d_70_COPPER5m1th!'
  • 在测试之后可以发现,这个问题就不需要在模\(n\)的环上进行,直接放在整数环上进行。这里就想明白了一些,\(Coppersmith\)算法底层是\(LLL\)算法以及\(Howgrave-Graham\)定理的结合,其本质就是在将一个在模 N 下的困难问题,转化为在整数域上相对容易解决的问题,具体的算法总结后续想着再单独写一篇笔记记录,所以最终我认为这里虽然不满足界但仍然有解的原因为\(m\)是多项式f在整数环上的解,这个解是很容易找到的,甚至可以写一个二分算法直接寻找。顺着这个思路我进行了下面的测试(如果% p在这里起作用了,并且在我们计算的\(Coppersmith\)的界明确不足以求解\(m\)的时候,是否还能完成根的求解)
    gendata.py
from Crypto.Util.number import *
from random import *
# from secret import flag
flag = b'NSSCTF{F1nd_4_M3th0d_70_COPPER5m1th!!!!}'
assert flag.startswith(b"NSSCTF{") and flag.endswith(b"}")
assert len(flag) == 40

def gen_data(p,length):
    coeff = [randint(-2**32,2**32) for i in range(length-1)] + [randint(1,2**32)]
    print(f"K = {coeff}")
    return sum([coeff[i]*p**i for i in range(length)])

b = getPrime(1400)
a = gen_data(b, 6)
p = getPrime(256 * 5)
q = getPrime(256)
n = p * q
m = bytes_to_long(flag[7:-1])

print("a =",a)
print("b =",b)
print("n =",n)
print("c =",a % (b-m) % p)
  • 经过测试发现,当% p起到实质性作用的时候,直接在整数环上的求解便失效了,无法求解到有效根:
    test.sage
from Crypto.Util.number import long_to_bytes
K = [250693569, 2288806459, 4137781369, -3508776352, 3817854811, 1154705382]
n = 1038760302941354854107862437493523093034241489565801722913295050894976288348751548318192801918188856308789242968127124520893398683136970007988994078553208603843927331287562383831223607162677194533912177451843651617164897000165607766269609678537219166432288087157483618987978066029375311222900486868053670921382325047335670307868811724337232892996557511015326598333116812781578953827753775292614532582454608970558921799387359723821660848448649933933198657767108121
c = 11006323688908876223081966229992919643307779638021006744233965335849974327574097928481191499076377686488535000114218627121335049938218429683602387692280765346687505492238298327896911681979543574630997438274797169662601565031402400805933096043111848701737451699661601817207652989061725094686231513163172271333284053104329552135534544322331441871990100166062694870388709952123049329127147

R.<m> = PolynomialRing(ZZ, 'm') # ⭐⭐⭐
f = 0
for i in range(6):
    f = f + K[i] * m ** i
# print(f)
f = f - c
# f = f.monic()
res = f.roots()
print(res) # []
  • 同时我们去调用\(Coppersmith\)算法去测试,发现同样失效了,这里我就归结于是我们求解的根的大小不适配与该算法的界:
    test.sage
from Crypto.Util.number import long_to_bytes
K = [250693569, 2288806459, 4137781369, -3508776352, 3817854811, 1154705382]
n = 1038760302941354854107862437493523093034241489565801722913295050894976288348751548318192801918188856308789242968127124520893398683136970007988994078553208603843927331287562383831223607162677194533912177451843651617164897000165607766269609678537219166432288087157483618987978066029375311222900486868053670921382325047335670307868811724337232892996557511015326598333116812781578953827753775292614532582454608970558921799387359723821660848448649933933198657767108121
c = 11006323688908876223081966229992919643307779638021006744233965335849974327574097928481191499076377686488535000114218627121335049938218429683602387692280765346687505492238298327896911681979543574630997438274797169662601565031402400805933096043111848701737451699661601817207652989061725094686231513163172271333284053104329552135534544322331441871990100166062694870388709952123049329127147

R.<m> = PolynomialRing(Zmod(n), 'm') # ⭐⭐⭐
f = 0
for i in range(6):
    f = f + K[i] * m ** i
# print(f)
f = f - c
f = f.monic()
res = f.small_roots(X=2 ** 255, beta=5/6)
print(res) # []
  • 总结一下,在判断\(Coppersmith\)算法能否解决我们恢复出来的多项式时发现,我们要求解的\(m\)并不满足这个界,但是依旧能够求出来的原因就是% p并未起到作用,整个多项式在整数环上也成立并且可解,这导致了在\(Coppersmith\)算法也可以解。
  • 这里我求解\(k_i\)的思路就是:
    1. 计算\(r=a\%b\)可以得到低位信息,多项式\(a=k_5b^5+\cdots +k_1b+k_0\),模\(b\)后只剩下\(k_0\),但得到的是\(k_0\pmod{b}\),在\([0,b-1]\)之间,当\(k_0≥0\)时,\(r=k_0\);当\(k_0<0\)时,\(r=b-k_0\)。
    2. 因为\(b>>k_i\),所以若\(r≤\lfloor{b\over2}\rfloor\),说明真实系数一定非负,\(k=r\);反之说明真实系数是负的,\(k=r-b\)。
    3. 得到真实的\(k_i\)后,\(a-k_i\)正好是\(b\cdots(k_5b^5+\cdots k_{i+1}b)\),除以\(b\)后,下一次循环就可以提取\(k_{i+1}\)。
      recoverk.py
a = 5549997533567190765451060003378594328208085965171057613046272782399320385801262427125465925310587069826816190505343998268891453664853919954972318043604177749860432778530185933735996050024160286370510179686746394158379258246587487978911503057556662561587215910791569507689015766531668277131113986057590781396398336299292315557904756600303993655014781202374308079885517355500419820878630803963625154724593589268277135757575099029314373537333985928427361897778453968429622806601778705482232467565493789524705788745221275426482603133790789320192127238641529684801868328091692081269798378555677980295282241736002111769899269365161305274768242912746034221266737507823710607682307448305506469386320122162063129111963956813928790055972563594890074229166442357508298255754150913934863439503751818506701092029312330070104636530223422238884679342877228773018649571938389437053258838947075103547766682006907290041060099030401078504154580918834901230411520541473984503892569919351799880236285333890681120340758177935223362561618860440679402085624994947954310720781671847323988290985994849577007330168617301104973145130975458942852068773890456852008660678772797081769299221752824132395057925957966236190693386264986627705062247274388532481890731361579962994281795847973719352965725024336536011747744286094626784548450945808977185031072003163884715639335582500821735372264778337277968982744615544014751382104123657815885683865159874330848264901320900587726777026680111663529893241507534145907710815328515080873983547987954554660585289621642017226020909406358582195295944513518489513783744170261194300084082634297086645338918551616443632828664857206551814971287007726341429714596407033649324399745531577851802389713905682407743356548494218886990858323750912334486066574949941863337284447418868362349705104596997062541665923824132293441261853623963153677146052741893849156220411360012198280306111549261407941431691085949136294791896025980996617245031309439043593114299292206028201810212469726512376109348231607843056259556121448353511032010729667364764526544162088652166231118576736952880650975321889920727908566750809108688218503956
b = 19088700216864219992000481909154962955010217153589993304722719340884054355376558326105036947257582728860147557431276912919643940358478125733042829478349114754313750607492935206321298801011776939307313478546331523512938761672813983870399597182080005906066953602228332948790458576153448761425614070248334986663583719133564252378947422392557755444674008030141846366476287826338519233008704965899055639264609925259823048079319

coffs = []
for i in range(6):
    r = a % b
    if r > b // 2:
        c = r - b
    else:
        c = r
    coffs.append(c)
    a = (a - c) // b
print(coffs)
# [-2199405606, -2479878525, 2139313240, 815370412, -1991983599, 2189833032]
  • 或者可以构建格来进行求解(\(a.bit\_length()=7029\)):

\[\sum_{i=0}^5(k_i\cdot b^i)-a=0 \]

\[L = \left[ \begin{matrix} 0 & 0 & 0 & 0 & 0 & 0 & 1\\ 0 & 0 & 0 & 0 & 0 & 1 & 0\\ 0 & 0 & 0 & 0 & 1 & 0 & 0\\ 0 & 0 & 0 & 1 & 0 & 0 & 0\\ 0 & 0 & 1 & 0 & 0 & 0 & 0\\ 0 & 1 & 0 & 0 & 0 & 0 & 0\\ -a & b^5 & b^4 & b^3 & b^2 & b & 1 \end{matrix} \right];\vec{b}=\left[ \begin{matrix} 1\\ k_5\\ k_4\\ k_3\\ k_2\\ k_1\\ k_0 \end{matrix} \right]\rightarrow L\cdot \vec{b}=\left[ \begin{matrix} k_0\\ k_1\\ k_2\\ k_3\\ k_4\\ k_5\\ 0 \end{matrix} \right]=\vec{v} \]

\[||\vec{v}||=\sqrt{k_0^2+k_1^2+\cdots k_5^5}<\sqrt{6}\cdot 2^{32}<<bound=\sqrt{7}\cdot a^{1\over7} \]

  • 注意,为了避免\(L\)最后一行中增加权重因子([[easy_mod]])\(W=2^{256}\)避免伪解过多导致规约不到我们的目标向量\(\vec{v}\)。
    recoverk.sage
a = 5549997533567190765451060003378594328208085965171057613046272782399320385801262427125465925310587069826816190505343998268891453664853919954972318043604177749860432778530185933735996050024160286370510179686746394158379258246587487978911503057556662561587215910791569507689015766531668277131113986057590781396398336299292315557904756600303993655014781202374308079885517355500419820878630803963625154724593589268277135757575099029314373537333985928427361897778453968429622806601778705482232467565493789524705788745221275426482603133790789320192127238641529684801868328091692081269798378555677980295282241736002111769899269365161305274768242912746034221266737507823710607682307448305506469386320122162063129111963956813928790055972563594890074229166442357508298255754150913934863439503751818506701092029312330070104636530223422238884679342877228773018649571938389437053258838947075103547766682006907290041060099030401078504154580918834901230411520541473984503892569919351799880236285333890681120340758177935223362561618860440679402085624994947954310720781671847323988290985994849577007330168617301104973145130975458942852068773890456852008660678772797081769299221752824132395057925957966236190693386264986627705062247274388532481890731361579962994281795847973719352965725024336536011747744286094626784548450945808977185031072003163884715639335582500821735372264778337277968982744615544014751382104123657815885683865159874330848264901320900587726777026680111663529893241507534145907710815328515080873983547987954554660585289621642017226020909406358582195295944513518489513783744170261194300084082634297086645338918551616443632828664857206551814971287007726341429714596407033649324399745531577851802389713905682407743356548494218886990858323750912334486066574949941863337284447418868362349705104596997062541665923824132293441261853623963153677146052741893849156220411360012198280306111549261407941431691085949136294791896025980996617245031309439043593114299292206028201810212469726512376109348231607843056259556121448353511032010729667364764526544162088652166231118576736952880650975321889920727908566750809108688218503956
b = 19088700216864219992000481909154962955010217153589993304722719340884054355376558326105036947257582728860147557431276912919643940358478125733042829478349114754313750607492935206321298801011776939307313478546331523512938761672813983870399597182080005906066953602228332948790458576153448761425614070248334986663583719133564252378947422392557755444674008030141846366476287826338519233008704965899055639264609925259823048079319

M = []
for i in range(6):
    v = [0] * 7
    v[6 - i] = 1
    M.append(v)
v = [-a] + [b ** i for i in range(5, -1, -1)]
W = 2 ** 256
v = [x * W for x in v]
M.append(v)

M = Matrix(ZZ, M)
L = M.transpose().LLL()
print("LLL done.")
for row in L:
    if row[-1] == 0:
        if row[-2] < 0:
            row = -row
        print(row)
# (-2199405606, -2479878525, 2139313240, 815370412, -1991983599, 2189833032, 0)

easy_copper2

task.py

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

m = bytes_to_long(flag)

p = getPrime(512)
q = getPrime(512)
n = p*q
e = 65537
c = pow(m,e,n)

print("n =", n)
print("c =", c)
print("leak1 =", p >> 282)
print("leak2 =", p % 215656441)

'''
n = 83732821313465518052403665361614770500711747426707910445616394700719876467737514967114877768176244233541342950517438107504392659632618504678367884223695674258126620001220856677629607205209582904215330731871567514530350222492246762740556482040907225061791231222448377878854527601783227627969726021295513927063
c = 46663818733755991848242947341712498383456884024793897130170411388799402223110989123025227270450872334684154450132747808192836148157068113180136519163245994436646022864578219391320904777242102617963109623497099134092899460260651347833764105572783843769863133591669278971958095602865992957181139586462882547338
leak1 = 1166802227519044965330497437183661580954600955790078699599066071608461
leak2 = 100652187
'''

analysis

  • \(leak_1\)是\(p\)的高\(512-282=230\)位,根据上界我们可以算一下小根的取值范围\(|x|<n^{({1\over2})^2\over1}=n^{1\over4}≈2^{256}\),由于\(2^{282}>2^{256}\),由于\(282-256=26\),这个范围也是可以爆破的,为了保证\(Coppersmith\)算法本身的速度,我们不要去卡\(2^{256}\)的极限,我们可以再多爆破几位:
    exp.sage
from tqdm import tqdm
n = 83732821313465518052403665361614770500711747426707910445616394700719876467737514967114877768176244233541342950517438107504392659632618504678367884223695674258126620001220856677629607205209582904215330731871567514530350222492246762740556482040907225061791231222448377878854527601783227627969726021295513927063
c = 46663818733755991848242947341712498383456884024793897130170411388799402223110989123025227270450872334684154450132747808192836148157068113180136519163245994436646022864578219391320904777242102617963109623497099134092899460260651347833764105572783843769863133591669278971958095602865992957181139586462882547338
leak1 = 1166802227519044965330497437183661580954600955790078699599066071608461
R.<x> = PolynomialRing(Zmod(n), 'x')
for i in tqdm(range(2 ** 27)):
    f = leak1 * 2 ** 282  + x * 2 ** 27 + i
    f = f.monic()
    res = f.small_roots(X=2**256, beta=1/2)
    if res:
        print(res)
  • 但是这里发现这里的爆破效率极慢,在没有添加epsilon = 0.01的限制去卡\(Coppersmith\)算法的界就已经很慢了,因此这里想着去使用一下\(leak_2\)。

\[leak_2\equiv p\pmod{215656441}\rightarrow p=k\cdot 215656441+leak_2\rightarrow k=? \]

\[p=p_{high}+p_{mid}+p_{low};let\quad p_{low}=leak_2\rightarrow p_{high}+p_{mid}\equiv 0\pmod{215656441} \]

\[p_{high}=leak_1\cdot 2^{282};let\quad p_{high}'=p_{high}-(p_{high}\pmod{215656441}) \]

\[p_{high}'\equiv0\pmod{215656441}\rightarrow p_{mid}\equiv 0\pmod{215656441}\rightarrow p=p_{high}'+215656441\cdot p_{mid}+p_{low} \]

  • 此时就相当于是我们在高位信息的基础上还获取得到了低位信息(\(215656441.bit\_length()=28\)),要求解的根就是\(p_{mid}\),但是我们需要爆破一些二进制位,这里我取的是\(9\)位的一个爆破,爆破求解之后进行带入求解\(p\),解密即可。
    exp.sage
from Crypto.Util.number import long_to_bytes
from tqdm import tqdm
n = 83732821313465518052403665361614770500711747426707910445616394700719876467737514967114877768176244233541342950517438107504392659632618504678367884223695674258126620001220856677629607205209582904215330731871567514530350222492246762740556482040907225061791231222448377878854527601783227627969726021295513927063
c = 46663818733755991848242947341712498383456884024793897130170411388799402223110989123025227270450872334684154450132747808192836148157068113180136519163245994436646022864578219391320904777242102617963109623497099134092899460260651347833764105572783843769863133591669278971958095602865992957181139586462882547338
leak1 = 1166802227519044965330497437183661580954600955790078699599066071608461
leak2 = 100652187
temp = 215656441
p_high = leak1 * 2 ** 282
p_high = p_high - (p_high % temp)
R.<x> = PolynomialRing(Zmod(n), 'x')
for i in tqdm(range(2 ** 9)):
    f = p_high + temp * (i + 2 ** 9 * x) + leak2
    f = f.monic()
    res = f.small_roots(X=2**246, beta=1/2, epsilon=0.01)
    if res:
        print(res)
        break
  • 这道题目的exp.sage运行效率很慢,完成这个系列赛题之后去看了一下tangcuxijkuai师傅的题解,发现了其说道下面的一种优化思路,将其归结为剪枝优化:
    1. 在已知高位和低位的情况下,原本的等式是:\(p=p_{high}+215656441\cdot x+p_{low}\)这里\(x\)是我们要用\(Coppersmith\)算法求出的未知数,为了保证未知数的位数满足该算法的界常规的思路就是进行二进制位爆破。
    2. 把\(x\)的最后\(5\)个二进制位爆破猜出,\(x=2^5\cdot x'+i[i\in[0,2^{5})]\),代入原式\(p=p_{high}+215656441\cdot(32\cdot x'+i)+p_{low}\rightarrow p=[p_{high}+215656441\cdot i +p_{low}]+(32\cdot 215656441)\cdot x'\),这样做的好处就是把未知的系数从\(215656441\)扩大了\(32\)倍,相应的,剩下的未知数的上限就缩小了\(32\)倍,\(Coppersmith\)的可解性也就增加了。
    3. 小素数优化的剪枝思路就是利用\(p\)是素数的性质,\(215656441=7×11×13×17×19×23×29\),其中缺少了小素数\(2,3,5\),既然二进制爆破是把\(x\)除以\(32\),我们就可以把\(x\)除以\(2×3×5=30\)进行爆破,\(x=30\cdot x'+i[i\in[0,30)]\),此时有\(p=[p_{high}+215656441\cdot i +p_{low}]+(30\cdot 215656441)\cdot x'\),表面上看,未知数缩小了\(30\)倍,我们需要循环\(30\)次(\(i\)是从\(0\)到\(29\)),似乎没有区别,但已知\(p\)是一个大素数,即\(2,3,5\)不可能整除\(p\),对于上面这个等式而言,后半部分\((30\cdot 215656441)\cdot x'\)显然是\(30\)的倍数,那么前半部分一定不能被\(2,3,5\)整除,那么\(i\)从\(0\)到\(29\)的选择中,有多少个\(i\)能让该常数项与\(30\)互素?根据欧拉公式\(φ(n)=30×(1-{1\over2})×(1-{1\over3})×(1-{1\over5})=8\)可知,在每\(30\)个连续的数中,只有\(8\)个数与\(30\)互素。这意味着我们可以将\(30\)次循环直接通过常数项是否与\(30\)互素降低为\(8\)次循环爆破,起到剪枝优化的效果。
      exp-pro.sage
from Crypto.Util.number import long_to_bytes
from tqdm import tqdm
n = 83732821313465518052403665361614770500711747426707910445616394700719876467737514967114877768176244233541342950517438107504392659632618504678367884223695674258126620001220856677629607205209582904215330731871567514530350222492246762740556482040907225061791231222448377878854527601783227627969726021295513927063
c = 46663818733755991848242947341712498383456884024793897130170411388799402223110989123025227270450872334684154450132747808192836148157068113180136519163245994436646022864578219391320904777242102617963109623497099134092899460260651347833764105572783843769863133591669278971958095602865992957181139586462882547338
leak1 = 1166802227519044965330497437183661580954600955790078699599066071608461
leak2 = 100652187
M = 215656441
p_high = leak1 * 2 ** 282
p_high = p_high - (p_high % M)
guess = []
num = 2 * 3 * 5
for i in range(num):
    temp = p_high + leak2 + i * M
    if gcd(temp, num) == 1:
        guess.append(i)
# print(guess)
R.<x> = PolynomialRing(Zmod(n), 'x')
for i in tqdm(guess):
    f = p_high + M * (num * x + i) + leak2
    f = f.monic()
    res = f.small_roots(X=2**282 // (num * M) , beta=0.499, epsilon=0.01)
    if res:
        p = int(p_high + M * (num * res[0] + i) + leak2)
        q = n // p
        phi = (p - 1) * (q - 1)
        d = pow(65537, -1, phi)
        m = pow(c, d, n)
        print(long_to_bytes(m))
        break
# b'NSSCTF{An0t3hr_b3tt3r_m3th0d_t0_brut3f0rc3_c0pp3r!}'
  • 在解题过程中发现这道题目的界卡的很死,对于\(sagemath\)中的small_root而言,如果将X = 2 ** 282 // (num * M)替换为2 ** 251就会导致无解。
  • 这个优化思路是不是通用的呢?我们可以推演一下,我们设扩展数为\(num(30)\),泄露低位所使用的模数是\(M\),\(p=p_{high}+p_{low}+M\cdot p_{mid}\),\(p_{mid}=num\cdot x+i[i\in[0,num)]\),因此\(p=[p_{high}+p_{low}+M\cdot i]+(M\cdot num)\cdot x\)。已知\(gcd(num,p)=1\),后半部分显然被\(num\)整除,因此我们可以通过前半部分与\(num\)互素来筛选\(i\)。如果说\(gcd(num,M)=1\),那么在\(i\)连续遍历的过程中,有\(φ(num)\)个\(i\)满足条件;但是如果\(gcd(num,M)≠1\),这个时候\(gcd(p_{high}+p_{low}+M\cdot i,num)\)不再由\(φ(num)\)决定,基本起不到剪枝筛选的效果。例如\(M=2^{32}\),采用\(num=15/16\)进行\(i\)的筛选结果如下:
    test.py
from Crypto.Util.number import getPrime, GCD

p = getPrime(512)
high = p >> 282 << 282
M = 2 ** 32
low = p % M
num = 16 # 15
# ⭐ gcd(num, M) = 1
guess = []
for i in range(num):
    temp = low + M * i
    if GCD(temp, num) == 1:
        guess.append(i)
print(len(guess))
"""
num = 15 --> len(guess) = 8
num = 16 --> len(guess) = 16
"""

easy_copper3

task.py

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

p = getPrime(512)
q = getPrime(512)
n = p*q
e = getPrime(32)
c = pow(bytes_to_long(flag), e, n)
u = inverse(q,p)

print("n =",n)
print("e =",e)
print("c =",c)
print("u =",u)

'''
n = ...
e = ...
c = ...
u = ..
'''

analysis

\[u\equiv q^{-1}\pmod{p}\rightarrow uq=kp+1\rightarrow uq^2=kn+q\rightarrow uq^2\equiv q\pmod{n}\rightarrow uq^2-q\equiv0\pmod{n} \]

\[q.bit\_lengh()=512;n^{1^2\over 2}=n^{1\over2}≈2^{512} \]

  • 尝试爆破低位进行\(Coppersmith\),这道题目中的\(M\)就相当于是\(n\)本身了,因此我们可以根据需要爆破的范围选取\(num=2×3×5×7×11=2310(12-bits)\),\(q=num\cdot x + i[i\in[0,num)]\),常数项为\(i\),其中\(φ(2310)=480\)。\(f = u\cdot (num\cdot x + i)^2-(num\cdot x + i)\)
    exp.sage
from Crypto.Util.number import long_to_bytes
from multiprocessing import Pool
from tqdm import tqdm
n = 57054300236523364297068573084561858708294662390960413501481702646411973922601148943850295724745015979460852792874663924727251780057665081615966986614006891899776582114850433561286026344516509555159123543852355598205747122411634510298915483597709877911019093953862935760391037639842229970271659629400825516949
e = 3914808559
c = 34367961236912765697312756121175172638962230270006286310938236988443532571967649877497844350082733634910858022991620956125616557464236977620127700161939950208821044710180411053388650174106798369640918760166665311872344484980029847646184112357339382437302111172508120844002507660831969088208714656235021379114
u = 3971337705608798216937148389824777665706019231614517125236102421717365951782208458636759087630906187451681891734064328433966129922001851802738564150946911

possible = []
num = 2 * 3 * 5 * 7 * 11
for i in range(num):
    if gcd(i, num) == 1:
        possible.append(i)
# print(len(possible))
def attack(q_low):
    R.<x> = PolynomialRing(Zmod(n), 'x')
    f = u * (num * x + q_low) ** 2 - (num * x + q_low)
    f = f.monic()
    res = f.small_roots(X=2**512 // num, beta=1.00, epsilon=0.0199)
    if res:
        print(f"q_low = {q_low}")
        print(res)
        for qq in res:
            q = int(num * qq + q_low)
            if is_prime(q):
                p = n // q
                phi = (p - 1) * (q - 1)
                d = pow(e, -1, phi)
                m = pow(c, d, n)
                print(long_to_bytes(m))
                return True
                
with Pool(12) as pool:
    for i in tqdm(pool.imap(attack, possible), total=len(possible)):
        if(i == True):
            break
# p_low = 1751
# [2979090122303849723567441487327425447612923043044261718840442956425039351529737516483652398319666119083476292000977294122802833138639858356048732934023]
# b'NSSCTF{En0ugh_F0r_c0op3r!}'

总结

  • 这个系列的题目界限都卡的比较死,解题的时候exp.sage的运行时间普遍也都比较长,但是学到了以前没有认知的东西:
    1. easy_copper1用到了伪梅森素数优化取模运算,但是我认为的一个缺点就是并没有将问题严格地限制在模\(N\)的齐次多项式问题上,最终是\(Coppersmith\)的界并不满足,但是整数环上可解的一个问题。
    2. easy_copper2,由于常见的\(RSA\)问题中,需求都是尝试获取得到\(p/q\)这两个大素数,找到相应的多项式\(f(x)\),需要爆破时利用多项式表示\(p/q\),以前常用的可能是对\(p=p_{high}+(2^{p_{low}.bit\_length()}\cdot i + 2^{p_{low}.bit\_length()+k}\cdot x)+p_{low}\)直接进行爆破,利用素数的性质,可以提前剪枝筛选出不需要尝试的\(i\),我们要选取已知系数\(M\)中没有的小素数,进行乘积\(num\),我们最终需要爆破尝试的\(i\)就有\(φ(num)\)个,设化简后的多项式中的常数项为\(temp\),筛选的条件就是\(gcd(temp, num)==1\),起到一个优化加速的效果。
    3. easy_copper3则是在已知\(p^{-1}\pmod{q}/q^{-1}\pmod{p}\)的情况下的模型,但是最终的考点还是为了保证\(Coppersmith\)算法的可解性以及效率问题上进行爆破,上述解题脚本中参考了[P5564]官方解题思路 - NSSCTF,本人Python并发编程学习笔记。
  • 受益匪浅,另外就是在总结部分搞清楚\(Coppersmith\)算法的界以及\(sagemath\)中调用small_roots(X, beta, epsilon)这些参数的精确度范围,因为在easy_copper3解题过程中,我发现epsilon=0.01解题速度过于得慢,epsilon=0.02又找不到解,但是参考上述师傅的题解后发现epsilon=0.0199则恰好可以权衡效率与精确度。
  • \(Coppersmith\)算法的研究和改进是经过很多代密码学家的研究和改进的,准确的界为\(X < c_1(n) \cdot N^{\frac{2 \beta m n - d m(m+1)}{n(n-1)}}\),但是对于我们正常使用,而非继续研究其底层基理的话,对于单变量\(Coppersmith\)算法,我们记住下面的界就可以了\(X = \left\lceil \frac{1}{2} N^{\frac{\beta^2}{\delta} - \epsilon} \right\rceil\)。
  • 因为我记得最初学习使用small_roots函数的时候记得有篇博客说beta的接收参数精确度就只有一位还是两位,大致的意思就是beta=0.51与beta=0.59的效果是一致的,所以平时在使用的时候,对于epsilon的参数调整,基本只有调整过0.01/0.02/0.03,潜意识中没有使用过0.0199这样更精确的参数。这里\(Gemini-Deep\ Research\)的说法就是,我们传入的参数最终会被用于建立\(LLL\)算法所建立的矩阵,对于正常的解题使用而言,精度是全然可以接收使用的,如果追求更高的精度,可以使用下面的代码模块:
# 手动创建一个具备 256 位极端二进制精度的底层 MPFR 实数域
HighPrecField = RealField(256)
# 将高精度浮点参数作为字面量字符串传入,防止Python进行预先的64位破坏性截断
extreme_epsilon = HighPrecField('0.01990000000000000000000000001')
# 将此具备抗漂移能力的极高精度参数注入求根引擎
f.small_roots(X=bound_limit, beta=1.0, epsilon=extreme_epsilon)
  • 至于建立的矩阵维度\(n=d\cdot m+t\),其中\(m = \lceil \frac{\beta^2}{d \cdot \epsilon} \rceil\);\(t = \lfloor d \cdot m \left( \frac{1}{\beta} - 1 \right) \rfloor\)。
posted @ 2026-05-10 11:30  chen_xing  阅读(57)  评论(0)    收藏  举报