0%

BUUCTF 每日打卡 2021-5-12

BUUCTF 每日打卡 2021-5-12

引言

求 wp 的时候,大佬们告诉我 2021 红帽杯的 crypto 都是原题。。。 一共有三份 wp 其中两份的思路是一样(也就是原题的解答),另外一份是和我同级的 Pheonix dl 当时的解答(感谢 Pheonix dl!)花了一小时解出来的

[2021 红帽杯]primegame

原题加密代码如下:

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
#!/usr/bin/env python3

from decimal import *
import math
import random
import struct

from flag import flag

primes = [2]
for i in range(3, 100):
f = True
for j in primes:
if i * i < j:
break
if i % j == 0:
f = False
break
if f:
primes.append(i)

# Random shuffle the primes
# Now you cannot know the order
seed = struct.unpack('<i', flag[5:9])[0]
random.seed(seed)
random.shuffle(primes)

# Use ln function
# Now you cannot know the key itself
getcontext().prec = 100
keys = []
for i in range(len(flag)):
keys.append(Decimal(primes[i]).ln())

# Sum values
# Now you cannot know the flag
sum_ = Decimal(0.0)
for i, c in enumerate(flag):
sum_ += c * Decimal(keys[i])

ct = math.floor(sum_ * 2 ** 256)
print(ct)
该代码的结构如下: 1、查找不超过100的素数。(共25个) 2、通过使用 flag 的第6到第9字节作为随机种子,可以对十进制素数数组进行改组。 3、每个改组的素数都取以 e 为底的对数,十进制精度为100。 4、将上一步得到的 ln 值乘以标记的每个字节所获得的值相加,乘以 2 ** 256 然后四舍五入。

对比红帽杯的加密代码:

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
#!/usr/bin/env python3

from decimal import *
import math
import random
import struct
from flag import flag

assert (len(flag) == 48)
msg1 = flag[:24]
msg2 = flag[24:]
primes = [2]
# 获取 [2,90] 的素数
for i in range(3, 90):
f = True
for j in primes:
if i * i < j: # i^2 >= j
break
if i % j == 0:
f = False
break
if f:
primes.append(i)

getcontext().prec = 100 # 保留 100 位小数
keys = []
for i in range(len(msg1)): # len(primes) = 24
keys.append(Decimal(primes[i]).ln())

sum_ = Decimal(0.0)
for i, c in enumerate(msg1):
sum_ += c * Decimal(keys[i])

ct = math.floor(sum_ * 2 ** 256)
print(ct)
# 597952043660446249020184773232983974017780255881942379044454676980646417087515453
sum_ = Decimal(0.0)
for i, c in enumerate(msg2):
sum_ += c * Decimal(keys[i])

ct = math.floor(sum_ * 2 ** 256)
print(ct)
# 425985475047781336789963300910446852783032712598571885345660550546372063410589918
发现原题代码结构中的第二步(也就是打乱素数数组顺序)是没有的(我说哪来的 random 和 struct 库😂) 而且将 flag 分成两段加密,因此给了两个 ct

首先,这可以转化成背包加密问题 背包加密是对密文的二进制字符串进行加密 至于为什么这道题是背包加密,就我个人的理解的话,大多数加密问题都可以转化成背包问题解决(例如 RSA 中的 CopperSmith 攻击法),给我灵感的是 2 ** 256 也就是 \(2^{2^{8}}\),联想到字符 8 位二进制(虽然没什么根据就是了) 最后也没有思路就是了。。。

wp 的解法也都是转化为对两段分别进行背包问题求解

Peonix dl 解法

dl 的手稿: 在这里插入图片描述 首先,将 flag 前24位转化为 24*8 位的二进制 因为原来的 keys 是直接对每位 flag 进行操作,而现在是要对每位二进制进行操作,所以要重新构造 keys 代码如下:

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
from decimal import *
import math

flag = []
for i in range(48*8):
flag.append(0)
assert (len(flag) == 48*8)
msg1 = flag[:24*8]
msg2 = flag[24*8:]
primes = [2]
for i in range(3, 90):
f = True
for j in primes:
if i * i < j:
break
if i % j == 0:
f = False
break
if f:
primes.append(i)

getcontext().prec = 100
keys = []
for i in range(len(msg1)):
keys.append(math.floor(2 ** 256 * (2 ** (i % 8)) * Decimal(primes[i//8]).ln()))
print(keys)

需要注意的是这里的二进制字符串是倒序的,之后解密需要倒回来 如手稿中所写的,由于对 keys 向下取整了,所以答案会偏小,所以需要调整 c(据说调整花了大部分时间,这也是这个方案的一个缺陷) 然后接下来就是构造矩阵,用 LLL 算法解出最短向量得到结果了 这里注意最后结果是一个最后一位是 0,其他都是 -1 或 1 的向量,有把 -1 替换成 0 和 1 替换成 0 并且 -1 替换成 1 两种情况,需要分别尝试 在这里插入图片描述 怎么构造矩阵参照背包加密问题,至于 LLL 算法,曾经在研究 cryptohack 的时候 dl 尝试复现过,但是非常慢(当然我也试过,但是失败了),用 sagemath 封装好的速度会比较快 最后把结果倒序之后 long_to_bytes 就行了 结果为: 在这里插入图片描述 我个人认为难点在于构造 keys 以及想到去构造 keys ,还有就是存在需要调整 c 这个缺陷 至于其他部分,想到背包问题就是顺理成章的了

原题解法

原题中,将 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
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
import math
from decimal import *
import random
import struct

getcontext().prec = int(100)

primes = [2]
for i in range(3, 100):
f = True
for j in primes:
if i * i < j:
break
if i % j == 0:
f = False
break
if f:
primes.append(i)

keys = []
for i in range(len(primes)):
keys.append(Decimal(int(primes[i])).ln())

arr = []
for v in keys:
arr.append(int(v * int(16) ** int(64)))

ct = 737384863737803670841307970259513146291422299366557325168325233349136771464845311
#ct = 425985475047781336789963300910446852783032712598571885345660550546372063410589918

def encrypt(res):
h = Decimal(int(0))
for i in range(len(keys)):
h += res[i] * keys[i]

ct = int(h * int(16)**int(64))
return ct

def f(N):
ln = len(arr)
A = Matrix(ZZ, ln + 1, ln + 1)
for i in range(ln):
A[i, i] = 1
A[i, ln] = arr[i] // N
A[ln, i] = 64

A[ln, ln] = ct // N

res = A.LLL()

for i in range(ln + 1):
flag = True
for j in range(ln):
if -64 <= res[i][j] < 64:
continue
flag = False
break
if flag:
vec = [int(v + 64) for v in res[i][:-1]]
ret = encrypt(vec)
if ret == ct:
print(N, bytes(vec))
else:
print("NO", ret, bytes(vec))

for i in range(2, 10000):
print(i)
f(i)
由于 flag 实际上是可以输入的值,所以将这个问题视为0~127背包而不是0/1背包,因此它们不能超过128,因此我们将边界设置为0~127。 类似于上文所述的方法构造矩阵,对角线位置相同,而将最后一行前面的数替换成 64 (128/2),不同上文的 1 (2/1) 而在寻找最短向量的时候将范围限制在 [-64, 64] ,不同于上文的 -1/1 而至于整除 N 的操作,可以理解为是一种误差处理的方式,由于加密时乘 \(2^{256}\) 造成了很大误差,所以将其放在 N 环上而不是整数环上讨论(我也不懂这是什么道理,但是感觉很有道理) 而 keys 就和原来一样,只是换了个表示

结语

事实上,类似的问题也可以用类似的方法解,如原题 wp 中提到的 cryptohack 中的一题 太晚了明天再说 分析 wp 究竟能学到什么呢?只能说长点见识吧,我见的还是太少了 太弱小了,因为 我们没有力量~ 希望继续坚持

欢迎关注我的其它发布渠道

-------- 本文结束 感谢阅读 --------