CTF密码学综合教学指南--第九章

第九章:伪随机数生成器(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位状态)

攻击方法:

  1. 收集624个连续的32位输出(即624次 random.getrandbits(32))

  2. 对每个输出进行逆向untemper操作,恢复对应的内部状态

  3. 用恢复的状态替换PRNG的内部状态

  4. 预测所有后续输出或向前推算之前的输出

    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 y

    def 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
    make

    2. 运行破解

    语法: ./php_mt_seed <输出值1> [输出值2 ...]

    这里的 "12345" 是 mt_rand() 的第一个输出

    ./php_mt_seed 12345

    输出示例:

    Found seed: 1234567890

    ...

9.7 随机数偏置攻击

原理

如果随机数生成器(RNG)有缺陷,导致 kk 值不是完全随机的,而是存在某种规律(例如高位固定、低位固定或长度很短),攻击者可以使用 格基约化算法 来恢复私钥。

常见情况

  1. kk 值过小: kk 只有几十位,而不是 256 位。
  2. MSB 泄露: kk 的高位已知。
  3. 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 万枚比特币。
相关推荐
砚底藏山河1 小时前
Python量化开发:2026最佳实时股票数据API接口推荐与对比
开发语言·windows·python
xlq223222 小时前
50.UDP套接字
网络·网络协议·udp
南境十里·墨染春水2 小时前
linux学习笔记 网络编程——Socket入门与TCP客户端/服务器实现
linux·服务器·网络
AlunYegeer2 小时前
JAVA,以后端的视角理解前端。在全栈的路上迈出第一步。
java·开发语言·前端
研究点啥好呢2 小时前
专为求职者开发的“面馆”!!!摆脱面试焦虑!!!
python·面试·开源·reactjs·求职招聘·fastapi
学网安的肆伍2 小时前
【043-WEB攻防篇】PHP应用&SQL注入&符号拼接&请求方法&HTTP头&JSON&编码类
sql·安全·php
李白你好3 小时前
基于AI大模型的网络设备安全基线排查工具
安全
hixiong1233 小时前
C# OpenvinoSharp使用DINOv2模型进行图像相似度计算
开发语言·c#
qq_三哥啊3 小时前
【mitmproxy】通过 mitmproxy 的HTTP代理模式获取 OpenCode 发起的 AI API 请求的详细信息
网络·http·代理模式