第九章:伪随机数生成器(PRNG)攻击
9.1 PRNG基础
真随机 vs 伪随机:
- 真随机数( TRNG ): 来自物理过程(放射性衰变、电子噪声等),不可预测
- 伪随机数( PRNG ): 由确定性算法生成,给定种子(seed)后输出序列完全确定,可预测
PRNG 工作原理: 种子(seed)→ 初始化内部状态 → 通过状态转换函数生成输出 → 更新状态 → 循环
|----------------------------------------------------------------------------------------------------------------|
| ⚠ 安全警告 Python的 random 模块使用的是Mersenne Twister算法,不是密码学安全的。在需要安全随机数的场景中,应使用 secrets 模块或 os.urandom()。 |
9.2 线性同余生成器(LCG)
公式: X_{n+1} = (a × X_n + c) mod m
其中a是乘数、c是增量、m是模数、X_0是种子。
已知参数时的预测: 知道a, c, m和当前状态X_n,即可计算所有后续输出。
已知输出恢复参数:
from math import gcd
from functools import reduce
def crack_lcg(outputs):
"""
已知LCG的连续输出序列,恢复参数a, c, m
需要至少6个连续输出
"""
# 第一步:恢复模数m
# 利用关系:t_n = s_{n+1} - s_n
# t_{n+1}*t_{n-1} - t_n^2 ≡ 0 (mod m)
diffs = [outputs[i+1] - outputs[i] for i in range(len(outputs)-1)]
zeroes = []
for i in range(len(diffs) - 2):
zeroes.append(diffs[i+2] * diffs[i] - diffs[i+1] ** 2)
m = abs(reduce(gcd, zeroes))
if m == 0:
return None
print(f"恢复的模数 m = {m}")
# 第二步:恢复乘数a
# a = (s2 - s1) * (s1 - s0)^(-1) mod m
a = ((outputs[2] - outputs[1]) * pow(outputs[1] - outputs[0], -1, m)) % m
print(f"恢复的乘数 a = {a}")
# 第三步:恢复增量c
c = (outputs[1] - a * outputs[0]) % m
print(f"恢复的增量 c = {c}")
return m, a, c
# 示例
# 已知参数生成序列
a_real, c_real, m_real = 1103515245, 12345, 2**31
x = 42 # 种子
outputs = []
for _ in range(8):
x = (a_real * x + c_real) % m_real
outputs.append(x)
print("已知输出:", outputs[:6])
result = crack_lcg(outputs[:6])
if result:
m, a, c = result
# 预测下一个输出
predicted = (a * outputs[5] + c) % m
print(f
9.3 Mersenne Twister(MT19937)
Python的 random 模块使用Mersenne Twister算法,这是CTF中最常考的PRNG。
内部状态: 624个32位无符号整数(共19968位状态)
攻击方法:
-
收集624个连续的32位输出(即624次 random.getrandbits(32))
-
对每个输出进行逆向untemper操作,恢复对应的内部状态
-
用恢复的状态替换PRNG的内部状态
-
预测所有后续输出或向前推算之前的输出
pip install randcrack
import random
from randcrack import RandCrack模拟:收集624个32位输出
rng = random.Random()
rng.seed(12345) # 服务端使用的种子(我们不知道)rc = RandCrack()
收集624个输出喂给RandCrack
for i in range(624):
value = rng.getrandbits(32)
rc.submit(value)现在可以预测后续输出
for i in range(10):
predicted = rc.predict_getrandbits(32)
actual = rng.getrandbits(32)
print(f"预测: {predicted}, 实际: {actual}, 正确: {predicted == actual}")def untemper(y):
"""逆向MT19937的temper操作"""
# 逆向 y = y ^ (y >> 18)
y = y ^ (y >> 18)# 逆向 y = y ^ ((y << 15) & 0xEFC60000) y = y ^ ((y << 15) & 0xEFC60000) # 逆向 y = y ^ ((y << 7) & 0x9D2C5680) tmp = y tmp = y ^ ((tmp << 7) & 0x9D2C5680) tmp = y ^ ((tmp << 7) & 0x9D2C5680) tmp = y ^ ((tmp << 7) & 0x9D2C5680) y = y ^ ((tmp << 7) & 0x9D2C5680) # 逆向 y = y ^ (y >> 11) tmp = y tmp = y ^ (tmp >> 11) y = y ^ (tmp >> 11) return ydef clone_mt_state(outputs_32bit):
"""从624个32位输出克隆MT19937状态"""
assert len(outputs_32bit) == 624
state = [untemper(o) for o in outputs_32bit]
return state
9.4 时间戳种子攻击
常见漏洞: 程序使用当前时间戳(秒级或毫秒级)作为PRNG的种子。
攻击方法: 记录请求时间,然后枚举前后一定范围内的时间戳作为种子,逐一尝试。
import random
import time
def timestamp_seed_attack(known_output, time_range=3600):
"""
时间戳种子攻击
known_output: 已知的PRNG输出值
time_range: 搜索的时间范围(秒)
"""
current_time = int(time.time())
for seed in range(current_time - time_range, current_time + 1):
rng = random.Random(seed)
if rng.getrandbits(32) == known_output:
print(f"找到种子: {seed}")
print(f"对应时间: {time.ctime(seed)}")
# 预测后续输出
next_values = [rng.getrandbits(32) for _ in range(5)]
print(f"后续5个输出: {next_values}")
return seed
print("未找到匹配的种子")
return None
# 模拟攻击
seed_actual = int(time.time()) - 42 # 假设42秒前生成的
rng = random.Random(seed_actual)
known = rng.getrandbits(32)
print(f"已知输出: {known}")
timestamp_seed_attack(known)
9.5 实战案例:MT19937状态恢复
|-------------------------------------------------------------------------------------------------------------------------------|
| 📋 题目 服务端使用Python的random模块生成token:token = random.getrandbits(32)。你可以请求服务端生成token并返回给你。收集足够多的token后,预测下一个token以获取flag。 |
import random
from randcrack import RandCrack
# ===== 模拟与服务端交互 =====
# 服务端初始化(攻击者不知道种子)
server_rng = random.Random()
server_rng.seed() # 使用默认种子
def get_token():
"""模拟向服务端请求token"""
return server_rng.getrandbits(32)
def verify_prediction(predicted_token):
"""模拟提交预测"""
actual = server_rng.getrandbits(32)
if predicted_token == actual:
return "flag{mt19937_is_not_cryptographically_secure}"
return "预测错误"
# ===== 攻击过程 =====
print("第一步:收集624个token...")
rc = RandCrack()
for i in range(624):
token = get_token()
rc.submit(token)
if (i + 1) % 100 == 0:
print(f" 已收集 {i+1}/624 个token")
print("\n第二步:预测下一个token...")
predicted = rc.predict_getrandbits(32)
print(f"预测的token: {predicted}")
print("\n第三步:提交预测...")
result = verify_prediction(predicted)
print(f"结果: {result}")
9.6 PHP mt_rand() 的特殊性
背景 :
在CTF中,PHP题目非常常见。PHP的 mt_rand() 底层调用的是C语言的 glibc 实现,与 Python 的 random 模块(Python实现)虽然都是 Mersenne Twister 算法,但 Temper 变换的参数不同。
攻击工具 :
不要直接用 Python 的 randcrack,而应该使用专门针对 PHP 的工具,如 php_mt_seed。
常见考点:
-
版本差异 :PHP 7.1 前后
mt_rand()的实现有变化(PHP 7.1+ 引入了 CSPRNG 混合)。 -
部分输出 :如果只给出了
mt_rand()输出的一部分(例如只取了低16位),需要修改工具进行暴力破解。这是一个命令行工具的使用示例,而非 Python 脚本
假设你拿到了 PHP 输出的几个连续数字:12345, 67890, 11111...
1. 安装 php_mt_seed (需要 git 和 gcc)
git clone https://github.com/ajmwagar/php_mt_seed.git
cd php_mt_seed
make2. 运行破解
语法: ./php_mt_seed <输出值1> [输出值2 ...]
这里的 "12345" 是 mt_rand() 的第一个输出
./php_mt_seed 12345
输出示例:
Found seed: 1234567890
...
9.7 随机数偏置攻击
原理 :
如果随机数生成器(RNG)有缺陷,导致 kk 值不是完全随机的,而是存在某种规律(例如高位固定、低位固定或长度很短),攻击者可以使用 格基约化算法 来恢复私钥。
常见情况:
- kk 值过小: kk 只有几十位,而不是 256 位。
- MSB 泄露: kk 的高位已知。
- LSB 泄露: kk 的低位已知。
工具 :
这类攻击通常需要使用 SageMath 中的 LLL 算法或专门的工具(如 ecdsa-signature-bias-attack)。
9.8 现实世界中的 PRNG 惨案
1. Sony PS3 签名密钥泄露 (2010)
- 原因 :Sony 在实现 ECDSA 签名时,犯了一个致命错误------kk 值固定。
- 后果:黑客(如 Geohot)通过对比两个不同游戏的签名,利用 Nonce 重用攻击轻松算出了 Sony 的私钥。从此 PS3 的防线彻底崩溃。
2. Android Java SecureRandom 漏洞 (2013)
- 原因 :Android 的
SecureRandom类在生成 ECDSA 随机数 kk 时存在缺陷,导致 kk 值可预测。 - 后果:比特币钱包应用受到影响,导致用户的私钥被泄露,大量比特币被盗。
3. LuBian 矿池被盗案 (2020)
- 原因:使用了仅依赖 32 位时间戳作为种子的弱随机数生成器(Mersenne Twister)。
- 后果:攻击者仅用约 2 小时就暴力破解了私钥,盗走了 12.7 万枚比特币。