历史发展
- 伪随机数生成器(\(PRNG\))是一种基于严格确定性代数方程的算法系统,它接收一个初始的熵值作为“种子(\(Seed\))”,随后通过复杂的数学变换,输出一系列在统计学分布上表现出高度随机特征的数值序列。
- 在二十世纪九十年代中期之前,主流计算系统高度依赖结构相对简单的\(PRNG\),其中最为典型的是线性同余生成器(\(LCG\))。\(LCG\)算法凭借其极小的内存占用与极快的计算速度,曾一度同志了几乎所有的标准库系统。然而,\(LCG\)及其衍生算法存在着致命的数学缺陷,最突出的是其相对较短的循环周期以及在多维空间中表现出的“超平面聚集(\(Hyperplane\ Clustering\))”现象,这意味着当\(LCG\)生成的序列被映射到高维坐标系时,数据点回退化并散落到有限且稀疏的平行面上,导致依赖高维分布的科学模拟产生严重偏差。
- \(1997\)年,日本学者松本眞(\(Makoto\ Matsumoto\))与西村拓士(\(Takuji\ Nishimura\))联合提出了一种革命性的\(PRNG\)架构——梅森旋转算法(\(Mersenne\ Twister\)) 。该算法问世被视为随机数生成技术史上的分水岭,作为其最为知名与应用最广的具体实现版本,\(MT19937\)凭借其超长的周期、卓越的高维等距分布特性以及对底层硬件极其友好的纯位运算机制,彻底颠覆了彼时的\(PRNG\)格局,并在随后的四分之一个世纪里,成为了包括\(C++、Python、PHP、R、MATLAB\)在内的大多数核心编程语言与科学计算平台的默认选择。
数学与工程背景
- \(MT19937\)在本质上是一种高度优化的广义反馈移位寄存器(\(GFSR\))的变体,更准确地说,它属于扭曲广义反馈移位寄存器(\(TGFSR\))家族。在这些生成器中,所有的状态转换均在有限域(\(Galois\ Field\))\(GF(2)\)上进行,这意味着算法摒弃了传统处理器中耗时地整数乘法与浮点除法运算,完全使用布尔代数中的异或(\(XOR\))、按位与(\(AND\))以及移位(\(Shift\))操作来进行数据的推演。
- 对于伪随机数生成器而言,循环周期(\(Period\ Length\))是指算法在穷尽所有可能的内部状态并开始重复输出完全相同的序列之前,所能独立生成的随机数值总数。\(MT19937\)算法的循环周期被精准地设定为\(2^{19937}-1\)。如此庞大的周期确保了在进行任何规模的蒙特卡洛模拟或长期服务器运行中,由\(MT19937\)驱动的随机数流绝不会发生全局性的序列重现,从而彻底排除了周期重叠引起的统计误差。
MT19937的代数基础与核心参数矩阵架构
- 为了支撑高达\(2^{19937}-1\)的宏大周期,\(MT19937\)必须在其内存中维持一个极为庞大且非线性的内部状态空间。标准的\(MT19937\)算法(生成\(32\)位机器字的实现版本)在堆内存或栈区中分配并维护一个包含\(n=624\)个\(32\)位无符号整数的数组。选取\(624\)个\(32\)位整数是因为\(624×32=19968\)个二进制位,这刚好略大于\(19937\)位。多余的\(31\)位在算法的矩阵运算中被特定掩码遮蔽或丢弃,以此确保状态转换矩阵的特征多项式能够完美映射并对齐到上述的梅森素数域上,从而实现全周期的遍历。
- 在算法的实际编码结构中。\(MT19937\)的运转受到一组极其精密且经过海量计算机代数系统演化筛选的常数矩阵的约束。这些数学“\(Maginc\ Numbers\)”决定了算法在状态跃位切割、截断以及最终滤波时的所有行为表现。
| 参数符号 |
默认数值 |
意义 |
| \(w\) |
\(32\) |
定义系统的工作字长(\(Word\ size\)),即每次运算和输出的比特数深度。 |
| \(n\) |
\(624\) |
内部状态数组(\(State\ array\))的长度标量,构成了支持\(2^{19937}-1\)周期的空间基础。 |
| \(m\) |
\(397\) |
旋转跃迁过程中的反馈抽头偏移量(\(Offset\ parameter\)),决定了新状态与历史状态融合的跨度。 |
| \(r\) |
\(31\) |
低位掩码的有效截断位数(\(Sqparation\ point\ word\)),用于在拼接时精确剥离并重组相邻两个状态字的高低比特。 |
| \(a\) |
\(0x9908B0DF\) |
有理标准型扭曲矩阵(\(Rational\ normal\ form\ twist\ matrix\))的最低行系数向量,在多项式模\(2\)乘法中作为核心反馈项。 |
| \(f\) |
\(1812433253\) |
初始化级联乘法器机制中使用的固定非线性放大常数。 |
| \(u,d\) |
\(u=11,d=0xFFFFFFFF\) |
调和变换(\(Tempering\))首个阶段的位移深度与按位与掩码,用于向下传到高位熵。 |
| \(s,b\) |
\(s=7,b=0x9D2C5680\) |
调和变换第二阶段的向左位移深度与魔数掩码,用于破坏原始阵列在低位的线性相关性。 |
| \(t,c\) |
\(t=15,c=0xEFC60000\) |
调和变换第三阶段的向左位移深度与魔数掩码,进一步实现中高位比特的雪崩混淆。 |
| \(l\) |
\(18\) |
调和变换最后阶段的向右无符号位移深度,完成全字的位平衡。 |
- 上述特定常数的选择并非是随机选择出来的,而是松本眞团队在多维等距分布的极值搜寻测试中计算出的全局最优解。
任意修改上述列表中的哪怕一个单一比特,都会导致生成器的特征多项式偏离梅森素数,导致循环周期灾难性地缩减,或者使得输出序列无法通过严苛的统计学检验 。(个人看法)
算法解析
- 作为一种状态机(\(State\ Machine\)),\(MT19937\)从接受外部干预到持续输出伪随机数的过程,在时间线上可以被划分为三个不重叠的操作阶段:状态初始化(\(Initialization\ and\ Seeding\))、核心算法旋转(\(The\ Twist\ Mechanism\))以及输出调和变换(\(Tempering\ Transform\))。
- 状态初始化
- 在任何伪随机序列生成之前,\(MT19937\)必须首先完成其\(624\)维庞大状态数组\(S\)的初始数据填充。在理想状态下,这\(2504\)字节的状态池(包含\(624\)个\(32\)位整数以及额外一个状态指针索引)应当被灌入同样规模的纯粹物理真随机熵。然而在现实的\(API\)调用中,用户或者操作系统通常仅能提供一个单一的\(32\)位无符号整数作为系统“种子(\(Seed\))”。如何通过一个\(32\)位的微小种子去确定性且均匀地填满一个包含\(19968\)位地状态矩阵,是算法工程实现上的第一个严峻挑战。
- 在早期(\(1997\)年至\(2001\)年)的\(MT19937\)实现中,开发者使用了一种相对简单的线性生成公式来初始化数组。但随后的密码学与统计学分析挖掘出了一个被称为“零重叠(\(Zero-heavy\ state\))”或“零散列困境”的漏洞:如果输入的种子值或者随后计算出的初始状态序列中包含了过多的二进制"0",由于底层\(GFSR\)的代数惰性,系统可能需要经历多达数十万次的内部迭代转换,才能彻底“稀释”这些密集的零位并恢复到统计学上均匀的高熵状态。这种现象在某些特定模式下,会导致输出序列在相当长的周期内呈现出极为明显的非随机倾向。例如,当两个初始种子的海明距离(\(Hamming\ Distance\))非常接近时,早期版本的初始化机制会导致这两个种子产生的早期随机数流极为相似。
- 为了解决这一问题,在\(2002\)年,研究团队发布了经过全面重构的标准化代码版本
mt19937ar.c(其中的"ar"代表了增其昂的数组初始化特性)。新版本引入了强烈的非线性级联迭代公式,用以保证初始状态的多样性与抗雪崩性能。该初始化机制的首个状态\(S\)直接等于用户提供的种子值,而后续的数组元素(从\(i=1\)到\(n-1\))则严格依据以下递归公式计算产生:\(S[i]=(f×(S[i-1]\oplus (S[i-1]>>30))+i)\&d\)这一公式中,常数\(f=1812433253\),位掩码\(d=0xFFFFFFFF\),确保了所有的乘法结果被严格截断回\(32\)位的字长限制内。公式的精妙之处在于\(S[i-1]\oplus (S[i-1]>>32)\)这一结构,它通过向右位移\(30\)位(即\(w-2\))并将结果与原值异或,强制前一个状态的最高几位熵被立即传递并混淆到最低位中。随后的非线性整数乘法与加上数组索引\(i\)的操作,共同打破了位模式的对称性。这个级联乘法器结构保证了即使初始种子在二进制层面仅有单个比特的反转差异,整个\(624\)维度的状态数组也会呈现出几乎完全不同的不可预测分布,有效避免了“零重叠”漏洞。
- 然而,尽管数学上解决了零状态恢复缓慢的问题,现代的高级编程语言生态在调用该初始化机制时仍存在显著的工程隐患。以 C++ 为例,Melissa O'Neill 等研究者指出,绝大多数开发者使用类似
std::mt19937 my_rng(std::random_device{}()); 的语法来播种生成器,这在本质上向引擎输入了仅仅 32 位的熵 。基于 32 位熵仅能触及 \(2^{32}\)(约 42.9 亿)种可能的初始状态,而 MT19937 的完整状态空间高达 \(2^{19968}\)。这种被称为“欠播种(Under-seeding)”的操作不仅意味着绝大多数的随机序列轨迹永远不可能被程序走到,甚至会导致极其罕见的边界状况发生——例如,在特定 32 位随机数下,上述初始化代码使得该生成器永远不可能在第一次调用时输出整数 7 或 42 。为了实现绝对严谨的系统重置,现代最佳实践强烈建议采用更高熵维度的结构进行初始化,例如在 C++ 中结合系统级 CSPRNG(如 Unix/Linux 环境下的 /dev/urandom 或 Windows 平台的 CryptGenRandom)提取至少 256 位的真实随机字节流,并通过堆栈分配的 std::seed_seq 引擎将之充分搅拌后再喂给 MT19937 进行完整的阵列初始化 。
- 核心算法旋转
- 一旦\(624\)个元素的状态数组被合法且充分地填充,\(MT19937\)便具备了对外提供伪随机数流地能力。然而,为了保持算法地高吞吐特性,\(MT\)算法并不会在每次系统请求随机数时都去执行复杂地内部状态更新。相反,它采用了一种延迟执行地批处理策略:算法直接从数组中依次取值,仅当数组中所有地\(624\)个数值都被耗尽读取后,系统才会触发一次全局的“旋转(\(Twist\))”事件。
- 这个旋转引擎是梅森旋转算法的代数核心,它负责将旧的\(624\)个状态通过严密的模\(2\)线性递推转化为全新的\(624\)个状态。旋转机制完全摒弃了导致\(CPU\)指令周期漫长的算术乘除与取模,纯粹通过极简的位操作完成。这使得生成器在其核心循环中极为轻量,具体而言,算法通过遍历当前的数组索引\(i\)(从\(0\)到\(n-1\)),并依照以下复杂的逻辑来合成下一个周期的状态\(S_{new}[i]\):
- 相邻两个历史状态的高低位切割与重组(位拼接)。算法通过高位掩码(\(Upper/High\ Mask\),对\(32\)位而言就是\(0x80000000\))提取出当前状态\(S[i]\)的最高\(w-r\)位(即最高位);与此同时,通过低位掩码(\(Lower\ Mask\),即\(0x7FFFFFFF\))提取出数组中紧邻的下一个状态\(S[(i+1)\pmod{n}]\)的较低\(r\)位(即低\(31\)位)。这两个部分被利用逻辑或(\(OR\))操作无缝拼接成一个新的中间临时变量\(Y\)。
- 算法对临时变量\(Y\)执行乘以有利标准型转移矩阵\(A\)的代数运算。在有限域\(GF(2)\)中,矩阵乘法由于没有进位效应,退化为精妙的条件移位与异或网络(参考\(AES\)算法中的列混淆所涉及的有限域乘法)。算法检测\(Y\)的最低有效位(\(Least\ Significant\ Bit\)):
- 若\(Y\)为偶数(最低位为\(0\)),则结果仅为将\(Y\)向右逻辑位移\(1\)位(\(Y>>1\))
- 若\(Y\)为奇数(最低位为\(1\)),则在将\(Y\)向右逻辑移位\(1\)位后,还需要与固定的魔数阵列常数\(a=0x9908B0DF\)进行异或(\(XOR\))融合操作(\((Y>>1)\oplus a)\))
- 经历过特征多项式扭曲的临时结果,还必须与状态数组中距离当前位置具有一定历史跨度的数据进行反馈汇聚。这一跨度被称为偏移量参数 \(m\),在 MT19937 中定为 397 。算法将矩阵乘法的结果与历史状态 \(S[(i+m) \pmod n]\) 执行异或,以此产出最终被写回原数组的新状态新值: \(S_{new}[i] = S[(i+m) \pmod n] \oplus \text{Twist}(Y)\)
这里多说一些,在搜集这方面相关学习资料的时候,看到了当前很多密码工程的优化,所以我认为引入偏移参数 \(m=397\) 是整个架构中非常精湛的一笔。这使得任何单个比特的变化都不会仅仅影响其直接相邻的下一代比特,而是跨越了数百个整数的内存屏障,引发了极其深远的非线性雪崩效应。底层的 C/C++ 编译器通常能将这一循环极度优化,在某些现代 CPU 上,一个循环步进只耗费不到三条微指令。即便如此,在更先进的体系结构要求下,科研界基于此原理拓展出了基于单指令多数据流(SIMD)扩展指令集(如 SSE2、AVX2)的变种 VMT19937 与 SFMT,将生成与轮转操作彻底向量化,使得吞吐量能够随着现代处理器内部寄存器宽度的成倍增加而呈线性扩展,且免于重新校准底层复杂的数学多项式参数 。
- 调和变换
- 前面学习的过程中,看到了很多师傅都将其视作“提取输出”的操作,其实其是以调和变换的方式完成了“提取输出”的作用,两者并不冲突。
- 经过旋转操作之后,内部数组\(S\)在宏观生命周期熵完美契合了梅森素数的多项式要求,但是它依旧是一个纯移位寄存器系统。如果将此时的状态\(S[i]\)直接作为生成器的随机数暴露给外部,由于模\(2\)线性递推结构的数学性质,这些数值在比特位模式分析中会展现出明显的伪影--它们不仅在线性复杂度上不过关,更会在坐标系中勾勒出规律的“晶格(\(Lattice\))”状结构,导致统计学检验的严重失败。[[CTF/随机数/流密码|流密码]]
- 为了避免类似于\(B-M\)算法此类的密码分析,\(MT19937\)在随机数值离开生成器输出的时候,增加了一道“调和变换(\(Tempering\ Transform\))”的不可逆过滤机制,这里的“不可逆”是针对不知道特定魔数或者仅具备常规统计能力的应用程序而言的;在严格的密码学分析中,它本质上仍是一个全排列双射。
- 调和函数通过四个连续且互相依存的移位与异或步骤对准备输出的内部值\(x\)进行彻底的比特“搅动(\(Scrambling\))”:
- 向下弥散高位熵:\(y_1=x\oplus((x>>11)\&0xFFFFFFFF)\),通过向右移位\(11\)位,强迫最高位置的随机性叠加到中段位移区。
- 利用左移向上传导扰动:\(y_2=y_1\oplus ((y_1<<7)\&0x9D2C5680)\)。这里的向左位移\(7\)位操作收到魔数掩码\(b\)的严格限制,掩码的分布特征精确截断了不合期望的线性关联。
- 强化中高位域的雪崩效果:\(y_3=y_2\oplus ((y_2 << 15)\& 0xEFC60000)\),通过左移\(15\)位以及掩码\(c\)的阻隔,原本存在于\(GFSR\)低位的脆弱结构被完全打散,化作为伪噪声的本底。
- 全局平衡扫描:\(y_4=y_3\oplus(y_3>>18)\),再一次向下位移\(18\)位,完成了全字范围内的数据回环。最终输出的\(y_4\)为整个系统输出的“伪随机数”。
示例代码
const N: usize = 624;
const M: usize = 397;
const MATRIX_A: u32 = 0x9908b0df; // 旋转矩阵常数向量
const UPPER_MASK: u32 = 0x80000000; // 提取最高位
const LOWER_MASK: u32 = 0x7FFFFFFF; // 提取低32位
pub struct MT19937 {
mt: [u32; N], // 长度为 624 的状态数组
index: usize // 状态指针索引
}
impl MT19937 {
// 初始化
pub fn new(seed: u32) -> Self {
let mut mt = [0; N];
mt[0] = seed;
for i in 1..N {
let prev = mt[i - 1];
let temp = prev ^ (prev >> 30);
mt[i] = 1812433253u32.wrapping_mul(temp).wrapping_add(i as u32);
}
Self {
mt,
index: N //初始化后设为N,确保第一次提取时触发 twist
}
}
// 旋转
fn twist(&mut self) {
for i in 0..N {
// 将当前元素的最高位与下一个元素的低31位拼接
let x = (self.mt[i] & UPPER_MASK) | (self.mt[(i + 1) % N] & LOWER_MASK);
let mut x_a = x >> 1;
// 如果最低位是 1 ,则于魔法矩阵常数 A 异或
if x & 1 == 1 {
x_a ^= MATRIX_A;
}
// 将相距M的元素与计算结果异或,生成新状态
self.mt[i] = self.mt[(i + M) % N] ^ x_a;
}
self.index = 0; // 旋转完毕后,索引置为0
}
// 调和输出
pub fn extract_number(&mut self) -> u32 {
if self.index >= N {
self.twist();
}
let mut y = self.mt[self.index];
self.index += 1;
y ^= y >> 11;
y ^= (y << 7) & 0x9d2c5680;
y ^= (y << 15) & 0xefc60000;
y ^= y >> 18;
y
}
}
fn main() {
let seed = 2023;
let mut rng = MT19937::new(seed);
for i in 1..=5 {
println!("P{} = {}", i, rng.extract_number());
}
}
伪随机序列的评价标准
- 在这套系统的庇护下,最终输出方能安然通过包括 \(Diehard\) 测试集在内的大量苛刻验证 。
- 评估伪随机数生成器质量的理论标准之一,就是考察其所生成序列在\(k-\)维超空间内的均匀分布法则,统称为\(k-分布特性(k-distribution\ property)\)。其内容如下:假设拥有一个周期为\(P\)且每个独立数值占据\(w\)个二进制位的伪随机序列,如果截取该序列的连续\(k\)个值组成一个多维向量,并且在这个规模庞大的周期内遍历所有的可能向量,只要该向量序列在其前\(v\)位的精度截断平面上,能够在总计\(k×v\)个比特组成的全排列空间中呈现出完全等概率的出现频次(除了由于全零向量导致的微小差异),则数学上严格证明该伪随机序列满足精度为\(v\)的\(k-\)维等距分布特性。
- \(MT19937\)是人类计算机代数史上首批成功实现\(623维等距分布(623-dimensionally\ equidistributed)\) 并保持在全量\(32\)精度的算法之一。这意味着在进行极端苛刻的复杂蒙特卡洛抽样检测时,如果将\(MT19937\)生成的随机序列以\(623\)个连续数字为一组,包裹并投射到一个具有\(623\)个正交维度轴的集合超立方体内;那么当生成器完成其\(2^{19937}-1\)步的宏观周期后,这些难以计数的坐标散点将以近乎绝对完美的概率密度矩阵填满整个高维空间内部的每一个细微网络。对于更低的位精度容忍度而言,其表现则更加完美--若只观测最低有效\(6\)位,\(MT19937\)可以展现出深达\(2492\)维的极致均匀分布表现。
MT19937的相关攻击
- 必须在最高级别的安全规范指导下明确指出:尽管梅森旋转算法在处理海量科学模拟和通用逻辑判断时性能超卓,但它在根本上不属于(\(CSPRNG, Cryptographically Secure Pseudo-Random Number Generator\))的角色 。无论是利用该引擎生成应用层的会话密钥(\(Session\ Keys\))、用户验证安全令牌、密码哈希的加盐成分,还是作为加密算法的初始化向量(\(IV\)),都会将整个系统架构暴露在入侵风险之中。
- \(MT19937\) 的底层逻辑对于攻击者而言是呈现“全状态透明”的,这种脆弱性的直接根源在于其调和变换矩阵(\(Tempering\))纯粹的数学可逆性以及线性系统的低熵防御壁垒 。
- 前文曾经解构过输出前的四步调和过滤流程。这四个操作仅仅是在一个有限的 \(32\) 维特征向量空间内的纯线性双射映射变换而已 。这就意味着,通过观察到一个被调和后的最终输出数值 \(y\),执行逆向工程(即“反调和\(Untempering\)”)就可以丝毫不差地恢复出它在进入过滤器前的原始内部晶格状态 \(x\) 。
- 举个例子,推演对最后一步操作 \(y = x \oplus (x \gg 18)\) 的逆向破解法则。由于进行的是长达 \(18\) 位的向右强制截断推移,内部隐秘状态 \(x\) 在右移 \(18\) 位之后,其最高层级的 \(18\) 位将由于截断而不可逆转地全部归零(即 \(x \gg 18\) 的前 \(18\) 位全部为 \(0\))。依据异或运算 \(\oplus\) 的特性,任何数异或零均等于其自身。因此,在截获了系统的直接输出 \(y\) 之后,不争的事实就是:\(y\) 序列的高端 \(18\) 位必须与隐藏状态 \(x\) 的高端 \(18\) 位一模一样(即 \(x_{[31:14]} = y_{[31:14]}\))。一旦获得了这 \(18\) 个确切比特的支配权,攻击者就能顺着位移的方向,使用已知的这部分 \(x\) 高位数据去模拟 \((x \gg 18)\) 在更低位(即第 \(13\) 到 \(0\) 位)所产生的影响,进而将其与已知的 \(y\) 的对应低位进行二次异或抵消,从而还原出剩余的所有下层比特。
- 同样的层层渗透逻辑,通过精心设计的向左或向右逆向位掩码逐段递推技巧,可以解开剩余另外三个涉及魔数掩码和\(7、11、15\)移位差值的。这种操作被安全界简称为“反调和(\(Untempering\))”算法 。
- 鉴于 \(MT19937\) 的全局状态阵列由 \(624\) 个 \(32\) 位基石构成,且其内部旋转跃迁操作(\(The\ Twist\))是绝对固定的递推逻辑;如果存在一个潜伏的监听端,只要它成功地捕捉到由系统连续输出的 \(624\) 个序列值,攻击者会对这 \(624\) 个数据逐个施加反调和函数,从而完美且完整地在本地内存中复现出生成系统在此时刻的所有内部状态矩阵 。
- 如果攻击者成功还原出了内部状态矩阵,不仅可以完成后续随机序列的预测;得益于旋转算法代数方程群本身也可以构造逆运算网络,攻击者还能反向推演出系统先前生成的随机序列,甚至可能通过反向回溯穷尽追查出最初用于激活系统的起源“种子” 。因此为了防范此类密码学安全漏洞,现代密码系统抛弃了基于简单状态演化的引擎,转而拥抱具备单向散列安全底蕴(例如 \(SHA-256\))或是抗预测加密计数器(如 \(AES-CTR\) 及 \(ChaCha20\) 流密码体系)构建的真正的 \(CSPRNG\) 。
- 在原来进行\(CTF\)竞赛的过程中,遇到过此类的攻击,我印象中比较典型的是
randcrack这个项目(https://github.com/tna0y/Python-random-module-cracker),这里分析一下他的源代码,大致的思想就是进行上述内容中的反调和函数以及进行反向旋转回退状态,下面是按照该项目进行仿写的一个工具类:
import random
import time
class FastRandCrack:
def __init__(self):
self.mt = [0] * 624
self.index = 0
def _unshift_right(self, value, shift):
""" 逆向处理 y = (x >> shift) ^ x """
res = value
# 32 位整数最多只需要循环 32 // shift 次
for _ in range(32 // shift):
res = value ^ (res >> shift)
return res
def _unshift_left(self, value, shift, mask):
""" 逆向处理 y = x ^ ((x << shift) & mask) """
res = value
for _ in range(32 // shift):
res = value ^ ((res << shift) & mask & 0xffffffff)
return res
def _untemper(self, y):
""" 逆向 temper """
y = self._unshift_right(y, 18)
y = self._unshift_left(y, 15, 0xefc60000)
y = self._unshift_left(y, 7, 0x9d2c5680)
y = self._unshift_right(y, 11)
return y
def submit(self, number):
if self.index >= 624:
raise Exception("Full!")
self.mt[self.index] = self._untemper(number)
self.index += 1
def _twist(self):
for i in range(624):
y = (self.mt[i] & 0x80000000) | (self.mt[(i + 1) % 624] & 0x7fffffff)
y = y >> 1
if y & 1:
y ^= 0x9908b0df
self.mt[i] = y ^ self.mt[(i + 397) % 624]
self.index = 0
def predict(self):
if self.index == 624:
self._twist()
# 提取当前状态
y = self.mt[self.index]
self.index += 1
y ^= (y >> 11)
y ^= ((y << 7) & 0x9d2c5680)
y ^= ((y << 15) & 0xefc60000)
y ^= (y >> 18)
return y
def _untwist(self):
n, m = 624, 397
prev = [0] * n
y = [0] * n
def rev_A(z):
return (((z ^ 0x9908b0df) << 1) | 1) & 0xffffffff if (z & 0x80000000) else (z << 1) & 0xffffffff
for i in range(n - 1, n - m - 1, -1):
y[i] = rev_A(self.mt[i] ^ self.mt[i - (n - m)])
for i in range(n - 1, n - m, -1):
prev[i] = (y[i] & 0x80000000) | (y[i - 1] & 0x7fffffff)
for i in range(n - m - 1, -1, -1):
y[i] = rev_A(self.mt[i] ^ prev[i + m])
for i in range(n - m, 0, -1):
prev[i] = (y[i] & 0x80000000) | (y[i - 1] & 0x7fffffff)
prev[0] = (y[0] & 0x80000000) | (y[623] & 0x7fffffff)
self.mt = prev
def offset(self, n):
""" 控制状态指针前进或回退 """
if n >= 0:
for _ in range(n):
self.predict()
else:
tempIndex = self.index + n
while tempIndex < 0:
self._untwist()
tempIndex += 624
self.index = tempIndex
- 由于这里保留的接口基本与原项目保持一致,所以原项目中的
test.py基本上可以直接进行保持运行:
import random
import time
from attack import FastRandCrack
random.seed(time.time())
unknown = [random.getrandbits(32) for _ in range(10)]
cracker = FastRandCrack()
for i in range(624):
cracker.submit(random.getrandbits(32))
cracker.offset(-624)
cracker.offset(-10)
print(f"Unknown:{unknown}")
print(f"Guesses:{[cracker.predict() for _ in range(10)]}")
- 此后要做的事情就是对与这里的工具进行优化,运用的是《密码工程》课程中的相关思想和方法:
use clap::Parser;
use std::fs::File;
use std::io::{BufRead, BufReader};
#[derive(Parser, Debug)]
#[command(name = "MT19937-Cracker")]
#[command(author = "chenxing")]
#[command(version = "1.0")]
#[command(about = "MT19937", long_about = None)]
struct Args {
#[arg(short, long = "in", value_name = "FILE")]
in_file: String,
#[arg(short, long, allow_hyphen_values = true)]
offset: isize,
}
pub struct RandCrack {
mt: [u32; 624],
index: usize,
}
impl RandCrack {
pub fn new() -> Self {
Self { mt: [0; 624], index: 0 }
}
#[inline(always)]
fn unshift_right(value: u32, shift: u32) -> u32 {
let mut res = value;
for _ in 0..(32 / shift) { res = value ^ (res >> shift); }
res
}
#[inline(always)]
fn unshift_left(value: u32, shift: u32, mask: u32) -> u32 {
let mut res = value;
for _ in 0..(32 / shift) { res = value ^ ((res << shift) & mask); }
res
}
#[inline(always)]
fn untemper(&self, mut y: u32) -> u32 {
y = Self::unshift_right(y, 18);
y = Self::unshift_left(y, 15, 0xefc60000);
y = Self::unshift_left(y, 7, 0x9d2c5680);
y = Self::unshift_right(y, 11);
y
}
pub fn submit(&mut self, number: u32) {
if self.index < 624 {
self.mt[self.index] = self.untemper(number);
self.index += 1;
}
}
#[inline(always)]
fn twist(&mut self) {
for i in 0..624 {
let y = (self.mt[i] & 0x80000000) | (self.mt[(i + 1) % 624] & 0x7FFFFFFF);
let mask = (y & 1).wrapping_neg();
self.mt[i] = self.mt[(i + 397) % 624] ^ (y >> 1) ^ (mask & 0x9908B0DF);
}
self.index = 0;
}
pub fn predict(&mut self) -> u32 {
if self.index >= 624 { self.twist(); }
let mut y = self.mt[self.index];
self.index += 1;
y ^= y >> 11;
y ^= (y << 7) & 0x9D2C5680;
y ^= (y << 15) & 0xEFC60000;
y ^= y >> 18;
y
}
#[inline(always)]
fn untwist(&mut self) {
let n = 624;
let m = 397;
let mut prev = [0u32; 624];
let mut y = [0u32; 624];
let rev_a = |z: u32| -> u32 {
let msb = z >> 31;
let mask = msb.wrapping_neg();
((z ^ (mask & 0x9908b0df)) << 1) | msb
};
for i in (n - m..n).rev() { y[i] = rev_a(self.mt[i] ^ self.mt[i - (n - m)]); }
for i in (n - m + 1..n).rev() { prev[i] = (y[i] & 0x80000000) | (y[i - 1] & 0x7FFFFFFF); }
for i in (0..=n - m - 1).rev() { y[i] = rev_a(self.mt[i] ^ prev[i + m]); }
for i in (1..=n - m).rev() { prev[i] = (y[i] & 0x80000000) | (y[i - 1] & 0x7FFFFFFF); }
prev[0] = (y[0] & 0x80000000) | (y[623] & 0x7FFFFFFF);
self.mt = prev;
}
pub fn offset(&mut self, n: isize) {
if n >= 0 {
for _ in 0..n { self.predict(); }
} else {
let mut new_index = self.index as isize + n;
while new_index < 0 {
self.untwist();
new_index += 624;
}
self.index = new_index as usize;
}
}
}
fn main() {
let args = Args::parse();
let file = File::open(&args.in_file).unwrap_or_else(|_| {
eprintln!("[x] Error: Unable to open file '{}'", args.in_file);
std::process::exit(1);
});
let reader = BufReader::new(file);
let mut data = Vec::with_capacity(624);
for line in reader.lines() {
if let Ok(l) = line {
for word in l.split_whitespace() {
if let Ok(num) = word.parse::<u32>() {
data.push(num);
}
}
}
}
if data.len() < 624 {
eprintln!("[x] Error: Insufficient numbers in file, expected 624! (found: {})", data.len());
std::process::exit(1);
}
let mut cracker = RandCrack::new();
for &num in &data[0..624] {
cracker.submit(num);
}
cracker.offset(args.offset);
cracker.offset(-624);
let seed = cracker.mt[cracker.index];
println!("{}", seed);
}
- 优化的点就在于利用数学的确定性减少了一次条件分支,对于在旋转过程中是否与矩阵\(A\)发生异或操作取决于右移后的\(y\)是奇数还是偶数,这里存在一个条件分支,是比较占用时钟周期的。我们可以:
y & 1 == 1时,通过wrapping_neg()把1变为0xFFFFFFFF(全\(1\)掩码)。
y & 1 == 0时,把0变成0x00000000(全\(0\)掩码)。
- 此后将这个掩码与
0x9908B0DF进行按位与&运算,如果是0xFFFFFFFF & 0X9908B0DF,常数被完整保留,参与后续的异或;如果是0x00000000 & 0x9908b0df,常数会变为0,异或0操作空转。
现代计算架构下的MT19937面临的挑战
- 随着现代计算机的发展以及指令集架构(\(ISA\))的不断扩张,评估一款随机数生成器商业与科学价值的核心指标逐渐从单纯的高维数学纯洁性,向兼顾缓存层次结构亲和性、内存总线带宽以及多线程并行吞吐能力的综合坐标转移。\(MT19937\)原有的生成效率优点就变得不那么突出了。
| 伪随机数算法族系 |
内部状态空间占用(Footprint) |
相对运算速度(估值与测试吞吐量) |
循环周期预测 |
| 标准线性同余生成器 (\(LCG/minstd\)) |
仅需 \(4\) 字节 (单整形变量) |
极其高效迅速 |
受限于 \(2^{31} - 2\) |
| 梅森旋转算法 (\(MT19937\)) |
高达 \(2504\) 字节 (\(~2.5 KB\)) |
中等偏上 (\(~1700 MB/s\) 级别测试) |
宏伟的 \(2^{19937} - 1\) |
| 置换同余生成器族系 (\(PCG64\)) |
极小,约 \(8 - 16\) 字节 |
极速运转 (\(~4150 MB/s\)) |
\(2^{64}\) 至 \(2^{128}\) |
| 异或位移生成器体系 (\(Xoroshiro128+\)) |
极小,稳定在 \(16\) 字节 |
当前巅峰 (\(~8100 MB/s\)) |
\(2^{128} - 1\) |
- 通过上述矩阵式的宏观对照,\(MT19937\)面临的最直观的工程抗议源于其臃肿的运行时状态负荷。前文已经明确,要维持算法机器的运转,系统必须为其分配包含 \(624\) 个元素的状态数组再加上用于追踪旋转进度的索引游标变量,合计约 \(2504\) 字节(或 \(2.5 KB\) 左右)的内存区段 。
当前的发展
- 在意识到 \(MT19937\) 在内存与性能天平上的巨大倾斜后,统计学界与工程界在过去十年内推动了数次技术转移。新一波崛起的新型伪随机生成器以\(Melissa\ E.\ O'Neill\)教授提出的置换同余生成器(\(PCG, Permuted\ Congruential\ Generator\))家族,以及由\(Sebastiano\ Vigna\)主导研发的 \(Xoroshiro(XOR/rotate/shift/rotate 的精简组合)\)算法系列为绝对的主导力量 。
写在最后
- 这篇学习笔记写了很久,一直相对密码学的随机数这块进行一下研究学习,但是从编程语言的学习到阅读完\(MT19937\)的论文中间自己拖了很长时间。随机性可以说是密码学的基石之一了,为何比较执着于\(MT19937\),一部分原因是因为先前在\(CTF\)赛事中遇到过不少基于该方案出的题目,当时比较依赖于一些现有的开源库,并不知道其中的攻击方法,前段时间又回过头学习了一下流密码的相关内容,感觉随机数这里还是要下点功夫的。这篇笔记主要就是记录了我对于\(MT19937\)的认知,其中也不乏有我阅读部分博客之后的见解。
- 至于随机数的相关学习,其实\(MT19937\)是远远不够的,甚至说是对于密码学而言,其目前应该是无法投入使用的了,当前主流的密码学随机数发生器一般采用的都是\(TRNG+CSPRNG\)的组合。但是\(MT19937\)的思想是值得学习的,毕竟它在刚进行推出的时候效果是几乎超过了彼时所有的伪随机数发生器,包括我们当前的日常生活使用中,几乎所有的编程语言都在使用着\(MT19937\),甚至导致了现在如果要追求极致的密码学随机数安全进行更换的话,出现了巨大的功能耦合的问题。
- 后续的话,对于随机数的学习想着学习一下\(CSPRNG\),了解学习一下密码学安全的伪随机数发生器的思想以及构造。