huangx607087开学前的切题2 0.简介 一个密码学fw的切题笔记,很菜,别看
之前BUUCTF上到了3000分,进入了Crypto的深水区,题目难度都是很大的,看来我是不是只能看着wp搞懂题目后再做题了,wtcl。
由于题目太难(实际上是自己太菜),这篇文章原计划是12日至13日发布的,最后直到16日才开始写,然后火速发布(并没有)。
1.[BSidesSF2020]decrypto-1 通过观察下面的代码,可以发现,这个实际上是对一个文件进行了加密操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 import sysimport jsonimport hashlibclass Crypto : def __init__ (self, key ): if not isinstance (key, bytes ): raise TypeError('key must be of type bytes!' ) self .key = key self ._buf = bytes () self ._out = open ("/dev/stdout" , "wb" ) def _extend_buf (self ): self ._buf += self .key def get_bytes (self, nbytes ): while len (self ._buf) < nbytes: self ._extend_buf() ret, self ._buf = self ._buf[:nbytes], self ._buf[nbytes:] return ret def encrypt (self, buf ): if not isinstance (buf, bytes ): raise TypeError('buf must be of type bytes!' ) stream = self .get_bytes(len (buf)) return bytes (a ^ b for a, b in zip (buf, stream)) def set_outfile (self, fname ): self ._out = open (fname, "wb" ) def encrypt_file (self, fname ): buf = open (fname, "rb" ).read() self ._out.write(self .encrypt(buf))class JSONCrypto (Crypto ): def encrypt_file (self, fname ): buf = open (fname, "r" ).read().strip() h = hashlib.sha256(buf.encode('utf-8' )).hexdigest() data = { "filename" : fname, "hash" : h, "plaintext" : buf, } outbuf = json.dumps(data, sort_keys=True , indent=4 ) self ._out.write(self .encrypt(outbuf.encode("utf-8" )))def main (argv ): if len (argv) not in (3 , 4 ): print ("%s <key> <infile> [outfile]" % sys.argv[0 ]) return argv.pop(0 ) key = argv.pop(0 ) inf = argv.pop(0 ) crypter = JSONCrypto(key.encode("utf-8" )) if sys.argv: crypter.set_outfile(argv.pop(0 )) crypter.encrypt_file(inf)if __name__ == '__main__' : main(sys.argv)
而整个加密过程,实际上就是data和key之间异或的操作。根据平常的经验,key的长度应该不会很长。而密文的加密文档叫flag.txt.enc。因此盲猜filename那一栏的值是flag.txt。这样子的话,data的长度已经超过了15。
然后我们就可以构造一个data,模仿他的构造字符串的过程对data异或密文,可以看出最前面实际上出现了循环结构,说明这就是我们需要的key。n0t4=l4g
最后我们将key和data进行异或,就可以得到明文内容了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 from Crypto.Util.number import *import sysimport hashlibimport json fp=open ("flag.txt.enc" ,"rb" ) b=fp.read() fp.close() data = { "filename" : 'flag.txt' , "hash" : '000000000000000000000000000000000000000' , "plaintext" :'000000000000000000000000000000000000' } outbuf = json.dumps(data, sort_keys=True , indent=4 ) s="" for i in range (min (len (outbuf),len (b))): s+=chr (b[i]^ord (outbuf[i]))print (s) key=b'n0t4=l4g' ans='' for i in range (len (b)): ans+=chr (b[i]^key[i%len (key)])print (ans)''' { "filename": "flag.txt", "hash": "2f98b8afa014bf955533a3e72cee0417413ff744e25f2b5b5838f5741cd69547", "plaintext": "CTF{plz_dont_r0ll_ur_own_crypto}" } '''
2.[羊城杯 2020]GMC 拿到题目,很显然啊,看到了有随机数的平方,容易想到使用之前提到过的二次剩余的相关知识来解决。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 from Crypto.Util.number import getPrime,bytes_to_long,getRandomNBitIntegerfrom secret import flagfrom gmpy2 import gcddef gmc (a, p ): if pow (a, (p-1 )//2 , p) == 1 : return 1 else : return -1 def gen_key (): [gp,gq] = [getPrime(512 ) for i in range (2 )] gN = gp * gq return gN, gq, gpdef gen_x (gq,gp ): while True : x = getRandomNBitInteger(512 ) if gmc(x,gp) ^ gmc(x,gq) == -2 : return x def gen_y (gN ): gy_list = [] while len (gy_list) != F_LEN: ty = getRandomNBitInteger(768 ) if gcd(ty,gN) == 1 : gy_list.append(ty) return gy_listif __name__ == '__main__' : flag = bin (bytes_to_long(flag))[2 :] F_LEN = len (flag) N, q, p = gen_key() x = gen_x(q, p) y_list = gen_y(N) ciphertext = [] for i in range (F_LEN): tc = pow (y_list[i],2 ) * pow (x,int (flag[i])) % N ciphertext.append(tc) with open ('./output.txt' ,'w' ) as f: f.write(str (N) + '\n' ) for i in range (F_LEN): f.write(str (ciphertext[i]) + '\n' )
很显然,gmc函数就是判断一个数是不是二次剩余的函数,因为在有限域里,二次剩余的ord值一定是$\dfrac{p-1}2$的因数。因此如果$a$是有限域中的二次剩余,那么$a^{\frac{p-1}2} \equiv 1 \pmod p$。
不过这里判断的是$n=pq$的二次剩余情况。根据阅读代码,两个异或值等于$-2$的话,只有可能是一个$1$一个$-1$。而二次剩余的符号是可以相乘的。因此我们可以知道,生成的数字肯定不是二次剩余。
根据最后给出的数字:$y_i^2x^{f_i}$,其中$f_i$的取值为$0,1$。因此如果最后给出数字是二次剩余,那么对应的flag位是$0$,如果给出的数字是二次非剩余,那么对应的flag位是$1$。
这道题还算是比较简单的,直接上脚本,具体讲解可以回顾**0xGame Div 4[2020.10.29]**。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 from Crypto.Util.number import *def isqr (x,p ): if p==1 or x==1 : return 1 sgn=1 while x%2 ==0 : x=x//2 if p%8 ==3 or p%8 ==5 : sgn=-sgn if x<p: _tmp=p p=x x=_tmp if x%4 ==3 and p%4 ==3 : sgn=-sgn return sgn*isqr(x%p,p) n,s=0 ,[] fp=open ("output.txt" ,"r" ) n=int (fp.readline())while True : try : a=int (fp.readline()) s.append(a) except : break assert len (s)+1 ==304 ans="" for i in s: if (isqr(i,n)==1 ): ans+='0' else : ans+='1' print (long_to_bytes(int (ans,2 )))
3.[b01lers2020]Des-MMXX 首先看一下题目的加密代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 import sysfrom hashlib import sha256from Crypto.Cipher import DES SECRET = 0xa seed = b'secret_sauce_#9' def keygen (s ): keys = [] for i in range (2020 ): s = sha256(s).digest() keys.append(s) return keysdef scramble (s ): ret = "" .join( [format (s & 0xfffff , '020b' )]*101 ) ret += "" .join( [format (s >> 20 , '020b' )]*101 ) return int (ret, 2 )def encrypt (keys, msg ): dk = scramble(SECRET) for v in keys: idx = dk & 3 dk >>= 2 k = v[idx*8 :(idx+1 )*8 ] cp = DES.new(k, DES.MODE_CBC, bytes (8 )) msg = cp.encrypt(msg) return msg keys = keygen(seed)with open ("flag.txt" , "rb" ) as f: msg = f.read() ctxt = encrypt(keys, msg)print (ctxt)
通过题目给出的加密代码中,我们发现了最后还给出了一组附加的明文和密文,看来是有一定用处的。而SECRET虽然中间八位十六进制数被擦去,但给出了最高位的$\text{AH}$和最低位的$\text{EH}$($\text H$后缀表示十六进制)。
观察一下scramble函数,与secret密切相关,因此我构造了一个十位的十六进制数$\text{123456789AH}$放进去,得出来的结果是$\text{6789A6789A6789A……123451234512345H}$。而加密的时候,取这个数字作为dk,然后dk每次除以$4$并获取余数。余数作为对应keys密钥中的密钥段。
注意到加密的前半段和后半段基本无关,而题目又给出了一个附加明文和附加密文。很显然,可以使用中间碰撞法进行攻击,通过分别枚举两端SECRET的值,然后比对输出结果,在两组每组$60000$多个输出中寻找到相同的输出。
构造密钥直接模仿其构造密钥的过程即可。而最需要主义的就是对key进行下标关系的计算,以及对生成的半密钥除以多少(也就是需要右移多少位)的问题,反正都是线性函数,举两个例子就可以判断了。
枚举前半段加密密钥脚本(对附加明文加密到一半的操作)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 from Crypto.Util.number import *from Crypto.Cipher import DESfrom hashlib import sha256 m=b"Attack at DAWN!!" def keygen (s ): keys = [] for i in range (2020 ): s = sha256(s).digest() keys.append(s) return keysdef K (x ): ret = "" .join( [format (x & 0xfffff , '020b' )]*101 ) return int (ret,2 )def incrypt (kinum,cip,keys ): for i in range (1010 ): idx=(kinum>>(2 *i))&3 k=keys[i][idx*8 :idx*8 +8 ] cp = DES.new(k, DES.MODE_CBC, bytes (8 )) cip=cp.encrypt(cip) return cip keys = keygen(b'secret_sauce_#9' ) fp=open ("ENCMID.txt" ,"w" )for i in range (0 ,65536 ): if (i%150 ==0 ): print (i) keynum=K(0xa0000 +i) fp.write(hex (bytes_to_long(incrypt(keynum,m,keys)))[2 :]+'\n' )
枚举后半段加密密钥(对附加密文进行解密到一半的操作)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 from Crypto.Util.number import *from Crypto.Cipher import DESfrom hashlib import sha256 c=b"\x15\x08\x54\xff\x3c\xf4\xc4\xc0\xd2\x3b\xd6\x8a\x82\x34\x83\xbe" def keygen (s ): keys = [] for i in range (2020 ): s = sha256(s).digest() keys.append(s) return keysdef K (x ): ret = "" .join( [format (x & 0xfffff , '020b' )]*101 ) return int (ret,2 )def dicrypt (kinum,cip,keys ): for i in range (1010 ): idx=(kinum>>(2018 -2 *i))&3 k=keys[2019 -i][idx*8 :idx*8 +8 ] cp = DES.new(k, DES.MODE_CBC, bytes (8 )) cip=cp.decrypt(cip) return cip keys = keygen(b'secret_sauce_#9' ) fp=open ("DECMID.txt" ,"w" )for i in range (0 ,65536 ): if (i%150 ==0 ): print (i) keynum=K(i*16 +0xe ) fp.write(hex (bytes_to_long(dicrypt(keynum,c,keys)))[2 :]+'\n' )
在两组文件中寻找配对,大概需要用$20$秒左右。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 s=[] f=open ("ENCMID.txt" ,"r" )for i in range (65536 ): a=int (f.readline(),16 ) s.append(a) f.close()print (1 ) f=open ("DECMID.txt" ,"r" )for i in range (65536 ): b=int (f.readline(),16 ) if b in s: print (hex (i)) print (hex (s.index(b))) print (hex (b)) f.close()''' 0x3618 0x4d9e 0x8ac245ba53950c4fb8352bfe2639fcd9 '''
我们可以看出,加密ID是$\text{3618H}$,解密ID是$\text{4D9EH}$,所以我们恢复了最后的SECRET的值为$\text{A4D9E3618EH}$。然后简单修改一下解密脚本就可以得出flag了。不过重要的是注意下标计算的问题,比如最终解密脚本中的 $4038-2i$和$2019-i$是如何算的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 import sysfrom hashlib import sha256from Crypto.Cipher import DES SECRET=0xa4d9e3618e seed = b'secret_sauce_#9' def keygen (s ): keys = [] for i in range (2020 ): s = sha256(s).digest() keys.append(s) return keysdef scramble (s ): ret = "" .join( [format (s & 0xfffff , '020b' )]*101 ) ret += "" .join( [format (s >> 20 , '020b' )]*101 ) return int (ret, 2 )def decrypt (keys, msg ): dk = scramble(SECRET) for i in range (2020 ): idx = (dk>>(4038 -(2 *i)))&3 k = keys[2019 -i][idx*8 :(idx+1 )*8 ] cp = DES.new(k, DES.MODE_CBC, bytes (8 )) msg = cp.decrypt(msg) return msg keys = keygen(seed)with open ("flag.enc" , "rb" ) as f: msg = f.read() ctxt = decrypt(keys, msg)print (ctxt)
pctf{Two_tO_thE_s1xt33n7h?_E4sy-p3asy..}
4.[pasecactf_2019]tornado_casino 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 from sys import argvfrom random import getrandbits flag = '<redacted>' tornado_banner = ''' 88 ,d 88 88 88 MM88MMM ,adPPYba, 8b,dPPYba, 8b,dPPYba, ,adPPYYba, ,adPPYb,88 ,adPPYba, 88 a8" "8a 88P' "Y8 88P' `"8a "" `Y8 a8" `Y88 a8" "8a 88 8b d8 88 88 88 ,adPPPPP88 8b 88 8b d8 88, "8a, ,a8" 88 88 88 88, ,88 "8a, ,d88 "8a, ,a8" "Y888 `"YbbdP"' 88 88 88 `"8bbdP"Y8 `"8bbdP"Y8 `"YbbdP"' ''' casino_banner = ''' 88 "" ,adPPYba, ,adPPYYba, ,adPPYba, 88 8b,dPPYba, ,adPPYba, a8" "" "" `Y8 I8[ "" 88 88P\' `"8a a8" "8a 8b ,adPPPPP88 `"Y8ba, 88 88 88 8b d8 "8a, ,aa 88, ,88 aa ]8I 88 88 88 "8a, ,a8" `"Ybbd8"' `"8bbdP"Y8 `"YbbdP"\' 88 88 88 `"YbbdP"\' ''' tornado_art = ''' (( "####@@!!$$ )) `#####@@!$$` )) (( '####@!!$: (( ,####@!!$: )) .###@!!$: `##@@!$: `#@!!$ !@# `#@!$: @#$ #$ `#@!$: !@! '@!$: '`\ "!$: /`' '\ '!: /' "\ : /" -."-/\\\-."//.-"/:`\."-.JrS"."-=_\\ " -."-.\\"-."//.-".`-."_\\-.".-\".-//''' welcome = 'Welcome!\n[1] - To slotmachine\n[2] - Enter promocode\n[3] - Exit\n' '' def sltmchn_wndw (num ): print (num) return '|' + '|' .join(list (hex (num)[2 :].zfill(8 ))) + '|' slotmachine_menu = '[$] - $$$SPIN$$$\n' print (tornado_banner)print (casino_banner)print (tornado_art) user_balance = 10 promo = '' while True : choice1 = input (welcome) if choice1 == '1' : print ('$$$Its point of no return!$$$\n$$$ all or nothing $$$\n' ) print (f'Your balance: {user_balance} ' ) while True : if user_balance > 0 : spin = input (slotmachine_menu) if spin == '$' : state = getrandbits(32 ) try : pff_try = int (input ('It will be: ' ), 16 ) except : exit(0 ) if pff_try == state: print (sltmchn_wndw(state)) print ('OMGWTF$$$$$$$$$$$$' ) print (flag) exit(0 ) else : print (sltmchn_wndw(state)) print ('Nice try!' ) user_balance -= 1 print (f'Your balance: {user_balance} ' ) else : exit(0 ) else : print ('Sorry!' ) exit(0 ) elif choice1 == '2' : if not promo: promo = input ('Enter your promocode: ' ) if promo == 'b33_1_4m_b3333' : print ('Great!' ) user_balance += 1000 else : print ('Only once!' ) elif choice1 == '3' : exit(0 )
观察加密脚本可以发现,很容易地发现getrandbits(32)
。而根据之前做题经验,这个就是让你预测MT19937。
首先上一下自己做的一个MT19937的工具包
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 def _int32 (x ): return int (0xFFFFFFFF & x)class MT19937 : def __init__ (self, seed ): self .mt = [0 ] * 624 self .mt[0 ] = seed self .mti = 0 for i in range (1 , 624 ): self .mt[i] = _int32(1812433253 * (self .mt[i - 1 ] ^ self .mt[i - 1 ] >> 30 ) + i) def extract_number (self ): if self .mti == 0 : self .twist() y = self .mt[self .mti] y = y ^ y >> 11 y = y ^ y << 7 & 2636928640 y = y ^ y << 15 & 4022730752 y = y ^ y >> 18 self .mti = (self .mti + 1 ) % 624 return _int32(y) def twist (self ): for i in range (0 , 624 ): y = _int32((self .mt[i] & 0x80000000 ) + (self .mt[(i + 1 ) % 624 ] & 0x7fffffff )) self .mt[i] = (y >> 1 ) ^ self .mt[(i + 397 ) % 624 ] if y % 2 != 0 : self .mt[i] = self .mt[i] ^ 0x9908b0df def inverse_right (res, shift, mask=0xffffffff , bits=32 ): tmp = res for i in range (bits // shift): tmp = res ^ tmp >> shift & mask return tmpdef inverse_left (res, shift, mask=0xffffffff , bits=32 ): tmp = res for i in range (bits // shift): tmp = res ^ tmp << shift & mask return tmpdef extract_number (y ): y = y ^ y >> 11 y = y ^ y << 7 & 2636928640 y = y ^ y << 15 & 4022730752 y = y ^ y >> 18 return y&0xffffffff def recover (y ): y = inverse_right(y,18 ) y = inverse_left(y,15 ,4022730752 ) y = inverse_left(y,7 ,2636928640 ) y = inverse_right(y,11 ) return y&0xffffffff
然后我们根据题目的提示,获取$624$个内部状态就可以预测随机数了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 from pwn import *from MT19937 import *from mt19937predictor import MT19937Predictordef getnumber (s29 ): assert len (s29)==29 s=s29[13 ]+s29[15 ]+s29[17 ]+s29[19 ]+s29[21 ]+s29[23 ]+s29[25 ]+s29[27 ] return int (s,16 ) sh=remote("node3.buuoj.cn" ,29601 ) str1=sh.recvuntil('[3] - Exit' ) sh.sendline('2' ) str1=sh.recvuntil('Enter your promocode:' ) sh.sendline('b33_1_4m_b3333' ) str1=sh.recvuntil('[3] - Exit' ) sh.sendline('1' ) S=[]for i in range (624 ): if (i%31 ==0 ): print (i) str1=sh.recvuntil('N$$$' ) sh.sendline('$' ) sh.sendline('10000000' ) str1=sh.recvline(keepends=False ) str2=sh.recvline(keepends=False ) S.append(getnumber(str2)) Q=[] R=[]for i in S: Q.append(recover(i)) B=MT19937(0 )assert len (Q)==624 B.mt=Qfor i in range (10 ): print (i,hex (extract_number(B.mt[i]))) predictor = MT19937Predictor()for i in range (624 ): predictor.setrandbits(S[i],32 ) Y2=predictor.getrandbits(32 )print ('ans=' ,hex (Y2)) sh.interactive()
当然,也可以使用专门的包用来预测,这个包网上可以通过pip下载下来。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 from pwn import *from mt19937predictor import MT19937Predictordef getnumber (s29 ): assert len (s29)==29 s=s29[13 ]+s29[15 ]+s29[17 ]+s29[19 ]+s29[21 ]+s29[23 ]+s29[25 ]+s29[27 ] return int (s,16 ) sh=remote("node3.buuoj.cn" ,29601 ) str1=sh.recvuntil('[3] - Exit' ) sh.sendline('2' ) str1=sh.recvuntil('Enter your promocode:' ) sh.sendline('b33_1_4m_b3333' ) str1=sh.recvuntil('[3] - Exit' ) sh.sendline('1' ) S=[]for i in range (624 ): if (i%31 ==0 ): print (i) str1=sh.recvuntil('N$$$' ) sh.sendline('$' ) sh.sendline('10000000' ) str1=sh.recvline(keepends=False ) str2=sh.recvline(keepends=False ) S.append(getnumber(str2)) predictor = MT19937Predictor()for i in range (624 ): predictor.setrandbits(S[i],32 ) ans=predictor.getrandbits(32 ) sh.sendline('$' ) sh.sendline(hex (ans)[2 :]) sh.interactive()
5.[羊城杯 2020]RRRRRRSA 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 import hashlibimport sympyfrom Crypto.Util.number import * flag = 'GWHT{************}' flag1 = flag[:19 ].encode() flag2 = flag[19 :].encode()assert (len (flag) == 38 ) P1 = getPrime(1038 ) P2 = sympy.nextprime(P1)assert (P2 - P1 < 1000 ) Q1 = getPrime(512 ) Q2 = sympy.nextprime(Q1) N1 = P1 * P1 * Q1 N2 = P2 * P2 * Q2 E1 = getPrime(1024 ) E2 = sympy.nextprime(E1) m1 = bytes_to_long(flag1) m2 = bytes_to_long(flag2) c1 = pow (m1, E1, N1) c2 = pow (m2, E2, N2) output = open ('secret' , 'w' ) output.write('N1=' + str (N1) + '\n' ) output.write('c1=' + str (c1) + '\n' ) output.write('E1=' + str (E1) + '\n' ) output.write('N2=' + str (N2) + '\n' ) output.write('c2=' + str (c2) + '\n' ) output.write('E2=' + str (E2) + '\n' ) output.close()
这道题中,$n$具有$n=p^2q$的形式,并且构成$n_1,n_2$中的$p,q$都是非常非常接近的(就是相邻两个素数的关系)不过,$p$和$q$的位数相差较大。
Insert:关于连分数逼近的问题 在之前RSA的笔记中,我们使用的WienerAttack的原理就是连分数逼近,所以我们这里补充介绍一下勒让德定理:、
勒让德定理 :假设$x\in R_+$,那么在$x$的连分数逼近展开中,如果有$|x-\dfrac a b|<\dfrac 1 {2b^2}$,其中$\gcd (a,b)=1$,那么可以认为$\dfrac ab$是$x$的连分数展开中的其中一项的值。
我们来回顾一下RSA中wienerattack的过程:
由$ed\equiv 1 \pmod \phi$,我们可以得到$ed-k\phi=1$。两边同时除以$d\phi$,那么就有$|\dfrac e \phi-\dfrac k d |=\dfrac 1 {d\phi}$。
又因为$n-\phi=p+q-1<3\sqrt n$。
所以根据勒让德定理,我们需要证明$|\dfrac e \phi-\dfrac k d |<\dfrac 1 {2d^2}$,下面简单地给出证明::
左边分式通分,得 $$ |\dfrac {ed-nk}{dn}| $$ 分母凑一个$k\phi$,可以有 $$ |\dfrac {ed-k\phi-nk+k\phi}{dn}| $$ 又因为$ed-k\phi=1$,那么分子化简得 $$ |\dfrac {1-k(n-\phi)}{dn}| $$ 这个数字是小于$\dfrac{3k\sqrt n}{dn}=\dfrac{3k}{d\sqrt n}$。因此我们根据前面的几个条件,可以推出最后的结果小于$\dfrac{1}{2d^2}$。所以,在整个连分数逼近的过程中,中间的结果必然准确覆盖了$\dfrac k d$的值。
回到上面这道题,由于$p$是$1024$位,$p^2$是$2048$位,$q$是$512$位,比$2048$位小得多。而$p_1,p_2$、$q_1,q_2$是两组非常接近的数字,考虑到$p$更大,因此,当我们对$\dfrac {n_1} {n_2}$进行连分数逼近时,一定会准确覆盖到$\dfrac{p_1^2}{p_2^2}$或者$\dfrac {q_1}{q_2}$。
下面上一下解题过程,其中solve
函数是连分数展开的过程,simplify
函数和getmidfrac
可以计算g
数组中所涉及的所有的分数,以分子和分母形成的数对形式表现。最后枚举这个数对即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 from Crypto.Util.number import *from gmpy2 import *from given import *def solve (a, b ): g=[] while (a!=1 ): g.append(a//b) a,b=b,a%b g.append(b) return gdef simplify (x ): n,d=0 ,1 for i in x[::-1 ]: n,d=d,i*d+n return n,ddef getmidfrac (x ): r=[] for i in range (1 ,len (x)): r.append(simplify(x[:i])) return r g=solve(N1,N2) g=getmidfrac(g) q1,q2=None ,None for (a,b) in g: if b==0 : continue if N1%b==0 and b-1 and b-N1: q1=b q2=a break p1,p2=iroot(N1//q1,2 )[0 ],iroot(N2//q2,2 )[0 ] phi1,phi2=p1*(p1-1 )*(q1-1 ),p2*(p2-1 )*(q2-1 ) d1,d2=inverse(E1,phi1),inverse(E2,phi2)print (long_to_bytes(pow (c1,d1,N1)))print (long_to_bytes(pow (c2,d2,N2)))