安洵杯2021_Crypto_复现

little_trick

k e y w o r d s : keywords: keywords: python_random模块RSA dp&dq泄露(~a|b) & (a|~b)逻辑式运算

d p , d q 泄 露 dp,dq泄露 dp,dq

经典的关于dp,dq泄露,是已知RSA加密的dp,dq,p,q,c

其中dp,dq分别满足
d p ≡ d ( m o d ( p − 1 ) ) d q ≡ d ( m o d ( q − 1 ) ) dp \equiv d\pmod {(p-1)}\\ dq \equiv d\pmod {(q-1)} dpd(mod(p1))dqd(mod(q1))
如何从以上已知条件推导出明文m呢?

要把以上两个同余式尽量等价起来

公式推导


{ m p ≡ c d p ( m o d p ) m q ≡ c d q ( m o d q ) ⇒ { m p ≡ c d ( m o d ( p − 1 ) ) ( m o d p ) m q ≡ c d ( m o d ( q − 1 ) ) ( m o d q ) \begin{cases} m_p\equiv c^{dp}\pmod p\\ m_q \equiv c^{dq}\pmod q \end{cases} \Rightarrow \begin{cases} m_p \equiv c^{d\pmod{(p-1)}}\pmod p\\ m_q \equiv c^{d\pmod {(q-1)}}\pmod q \end{cases} {mpcdp(modp)mqcdq(modq){mpcd(mod(p1))(modp)mqcd(mod(q1))(modq)
欧拉定理 a ϕ ( m ) ≡ 1 ( m o d m ) a^{\phi(m)}\equiv 1\pmod m aϕ(m)1(modm),其中 a a a m m m互素;所以 ϕ ( p ) = p − 1 \phi(p)=p-1 ϕ(p)=p1,所以 c p − 1 ≡ 1 ( m o d p ) c^{p-1}\equiv 1\pmod p cp11(modp)
{ m p ≡ c d ( m o d ( p − 1 ) ) ( m o d p ) m q ≡ c d ( m o d ( q − 1 ) ) ( m o d q )    ⟺    { m p ≡ c d + k 1 ′ ⋅ ( p − 1 ) ( m o d p ) m q ≡ c d + k 2 ′ ⋅ ( q − 1 ) ( m o d q )    ⟺    { m p ≡ c d ( m o d p ) m q ≡ c d ( m o d q ) \begin{cases} m_p \equiv c^{d\pmod{(p-1)}}\pmod p\\ m_q \equiv c^{d\pmod {(q-1)}}\pmod q \end{cases} \iff \begin{cases} m_p \equiv c^{d+k_1'\cdot (p-1)}\pmod p \\ m_q \equiv c^{d+k_2'\cdot (q-1)}\pmod q \end{cases} \iff \begin{cases} m_p \equiv c^{d}\pmod p \\ m_q \equiv c^{d}\pmod q \end{cases} {mpcd(mod(p1))(modp)mqcd(mod(q1))(modq){mpcd+k1(p1)(modp)mqcd+k2(q1)(modq){mpcd(modp)mqcd(modq)

那么经过推导得到** m p , m q m_p,m_q mp,mq相当于明文m在分别模p,q的情况下的值**

这样我们就可以以c^d为中间量,在两个同余式之间建立一个等式

将同余式转换为普通等式
m p + k 1 ⋅ p = m q + k 2 ⋅ q ⇒ k 1 ⋅ p = m q − m p + k 2 ⋅ q m_p+k_1\cdot p=m_q+k_2\cdot q\\ \Rightarrow k_1\cdot p = m_q-m_p + k_2\cdot q mp+k1p=mq+k2qk1p=mqmp+k2q
现在等式两边有两个未知数 k 1 , k 2 k_1,k_2 k1,k2,但是可以把其中一个未知数化作求余运算,这样就可以计算等式一边的值
k 1 ⋅ p ≡ m q − m p ( m o d q ) ⇒ k 1 ≡ p − 1 ⋅ ( m q − m p ) ( m o d q ) k_1\cdot p \equiv m_q-m_p \pmod q\\ \Rightarrow k_1\equiv p^{-1} \cdot(m_q-m_p)\pmod q k1pmqmp(modq)k1p1(mqmp)(modq)
其中 p − 1 p^{-1} p1 p p p对模数 q q q的逆元,也就是 p ⋅ p − 1 ≡ 1 ( m o d q ) p\cdot p^{-1}\equiv 1\pmod q pp11(modq)

这样我们能够计算出 k 1 k_1 k1的大小,就可以进而代回之前的等式(经过推导得到 m p ≡ c d p ≡ c d ( m o d p ) m_p\equiv c^{dp}\equiv c^{d}\pmod p mpcdpcd(modp)
m p ≡ c d ( m o d p ) ⇒ c d = m p + k 1 ⋅ p m_p\equiv c^{d}\pmod p\\ \Rightarrow c^{d}=m_p+k_1\cdot p mpcd(modp)cd=mp+k1p

可计算得到c^d,再计算 c d ( m o d ( p ⋅ q ) ) c^d \pmod {(p\cdot q)} cd(mod(pq)),得到真实的明文m

实现脚本

from Crypto.Util.number import long_to_bytes
import gmpy2
p = ...
q = ...
dp = ...
dq = ...
c = ...
inv_p = gmpy2.invert(p,q)
mp = pow(c,dp,p)
mq = pow(c,dq,q)
m = (((mq - mp) * inv_p) % q) + mp
m = m % (p * q)
print(long_to_bytes(m).decode())

P r o b l e m Problem Problem

from Crypto.Util.number import sieve_base, bytes_to_long, getPrime
import random
import gmpy2
import os

flag = b'D0g3{}'
flag = bytes_to_long(flag)
p = getPrime(1024)
q = getPrime(1024)
n = p * q
e = gmpy2.next_prime(bytes_to_long(os.urandom(3)))
c = gmpy2.powmod(flag,e,n)
print(p)
print(q)
print(c)

dp = ''
seeds = []
for i in range(0,len(dp)):
    seeds.append(random.randint(0,99))
print(seeds)

result = []
for j in range(0,len(dp)):
    random.seed(seeds[j])
    rands = []
    for k in range(0,4):
        rands.append(random.randint(0,99))
    result.append((~ord(dp[j])|rands[j%4]) & (ord(dp[j])|~rands[j%4]))
    del rands[j%4]
    print(rands)
print(result)


dq = ''
C = []
E = 0x10001
list_p = sieve_base[0:len(dq)]
list_q = sieve_base[len(dq):2*len(dq)]
for l in range(0,len(dq)):
    P = list_p[l]
    Q = list_q[l]
    C.append(pow(int(dq[l]),E,P*Q))
print(C)

A n a l y s i s Analysis Analysis

在对flag的加密使用的标准的RSA,但是没有给出e,反而给出了p,q;根据之后脚本对dp,dq的倒腾,可以判断是先恢复dp,dq,再进而套用RSA dp&dq泄露的脚本即可

在恢复dp的过程中,重点是

result.append((~ord(dp[j])|rands[j%4]) & (ord(dp[j])|~rands[j%4]))

可以把ta简单看作是一个逻辑运算等式
( ¬ a ∨ b ) ∧ ( a ∨ ¬ b ) (\lnot a \vee b)\land (a\vee \lnot b) (¬ab)(a¬b)
其中 ∨ \vee 的运算优先度高于 ∧ \land ,所以在我看来不好化简,既然只有两个输入值,那么我们就把真值表弄出来看看ta有什么特殊的性质

a a a b b b ( ¬ a ∨ b ) ∧ ( a ∨ ¬ b ) (\lnot a \vee b)\land (a\vee \lnot b) (¬ab)(a¬b)
0 0 0 0 0 0 1 1 1
0 0 0 1 1 1 0 0 0
1 1 1 0 0 0 0 0 0
1 1 1 1 1 1 1 1 1

通过观察可以发现,只要 a , b a,b a,b不同得到的结果即为 0 0 0,相同则为 1 1 1

有意思的是如果我们将逻辑式得到的结果作为 a a a或者 b b b再进行一次逻辑式运算,得到的结果就和最初的 a a a或者 b b b相同

也就是说
设 f ( a , b ) = ( ¬ a ∨ b ) ∧ ( a ∨ ¬ b ) , 则 f ( f ( a , b ) , b ) = a f ( a , f ( a , b ) ) = b 设f(a,b)=(\lnot a \vee b)\land (a\vee \lnot b),则\\ f(f(a,b),b) = a\\ f(a,f(a,b)) = b f(a,b)=(¬ab)(a¬b)f(f(a,b),b)=af(a,f(a,b))=b
PS:后来发现这就是异或的性质,查阅了异或的逻辑表达式
a ⊕ b = ( ¬ a ∧ b ) ∨ ( ¬ b ∧ a ) a \oplus b=(\lnot a\land b)\vee(\lnot b \land a) ab=(¬ab)(¬ba)
本题所使用的逻辑运算式实际上就是 ¬ ( a ⊕ b ) \lnot(a\oplus b) ¬(ab)

那么根据

for j in range(0,len(dp)):
    random.seed(seeds[j])
    rands = []
    for k in range(0,4):
        rands.append(random.randint(0,99))
    result.append((~ord(dp[j])|rands[j%4]) & (ord(dp[j])|~rands[j%4]))
    del rands[j%4]
    print(rands)

我们只需要生成与脚本加密过程中相同的rands序列,实际上也就是逻辑式运算中的b

再把第一轮经过逻辑式运算的结果作为新的a,继续进行一轮逻辑式运算就可以得到原始的a,也就是dp

关于生成rands序列,题目中使用的确认seed,使得每次生成的随机数一样(random模块采用的是伪随机数发生器),比如说

>>> import random
>>> random.seed(50)
>>> random.randint(0,99)
63
>>> random.seed(50)
>>> random.randint(0,99)
63

题目中给出了seeds种子序列,我们只需要重复一样的脚本设置即可

先默认使用的是python3进行恢复dp,但是发现最后得到dp是一串奇怪的字符串,再对比了题目给出的非关键序列部分的rands中的随机数,python3调用random.seed()所生成的rands是不同于题目的(也是为什么题目要把非关键部分的rands打印出来);既然python3生成的不同,就试试python2random模块

得到正确结果

result1 = [-38, -121, -40, -125, -51, -29, -2, -21, -59, -54, -51, -40, -105, -5, -4, -50, -127, -56, -124, -128, -23, -104, -63, -112, -34, -115, -58, -99, -24, -102, -1, -5, -34, -3, -104, -103, -21, -62, -121, -24, -115, -9, -87, -56, -39, -30, -34, -4, -33, -5, -114, -21, -19, -7, -119, -107, -115, -6, -25, -27, -32, -62, -28, -20, -60, -121, -102, -10, -112, -7, -85, -110, -62, -100, -110, -29, -41, -55, -113, -112, -45, -106, -125, -25, -57, -27, -83, -2, -51, -118, -2, -10, -50, -40, -1, -82, -111, -113, -50, -48, -23, -33, -112, -38, -29, -26, -4, -40, -123, -4, -44, -120, -63, -38, -41, -22, -50, -50, -17, -122, -61, -5, -100, -22, -44, -47, -125, -125, -127, -55, -117, -100, -2, -26, -32, -111, -123, -118, -16, -24, -20, -40, -92, -40, -102, -49, -99, -45, -59, -98, -49, -13, -62, -128, -121, -114, -112, -13, -3, -4, -26, -35, -15, -35, -8, -18, -125, -14, -6, -60, -113, -104, -120, -64, -104, -55, -104, -41, -34, -106, -105, -2, -28, -14, -58, -128, -3, -1, -17, -38, -18, -12, -59, -4, -19, -82, -40, -122, -18, -42, -53, -60, -113, -40, -126, -15, -63, -40, -124, -114, -58, -26, -35, -26, -8, -48, -112, -52, -11, -117, -52, -32, -21, -38, -124, -13, -103, -6, -30, -33, -28, -31, -1, -97, -59, -64, -28, -1, -40, -2, -10, -26, -24, -3, -50, -113, -125, -122, -124, -5, -50, -62, -11, -8, -88, -109, -7, -31, -105, -54, -28, -8, -62, -58, -101, -58, -53, -124, -18, -124, -17, -109, -52, -45, -40, -109, -85, -7, -108, -121, -58, -49, -91, -102, -8, -10, -17, -55, -19, -11, -116, -47, -120, -121, -23, -99, -19, -51, -36, -110, -126, -29, -110, -9, -97, -54, -83, -86]
seeds = [3, 0, 39, 78, 14, 49, 73, 83, 55, 48, 30, 28, 23, 16, 54, 23, 68, 7, 20, 8, 98, 68, 45, 36, 97, 13, 83, 68, 16, 59, 81, 26, 51, 45, 36, 60, 36, 94, 58, 11, 19, 33, 95, 12, 60, 38, 51, 95, 21, 3, 38, 72, 47, 80, 7, 20, 26, 80, 18, 43, 92, 4, 64, 93, 91, 12, 86, 63, 46, 73, 89, 5, 91, 17, 88, 94, 80, 42, 90, 14, 45, 53, 91, 16, 28, 81, 62, 63, 66, 20, 81, 3, 43, 99, 54, 22, 2, 27, 2, 62, 88, 99, 78, 25, 76, 49, 28, 96, 95, 57, 94, 53, 32, 58, 32, 72, 89, 15, 4, 78, 89, 74, 86, 45, 51, 65, 13, 75, 95, 42, 20, 77, 34, 66, 56, 20, 26, 18, 28, 11, 88, 62, 72, 27, 74, 42, 63, 76, 82, 97, 75, 92, 1, 5, 20, 78, 46, 85, 81, 54, 64, 87, 37, 91, 38, 39, 1, 90, 61, 28, 13, 60, 37, 90, 87, 15, 78, 91, 99, 58, 62, 73, 70, 56, 82, 5, 19, 54, 76, 88, 4, 3, 55, 3, 3, 22, 85, 67, 98, 28, 32, 42, 48, 96, 69, 3, 83, 48, 26, 20, 45, 16, 45, 47, 92, 0, 54, 4, 73, 8, 31, 38, 3, 10, 84, 60, 59, 69, 64, 91, 98, 73, 81, 98, 9, 70, 44, 44, 24, 95, 83, 49, 31, 19, 89, 18, 20, 78, 86, 95, 83, 23, 42, 51, 95, 80, 48, 46, 88, 7, 47, 64, 55, 4, 62, 37, 71, 75, 98, 67, 98, 58, 66, 70, 24, 58, 56, 44, 11, 78, 1, 78, 89, 97, 83, 72, 98, 12, 41, 33, 14, 40, 27, 5, 18, 35, 25, 31, 69, 97, 84, 47, 25, 90, 78, 15, 72, 71]
ls_dp = []
for j in range(len(seeds)):
    random.seed(seeds[j])
    rands = []
    for k in range(0,4):
        rands.append(random.randint(0,99))
    ls_dp.append((~result1[j]|rands[j%4]) & (result1[j]|~rands[j%4]))
    print(rands,end="")
print("".join([chr(i) for i in ls_dp]))
# 23458591381644494879596426183878928641891759871602961070839457303969747353773411708437315165237216481430908369709167907047043280248152040749469402814146054871536032870746473649690743697560576735624528397398691515920649222501258921802372365480019200479555430922883680472732415240714991623845227274793947921407

那么再恢复dq

dq = ''
C = []
E = 0x10001
list_p = sieve_base[0:len(dq)]
list_q = sieve_base[len(dq):2*len(dq)]
for l in range(0,len(dq)):
    P = list_p[l]
    Q = list_q[l]
    C.append(pow(int(dq[l]),E,P*Q))
print(C)

dq的加密过程就简单了许多,其中sieve_base是由 2 2 2 104729 104729 104729的素数构成的一个序列

所以实际上我们已知了PQ以及给出的CE,那么解pow(dp[l],E,P*Q)还不容易吗

# 恢复dq
E = 0x10001
C = [1, 0, 7789, 1, 17598, 20447, 15475, 23040, 41318, 23644, 53369, 19347, 66418, 5457, 0, 1, 14865, 97631, 6459, 36284, 79023, 1, 157348, 44667, 185701, 116445, 23809, 220877, 0, 1, 222082, 30333, 55446, 207442, 193806, 149389, 173229, 349031, 152205, 1, 149157, 196626, 1, 222532, 10255, 46268, 171536, 0, 351788, 152678, 0, 172225, 109296, 0, 579280, 634746, 1, 668942, 157973, 1, 17884, 662728, 759841, 450490, 0, 139520, 157015, 616114, 199878, 154091, 1, 937462, 675736, 53200, 495985, 307528, 1, 804492, 790322, 463560, 520991, 436782, 762888, 267227, 306436, 1051437, 384380, 505106, 729384, 1261978, 668266, 1258657, 913103, 935600, 1, 1, 401793, 769612, 484861, 1024896, 517254, 638872, 1139995, 700201, 308216, 333502, 0, 0, 401082, 1514640, 667345, 1015119, 636720, 1011683, 795560, 783924, 1269039, 5333, 0, 368271, 1700344, 1, 383167, 7540, 1490472, 1484752, 918665, 312560, 688665, 967404, 922857, 624126, 889856, 1, 848912, 1426397, 1291770, 1669069, 0, 1709762, 130116, 1711413, 1336912, 2080992, 820169, 903313, 515984, 2211283, 684372, 2773063, 391284, 1934269, 107761, 885543, 0, 2551314, 2229565, 1392777, 616280, 1368347, 154512, 1, 1668051, 0, 2453671, 2240909, 2661062, 2880183, 1376799, 0, 2252003, 1, 17666, 1, 2563626, 251045, 1593956, 2215158, 0, 93160, 0, 2463412, 654734, 1, 3341062, 3704395, 3841103, 609968, 2297131, 1942751, 3671207, 1, 1209611, 3163864, 3054774, 1055188, 1, 4284662, 3647599, 247779, 0, 176021, 3478840, 783050, 4613736, 2422927, 280158, 2473573, 2218037, 936624, 2118304, 353989, 3466709, 4737392, 2637048, 4570953, 1473551, 0, 0, 4780148, 3299784, 592717, 538363, 2068893, 814922, 2183138, 2011758, 2296545, 5075424, 1814196, 974225, 669506, 2756080, 5729359, 4599677, 5737886, 3947814, 4852062, 1571349, 4123825, 2319244, 4260764, 1266852, 1, 3739921, 1, 5948390, 1, 2761119, 2203699, 1664472, 3182598, 6269365, 5344900, 454610, 495499, 6407607, 1, 1, 476694, 4339987, 5642199, 1131185, 4092110, 2802555, 0, 5323448, 1103156, 2954018, 1, 1860057, 128891, 2586833, 6636077, 3136169, 1, 3280730, 6970001, 1874791, 48335, 6229468, 6384918, 5412112, 1, 7231540, 7886316, 2501899, 8047283, 2971582, 354078, 401999, 6427168, 4839680, 1, 44050, 3319427, 0, 1, 1452967, 4620879, 5525420, 5295860, 643415, 5594621, 951449, 1996797, 2561796, 6707895, 7072739]
list_p = sieve_base[0:len(C)]
list_q = sieve_base[len(C):2*len(C)]
for i in range(len(C)):
    P = list_p[i]
    Q = list_q[i]
    phi_N = (P - 1) * (Q - 1)
    D = gmpy2.invert(E,phi_N)
    print(pow(C[i],D,P*Q),end="")
# 104137587579880166582178434901328539485184135240660490271571544307637817287517428663992284342411864826922600858353966205614398977234519495034539643954586905495941906386407181383904043194285771983919780892934288899562700746832428876894943676937141813284454381136254907871626581989544814547778881240129496262777

最后dp,dq成功恢复,直接套之前的RSA dp&dq泄露脚本即可

S o l v i n g   c o d e Solving~code Solving code

import random
from Crypto.Util.number import *
import gmpy2


p = 119494148343917708105807117614773529196380452025859574123211538859983094108015678321724495609785332508563534950957367289723559468197440246960403054020452985281797756117166991826626612422135797192886041925043855329391156291955066822268279533978514896151007690729926904044407542983781817530576308669792533266431
q = 125132685086281666800573404868585424815247082213724647473226016452471461555742194042617318063670311290694310562746442372293133509175379170933514423842462487594186286854028887049828613566072663640036114898823281310177406827049478153958964127866484011400391821374773362883518683538899757137598483532099590137741
c = 10238271315477488225331712641083290024488811710093033734535910573493409567056934528110845049143193836706122210303055466145819256893293429223389828252657426030118534127684265261192503406287408932832340938343447997791634435068366383965928991637536875223511277583685579314781547648602666391656306703321971680803977982711407979248979910513665732355859523500729534069909408292024381225192240385351325999798206366949106362537376452662264512012770586451783712626665065161704126536742755054830427864982782030834837388544811172279496657776884209756069056812750476669508640817369423238496930357725842768918791347095504283368032

# # 恢复dp
# result1 = [-38, -121, -40, -125, -51, -29, -2, -21, -59, -54, -51, -40, -105, -5, -4, -50, -127, -56, -124, -128, -23, -104, -63, -112, -34, -115, -58, -99, -24, -102, -1, -5, -34, -3, -104, -103, -21, -62, -121, -24, -115, -9, -87, -56, -39, -30, -34, -4, -33, -5, -114, -21, -19, -7, -119, -107, -115, -6, -25, -27, -32, -62, -28, -20, -60, -121, -102, -10, -112, -7, -85, -110, -62, -100, -110, -29, -41, -55, -113, -112, -45, -106, -125, -25, -57, -27, -83, -2, -51, -118, -2, -10, -50, -40, -1, -82, -111, -113, -50, -48, -23, -33, -112, -38, -29, -26, -4, -40, -123, -4, -44, -120, -63, -38, -41, -22, -50, -50, -17, -122, -61, -5, -100, -22, -44, -47, -125, -125, -127, -55, -117, -100, -2, -26, -32, -111, -123, -118, -16, -24, -20, -40, -92, -40, -102, -49, -99, -45, -59, -98, -49, -13, -62, -128, -121, -114, -112, -13, -3, -4, -26, -35, -15, -35, -8, -18, -125, -14, -6, -60, -113, -104, -120, -64, -104, -55, -104, -41, -34, -106, -105, -2, -28, -14, -58, -128, -3, -1, -17, -38, -18, -12, -59, -4, -19, -82, -40, -122, -18, -42, -53, -60, -113, -40, -126, -15, -63, -40, -124, -114, -58, -26, -35, -26, -8, -48, -112, -52, -11, -117, -52, -32, -21, -38, -124, -13, -103, -6, -30, -33, -28, -31, -1, -97, -59, -64, -28, -1, -40, -2, -10, -26, -24, -3, -50, -113, -125, -122, -124, -5, -50, -62, -11, -8, -88, -109, -7, -31, -105, -54, -28, -8, -62, -58, -101, -58, -53, -124, -18, -124, -17, -109, -52, -45, -40, -109, -85, -7, -108, -121, -58, -49, -91, -102, -8, -10, -17, -55, -19, -11, -116, -47, -120, -121, -23, -99, -19, -51, -36, -110, -126, -29, -110, -9, -97, -54, -83, -86]
# seeds = [3, 0, 39, 78, 14, 49, 73, 83, 55, 48, 30, 28, 23, 16, 54, 23, 68, 7, 20, 8, 98, 68, 45, 36, 97, 13, 83, 68, 16, 59, 81, 26, 51, 45, 36, 60, 36, 94, 58, 11, 19, 33, 95, 12, 60, 38, 51, 95, 21, 3, 38, 72, 47, 80, 7, 20, 26, 80, 18, 43, 92, 4, 64, 93, 91, 12, 86, 63, 46, 73, 89, 5, 91, 17, 88, 94, 80, 42, 90, 14, 45, 53, 91, 16, 28, 81, 62, 63, 66, 20, 81, 3, 43, 99, 54, 22, 2, 27, 2, 62, 88, 99, 78, 25, 76, 49, 28, 96, 95, 57, 94, 53, 32, 58, 32, 72, 89, 15, 4, 78, 89, 74, 86, 45, 51, 65, 13, 75, 95, 42, 20, 77, 34, 66, 56, 20, 26, 18, 28, 11, 88, 62, 72, 27, 74, 42, 63, 76, 82, 97, 75, 92, 1, 5, 20, 78, 46, 85, 81, 54, 64, 87, 37, 91, 38, 39, 1, 90, 61, 28, 13, 60, 37, 90, 87, 15, 78, 91, 99, 58, 62, 73, 70, 56, 82, 5, 19, 54, 76, 88, 4, 3, 55, 3, 3, 22, 85, 67, 98, 28, 32, 42, 48, 96, 69, 3, 83, 48, 26, 20, 45, 16, 45, 47, 92, 0, 54, 4, 73, 8, 31, 38, 3, 10, 84, 60, 59, 69, 64, 91, 98, 73, 81, 98, 9, 70, 44, 44, 24, 95, 83, 49, 31, 19, 89, 18, 20, 78, 86, 95, 83, 23, 42, 51, 95, 80, 48, 46, 88, 7, 47, 64, 55, 4, 62, 37, 71, 75, 98, 67, 98, 58, 66, 70, 24, 58, 56, 44, 11, 78, 1, 78, 89, 97, 83, 72, 98, 12, 41, 33, 14, 40, 27, 5, 18, 35, 25, 31, 69, 97, 84, 47, 25, 90, 78, 15, 72, 71]
# tmp = "[54, 36, 60] [84, 42, 25] [20, 38, 39] [81, 9, 92] [70, 65, 94] [6, 11, 75] [27, 50, 46] [49, 85, 8] [95, 14, 73] [54, 71, 30] [53, 28, 65] [11, 13, 59] [94, 89, 8] [36, 41, 44] [91, 13, 48] [92, 94, 89] [94, 74, 90] [32, 65, 7] [90, 68, 90] [22, 96, 12] [83, 35, 5] [74, 74, 90] [27, 48, 33] [32, 98, 95] [80, 37, 84] [25, 68, 84] [49, 85, 37] [74, 94, 74] [48, 41, 44] [22, 94, 2] [50, 45, 38] [74, 20, 20] [50, 16, 82] [27, 8, 33] [32, 98, 91] [30, 57, 26] [98, 95, 91] [54, 28, 43] [58, 20, 94] [45, 55, 92] [78, 52, 51] [57, 81, 27] [76, 51, 53] [47, 65, 66] [57, 26, 80] [63, 72, 6] [24, 50, 82] [76, 51, 99] [68, 63, 47] [23, 36, 60] [63, 42, 6] [7, 59, 98] [43, 45, 34] [27, 70, 95] [32, 15, 7] [90, 68, 76] [20, 20, 60] [27, 70, 95] [18, 66, 19] [3, 69, 14] [56, 55, 58] [23, 39, 15] [47, 63, 92] [91, 49, 56] [17, 68, 16] [47, 66, 14] [79, 3, 31] [44, 29, 90] [39, 58, 85] [27, 56, 46] [8, 60, 14] [62, 74, 79] [17, 68, 16] [52, 96, 28] [39, 18, 62] [54, 12, 28] [54, 70, 95] [63, 27, 22] [20, 9, 58] [10, 70, 65] [48, 8, 33] [61, 45, 71] [8, 17, 16] [36, 48, 41] [13, 59, 17] [50, 55, 38] [92, 17, 23] [44, 29, 90] [43, 24, 44] [90, 76, 90] [50, 45, 38] [23, 54, 36] [69, 14, 46] [40, 17, 24] [91, 13, 48] [95, 14, 2] [94, 5, 8] [64, 95, 19] [95, 94, 8] [92, 17, 97] [18, 90, 62] [40, 17, 24] [81, 9, 73] [37, 92, 84] [95, 20, 29] [6, 11, 75] [11, 13, 17] [37, 90, 39] [51, 99, 53] [4, 1, 51] [54, 12, 43] [61, 89, 45] [21, 30, 90] [58, 64, 94] [7, 21, 90] [7, 59, 98] [60, 99, 14] [96, 73, 15] [23, 10, 15] [81, 9, 92] [60, 99, 14] [85, 11, 12] [79, 3, 31] [27, 48, 8] [50, 16, 82] [41, 84, 44] [25, 68, 84] [45, 43, 4] [51, 99, 53] [63, 27, 22] [90, 68, 90] [79, 32, 24] [58, 84, 89] [7, 24, 44] [96, 55, 52] [90, 68, 76] [20, 20, 60] [18, 33, 19] [11, 13, 17] [45, 55, 92] [18, 90, 62] [92, 97, 23] [7, 59, 34] [64, 70, 95] [51, 11, 12] [63, 27, 22] [44, 29, 48] [37, 95, 20] [48, 50, 96] [19, 37, 84] [45, 43, 76] [42, 56, 55] [84, 76, 25] [62, 79, 94] [90, 68, 90] [81, 9, 92] [39, 58, 85] [19, 10, 90] [50, 45, 38] [91, 13, 55] [63, 40, 92] [14, 83, 54] [68, 9, 84] [8, 17, 68] [42, 72, 6] [20, 19, 39] [13, 84, 25] [20, 9, 65] [55, 80, 32] [11, 59, 17] [25, 68, 84] [30, 57, 26] [9, 61, 84] [20, 65, 58] [14, 18, 54] [96, 1, 73] [9, 92, 73] [8, 68, 16] [40, 20, 24] [58, 20, 64] [17, 97, 23] [27, 56, 46] [90, 29, 13] [96, 55, 47] [48, 50, 96] [62, 79, 94] [67, 78, 51] [91, 13, 55] [95, 20, 29] [39, 90, 62] [23, 10, 15] [23, 54, 36] [95, 14, 73] [23, 36, 60] [23, 54, 60] [95, 14, 2] [61, 10, 90] [7, 97, 41] [35, 83, 5] [11, 13, 59] [21, 30, 90] [63, 27, 22] [54, 13, 30] [37, 90, 39] [9, 16, 60] [23, 36, 60] [49, 85, 37] [54, 13, 71] [20, 20, 60] [90, 76, 90] [27, 48, 33] [36, 48, 41] [48, 8, 33] [35, 45, 34] [42, 56, 58] [84, 75, 42] [13, 55, 48] [23, 39, 15] [27, 50, 46] [22, 96, 12] [11, 39, 68] [63, 72, 6] [23, 54, 60] [57, 42, 57] [91, 3, 0] [30, 26, 80] [22, 93, 2] [68, 9, 16] [63, 40, 92] [8, 68, 16] [35, 83, 5] [27, 50, 56] [45, 55, 38] [35, 35, 5] [46, 37, 86] [90, 29, 45] [54, 86, 17] [40, 86, 17] [71, 83, 99] [76, 51, 99] [85, 8, 37] [6, 11, 75] [1, 11, 68] [67, 78, 52] [60, 99, 14] [18, 33, 19] [90, 68, 90] [81, 9, 92] [3, 83, 31] [76, 99, 53] [49, 85, 37] [92, 94, 89] [2, 27, 22] [24, 16, 82] [76, 51, 53] [27, 54, 70] [13, 71, 30] [88, 58, 85] [39, 18, 62] [32, 15, 65] [43, 45, 34] [47, 40, 92] [9, 95, 73] [23, 10, 39] [17, 97, 23] [68, 61, 84] [32, 62, 98] [45, 43, 4] [83, 35, 5] [7, 97, 41] [35, 83, 5] [58, 20, 64] [43, 24, 44] [90, 45, 13] [71, 83, 99] [58, 20, 64] [55, 47, 52] [40, 86, 17] [45, 55, 46] [81, 9, 92] [84, 76, 25] [81, 92, 73] [8, 60, 14] [19, 80, 37] [85, 8, 37] [7, 98, 34] [35, 83, 5] [47, 65, 66] [23, 16, 91] [57, 81, 27] [10, 70, 94] [45, 87, 3] [70, 95, 19] [62, 79, 94] [18, 66, 19] [54, 75, 74] [92, 84, 21] [1, 39, 68] [68, 9, 60] [19, 80, 37] [91, 3, 0] [35, 45, 34] [37, 92, 21] [20, 9, 65] [9, 92, 73] [96, 73, 15] [7, 59, 34] [32, 62, 0]"
# ls_dp = []
# for j in range(len(seeds)):
#     random.seed(seeds[j])
#     rands = []
#     for k in range(0,4):
#         rands.append(random.randint(0,99))
#     ls_dp.append((~result1[j]|rands[j%4]) & (result1[j]|~rands[j%4]))
#     print(rands,end="")
# print("".join([chr(i) for i in ls_dp]))
# 23458591381644494879596426183878928641891759871602961070839457303969747353773411708437315165237216481430908369709167907047043280248152040749469402814146054871536032870746473649690743697560576735624528397398691515920649222501258921802372365480019200479555430922883680472732415240714991623845227274793947921407
dp = 23458591381644494879596426183878928641891759871602961070839457303969747353773411708437315165237216481430908369709167907047043280248152040749469402814146054871536032870746473649690743697560576735624528397398691515920649222501258921802372365480019200479555430922883680472732415240714991623845227274793947921407

# 恢复dq
E = 0x10001
C = [1, 0, 7789, 1, 17598, 20447, 15475, 23040, 41318, 23644, 53369, 19347, 66418, 5457, 0, 1, 14865, 97631, 6459, 36284, 79023, 1, 157348, 44667, 185701, 116445, 23809, 220877, 0, 1, 222082, 30333, 55446, 207442, 193806, 149389, 173229, 349031, 152205, 1, 149157, 196626, 1, 222532, 10255, 46268, 171536, 0, 351788, 152678, 0, 172225, 109296, 0, 579280, 634746, 1, 668942, 157973, 1, 17884, 662728, 759841, 450490, 0, 139520, 157015, 616114, 199878, 154091, 1, 937462, 675736, 53200, 495985, 307528, 1, 804492, 790322, 463560, 520991, 436782, 762888, 267227, 306436, 1051437, 384380, 505106, 729384, 1261978, 668266, 1258657, 913103, 935600, 1, 1, 401793, 769612, 484861, 1024896, 517254, 638872, 1139995, 700201, 308216, 333502, 0, 0, 401082, 1514640, 667345, 1015119, 636720, 1011683, 795560, 783924, 1269039, 5333, 0, 368271, 1700344, 1, 383167, 7540, 1490472, 1484752, 918665, 312560, 688665, 967404, 922857, 624126, 889856, 1, 848912, 1426397, 1291770, 1669069, 0, 1709762, 130116, 1711413, 1336912, 2080992, 820169, 903313, 515984, 2211283, 684372, 2773063, 391284, 1934269, 107761, 885543, 0, 2551314, 2229565, 1392777, 616280, 1368347, 154512, 1, 1668051, 0, 2453671, 2240909, 2661062, 2880183, 1376799, 0, 2252003, 1, 17666, 1, 2563626, 251045, 1593956, 2215158, 0, 93160, 0, 2463412, 654734, 1, 3341062, 3704395, 3841103, 609968, 2297131, 1942751, 3671207, 1, 1209611, 3163864, 3054774, 1055188, 1, 4284662, 3647599, 247779, 0, 176021, 3478840, 783050, 4613736, 2422927, 280158, 2473573, 2218037, 936624, 2118304, 353989, 3466709, 4737392, 2637048, 4570953, 1473551, 0, 0, 4780148, 3299784, 592717, 538363, 2068893, 814922, 2183138, 2011758, 2296545, 5075424, 1814196, 974225, 669506, 2756080, 5729359, 4599677, 5737886, 3947814, 4852062, 1571349, 4123825, 2319244, 4260764, 1266852, 1, 3739921, 1, 5948390, 1, 2761119, 2203699, 1664472, 3182598, 6269365, 5344900, 454610, 495499, 6407607, 1, 1, 476694, 4339987, 5642199, 1131185, 4092110, 2802555, 0, 5323448, 1103156, 2954018, 1, 1860057, 128891, 2586833, 6636077, 3136169, 1, 3280730, 6970001, 1874791, 48335, 6229468, 6384918, 5412112, 1, 7231540, 7886316, 2501899, 8047283, 2971582, 354078, 401999, 6427168, 4839680, 1, 44050, 3319427, 0, 1, 1452967, 4620879, 5525420, 5295860, 643415, 5594621, 951449, 1996797, 2561796, 6707895, 7072739]
list_p = sieve_base[0:len(C)]
list_q = sieve_base[len(C):2*len(C)]
for i in range(len(C)):
    P = list_p[i]
    Q = list_q[i]
    phi_N = (P - 1) * (Q - 1)
    D = gmpy2.invert(E,phi_N)
    print(pow(C[i],D,P*Q),end="")
# 104137587579880166582178434901328539485184135240660490271571544307637817287517428663992284342411864826922600858353966205614398977234519495034539643954586905495941906386407181383904043194285771983919780892934288899562700746832428876894943676937141813284454381136254907871626581989544814547778881240129496262777
dq = 104137587579880166582178434901328539485184135240660490271571544307637817287517428663992284342411864826922600858353966205614398977234519495034539643954586905495941906386407181383904043194285771983919780892934288899562700746832428876894943676937141813284454381136254907871626581989544814547778881240129496262777
print()

inv_p = gmpy2.invert(p,q)
mp = pow(c,dp,p)
mq = pow(c,dq,q)
m = (((mq - mp) * inv_p) % q) + mp
m = m % (p * q)
print(long_to_bytes(m).decode())

C o n c l u s i o n Conclusion Conclusion

关于逻辑运算式,一般是化简或者写出真值表找性质的做法

python2random模块与python3random模块所使用的伪随机数发生器算法不同

U n e x p e c t e d   s o l u t i o n Unexpected~solution Unexpected solution

有的解法是既然题目已经给出了 e e e的取值范围,那么我们直接爆破 e e e,判断条件就是最后明文m中是否包括D0g3

由于RSA中的p,q,c都已知,那么我们爆破出e很简单就能求解

strage

k e y w o r d s : keywords: keywords: 明文大小相比hint小真值表coppersmith

P r o b l e m Problem Problem

from Crypto.Util.number import *
import os

flag = b'D0g3{}'
m = bytes_to_long(flag)

p = getPrime(1024)
q = getPrime(1024)
n = p * q
e = 3

hint = bytes_to_long(os.urandom(256))

m1 = m | hint
m2 = m & hint

c = pow(m1, e, n)

with open('output.txt','a') as f:
    f.write(str([n,c,m2,hint]))
    f.close()

A n a l y s i s Analysis Analysis

题目已知n,c,m2,hint

m2 = m & hint,未知的m1m1 = n | hint

这道题仔细看会发现前面生成的p,q使用的RSA并不是直接给flag进行加密的,而是加密的是m1

m1 = m | hint,也就是说m1.bit_length() = hint.bit_length(),为什么等于的是hint的二进制位长度而不是m的二进制位长度呢?因为

hint = bytes_to_long(os.urandom(256))

也就是说hint的二进制位长度是2048,而已知的m2 = m & hint,所以m2的二进制位长度等于m

m2.bit_length()
# 383

所以在m1中,二进制位长度大于383的部分都是hint本身,而不是hintm的结果;

另外m1的二进制位长度太大了,虽然这里RSA加密使用的e只有3,但是三次方也已经使得pow(m1,3) > n

所以不能使用低加密指数攻击

但是对于之后计算有用的只有整个m1的前383个二进制位,所以我们可以将我们真正需要的前383个二进制位组成的数作为未知数,而m1剩余的二进制位我们实际上已知(就是hint的对应二进制位),以此来构造出一个新的等式
c ≡ ( h i g h _ m 1 + l o w _ m 1 ) 3 ( m o d n ) c\equiv (high\_m_1 + low\_m_1)^{3}\pmod n c(high_m1+low_m1)3(modn)
由于未知数的二进制位数满足coppersmith定理的要求条件,那么就使用coppersmith定理进行求解即可

high_m1 = (hint >> 383) * pow(2,383)
PR.<x> = PolynomialRing(Zmod(n))
f = (high_m1 + x) ^ 3 - c
f.small_roots(2^383,beta = 0.4)

求得m1的前383位二进制

现在来看看整体,已知了m1,m2,hint

m1 = m | hintm2 = m & hint;实际上就是简单的逻辑运算,已知了两个等式分别的两个数,求剩下的数,列一个简略的真值表(下面均以单个二进制位的值为例)

如果hint == 0,那么此时由于与运算的性质,m2一定为0,而此时当m1 == 0时,m一定为0,当m1 == 1时,m一定为1(也可以这么说,m == m1

如果hint == 1,那么此时由于或运算的性质,m1一定为1,而此时当m2 == 0时,m一定为0,当m2 == 1时,m一定为1(也可以这么说,m == m2

我们就可以通过以上性质还原m的各个二进制位了

原本是直接把m1,m2,hint都先转换为二进制形式,再逐一判断,但是二进制位可能错位了,导致flag只有极少部分是正确的

使用类似于LSFR流密码生成的过程来还原m的二进制位

m = ""
while m2 > 0: # m1也可
    a = hint & 1
    b = m1 & 1
    c = m2 & 1
    if a == 0:
        assert c == 0
        m += str(b)
    if a == 1:
        assert b == 1
        m += str(c)
    hint = hint >> 1
    m1 = m1 >> 1
    m2 = m2 >> 1
m = m[::-1] # 由于原来所求得的m是逆序二进制位的,所以要翻转过来

s o l v i n g   c o d e solving~code solving code

from Crypto.Util.number import *
import gmpy2

n,c,m2,hint = (13002904520196087913175026378157676218772224961198751789793139372975952998874109513709715017379230449514880674554473551508221946249854541352973100832075633211148140972925579736088058214014993082226530875284219933922497736077346225464349174819075866774069797318066487496627589111652333814065053663974480486379799102403118744672956634588445292675676671957278976483815342400168310432107890845293789670795394151784569722676109573685451673961309951157399183944789163591809561790491021872748674809148737825709985578568373545210653290368264452963080533949168735319775945818152681754882108865201849467932032981615400210529003, 8560367979088389639093355670052955344968008917787780010833158290316540154791612927595480968370338549837249823871244436946889198677945456273317343886485741297260557172704718731809632734567349815338988169177983222118718585249696953103962537942023413748690596354436063345873831550109098151014332237310265412976776977183110431262893144552042116871747127301026195142320678244525719655551498368460837394436842924713450715998795899172774573341189660227254331656916960984157772527015479797004423165812493802730996272276613362505737536007284308929288293814697988968407777480072409184261544708820877153825470988634588666018802, 9869907877594701353175281930839281485694004896356038595955883788511764488228640164047958227861871572990960024485992, 9989639419782222444529129951526723618831672627603783728728767345257941311870269471651907118545783408295856954214259681421943807855554571179619485975143945972545328763519931371552573980829950864711586524281634114102102055299443001677757487698347910133933036008103313525651192020921231290560979831996376634906893793239834172305304964022881699764957699708192080739949462316844091240219351646138447816969994625883377800662643645172691649337353080140418336425506119542396319376821324619330083174008060351210307698279022584862990749963452589922185709026197210591472680780996507882639014068600165049839680108974873361895144)

high_m1 = (hint >> int(m2).bit_length()) * pow(2,int(m2).bit_length())
PR.<x> = PolynomialRing(Zmod(n))
f = (high_m1 + x) ^ 3 - c
low_m1 = f.small_roots(2^int(m2).bit_length(),beta=0.4)[0]
low_m1 = 13420866878657192881981508918368509601760484822510871697454710042290632315733970543259862148639047993224391010676733
m1 = low_m1

assert int(m2).bit_length() == int(m1).bit_length()
m = ""
while m2 > 0:
    a = hint & 1
    b = m1 & 1
    c = m2 & 1
    if a == 0:
        assert c == 0
        m += str(b)
    if a == 1:
        assert b == 1
        m += str(c)
    hint = hint >> 1
    m1 = m1 >> 1
    m2 = m2 >> 1
m = "0" + m[::-1]
print(long_to_bytes(int(m,2)).decode())

ez_eqution

k e y w o r d s : keywords: keywords: 提取最大公因数解一元二次方程RSA p,q接近的情况

P r o b l e m Problem Problem

from Crypto.Util.number import *
from functools import reduce
import os

pad1 = os.urandom(256)
pad2 = os.urandom(256)
flag = b'D0g3{}'

primelist = [getPrime(1024) for i in range(3)]
p = getPrime(1024)
q = nextprime(p)
n = reduce(lambda a, b:a * b, primelist) * p * q

x1 = pow(primelist[0],2)
x2 = pow(primelist[1],2)
x3 = primelist[0] * primelist[1]
y1 = x1 * primelist[1] + x2 * primelist[0]
y2 = x2 * (primelist[2] + 1) - 1
y3 = x3 * (primelist[2] + 1) - 1

m = bytes_to_long(pad1+flag+pad2)
e = 0x10001
c = pow(m,e,n)

print('#M1=',y1 + x2 + x3)
print('#M2=',y2 + y3)
print('#c=',c)
print('#n=',n)

#M1= 
#M2= 
#c= 
#n= 

A n a l y s i s Analysis Analysis

pad1 + flag + pad2进行RSA加密,很显然明文已经足够大了,不可能有直接开方之类的非预期解;再来看看模数n的构造

primelist = [getPrime(1024) for i in range(3)]
p = getPrime(1024)
q = nextprime(p)
n = reduce(lambda a, b:a * b, primelist) * p * q

作用相当于n = primelist[0] * primelist[1] * primelist[2] * p * q

为了能解出flag,我们需要知道fai_n,至少要职知道primelist[i]的大小(之后就将primelist[i]写作pi

题目又给出了M1,M2;其中
M 1 = p 1 2 ⋅ p 0 + p 1 ⋅ p 0 2 + p 0 ⋅ p 1 + p 1 2 M 2 = ( p 1 2 + p 0 ⋅ p 1 ) ⋅ ( p 2 + 1 ) − 2 M_1=p_1^2\cdot p_0+p_1\cdot p_0^2 + p_0\cdot p_1+p_1^2\\ M_2=(p_1^2+p_0\cdot p_1)\cdot(p_2+1)-2 M1=p12p0+p1p02+p0p1+p12M2=(p12+p0p1)(p2+1)2
本来很显然 M 1 , M 2 + 2 M_1,M_2+2 M1,M2+2一定有公约数 p 1 p_1 p1
M 1 = p 1 ⋅ ( p 1 ⋅ p 0 + p 0 2 + p 0 + p 1 ) M 2 + 2 = p 1 ⋅ ( p 1 + p 0 ) ⋅ ( p 2 + 1 ) M_1=p_1\cdot(p_1\cdot p_0 + p_0^2 + p_0+p_1)\\ M_2+2= p_1\cdot(p_1+p_0)\cdot(p_2 + 1) M1=p1(p1p0+p02+p0+p1)M2+2=p1(p1+p0)(p2+1)
但是根据计算出来的结果,两者的最大公约数并不等于 p 1 p_1 p1

>>> import gmpy2
>>> p1 = gmpy2.gcd(M1,M2 + 2)
>>> p1.bit_length()
2051
# 正确的p1的bit_length()应该等于1024

原因大概是 M 1 , M 2 + 2 M_1,M_2+2 M1,M2+2中涉及的括号里加法的结果很可能含有较多较大的公因数,导致整体的最大公约数不是等于 p 1 p_1 p1,而是等于 p 1 ⋅ u n k n o w n p_1\cdot unknown p1unknown;(也就是说 M 1 M_1 M1中的 ( p 1 ⋅ p 0 + p 0 2 + p 0 + p 1 ) (p_1\cdot p_0 + p_0^2 + p_0+p_1) (p1p0+p02+p0+p1) M 2 + 2 M_2+2 M2+2中的 ( p 1 + p 0 ) ⋅ ( p 2 + 1 ) (p_1+p_0)\cdot(p_2 + 1) (p1+p0)(p2+1)很可能有其他公约数,导致 M 1 , M 2 + 2 M_1,M_2+2 M1,M2+2的最大公约数不单单只是 p 1 p_1 p1

也尝试爆破过 u n k n o w n unknown unknown的大小,但是其中含有较大的数,导致无法爆破成功

但是含有 p 1 p_1 p1作为因数的已知量并不只有 M 2 + 2 M_2+2 M2+2,模数 n n n也含有 p 1 p_1 p1作为因数,且没有多余的加法导致非素数的公因数生成,尝试一下

>>> import gmpy2
>>> p1 = gmpy2.gcd(M1,n)
>>> p1.bit_length()
1024 #正确

所以想要将 p 1 p_1 p1作为两个数的最大公约数输出,要求至少其中一个数的组成的所有部分没有新的因数

p1已知了,现在代回M1求解以 p 0 p_0 p0作为未知数的一元二次方程
M 1 = p 0 2 ⋅ p 1 + p 0 ⋅ ( p 1 2 + p 1 ) + p 1 2 M_1= p_0^2\cdot p_1 + p_0\cdot(p_1^2+ p_1)+p_1^2 M1=p02p1+p0(p12+p1)+p12
可以手推一下配方法,也可以直接代入sagemath求解

p0 = var("p0")
f = p0 ^ 2 + p0 * (p1 + 1) + p1 - (M1//p1)
solve(p0 ^ 2 + p0 * (p1 + 1) + p1 == M1 // p1,p0)[1]
# 117379993488408909213785887974472229016071265566403849836216754847295401565166151872329440545598767396499252325133419296775798211888305050776586647999185549171166433935032159605367762650398185050063643611720499373962310459705000471248897299568458251778545586376091559089442503748421906239117101764062329447353

配方法
M 1 p 1 − p 1 + 1 4 ⋅ ( p 1 + 1 ) 2 = p 0 2 + p 0 ⋅ ( p 1 + 1 ) + 1 4 ⋅ ( p 1 + 1 ) 2 = ( p 0 + 1 2 ( p 1 + 1 ) ) 2 \frac{M_1}{p_1}-p_1+\frac{1}{4}\cdot(p_1+1)^2 = p_0^2+p_0\cdot(p_1+1)+\frac{1}{4}\cdot(p_1+1)^2=(p_0+\frac{1}{2}(p_1+1))^2 p1M1p1+41(p1+1)2=p02+p0(p1+1)+41(p1+1)2=(p0+21(p1+1))2

f = (M1 // p1) - p1 + ((p1 + 1) ** 2) // 4 
p0 = gmpy2.iroot(f,2)[0] - (p1 + 1) // 2

现在已知p0,p1,代回M2直接求解p2即可

p2 = (M2 + 2) // (p1**2 + p0 * p1) - 1

最后已知p0,p1,p2此时剩下p,q未知;而p,q是邻接的素数,我们可以计算出p*q的大小,那么可以通过开平方,查找开平方数的邻接的两个素数

pq = n // p0 // p1 // p2
q = nextprime(gmpy2.iroot(pq,2)[0])
p = prevprime(q)

最后p,q,p0,p1,p2均已知,那么就可以正常求解RSA

S o l v i n g   c o d e Solving~code Solving code

from Crypto.Util.number import *
import gmpy2
from sympy import *

M1= 3826382835023788442651551584905620963555468828948525089808250303867245240492543151274589993810948153358311949129889992078565218014437985797623260774173862776314394305207460929010448541919151371739763413408901958357439883687812941802749556269540959238015960789123081724913563415951118911225765239358145144847672813272304000303248185912184454183649550881987218183213383170287341491817813853157303415010621029153827654424674781799037821018845093480149146846916972070471616774326658992874624717335369963316741346596692937873980736392272357429717437248731018333011776098084532729315221881922688633390593220647682367272566275381196597702434911557385351389179790132595840157110385379375472525985874178185477024824406364732573663044243615168471526446290952781887679180315888377262181547383953231277148364854782145192348432075591465309521454441382119502677245090726728912738123512316475762664749771002090738886940569852252159994522316
M2= 4046011043117694641224946060698160981194371746049558443191995592417947642909277226440465640195903524402898673255622570650810338780358645872293473212692240675287998097280715739093285167811740252792986119669348108850168574423371861266994630851360381835920384979279568937740516573412510564312439718402689547377548575653450519989914218115265842158616123026997554651983837361028152010675551489190669776458201696937427188572741833635865019931327548900804323792893273443467251902886636756173665823644958563664967475910962085867559357008073496875191391847757991101189003154422578662820049387899402383235828011830444034463049749668906583814229827321704450021715601349950406035896249429068630164092309047645766216852109121662629835574752784717997655595307873219503797996696389945782836994848995124776375146245061787647756704605043856735398002012276311781956668212776588970619658063515356931386886871554860891089498456646036630114620806
c= 1394946766416873131554934453357121730676319808212515786127918041980606746238793432614766163520054818740952818682474896886923871330780883504028665380422608364542618561981233050210507202948882989763960702612116316321009210541932155301216511791505114282546592978453573529725958321827768703566503841883490535620591951871638499011781864202874525798224508022092610499899166738864346749753379399602574550324310119667774229645827773608873832795828636770263111832990012205276425559363977526114225540962861740929659841165039419904164961095126757294762709194552018890937638480126740196955840656602020193044969685334441405413154601311657668298101837066325231888411018908300828382192203062405287670490877283269761047853117971492197659115995537837080400730294215778540754482680476723953659085854297184575548489544772248049479632420289954409052781880871933713121875562554234841599323223793407272634167421053493995795570508435905280269774274084603687516219837730100396191746101622725880529896250904142333391598426588238082485305372659584052445556638990497626342509620305749829144158797491411816819447836265318302080212452925144191536031249404138978886262136129250971366841779218675482632242265233134997115987510292911606736878578493796260507458773824689843424248233282828057027197528977864826149756573867022173521177021297886987799897923182290515542397534652789013340264587028424629766689059507844211910072808286250914059983957934670979551428204569782238857331272372035625901349763799005621577332502957693517473861726359829588419409120076625939502382579605
n= 19445950132976386911852381666731799463510958712950274248183192405937223343228119407660772413067599252710235310402278345391806863116119010697766434743302798644091220730819441599784039955347398797545219314925103529062092963912855489464914723588833817280786158985269401131919618320866942737291915603551320163001129725430205164159721810319128999027215168063922977994735609079166656264150778896809813972275824980250733628895449444386265971986881443278517689428198251426557591256226431727934365277683559038777220498839443423272238231659356498088824520980466482528835994554892785108805290209163646408594682458644235664198690503128767557430026565606308422630014285982847395405342842694189025641950775231191537369161140012412147734635114986068452144499789367187760595537610501700993916441274609074477086105160306134590864545056872161818418667370690945602050639825453927168529154141097668382830717867158189131567590506561475774252148991615602388725559184925467487450078068863876285937273896246520621965096127440332607637290032226601266371916124456122172418136550577512664185685633131801385265781677598863031205194151992390159339130895897510277714768645984660240750580001372772665297920679701044966607241859495087319998825474727920273063120701389749480852403561022063673222963354420556267045325208933815212625081478538158049144348626000996650436898760300563194390820694376019146835381357141426987786643471325943646758131021529659151319632425988111406974492951170237774415667909612730440407365124264956213064305556185423432341935847320496716090528514947
e = 65537

p1 = int(gmpy2.gcd(M1,n))
# print(p1.bit_length())

f = (M1 // p1) - p1 + ((p1 + 1) ** 2) // 4 
p0 = gmpy2.iroot(f,2)[0] - (p1 + 1) // 2

p2 = (M2 + 2) // (p1**2 + p0 * p1) - 1

pq = n // p0 // p1 // p2
q = nextprime(gmpy2.iroot(pq,2)[0])
p = prevprime(q)

fai_n = (p-1) * (q-1) * (p0 - 1) * (p1 - 1) * (p2 - 1)
d = gmpy2.invert(e,fai_n)
m = pow(c,d,n)
print(long_to_bytes(m)[256:-256].decode())

C o n c l u s i o n Conclusion Conclusion

求两个变量的最大公约数,需要确定表达式中没有多余的不可见的公因数(一般是通过加减法生成的)出现

RSA p,q相近的情况应该直接将p*q的结果开方查找邻近的素数

air encryption

k e y w o r d s : keywords: keywords: AES_CTRxor交互欧拉定理数据处理

P r o b l e m Problem Problem

#!/usr/bin/python
import socketserver
import random
import os
import string
import binascii
import hashlib
from Crypto.Cipher import AES
from Crypto.Util import Counter 
from Crypto.Util.number import getPrime
from hashlib import sha256
import gmpy2
from flag import flag

def init():
    q = getPrime(512)
    p = getPrime(512)
    e = getPrime(64)
    n = q*p
    phi = (q-1) * (p-1)
    d = gmpy2.invert(e, phi)
    hint = 2 * d + random.randint(0, 2**16) * e * phi
    mac = random.randint(0, 2**64)
    c = pow(mac, e, n)
    counter = random.randint(0, 2**128)
    key = os.urandom(16)
    score = 0
    return n, hint, c, counter, key, mac, score

class task(socketserver.BaseRequestHandler):

    def POW(self):
        random.seed(os.urandom(8))
        proof = ''.join([random.choice(string.ascii_letters+string.digits) for _ in range(20)])
        result = hashlib.sha256(proof.encode('utf-8')).hexdigest()
        self.request.sendall(("sha256(XXXX+%s) == %s\n" % (proof[4:],result)).encode())
        self.request.sendall(b'Give me XXXX:\n')
        x = self.recv()
        
        if len(x) != 4 or hashlib.sha256((x+proof[4:].encode())).hexdigest() != result: 
            return False
        return True

    def recv(self):
        BUFF_SIZE = 2048
        data = b''
        while True:
            part = self.request.recv(BUFF_SIZE)
            data += part
            if len(part) < BUFF_SIZE:
                break
        return data.strip()

    def padding(self, msg):
        return  msg + chr((16 - len(msg)%16)).encode() * (16 - len(msg)%16)

    def encrypt(self, msg):
        msg = self.padding(msg)
        if self.r != -1:
            self.r += 1
            aes = AES.new(self.key, AES.MODE_CTR, counter = Counter.new(128, initial_value=self.r))
            return aes.encrypt(msg)
        else:
            return msg

    def send(self, msg, enc=True):
        print(msg, end= '   ')
        if enc:
            msg = self.encrypt(msg)
        print(msg, self.r)
        self.request.sendall(binascii.hexlify(msg) + b'\n')

    def set_key(self, rec):
        if self.mac == int(rec[8:]):
            self.r = self.counter

    def guess_num(self, rec):
        num = random.randint(0, 2**128)
        if num == int(rec[10:]):
            self.send(b'right')
            self.score += 1
        else:
            self.send(b'wrong')

    def get_flag(self, rec):
        assert self.r != -1
        if self.score ==  5:
            self.send(flag, enc=False)
        else:
            self.send(os.urandom(32) +  flag)

    def handle(self):
        self.r = -1

        if not self.POW():
            self.send(b'Error Hash!', enc= False)
            return

        self.n, self.hint, self.c ,self.counter, self.key, self.mac, self.score = init()

        self.send(str(self.n).encode(), enc = False)
        self.send(str(self.hint).encode(), enc = False)
        self.send(str(self.c).encode(), enc = False)

        for _ in range(6):
            rec = self.recv()
            if rec[:8] == b'set key:':
                self.set_key(rec)
            elif rec[:10] == b'guess num:':
                self.guess_num(rec)
            elif rec[:8] == b'get flag':
                self.get_flag(rec)
            else:
                self.send(b'something wrong, check your input')

class ForkedServer(socketserver.ForkingMixIn, socketserver.TCPServer):
    pass

def main():
    HOST, PORT = '127.0.0.1', 10086
    server = ForkedServer((HOST, PORT), task)
    server.allow_reuse_address = True
    server.serve_forever()

if __name__ == '__main__':
    main()

A n a l y s i s Analysis Analysis

经典的密码nc连接题,仔细观察发现重点函数是handle()

    def handle(self):
        self.r = -1

        if not self.POW():
            self.send(b'Error Hash!', enc= False)
            return

        self.n, self.hint, self.c ,self.counter, self.key, self.mac, self.score = init()

        self.send(str(self.n).encode(), enc = False)
        self.send(str(self.hint).encode(), enc = False)
        self.send(str(self.c).encode(), enc = False)

        for _ in range(6):
            rec = self.recv()
            if rec[:8] == b'set key:':
                self.set_key(rec)
            elif rec[:10] == b'guess num:':
                self.guess_num(rec)
            elif rec[:8] == b'get flag':
                self.get_flag(rec)
            else:
                self.send(b'something wrong, check your input')

先进行POW函数验证(相当于拖慢脚本运行速度),就是判断hash值,爆破即可

按照RSA加密的方式给出了n,chint(注意服务端给出的这几个值是通过字节转十六进制了的,需要转回正常的字节,而原来的字节是十进制数,所以使用binascii.unhexlify()),且加密的明文是mac;先把ta求出来
h i n t = 2 ⋅ d + r a n d o m ⋅ e ⋅ ϕ ( n ) ∵ a ϕ ( p ) ≡ 1 ( m o d p ) , 欧 拉 定 理 ∴ c h i n t ≡ c 2 ⋅ d ⋅ c r a n d o m ⋅ e ⋅ ϕ ( n ) ≡ c 2 ⋅ d ( m o d n ) hint = 2\cdot d+random\cdot e \cdot \phi(n)\\ \because a^{\phi(p)}\equiv 1\pmod p,欧拉定理\\ \therefore c^{hint}\equiv c^{2\cdot d}\cdot c^{random\cdot e\cdot \phi(n)}\equiv c^{2\cdot d}\pmod n \\ hint=2d+randomeϕ(n)aϕ(p)1(modp)chintc2dcrandomeϕ(n)c2d(modn)
所以mac = gmpy2.iroot(pow(c,hint,n),2)[0];需要知道mac的作用

再来看到之后提供三种选择,set keyguess numget flag

分别查看函数内容

    def set_key(self, rec):
        if self.mac == int(rec[8:]):
            self.r = self.counter

似乎是判定输入的内容是否和mac一致,若一致,则进行一个赋值操作,看到是将counter赋值给r

寻找counter的生成

def init():
    ...
	counter = random.randint(0, 2**128)

就是一个随机数,而self.r初始值是-1

为什么要进行赋值操作呢,可以在encrypt()函数中看到

    def encrypt(self, msg):
        msg = self.padding(msg)
        if self.r != -1:
            self.r += 1
            aes = AES.new(self.key, AES.MODE_CTR, counter = Counter.new(128, initial_value=self.r))
            return aes.encrypt(msg)
        else:
            return msg

如果self.r的值没有改变的话,也就是说没有进行set key操作的话,AES_CRT加密过程就不会生效;加密不生效就可以直接拿flag吗,再来看到get flag操作

    def get_flag(self, rec):
        assert self.r != -1
        if self.score ==  5:
            self.send(flag, enc=False)
        else:
            self.send(os.urandom(32) +  flag)

有个assert self.r != -1,意味着必须self.r被随机生成的counter赋值才能进行get flag,否则会直接assert报错

三种选择中还剩下一个guess num操作

    def guess_num(self, rec):
        num = random.randint(0, 2**128)
        if num == int(rec[10:]):
            self.send(b'right')
            self.score += 1
        else:
            self.send(b'wrong')

如果猜对随即生成的数字,则使得self.score += 1,而self.score的作用是在get flag

        if self.score ==  5:
            self.send(flag, enc=False)

意味着连续猜中5次则能使得get flag操作直接返回flag

但是细看一下,首先随机数生成的范围很大,不可能爆破;只能猜测一次,否则会在6次选择中浪费一次机会;最重要的是,6次选择中有一次机会必须用来set key(否则无法进行get flag操作),有一次机会用来get flag,只剩下4次机会,而我们需要连续猜中5次随机数;所以guess num操作在现在看来就是一个幌子


那么关于能取得flag的机会只有落在encrypt()函数上,是AES_CTR加密,关于这个模式

  • 加密过程

请添加图片描述

如图所示,引入了一个计数器CTR,使得在第二步中与各个明文分组进行异或的加密流不同,因为每加密一个明文分组(16 bytes),计数器CTR就会自动+1

题目当中是如何生成CTR

from Crypto.Util import Counter 
aes = AES.new(self.key, AES.MODE_CTR, counter = Counter.new(128, initial_value=self.r))

看得出来是Counter.new(128, initial_value=self.r))initial_value变量在设置CTR的初始值

CTR所表示的值也不是简单的0001,而是一组由随机数nonce分组序号组成的初始值

请添加图片描述

每加密一个明文分组,CTR的分组序号则会+1,使得进行加密操作之后的加密流与之前的加密流完全不同,达到与各个明文分组进行异或的字节串互不相同

PS:这里以及之后提到的加密流都是请添加图片描述
这一部分的结果;并且加密操作特指CTR生成后马上进行的加密器的操作

  • 解密过程

请添加图片描述

可以发现就是重置计数器CTR使其在对应的密文分组上产生与加密时一样的CTR,再将其进行加密操作,生成与加密过程中一样的加密流,与密文分组异或即得到明文分组


分析完AES_CTR的加解密过程,发现实际上加解密过程都可以分为两大块,CTR加密异或

再回到题目脚本,这里容易发现,在每次服务器脚本send的时候,函数中很多地方都是send(x,enc = False),可以找到脚本中自己定义的send函数

    def send(self, msg, enc=True):
        print(msg, end= '   ')
        if enc:
            msg = self.encrypt(msg)
        print(msg, self.r)
        self.request.sendall(binascii.hexlify(msg) + b'\n')

作用就是写出enc = Falsesend函数里面,会直接输出内容,也就是正常交互过程中的send函数;而当enc = True,或者说send函数里面没有对enc进行再赋值,那么输出的内容会进行encrypt()加密,也就是AES_CTR加密

仔细观察可以发现

        self.send(b'something wrong, check your input')
# 以及
        if num == int(rec[10:]):
            self.send(b'right')
            self.score += 1
        else:
            self.send(b'wrong')

这里的send函数返回的实际上不是明文rightsomething wrong...;而是AES_CTR加密后的密文(也可以在本地测试debug的时候发现这个特征)

不可能无缘无故地把这些返回内容设置为加密之后再返回,所以这里一定是突破点

现在我们可以得到一对明密文AES_CTR加密后的flag;加密密钥key未知也无法知道,显然不能通过正常求密钥key来解密

总共给予了6次选择的机会,那如果我们多得到几组明密文,有什么用呢

AES_CTR加密中,如果我们已知明文以及对应的密文,将两者异或即可得到加密流,因为加密过程中
m ⊕ e n c _ s t r e a m = c m\oplus enc\_stream=c menc_stream=c
而加密流是由不同的CTR生成的,如果我们多收集一些加密流拼接在一起,再用来和加密后的flag进行异或,不就模拟了AES_CTR模式的解密过程吗

那么最终的问题就是我们需要哪些加密流,也就是说哪些加密流是加密flag的时候真正使用的

加密流当然是越长越好,所以我们使用something wrong...作为明密文组求得加密流,默认经过padding函数后明文长度为48

正常情况下(本题不是这样的正常情况,之后会提到,也是关键点之一)是得到的加密流应该是由CTRCTR+1CTR+2生成的,那么很可能不够长足以使得与加密之后的flag异或可以得到完整的flag(因为flag加密过程中是作为padding(os.urandom(32)+flag)进行加密的,所以密文会比较长)

那么现在有一个疑惑就是如果连续加密两次及以上的话,counter会不会自动继续+1,使得我们确实可以将对不同的加密过程(虽然明文确实是相同的)中生成的多个加密流按照CTR+i的顺序拼接在一起,作为一整个加密流

实践一下

>>> from Crypto.Cipher import AES
>>> from Crypto.Util import Counter
>>> from pwn import *
>>> key = b'\x01' * 16
>>> msg = b"flag1111111111111111111111111111111111111111111111111111111"
>>> t = Counter.new(128, initial_value=123)
>>> aes = AES.new(key, AES.MODE_CTR, counter = t)
>>> enc1 = aes.encrypt(msg)
>>> enc1
b"0=J\xe6\x87\xec\x07r\xf1{L\x94\xf7\xf2\xfcp|-\xf0\xca\xba\xe4:\x89\x7f\rI\xb5\x84\xb8\x00\xe2%(\x02o\xa5\x8c\xa3\x93\x18'\xb0\xa2\xe2\xd4]\xb1*\xb5\x8fH7\xd8\xfa\x8f\x08\xe7X"
>>> enc2 = aes.encrypt(msg)
>>> enc2
b'\x97\xf5\xe3_\xdc\xb3\x1c\x11\x1fM\n\x16\x06}:hvU\x1d\x93"\xf6\x89\xc3\x05\x94\x8b>6ha\xce\'\x9f\xb5$\x07Hm\xa5\xd1]y)P\xff\xd3e\xea\x7f\xa9\xb3\xe9\xcd\x88\x97>\x8d\xad'

实践证明确实可以将不同加密过程中得到的加密流拼接在一起,但是如何确保对flag加密的加密流和我们求得的加密流一致呢,那就是重复使用set key操作

大致重演一下重复使用set key的作用

>>> from Crypto.Cipher import AES
>>> from Crypto.Util import Counter
>>> from pwn import *
>>> key = b'\x01' * 16
>>> msg = b"flag1111111111111111111111111111111111111111111111111111111"
>>> t = Counter.new(128, initial_value=123)
>>> aes = AES.new(key, AES.MODE_CTR, counter = t)
>>> enc1 = aes.encrypt(msg)
>>> enc2 = aes.encrypt(msg)
>>> t = Counter.new(128, initial_value=123)
>>> aes = AES.new(key, AES.MODE_CTR, counter = t)
>>> enc3 = aes.encrypt(msg)
>>> enc4 = aes.encrypt(msg)
>>> assert xor(enc1,msg) == xor(enc3,msg)
>>> assert xor(enc2,msg) == xor(enc4,msg)
>>>

意思就是当我们重复对counter进行赋相同的值时,整个CTR初始值会被刷新

那么我们是不是只需要两到三个something wrong...明密文对得到的加密流拼接在一起与flag进行异或就好了呢

实践发现只能得到一半的flag,刚好就是前48位的padding(os.urandom(32)+flag);之后的加密流为什么不是正确对应的呢

发现作者在self.raes上面动了手脚,在每次加密整个明文之前self.r会自己首先+1,也就是CTR + 1;并且在每次加密整个明文之前都会重新定义一遍 aes = AES.new(self.key, AES.MODE_CTR, counter = Counter.new(128, initial_value=self.r)),这就意味着对两次明文(不是明文分组)加密中所使用两个的CTR生成的加密流中,上一个明文的最后一组明文分组使用的CTR + i,下一个明文的第一组明文分组的不是CTR + i + 1

    def encrypt(self, msg):
        msg = self.padding(msg)
        if self.r != -1:
            self.r += 1
            aes = AES.new(self.key, AES.MODE_CTR, counter = Counter.new(128, initial_value=self.r))
            return aes.encrypt(msg)
        else:
            return msg

所以加密过程中的加密流实际上是CTR + 1CTR + 2CTR + 3等等通过加密操作生成的

并且上一个明文(不是明文分组)加密所涉及的CTR初始值是CTR + 1,下一个明文加密所涉及的CTR初始值是CTR + 2(因为每次加密整个明文之前self.r += 1

所以
m 1 : C T R + 1    C T R + 2    C T R + 3 m 2 : C T R + 2    C T R + 3    C T R + 4 m 3 : C T R + 3    C T R + 4    C T R + 5 m_1 : CTR+1~~CTR+2~~CTR+3\\ m_2:CTR+2~~CTR+3~~CTR+4\\ m_3:CTR+3~~CTR+4~~CTR+5\\ m1:CTR+1  CTR+2  CTR+3m2:CTR+2  CTR+3  CTR+4m3:CTR+3  CTR+4  CTR+5
其中 m i m_i misomgthing srong...的明文,意思是该明文在前后的加密过程中所使用的加密流是由以上CTR + i生成的

为了使得flag使用的是已知的加密流,我们使用set key操作把CTR刷新,使得
f l a g : C T R + 1    C T R + 2    C T R + 3    C T R + 4    C T R + 5 flag:CTR+1~~CTR+2~~CTR+3~~CTR+4~~CTR+5 flag:CTR+1  CTR+2  CTR+3  CTR+4  CTR+5
那么对 m i m_i mi进行对应的截取即可

S o l v i n g   c o d e Solving~code Solving code

from pwn import *
from Crypto.Util.number import *
import gmpy2
import hashlib
import string
import random
import binascii

context.log_level='debug'
io = remote("192.168.109.129","9998")
io.recvuntil(b"+")
tmp = io.recvuntil(b"\n")
part_proof = tmp.split(b") == ")[0]
result = tmp.split(b") == ")[1][:-1]
table = string.ascii_letters + string.digits
while True:
    XXXX = ("".join(random.sample(table,4))).encode()
    if hashlib.sha256(XXXX + part_proof).hexdigest() == result.decode():
        io.recvuntil(b"\n")
        io.sendline(XXXX)
        break

n = int(binascii.unhexlify(io.recvline()[:-1]))
hint = int(binascii.unhexlify(io.recvline()[:-1]))
c =  int(binascii.unhexlify(io.recvline()[:-1]))
mac = gmpy2.iroot(pow(c,hint,n) % n,2)[0]


io.sendline(b"set key:" + str(mac).encode())
enc = []
for _ in range(3):
    io.sendline(b"I_want_flag")
    time.sleep(0.5)
    tmp = io.recvline()[:-1].decode()
    if _ != 2:
        enc.append(long_to_bytes(int(str(tmp),16))[:16])
    else:
        enc.append(long_to_bytes(int(str(tmp),16)))
# print(enc)
io.sendline(b"set key:" + str(mac).encode())
time.sleep(0.5)
io.sendline(b"get flag")
time.sleep(0.5)
tmp = io.recvline()
enc_flag = binascii.unhexlify(tmp[:-1])

msg = b'something wrong, check your input'
msg = msg + chr((16 - len(msg)%16)).encode() * (16 - len(msg)%16)
key_stream = b""
for i in enc:
    key_stream += xor(i,msg[:len(i)]) 
print(xor(enc_flag,key_stream[:len(enc_flag)]))

C o n c l u s i o n Conclusion Conclusion

关于AES的深入一点的题目都是交互式的,正常情况不会要求求密钥key,而是利用xor的特性以及加密器(相当于黑盒加密)来从服务器脚本处骗取可以求得flag的数值

关于交互式的题目没有头绪的时候更倾向于直接与服务端进行交互查看debug返回,而不单是查看服务器所使用的脚本

Crypto的交互式题目需要注意服务端返回的内容是什么类型的,很可能脚本会对十进制数值转字节再转十六进制,可能要进行一定的数据处理

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

M3ng@L

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值