
前几天开坑说要用gf2bv试验一下,就趁着这个机会稍微研究一下MT19937吧。
Avant-填坑
gf2bv其实是maple佬写出来求解GF(2)上线性方程组的。恰好MT19937在GF(2)上关于初始状态也是线性递推,所以拿来搞MT也不是不行。
跟maple给的例子对拍了一下,这个板子主要要改的就是
1 | for o in out: |
这里,zeros.append()的时候需要注意和题目中获取randbits的方式一致。本题中是每两个getrandbits(32)获取一个,所以循环里也是这么写的。其他地方几乎不用动。
exp
1 | from tqdm import trange |

本地生成数据测了一下,比之前手搓抄的脚本快了好多🥹...
正片
Python的MT19937中,getrandbits()是按照32bit为单位产生的。也就是说:
- 如果getrandbits(0),会直接返回0,不会调用随机数生成
- 如果getrandbits(32k),那么会连续产生k个32bit的数并拼接起来
- 如果getrandbits(t),
,那么会先产生一个32bit的数字,然后截取其高t位作为本次随机数
在生成随机数前,MT19937会初始化一个state,state由624个32bit的数字组成。往后所有getrandbits产生的数字,都是关于这个初始state模2下线性的。这里的线性,意思是把初始state的二进制展开记作向量
所以实际上我们只要获取输出中任意不同位置的19968个bit,就能通过构造矩阵解方程的方法拿到初始状态(假定矩阵满秩):
这里的
exp copied from huangx607087
1 | Dall=list(map(int,open('data3.txt','r').readlines())) |
番外
Seed Recovery
有篇博文详细地讲解了MT19937的工作原理。文中特别提到,如果Python的MT19937是32bit整数的seed,那么只需要一轮624个输出中特定位置的6个即可恢复seed。当然恢复出的seed有两种可能,实际使用时还得做一些额外的判断。
2025 ApoorvCTF
task
1 | from Crypto.Util.number import getPrime, bytes_to_long, long_to_bytes |
这道题完全就是照搬上面那篇博文,直接抄就行。
exp
1 | def recover_Kj_from_Ii(Ii, Ii1, Ii2, i): |
对异或封闭
之前打本部的405杯的时候提过这个事情,也就是多个MT19937的输出异或后等价于一个MT19937的输出。如果把所有MT19937的输出流看作一个集合,无非就是说这个集合的元素对二进制异或封闭而已,原因在那篇blog里也简要阐明了。其实在本篇的观点下更容易,因为异或就是GF(2)上的加法嘛,而线性映射
做个实验:
1 | #!/usr/bin/sage |

两个MT输出流即使其中一个有"偏移"(先生成一些输出之后两者再开始异或)也是同样成立的,可以理解为MT生成一次输出后内部状态经过了一次线性递推,然后在一个新的初始状态下两个输出流进行GF(2)上的加法运算。
不过很奇怪的是,上面这个例子用maple的gf2bv就没办法求解出来,可能是zeros的写法需要进一步修正?
继续开坑
如果是多个不同bit数的MT19937输出流异或之后,有没有办法恢复初始状态呢?我觉得只取两者异或的部分作为一个MT的输出再去建立方程是可以求解的,不过就留待以后再实验吧。