剪枝

剪枝

最近ctfshow西瓜杯做到一题

factor

附件:

from Crypto.Util.number import *
import gmpy2
import os
from enc import flag

hint = os.urandom(36)
tmp = bytes_to_long(hint)
m = bytes_to_long(flag)
p = getPrime(512)
q = getPrime(512)
d = getPrime(400)
phi = (p-1)*(q-1)
e = gmpy2.invert(d,phi)
n = p*q
c = pow(m,e,n)
leak1 = p^tmp
leak2 = q^tmp
print(f"n = {n}")
print(f"e = {e}")
print(f"c = {c}")
print(f"leak1 = {leak1}")
print(f"leak2 = {leak2}")
'''
n = 145462084881728813723574366340552281785604069047381248513937024180816353963950721541845665931261230969450819680771925091152670386983240444354412170994932196142227905635227116456476835756039585419001941477905953429642459464112871080459522266599791339252614674500304621383776590313803782107531212756620796159703
e = 10463348796391625387419351013660920157452350067191419373870543363741187885528042168135531161031114295856009050029737547684735896660393845515549071092389128688718675573348847489182651631515852744312955427364280891600765444324519789452014742590962030936762237037273839906251320666705879080373711858513235704113
c = 60700608730139668338977678601901211800978306010063875269252006068222163102100346920465298044880066999492746508990629867396189713753873657197546664480233269806308415874191048149900822050054539774370134460339681949131037133783273410066318511508768512778132786573893529705068680583697574367357381635982316477364
leak1 = 13342820281239625174817085182586822673810894195223942279061039858850534510679297962596800315875604798047264337469828123370586584840078728059729121435462780
leak2 = 10901899434728393473569359914062349292412269512201554924835672710780580634465799069211035290729536290605761024818770843901501694556825737462457471235151530
'''

比较简单

可以看出关键是异或,且已知leak1和leak2,将二者异或得到的就是p和q异或的值,所以想到剪枝

import gmpy2
from Crypto.Util.number import*
n = 145462084881728813723574366340552281785604069047381248513937024180816353963950721541845665931261230969450819680771925091152670386983240444354412170994932196142227905635227116456476835756039585419001941477905953429642459464112871080459522266599791339252614674500304621383776590313803782107531212756620796159703
e = 10463348796391625387419351013660920157452350067191419373870543363741187885528042168135531161031114295856009050029737547684735896660393845515549071092389128688718675573348847489182651631515852744312955427364280891600765444324519789452014742590962030936762237037273839906251320666705879080373711858513235704113
c = 60700608730139668338977678601901211800978306010063875269252006068222163102100346920465298044880066999492746508990629867396189713753873657197546664480233269806308415874191048149900822050054539774370134460339681949131037133783273410066318511508768512778132786573893529705068680583697574367357381635982316477364
leak1 = 13342820281239625174817085182586822673810894195223942279061039858850534510679297962596800315875604798047264337469828123370586584840078728059729121435462780
leak2 = 10901899434728393473569359914062349292412269512201554924835672710780580634465799069211035290729536290605761024818770843901501694556825737462457471235151530
xor=leak1^leak2
pbits=512
xor = str(bin(xor)[2:]).zfill(pbits)

ph = ''
qh = ''

def find(ph,qh):
l0 = len(ph)
l1 = len(qh)
tmp0 = ph + '0' * (pbits-l0)
tmp1 = ph + '1' * (pbits-l0)
tmq0 = qh + '0' * (pbits-l1)
tmq1 = qh + '1' * (pbits-l1)
if int(tmp0,2) * int(tmq0,2) > n:#剪枝条件1
return
if int(tmp1,2) * int(tmq1,2) < n:#剪枝条件2
return

if l0 == pbits:#结束条件
if int(ph,2) * int(qh,2) == n:
print(f'p = {int(ph,2)}')
print(f'q = {int(qh,2)}')
return

else:
if xor[l1] == '1':
find(ph+'0',qh+'1')
find(ph+'1',qh+'0')
else:
find(ph+'1',qh+'1')
find(ph+'0',qh+'0')

find(ph,qh)

p=13342820281239625174817085182586822673810894195223942279061039858850820238924757629478502588722905946742147654397240559596444430114180174785691409037959681
q=10901899434728393473569359914062349292412269512201554924835672710780357314223209262466102457697460432424101968655028724203256351586952677194451591494555863

d=gmpy2.invert(e,(p-1)*(q-1))
print(long_to_bytes(pow(c,d,n)))

#b'cftshow{do_you_know_what_is_xor_and_prune!!!}'

比较容易想到这个思路

写到这题了那就再写一下剪枝吧^_^

简单的剪枝比较容易看出来,像上面那题套个脚本就出了

p^q

就像上面那题,给出p*q和p^q,对于每一位p^q的结果来说,该位的组合情况有 4 种1^0,0^1,1^1,0^0

搜索方式是从低位开始搜索,根据

条件限定,并且需要满足

将p和q剩下位全部填充为1,需要满足 p*q > n

将p和q剩下位全部填充为0,需要满足 p*q < n

脚本:

pbits=
xor = str(bin(xor)[2:]).zfill(pbits)
ph = ''
qh = ''

def find(ph,qh):
l0 = len(ph)
l1 = len(qh)
tmp0 = ph + '0' * (pbits-l0)
tmp1 = ph + '1' * (pbits-l0)
tmq0 = qh + '0' * (pbits-l1)
tmq1 = qh + '1' * (pbits-l1)
if int(tmp0,2) * int(tmq0,2) > n:#剪枝条件1
return
if int(tmp1,2) * int(tmq1,2) < n:#剪枝条件2
return

if l0 == pbits:#结束条件
if int(ph,2) * int(qh,2) == n:
print(f'p = {int(ph,2)}')
print(f'q = {int(qh,2)}')
return

else:
if xor[l1] == '1':
find(ph+'0',qh+'1')
find(ph+'1',qh+'0')
else:
find(ph+'1',qh+'1')
find(ph+'0',qh+'0')

find(ph,qh)
例题

帕鲁杯 江枫渔火对愁眠

from Crypto.Util.number import *
flag = b'paluctf{******************}'
p = getPrime(512)
q = getPrime(512)
m = bytes_to_long(flag)
n = p * q
e = 0x10001
c = pow(m, e, n)
leak1 = p & q
leak2 = p | q
print(n)
print(leak1)
print(leak2)
print(c)
# 116117067844956812459549519789301338092862193317140117457423221066709482979351921356314593636327834899992321545232613626111009441254302384449742843180876494341637589103640217194070886174972452908589438599697165869525189266606983974250478298162924187424655566019487631330678770727392051485223152309309085945253
# 8605081049583982438298440507920076587069196185463800658188799677857096281403951362058424551032224336538547998962815392172493849395335237855201439663804417
# 13407373154151815187508645556332614349998109820361387104317659096666170318961881115942116046384020162789239054091769561534320831478500568385569270082820389
# 77391898018025866504652357285886871686506090492775075964856060726697268476460193878086905273672532025686191143120456958000415501059102146339274402932542049355257662649758904431953601814453558068056853653214769669690930883469679763807974430229116956128100328073573783801082618261383412539474900566590518020658

看起来非常简单,结果卡住了,我印象里|和^都是按位异或,但试了一下才发现实际上是

| 按位或:对应位有一个为1则为1。

^按位异或:对应位不同则为1,相同则为0。(我是笨蛋,这都会记错

所以leak1^leak2=p^q,常规剪枝

import gmpy2
from Crypto.Util.number import*

n=116117067844956812459549519789301338092862193317140117457423221066709482979351921356314593636327834899992321545232613626111009441254302384449742843180876494341637589103640217194070886174972452908589438599697165869525189266606983974250478298162924187424655566019487631330678770727392051485223152309309085945253
leak1=8605081049583982438298440507920076587069196185463800658188799677857096281403951362058424551032224336538547998962815392172493849395335237855201439663804417
leak2=13407373154151815187508645556332614349998109820361387104317659096666170318961881115942116046384020162789239054091769561534320831478500568385569270082820389
xor=leak1^leak2
pbits=512
xor = str(bin(xor)[2:]).zfill(pbits)
ph = ''
qh = ''

def find(ph,qh):
l0 = len(ph)
l1 = len(qh)
tmp0 = ph + '0' * (pbits-l0)
tmp1 = ph + '1' * (pbits-l0)
tmq0 = qh + '0' * (pbits-l1)
tmq1 = qh + '1' * (pbits-l1)
if int(tmp0,2) * int(tmq0,2) > n:#剪枝条件1
return
if int(tmp1,2) * int(tmq1,2) < n:#剪枝条件2
return

if l0 == pbits:#结束条件
if int(ph,2) * int(qh,2) == n:
print(f'p = {int(ph,2)}')
print(f'q = {int(qh,2)}')
return

else:
if xor[l1] == '1':
find(ph+'0',qh+'1')
find(ph+'1',qh+'0')
else:
find(ph+'1',qh+'1')
find(ph+'0',qh+'0')

find(ph,qh)

p=8765698777357218895930455433534622474349736018036786722894513584283441223303812128653973162721831346202633677284766954990094900299096944074318482652846369
q=13246755426378578729876630630718068462717569987788401039611945190239825377062020349346567434694413153125153375769817998716719780574738862166452227093778437
e=0x10001
c=77391898018025866504652357285886871686506090492775075964856060726697268476460193878086905273672532025686191143120456958000415501059102146339274402932542049355257662649758904431953601814453558068056853653214769669690930883469679763807974430229116956128100328073573783801082618261383412539474900566590518020658
d=gmpy2.invert(e,(p-1)*(q-1))
print(long_to_bytes(pow(c,d,n)))
#b'paluctf{&&&|||&&&|||&&&&&&&&&&&&|||||||||}'

首尾剪枝(p^q_rev)

已知p与反方向二进制的q异或的值和p*q

从两端向中间搜索,每次搜索利用p^q_rev当前位数(与p^q_rev当前最高位有关的是当前p的最高位和q的最低位,p^q_rev当前最低位有关的是当前p的最低位和q的最高位)

如果当前搜索的最高位为”1”,则可能是:p该位为1,q对应低位为0;p该位为0,q对应低位为1。

如果当前搜索的最高位为”0”,则可能是:p该位为0,q对应低位为0;p该位为1,q对应低位为1。

搜索最低位也一样

从以下几个限定剪枝:

将p、q未搜索到的位全填0,乘积应该小于n;

将p、q未搜索到的位全填1,乘积应该大于n;

p、q低k位乘积再取低k位应该和n的低k位相同

脚本:

def find(ph,qh,pl,ql):
l = len(ph)
tmp0 = ph + (256-2*l)*"0" + pl
tmp1 = ph + (256-2*l)*"1" + pl
tmq0 = qh + (256-2*l)*"0" + ql
tmq1 = qh + (256-2*l)*"1" + ql
if(int(tmp0,2)*int(tmq0,2) > n):
return
if(int(tmp1,2)*int(tmq1,2) < n):
return
if(int(pl,2)*int(ql,2) % (2**(l-1)) != n % (2**(l-1))):
return

if(l == 128):
pp0 = int(tmp0,2)
if(n % pp0 == 0):
pf = pp0
qf = n//pp0
phi = (pf-1)*(qf-1)
d = inverse(e,phi)
m1 = pow(c,d,n)
print(long_to_bytes(m1))
exit()

else:
if(pxorq[l] == "1" and pxorq[255-l] == "1"):
find(ph+"1",qh+"0","1"+pl,"0"+ql)
find(ph+"0",qh+"0","1"+pl,"1"+ql)
find(ph+"1",qh+"1","0"+pl,"0"+ql)
find(ph+"0",qh+"1","0"+pl,"1"+ql)
elif(pxorq[l] == "1" and pxorq[255-l] == "0"):
find(ph+"1",qh+"0","0"+pl,"0"+ql)
find(ph+"0",qh+"0","0"+pl,"1"+ql)
find(ph+"1",qh+"1","1"+pl,"0"+ql)
find(ph+"0",qh+"1","1"+pl,"1"+ql)
elif(pxorq[l] == "0" and pxorq[255-l] == "1"):
find(ph+"0",qh+"0","1"+pl,"0"+ql)
find(ph+"0",qh+"1","0"+pl,"0"+ql)
find(ph+"1",qh+"0","1"+pl,"1"+ql)
find(ph+"1",qh+"1","0"+pl,"1"+ql)
elif(pxorq[l] == "0" and pxorq[255-l] == "0"):
find(ph+"0",qh+"0","0"+pl,"0"+ql)
find(ph+"1",qh+"0","0"+pl,"1"+ql)
find(ph+"0",qh+"1","1"+pl,"0"+ql)
find(ph+"1",qh+"1","1"+pl,"1"+ql)

p^(q>>kbits)

思路一

已知n=p*q和p^(q>>kbits)

从高位向低位搜索,p的高kbits位已知(与给出的p^(q>>kbits)值的高kbits位相同),那搜索就从p^(q>>kbits)的kbits位后开始,即p的第kbits位,q的第1位。

若p^(q>>kbits)搜索的当前位值为1,则可能为:p为1,q为0 或者 p为0,q为1;反之若p^(q>>kbits)当前位为0,则p为1,q为1 或者 p为0,q为0。(p,q指的都是对应位的)

剪枝条件:

将p和q剩下位全部填充为1,需要满足 p*q > n

将p和q剩下位全部填充为0,需要满足 p*q < n

要注意把p的已知的高kbits位加上

结束条件是

脚本:

n = 
xor =
kbits =
pbits =
xor = str(bin(xor)[2:])
ph = xor[:kbits]
qh = ''
xor = xor[kbits:]
qh = ''
def find(ph,qh):
l0 = len(ph)
l1 = len(qh)
tmp0 = ph + '0' * (pbits-l0)
tmp1 = ph + '1' * (pbits-l0)
tmq0 = qh + '0' * (pbits-l1)
tmq1 = qh + '1' * (pbits-l1)
if int(tmp0,2) * int(tmq0,2) > n:#剪枝条件1
return
if int(tmp1,2) * int(tmq1,2) < n:#剪枝条件2
return

if l0 == pbits:#结束条件
if n % int(ph,2) == 0:
print(f'p = {int(ph,2)}')
return

else:
if xor[l1] == '1':
find(ph+'0',qh+'1')
find(ph + '1',qh+'0')
else:
find(ph+'1',qh+'1')
find(ph + '0',qh+'0')
find(ph,qh)
思路二

因为异或后的高 kbits 位就是 P 的高 kbits 位。用 N 除以 P 后得到的就是 Q 的高位,再次利用 Q 的高位和 gift,可以求出 P 的 kbits ~ 2*kbits 位,以此类推来恢复 P、Q。

例如下面这个:

P = getPrime(512)
Q = getPrime(512)
N = P * Q
gift = P ^ (Q >> 16)


pbar = gift >>(512-16)

while True:
try:
qbar = (N>>(1024 - pbar.bit_length()*2))//pbar
qbar = qbar>>6
gifts = gift^(qbar<<(512-16-qbar.bit_length()))
pbar = gifts >> (512-16-qbar.bit_length())
except:
break

for i in range(64):
if N%((pbar<<6)+i) == 0:
p = (pbar<<6)+i
q = N//p
print("[+] p =",p)
print("[+] q =",q)
break

来源: 一些关于p^q问题剪枝算法 _

我一开始不明白右移6位这个6是怎么来的,下面那个左移的位数和上面那个右移的联系是啥

后面貌似想明白了

直接说结论吧:上面那个6取别的值也是可以的,不能大于kbits,下面那个值则是最后的pbits-kbits-qbar.bit_length()

也就是说这个脚本也可以写成这样:

n = 
xor =
kbits =
pbits =
pbar = xor >>(pbits-kbits)

while True:
try:
qbar = (n>>(n.bit_length() - pbar.bit_length()*2))//pbar
qbar = qbar>>kbits-1
xors = xor^(qbar<<(pbits-kbits-qbar.bit_length()))
pbar = xors >> (pbits-kbits-qbar.bit_length())
except:
break

for i in range(n.bit_length()//kbits):
if n%(pbar+i) == 0:
p =pbar+i
q = n//p
print("[+] p =",p)
print("[+] q =",q)
break

(更好套一点,原博客的别的题不能直接套)

例题

DASCTF 2023 & 0X401七月暑期挑战赛 ezRSA

from Crypto.Util.number import *
from secret import secret, flag
def encrypt(m):
return pow(m, e, n)
assert flag == b"dasctf{" + secret + b"}"
e = 11
p = getPrime(512)
q = getPrime(512)
n = p * q
P = getPrime(512)
Q = getPrime(512)
N = P * Q
gift = P ^ (Q >> 16)
print(N, gift, pow(n, e, N))
print(encrypt(bytes_to_long(secret)),
encrypt(bytes_to_long(flag)))

N = 75000029602085996700582008490482326525611947919932949726582734167668021800854674616074297109962078048435714672088452939300776268788888016125632084529419230038436738761550906906671010312930801751000022200360857089338231002088730471277277319253053479367509575754258003761447489654232217266317081318035524086377
gift = 8006730615575401350470175601463518481685396114003290299131469001242636369747855817476589805833427855228149768949773065563676033514362512835553274555294034
c1 = 14183763184495367653522884147951054630177015952745593358354098952173965560488104213517563098676028516541915855754066719475487503348914181674929072472238449853082118064823835322313680705889432313419976738694317594843046001448855575986413338142129464525633835911168202553914150009081557835620953018542067857943
c2 = 69307306970629523181683439240748426263979206546157895088924929426911355406769672385984829784804673821643976780928024209092360092670457978154309402591145689825571209515868435608753923870043647892816574684663993415796465074027369407799009929334083395577490711236614662941070610575313972839165233651342137645009
c3 = 46997465834324781573963709865566777091686340553483507705539161842460528999282057880362259416654012854237739527277448599755805614622531827257136959664035098209206110290879482726083191005164961200125296999449598766201435057091624225218351537278712880859703730566080874333989361396420522357001928540408351500991

P,Q的获取可用剪枝获得,得到之后可得n

from Crypto.Util.number import*
import gmpy2
N = 75000029602085996700582008490482326525611947919932949726582734167668021800854674616074297109962078048435714672088452939300776268788888016125632084529419230038436738761550906906671010312930801751000022200360857089338231002088730471277277319253053479367509575754258003761447489654232217266317081318035524086377
xor = 8006730615575401350470175601463518481685396114003290299131469001242636369747855817476589805833427855228149768949773065563676033514362512835553274555294034
c1 = 14183763184495367653522884147951054630177015952745593358354098952173965560488104213517563098676028516541915855754066719475487503348914181674929072472238449853082118064823835322313680705889432313419976738694317594843046001448855575986413338142129464525633835911168202553914150009081557835620953018542067857943
kbits = 16
pbits = 512
pbar = xor >>(pbits-kbits)
e=11

while True:
try:
qbar = (N>>(N.bit_length() - pbar.bit_length()*2))//pbar
qbar = qbar>>kbits-1
xors = xor^(qbar<<(pbits-kbits-qbar.bit_length()))
pbar = xors >> (pbits-kbits-qbar.bit_length())
except:
break

for i in range(N.bit_length()//kbits):
if N%(pbar+i) == 0:
p =pbar+i
q = N//p
d=gmpy2.invert(e,(p-1)*(q-1))
print("n=",pow(c1,d,N))
break
#n= 8410363083727227985204019150296233995423906412694890252698371563789022268553444336554986979907257458547381598181369620318848637391220240378808211998052306324620364339595355706922325759625785590466818309839146408927226283350419069859849879835884942537531811470537915106995685907400782213608736735862576031042

结果一看,发现求出的n结尾居然是2,并且长度只有1020位,所以可以想到是因为n>N导致结果-N,所以真实的n应该再加上N,验算位数正好是1024位,然后是Franklin-Reiter攻击解flag

from Crypto.Util.number import*

N = 75000029602085996700582008490482326525611947919932949726582734167668021800854674616074297109962078048435714672088452939300776268788888016125632084529419230038436738761550906906671010312930801751000022200360857089338231002088730471277277319253053479367509575754258003761447489654232217266317081318035524086377
n1= 8410363083727227985204019150296233995423906412694890252698371563789022268553444336554986979907257458547381598181369620318848637391220240378808211998052306324620364339595355706922325759625785590466818309839146408927226283350419069859849879835884942537531811470537915106995685907400782213608736735862576031042
n=n1+N
c2 = 69307306970629523181683439240748426263979206546157895088924929426911355406769672385984829784804673821643976780928024209092360092670457978154309402591145689825571209515868435608753923870043647892816574684663993415796465074027369407799009929334083395577490711236614662941070610575313972839165233651342137645009
c3 = 46997465834324781573963709865566777091686340553483507705539161842460528999282057880362259416654012854237739527277448599755805614622531827257136959664035098209206110290879482726083191005164961200125296999449598766201435057091624225218351537278712880859703730566080874333989361396420522357001928540408351500991

def GCD(a,b):
if b == 0:
return a.monic()
else:
return GCD(b,a % b)
PR.<x> = PolynomialRing(Zmod(n))
for i in range(50):
f1 = x ^ 11 - c2
f2 = (bytes_to_long(b'dasctf{' + b'\x00' * i + b'}') + 256 * x) ^ 11 - c3
if GCD(f1,f2)[0] != 1:
print(long_to_bytes(int(n - GCD(f1,f2)[0])))
#b'C0pper_Sm1th_Mak3s_T1ng5_Bet4er'

nextprime剪枝

类似于这题

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

def gen():
p = getPrime(512)
q = nextprime(int(str(p)[::-1]))
a = p&(10**6)
b = q%(10**6)
return p, q, a, b

p, q, a, b = gen()
n = p * q
e = 65537
c = pow(flag, e, n)

print('a =', a)
print('b =', b)
print('n =', n)
print('c =', c)

'''
a = 393792
b = 657587
n = 369861662178631957928245949770898273807810914183024109983427665445735262653137205864716795669578458854257293391751966280054435051199001813062804877555180285022533020724584142987601931508644551710279816508694942797383860411279321090090880184911983657558416043011288974643282183209071299542379616927857228873411
c = 71672159337725868237152385281240906653912972455699529924728534161923598107229667985188160316648005550663968313537597184857765083556920840254397949219027087098695062021613177519248117588571374167137534818793726427056016629301262986053118989821488864074425276527050110912787300008659230186841168308795745942580
'''

与普通首尾剪枝的区别在于这边逆序是十进制而普通的是二进制,且这题取逆序之后还进行了一次nextprime

如果能爆破出p的低6位,也就有了q的高6位,因为给的b泄露了q的低6位,n的低6位一定等于p*q的低6位,所以可以爆破得到p的低6位从而得到q的高6位。

#find plow
for i in range(1000000):
if(i*b % (10**6) == n % (10**6)):
plow = str(i)

qhigh = plow[::-1]

因为nextprime偏移很小一般不会超过2000,且q的低6位泄露,q是p十进制逆序的下一个值,所以q的低6位偏移不超过2000再倒序就是p的高6位,可以爆破得到。

爆破的依据是:

对可能的p高六位后面填充全0,与q高六位后面填充全0,乘积应小于n

对可能的p高六位后面填充全9,与q高六位后面填充全9,乘积应大于n

#find phigh
for i in range(2000):
phigh = str(b-i)[::-1]
if(int(phigh + "0" * (dec_len-6)) * int(qhigh + "0" * (dec_len-6)) < n and int(phigh + "9" * (dec_len-6)) * int(qhigh + "9" * (dec_len-6)) > n):
break

至此已知条件就成了已知p、q的高六位与低六位(十进制位)和p*q,并且p,q互为逆序,要还原p,q

搜索方式:从两端向中间搜索,每次搜索有10×10种可能

剪枝条件:

p,q未搜索到的位全填0,乘积应该小于n

p,q未搜索到的位全填9,乘积应该大于n

p,q低k位的乘积取低k位,应该和n的低k位相同

def dec(p,q,c,n):
phi = (p-1)*(q-1)
d = inverse(65537,phi)
print(long_to_bytes(pow(c,d,n)))

def find(ph,qh,pl,ql):
#条件1,2
padding0 = (dec_len-len(ph)-len(pl))*"0"
padding9 = (dec_len-len(ph)-len(pl))*"9"
if(int(qh+padding0+ql)*int(ph+padding0+pl) > n or int(qh+padding9+ql)*int(ph+padding9+pl) < n):
return

#条件三
mask = 10**len(pl)
if(int(ql)*int(pl) % mask != n % mask):
return

for i in range(10):
if(n % int(ph+str(i)+pl) == 0):
p = int(ph+str(i)+pl)
q = n // p
dec(p,q,c,n)
exit()

#search
for i in range(10):
for j in range(10):
find(ph + str(i), qh + str(j), str(j) + pl, str(i) + ql)

好难

到这步接下来就能得到flag了

剪枝这一块参照:

Crypto趣题-剪枝

Crypto-剪枝