背包密码
背包密码
背包问题与超递增序列
1. 背包问题 (Subset Sum Problem)
背包密码的核心源于一个著名的组合数学难题——子集和问题(Subset Sum Problem)。
问题描述:
在一般情况下,这是一个 NP-Hard 问题。随着 n 的增大,暴力破解的复杂度呈指数级上升,几乎无法在多项式时间内求解。
2. 超递增序列 (Superincreasing Sequence)
为了将这个难题用于加密,我们需要一个“陷门”——即在某种特殊情况下,这个问题变得非常容易解决。这就是超递增序列。
定义:
一个序列 A 是超递增的,当且仅当序列中的每一个元素都大于它前面所有元素的和。即:
性质:对于超递增序列构成的背包问题,我们可以使用贪心算法(Greedy Algorithm)在 O(n)时间内求出解。
Merkle-Hellman 加解密过程
1. 密钥生成 (Key Gen)
-
生成私钥:
构建一个超递增序列
\[A = \{a_1, \dots, a_n\}。 \]选择一个模数 p,使得
\[p > \sum a_i。 \]选择一个乘数 q,使得
\[gcd(p, q) = 1(互素) \]私钥为 (A, p, q)。
-
生成公钥:
计算序列 B,其中
\[B_i \equiv a_i \times q \pmod p \]公钥为序列 B
2. 加密 (Encryption)
发送方将明文消息转化为二进制向量
,利用公钥 B 计算密文 S:
3. 解密 (Decryption)
接收方拥有私钥 (A, p, q)
-
计算 q 在模 p 下的逆元 q^
-
将密文 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 \] -
此时问题转化为了求解
\[\sum x_i a_i = S' \]由于 A 是超递增序列,直接使用贪心算法即可解出 x(即明文)。
背包密码求解
由于 Merkle-Hellman 系统的公钥虽然不是超递增的 但密度通常较低(即d <0.9408) 这使得我们可以将其转化为最短向量问题 (SVP),利用 LLL 或 BKZ 算法进行格基规约攻击
n为维度,也是公钥元素的数量 max a_i就是公钥中最大的元素
根据题目的不同(有无模数、密度高低)我们可以构造不同的格 对于形如
可以打如下格
第一类格
构造思路:
我们需要寻找向量x
构造如下矩阵:
关系是
第二类格
关系是
所以可以攻击密度更大的背包
带模的背包加密
形如
一半有两种方式 构造和爆破
构造格子则转变为
由于加密消息的长度并不会很长,所以的范围是在可爆破范围内的,我们就可以爆破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减去已知就得到未知部分的和
既然前缀和后缀对应的公钥已经用过了 它们对于解密中间部分就没有用了 反而会增加格的维度干扰计算
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')
仅仅针对该题乘法背包部分进行学习
原始式子是
看到连乘,想到对数,把 乘法 变成 加法
又因为 p-1光滑,可以想到离散对数 将modp 转化为mod(p-1)
所以具体做法是:我们随便取一个生成元g,然后对等式两边取对数
我们可以简单证明一下
并且k_i=0时的项可以忽略 只剩下k_i=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值
依旧构造
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

浙公网安备 33010602011771号