III. Một số bài toán trên lưới
1. Bài toán Knapsack và Subset Sum
Bài toán:
Tìm các giá trị biết rằng:
Với là các giá trị cho trước.
Bài toán này có hai cách gọi phổ biến:
- Knapsack Problem (bài toán balo), xuất phát từ ý tưởng chọn một tập hợp con tối ưu từ các vật phẩm.
- Subset Sum Problem (bài toán tổng tập con), tập trung vào việc tìm một tập con của các số sao cho tổng của chúng bằng .
Bài toán này liên quan mật thiết đến mật mã công khai Merkle-Damgård, trong đó bài toán subset sum được sử dụng làm cơ sở toán học để xây dựng các hệ thống mã hóa. Tuy nhiên, nếu bài toán subset sum có mật độ thấp (low-density subset sum), nó có thể bị phá vỡ bằng phương pháp Lattice Reduction, cụ thể là thuật toán LLL (Lenstra–Lenstra–Lovász).
Phân tích và ý tưởng giải
Bài toán có thể được đưa về bài toán trên mạng số nguyên (lattice). Cụ thể, ta dựng một ma trận có kích thước như sau:
Xét các Vector theo hàng ta thấy
Vector này chỉ nhận giá trị nên ta mong nó có thể đủ nhỏ để hoặc hoặc lọt vào mạng LLL.
Mẫu giải bài toán này sử dụng Sagemath:
def solve(A, a: list): """ Return tuple (x1,x2,...xn) such x1*a1 + x2*a2 + ... + xn*an = A for xi in {0,1} """ # Generate Matrix n = len(a) list_mat = [] for i in range(0,n): v = [0,] * i + [2,] + [0,] * (n - 1 - i) + [a[i],] list_mat.append(v) v = [1,] * n + [A,] list_mat.append(v) B = matrix(ZZ, list_mat) B = B.LLL() # Find solution in B after LLL solution = [] for v in B: if v[n] == 0: x = [] for i in range(n): if v[i] != 1 and v[i] != -1: break x.append((v[i]+1) // 2) if len(x) == n: # Check solution t = 0 for i in range(0,n): t += x[i] * a[i] if t == A: solution.append(x) t = 0 for i in range(0, n): t += (1-x[i]) * a[i] x[i] = 1 - x[i] if t == A: solution.append(x) return solution
Thuật toán giải
Xây dựng ma trận :
- Với mỗi , tạo một hàng trong ma trận , với nằm ở vị trí thứ .
- Hàng cuối cùng là .
Áp dụng thuật toán LLL trên ma trận:
- Thuật toán LLL sẽ tìm các vector ngắn trong lattice.
- Một vector ngắn có dạng có thể được tìm ra.
Xác định nghiệm từ vector ngắn:
- Từ vector ngắn , tính:
- Kiểm tra tính chính xác của nghiệm:
Xét các hoán vị khác:
- Có thể cần xem xét nghiệm đảo (vì thuộc ) để tìm tất cả các nghiệm.
Lưu ý
- Thuật toán LLL hiệu quả cho bài toán mật độ thấp (low-density subset sum). Nếu mật độ cao, bài toán trở nên khó hơn và có thể yêu cầu các kỹ thuật khác.
- SageMath hoặc các thư viện toán học tương tự như NumPy không tích hợp trực tiếp LLL, vì vậy cần cài đặt thư viện hoặc sử dụng SageMath.
Challenge CTF bạn có thể thử: Knapsack Ls Cookie Arena CTF 2022.
2. Approximate GCD
Bài toán:
Cho các số , với nhỏ. Tìm là ước chung lớn nhất (GCD) của các mẫu, với sai số
Trong một số trường hợp : Tức là , ta có thể nghĩ đến trường hợp RSA với 1 số thông tin bổ sung
Phân tích và ý tưởng
Với , ta có thể viết:
Vế trái phụ thuộc vào các , vế phải rất nhỏ vì rất nhỏ.
Do đó, ta có thể tạo ra một vector ngắn trong một lattice (mạng số nguyên) chứa các phần tử của . Kỹ thuật lattice reduction (LLL) sẽ giúp chúng ta tìm các vector ngắn, từ đó suy ra .
Giới hạn bẳng cách đặt thoả mãn và nhỏ nhất.
Tăng trọng số các sai số trong lattice để tối ưu tìm vector ngắn.
Xây dựng ma trận:
\begin{pmatrix} 2^{\lambda+1} & x_1 & x_2 & \ldots & x_n \ 0 & x_0 & 0 & \ldots & 0 \ 0 & 0 & x_0 & \ldots & 0 \ \vdots & \vdots & \vdots & \ddots & \vdots \ 0 & 0 & 0 & \ldots & x_0 \end{pmatrix}
Để ý rằng $$q_0x_i - q_ix_0 = q_0r_i - q_ir_0$$.
Nên shortvector là $$v = (2^{\lambda+1} q_0, q_0 r_1 - q_1 r_0, ...,q_0r_n - q_nr_0)$$ sẽ lọt vào mạng lưới Lattice, và để ý rằng sẽ có giá trị rất nhỏ do $$ \frac{x_i}{x_0} \approx \frac{q_i}{q_0}$$.
Vì vậy ta có thể tìm ra giá trị .
Bạn đọc có thể dựng lại Code để có thể hiểu hơn về bài toán này.
Một challenge CTF có thể thử: Sign
trong ISITDTU CTF Quals 2024.
#!/usr/bin/env python3 import os from Crypto.Util.number import *
from Crypto.Signature import PKCS1_v1_5
from Crypto.PublicKey import RSA
from Crypto.Hash import SHA256 flag = b'ISITDTU{aaaaaaaaaaaaaaaaaaaaaaaaaa}'
flag = os.urandom(255 - len(flag)) + flag def genkey(e=11): while True: p = getPrime(1024) q = getPrime(1024) if GCD(p-1, e) == 1 and GCD(q-1, e) == 1: break n = p*q d = pow(e, -1, (p-1)*(q-1)) return RSA.construct((n, e, d)) def gensig(key: RSA.RsaKey) -> bytes: m = os.urandom(256) h = SHA256.new(m) s = PKCS1_v1_5.new(key).sign(h) ss = bytes_to_long(s) return s def getflagsig(key: RSA.RsaKey) -> bytes: return long_to_bytes(pow(bytes_to_long(flag), key.d, key.n)) key = genkey() while True: print( """=================
1. Generate random signature
2. Get flag signature
=================""" ) try: choice = int(input('> ')) if choice == 1: sig = gensig(key) print('sig =', sig.hex()) elif choice == 2: sig = getflagsig(key) print('sig =', sig.hex()) except Exception as e: print('huh') exit(-1)
Đoạn mã trên chứa một hệ RSA với:
- là module RSA
- là số mũ công khai
- là số mũ bí mật
Trường hợp sai số nhỏ: Sai số có thể được mô hình hóa, và bài toán trở thành Approximate GCD. Sử dụng các chữ ký , ta có thể xây dựng lattice để tìm 𝑝 p
Trong trường hợp , tấn công có thể dễ dàng hơn nếu là một bội của .
3. Một số ứng dụng khác
Recovery a Polynomial from one solution in
Như mình đã biết một đa thức bậc thì phải cần ít nhất cặp giá trị để khôi phục lại nó, hoặc là nghiệm phức của nó.
Như vậy nếu có 1 thì sao? LLL cho phép ta làm điều đó!
Challenge:
import os
import random
bits = 1111
C = ComplexField(bits)
P = PolynomialRing(C, names='x')
(x,) = P.gens()
key = os.urandom(256)
coeff = [int.from_bytes(key[i:i+16], 'little') for i in range(0, len(key), 16)]
f = sum([a * x ** i for i, a in enumerate(coeff)])
r = random.choice(f.roots())[0]
print(r) # Hint
Xét đa thức có nghiệm .
Đặt với .
Xét ma trận kích thức sau:
Ta thấy Vector sẽ thuộc LLL nhưng mà LLL không dùng được trong số thực, vậy làm sao để fix.
Như ta đã biết thì số thực trong máy tính sẽ được lưu với độ chính xác nhất định, như ở code trên có lấy độ chính xác là 1111 bits, như vậy ta chỉ cần nhân với và làm tròn về số nguyên, khi dùng LLL thì Vector cũng sẽ thuộc LLL với , là 2 số gần .
round(2**1111 * i_j)
Như vậy ta có hàm giải bài này như sau:
def solve(n, bits, r): """ ::param n số bậc của đa thức ::param bits độ chính xác của số thực ::param r nghiệm của đa thức có thể ở dạng (re,im) return tuple của hệ số (a_0, a_1, ..., a_n) """ C = ComplexField(bits) r = C(r) real = [(r ** i)[0] for i in range(n + 1)] imag = [(r ** i)[1] for i in range(n + 1)] K = 2 ** (bits) M = matrix([ [round(K * x) for x in imag], [round(K * x) for x in real] ]) M = matrix.identity(n + 1).stack(M) M = M.T.LLL() coeff = M[0].list()[:n+1] if all(i < 0 for i in coeff): coeff = [-i for i in coeff] return coeff M = solve(15, 1111, r)
print(M)