算法描述
主要步骤
- 利用 seed 初始化寄存器状态
- 对寄存器状态进行旋转
- 根据寄存器状态提取伪随机数
初始化可能用的是固定的种子,也有可能是服务器时间戳,生成一个长度为624的状态数组,填充完后作为初始状态
旋转的目的是增加不确定性还有均匀性,主要是位运算实现的线性变换
提取伪随机数这一步会从状态数组中依次提取一个整数,并对其进行位运算和异或运算,生成的数即为输出的伪随机数。当所有数组全被遍历过之后,就会对状态数组再次进行一次旋转,重新生成新的状态数组。
python代码实现
这是一个简单的实现代码
def _int32(x): return int(0xFFFFFFFF & x)
class MT19937: # 初始化 def __init__(self, seed): self.mt = [0] * 624 self.mt[0] = seed self.mti = 0 for i in range(1, 624): self.mt[i] = _int32(1812433253 * (self.mt[i - 1] ^ self.mt[i - 1] >> 30) + i)
# 提取伪随机数 def extract_number(self): if self.mti == 0: self.twist() y = self.mt[self.mti] y = y ^ y >> 11 y = y ^ y << 7 & 2636928640 y = y ^ y << 15 & 4022730752 y = y ^ y >> 18 self.mti = (self.mti + 1) % 624 return _int32(y)
# 旋转状态 def twist(self): for i in range(0, 624): y = _int32((self.mt[i] & 0x80000000) + (self.mt[(i + 1) % 624] & 0x7fffffff)) self.mt[i] = (y >> 1) ^ self.mt[(i + 397) % 624]
if y % 2 != 0: self.mt[i] = self.mt[i] ^ 0x9908b0df
为什么说是简单的生成方法呢?可能知道python的random
库也是MT19937生成伪随机数,但是它对 seed 的传入经过了两步处理 init_genrand
和 init_by_array
,因此和上面的实现是有区别的,也就是说两者产生的状态矩阵和伪随机数是不一样的。
逆向extract_number
这里跟随xenny老师的思路走,就拿y = y ^ y << 7 & 2636928640
举例
那么整个式子就是
注意后七位都是0,那么最后结果的后七位和最初状态的后七位就是相同的,在y已知的情况下,我们就可以步步回推了,比如倒数8~14位就是
其他的类似,用代码实现如下
def invert(res, shift, right=True, mask=0xffffffff, bits=32): tmp = res if right: for i in range(bits // shift): tmp = res ^ tmp >> shift & mask return tmp else: for i in range(bits // shift): tmp = res ^ tmp << shift & mask return tmp
def inv_extract_number(y): y = invert(y,18,True) y = invert(y,15,False,4022730752) y = invert(y,7,False,2636928640) y = invert(y,11,True) return _int32(y)
逆向twist
在上文中我们提到如果得到了某一轮 state 的全部信息便可以向后预测随机数,那么如果我们需要向前恢复随机数,则需要对 twist 函数进行逆向。
def twist(self): for i in range(0, 624): y = _int32((self.mt[i] & 0x80000000) + (self.mt[(i + 1) % 624] & 0x7fffffff)) self.mt[i] = (y >> 1) ^ self.mt[(i + 397) % 624]
if y % 2 != 0: self.mt[i] = self.mt[i] ^ 0x9908b0df
先关注旋转的后几步操作,能注意到由于 y>>1
的最高位一定为 0,所以最终 self.mt[i]
的最高位一定由 self.mt[(i + 397) % 624]
或 self.mt[(i + 397) % 624] ^ 0x9908b0df
控制,所以可以判断出是否经历了xor 0x9908b0df
操作。然后由于是否异或操作同时受最低位控制,那么逆向的时候即可,通过是否异或来恢复因为就右移而丢失的最低位。于是我们就得到了 y
然后分析旋转的第一步,y 是由 self.mt[i]
的最高位和 self.mt[(i + 1) % 624]
的除最高位部分组合得到的。所以我们只要计算 self.mt[i]
和 self.mt[(i - 1) % 624]
两个位置的 y 就能得到 self.mt[i]
的值了
def inv_twist(state): high = 0x80000000 low = 0x7fffffff mask = 0x9908b0df
def _recover(i): y = state[i] ^ state[(i + 397) % 624] if y & high == high: y ^= mask y <<= 1 y |= 1 else: y <<= 1 return y
for i in range(len(state)-625, -1, -1): state[i] = _recover(i) & high state[i] |= _recover(i-1) & low return state
逆向init
这玩意的主要操作是
self.mt[i] = _int32(1812433253 * (self.mt[i - 1] ^ self.mt[i - 1] >> 30) + i)
你可以发现,里面主要是乘法加法和self.mt[i - 1] ^ self.mt[i - 1] >> 30
,前两者可逆运算,后面这个和前面的类似,通过invert逐位还原即可
def inv_init(last): n = 1<<32 inv = pow(1812433253,-1,n) for i in range(623,0,-1): last = ((last-i)*inv)%n last = invert(last,30) return last
给出任意19937个bit
这个类型是看鸡块神的博客学到的,主要的地方还是在于构建矩阵,直接上脚本吧
Dall=list(map(int,open('data3.txt','r').readlines()))from Crypto.Util.number import *from random import *from tqdm import *n=1250D=Dall[:n]rng=Random()def getRows(rng): #这一部分根据题目实际编写,必须和题目实际比特获取顺序和方式完全一致,且确保比特数大于19937,并且请注意zfill。 row=[] for i in range(n): row+=list(map(int, (bin(rng.getrandbits(16))[2:].zfill(16)))) return rowM=[]for i in tqdm_notebook(range(19968)):#这一部分为固定套路,具体原因已经写在注释中了 state = [0]*624 temp = "0"*i + "1"*1 + "0"*(19968-1-i) for j in range(624): state[j] = int(temp[32*j:32*j+32],2) rng.setstate((3,tuple(state+[624]),None)) #这个setstate也是固定格式,已于2025.1.21测试 M.append(getRows(rng))M=Matrix(GF(2),M)y=[]for i in range(n): y+=list(map(int, (bin(D[i])[2:].zfill(16))))y=vector(GF(2),y)s=M.solve_left(y)#print(s)G=[]for i in range(624): C=0 for j in range(32): C<<=1 C|=int(s[32*i+j]) G.append(C)import randomRNG1 = random.Random()for i in range(624): G[i]=int(G[i])RNG1.setstate((int(3),tuple(G+[int(624)]),None))
print([RNG1.getrandbits(16) for _ in range(75)])print(D[:75])
常用脚本
原始版本
最经典的款式,速度慢,不好用
def construct_a_row(RNG): row = [] for _ in range(19968//32): tmp = RNG.getrandbits(32) row += list(map(int, bin(tmp)[2:].zfill(32))) return row
# 构造线性方程组的矩阵L = []for i in trange(19968): state = [0]*624 # MT19937使用624个32位整数作为状态 # 构造一个只有一位为1,其他都为0的序列 temp = "0"*i + "1"*1 + "0"*(19968-1-i) # 将这个序列分成624段,每段32位,转换为整数 for j in range(624): state[j] = int(temp[32*j:32*j+32], 2)
RNG = Random() RNG.setstate((3,tuple(state+[624]),None)) L.append(construct_a_row(RNG))
# 将L转换为GF(2)上的矩阵(二进制域)L = Matrix(GF(2),L)print(L.nrows(), L.ncols())
def MT19937_re(state): try: # 构造目标向量R R = [] for i in state: R += list(map(int, bin(i)[2:].zfill(32)))
R = vector(GF(2), R) s = L.solve_left(R) # 这里可能会抛出异常
# 将解转换为二进制字符串 init = "".join(list(map(str,s))) state = [] # 将解重新分割成624个32位整数 for i in range(624): state.append(int(init[32*i:32*i+32],2))
# 创建新的RNG并设置恢复出的状态 RNG1 = Random() RNG1.setstate((3,tuple(state+[624]),None))
return RNG1
except Exception as e: print(f"[-]{e}") pass
RNG = MT19937_re()
randcrack
一个无脑的方法?直接利用这个库进行预测,但是只能是624*32,要不然不行
import randomfrom randcrack import RandCrack
rc = RandCrack()for i in range(624): rc.submit(random.getrandbits(32))print(random.getrandbits(64))print(rc.predict_getrandbits(64))
github上有一个优化后的版本,但是还没有用过,可以看看链接
gf2bv
maple神写的一个库,本质是接GF(2)方程组的,MT19937刚刚好满足,所以可以拿来使用,传送门。TGCTF的那个题就可以用这个库去写,非常的好用。
示例代码
from gf2bv import LinearSystemfrom gf2bv.crypto.mt import MT19937from tqdm import *import randomfrom Crypto.Util.number import *def mt19937(bs, out): lin = LinearSystem([32] * 624) mt = lin.gens()
rng = MT19937(mt) #rng.getrandbits(175) zeros = [rng.getrandbits(bs) ^ o for o in out] + [mt[0] ^ 0x80000000] print("solving...")
sol = lin.solve_one(zeros)
rng = MT19937(sol) pyrand = rng.to_python_random() for i in range(2496): out.append(pyrand.getrandbits(8)) print(pyrand.getrandbits(8))import randomrandom.seed(1)out=[]for i in range(2496): out.append(random.getrandbits(8))mt19937(8, out)
这里,zeros.append()
的时候需要注意和题目中获取randbits的方式一致。生成的pyrand其实是它的初始状态,需要预测哪个就往后递推就行了。
关于安装:
- mac直接去github下m4ri的包,然后本地编译后sudo make install
- 如果像我一样在pyenv这种虚拟环境里面跑python的,把环境注入进去
export CFLAGS="-I/usr/local/include"
export LDFLAGS="-L/usr/local/lib"
然后再pip install .