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 đó

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 . 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 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
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

Shortest Integer solutions - SIS

Tài liệu

Lattice’s sieving

Hermite Normal Form