攻擊線性同餘生成器(LCG)
目前我們在程式設計中經常會使用隨機數,但是其中會不會存在什麼問題呢?要知道CPU計算中的各種狀態都是確定的,在其中的隨機數不是憑空產生的,所以這種隨機數真的隨機嗎?目前生成隨機數的方式主要分為以下幾種:
- 硬體隨機數生成器
- 利用現有硬體,從非預期方式產生隨機數(比如利用音訊的產生、硬碟定址時間等)
- 偽隨機數
- 量子技術
PS: RDRAND指令產生的隨機數目前存在爭議,在此不做詳細討論。有興趣可以參考 ofollow,noindex">RdRand
雖然選擇很多,但是目前還是主要採用偽隨機數的方式來應對實際開發中需要的場景。用於產生這些看起來隨機但實際是由確定性演算法生成數字的機制被稱為”偽隨機數發生器”,簡稱為PRNG。
PRNG的中心是確定的,如果攻擊者知道其內部的完整狀態,則可以對未來的值和過去的值進行預測。如果PRNG被用於加密金鑰、生成證書等場景,就會出現安全問題。
接下來我將詳細講解對線性同餘發生器的攻擊。
0x01 線性同餘生成器(LCG)
1.線性同餘方法
線性同餘方法(LCG)是個產生偽隨機數的方法。
它是根據遞迴公式:
其中A,B,M是產生器設定的常數。
LCG的週期最大為 M,但大部分情況都會少於M。要令LCG達到最大週期,應符合以下條件:
- B,M互質;
- M的所有質因數都能整除A-1;
- 若M是4的倍數,A-1也是;
- A,B,N[0]都比M小;
- A,B是正整數。
2.Python程式碼實現
由上面的原理我們可以看到,其中最重要的是定義了三個整數,乘數A、增量B和模數M,因此我們在此用簡單的幾行Python程式碼實現一下:
class prng_lcg: m = 672257317069504227# "乘數" c = 7382843889490547368# "增量" n = 9223372036854775783# "模數" def __init__(self, seed): self.state = seed# the "seed" def next(self): self.state = (self.state * self.m + self.c) % self.n return self.state def test(): gen = prng_lcg(123)# seed = 123 print gen.next()# 第一個生成值 print gen.next()# 第二個生成值 print gen.next()# 第三個生成值
3.LCG的優缺點
LCG目前是分流行,得益於其在數學表達實現上十分優雅、非常容易理解並且容易設計實現、計算速度可以非常快。但是它也存在一些缺點,比如它在加密安全性方面十分弱。接下來將從以下幾種情況對其進行攻擊。
0x02 攻擊LCG
1. 對於A、B、M以及N0已知的情況
假設我們觀察到有一個LCG系統產生了以下三組連續的值,並且我們知道內部的引數如下:
# 三組連續的值 s0 = 2300417199649672133 s1 = 2071270403368304644 s2 = 5907618127072939765
# 內部的引數 m = 672257317069504227# the "multiplier" c = 7382843889490547368# the "increment" n = 9223372036854775783# the "modulus"
在已知了這些引數之後我們可以很快的推算出未來的數值或者之前的某個數值,所以還是存在安全問題的。
In [1]: m = 672257317069504227 In [2]: c = 7382843889490547368 In [3]: n = 9223372036854775783 In [4]: s0 = 2300417199649672133 In [5]: s1 = (s0*m + c) % n In [6]: s2 = (s1*m + c) % n In [7]: s3 = (s2*m + c) % n In [8]: s4 = (s3*m + c) % n In [9]: s1 Out[9]: 2071270403368304644L In [10]: s2 Out[10]: 5907618127072939765L In [11]: s3 Out[11]: 5457707446309988294L
2.增量未知
我們不清楚增量,但是我們知道以下資訊:
m = 81853448938945944 c = # unknown n = 9223372036854775783
# 初值和第一個計算值 s0 = 4501678582054734753 s1 = 4371244338968431602
我們稍稍改寫下公式就可以將目標c計算出來
s1 = s0*m + c(mod n) c= s1 - s0*m(mod n)
此種類型Python攻擊程式碼如下所示:
def crack_unknown_increment(states, modulus, multiplier): increment = (states[1] - states[0]*multiplier) % modulus return modulus, multiplier, increment print crack_unknown_increment([4501678582054734753, 4371244338968431602], 9223372036854775783, 81853448938945944)
3.增量和乘數都未知
我們雖然不知道增量和乘數但是我們知道以下數值
m = # unknown c = # unknown n = 9223372036854775783
# LCG生成的初值和後面生成的兩個值 s0 = 6473702802409947663 s1 = 6562621845583276653 s2 = 4483807506768649573
解決辦法很簡單,想想怎麼解線性方程組就好了
s_1 = s0*m + c(mod n) s_2 = s1*m + c(mod n) s_2 - s_1 = s1*m - s0*m(mod n) s_2 - s_1 = m*(s1 - s0)(mod n) m = (s_2 - s_1)/(s_1 - s_0)(mod n)
此種類型Python攻擊程式碼如下所示:
def crack_unknown_multiplier(states, modulus): multiplier = (states[2] - states[1]) * modinv(states[1] - states[0], modulus) % modulus return crack_unknown_increment(states, modulus, multiplier) print crack_unknown_multiplier([6473702802409947663, 6562621845583276653, 4483807506768649573], 9223372036854775783)
這個演算法中應用到了求模,所以我們就需要逆推。詳情參考: Recursive algorithm
def egcd(a, b): if a == 0: return (b, 0, 1) else: g, x, y = egcd(b % a, a) return (g, y - (b // a) * x, x) def modinv(b, n): g, x, _ = egcd(b, n) if g == 1: return x % n
4.增量,乘數和模數均未知
現在內部狀態基本是都不知道了,但是我們知道初值和隨後LCG產生的連續的幾個值。
m = # unknown c = # unknown n = # unknown
s0 = 2818206783446335158 s1 = 3026581076925130250 s2 = 136214319011561377 s3 = 359019108775045580 s4 = 2386075359657550866 s5 = 1705259547463444505 s6 = 2102452637059633432
這次用線性方程式不好解決的了,因為對於每一個方程,我們是不知道前一個模數,因此我們將形成的每個方程都會引入新的未知量:
s1 = s0*m + c(mod n) s2 = s1*m + c(mod n) s3 = s2*m + c(mod n)
s1 - (s0*m + c) = k_1 * n s2 - (s1*m + c) = k_2 * n s3 - (s2*m + c) = k_3 * n
這就相當於六個未知數和三個方程。所以線性方程組是不可能行得通的了,但是數論裡面有一條很有用:如果有幾個隨機數分別乘以n,那麼這幾個數的歐幾里德演算法(gcd)就很可能等於n。
In [944]: n = 123456789 In [945]: reduce(gcd, [randint(1, 1000000)*n, randint(1, 1000000)*n, randint(1, 1000000)*n]) Out[945]: 123456789
某些取模運算是會等於0的
X = 0 (mod n)
然後,根據定義,這相當於:
X = k*n
所以這種X != 0但是X = 0 (mod n)的情況就很有趣。我們只需要取幾個這樣的值進行gcd運算,我們就可以解出n的值。這種是在模數未知的情況下十分常用的方法。
我們在此引入一個序列 – T(n) = S(n+1) - S(n):
t0 = s1 - s0 t1 = s2 - s1 = (s1*m + c) - (s0*m + c) = m*(s1 - s0) = m*t0 (mod n) t2 = s3 - s2 = (s2*m + c) - (s1*m + c) = m*(s2 - s1) = m*t1 (mod n) t3 = s4 - s3 = (s3*m + c) - (s2*m + c) = m*(s3 - s2) = m*t2 (mod n)
之後我們就可以得到我們想要的效果了:
t2*t0 - t1*t1 = (m*m*t0 * t0) - (m*t0 * m*t0) = 0 (mod n)
然後我們就可以生成幾個這樣模是0的值,進而利用我們上文講述的技巧,此種類型Python攻擊程式碼如下所示:
def crack_unknown_modulus(states): diffs = [s1 - s0 for s0, s1 in zip(states, states[1:])] zeroes = [t2*t0 - t1*t1 for t0, t1, t2 in zip(diffs, diffs[1:], diffs[2:])] modulus = abs(reduce(gcd, zeroes)) return crack_unknown_multiplier(states, modulus) print crack_unknown_modulus([2818206783446335158, 3026581076925130250, 136214319011561377, 359019108775045580, 2386075359657550866, 1705259547463444505])
0x03 總結
此處我們簡述了對LCG的攻擊方式,這種方式剛在P.W.N CTF中出現過,具體的題目以及解答可以參考我的下一篇文章–《P.W.N. CTF》中的LCG and the X題目解析。
0x04 參考
Cryptographically secure pseudorandom number generator
Lenstra–Lenstra–Lovász lattice basis reduction algorithm
Cracking RNGs: Linear Congruential Generators
Algorithm Implementation/Mathematics/Extended Euclidean algorithm