Truncated LCG
Cơ bản về LCG
Ở phần này ta sẽ tập trung vào khai thác một dạng PRNG đơn giản đó chính là Linear congruential generator.
Generator sẽ có dạng một biểu thức truy hồi tuyến tính (đây cũng sẽ là dạng biểu thức mà ta sẽ nghiên cứu trong phần mở rộng sau này):
Dãy các giá trị sẽ được kí hiệu là , trong đó
- được gọi là modulus của dãy
- là multiplier
- là increment
- được gọi là seed của dãy
Ví dụ một số bộ tham số cho LCG như sau: https://en.wikipedia.org/wiki/Linear_congruential_generator

class LCG:
m = 2**31
a = 1103515245
c = 12345
def __init__(self,seed):
self.seed = seed
def next(self):
self.seed = (self.seed*self.a + self.c) % self.m
return self.seed
if __name__ == "__main__":
lcg = LCG(42)
for _ in range(10):
print(lcg.next())
Ta có thể viết lại từng số dạng trong dãy LCG dưới dạng tổng quát như sau:
Như vậy ta có thể biểu diễn, trong trường hợp , dãy LCG dưới dạng
Còn nếu không thì ta sẽ biểu diễn như sau:
Do LCG được lấy theo modulo cho nên theo nguyên lí Dirichlet thì dãy sẽ lặp lại các số hạng kể từ một lúc nào đó. Tức là tồn tại hai chỉ số sao cho . Như vậy để LCG trở nên “an toàn” hơn thì ta cần chọn đủ lớn và các tham số sao cho chu kì của dãy là đủ dài.
Về việc này, mọi người có thể đọc qua định lý Hull-Dobell tại đây.
So, how to break it?
Sẽ có 3 trường hợp có thể xảy ra
Trường hợp 1. Ta chỉ biết thông tin về , khôi phục và seed.
Đối với trường hợp này ta chỉ cần 2 output liên tiếp là sẽ khôi phục được . Giả sử ta có thì ta tính lại được bằng cách lấy:
Sau khi có được và một output rồi thì ta có thể thử recover lại seed từ công thức tổng quát của dãy LCG như trên:
Test:
import random
class LCG:
m = 2**31
a = 1103515245
c = 12345
def __init__(self,seed):
self.seed = seed
def next(self):
self.seed = (self.seed*self.a + self.c) % self.m
return self.seed
if __name__ == "__main__":
seed = random.randrange(2**31)
a = 1103515245
print(f"orignial seed: {seed}")
lcg = LCG(seed)
for _ in range(10):
print(f"x{_+1} : {lcg.next()}")
x11 = lcg.next()
x12 = lcg.next()
c = (x12 - a*x11) % 2**31
print(f"c : {c}")
s = sum([a**i for i in range(11)])
seed_rec = (pow(a,-11,2**31)*(x11-c*s)) % 2**31
print(f"reconstructed seed : {seed_rec}")Trường hợp 2. Ta chỉ biết modulus . Cần tìm lại .
Việc tìm lại cũng tương tự như trên, chỉ cần biết 2 output liên tiếp thì ta sẽ tìm lại được .
Chẳng hạn ta có
Thì ta có thể tính lại bằng cách lấy:
Khi và chỉ khi .
Nếu như ta không có được thì ta cần thêm ít nhất 1 output nữa để có thể recover được .
Cụ thể
Ta có
Cho nên
Nếu như LCG của ta thỏa mãn các điều kiện của định lí Hull-Dober bên trên thì khi đó sẽ tồn tại nghịch đảo modulo của .
Test:
import random
class LCG:
m = 2**31
a = 1103515245
c = 12345
def __init__(self,seed):
self.seed = seed
def next(self):
self.seed = (self.seed*self.a + self.c) % self.m
return self.seed
if __name__ == "__main__":
seed = random.randrange(2**31)
a = 1103515245
print(f"orignial seed: {seed}")
lcg = LCG(seed)
for _ in range(10):
print(f"x{_+1} : {lcg.next()}")
x11 = lcg.next()
x12 = lcg.next()
x13 = lcg.next()
c = (x12 - a*x11) % 2**31
a = ((x13-x12)*pow(x12-x11,-1,2**31)) % 2**31
print(a)Trường hợp 3. Trường hợp cuối cùng là khi ta không biết bất cứ thông tin gì về .
Để giải trường hợp này thì ta cần ít nhất 5 output liên tiếp của dãy LCG.
Ta xét 5 output đó lần lượt là .
Đặt thì ta có được
Ta sẽ tiến hành nhân chéo lên như sau:
Do nên ta có thể suy ra . Làm tương tự ta cũng có được
Sau đó ta lấy ước chung của 2 biểu thức trên và khôi phục lại . Sau khi có rồi thì ta làm như 2 trường hợp trên để lấy lại .
Test:
import random
from sage.all import gcd
from factordb.factordb import FactorDB
class LCG:
m = 2**31
a = 1103515245
c = 12345
def __init__(self,seed):
self.seed = seed
def next(self):
self.seed = (self.seed*self.a + self.c) % self.m
return self.seed
if __name__ == "__main__":
seed = random.randrange(2**31)
a = 1103515245
print(f"orignial seed: {seed}")
print(f"modulus = {LCG.m}")
lcg = LCG(seed)
for _ in range(10):
print(f"x{_+1} : {lcg.next()}")
x = [lcg.next() for _ in range(5)]
d = [x[i+1]-x[i] for i in range(4)]
u = d[1]**2-d[2]*d[0]
v = d[2]**2 - d[3]*d[1]
res = int(gcd(u,v))
print(res)
f = FactorDB(res)
f.connect()
lst = f.get_factor_from_api()
print(lst)Bài tập ví dụ: RSA vs RNG / CryptoHack
Source code của bài:
from Crypto.Util.number import *
import json
MOD = 2**512
A = 2287734286973265697461282233387562018856392913150345266314910637176078653625724467256102550998312362508228015051719939419898647553300561119192412962471189
B = 4179258870716283142348328372614541634061596292364078137966699610370755625435095397634562220121158928642693078147104418972353427207082297056885055545010537
FLAG = b'crypto{???????????????????????????}'
class PRNG:
def __init__(self, seed):
self.state = (seed % MOD)
def get_num(self):
self.state = (A * self.state + B) % MOD
return self.state
def get_prime(self):
p = self.get_num()
while not isPrime(p):
p = self.get_num()
return p
seed = getRandomRange(1, MOD)
rng = PRNG(seed)
P = rng.get_prime()
Q = rng.get_prime()
N = P * Q
E = 0x10001
pt = bytes_to_long(FLAG)
ct = long_to_bytes(pow(pt, E, N))
json.dump({{"N": 95397281288258216755316271056659083720936495881607543513157781967036077217126208404659771258947379945753682123292571745366296203141706097270264349094699269750027004474368460080047355551701945683982169993697848309121093922048644700959026693232147815437610773496512273648666620162998099244184694543039944346061, "E": 65537, "ciphertext": "04fee34327a820a5fb72e71b8b1b789d22085630b1b5747f38f791c55573571d22e454bfebe0180631cbab9075efa80796edb11540404c58f481f03d12bb5f3655616df95fb7a005904785b86451d870722cc6a0ff8d622d5cb1bce15d28fee0a72ba67ba95567dc5062dfc2ac40fe76bc56c311b1c3335115e9b6ecf6282cca"}
'N': N,{"N": 95397281288258216755316271056659083720936495881607543513157781967036077217126208404659771258947379945753682123292571745366296203141706097270264349094699269750027004474368460080047355551701945683982169993697848309121093922048644700959026693232147815437610773496512273648666620162998099244184694543039944346061, "E": 65537, "ciphertext": "04fee34327a820a5fb72e71b8b1b789d22085630b1b5747f38f791c55573571d22e454bfebe0180631cbab9075efa80796edb11540404c58f481f03d12bb5f3655616df95fb7a005904785b86451d870722cc6a0ff8d622d5cb1bce15d28fee0a72ba67ba95567dc5062dfc2ac40fe76bc56c311b1c3335115e9b6ecf6282cca"}
'E': E,
'ciphertext': ct.hex()
}, open('flag.enc', 'w'))Và một file json data chứa các tham số , và ciphertext
{"N": 95397281288258216755316271056659083720936495881607543513157781967036077217126208404659771258947379945753682123292571745366296203141706097270264349094699269750027004474368460080047355551701945683982169993697848309121093922048644700959026693232147815437610773496512273648666620162998099244184694543039944346061, "E": 65537, "ciphertext": "04fee34327a820a5fb72e71b8b1b789d22085630b1b5747f38f791c55573571d22e454bfebe0180631cbab9075efa80796edb11540404c58f481f03d12bb5f3655616df95fb7a005904785b86451d870722cc6a0ff8d622d5cb1bce15d28fee0a72ba67ba95567dc5062dfc2ac40fe76bc56c311b1c3335115e9b6ecf6282cca"}
Bài cho ta một modulo và cặp số . class PRNG sẽ lấy đầu vào là một seed.
Ta gọi state ban đầu là chẳng hạn thì .
Ở mỗi bước, khi ta gọi hàm get_num nó sẽ tính còn khi gọi get_prime thì nó sẽ lấy liên tục cho tới khi là số nguyên tố.
Mấu chốt của bài này là hai số nguyên tố được sinh ra từ cùng 1 seed và ta phải tìm cách khôi phục lại 2 số này để giải RSA.
Đầu tiên ta thử viết lại theo .
Giả sử thực hiện lần. Thì khi đó . Giả sử chẳng hạn, mình muốn tìm một công thức gọn hơn cho theo , trong đó
Như vậy mình có được
Tương tự, nếu như ta thực hiện lần để get_num() tạo ra số thì khi đó
Ở đây ta gọi lấy trước rồi mới lấy tức là trạng thái state khi ta gọi sẽ là chuyển tiếp từ trạng thái của cho nên ta có với là một số nào đó mà ta không rõ.
Vậy thì
Bây giờ ta có
Xét
Ta có phương trình bậc 2 với các hệ số
Lưu ý ở bài này là số lẻ cho nên ta không thể lấy được.
Giả sử ta muốn giải
Mấu chốt ở đây là reduce từ modulo về modulo .
Ta không thể chia 2 trên modulo được, giả sử ta có một số chẳng hạn và ta biết . Ta muốn tính thì ta cần làm sao?
Đầu tiên ta viết thì khi đó . Ta lấy chia lấy phần nguyên cho 2 rồi sau đó tính modulo theo là ra được số dư, sau đó nếu muốn lift lên modulo lại thì ta cần cộng thêm với (vì các số trong bài lấy theo modulo nên không thể quá lớn được.)
from sage.all import *
from Crypto.Util.number import *
MOD = 2**512
A = 2287734286973265697461282233387562018856392913150345266314910637176078653625724467256102550998312362508228015051719939419898647553300561119192412962471189
B = 4179258870716283142348328372614541634061596292364078137966699610370755625435095397634562220121158928642693078147104418972353427207082297056885055545010537
class PRNG:
def __init__(self, seed):
self.state = (seed % MOD)
def get_num(self):
self.state = (A * self.state + B) % MOD
return self.state
def get_prime(self):
p = self.get_num()
while not isPrime(p):
p = self.get_num()
return p
N = 95397281288258216755316271056659083720936495881607543513157781967036077217126208404659771258947379945753682123292571745366296203141706097270264349094699269750027004474368460080047355551701945683982169993697848309121093922048644700959026693232147815437610773496512273648666620162998099244184694543039944346061
E = 65537
ciphertext = "04fee34327a820a5fb72e71b8b1b789d22085630b1b5747f38f791c55573571d22e454bfebe0180631cbab9075efa80796edb11540404c58f481f03d12bb5f3655616df95fb7a005904785b86451d870722cc6a0ff8d622d5cb1bce15d28fee0a72ba67ba95567dc5062dfc2ac40fe76bc56c311b1c3335115e9b6ecf6282cca"
ct=int(ciphertext,16)
assert gcd(N,A)==1
x=0
found = true
while found:
x+=1
print(x)
a = pow(A,x,MOD)
b = B * sum(pow(A,i,MOD) for i in range(x)) % MOD
c = - N
try:
det = int(Mod(b ** 2 - 4 * a * c, MOD).nth_root(2))
rt1 = (-b + det) * pow(a,-1,MOD) % MOD
rt2 = (-b - det) * pow(a,-1,MOD) % MOD
for k in range(2):
p = (rt1 % (MOD // 2)) // 2 + k * (MOD // 2)
if N % p == 0:
print(p)
q=N//p
phi=(p-1)*(q-1)
d=pow(E,-1,phi)
m=pow(ct,d,N)
print(long_to_bytes(m))
found = false
else:
print("failed")
for k in range(2):
p = (rt2 % (MOD // 2)) // 2 + k * (MOD // 2)
if N % p == 0:
print(p)
q=N//p
phi=(p-1)*(q-1)
d=pow(E,-1,phi)
m=pow(ct,d,N)
print(long_to_bytes(m))
found=false
else:
print("failed")
except:
print("failed")Truncated LCG
Knuth’s truncated LCG
Xét một LCG như sau:
Gọi là bit length của . Xét
Trong đó là số lượng MSB bit được leak ra và ta được biết toàn bộ các giá trị này.
Bài toán đặt ra sẽ là: Biết MSB của là . Yêu cầu tìm lại .
Trước tiên ta giới thiệu 1 số tham số:
- là bit length của modulus
- là số lượng MSB được biết
Attacks gồm 2 bước chính.
Bước 1. Dựng đa thức và LLL
Xét danh sách gồm các giá trị đầu vào là . Ta sẽ chia thành các block, mỗi block bao gồm vector. Bắt đầu từ tới . Các tham số ta sẽ chọn sao cho
Mỗi vector mà ta build sẽ có dạng
Tiếp theo ta sẽ tìm một vector thỏa mãn
Xét lattice sau:
Ta có thể đảm bảo rằng tồn tại một vector thỏa ràng buộc tuyến tính trên nếu như với mọi và thỏa
Ta sẽ chọn thỏa mãn và ta kì vọng rằng vector tìm được sẽ có chuẩn Euclid không vượt quá và thỏa phương trình trên.
Tiếp theo ta xét các vector có dạng:
Với các parameters được chọn như trên, sẽ có một trong số các vector thỏa mãn
Test:
import random
import math
from sage.all import *
from itertools import combinations
class LCG:
m = 10734367385013619889
a = 9807963723765715717
b = 7226300108115682840
def __init__(self,seed):
self.seed = seed
def next(self):
self.seed = (self.seed*self.a + self.b) % self.m
return self.seed
seed = 2877244225168654778
lcg = LCG(seed)
m = LCG.m
a = LCG.a
b = LCG.b
# print(m.bit_length())
# generate truncated xs
xs = []
ys = []
zs = []
mod = 2**32
alpha = 1/2
k = 64
s = 32
for _ in range(17):
x = lcg.next()
xs.append(x)
ys.append(x >> 32)
zs.append(x % mod)
assert all(y*(2**32)+z==x for x,y,z in zip(xs,ys,zs))
# print(ys) # được biết ys
t = 3
n = 14
Vs = []
for i in range(n):
V = []
for j in range(t):
V.append(ys[i+j+1]-ys[i+j])
Vs.append(V)
K = 356131
Ws = []
for i in range(n):
W = []
for j in range(t):
W.append(xs[i+j+1]-xs[i+j])
Ws.append(W)
Vs = [vector(ZZ, v) for v in Vs]
B = matrix(ZZ, [K * v for v in Vs])
I = identity_matrix(ZZ, n)
M = B.augment(I)
L = M.LLL()
Ws = [vector(ZZ, w) for w in Ws]
print(Ws)
for v in L:
v_ = v[3:]
s_ = 0
for i in range(len(Ws)):
s_ += v_[i] * Ws[i]
print(s_)
Tiếp theo ta có . Cho nên
Như vậy:
. Nếu như thì khi đó xét đa thức
thỏa mãn . Trong đa số các trường hợp thì nên ta có thể xét đa
thức
Các đa thức này có nghiệm chung là nên ta có thể dùng kết thức (resultant) để tính lại .
Test:
R = PolynomialRing(ZZ,'x')
x = R.gen()
lamb = []
ps = []
for vec in L:
P = sum(vec[3:][i]*x**i for i in range(n))
print(P)
print(P(a)%m) # for testing
ps.append(P)
ms = []
# recover m
for comb in combinations(ps,3):
p0 = comb[0]
p1 = comb[1]
p2 = comb[2]
m_ = math.gcd(p0.resultant(p1), p1.resultant(p2), p0.resultant(p2))
print(m_)
if (m_.bit_length() > 20):
ms.append(m_)
print(set(ms))Giải thích: Sau khi rút gọn LLL. thì ta sẽ lấy các vector trong L để build đa thức . Sau đó sẽ tạo các tổ hợp gồm 3 đa thức và tính resultant của các cặp đa thức này.
Như vậy là ta đã có được một list các số có khả năng là modulus .
Bước tiếp theo là recover lại .
Ta biết rằng các đa thức này đều có nghiệm chung là . Như vậy ta sẽ lấy GCD của chúng để thu được 1 đơn thức có nghiệm là .
Bước tiếp theo ta cần recover lại .
Ý tưởng 1. Mình thử đưa bài toán recover về bài HNP.
Nhắc lại bài toán HNP:
Bài toán HNP yêu cầu ta recover lại một số nguyên bí mật trong đó là số nguyên tố, từ cặp giá trị thỏa mãn
Đối với bài LCG của ta thì ta có:
Vì đa tìm được nên ta có thể thử viết lại dưới dạng sau:
Ta có cho nên
Ta đưa về dạng HNP với và
Nhưng rất tiếc là cách này không cho ta solutions vì các giá trị có bound quá lớn

Cụ thể
Nhưng ta đang muốn với được đánh giá như trên. gốc trong bài toán của ta gần như xấp xỉ nên cách này có vẻ không ổn lắm

Vậy ta cần tìm một cách dựng ma trận khác hợp lí hơn.
Bây giờ ta có
Từ công thức tổng quát của LCG ta có
Đặt các tham số
Thì khi đó từ ta viết lại:
Với 2 unknown variables là .
Tài liệu
- https://eprint.iacr.org/2022/1134.pdf
- https://eprint.iacr.org/2021/1204.pdf
- https://sci-hub.se/10.1007/11506157_5
- https://www.math.cmu.edu/~af1p/Texfiles/RECONTRUNC.pdf
- https://dunkirkturbo.github.io/2020/02/29/Lattice-Learning-1/
- https://link.springer.com/article/10.1007/s001459900042
- https://www.texmacs.org/joris/chinese-macis/chinese-macis.pdf
- https://cseweb.ucsd.edu/~mihir/papers/dss-lcg.pdf
- https://jia.je/ctf-writeups/misc/lcg.html
- https://math.stackexchange.com/questions/3597480/understanding-resultant