BUUCTF 每日打卡 2021-5-11

引言

求 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 究竟能学到什么呢?只能说长点见识吧,我见的还是太少了
太弱小了,因为 我们没有力量~
希望继续坚持

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值