背包密码

背包密码

背包问题与超递增序列

1. 背包问题 (Subset Sum Problem)

背包密码的核心源于一个著名的组合数学难题——子集和问题(Subset Sum Problem)。

问题描述

\[给定一组整数序列 A = \{a_1, a_2, \dots, a_n\} 和一个目标值 S, \\是否存在一个二进制向量 x = \{x_1, x_2, \dots, x_n\}(其中 x_i \in \{0, 1\}),使得: \\ S = \sum_{i=1}^{n} x_i a_i 通俗地说,就是能否从一堆数字中挑出几个,让它们的和恰好等于 S \]

在一般情况下,这是一个 NP-Hard 问题。随着 n 的增大,暴力破解的复杂度呈指数级上升,几乎无法在多项式时间内求解。

2. 超递增序列 (Superincreasing Sequence)

为了将这个难题用于加密,我们需要一个“陷门”——即在某种特殊情况下,这个问题变得非常容易解决。这就是超递增序列

定义

一个序列 A 是超递增的,当且仅当序列中的每一个元素都大于它前面所有元素的和。即:

\[a_i > \sum_{j=1}^{i-1} a_j \]

性质:对于超递增序列构成的背包问题,我们可以使用贪心算法(Greedy Algorithm)在 O(n)时间内求出解。

\[从最大的元素 a_n 开始看,如果 a_n \le S,\\那么 a_n 必选(x_n=1),\\然后 S = S - a_n。 如果 a_n > S,那么 a_n 必不选(x_n=0)。\\ 依次类推,直到遍历完所有元素。 \]

Merkle-Hellman 加解密过程

1. 密钥生成 (Key Gen)

  1. 生成私钥

    构建一个超递增序列

    \[A = \{a_1, \dots, a_n\}。 \]

    选择一个模数 p,使得

    \[p > \sum a_i。 \]

    选择一个乘数 q,使得

    \[gcd(p, q) = 1(互素) \]

    私钥为 (A, p, q)。

  2. 生成公钥

    计算序列 B,其中

    \[B_i \equiv a_i \times q \pmod p \]

    公钥为序列 B

2. 加密 (Encryption)

发送方将明文消息转化为二进制向量

\[x = \{x_1, \dots, x_n\} \]

,利用公钥 B 计算密文 S:

\[S = \sum_{i=1}^{n} x_i B_i \]

3. 解密 (Decryption)

接收方拥有私钥 (A, p, q)

  1. 计算 q 在模 p 下的逆元 q^

  2. 将密文 S 还原为超递增序列下的和 S':

    \[S' \equiv S \times q^{-1} \equiv (\sum x_i a_i q) q^{-1} \equiv \sum x_i a_i \pmod p \]

  3. 此时问题转化为了求解

    \[\sum x_i a_i = S' \]

    由于 A 是超递增序列,直接使用贪心算法即可解出 x(即明文)。

背包密码求解

由于 Merkle-Hellman 系统的公钥虽然不是超递增的 但密度通常较低(即d <0.9408) 这使得我们可以将其转化为最短向量问题 (SVP),利用 LLLBKZ 算法进行格基规约攻击

\[密度d = \frac{n}{\log_2(\max a_i)} \]

n为维度,也是公钥元素的数量 max a_i就是公钥中最大的元素

根据题目的不同(有无模数、密度高低)我们可以构造不同的格 对于形如

\[\sum x_i B_i = S \]

可以打如下格

第一类格

构造思路

我们需要寻找向量x

\[使得 \sum x_i B_i - S = 0 \]

构造如下矩阵:

\[M = \begin{pmatrix} 1 & 0 & \dots & 0 & N \cdot B_1 \\ 0 & 1 & \dots & 0 & N \cdot B_2 \\ \vdots & \vdots & \ddots & \vdots & \vdots \\ 0 & 0 & \dots & 1 & N \cdot B_n \\ 0 & 0 & \dots & 0 & -N \cdot S \end{pmatrix} \]

关系是

\[\begin{pmatrix} x_1 & x_2 & … & x_n & -1 \end{pmatrix} \begin{pmatrix} 1 & 0 & … & 0 & N \cdot M_1\\ 0 & 1 & … & 0 & N \cdot M_2\\ \vdots & \vdots & \ddots & \vdots & \vdots \\ 0 & 0 & … & 1 & N\cdot M_n\\ 0 & 0 & … & 0 & N\cdot S \end{pmatrix} = \begin{pmatrix} x_1 & x_2 & … & x_n & 0 \end{pmatrix} \]

第二类格

\[\begin{pmatrix} 2 & 0 & \dots & 0 & N \cdot M_1 \\ 0 & 2 & \dots & 0 & N \cdot M_2 \\ \vdots & \vdots & \ddots & \vdots & \vdots \\ 0 & 0 & \dots & 2 & N \cdot M_n \\ 1 & 1 & \dots & 1 & N \cdot S \end{pmatrix} \]

关系是

\[\begin{pmatrix} x_1 & x_2 & \dots & x_n & -1 \end{pmatrix} \begin{pmatrix} 2 & 0 & \dots & 0 & N \cdot M_1 \\ 0 & 2 & \dots & 0 & N \cdot M_2 \\ \vdots & \vdots & \ddots & \vdots & \vdots \\ 0 & 0 & \dots & 2 & N \cdot M_n \\ 1 & 1 & \dots & 1 & N \cdot S \end{pmatrix} = \begin{pmatrix} 2x_1-1 & 2x_2-1 & \dots & 2x_n-1 & 0 \end{pmatrix} \]

\[不难发现最后得出的向量元素由0,-1 全部转化为了\pm 1 \]

所以可以攻击密度更大的背包

带模的背包加密

形如

\[S = \sum_{i=0}^{n}x_iM_i \mod p \longrightarrow S = \sum_{i=0}^nx_iM_i - kp \]

一半有两种方式 构造和爆破

构造格子则转变为

\[M = \begin{pmatrix} 1 & 0 & \dots & 0 & 0 & W \cdot a_1 \\ 0 & 1 & \dots & 0 & 0 & W \cdot a_2 \\ \vdots & \vdots & \ddots & \vdots & \vdots & \vdots \\ 0 & 0 & \dots & 1 & 0 & W \cdot a_n \\ 0 & 0 & \dots & 0 & 1 & -W \cdot S \\ 0 & 0 & \dots & 0 & 0 & W \cdot p \end{pmatrix} \]

\[\begin{pmatrix} x_1 & x_2 & … & x_n & -1 & k \end{pmatrix} \begin{pmatrix} 2 & 0 & … & 0 & 0 &M_1\\ 0 & 2 & … & 0 & 0 &M_2\\ \vdots & \vdots & \ddots &\vdots& \vdots & \vdots \\ 0 & 0 & … & 2 & 0 &M_n\\ 0 & 0 & … & 0 & 1 & S\\ 1 & 1 & … & 1 & 0 & p \end{pmatrix} = \begin{pmatrix} 2x_1-1 & 2x_2-1 & … & 2x_n-1 &1 & 0 \end{pmatrix} \]

由于加密消息的长度并不会很长,所以的范围是在可爆破范围内的,我们就可以爆破k,把带模背包转换为不带模的背包进行求解。

下面举一道例题

PCTF-背包包

import random
from sympy import nextprime

private_key = []
s = 1000
nbits=248
for i in range(nbits):
    private_key.append(s)
    s = 2*s + 1    #超递增序列

S = sum(private_key)  #私钥求和
m=S+1000
m=nextprime(m)        #生成一个模数

n = 65537
public_key = [(x * n) % m for x in private_key] #就是基本的私钥生成方式


flag_bin = ''.join(format(ord(c), '08b') for c in flag) #将flag转化为二进制


cipher = sum(int(flag_bin[i]) * public_key[i] for i in range(248)) #加密
print("Public key (first 10):", public_key[:10])
print("Ciphertext:", cipher)

with open('private_key.txt', 'w') as f:
    for x in private_key:
        f.write(f"{x}\n")


with open('public_key.txt', 'w') as f:
    for x in public_key:
        f.write(f"{x}\n")

with open('ciphertext.txt', 'w') as f:
    f.write(str(cipher))

个人感觉题目出的挺奇怪 题目基本上把条件全部放出 公钥 私钥 密文 乘数 模数的生成方式甚至也给出了

from pathlib import Path
from sympy import nextprime
#open("output.txt",'r').readlines()
private_key = [int(line) for line in Path('private_key.txt').read_text(encoding='utf-8').splitlines() if line.strip()]
public_key = [int(line) for line in Path('public_key.txt').read_text(encoding='utf-8').splitlines() if line.strip()]
c=359049539563494630362441380475930159184403585611307929664706367511971196282089   #处理数据
n=65537
# S = sum(private_key)
# m=S+1000
# m=nextprime(m)
# print(m)     可以得出m
m=452765161431849654761697484350377327191887713477758611732410318718441573318479

方法一:直接解密

处理一下数据 恢复出m 然后直接进行解密

from Crypto.Util.number import long_to_bytes, inverse

priv_key = private_key # 私钥 (必须是超递增序列)
r =n                   # 乘数 (Multiplier / r)
q = m                  # 模数 (Modulus / q)
enc = c                # 密文 (Ciphertext / enc)


def decrypt():
    try:
        r_inv = pow(r, -1, q)
    except ValueError:
        print("错误: r 和 q 不互素")
        return

    s = (enc * r_inv) % q            #恢复原始的子集和 (S)
    print(f"[*] 恢复出的私钥背包和 S: {s}")

    # 贪心算法求解超递增背包
    # 关键点:必须从最大的数开始判断

    # 假设 priv_key 是从小到大排序的,我们需要倒序遍历
    # 如果 priv_key 是乱序的,请先通过 priv_key.sort() 排序,但要记住原始索引(通常私钥都是排好序的)

    n = len(priv_key)
    bits = [0] * n

    current_val = s

    for i in range(n - 1, -1, -1):
        if current_val >= priv_key[i]:
            bits[i] = 1
            current_val -= priv_key[i]               # 从最后一个元素(最大值)往前遍历
    bits_str = "".join(str(b) for b in bits)         #拼接
    decrypted_int = int(bits_str, 2)                 # 转成整数
    try:
        flag = long_to_bytes(decrypted_int)
        print(f"\n[+] Flag: {flag}")
    except Exception as e:
        print(f"\n[-] 转换字符失败: {e}")
        print(f"    解出的十进制数: {decrypted_int}")


if __name__ == '__main__':
    decrypt()
    
   
#[*] 恢复出的私钥背包和 S: 337501605441327537559680912261243406291671620342723576121601135181665619588181
#[+] 成功还原二进制位!

#[+] Flag: b'flag{ca22y_0n_th3_kn4p5Ack_t0+}'

方法二:直接打BKZ

回头看看这道题能不能打LLL呢 我们需要先计算一下密度

import math


def check_density(pubkey):
    # n: 维度 (公钥的个数)
    n = len(pubkey)

    # max_val: 公钥中最大的那个数
    max_val = max(pubkey)

    log_max = math.log2(max_val)

    # 计算密度
    density = n / log_max

    print(f"[-] 维度 (n): {n}")
    print(f"[-] 最大值比特数: {log_max:.4f}")
    print(f"[*] 密度 (d): {density:.4f}")

    if density < 0.9408:
        print("[+] 密度低 (< 0.9408)")
    else:
        print("[!] 密度高 (> 0.9408)")

    return density

check_density(public_key)



#[-] 维度 (n): 248
#[-] 最大值比特数: 256.9672
#[*] 密度 (d): 0.9651
#[!] 密度高 (> 0.9408)

看起来貌似打不了 实际操作下来 LLL没打出来 但是BKZ(block_size=26) and Lattice2确实可以解

from sage.all import *
from Crypto.Util.number import long_to_bytes

enc=c
pubkey= public_key

def solve_cjloss_bkz(pubkey, target, block_size=26):
    n = len(pubkey)
    pubkey = [ZZ(x) for x in pubkey]
    target = ZZ(target)
    M = Matrix(ZZ, n + 1, n + 1)
    for i in range(n):
        M[i, i] = 2
        M[i, n] = pubkey[i]
    for i in range(n):
        M[n, i] = 1
    M[n, n] = target
    res = M.BKZ(block_size=block_size)
    for row in res:
        if row[n] == 0:
            potential_sol = []
            valid = True
            for i in range(n):
                val = row[i]
                if val == 1:
                    potential_sol.append(1)  # 2x-1=1 -> x=1
                elif val == -1:
                    potential_sol.append(0)  # 2x-1=-1 -> x=0
                else:
                    valid = False
                    break

            if valid:
                return potential_sol


    return None

if __name__ == '__main__':
    bits = solve_cjloss_bkz(pubkey, enc, block_size=26)

    if bits:
        print("[+] 攻击成功!")
        bits_str = "".join(str(b) for b in bits)
        try:
            m_int = int(bits_str, 2)
            print(f"FLAG: {long_to_bytes(m_int)}")
        except Exception as e:
            print(f"解码失败: {e}")
    else:
        print("[-] BKZ-26 失败。尝试增大 block_size (如 30, 35)。")
        
        
#[+] 攻击成功!
#FLAG: #b'\x99\x93\x9e\x98\x84\x9c\x9e\xcd\xcd\x86\xa0\xcf\x91\xa0\x8b\x97\xcc\xa0\x94\x91\xcb\x8f\xca\xbe\x9c\x94\#xa0\x8b\xcf\xd4\x82'

跑出来的: 1 0 0 1 1 0 0 1 (0x99) 而正确的flag: 0 1 1 0 0 1 1 0 ('f') 我们需要按位取反

hex_bytes = b'\x99\x93\x9e\x98\x84\x9c\x9e\xcd\xcd\x86\xa0\xcf\x91\xa0\x8b\x97\xcc\xa0\x94\x91\xcb\x8f\xca\xbe\x9c\x94\xa0\x8b\xcf\xd4\x82'
recovered = bytes([b ^ 0xFF for b in hex_bytes])
print(recovered)

#b'flag{ca22y_0n_th3_kn4p5Ack_t0+}'

方法三:密度过大时尝试通过已知位降维,再打LLL

如果我们知道了flag的首位 能不能做些处理来降维呢

让我们回顾一下加密的原理 将flag转化为二进制 再与公钥分别相乘再求和得出c 确定了头和尾就可以得出已知部分的和 接下来拿c减去已知就得到未知部分的和

\[Sum_{known} = \sum (\text{前缀bits} \times \text{前几个公钥}) + \sum (\text{后缀bits} \times \text{后几个公钥})\\S_{new} = S_{old} - Sum_{known} \]

既然前缀和后缀对应的公钥已经用过了 它们对于解密中间部分就没有用了 反而会增加格的维度干扰计算

248-6x8=200 维度变为200 得出来的密度变为0.78这下甚至可以通过LLL打出结果

from sage.all import *
from Crypto.Util.number import long_to_bytes, bytes_to_long

enc=c
pubkey= public_key
p=None
known_prefix = 'flag{'  # 根据实际题目修改
known_suffix = '}'


def reduce_dimension(pubkey, enc, known_prefix, known_suffix):
    """降维函数:移除已知前缀后缀,只保留中间未知部分"""
    def str_to_bits(s):
        bits = []
        for char in s:
            val = ord(char)
            for i in range(7, -1, -1):
                bits.append((val >> i) & 1)
        return bits

    prefix_bits = str_to_bits(known_prefix)
    suffix_bits = str_to_bits(known_suffix)
    if len(pubkey) == 127:  # 假设总长本该是 128
        print("[!] 检测到前导零截断,修正前缀 bits...")
        prefix_bits = prefix_bits[1:]
    known_sum = 0
    for i in range(len(prefix_bits)):
        if prefix_bits[i] == 1:
            known_sum = (known_sum + pubkey[i])
    suffix_start = len(pubkey) - len(suffix_bits)
    for i in range(len(suffix_bits)):
        if suffix_bits[i] == 1:
            known_sum = (known_sum + pubkey[suffix_start + i])

    new_enc = ZZ(enc) - ZZ(known_sum)

    new_pubkey = pubkey[len(prefix_bits): len(pubkey) - len(suffix_bits)]

    print(f"[+] 降维完成: {len(pubkey)} -> {len(new_pubkey)}")
    return new_pubkey, new_enc


def solve_knapsack(pubkey, target):
    n = len(pubkey)
    pubkey = [ZZ(x) for x in pubkey]
    target = ZZ(target)  
    rows = n + 1
    cols = n + 1
    M = Matrix(ZZ, rows, cols)
    for i in range(n):
        M[i, i] = 1
        M[i, n] = pubkey[i] * N
    M[rows - 1, n] = -target * N
    res = M.LLL()

    for row in res:
        if row[n] == 0:
            bits = row[:n]
            # 检查 0/1
            if all(x in [0, 1] for x in bits):
                return list(bits)
            # 检查 0/-1 (取反)
            elif all(x in [0, -1] for x in bits):
                return [-x for x in bits]
    return None



if __name__ == '__main__':
    new_pubkey, new_enc = reduce_dimension(pubkey, enc,  known_prefix, known_suffix)
    bits = solve_knapsack(new_pubkey, new_enc)

    if bits:
        print("[+] LLL 攻击成功!")
        bits_str = "".join(str(b) for b in bits)
        m_int = int(bits_str, 2)
        try:
            middle = long_to_bytes(m_int)
            print(f"[*] 中间部分 hex: {middle.hex()}")
            print(f"[*] 中间部分 str: {middle}")
            print(f"\n[SUCCESS] FLAG: {known_prefix}{middle.decode(errors='ignore')}{known_suffix}")
        except Exception as e:
            print(f"[-] 解码失败: {e}")
            print(f"    Raw Int: {m_int}")
    else:
        print("[-] LLL 未找到解,请尝试 BKZ。")

乘法背包

WKCTF——meet me in the summer

from random import choice, randint
from Crypto.Util.number import isPrime, sieve_base as primes, getPrime
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
from hashlib import md5

flag = b'WKCTF{}'
flag = pad(flag,16)
def myPrime(bits):
    while True:
        n = 2
        while n.bit_length() < bits:
            n *= choice(primes)
        if isPrime(n + 1):
            return n + 1

def encrypt(key, message):
    return pow(65537, message, key)

p = myPrime(512)
q = getPrime(512)
N = p * q
m = [getPrime(512) for i in range(1024)]
enc = [encrypt(N, _) for _ in m]

a = [randint(1,2 ** 50) for i in range(70)]
b = [randint(1,2 ** 50) for i in range(50)]
secret = randint(2**119, 2**120)

ra = 1
rb = 1
for i in range(120):
    if(i < 70):
        if (secret >> i) & 1:
            ra *= a[i]
            ra %= p
    else:
        if (secret >> i) & 1:
            rb *= b[i-70]
            rb %= q


key = md5(str(secret).encode()).hexdigest()[16:].encode()
cipher = AES.new(key,AES.MODE_ECB)

with open("output.txt","w") as f:
    f.write(f'c = {cipher.encrypt(flag).hex()}\n')
    f.write(f'm = {m}\n')
    f.write(f'enc = {enc}\n')
    f.write(f'a = {a}\n')
    f.write(f'b = {b}\n')
    f.write(f'ra = {ra}\n')
    f.write(f'rb = {rb}\n')

仅仅针对该题乘法背包部分进行学习

原始式子是

\[ra \equiv \prod a_i^{k_i} \pmod p \]

看到连乘,想到对数,把 乘法 变成 加法

又因为 p-1光滑,可以想到离散对数 将modp 转化为mod(p-1)

所以具体做法是:我们随便取一个生成元g,然后对等式两边取对数

\[\log_{g}{(ra)} \equiv \sum_{i=0}^{69} \log_{g}(a_ik_i) \mod p - 1 \]

我们可以简单证明一下

\[g^Y \equiv g^{\sum A_i \cdot k_i} \pmod p\\ 为了看着方便,我们把左边的指数记为 U,右边的指数记为 V,\\ 即:g^U \equiv g^V \pmod p假设 U > V,\\ 我们将等式两边同时除以 g^V\\ g^{U-V} \equiv 1 \pmod p\\ U \equiv V \pmod{p-1}\\ 把 U 和 V 替换回原来的样子,就得到了最终的方程:\\ Y \equiv \sum_{i=0}^{69} A_i \cdot k_i \pmod{p-1} \]

并且k_i=0时的项可以忽略 只剩下k_i=1 于是我们可以转化为

\[\log_{g}{(ra)} \equiv \sum_{i=0}^{69} k_i\cdot \log_{g}{(a_i)} \mod p - 1 \]

至此我们将一个乘法背包转化为了一个 模p-1的加法背包密码

p = 266239931579101788237217833822346198682539336616234011732898866661722928035386747695230192006141430294833011494452114878744414084025005167432139516382471637567

# 直接找出 p 的最小原根
g = primitive_root(p)
print(f"[+] 自动找到的生成元 g = {g}")
from tqdm import *

a = [810922431519561, 446272766988725, 167807402211751, 137130339017755, 214986582563833, 141074297736993, 1116944910925939, 827779449967114, 887541522977945, 698795918391810, 180874459256817, 42309568567278, 148563974468327, 43541894027392, 369461465628947, 226728238060977, 902554563386031, 369980733296039, 495826170604031, 202556971656774, 1124261777691439, 533425503636189, 393536945515725, 242107802161603, 506637008093239, 846292038115984, 686372167052341, 923093823276073, 557898577262848, 719859369760663, 51513645433906, 946714837276014, 24336055796632, 302053499607130, 970564601798660, 1082742759743394, 499339281736843, 13407991387893, 667336471542364, 38809146657917, 29069472887681, 420834834946561, 1044601747029985, 854268790341671, 918316968972873, 737863884666895, 1036231016223653, 792781009835942, 142149344663288, 828341073371968, 186470549619656, 279923049419811, 487848895651491, 737257307326881, 1065005635075133, 628186519179693, 554767859759026, 606623194910240, 497855707815081, 88176594691403, 278020899501967, 440746393631841, 921270589876795, 800698974218498, 437669423813782, 717945417305277, 191204872168085, 791101652791845, 772875127585562, 174750251898037]
ra = 215843182933318975496532456029939484729806294336845406882490936458079210569046120528327121994744424727894554328344229010979127024288283698486557728305231262446
p = 266239931579101788237217833822346198682539336616234011732898866661722928035386747695230192006141430294833011494452114878744414084025005167432139516382471637567

Zp = Zmod(p)
g = 5
assert Zp(g).multiplicative_order() == p-1

tmp = discrete_log(mod(ra,p),mod(g,p))
A = [discrete_log(mod(a[i],p),mod(g,p)) for i in range(n)]

n = len(A)
d = n / log(max(A), 2)
print(f"背包密度为: {CDF(d)}")

for k in trange(1,30):
    S = tmp + k*(p-1)
    Ge = Matrix(ZZ,n+1,n+1)
    for i in range(n):
        Ge[i,i] = 2
        Ge[-1,i] = 1
        Ge[i,-1] = A[i]
    Ge[-1,-1] = S
    
    for line in Ge.LLL():
        if set(line[:-1]).issubset({-1,1}):
            m = ''
            for i in line[:-1]:
                if i == 1:
                    m += '0'
                else:
                    m += '1'
            print(f"secret = {m[::-1]}")
            break
"""
背包密度为: 0.13302518379683503
 55%|█████▌    | 16/29 [00:09<00:08,  1.61it/s]
secret = 1100100011000011000001010101000010111110000001000010101010100100011011
100%|██████████| 29/29 [00:17<00:00,  1.63it/s]
"""

本篇文章主要还是基于DexterJie师傅的文章进行学习的

所以接下来还是记一些师傅分享的几道题目 后续做到题目还会进行补充

MoeCTF2022——MiniMiniBackPack

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

def gen_key(size):
    s = 1000
    key = []
    for _ in range(size):
        a = random.randint(s + 1, 2 * s)
        assert a > sum(key)
        key.append(a)
        s += a
    return key


m = bytes_to_long(flag)
L = len(bin(m)[2:])
key = gen_key(L)
c = 0

for i in range(L):
    c += key[i]**(m&1)
    m >>= 1

print(key)
print(c)

L是flag转化为的二进制的长度

key是按照L生成的超递增序列

m & 1会判断变量 m 当前的最后一位:如果是 1,结果就是 1;如果是 0,结果就是0

c+=key[i]**(m&1) 产生的效果就是 若m末尾为1 则累加key[i] 否则累加1

最后再将m左移一位

解题就是用贪心算法还原

from Crypto.Util.number import *

key = [...]
c = 2396891354790728703114360139080949406724802115971958909288237002299944566663978116795388053104330363637753770349706301118152757502162

m = ''
for i in reversed(key):
    if c > i:
        m += '1'
        c -= i
    else:
        m += '0'
        c -= 1


flag = long_to_bytes(int(m,2))
print(flag)

MoeCTF2022——knapsack

from random import randint
from Crypto.Util.number import bytes_to_long,long_to_bytes,GCD,inverse
from secret import flag
def bitlength(n):#判断消息长度
	length=len(bin(bytes_to_long(n))[2:])
	return length
def makeKey(n):#生成超递增序列,得到私钥、公钥
	length=len(n)
	privKey = [randint(1, 65536**length)]
	sum = privKey[0]
	for i in range(1, length):
		privKey.append(randint(sum*255 + 1, 65536**(length + i)))
		sum += privKey[i]
	q = 255*randint(privKey[length-1] + 1, 2*privKey[length-1])
	r = randint(1, q)
	while GCD(r, q) != 1:
		r = randint(1, q)
	pubKey = [ r*w % q for w in privKey ]#将超递增序列变为非超递增序列,作为公钥
	return privKey, q, r, pubKey

def encrypt(msg, pubKey):#用公钥加密消息
	cipher = 0
	i = 0
	for bit in msg:
		cipher += bit*pubKey[i]
		i += 1
	return cipher

def decrypt(cipher, privKey, q, r):#用私钥求得超递增序列并解密
	d = inverse(r, q)
	msg = cipher*d % q
	res = b''
	n = len(privKey)
	for i in range(n - 1, -1, -1):
		temp=0
		if msg >= privKey[i]:
			while msg >= privKey[i]:
				temp=temp+1
				msg -= privKey[i]
			res =  bytes([temp]) + res
		else:
			res =  bytes([0]) + res 
	return res
privKey, q, r, pubKey=makeKey(flag)
cipher=encrypt(flag,pubKey)
f=open("pubKey.txt",'w')
f.write(str(pubKey))
f.close()
f=open("cipher.txt",'w')
f.write(str(cipher))
f.close()
print(decrypt(encrypt(flag,pubKey),privKey,q,r))
assert decrypt(encrypt(flag,pubKey),privKey,q,r)==flag

和标准背包的区别是flag由二进制转变为了每个字符的ASCII值

依旧构造

\[\begin{pmatrix} x_1 & x_2 & … & x_n & -1 \end{pmatrix} \begin{pmatrix} 1 & 0 & … & 0 & M_1\\ 0 & 1 & … & 0 & M_2\\ \vdots & \vdots & \ddots & \vdots & \vdots \\ 0 & 0 & … & 1 &M_n\\ 0 & 0 & … & 0 & S \end{pmatrix} = \begin{pmatrix} x_1 & x_2 & … & x_n & 0 \end{pmatrix} \]

pk = [...]
cipher = 

n = len(pk)
Ge = Matrix(ZZ,n+1,n+1)
for i in range(n):
    Ge[i,i] = 1
    Ge[i,-1] = pk[i]
Ge[-1,-1] = cipher
Ge[:,-1] *= 2**1000
for line in Ge.LLL():
    if all(0 <= abs(x) <= 255 for x in line[:-1]):
        flag = b''
        for i in line[:-1]:
            flag += bytes([abs(i)])
        print(flag)

2023天融信杯——easybag

import os
import random
from hashlib import md5
from Crypto.Util.number import *

rr = os.urandom(10)
flag = "flag{"+rr.hex()+"}"
flag_md5 = md5(flag.encode()).hexdigest()
print(flag)

m = bin(bytes_to_long(rr))[2:].zfill(8 * len(rr))
p = getPrime(256)
def encrypt(m):
    pubkey = [random.randint(2,p - 2) for i in range(len(m))]
    enc = 0
    for k,i in zip(pubkey,m):
        enc += k * int(i)
        enc %= p         #带有模数的背包
    return pubkey,enc    #得到公钥和密文

pubkey,c = encrypt(m)
f = open("output.txt","w")
f.write(f"p = {p}\n")
f.write(f"pubkey = {pubkey}\n")
f.write(f"c = {c}\n")
f.write(f"flag_md5 = {flag_md5}\n")
f.close()

直接构造模p的背包密码

from Crypto.Util.number import *
import hashlib

p = 85766816683407427477074053090759168259205489535331001301483049660772943816017
pubkey = []
c = 1381426073179447662111620044316177635969142117258054810267264948634812447218
flag_md5 = "cae8243e01090ccd03a66e3a4c52b7ee"

n = len(pubkey)

Ge = Matrix(ZZ,n+2,n+2)
for i in range(n):
    Ge[i,i] = 1
    Ge[i,-1] = pubkey[i]

Ge[-2,-2] = 1
Ge[-2,-1] = c
Ge[-1,-1] = p


L = Ge.BKZ()
ans = ''
for i in Ge.BKZ():
    if i[-1] == 0:
        tmp = i[:-2]
        for j in tmp:
            if abs(j) == 1:
                ans += '1'
            else:
                ans += '0'
        flag = "flag{" + hex(int(ans,2))[2:] + "}"
        flag_md51 = hashlib.md5(flag.encode()).hexdigest()
        if flag_md51 == flag_md5:
            print(flag)


以上
https://dexterjie.github.io/2024/07/29/背包密码/?highlight=背包#MoeCTF2022——MiniMiniBackPack

posted @ 2026-02-22 04:08  A1g3rn0n  阅读(96)  评论(0)    收藏  举报