引言
求 wp 的时候,大佬们告诉我 2021 红帽杯的 crypto 都是原题。。。
一共有三份 wp
其中两份的思路是一样(也就是原题的解答),另外一份是和我同级的 Pheonix dl 当时的解答(感谢 Pheonix dl!)花了一小时解出来的
[2021 红帽杯]primegame
原题加密代码如下:
#!/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 然后四舍五入。
对比红帽杯的加密代码:
#!/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
2^{2^{8}}
228,联想到字符 8 位二进制(虽然没什么根据就是了)
最后也没有思路就是了。。。
wp 的解法也都是转化为对两段分别进行背包问题求解
Peonix dl 解法
dl 的手稿:
首先,将 flag 前24位转化为 24*8 位的二进制
因为原来的 keys 是直接对每位 flag 进行操作,而现在是要对每位二进制进行操作,所以要重新构造 keys
代码如下:
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 直接作为字符串考虑,而不是转化为二进制再求解
代码如下:
from decimal import *
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
# sage代码:
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
2^{256}
2256 造成了很大误差,所以将其放在 N 环上而不是整数环上讨论(我也不懂这是什么道理,但是感觉很有道理)
而 keys 就和原来一样,只是换了个表示
结语
事实上,类似的问题也可以用类似的方法解,如原题 wp 中提到的 cryptohack 中的一题
太晚了明天再说
分析 wp 究竟能学到什么呢?只能说长点见识吧,我见的还是太少了
太弱小了,因为 我们没有力量~
希望继续坚持