e的取值范围为:
$$
e = \frac{y}{x} \cdot ((p+1)(q+1) \pm \frac{(p-q)\cdot N^{0.21}}{3(p+q)})
$$
两边除以$N$,可以化为:
$$
\frac{e}{N} = \frac{y}{x}(1 + \frac{p+q+1}{N} \pm \frac{(p-q)\cdot N^{0.21-1}}{3(p+q)})
$$
等式右边括号里面的内容,除了1以外,其他的一大坨实际上非常小,所以可以用连分数逼近出$x, y$
得到$x, y$后,可以再写成:
$$
\frac{ex}{y} = (p+1)(q+1) \pm \frac{(p-q)\cdot N^{0.21}}{3(p+q)}
$$
从中可以得到$(p+1)(q+1)$的近似值,进而得到$s = p + q$的近似值$s’$(大部分MSB)
再根据方程
$$
p^2 - sp + N == 0
$$
可以从$s’$中得到$p$的近似值(大部分MSB),再利用Coppersmith求small root即可得到$p$。
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
|
e = 288969517294013178236187423377607850772706067194956328319540958788120421760563745859661120809993097599452236235703456953461446476016483100948287481999230043898368061651387268308645842547879026821842863879967704742559469599469159759360184157244907772315674219466971226019794131421405331578417729612598931842872757269134756215101980595515566901217084629217607502582265295755863799167702741408881294579819035951888562668951997777236828957162036234849207438819692480197365737237130918496390340939168630111890207700776894851839829623749822549994705192645373973493114436603297829506747411555800330860323339168875710029679
N = 6321130275268755691320586594611921079666212146561948694592313061609721619539590734495630218941969050343046016393977582794839173726817429324685098585960482266998399162720208269336303520478867387042992449850962809825380612709067651432344409349798118550026702892042869238047094344883994914342037831757447770321791092478847580639207346027164495372017699282907858775577530313354865815011726710796887715414931577176850854690237886239119894136091932619828539390021389626283175740389396541552356118540397518601098858527880603493380691706649684470530670258670128352699647582718206243920566184954440517665446820063779925391893
# continued fraction to get x, y
for yx in continued_fraction(e/N).convergents():
y = yx.numerator()
x = yx.denominator()
if 505 < int(y).bit_length() < 512:
print(y, x)
y = 191647030322314063933446306550000152159918198096618463122326606379318850479081854860589073369290364918610034409117430532516778779852605002825941902686890
x = 4192227114056319822897394360156470548388606366132514768963399681506785727010581839300216734934782392422359863064113714026255201119666834070559961903071641
# partial p + q => partial p
FF = RealField(10000)
p1q1 = int(FF(e*x) / FF(y)) # (p+1)*(q+1) + t, 0 < |t| < 2^430
p_q = p1q1 - N - 1 # MSB of p + q
var('x')
sol = solve([x^2 - p_q*x + N == 0], [x])
print(sol)
p0 = int(2*sqrt(444482467821025143305714555098099874062257553979119449654587984340699281800173027994633689136951044849243548793156705333028767010195164948611035937158387510451319737136925831287185593903136350487308896025780715187816715278067041814405142556822075770900097258406937616064638280659257536275473948985510922869901638270771121240358810519179840326427061370935104409890106181463960493575108004043632173319301775433061901082094379630436136367612780550559235163533614514983589177092871648322832771654828255238330634284842440455106445053680715818851887524046345927284003752325161056009416047812577970302873994123319001497039) + 89994778440489847480440137769232042484935231349681875012384488554074077511548785060223148718425106134106617171828275416695240510121868575799056004416229473935531531025990820027338788037021162036854950042026290515874475009680903735739096745933824119815539107450507126377435744105357286336630442890699073670007)
print("p0:", p0)
PR.<x> = PolynomialRing(Zmod(N))
f = p0 + x
f = f.monic()
roots = f.small_roots(X=2^430, beta=0.4)
if roots:
p = p0 + roots[0]
print(p)
# p = 132160284144608950019816194803720605665582054407890340625286428343034451279699999656554400403442321672129341860427814515935184696844617907072796285688260865300923112869612920717393389100962210593903755734372629195470923938634371604924606564978967830639867288297401137624219856087339978669043930742514051454567
|
这边一部分是学弟@Am473ur
帮忙整的
flag拆成两部分作为一个坐标点pt,相当于对点pt数乘e得到ct,(不过是线性实现的$O(n)$,2000 years later…)。
观察key_gen()部分的实现,k是e对$(p + 1) * (q + 1)$的逆元,只给了N和e,但没给k,那么k应该就是解密用的私钥了,本地测试数据发现只要对密文ct数乘k就可以解出明文pt了。
这里还需要重新实现一下点的数乘($O(logn)$),几秒就可以得到flag。
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
|
from Crypto.Util.number import *
e, N = (288969517294013178236187423377607850772706067194956328319540958788120421760563745859661120809993097599452236235703456953461446476016483100948287481999230043898368061651387268308645842547879026821842863879967704742559469599469159759360184157244907772315674219466971226019794131421405331578417729612598931842872757269134756215101980595515566901217084629217607502582265295755863799167702741408881294579819035951888562668951997777236828957162036234849207438819692480197365737237130918496390340939168630111890207700776894851839829623749822549994705192645373973493114436603297829506747411555800330860323339168875710029679,
6321130275268755691320586594611921079666212146561948694592313061609721619539590734495630218941969050343046016393977582794839173726817429324685098585960482266998399162720208269336303520478867387042992449850962809825380612709067651432344409349798118550026702892042869238047094344883994914342037831757447770321791092478847580639207346027164495372017699282907858775577530313354865815011726710796887715414931577176850854690237886239119894136091932619828539390021389626283175740389396541552356118540397518601098858527880603493380691706649684470530670258670128352699647582718206243920566184954440517665446820063779925391893)
ct = (5899152272551058285195694254667877221970753694584926104666866605696215068207480540407327508300257676391022109169902014292744666257465490629821382573289737174334198164333033128913955350103258256280828114875165476209826215601196920761915628274301746678705023551051091500407363159529055081261677043206130866838451325794109635288399010815200512702451748093168790121961904783034526572263126354004237323724559882241164587153748688219172626902108911587291552030335170336301818195688699255375043513696525422124055880380071075595317183172843771015029292369558240259547938684717895057447152729328016698107789678823563841271755,
253027286530960212859400305369275200777004645361154014614791278682230897619117833798134983197915876185668102195590667437488411251835330785944874517235915807926715611143830896296709467978143690346677123639363900536537534596995622179904587739684155397043547262126131676366948937690378306959846311626889534352806134472610026603322329394769864728875293696851590640974817297099985799243285824842399573006841275494668451690794643886677303573329060084436896592291515021246248961538322485059619863786362159459122242131918702862396595818404578595841492379025543989260901540257216728185425462070297720884398220421012139424567)
def add(p1, p2):
d = (((p2[1])**2 - 1) * inverse(((p2[1])**2 + 1) * (p2[0])**2, N)) % N
p = (int((p1[0] * p2[1] + p1[1] * p2[0]) * inverse(1 + d * p1[0] * p2[0] * p1[1] * p2[1], N) % N),
int((p1[1] * p2[1] + d * p1[0] * p2[0]) * inverse(1 - d * p1[0] * p2[0] * p1[1] * p2[1], N) % N))
return p
def mul(n, p):
r = (0, 1)
tmp = p
while 0 < n:
if n & 1 == 1:
r = add(r, tmp)
n, tmp = n >> 1, add(tmp, tmp)
return r
p = 132160284144608950019816194803720605665582054407890340625286428343034451279699999656554400403442321672129341860427814515935184696844617907072796285688260865300923112869612920717393389100962210593903755734372629195470923938634371604924606564978967830639867288297401137624219856087339978669043930742514051454567
q = N//p
k = inverse(e, (p + 1) * (q + 1))
pt = mul(k, ct)
print(long_to_bytes(pt[0])+long_to_bytes(pt[1]))
|
X-NUCA{Youve_Forg0tt3n_th3_t4ste_0f_Rea1_h0ney_6f36940f714710af}
hhh,居然是《闻香识女人》里的台词,有空一定去二刷。
服务端实现了一个Encryption With Authentication功能,并提供加密和解密这两个选项。
- 加密:加密的明文有一定的格式,
"Uid=%d\xff" + "Username=%s\xff" + "T=%s\xff" + "Cmd=%s\xff" + Appendix
,允许客户端在相应位置填入数据。其中,Uid
(最多5bytes)可以用来指定后面T
的位数,而剩余的地方都最多只能填16bytes,而且还只能是0~127的byte(encode("ascii")
)。加密会assemble这些内容,对其进行加密,返回密文以及对应的auth。
- 解密:会对客户端提供的cipher进行解密,还会验证客户端提供的auth是否与cipher对应。
如果解密的内容满足以下2项要求,就能getflag:
- Username=Administrator
- Cmd=Give_Me_Flag
但是在加密的时候,不能填入这些数据。
审计具体加解密流程后,可以画一张图:
想了挺久怎么去flip bit来修改明文内容,但是这个铁锁连环实在是太难日了。
那么可以这么考虑:这2个16bytes的明密文可以看作是一组32bytes的明密文,这一组就是一个很难日的东西,但是组与组之间是相互独立的,可以通过巧妙地控制组与组之间的边界,来拿到我们想要的密文。
数了一下位数,发现如果Uid
部分填上5byte,Username
部分填上Administrator
,第一组刚好32bytes,那么可以继续在Administrator
后填上1byte;这样,通过uname == b"Administrator"
检测的同时,还能使得第一组密文所对应的明文为Uid=?????\xffUserName=Administrator
,只要再去找到一个开头是\xffT=...
第二组密文即可。利用同样的方法,稍微控制一下T
的长度,也可以拿到Cmd=Give_Me_Flag
的密文。
为此,构造了两组msg:
m1
- Uid: 10015
- Username: Administratorr
- Cmd: Give_Me_lag
- Appendix: fmyyfmyyfmyy
m2
- Uid: 10015
- Username: Bdministrator
- Cmd: Give_Me_F
- Appendix: aaaaaaaaaaaaaaa
那么c1的第一组 + c2的第二组 + c1的第三组
就可以满足那2项要求。
1
2
3
|
mm = b'Uid=10015\xffUserName=Administrator' + b'\xffT=5fdc5cf1b362311\xffCmd=Give_Me_F' + b'lag\xfffmyyfmyyfmyy'
m1 = b'Uid=10015\xffUserName=Administrator' + b'r\xffT=ab86207b745a777\xffCmd=Give_Me_' + b'lag\xfffmyyfmyyfmyy'
m2 = b'Uid=10015\xffUserName=Bdministrator' + b'\xffT=5fdc5cf1b362311\xffCmd=Give_Me_F' + b'\xffaaaaaaaaaaaaaaa'
|
构造auth:只需要让Sigma的最终值与mm
的一样即可。
1
2
|
# auth(mm) == auth(m3)
m3 = b'Uid=10014\xffUserName=Administrator' + b'r\xffT=ab86207b745a77\xffCmd=fmyyfmyyf' + b'jgg\xfffmyXbbe@Fq_Y'
|
上图红色的密文即为我们解密用的ticket,auth可以通过构造m3得到。
m3构造的时候,也可以选择不填充"fmyy",填充一些其他东西,方便构造。
exp.py如下:
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
|
import re
from hashlib import sha256
from itertools import product
from pwn import *
ALPHABET = string.ascii_letters + string.digits
def xor(a, b):
return bytes(x^y for x,y in zip(a,b))
DEBUG = False
# context.log_level = "debug"
if DEBUG:
r = remote("127.0.0.1", 45216)
else:
r = remote('123.57.4.93', 45216)
# proof of work
rec = r.recvline().decode()
suffix = re.findall(r"\(XXXX\+b\'(.*?)\'", rec)[0]
digest = re.findall(r"== b\'(.*?)\'", rec)[0]
log.info(f"suffix: {suffix} \ndigest: {digest}")
log.info('Calculating hash...')
for i in product(ALPHABET, repeat=4):
prefix = ''.join(i)
guess = prefix + suffix
if sha256(guess.encode()).hexdigest() == digest:
log.info(f"Find XXXX: {prefix}")
break
r.sendlineafter(b'Give me XXXX:', prefix.encode())
# m1
r.sendlineafter(b"Your option:", b"1")
r.sendlineafter(b"Set up your user id:", b"10015")
r.sendlineafter(b"Your username:", b"Administratorr")
r.sendlineafter(b"Your command:", b"Give_Me_lag")
r.sendlineafter(b"Any Appendix?", b"fmyy"*3)
r.recvuntil(b"Your ticket:")
c1 = r.recvline().strip()
# m2
r.sendlineafter(b"Your option:", b"1")
r.sendlineafter(b"Set up your user id:", b"10015")
r.sendlineafter(b"Your username:", b"Bdministrator")
r.sendlineafter(b"Your command:", b"Give_Me_F")
r.sendlineafter(b"Any Appendix?", b"a"*15)
r.recvuntil(b"Your ticket:")
c2 = r.recvline().strip()
ticket = c1[:64] + c2[64:128] + c1[128:]
# m3
r.sendlineafter(b"Your option:", b"1")
r.sendlineafter(b"Set up your user id:", b"10014")
r.sendlineafter(b"Your username:", b"Administratorr")
r.sendlineafter(b"Your command:", b"fmyyfmyyfjgg")
r.sendlineafter(b"Any Appendix?", b"fmyXbbe@Fq_Y")
r.recvuntil(b"With my Auth:")
auth = r.recvline().strip()
r.sendlineafter(b"Your option:", b"2")
r.sendlineafter(b"Ticket:", ticket)
r.sendlineafter(b"Auth:", auth)
r.interactive()
|
是电影《猫鼠游戏》里的台词。
题目分了3层。
-
首先是一个320*5
的矩阵$A$,乘上了一个随机变换矩阵5*7
的矩阵$R$,得到了一个320*7
的矩阵$B$(不过数据给的是7*320
的$B^T$
1
2
|
A = random_matrix(ZZ, 320, 5, x = 10, y = 1000)
B = Matrix(A * vector([randint(1, 2^1024) for _ in range(5)]) for _ in range(7))
|
-
然后是一个LWE,生成了64组数据,$s \cdot A_{lwe} + e = a$,没有直接给我们$A_{lwe}$和$a$。只给了$M = A_{lwe} \oplus A$,以及用$s$作为AES的key,对flag进行了加密。
1
2
3
4
|
L = LWE(n = 25, q = 1000, D = DGDIS(3))
S = [L() for _ in range(64)]
M = Matrix(64, 25, [int(i).__xor__(int(j)) for i,j in zip(A.list(), (Matrix([x for x, _ in S])).list())])
|
-
再就是一个knapsack problem,用长度为64的向量$a$与一个另外一个很大的长度为64的随机向量$T$相乘,得到一个很大的数$sum$。给了T以及$sum$。
1
2
|
T = Matrix([randint(1, 2^1024) for _ in range(64)])
R = T.transpose().stack(T * vector([y for _, y in S]).change_ring(ZZ))
|
先来解决knapsack problem,虽然不是0-1 knapsack problem(也就是subset-sum problem),但由于$a$很小,所以还是可以用subset-sum problem的格子来解决这个问题。
$$
a_0 T_0 + a_1 T_1 + \cdots + a_{63}T_{63} = sum
$$
那么可以构造如下格子,显然$(a_0, a_1, \cdots, a_{63}, 0)$是这个格子上的一个格点,且很小,可用LLL规约出来。
1
2
3
4
5
6
|
T = [...]
L_knapsack = identity_matrix(64).stack(vector([0]*64)).augment(matrix(65, 1, T[:-1] + [-T[-1]]))
L_reduced = L_knapsack.LLL()
a = L_reduced[0][:-1]
print(a)
# (868, 798, 863, 260, 206, 550, 326, 908, 49, 50, 273, 528, 584, 569, 975, 261, 885, 680, 116, 33, 677, 664, 922, 178, 999, 336, 60, 655, 102, 438, 269, 754, 988, 124, 10, 380, 589, 382, 668, 623, 335, 845, 104, 117, 961, 917, 114, 590, 255, 26, 81, 846, 925, 548, 446, 796, 543, 997, 492, 651, 485, 137, 701, 247)
|
再来看一下如何恢复出LWE的$A_{lwe}$,想要恢复$A_{lwe}$,首先得知道$A$,然后跟$M$异或就行。所以现在的问题是:如何求$A$?
关于$A$,我们还知道另外一个条件是:
$$
A \cdot R = B
$$
但是,这个式子里面只有$B$是已知的。
两个矩阵$A, R$相乘得到$B$,仅从$B$反推出$A, R$,这真的可以么???
确实是可以的,可能这就是lattice的魅力所在。
为了方便叙述,对$A \cdot R = B$两边同时transpose一下,可以得到$R^T \cdot A^T = B^T$.
$A^T$是一个由5 * 320
个非常小的元素(10 <= x < 1000
)构成的矩阵,$R^T$是一个由7 * 5
个非常大的元素(1 <= x < 2^1024
)构成的矩阵;而$B^T$也是一个由7 * 320
个非常大的元素构成的矩阵。
不难发现,$B^T$中的每一个行向量,都是$A^T$所有行向量的一个线性组合。也就是说$B^T$中的每一个行向量都在$A^T$所组成的格子中。我们只需要对$B^T$进行格基规约,就能找到$A^T$中的部分行向量($A^T$中的行向量都非常小)。
通过各种方法,尝试对7 * 320
的$B^T$进行规约,包括:使用LLL\BKZ算法、置换$B^T$中行向量的排序、修改算法的delta
值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
from tqdm import tqdm
B = [...]
BB = []
for i in range(0, len(B), 320):
BB.append(B[i:i+320])
As = []
for i in tqdm(range(1000)):
shuffle(BB)
for line in matrix(len(BB), 320, BB).BKZ(delta=float(randint(75000, 99999)/100000)):
if line[0] < 0:
line = -line
if line not in As and all(map(lambda x: 10 <= x <= 1000, line)):
print(len(BB), line)
As.append(line)
|
但是最后,都只能够得到4组行向量:
还有一个行向量,死活都整不出来。。。
有可能是这最后一个行向量的范数太小了,格基规约算法找不到这么小的。
所以,考虑了一下降维处理。
降维,只会减少到最后得到的LWE的组数,只要组数不是太小,LWE还是可以解出来的。
直接从320维降到了200维:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
from tqdm import tqdm
B = [...]
BB = []
for i in range(0, len(B), 320):
BB.append(B[i:i+200])
As = []
for i in tqdm(range(1000)):
shuffle(BB)
for line in matrix(len(BB), 2000, BB).BKZ(delta=float(randint(75000, 99999)/100000)):
if line[0] < 0:
line = -line
if line not in As and all(map(lambda x: 10 <= x <= 1000, line)):
print(len(BB), line)
As.append(line)
|
很快就能求出所有的$A^T$的5组行向量(虽然长度变短了一些):
有了这5组行向量,就可以得到200 * 5
的$A$,用这200 * 5
的$A$和$M$的前1000个元素异或即可得到$A_{lwe}$
这5组行向量的顺序是随机的,所以需要全排列一下,一共有$5!$种可能的$A_{lwe}$
1
2
3
4
5
6
7
|
from itertools import permutations
MM = [...]
for p in permutations(As, int(5)):
A = matrix(5, 200, p).transpose()
A_LWE = Matrix(40, 25, [int(i).__xor__(int(j)) for i,j in zip(A.list(), MM)])
|
最后就是要解LWE。
LWE实际上跟GGH
很类似,都是一个向量,经过矩阵变换后,加上了一些误差向量,得到一个结果向量;然后就很难反推回去。LWE与GGH不同的一点在于,GGH中的误差向量的选取是3或者-3,而LWE的误差向量则是一个满足正态分布的小向量。GGH正是因为只有3和-3,Nguyen直接对结果向量mod 6,就能得到明文的部分信息,进而将GGH所构造的CVP转化为了一个更容易解决的CVP。
LWE可以这么来理解:
如果这里不是“约等于”,而是“等于”,那么用高斯消元法可以很容易解出来。但正是因为加入了一些误差,如果使用高斯消元法的话,这些误差会聚集起来,使得解出来的东西跟实际值差很多。
也可以这么理解:
$$
s \cdot A + e = a
$$
$A$是一个格子,然后这个格子基底的线性组合$b = A\cdot s$,是这个格子上的一个格点,然后这个格点$b$加上了一个误差向量$e$,得到了一个非格点$a$。LWE就是要我们找到离这个非格点$a$最近的一个格点,即CVP。
不过LWE的困难度被证明是基于最坏情况的SIVP困难度,属于CVP中比较难的问题。
更多有关内容,请看大佬的博客:http://blog.higashi.tech/
但是这题里面$s$的长度很小,只有25,所以这个LWE所对应的CVP问题是比较容易解决的(维度高了后,LWE就很难了)。
怎么解呢?
LWE的式子
$$
s_0 a_{i,0} + s_1 a_{i,1} + \cdots + s_{24} a_{i,24} \equiv a_i - e_i \pmod{p}
$$
可以化为:
$$
s_0 a_{i,0} + s_1 a_{i,1} + \cdots + s_{24} a_{i,24} + k_ip + e_i = a_i
$$
那么可以构造矩阵$L$:
先对矩阵$L$进行规约,得到一个good basis,再用Babai’s algorithm求解CVP,即可得到离$a$最近的格点$b$。
然后,在$\bmod{1000}$的整数环里,解如下方程即可
$$
A_{lwe} \cdot s = b^{T}
$$
解出$s$后,对密文解密即可getflag:
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
|
from itertools import permutations
from Crypto.Cipher import AES
from tqdm import tqdm
from hashlib import sha256
from sage.modules.free_module_integer import IntegerLattice
def BabaisClosestPlaneAlgorithm(L, w):
'''
Yet another method to solve apprCVP, using a given good basis.
INPUT:
* "L" -- a matrix representing the LLL-reduced basis (v1, ..., vn) of a lattice.
* "w" -- a target vector to approach to.
OUTPUT:
* "v" -- a approximate closest vector.
Quoted from "An Introduction to Mathematical Cryptography":
In both theory and practice, Babai's closest plane algorithm
seems to yield better results than Babai's closest vertex algorithm.
'''
G, _ = L.gram_schmidt()
t = w
i = L.nrows() - 1
while i >= 0:
w -= round( (w*G[i]) / G[i].norm()^2 ) * L[i]
i -= 1
return t - w
data = bytes.fromhex("c338be5406289b99332176593ae94b5e254df0e6b31b3155f370845e99d55f3a5b8b9e5576a126512b93eacacb6b7865f925120c3a221d0a2fcff362d841ad6be183a796f0c0a8111704737b6fc412f4")
iv, ct = data[:16], data[16:]
module = 1000
row = 40
column = 25
a = vector(ZZ, a[:40])
M = [...]
for p in permutations(As, int(5)):
A = matrix(5, 200, p).transpose()
A_LWE = Matrix(40, 25, [int(i).__xor__(int(j)) for i,j in zip(A.list(), M)])
# solve LWE
Lattice = matrix(ZZ, row + column, row)
for i in range(row):
for j in range(column):
Lattice[row + j, i] = A_LWE[i][j]
Lattice[i, i] = module
lattice = IntegerLattice(Lattice, lll_reduce=True)
# target = vector(ZZ, LWE_c[:row])
closest_vector = BabaisClosestPlaneAlgorithm(lattice.reduced_basis, a)
e = closest_vector - a # error vector
print(e.norm()^2, e)
A_LWE = matrix(Zmod(module), A_LWE)
try:
s = A_LWE.solve_right(closest_vector)
except:
print("no solution")
continue
# try decryption
key = sha256(''.join(list(map(str, s))).encode()).digest()
cipher = AES.new(key, AES.MODE_CBC, iv)
pt = cipher.decrypt(ct)
print(pt)
if b"NUCA" in pt:
break
|
代码没测试,直接复制jupyter里面的,可能有点小问题
又是《美丽心灵》里的经典台词。
嗯,这几个flag提醒我该多去看看电影了。
X-NUCA的题目质量总是那么的顶,从去年的一道re+cry题看到自闭,到今年还能解出来2道题,感觉确实学到了很多。