梅森旋转生成随机数

梅森旋转生成随机数的逆向预测和恢复

  • 预测下一个随机数,恢复被隐藏的随机数据

首发于先知社区 :子集和问题的两种解决方式-先知社区

算法过程

  • wiki上有算法实现的源代码
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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,x=None):
        if self.mti == 0 and x is None:
            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

根据代码分析过程。因为社区已经有人讲过很详细的过程,我们这里就只说一下大致分为三个过程

  • 初始化

根据种子seed,生成初始状态mt(共有624个数据。然后mti==0.

  • 处理mt生成随机数

对mt进行一系列线性操作然后生成随机数。

  • 进行旋转

当已经生成了624个随机数时,相当于耗光了一轮的状态。就需要利用旋转生成新一轮数据,观察这个twist代码,我们可以利用这个预测624位之后出现的下一轮的mt状态然后预测下一个随机数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
M1=[]
M2=[]
rng=MT19937(seed=1123)
for i in range(1248):
    a=rng.extract_number()
    if i<624:
        M1.append(a)
    else:
        M2.append(a)
print(M1)
print(M2)

如图可以调用函数生成随机数我们可以利用M1预测M2的数据。

我们可以用RandCrack库预测生成的随机数和生成之前生成的随机数,也可以自己写一个预测的代码。

  • 库函数
1
2
3
4
5
rc=RandCrack()
for i in M1:
    rc.submit(i)
next=rc.predict_randrange(0, 0xFFFFFFFF)#(这里确定输出的起始和末尾bit,,这里在后面讲解为何这样)
print(next)
  • 逆向函数进行预测
  • 逆向extract_number()函数

​ y = y ^ y » 11 ​ y = y ^ y « 7 & 2636928640 ​ y = y ^ y « 15 & 4022730752 ​ y = y ^ y » 18

这是这个函数最关键的部分,我们在一轮中可以利用生成一轮的随机数恢复mt,然后预测下一轮的mt恢复数据,从下往上。

我们把操作后的y写成bit形式y=$y_1..y_{32}$ |同时记操作前$x=x_1..x_{32}$。。。

$$y=x\oplus x>>n$$

n>=32-n

那么有$y_1..y_{32}=x_1..x_{32}\oplus 0..0x_1..x_{32-n}$

那么$y_1..y_n=x_1..x_n$ $x_{n+1}..x_{32}\oplus x_1..x_{32-n}=y_{n+1}..y_{32}$

可以得到$x_{n+1}..x_{32}=y_{n+1}..y_{32}\oplus y_1..y_{32-n}$ 因为n>32-n,所以$y_1..y_{32-n}$是已知的

所以$x=y»n$

同理,当n<32-n ,情况有所转变

最后这里无法$y_{n+1}..y_{32}\oplus y_1..y_{32-n}$ 无法一次性完全恢复多轮几次就好了。

我们可以$x_{n+1}..x_{2n+1}=y_{n+1}..y_{32}\oplus y_1..y_{n+1}$

我们多进行几次这样的变化就行了。

逆向代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
    def re_right(self,x,bit,mask=0xffffffff):
        tmp=x
        for _ in range(32//bit):
            tmp=x^tmp>>bit&mask
        return tmp
   def re_left(self,x,bit,mask=0xffffffff):
        tmp=x
        for _ in range(32//bit):
            tmp=x^tmp<<bit&mask
        return tmp

这里如果我们只进行正向的预测,就暂时不用反向写一个twist代码了

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
    def re_extract(self,m):
        m=self.re_right(m,18,0xffffffff)
        m=self.re_left(m,15,4022730752)
        m=self.re_left(m,7,2636928640)
        m=self.re_right(m,11)
        return m&0xffffffff
    def re_state(self,outputs):
        if len(outputs)!=624:
            raise ValueError("Invalid number of outputs")

        self.mt=[self.re_extract(m)for m in outputs]
        self.mti=0
        return self.mt
M1=[]
M2=[]
rng=MT19937(seed=1123)
for i in range(1248):
    a=rng.extract_number()
    if i<624:
        M1.append(a)
    else:
        M2.append(a)
print(M1)
print(M2)
pre=MT19937()
pre.re_state(M1)
print("预测 RNG:", [pre.extract_number() for _ in range(10)])

结合上述代码可以很好的预测下一个。不过我们要如果进行恢复前面的数和更精简的操作。便需要逆向twist

1
2
3
4
5
6
7
 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
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
    def re_twist(self,mt):
        re_tw=[0]*624#生成列表
        a=deepcopy(mt[623])
        c=deepcopy(mt[396])
        for i in range(623,-1,-1):#从大到小遍历,以便twist[(i+397)%624]是符合条件的
            k=mt[i]^mt[(i+397) % 624]
            if (k&0x80000000)>>31==1:#判断y>>1的第一位
                k=k^0x9908b0df
                low=1
                high=(k&0x40000000)>>30
                re_tw[i]=high<<31
                re_tw[(i+1)%624]=re_tw[(i+1)%624]+((k&0x3fffffff)<<1)+low
                if i !=623:
                    mt[(i+1)%624]=re_tw[(i+1)%624]#还原正确的依赖
            elif (k & 0x80000000) >> 31 == 0:
                low = 0
                high = (k & 0x40000000) >> 30
                re_tw[i] = high << 31
                re_tw[(i + 1) % 624] = re_tw[(i + 1) % 624] + ((k & 0x3fffffff) << 1) + low
                if i != 623:
                    mt[(i + 1) % 624] = re_tw[(i + 1) % 624]

        return re_tw

这个反转函数,可读性不太好,我们从大到小遍历。

先将异或mt[(i+397) % 624] (从大到小的很明显的是能够直接利用这个反解,并且每一次循环后都会更改依赖)

不过这关有个缺陷就是不能还原出mt[0],

因为最后一个计算mt[0]的时候会自动把re_tw[0]变成re_tw[i]=high«31。

我恢复出来结果恢复出了它下一个的mt1[0]所以最后我换了一个方法。

比如我们要求mt[i],已知它旋转后mt1[]

我们利用mt1[i]恢复出mt[i]的最高的1位,然后再利用mt1[i-1]恢复出第的31位

具体恢复过程

1
2
0x7fffffff=0b01111111111111111111111111111111
0x80000000=0b10000000000000000000000000000000

如果这种操作应用给一个规定的32bit的数那么

1
y = _int32((self.mt[i] & 0x80000000) + (self.mt[(i + 1) % 624] & 0x7fffffff))

在y等于mt[i]第32Bit 和.mt[(i + 1) % 624]的后31bit之和

由于都是线性操作,我们先

k=mt[i]^mt[(i+397) % 624],

  • 如果k与0x9908b0df异或过,那么由于(y«1)^0x9908b0df=k,

k«31==1。

  • 如果k没与0x9908b0df异或

k«31==0

便能通过y得到我们想要的信息。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def re_tw1(mt):		
    high=0x80000000
    low=0x7fffffff
    mask=0x9908b0df
    for i in range(623,-1,-1):
        t=mt[i]^mt[(i+397) % 624]
        if t&high==high:
            t=t^mask
            t=t<<1
            t|=1#确定位奇数
        else:
            t=t<<1
        res=t&high #取得高位
        t=mt[i-1]^mt[(i+396) % 624]
        if t&high==high:
            t=t^mask
            t=t<<1
            t|=1
        else:
            t=t<<1
        res=res+(t&low)
        mt[i]=res
    return mt

验证代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
if __name__ == '__main__':
    M1 = []
    M2 = []
    rng = MT19937(seed=1123)
    for i in range(1248):
        a = rng.extract_number()
        if i < 624:
            M1.append(a)
        else:
            M2.append(a)
    print(f'M1={M1}')
    print(f'M2={M2}')
    pre = MT19937()
    m = pre.re_state(M2)
    pre.mt=re_tw1(m)
    print(pre.mt)
    print("预测 RNG:", [pre.extract_number(x=1) for _ in range(10)])

可以跑一下是一样的

这里的x=1是控制是否旋转的,具体翻阅上述源代码。

[TM19973/MT.py at main · rockfox0/TM19973]—这是MT19973的源码和逆向的代码的github链接

如果是不连续的输出信息

在一些比较难的梅森旋转的题目中,我们得到的随机数是间断,来自于不同的bit值。在正常情况下,我们如果知道了连续的19937个bit的信息(也就是一轮),我们恢复之前的和预测之后的是很轻易的。

因为梅森旋转是线性操作,我们可以利用线性代数的知识来做这个。

假设已知的数据是b,b可以转化为一个向量。y表示初始的state。y经过M这个矩阵的线性运算可以得到b。

$ yM=b—y=b(M)^{-1}$

我们只需要构造出M表示线性运算的过程。

举个例子。。在python 代码

1
 a=getrandombits(t)

如果t<32,就会先生成正常的一组32bit数,然后把32bit右移16bit。就能得到16bit以内的a。如果我们得到的是一些t=20bit的数据想要恢复它,显然用前面直接逆向的方式是不太可行的。我们这是就可以构造矩阵M,利用线性代数的运算表示线性的运算过程。

  • 我们要先自定义一个随机数生成器的函数,来表示我们生成随机数的方式。
1
2
3
4
5
def getRows(rng):
    row=[]
    for i in range(n):
        row+=list(map(int, (bin(rng.getrandbits(20))[2:].zfill(20))))
    return row

这里表示就是生成20bit内的随机数然后累加1000个到row上得到的row就是一个有20000个0/1数据的列表

其实我们要求19968Bit的已知数据就够了,不过这里多点也行。

  • 然后就是构建M线性运算的矩阵,M将初始矩阵变成
1
2
3
4
5
6
7
8
for i in tqdm(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)) 
    M.append(getRows(rng))
M=Matrix(GF(2),M)

这个生成M线性运算的矩阵代码是不变的可以直接。无论生成随机数是隐藏了哪些部分,这些代码都是不变的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
from Crypto.Util.number import *
from random import *
from tqdm import *
n=1000
D=[]#1000个得到20
rng=Random()

def getRows(rng):
    row=[]
    for i in range(n):
        row+=list(map(int, (bin(rng.getrandbits(20))[2:].zfill(20))))
    return row
M=[]

for i in tqdm(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)) 
    M.append(getRows(rng))
M=Matrix(GF(2),M)
print("行数:", M.nrows())
print("列数:", M.ncols())
r=[]
for i in range(n):
    r+=list(map(int, (bin(D[i])[2:].zfill(20))))
r=vector(GF(2),r)
y=M.solve_left(r)
G=[]
for i in range(624):
    C=0
    for j in range(32):
        C<<=1
        C|=int(y[32*i+j])
    G.append(C)#将恢复的state转化为每个元素32bit的列表
import random
RNG1 = random.Random()
for i in range(624):
    G[i]=int(G[i])
RNG1.setstate((int(3),tuple(G+[int(624)]),None))#将state作为随机数生成器的初始状态,测试生成的state是否正确
P=[RNG1.getrandbits(20) for _ in range(75)]
print(P)
print(D[:75])

知道这两个就比较容易了剩下可以看注释

  • ps:

多数人的sagemath应该是装在wsl等其他虚拟环境平台上的,因为这个题目比较消耗内存,wsl的内存默认只给6.5G。这个已知20bit的还好,如果未知bit太多消耗内存肯定是不够的。建议把wsl内存上限拉到16G。网上有很多更改教程这里就不说了

tpctf 2025

接下来可以看看一道tp的ctf题目

1
2
3
4
5
6
7
import random
with open("flag.txt","rb") as f:
    flag=f.read()
for i in range(2**64):
    print(random.getrandbits(32)+flag[random.getrandbits(32)%len(flag)])
    input()
    

这里把最后8bit加上了另一个随机数进行了混淆,不过经过测试,加法可能会影响20bit以上甚至更高。不过在10000bit数据以内大多出现20bit以上数据概率比较小。我们这里设置混淆的是末尾24bit并且是间隔获得信息,获取第一个信息后,后一个随机数就被混淆掉了。我们这里可以获得2700组随机的数据

构建恢复state的代码如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
from Crypto.Util.number import *
from random import *
from tqdm import *
n=2500
D=[]
print(len(D))
rng=Random()

def getRows(rng):
    row=[]
    for i in range(n):
        row+=list(map(int, (bin(rng.getrandbits(32)>>24)[2:].zfill(8))))
        _=rng.getrandbits(32)
    return row
M=[]

for i in tqdm(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)) 
    M.append(getRows(rng))
M=Matrix(GF(2),M)
print("行数:", M.nrows())
print("列数:", M.ncols())
r=[]
for i in range(n):
    r+=list(map(int, (bin(D[i]>>24)[2:].zfill(8))))
r=vector(GF(2),r)
y=M.solve_left(r)
G=[]
for i in range(624):
    C=0
    for j in range(32):
        C<<=1
        C|=int(y[32*i+j])
    G.append(C)#将恢复的state转化为每个元素32bit的列表
import random
RNG1 = random.Random()
for i in range(624):
    G[i]=int(G[i])
RNG1.setstate((int(3),tuple(G+[int(624)]),None))#将state作为随机数生成器的初始状态,测试生成的state是否正确

恢复出状态后我们可以得到未被混淆的随机数。我们先可以利用爆破求出flag的长度和flag

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
G=[]
for i in range(624):
    G[i]=int(G[i])
RNG1 = Random()
RNG1.setstate((int(3),tuple(G+[int(624)]),None))

flag=[]
ind=[]
for i in range(len(X)):
    a = RNG1.getrandbits(32)
    b = RNG1.getrandbits(32)
    f = X[i]-a
    if 0 < f < 255 :
        flag.append(f)
        ind.append(b)
for fl in range(len(set(flag)),50):#fl是flag的长度,利用爆破判断长度是否符合条件
    TN = [i%fl for i in ind]
    TF = []
    for i in range(fl):
        t = TN.index(i)
        TF.append(flag[t])
    TF =  bytes(TF)
    if TF[:6]==(b'TPCTF{'):
       print(TF)
Licensed under CC BY-NC-SA 4.0
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计