题目
一个level1目录,内部是很多公钥文件,一个level2压缩包带密码,密码应该在level1解密拿到
level1 有一个README,看一下内容
我们需要先恢复message内容
有两个python脚本,看名称一个是生成message,另一个是加密
message 先看generate-plaintexts.py,分析一下原始明文的内容
这里我们可以得知share的格式是模数d : 余数k : 原始比特长度
1 2 3 d = 一个素数 k = S mod d original_bits = 原消息长度
但是仔细看脚本里的Asmuth-Bloom并没有真正做标准的Asmuth-Bloom重构条件,本质上只是把秘密整数 S 拆成了很多个
也就是说后面只要拿到足够多组(di, ki),就能直接用中国剩余定理把S还原出来
往后看生成的消息和share,先把message1.txt的内容被复制到 10 个plaintext文件的开头
message2到message10每个message都被分成10份share,分别放进对应的plaintext-xxx.txt
加密分析 继续看encrypt.py分析加密方式
当rsa模数位数大于等于2048时,先随机生成AES key,再RSA-OAEP加密这个AES key,然后AES-GCM 加密正文
当 RSA 模数位数小于2048时,直接RSA加密AES key,再AES-GCM加密正文
现在我们的思路就是,先找出多个能被破解的RSA公钥,解开对应的ciphertext,拿到多条share,再对每个message用CRT重建
公钥分析 先看一下所有公钥的特征
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 key-0.pem 4096 bits, e=65537 key-1.pem 2048 bits, e=65537 key-2.pem 2047 bits, e=65537 key-3.pem 2047 bits, e=65537 key-4.pem 2566 bits, e=65537 key-5.pem 198 bits, e=3 key-6.pem 2047 bits, e=一个超大数 key-7.pem 2047 bits, e=65537 key-8.pem 2048 bits, e=65537 key-9.pem 4095 bits, e=65537 key-10.pem 4095 bits, e=65537 key-11.pem 4096 bits, e=65537 key-12.pem 2047 bits, e=一个超大数 key-13.pem 2048 bits, e=65537 key-14.pem 4095 bits, e=65537 key-15.pem 2565 bits, e=65537 key-16.pem 2048 bits, e=65537 key-17.pem 2047 bits, e=一个超大数 key-18.pem 2048 bits, e=65537 key-19.pem 4096 bits, e=65537
1、key-5.pem很小,可以直接分解
2、有几对大小相同,可能存在共因子或者相近素数
3、key-6、key-12、key-17 的e很大
两两求gcd(key 1、2、4、15) 编写脚本批量
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 from math import gcdfrom pathlib import Pathimport argparsefrom Crypto.PublicKey import RSAdef load_keys (key_dir: Path ): keys = [] for pem in sorted (key_dir.glob("key-*.pem" ), key=lambda p: int (p.stem.split("-" )[1 ])): idx = int (pem.stem.split("-" )[1 ]) key = RSA.import_key(pem.read_bytes()) keys.append({ "idx" : idx, "path" : pem, "n" : key.n, "e" : key.e, "bits" : key.n.bit_length(), }) return keys def main (): key_dir = Path("level1" ).resolve() keys = load_keys(key_dir) if not keys: raise SystemExit(f"No key-*.pem found in {key_dir} " ) hits = [] for i in range (len (keys)): for j in range (i + 1 , len (keys)): k1 = keys[i] k2 = keys[j] g = gcd(k1["n" ], k2["n" ]) if g == 1 : continue q1 = k1["n" ] // g q2 = k2["n" ] // g hits.append((k1, k2, g, q1, q2)) print (f"[!] shared factor found: key-{k1['idx' ]} .pem <-> key-{k2['idx' ]} .pem" ) print (f" gcd bits : {g.bit_length()} " ) print (f" q1 bits : {q1.bit_length()} " ) print (f" q2 bits : {q2.bit_length()} " ) print (f" check #1 : {g * q1 == k1['n' ]} " ) print (f" check #2 : {g * q2 == k2['n' ]} " ) print () if not hits: print ("[-] no non-trivial gcd found" ) return print ("[+] summary" ) for k1, k2, g, q1, q2 in hits: print ( f" key-{k1['idx' ]} / key-{k2['idx' ]} -> " f"shared {g.bit_length()} bits, others {q1.bit_length()} / {q2.bit_length()} bits" ) if __name__ == "__main__" : main()
共有两组存在共因子,接下来通过gcd恢复私钥
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 from math import gcdfrom pathlib import Pathfrom Crypto.PublicKey import RSAdef load_public_keys (level1_dir: Path ): keys = {} for pem in sorted (level1_dir.glob("key-*.pem" ), key=lambda p: int (p.stem.split("-" )[1 ])): idx = int (pem.stem.split("-" )[1 ]) keys[idx] = RSA.import_key(pem.read_bytes()) return keys def main (): root = Path(__file__).resolve().parent level1_dir = root / "level1" out_dir = root / "recovered_keys_from_gcd" out_dir.mkdir(exist_ok=True ) keys = load_public_keys(level1_dir) indices = sorted (keys) recovered = {} print (f"[+] scanning {len (indices)} public keys for shared factors" ) print () for i in range (len (indices)): for j in range (i + 1 , len (indices)): a = indices[i] b = indices[j] n1 = keys[a].n n2 = keys[b].n g = gcd(n1, n2) if g == 1 : continue print (f"[!] shared factor found: key-{a} .pem <-> key-{b} .pem" ) print (f" gcd bits = {g.bit_length()} " ) for idx, other in ((a, b), (b, a)): p = int (g) q = int (keys[idx].n // g) phi = (p - 1 ) * (q - 1 ) d = pow (keys[idx].e, -1 , phi) priv = RSA.construct((keys[idx].n, keys[idx].e, d, p, q)) recovered[idx] = { "p" : p, "q" : q, "d" : d, "other" : other, "priv" : priv, } out_path = out_dir / f"key-{idx} -private.pem" out_path.write_bytes(priv.export_key()) print (f" [OK] recovered key-{idx} .pem using gcd with key-{other} .pem" ) print (f" wrote {out_path.name} " ) print () print ("[+] summary" ) if not recovered: print (" no gcd-vulnerable keys found" ) return for idx in sorted (recovered): info = recovered[idx] print ( f" key-{idx} .pem: p_bits={info['p' ].bit_length()} " f"q_bits={info['q' ].bit_length()} d_bits={info['d' ].bit_length()} " ) if __name__ == "__main__" : main()
成功恢复4个私钥,解密得到两份plaintext
小模数直接因式分解(key 5) 直接写脚本分解
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 88 89 90 91 92 93 94 95 96 from pathlib import Pathfrom Crypto.PublicKey import RSAfrom sympy import factorintdef load_public_keys (level1_dir: Path ): keys = {} for pem in sorted (level1_dir.glob("key-*.pem" ), key=lambda p: int (p.stem.split("-" )[1 ])): idx = int (pem.stem.split("-" )[1 ]) keys[idx] = RSA.import_key(pem.read_bytes()) return keys def main (): root = Path(__file__).resolve().parent level1_dir = root / "level1" out_dir = root / "recovered_keys_small_modulus" out_dir.mkdir(exist_ok=True ) keys = load_public_keys(level1_dir) print ("[+] scanning for small RSA moduli" ) print () recovered = {} for idx in sorted (keys): key = keys[idx] bits = key.n.bit_length() if bits > 256 : continue print (f"[+] trying key-{idx} .pem ({bits} bits)" ) factors_dict = factorint(key.n) factors = [] for prime, exp in factors_dict.items(): factors.extend([int (prime)] * exp) prod = 1 for f in factors: prod *= f if prod != key.n: print (" [-] factorization check failed" ) print () continue phi = 1 for f in factors: phi *= (f - 1 ) try : d = pow (key.e, -1 , phi) except ValueError: print (" [-] e is not invertible modulo phi" ) print () continue recovered[idx] = { "factors" : factors, "d" : d, } out_path = out_dir / f"key-{idx} -private-info.txt" lines = [ f"key = key-{idx} .pem" , f"n_bits = {bits} " , f"e = {key.e} " , f"d = {d} " , f"factor_count = {len (factors)} " , "factors =" , ] lines.extend(str (f) for f in factors) out_path.write_text("\n" .join(lines) + "\n" ) print (f" [OK] factors found: {len (factors)} prime factors" ) print (f" [OK] wrote {out_path.name} " ) print () print ("[+] summary" ) if not recovered: print (" no small modulus key was factored" ) return for idx in sorted (recovered): info = recovered[idx] bit_list = ", " .join(str (f.bit_length()) for f in info["factors" ]) print (f" key-{idx} .pem -> factor bits [{bit_list} ]" ) if __name__ == "__main__" : main()
1 2 3 4 5 6 7 8 n_bits = 198 e = 3 d = 223190228465821694677907677521533419817411950213806180649643 factor_count = 3 factors = 1528159911436197621287 15950473204537742369 13734854446830088769
但是这一步得到的结果一个都解不出来,应该是误导
Fermat分解(key 3、7、13、18) 我们之前公钥分析的时候看到了有几组2047/2048位模数,很像正常双素数RSA的大小,尝试Fermat分解
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 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 from math import isqrtfrom pathlib import Pathfrom Crypto.PublicKey import RSAdef load_public_keys (level1_dir: Path ): keys = {} for pem in sorted (level1_dir.glob("key-*.pem" ), key=lambda p: int (p.stem.split("-" )[1 ])): idx = int (pem.stem.split("-" )[1 ]) keys[idx] = RSA.import_key(pem.read_bytes()) return keys def fermat_factor (n, max_steps=200000 ): a = isqrt(n) if a * a < n: a += 1 for step in range (max_steps + 1 ): b2 = a * a - n b = isqrt(b2) if b * b == b2: p = int (a - b) q = int (a + b) if p * q == n: return p, q, step a += 1 return None def main (): root = Path(__file__).resolve().parent level1_dir = root / "level1" out_dir = root / "recovered_keys_fermat" out_dir.mkdir(exist_ok=True ) keys = load_public_keys(level1_dir) print ("[+] scanning keys with Fermat factorization" ) print () recovered = {} for idx in sorted (keys): key = keys[idx] bits = key.n.bit_length() if bits < 512 : continue print (f"[+] trying key-{idx} .pem ({bits} bits)" ) result = fermat_factor(key.n) if not result: print (" [-] not factorable within step limit" ) print () continue p, q, step = result phi = (p - 1 ) * (q - 1 ) try : d = pow (key.e, -1 , phi) except ValueError: print (" [-] e is not invertible modulo phi" ) print () continue priv = RSA.construct((key.n, key.e, d, p, q)) pem_path = out_dir / f"key-{idx} -private.pem" info_path = out_dir / f"key-{idx} -info.txt" pem_path.write_bytes(priv.export_key()) info_path.write_text( "\n" .join([ f"key = key-{idx} .pem" , f"n_bits = {bits} " , f"method = Fermat" , f"step = {step} " , f"p_bits = {p.bit_length()} " , f"q_bits = {q.bit_length()} " , f"d_bits = {d.bit_length()} " , f"p = {p} " , f"q = {q} " , f"d = {d} " , ]) + "\n" ) recovered[idx] = {"step" : step, "p" : p, "q" : q, "d" : d} print (f" [OK] factorable by Fermat at step {step} " ) print (f" [OK] wrote {pem_path.name} " ) print () print ("[+] summary" ) if not recovered: print (" no key was factored by Fermat" ) return for idx in sorted (recovered): info = recovered[idx] print ( f" key-{idx} .pem -> step={info['step' ]} " f"p_bits={info['p' ].bit_length()} q_bits={info['q' ].bit_length()} " ) if __name__ == "__main__" : main()
成功恢复4个私钥,解密得到3份plaintext
中国剩余定理恢复message 到这一步我们已经拿到了5份plaintext
内容和我们之前分析的一致,对应的列数分别如下
先把share按message对齐
对每条message做CRT恢复
level1密码
得到密码:9Zr4M1ThwVCHe4nHnmOcilJ8
level2 用密码解压后得到
level3仍然是个带密码的压缩包,先来看这个脚本
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 import gmpy2import hashlibfrom Crypto.Util.number import *def generate_polynomial_coefficients (degree, bound=100 ): coefficients = [] for i in range (degree + 1 ): coefficients.append(getRandomRange(-bound, bound)) while coefficients[-1 ] == 0 : coefficients[-1 ] = getRandomRange(-bound, bound) return coefficients def evaluate_polynomial (coeffs, x, n ): result = 0 power = 1 for i, coeff in enumerate (coeffs): result = (result + coeff * power) % n power = (power * x) % n return result p = getPrime(512 ) q = getPrime(512 ) d = getPrime(180 ) lam = (p-1 ) * (q-1 ) // gmpy2.gcd(p-1 , q-1 ) e = gmpy2.invert(d, lam) n = p * q m1 = bytes_to_long(b"Secret message: " + b"A" * 16 ) poly2_coeffs = generate_polynomial_coefficients(2 ) poly3_coeffs = generate_polynomial_coefficients(3 ) m2 = evaluate_polynomial(poly2_coeffs, m1, n) m3 = evaluate_polynomial(poly3_coeffs, m1, n) c1 = pow (m1, e, n) c2 = pow (m2, e, n) c3 = pow (m3, e, n) flag_hash = hashlib.sha256(str (p + q).encode()).hexdigest() flag = f"next pass is {flag_hash} " poly2_coeffs_display = poly2_coeffs.copy() poly2_coeffs_display[0 ] = "?" poly3_coeffs_display = poly3_coeffs.copy() poly3_coeffs_display[0 ] = "?" print ("n =" , n)print ("e =" , e)print ("c1 =" , c1)print ("c2 =" , c2)print ("c3 =" , c3)print ("poly2_coeffs =" , poly2_coeffs_display) print ("poly3_coeffs =" , poly3_coeffs_display)
task.py分析
1、生成512-bit的p,q
2、生成一个180-bit的私钥指数d
3、构造公钥:lam = lcm(p-1, q-1);e = invert(d, lam)
4、计算c1、c2、c3
最终密码是sha256(str(p + q).encode()).hexdigest(),也就是说固定为sha256(str(p + q)),我们只需要分解n得到p、q即可
这道题首先想到从c1、c2、c3和多项式关系入手,但是多项式常数项被隐藏了,e也不是小指数,不一定能够快速分解,继续往下看
d只有180位,而n有1024位,那么n^(1/4) ≈ 2^256,d远远小于这个量级,也就是小私钥RSA,这里我们很容易联想到Wiener攻击
但这里并不是标准的Wiener,正常e为invert(d, phi(n))
标准Wiener推导很多时候默认的是ed - 1 = k * φ(n)
这里变成了ed - 1 = k * λ(n)
也就是说ed - 1是λ(n)的倍数,不一定是φ(n)的倍数
所以普通版Wiener套公式可能不直接命中,但小私钥的问题还是存在
概括一下思路:
1、先把它认成小私钥RSA,也就是Wiener家族问题
2、对e/n做连分数展开,拿到一串收敛分数候选
3、因为这里用的是λ(n)而不是φ(n),所以不能死套标准Wiener判定,而是做一个小修正:
为什么拿到ed-1的倍数就能分解n呢,因为如果拿到了某个数M是λ(n)的倍数,那么对于任意和n互素的a:a^M≡1modn
这就和RSA/群阶有关,然后可以像Miller-Rabin/平方根分解那样,把M写成:M=2^s*t
不断平方,寻找非平凡平方根,从而求出:gcd(x-1,n)
最后把n拆开,所以只要从小私钥候选里拿到一个正确的ed-1,就能把n因式分解
关键信息有:n、e、d很小、flag为sha256(str(p+q))
修正版Wiener分解p,q 编写脚本
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 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 from math import gcdfrom pathlib import Pathimport jsonimport randomN = 99573363048275234764231402769464116416087010014992319221201093905687439933632430466067992037046120712199565250482197004301343341960655357944577330885470918466007730570718648025143561656395751518428630742587023267450633824636936953524868735263666089452348466018195099471535823969365007120680546592999022195781 E = 12076830539295193533033212232487568888200963123024189287629493480058638222146972496110814372883829765692623107191129306190788976704250502316265439996891764101447017190377014980293589797403095249538391534986638973035285900867548420192211241163778919028921502305790979880346050428839102874086046622833211913299 def rational_to_contfrac (x: int , y: int ): out = [] while y: a = x // y out.append(a) x, y = y, x - a * y return out def convergents_from_contfrac (frac ): n0, d0 = 0 , 1 n1, d1 = 1 , 0 for a in frac: n2 = a * n1 + n0 d2 = a * d1 + d0 yield n2, d2 n0, d0, n1, d1 = n1, d1, n2, d2 def factor_from_lambda_multiple (n: int , m: int , trials: int = 80 ): if m <= 0 : return None t = m s = 0 while t % 2 == 0 : t //= 2 s += 1 if s == 0 : return None bases = [2 , 3 , 5 , 7 , 11 , 13 , 17 , 19 , 23 , 29 ] bases += [random.randrange(2 , n - 1 ) for _ in range (trials)] for a in bases: g = gcd(a, n) if 1 < g < n: return g, n // g x = pow (a, t, n) if x == 1 or x == n - 1 : continue for _ in range (s): y = pow (x, 2 , n) if y == 1 : p = gcd(x - 1 , n) if 1 < p < n: return p, n // p break if y == n - 1 : break x = y return None def main (): root = Path(__file__).resolve().parent out_json = root / "step5_level2_factor_result.json" out_txt = root / "step5_level2_factor_result.txt" frac = rational_to_contfrac(E, N) print ("[+] running modified Wiener attack on level2" ) print (f"[+] convergents: {len (frac)} " ) print () small_divisors = [2 , 4 , 6 , 8 , 10 , 12 , 14 , 16 , 18 , 20 , 22 , 24 , 26 , 28 , 30 , 32 ] checked = 0 for idx, (k, q_candidate) in enumerate (convergents_from_contfrac(frac), start=1 ): if q_candidate.bit_length() > 220 : break for g in small_divisors: if q_candidate % g != 0 : continue d = q_candidate // g if d <= 0 : continue m = E * d - 1 checked += 1 fac = factor_from_lambda_multiple(N, m) if not fac: continue p, q = fac if p * q != N: continue if p > q: p, q = q, p result = { "method" : "modified_wiener_lambda" , "convergent_index" : idx, "small_divisor_g" : g, "d" : str (d), "d_bits" : d.bit_length(), "p" : str (p), "q" : str (q), "p_bits" : p.bit_length(), "q_bits" : q.bit_length(), "n" : str (N), "e" : str (E), } out_json.write_text(json.dumps(result, ensure_ascii=False , indent=2 ) + "\n" ) out_txt.write_text( "\n" .join([ "[level2 factor result]" , f"method = {result['method' ]} " , f"convergent_index = {idx} " , f"small_divisor_g = {g} " , f"d_bits = {d.bit_length()} " , f"p_bits = {p.bit_length()} " , f"q_bits = {q.bit_length()} " , f"p = {p} " , f"q = {q} " , ]) + "\n" ) print (f"[OK] found factors at convergent #{idx} , g={g} " ) print (f" d bits = {d.bit_length()} " ) print (f" p bits = {p.bit_length()} " ) print (f" q bits = {q.bit_length()} " ) print (f"[+] wrote {out_json} " ) print (f"[+] wrote {out_txt} " ) return print (f"[-] not found after checking {checked} candidates" ) if __name__ == "__main__" : main()
1 2 3 4 5 6 7 8 method = modified_wiener_lambda convergent_index = 108 small_divisor_g = 4 d_bits = 180 p_bits = 512 q_bits = 512 p = 9107629749578703007521753435128188255997152813554367948373413785978810344628038189563213036161837882971963700600044412434325055960560499599730973768232229 q = 10932961240863053140021227333995503042196506533433133299916502568289020826078008576745192725272664686384293526720057562101790406102729886343233171385110689
成功分解p、q
从p,q计算下一层密码 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 from pathlib import Pathimport jsonimport hashlibdef main (): root = Path(__file__).resolve().parent in_json = root / "step5_level2_factor_result.json" out_txt = root / "step5_level2_next_pass.txt" if not in_json.exists(): raise SystemExit(f"Missing input file: {in_json} " ) result = json.loads(in_json.read_text()) p = int (result["p" ]) q = int (result["q" ]) s = p + q digest = hashlib.sha256(str (s).encode()).hexdigest() password = f"next pass is {digest} " out_txt.write_text( "\n" .join([ "[level2 next pass]" , f"p_plus_q = {s} " , f"sha256(str(p+q)) = {digest} " , password, ]) + "\n" ) print (f"[+] p + q = {s} " ) print (f"[+] sha256(str(p+q)) = {digest} " ) print (password) print (f"[+] wrote {out_txt} " ) if __name__ == "__main__" : main()
level2密码 1 2 3 [+] p + q = 20040590990441756147542980769123691298193659346987501248289916354267831170706046766308405761434502569356257227320101974536115462063290385942964145153342918 [+] sha256(str(p+q)) = 2aa9c360df99cbb4209e4dbab5a9f9ffd86d34906e3206fecfdabf0bb7aeb5ac next pass is 2aa9c360df99cbb4209e4dbab5a9f9ffd86d34906e3206fecfdabf0bb7aeb5ac
得到密码:2aa9c360df99cbb4209e4dbab5a9f9ffd86d34906e3206fecfdabf0bb7aeb5ac
level3 解压得到level3脚本
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 getPrime, bytes_to_longp = getPrime(1536 ) q = getPrime(1536 ) n = p * q e = 65537 flag = b"dart{**************}" c = pow (bytes_to_long(flag), e, n) CONST1 = 0xDEADBEEFCAFEBABE123456789ABCDEFFEDCBA9876543210 CONST2 = 0xCAFEBABEDEADBEEF123456789ABCDEF0123456789ABCDEF CONST3 = 0x123456789ABCDEFFEDCBA9876543210FEDCBA987654321 MOD128 = 2 ** 128 MASK64 = (1 << 64 ) - 1 MASK128 = (1 << 128 ) - 1 leak = ( (p * CONST1) ^ (q * CONST2) ^ ((p & q) << 64 ) ^ ((p | q) << 48 ) ^ ((p ^ q) * CONST3) ) + ((p + q) % MOD128) ^ ((p * q) & MASK64) print (f"n = {n} " )print (f"e = {e} " )print (f"c = {c} " )print (f"leak = {leak} " )
task.py分析 这一层题目给了个leak:
1 leak = ( (p * CONST1) ^ (q * CONST2) ^ ((p & q) << 64 ) ^ ((p | q) << 48 ) ^ ((p ^ q) * CONST3) ) + ((p + q) % MOD128) ^ ((p * q) & MASK64)
而且我们已知:n=p*q
这意味着n给了p*q的所有低位约束,leak又给了你p,q混合后的额外低位约束
这类题很适合做从低位开始,一位一位枚举p,q的bit,逐步筛掉错误分支,也就是bitlifting/low-bitreconstruction
假设已经猜出了p,q的低k位,那么可以检查p*q mod 2^k是否等于n mod 2^k
也可以检查leak的低k位是否匹配,如果都匹配就保留这个候选,如果不匹配就裁掉,由于这题约束很强,实际会很快收敛到唯一一组p,q
我们的思路就很清晰了,从低位恢复 p,q,计算d = e^{-1} mod (p-1)(q-1)、m = c^d mod n
得到最终 flag
bit-lifting分解p、q 编写bit-lifting脚本
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 88 89 90 91 92 93 94 from pathlib import Pathimport jsonMASK64 = (1 << 64 ) - 1 def main (): root = Path(__file__).resolve().parent in_json = root / "step6_level3_params.json" out_json = root / "step6_level3_factor_result.json" out_txt = root / "step6_level3_factor_result.txt" params = json.loads(in_json.read_text()) n = int (params["n" ]) leak = int (params["leak" ]) A = int (params["const1" ]) B = int (params["const2" ]) C = int (params["const3" ]) target = leak ^ (n & MASK64) def t_low (p: int , q: int , k: int ): mask = (1 << k) - 1 x = ((p * A) ^ (q * B) ^ ((p & q) << 64 ) ^ ((p | q) << 48 ) ^ ((p ^ q) * C)) s = (p + q) & ((1 << 128 ) - 1 ) return (x + s) & mask cands = {(1 , 1 )} print ("[+] starting low-bit lifting" ) for k in range (1 , 1536 ): mask = (1 << (k + 1 )) - 1 target_n = n & mask target_t = target & mask nxt = set () for p, q in cands: for bp in (0 , 1 ): for bq in (0 , 1 ): pp = p | (bp << k) qq = q | (bq << k) if (pp * qq) & mask != target_n: continue if t_low(pp, qq, k + 1 ) != target_t: continue nxt.add((pp, qq)) cands = nxt if not cands: raise SystemExit(f"No candidates left at bit {k + 1 } " ) if len (cands) > 10000 : raise SystemExit(f"Too many candidates at bit {k + 1 } : {len (cands)} " ) if (k + 1 ) % 128 == 0 or k < 20 : print (f" bits {k + 1 } : {len (cands)} candidate(s)" ) if len (cands) != 1 : raise SystemExit(f"Expected exactly 1 final candidate, got {len (cands)} " ) p, q = next (iter (cands)) if p > q: p, q = q, p if p * q != n: raise SystemExit("Recovered factors do not multiply back to n" ) result = { "method" : "bit_lifting_from_n_and_leak" , "p" : str (p), "q" : str (q), "p_bits" : p.bit_length(), "q_bits" : q.bit_length(), } out_json.write_text(json.dumps(result, ensure_ascii=False , indent=2 ) + "\n" ) out_txt.write_text( "\n" .join([ "[level3 factor result]" , f"method = {result['method' ]} " , f"p_bits = {p.bit_length()} " , f"q_bits = {q.bit_length()} " , f"p = {p} " , f"q = {q} " , ]) + "\n" ) print (f"[OK] recovered p and q" ) print (f"[+] wrote {out_json} " ) print (f"[+] wrote {out_txt} " ) if __name__ == "__main__" : main()
1 2 3 4 5 method = bit_lifting_from_n_and_leak p_bits = 1536 q_bits = 1536 p = 1707409731376314795999673399644896831773113631916193464259935767498639233398656041693151647684636824933980722820855222725690297941643517419906221825861730073489224554305559179283922396783611098752937409830400941648419260598819028494552512482509512236956144966960506482426135682006340832164470103583395459735070745893411476234089404558564875229768662461905874150673437533528191205114624917667791799816966017830043080586780620851038224028348188524334138215456616749 q = 2141573345627585371553787251648053741410445905256360451856340437573603641142767425814601119975790360922588103737873793493614076837288293103777795188569218362732735616305950754442774259470221411601014586798147206055920156749954034989669601788173864983031343224818275175942690789067112461738865540083192521838275489406927966810543155749231403489582462847315255827362973792259727757243033997062234932720733059810639405260209480382634232650958389211775287627610180991
成功分解
RSA解密 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 from pathlib import Pathimport jsonfrom Crypto.Util.number import long_to_bytesdef main (): root = Path(__file__).resolve().parent params_json = root / "step6_level3_params.json" factors_json = root / "step6_level3_factor_result.json" out_txt = root / "step6_level3_flag.txt" params = json.loads(params_json.read_text()) factors = json.loads(factors_json.read_text()) n = int (params["n" ]) e = int (params["e" ]) c = int (params["c" ]) p = int (factors["p" ]) q = int (factors["q" ]) phi = (p - 1 ) * (q - 1 ) d = pow (e, -1 , phi) m = pow (c, d, n) flag = long_to_bytes(m).decode("utf-8" ) out_txt.write_text( "\n" .join([ "[level3 flag]" , f"phi_bits = {phi.bit_length()} " , f"d_bits = {d.bit_length()} " , f"flag = {flag} " , ]) + "\n" ) print (flag) print (f"[+] wrote {out_txt} " ) if __name__ == "__main__" : main()
FLAG
得到flag:dart{379c9308-e9a8-45a1-bd55-45bbd822e86d}