Python 随机数的背后:MT19937 算法之 —— 小试牛刀
本文为几道 MT19937 预测题的题解。这些题都非常基础 + 典型,十分适合入门。
本文用到的 mt19937
来自此 gist。
第一题
#!/usr/bin/env python3
import random
for _ in range(624):
print(random.getrandbits(32))
if input() == str(random.random())
print(open("flag").read())
非常简单的预测,给了连续 624 个 32bit 随机数,只需把它们依次输入预测器,就能恢复出完整的内部状态。
import tqdm
from mt19937 import MT19937Predictor
from pwn import remote
r = remote(HOST, PORT)
predictor = MT19937Predictor()
for _ in tqdm.tqdm(range(624)):
data = int(r.recvline().decode())
predictor.setrand_int32(data)
r.sendline(str(predictor.random()).encode())
print(r.recv().decode())
第二题
#!/usr/bin/env python3
import random
number = random.getrandbits(32)
for _ in range(624):
print(random.getrandbits(32))
if input() == str(number):
print(open("flag").read())
同样先通过连续的 624 个 32bit 随机数恢复出内部状态,然后我们往回倒 625 次迭代,即可恢复出最前面的那个随机数产生之前的状态。
import tqdm
from pwn import remote
from mt19937 import MT19937Predictor
r = remote(HOST, PORT)
predictor = MT19937Predictor()
for _ in tqdm.tqdm(range(624)):
data = int(r.recvline().decode())
predictor.setrand_int32(data)
for i in range(625):
predictor.unextract_number()
ans = predictor.getrandbits(32)
r.sendline(str(ans).encode())
print(r.recv().decode())
第三题
#!/usr/bin/env python3
import random
for _ in range(19938):
print(random.getrandbits(1))
if input() == str(random.getrandbits(64)):
print(open("flag").read())
这一题就稍微难一些了,虽然看上去是连续生成了 19938 个 bit,但因为这个算法最小的生成单位是 32bit,每次调用 getrandbits(1)
时,其实内部先会生成一个 32bit 的随机数,然后取其最高位返回。不过经过前面的一些分析,我们很容易发现一个关键点:即 MT19937 算法每次迭代产生的随机数的每一个 bit,其实都是它内部状态某些 bit 的异或得来的,这说明取出随机数的操作是一个 z3
,我也找到了一个利用 z3
来恢复 MT19937 内部状态的项目:SymRandCracker
可惜,这道题的已知 bit 过于分散,每 32 个 bit 才知道其中一个的值,如果全输入符号求解器,复杂度会拉满,不过好在我们知道每次给的 bit 的位置(MSB)。
我们设初始状态下 random
的状态向量为
不妨大胆一点,假设从内部状态
答案是肯定的。
并且我们还能在本地预生成好这个矩阵,然后从服务器拿到 19938 个 MSB(
这里使用 Sage 编写代码:
import random
import tqdm
import os
length = 624 * 32
def generate_state():
state = [int(0)]*624
i = 0
while i<length:
idx = i//32
expont = i%32
state[idx] = int(1<<(31-expont))
s = (3,tuple(state+[int(624)]),None)
yield s
state[idx] = int(0)
i += 1
def get_row():
gs = generate_state()
for i in range(length):
s = next(gs)
random.setstate(s)
row = vector(GF(2), [random.getrandbits(1) for j in range(length)])
yield row
def build_matrix():
b = matrix(GF(2),length,length)
rg = get_row()
for i in tqdm.tqdm(range(length)):
b[i] = next(rg)
return b
if not os.path.exists('Matrix.sobj'):
b = build_matrix()
b.save("Matrix.sobj")
思路是遍历所有仅一个 bit 为 1,其他 bit 为 0 的状态(共 19968 个),每次将此状态赋值给随机数发生器,然后让它根据此状态连续生成 19968 个 32bit,我们每 32 个 bit 取出其 MSB,组合为矩阵的一行。将每个状态对应的行拼起来,组合得到一个
求解时由于我们只有 19938 个输入,就将矩阵进行一个截断:
T = load('Matrix')
T = T[:, :19938]
读入服务器发来的数据,然后调用 Sage 的 solve_left
,解一下方程即可:
from pwn import remote
r = remote(HOST, PORT)
leak = [int(r.recvline().strip().decode()) for i in tqdm.tqdm(range(19938))]
leak = vector(GF(2), leak)
x = T.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)
random.setstate((3, tuple(state + [624]), None))
for i in range(19938):
random.getrandbits(1)
r.sendline(str(random.getrandbits(64)).encode())
r.recv()
下一篇文章中笔者将结合实际案例,带来一道实战题的题解。