MT19937
文章目录
- MT19937
- 题型1 逆向extract_number
- [SUCTF2019]MT
- 题型2 预测随机数
- [GKCTF 2021]Random
- 题型3逆向twist
- [V&N2020 公开赛]Backtrace
- 题型4 逆向init
- 扩展题型
- WKCTF easy_random
- 现成模块
- randcrack库
- Extend MT19937 Predictor库
MT19937是一种周期很长的伪随机数生成算法,可以快速生成高质量的伪随机数,主要由三部分组成:
1.利用seed初始化624的状态
2.对状态进行旋转
3.根据状态提取伪随机数
梅森旋转算法可以产生高质量的伪随机数,并且效率高效,弥补了传统伪随机数生成器的不足。梅森旋转算法的最长周期取自一个梅森素数:2^19937 - 1由此命名为梅森旋转算法。常见的两种为基于32位的MT19937-32和基于64位的MT19937-64
32位的MT19937的python代码如下:
def _int32(x):#保证32位return int(0xFFFFFFFF & x)class MT19937:# 根据seed初始化624位的statedef __init__(self, seed):self.mt = [0] * 624self.mt[0] = seed#这里的mt[]数组就是我们的状态stateself.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()#624个数据取完之后进行旋转,即把数据更新y = self.mt[self.mti]y = y ^ y >> 11y = y ^ y << 7 & 2636928640y = y ^ y << 15 & 4022730752y = y ^ y >> 18#数据还要进行一系列的位运算和与运算self.mti = (self.mti + 1) % 624#以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算法
还有一个点:MT19937每次生成的随机数都是32位的
getrandbits(32)方法可以获得一个32位随机数
getrandbits(8)获得的是32位随机数的前8位,而在这之后再次使用getrandbits(8)获取的是下一个32位随机数的前8位
getrandbits(64)则是两个32位随机数的拼接,倒序拼接
import randomrandom.seed(1) print(hex(random.getrandbits(32))[2:])#getrandbits(a)是生成a位二进制的随机数 print(hex(random.getrandbits(32))[2:]) #2265b1f5 #91b7584arandom.seed(1) print(hex(random.getrandbits(8))[2:]) print(hex(random.getrandbits(8))[2:]) #22 #91random.seed(1) print(hex(random.getrandbits(64))[2:])#64位的是两个32位的拼接,但是是倒序拼接 #91b7584a2265b1f5random.seed(1) # print(random.randbytes(8))#ranbytes(a)是生成a个字节的数据 print(hex(bytes_to_long(random.randbytes(8)))[2:])#正好是getrandbits生成的数据的倒序 #f5b165224a58b791
总结来说:
getrandbits( a )生成指定a位数的数据
以32位为分界线
小于32位的取前a位,接下来再次生成的随机数又是从下一个32位数据中取
大于32位的就取多个32位数据进行拼接,倒序拼接
randbytes()生成指定字节数的数据
同getrandbits类似的,但是这个拼接是顺序拼接的,其实就是一个大端一个小端
state
是伪随机数发生器的重要变量,它决定了输出怎样的随机数,也就是说知道了state
你可以预测出它将要输出的数
import randomori_state = random.getstate() # 得到该伪随机数发生器的state,也就是状态量
# 输出随机数
print(random.getrandbits(32)) # 3410545957
print(random.getrandbits(32)) # 1647702295random.setstate(ori_state)
# 验证随机数是否相等
print(random.getrandbits(32)) # 3410545957
print(random.getrandbits(32)) # 1647702295
#也就是说相同的state可以得到相同的随机数
接下来看看state的组成:
import randomrandom.seed(1)
print(random.getstate())
#(3, (2147483648, 163610392, ... , 1781494222, 624), None)
中间的元组类型有625项,前624项即是初始化时得到的整数,而第625项(此时等于624)是该state此时对应的第几个整数,也就是说,在下一次执行extract_number()函数时的state里面应该调用第几个整数,换句话说,就是extract_number()函数中的mti,此时mti = 624,作用类似于计数器,因为每624为一个周期
其他参数似乎是默认的,在之后我们求出624 个整数之后,需要按这个格式进行构造真正的state;为了方便叙述,之后的state都暂时指代这624个整数
题型1 逆向extract_number
看到extract_number()函数
def extract_number(self):if self.mti == 0:self.twist()#624个数据取完之后进行旋转,即把数据更新y = self.mt[self.mti]y = y ^ y >> 11y = y ^ y << 7 & 2636928640y = y ^ y << 15 & 4022730752y = y ^ y >> 18#数据还要进行一系列的位运算和与运算self.mti = (self.mti + 1) % 624#以624为一个周期return _int32(y)
可以发现该函数对于state[i]进行了一系列的异或和位运算,最后输出随机数
那么我们一步步来推导一下:
y = y ^ ( y >> 18 )
可以发现这一步对于y的高18位并没有影响,即运算结束后得到的 y’= y ^ ( y >> 18 ) , y’ 的高18位就是 y 的高18位,这样就可以得到y的高18位,那么再异或回去,我们就可以得到y的高36位了,依次类推下去,循环的异或我们就能够得到y的所有位了
也就是说我们可以在有限步内,获得y的所有信息,即我们可以根据 y’ 逆向出 y
代码:
o = 2080737669
print(o.bit_length())#31
y = o^o>>18
# 控制位移的次数
for i in range(31//18):y = y^(y>>18)
print(y==o)
#如果o的位数大于36那么代码还需要做修改
#但是实际上y的大小应该是固定的,32位数,所以这段代码可以固定使用
继续分析:
y = y ^ y << 15 & 4022730752
根据运算符的优先级有:
y = y ^ ((y << 15) & 4022730752)
所以 y’ 的低15位是y的低15位异或4022730752的低15位的结果
那么通过 y’ 的低15位和4022730752的低15位异或就可以求到y的低15位,与上面同样的,我们根据y的低15位与4022730752与一下,再和 y’ 的15-30位进行异或就可以得到y的15-30位了,这样就求到了y的30位,以此类推,经过有限步,我们可以求到y的所有位
代码:
o = 2080737669
y = o ^ o << 15 & 4022730752
tmp = y
for i in range(32 // 15):# (y<<15)&40022730752 每次可以恢复y的15位y = tmp ^ y << 15 & 4022730752
print(y==o)
剩下的两步和上面的类似,最终的代码如下:
o = 2080737669# right shift inverse
def inverse_right(res, shift, bits=32):tmp = resfor i in range(bits // shift):tmp = res ^ tmp >> shiftreturn tmp# right shift with mask inverse
def inverse_right_mask(res, shift, mask, bits=32):tmp = resfor i in range(bits // shift):tmp = res ^ tmp >> shift & maskreturn tmp# left shift inverse
def inverse_left(res, shift, bits=32):tmp = resfor i in range(bits // shift):tmp = res ^ tmp << shiftreturn tmp# left shift with mask inverse
def inverse_left_mask(res, shift, mask, bits=32):tmp = resfor i in range(bits // shift):tmp = res ^ tmp << shift & maskreturn tmpdef extract_number(y):y = y ^ y >> 11y = y ^ y << 7 & 2636928640y = y ^ y << 15 & 4022730752y = y ^ y >> 18return y&0xffffffffdef recover(y):y = inverse_right(y,18)y = inverse_left_mask(y,15,4022730752)y = inverse_left_mask(y,7,2636928640)y = inverse_right(y,11)return y&0xffffffff#保证是32位y = extract_number(o)
print(recover(y) == o)
来道题练练吧:
[SUCTF2019]MT
from Crypto.Random import random
from Crypto.Util import number
from flag import flagdef convert(m):m = m ^ m >> 13m = m ^ m << 9 & 2029229568m = m ^ m << 17 & 2245263360m = m ^ m >> 19return mdef transform(message):assert len(message) % 4 == 0new_message = ''for i in range(len(message) / 4):block = message[i * 4 : i * 4 +4]block = number.bytes_to_long(block)block = convert(block)block = number.long_to_bytes(block, 4)new_message += blockreturn new_messagetransformed_flag = transform(flag[5:-1].decode('hex')).encode('hex')
print 'transformed_flag:', transformed_flag
# transformed_flag: 641460a9e3953b1aaa21f3a2
很明显和extract_number()函数很类似,直接套上面的脚本即可
from Crypto.Util.number import *
# right shift inverse
def inverse_right(res, shift, bits=32):tmp = resfor i in range(bits // shift):tmp = res ^ tmp >> shiftreturn tmp# right shift with mask inverse
def inverse_right_mask(res, shift, mask, bits=32):tmp = resfor i in range(bits // shift):tmp = res ^ tmp >> shift & maskreturn tmp# left shift inverse
def inverse_left(res, shift, bits=32):tmp = resfor i in range(bits // shift):tmp = res ^ tmp << shiftreturn tmp# left shift with mask inverse
def inverse_left_mask(res, shift, mask, bits=32):tmp = resfor i in range(bits // shift):tmp = res ^ tmp << shift & maskreturn tmp
def recover(y):y = inverse_right(y,19)y = inverse_left_mask(y,17,2245263360)y = inverse_left_mask(y,9,2029229568)y = inverse_right(y,13)return y&0xffffffff
enc='641460a9e3953b1aaa21f3a2'
enc=long_to_bytes(int(enc,16))
#b'd\x14`\xa9\xe3\x95;\x1a\xaa!\xf3\xa2'
m=b''
# print(len(enc))12
for i in range(3):block = enc[i * 4: i * 4 + 4]block = bytes_to_long(block)block = recover(block)m += long_to_bytes(block)
print(m)
print(hex(bytes_to_long(m))[2:])
#84b45f89af22ce7e67275bdc
这里还有另外一种办法,就是通过不断加密最终又可以回到明文
因为本题使用的加密算法是梅森旋转算法,是一种伪随机数生成算法
该算法的一个更新的和更常用的是MT19937, 32位字长,对应了题目
并且该算法生成的随机数具有周期性,这也就不难理解为什么一直加密密文就能得到明文了,因为经过一个周期后得到的还是密文,那么上一个就是明文了
from Crypto.Random import random
from Crypto.Util import numberdef convert(m):m = m ^ m >> 13m = m ^ m << 9 & 2029229568m = m ^ m << 17 & 2245263360m = m ^ m >> 19return mdef transform(message):assert len(message) % 4 == 0new_message = b''for i in range(len(message) // 4):block = message[i * 4 : i * 4 +4]block = number.bytes_to_long(block)block = convert(block)block = number.long_to_bytes(block, 4)new_message += blockreturn new_message
def circle(m):t=mwhile True:x=tt=transform(t)if t==m:return x
transformed_flag='641460a9e3953b1aaa21f3a2'
print(bytes.fromhex(transformed_flag))
flag = hex(bytes_to_long(circle(bytes.fromhex(transformed_flag))))[2:]
print('transformed_flag:', flag)
咱们上面的是基于对extract_number函数的观察所得到的逆向方法,实际上我们还可以从extract_number的运算本质上进行逆向。(接下来的这种方法也是比较重要的,即通过构建矩阵进行求解)
假设 s t a t e [ i ] 的二进制表示形式为: x 0 x 1 x 2 . . . x 31 输出的随机数形式为: z 0 z 1 z 2 . . . z 31 根据 e x t r a c t _ n u m b e r 函数可以知道 z 和 x 是存在线性关系的(可以推出来的) 即 X ∗ T = Z X = Z ∗ T − 1 这就把问题转换成了矩阵的问题了 其中 X , Z 是 G F ( 2 ) 上的 1 ∗ 32 的向量, T 是 G F ( 2 ) 上的 32 ∗ 32 的矩阵 我们要在 G F ( 2 ) 上求解 X , 已知 Z , 如何求 T 呢? 可以令 X = ( 1 , 0 , 0 , . . . . , 0 ) , 这样 X ∗ T 得到的 Z 就是 T 的第一行了 实际计算中,采用黑盒测试的方法进行猜解 T 就是设置特定的状态,然后向后预测得到 Z , 这个 Z 就是我们 T 的某一行 ( 因为 X T = Z 实际体现的是随机数预测的线性关系) 假设state[i]的二进制表示形式为:\\x_0x_1x_2...x_{31}\\输出的随机数形式为:\\z_0z_1z_2...z_{31}\\根据extract\_number函数可以知道z和x是存在线性关系的(可以推出来的)\\即X*T=Z\\X=Z*T^{-1}\\这就把问题转换成了矩阵的问题了\\其中X,Z是GF(2)上的1*32的向量,T是GF(2)上的 32*32的矩阵\\我们要在GF(2)上求解X,已知Z,如何求T呢?\\可以令X=(1,0,0,....,0),这样X*T得到的Z就是T的第一行了\\实际计算中,采用黑盒测试的方法进行猜解T\\就是设置特定的状态,然后向后预测得到Z,这个Z就是我们T的某一行\\(因为XT=Z实际体现的是随机数预测的线性关系) 假设state[i]的二进制表示形式为:x0x1x2...x31输出的随机数形式为:z0z1z2...z31根据extract_number函数可以知道z和x是存在线性关系的(可以推出来的)即X∗T=ZX=Z∗T−1这就把问题转换成了矩阵的问题了其中X,Z是GF(2)上的1∗32的向量,T是GF(2)上的32∗32的矩阵我们要在GF(2)上求解X,已知Z,如何求T呢?可以令X=(1,0,0,....,0),这样X∗T得到的Z就是T的第一行了实际计算中,采用黑盒测试的方法进行猜解T就是设置特定的状态,然后向后预测得到Z,这个Z就是我们T的某一行(因为XT=Z实际体现的是随机数预测的线性关系)
线性关系:(根据函数一位一位的看,这里的异或其实和+类似)
代码:
# sagemath 9.0
from sage.all import *
from random import Randomdef buildT():rng = Random()T = matrix(GF(2),32,32)for i in range(32):s = [0]*624# 构造特殊的states[0] = 1<<(31-i)rng.setstate((3,tuple(s+[0]),None))tmp = rng.getrandbits(32)#往后预测数据,即得到Z# 获取T矩阵的每一行row = vector(GF(2),[int(x) for x in bin(tmp)[2:].zfill(32)])T[i] = row#获取T的第i行return Tdef reverse(T,leak):Z = vector(GF(2),[int(x) for x in bin(leak)[2:].zfill(32)])X = T.solve_left(Z)#X=Z*T^(-1)state = int(''.join([str(i) for i in X]),2)return statedef test():rng = Random()# 泄露信息leak = [rng.getrandbits(32) for i in range(32)]originState = [i for i in rng.getstate()[1][:32]]# 构造矩阵TT = buildT()recoverState = [reverse(T,i) for i in leak]print(recoverState==originState)
test()
题型2 预测随机数
就是题目给出一系列的随机数,然后根据这些随机数预测后面的随机数
这一类题型本质上就是题型一,因为也是基于对extract_number 函数的逆向而发展来的
直接看题吧
[GKCTF 2021]Random
import random
from hashlib import md5def get_mask():file = open("random.txt","w")for i in range(104):file.write(str(random.getrandbits(32))+"\n")file.write(str(random.getrandbits(64))+"\n")file.write(str(random.getrandbits(96))+"\n")file.close()
get_mask()
flag = md5(str(random.getrandbits(32)).encode()).hexdigest()
print(flag)
分析:
我们可以看出前面先生成了许多的随机数,然后flag是之后生成的随机数的md5加密值
那么我们就需要根据给的数据往后预测即可
那么就需要根据给的数据恢复出624个状态才行
插入一个小知识点:
import randomrandom.seed(1) print(hex(random.getrandbits(32))[2:])#getrandbits(a)是生成a位二进制的随机数 print(hex(random.getrandbits(32))[2:]) #2265b1f5 #91b7584arandom.seed(1) print(hex(random.getrandbits(64))[2:])#64位的是两个32位的拼接,但是是倒序拼接 #91b7584a2265b1f5
即getrandbits()里面的位数如果大于32则是由多个随机数拼接而成的,且是倒序的
当然小于32则是取前几位,但下一个随机数是从新的32位随机数上取
那么题目中getrandbits(64)就是由2个32位的随机数拼接而成,getrandbits(96)就是由3个32位的随机数拼接而成
总共有104*(1+2+3)=624个随机数,恢复一下状态state,即可往后预测
exp:
from hashlib import md5from Crypto.Util.number import *
from gmpy2 import *# -*- coding: utf-8 -*-from random import Randomdef invert_right(m,l,val=''):length = 32mx = 0xffffffffif val == '':val = mxi,res = 0,0while i*l<length:mask = (mx<<(length-l)&mx)>>i*ltmp = m & maskm = m^tmp>>l&valres += tmpi += 1return resdef invert_left(m,l,val):length = 32mx = 0xffffffffi,res = 0,0while i*l < length:mask = (mx>>(length-l)&mx)<<i*ltmp = m & maskm ^= tmp<<l&valres |= tmpi += 1return resdef invert_temper(m):m = invert_right(m,18)m = invert_left(m,15,4022730752)m = invert_left(m,7,2636928640)m = invert_right(m,11)return mdef clone_mt(record):state = [invert_temper(i) for i in record]gen = Random()gen.setstate((3,tuple(state+[0]),None))#设置state,方便后面的预测return genf = open("random.txt",'r').readlines()
prng = []
j=0
for i in f:i = i.strip('\n')if(j%3==0):#分割数字,我们要按照32的位数来prng.append(int(i))elif(j%3==1):#将生成两次随机数的两个随机数分离出来,倒序的prng.append(int(i)& (2 ** 32 - 1))prng.append(int(i)>> 32)else:#将生成三次随机数的三个随机数分离出来prng.append(int(i)& (2 ** 32 - 1))prng.append(int(i)& (2 ** 64 - 1) >> 32)prng.append(int(i)>>64)j+=1# print(prng)
# print(len(prng))624g = clone_mt(prng[:624])#回溯状态,即题型1类似
for i in range(624):g.getrandbits(32)flag = md5(str(g.getrandbits(32)).encode()).hexdigest()
print(flag)
还有一种办法,可以直接用现成的randcrack库
from hashlib import md5
from randcrack import RandCrackf = open("random.txt",'r').readlines()
prng = []
j=0
for i in f:i = i.strip('\n')if(j%3==0):#分割数字,我们要按照32的位数来prng.append(int(i))elif(j%3==1):#将生成两次随机数的两个随机数分离出来,倒序的prng.append(int(i)& (2 ** 32 - 1))prng.append(int(i)>> 32)else:#将生成三次随机数的三个随机数分离出来prng.append(int(i)& (2 ** 32 - 1))prng.append(int(i)& (2 ** 64 - 1) >> 32)prng.append(int(i)>>64)j+=1rc = RandCrack()
for i in prng:rc.submit(i)#传入624位的数据
flag = rc.predict_getrandbits(32) # 在给出的随机数数量多时,predict_getrandbits()可以预测下一个随机数
print('GKCTF{' + md5(str(flag).encode()).hexdigest() + '}')
题型3逆向twist
前面我们是根据已知连续624个随机数恢复624个状态state,往后预测随机数
那么如何往前预测随机数呢?
因为每过一轮624个数据之后,都会用twist()函数进行数据更新
所以我们可以通过逆向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
考虑下面的例子:
1. 11100110110101000100101111000001 // state[i]
2. 10101110111101011001001001011111 // state[i + 1]
3. 11101010010001001010000001001001 // state[i + 397]// y = state[i] & 0x80000000 | state[i + 1] & 0x7fffffff
// |和+的效果是类似的,因为最后都是32位的数据
4. 10101110111101011001001001011111 // y
5. 01010111011110101100100100101111 // next = y >> 1
6. 11001110011100100111100111110000 // next ^= 0x9908b0df
0x9908b0df => 10011001000010001011000011011111
7. 00100100001101101101100110111001 // next ^= state[i + 397]
因为生成新的state[i]只与原来的state[i],state[i+1],state[i+397]有关。
而第7步是必须进行的一步(注意异或的次序是不影响结果的,所以异或state[i+397]可以看成最后一步)
第6步需要根据第4步结果的奇偶性来确定,即不一定有第6步
这里有一个很巧妙的点:因为把第7步放在最后,所以第7步是由第5步或者第6步的结果和state[i+397异或]得到的,看到第5步,因为y>>1,那么得到的next的高位一定是0,如果进行了第6步,即和0x9908b0df 异或
0x9908b0df => 10011001000010001011000011011111
得到的结果next的最高位通过0^1=1,所以我们可以通过第7步逆向得到的结果的高位是1还是0来区分是否进行第6步,进而可以往前推,因为第5步的next的后31位包含了state[i]的首位和state[i+1]的第2位到第31位,所以根据是否进行第6步可以知道y的最后一位是1还是0,即得到state[i+1]的第32位
综上所述:根据当前的state[i]和之前的state[i+397],我们可以得到原来的state[i]的第1位和state[i+1]的低31位,那么要获得原来state[i]的低31位则需要对state[i-1]进行同样的操作,依次循环下去即可
实现代码:
因为twist()函数生成新的state时是从0-623的,所以到生成state[227]时,最后的异或state[(i+397)%624]=state[0],这个时候异或的state[0]已经是新的state了,即state[227]之后异或的state[i+397]都是新的state了(即下一轮的state)
所以我们可以根据下一轮的state反推前一轮的state的
def backtrace(cur):high = 0x80000000low = 0x7fffffffmask = 0x9908b0dfstate = curfor i in range(623,-1,-1):tmp = state[i]^state[(i+397)%624]# recover Y,tmp = Yif tmp & high == high:tmp ^= masktmp <<= 1tmp |= 1else:tmp <<=1# recover highest bitres = tmp&high# recover other 31 bits,when i =0,it just use the method again it so beautiful!!!!tmp = state[i-1]^state[(i+396)%624]# recover Y,tmp = Yif tmp & high == high:tmp ^= masktmp <<= 1tmp |= 1else:tmp <<=1res |= (tmp)&lowstate[i] = res return state
那来道题目看看
[V&N2020 公开赛]Backtrace
# !/usr/bin/env/python3
import randomflag = "flag{" + ''.join(str(random.getrandbits(32)) for _ in range(4)) + "}"with open('output.txt', 'w') as f:for i in range(1000):f.write(str(random.getrandbits(32)) + "\n")print(flag)
分析:
给的数据是在flag的随机数之后的随机数,也就是说我们需要恢复前4个数据,一般来说我们是根据连续的624个随机数才能够得到上一组随机数的状态,虽然题目给了我们1000个随机数,但是前4个丢失
(这里是将1000个数据分为了两部分,一部分带着未知的4个数据-620个,一部分则是下一轮的数据-380个)
但是我们知道前4个状态经过twist()函数扭转后对应的是第625,626,627,628位随机数的状态,根据我们上面的推导是可逆的(直接逆回去即可),所以即便没有连续的624个随机数,也可以求解出完整的state
其实就是根据数据得到状态,再根据上面的推导反推前4个state就行
exp:
# -*- coding: utf-8 -*-
import randomdef invert_right(m,l,val=''):length = 32mx = 0xffffffffif val == '':val = mxi,res = 0,0while i*l<length:mask = (mx<<(length-l)&mx)>>i*ltmp = m & maskm = m^tmp>>l&valres += tmpi += 1return resdef invert_left(m,l,val):length = 32mx = 0xffffffffi,res = 0,0while i*l < length:mask = (mx>>(length-l)&mx)<<i*ltmp = m & maskm ^= tmp<<l&valres |= tmpi += 1return resdef invert_temper(m):m = invert_right(m,18)m = invert_left(m,15,4022730752)m = invert_left(m,7,2636928640)m = invert_right(m,11)return m#往前回溯
def backtrace(cur):high = 0x80000000low = 0x7fffffffmask = 0x9908b0dfstate = curfor i in range(3,-1,-1):tmp = state[i+624]^state[(i+397)%624]#根据新的结果推前一个state的结果# recover Y,tmp = Yif tmp & high == high:tmp ^= masktmp <<= 1tmp |= 1else:tmp <<=1# recover highest bitres = tmp&high# recover other 31 bits,when i =0,it just use the method again it so beautiful!!!!tmp = state[i-1+624]^state[(i+396)%624]# recover Y,tmp = Yif tmp & high == high:tmp ^= masktmp <<= 1tmp |= 1else:tmp <<=1res |= (tmp)&lowstate[i] = resreturn statef = open("output.txt",'r').readlines()
prng = []
for i in f:i = i.strip('\n')prng.append(int(i))state=[]
for i in range(1000):state.append(invert_temper(prng[i]))
state2=backtrace([0]*4+state)[:624]#[:624]取backtrace返回结果的前624个即可random.setstate((3,tuple(state2+[0]),None))
flag = "flag{" + ''.join(str(random.getrandbits(32)) for _ in range(4)) + "}"
print(flag)
题型4 逆向init
这种类型的题还没有出现过,但是也可以进行逆向
我们前面有通过输出的数据逆向对于的state,逆向上一组的state
这部分则是根据第一次的state,逆向求出seed
先来看init这个函数吧:
def _int32(x):#保证32位return int(0xFFFFFFFF & x)def __init__(self, seed):self.mt = [0] * 624self.mt[0] = seed#这里的mt[]数组就是我们的状态stateself.mti = 0#类似于一个计数器for i in range(1, 624):self.mt[i] = _int32(1812433253 * (self.mt[i - 1] ^ self.mt[i - 1] >> 30) + i)
看到关键代码:
self.mt[i] = _int32(1812433253 * (self.mt[i - 1] ^ self.mt[i - 1] >> 30) + i)
可以发现self.mt[i - 1] ^ self.mt[i - 1] >> 30是可逆的,类似于我们题型一,位运算和异或运算
注意我们的_int32()只是为了保证位数,相当于mod 2^32 ,所以我们完全可以通过求逆元得到self.mt[i - 1] ^ self.mt[i - 1] >> 30的值
其中gcd(1812433253,2^32)=1,即1812433253是存在逆元的
代码如下:
from gmpy2 import invertdef _int32(x):return int(0xFFFFFFFF & x)def init(seed):mt = [0] * 624mt[0] = seedfor i in range(1, 624):mt[i] = _int32(1812433253 * (mt[i - 1] ^ mt[i - 1] >> 30) + i)return mtseed = 2080737669def invert_right(res,shift):tmp = resfor i in range(32//shift):res = tmp^res>>shiftreturn _int32(res)def recover(last):n = 1<<32inv = invert(1812433253,n)#求逆元for i in range(623,0,-1):last = ((last-i)*inv)%nlast = invert_right(last,30)return laststate = init(seed)print(recover(state[-1]) == seed)
扩展题型
这个其实就是题型1中后面介绍的黑盒测试方法,即通过构建矩阵得到state,这个方法在一些特殊情况下是需要使用到的
构建矩阵的脚本:
#! /bin/bash/env python3
from sage.all import *
from random import Random
from tqdm import tqdm
prng = Random()
length = 19968
def myState():state = [0]*624i = 0while i<length:ind = i//32expont = i%32state[ind] = 1<<(31-expont)s = (3,tuple(state+[0]),None)yield sstate[ind] = 0i += 1def getRow():rng = Random()gs = myState()for i in range(length):s = next(gs)rng.setstate(s)
# print(s[1][0])row = vector(GF(2),[rng.getrandbits(1) for j in range(length)])yield rowdef buildBox():b = matrix(GF(2),length,length)rg = getRow()for i in tqdm(range(length)):b[i] = next(rg)return bdef test():prng = Random()originState = prng.getstate()# 这里都是用的MSB,如果采用不同的二进制位(如LSB)最后的矩阵T 也会不同leak = vector(GF(2),[prng.getrandbits(1) for i in range(length)])b = buildBox()f = open("Matrix","w")for i in range(b.nrows()):for j in range(b.ncols()):f.write(str(b[i,j])+"n")f.close()x = b.solve_left(leak)x = ''.join([str(i) for i in x])state = []for i in range(624):tmp = int(x[i*32:(i+1)*32],2)state.append(tmp)prng.setstate(originState)prng.getrandbits(1)originState = [x for x in prng.getstate()[1][:-1]]print(originState[1:] == state[1:])
# print(state)return state,b
test()
来看看下面这道题,能够理解那么MT19937也就差不多了
WKCTF easy_random
import random
from Crypto.Cipher import AES
from Crypto.Util.Padding import padflag = b'WKCTF{}'
pad_flag = pad(flag,16)#填充
key = random.randbytes(16)
cipher = AES.new(key,AES.MODE_ECB)#AES加密
print(cipher.encrypt(pad_flag))
# b'a\x93\xdc\xc3\x90\x0cK\xfa\xfb\x1c\x05$y\x16:\xfc\xf3+\xf8+%\xfe\xf9\x86\xa3\x17i+ab\xca\xb6\xcd\r\xa5\x94\xeaVM\xdeo\xa7\xdf\xa9D\n\x02\xa3'
with open('random.txt','w') as f:for i in range(2496):f.write(str(random.getrandbits(8))+'\n')
这个看我之前的wp吧,写的很详细了
WKCTFwp
现成模块
此外,介绍几个现成的模块,即可以直接用的,很方便
randcrack库
这个是官方的解释:https://pypi.org/project/randcrack/
import random, time
from randcrack import RandCrackrandom.seed(time.time())rc = RandCrack()for i in range(624):rc.submit(random.getrandbits(32))# Could be filled with random.randint(0,4294967294) or random.randrange(0,4294967294)print("Random result: {}\nCracker result: {}".format(random.randrange(0, 4294967295), rc.predict_randrange(0, 4294967295)))
大致来说:我们需要给出624个32位由random模块产生的随机数(即设置状态),然后就可以使用predict进行预测,这样我们就可以省去了根据数据恢复state的步骤了,其他函数就自己去探索吧(因为本人也没咋遇到过了)
import random
from randcrack import RandCrackrc=RandCrack()#实例化randcrack类
for i in range(624):rc.submit(random.getrandbits(32))#将624个随机数传入,即设置状态
print(random.getrandbits(64))
print(rc.predict_getrandbits(64))#根据前面给的状态往后预测
#10211364013887435935
#10211364013887435935
注意,使用这个库存在一些限制:
1.传入312个64位的数据
import random
from randcrack import RandCrackrc = RandCrack()
for i in range(312):rc.submit(random.getrandbits(64))
print(random.getrandbits(32))
print(rc.predict_getrandbits(32))
#报错了,ValueError: Didn't recieve enough bits to predict
#没有获取足够的位数去预测
2.那么试试传入624个64位的数据
import random
from randcrack import RandCrackrc = RandCrack()
for i in range(624):rc.submit(random.getrandbits(64))
print(random.getrandbits(32))
print(rc.predict_getrandbits(32))
# 888233832
# 0
#预测结果错误
3.试试传入624个16位的数据
import random
from randcrack import RandCrackrc = RandCrack()
for i in range(624):rc.submit(random.getrandbits(16))
print(random.getrandbits(32))
print(rc.predict_getrandbits(32))
# 2554149584
# 3275678419
#预测结果错误
也就是说,randcrack只能提交624次,且必须是32位数,位数多了少了都不行,这样预测出的结果才是正确的
可以看出randcrack库还是比较局限的
Extend MT19937 Predictor库
这个库相对于randcrack就要灵活很多了
官方解释:extend-mt19937-predictor · PyPI
一样,我们需要先传入624个32位的数据,即设置状态state:
ExtendMT19937Predictor().setrandbits(random.getrandbits(bits), bits)
#bits表示需要生成数据的位数
这个库可以传入不是32的位数,只要最后传入了不少于624个32位的数据即可
具体使用:
- 传入数据之后,可以使用predict_getrandbits()函数往后预测数据
import random
from extend_mt19937_predictor import ExtendMT19937Predictorpredictor = ExtendMT19937Predictor()for _ in range(624):predictor.setrandbits(random.getrandbits(32), 32)for _ in range(1024):assert predictor.predict_getrandbits(32) == random.getrandbits(32)assert predictor.predict_getrandbits(64) == random.getrandbits(64)assert predictor.predict_getrandbits(128) == random.getrandbits(128)assert predictor.predict_getrandbits(256) == random.getrandbits(256)
- backtrack_getrandbits() 实现往前预测
import random
from extend_mt19937_predictor import ExtendMT19937Predictornumbers = [random.getrandbits(64) for _ in range(1024)]predictor = ExtendMT19937Predictor()for _ in range(78):#78*(256/32)=624predictor.setrandbits(random.getrandbits(256), 256)
#先往前预测回退到之前的状态state
_ = [predictor.backtrack_getrandbits(256) for _ in range(78)]for x in numbers[::-1]:#往前预测assert x == predictor.backtrack_getrandbits(64)
参考博客:
大部分参考badmonkey的文章,大佬膜拜了:浅析MT19937伪随机数生成算法-安全客 - 安全资讯平台 (anquanke.com)
[CTF/randcrack]python随机数预测模块分析及改进方案