MT19937在连续输出存在截断的情况下利用z3符号执行推导内部状态以及等价种子

本文主要是介绍MT19937在连续输出存在截断的情况下利用z3符号执行推导内部状态以及等价种子,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

传统针对MT19937的逆向往往需要得到连续且完整的624个随机数,从而逆向出所有内部状态。但在实际场景中,想得到连续且完整的624个随机数是比较困难的,大部分情况下得到的是一些截断的数(例如得到连续的624个数,每个数是原始随机输出中的前16比特)。这种情况下,我们可以利用z3的符号执行推导部分内部状态,甚至可以推导一个“部分等价”的初始种子。本文针对python的random底层c库实现做了详细的分析,并给出了所有中间代码。

关键词:

  • MT19937逆向
  • MT19937基于z3的内部状态求解
  • 复原部分历史随机数生成
  • 从伪随机数的输出构造等价种子

文章目录

  • 1.MT19937及其逆向
    • 1.1 梅森旋转
    • 1.2 MT19937
    • 1.3 MT19937的逆向
    • 4.基于z3的MT19937的内部状态求解
  • 2.连续输出存在截断的情况下z3的求解部分内部状态
    • 2.1 原理
    • 2.2 应用一:完全推导内部状态
    • 2.3 应用二:部分推导,复原部分历史随机数生成
  • 3.生成等价种子
  • 4.完整代码及工具封装
  • 参考


1.MT19937及其逆向

1.1 梅森旋转

先给出维基百科的解释[1]:
在这里插入图片描述
为什么叫梅森旋转?

  • 算法实现的过程中,参数的选取取决于梅森素数
  • 整个算法的核心操作,是针对状态的旋转

1.2 MT19937

最为广泛使用Mersenne Twister的一种变体是MT19937,可以产生32位整数序列,其周期非常长,达到 2 19937 − 1 2^{19937}-1 2199371
有关MT19937的所有细节请参考原文:http://www.math.sci.hiroshima-u.ac.jp/m-mat/MT/ARTICLES/mt.pdf

简要实现如下:

def _int32(x):return int(0xFFFFFFFF & x)class MT19937:# 依据seed初始化内部状态(共624个state)def __init__(self, seed):self.mt = [0] * 624self.mt[0] = seedself.mti = 0for 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 >> 11y = y ^ y << 7 & 2636928640y = y ^ y << 15 & 4022730752y = y ^ y >> 18self.mti = (self.mti + 1) % 624return _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

1.3 MT19937的逆向

从上述MT19937的源码中,不难看出,其两个关键的函数都是可逆的。

extract_number四个关键操作的逆向函数:

def inverse_right(res, shift, bits=32):tmp = resfor i in range(bits // shift):tmp = res ^ tmp >> shiftreturn tmpdef inverse_left(res, shift, bits=32):tmp = resfor i in range(bits // shift):tmp = res ^ tmp << shiftdef inverse_right_mask(res, shift, mask, bits=32):tmp = resfor i in range(bits // shift):tmp = res ^ tmp >> shift & maskreturn tmpdef inverse_left_mask(res, shift, mask, bits=32):tmp = resfor i in range(bits // shift):tmp = res ^ tmp << shift & maskreturn tmp

从而可以根据一个随机数直接逆向出一个状态:

def get_one_state(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 _int32(y)

如果能得到连续且完整的624个随机数,即可以恢复出MT19937内部的所有状态,从而能克隆出一个完全一致的随机函数。关于这一逆向,已经有较成熟的开源实现randcrack:https://github.com/tna0y/Python-random-module-cracker

randcrack的代码看起来还是很优雅的,但我对这个模块设计有点奇怪的是,它竟然没有提供一个克隆的random,虽然很简单。所以一般这样使用更舒适:

from randcrack import RandCrack as _RandCrack
from random import Randomclass RandCrack(_RandCrack):def __init__(self):super().__init__()def get_random(self):state = []for i in self.mt[:624]:b = ''for j in i:b += str(j)state.append(int(b, 2))rand = Random()rand.setstate((3, tuple(state+[0]), None))return randrc = RandCrack()
r = Random()for i in range(624):rc.submit(r.getrandbits(32))my_rand = rc.get_random()for i in range(1000):assert r.getrandbits(32) == my_rand.getrandbits(32), f"index: {i}"

4.基于z3的MT19937的内部状态求解

在不逆向算法的前提下,也可以利用z3直接进行求解:

from z3 import *
def solve_one_state(out):y1 = BitVec('y1', 32)y2 = BitVec('y2', 32)y3 = BitVec('y3', 32)y4 = BitVec('y4', 32)y = BitVecVal(out, 32)s = Solver()equations = [y2 == y1 ^ (LShR(y1, 11)),y3 == y2 ^ ((y2 << 7) & 0x9D2C5680),y4 == y3 ^ ((y3 << 15) & 0xEFC60000),y == y4 ^ (LShR(y4, 18))]s.add(equations)s.check()return s.model()[y1].as_long()

2.连续输出存在截断的情况下z3的求解部分内部状态

2.1 原理

假设连续输出若干个伪随机数 p 1 , p 2 . . . p 1248 p_{1},p_{2}...p_{1248} p1,p2...p1248,其中, p i p_{i} pi为16位。由python random模块的底层c源代码[2]可以看出,每个随机数实际是输出随机数的高16位,相当于每个原始输出随机数的截断了一半。
在这里插入图片描述

此时,我们仍然可以利用z3的符号执行去求解可能的内部状态,可以把已知的部分比特位给z3作为约束,尝试求解。因为构造的矩阵是非满秩的,所以得到的状态是满足约束条件的可能解,如果给出的约束足够多,例如上述给出了1248个16位的输出数,也可以得到确定的状态。

例如,假设一个只得到一个随机数的高16位,则可以将其构造为 p i ∣ ∣ ′ ? ′ ∗ 16 p_{i}||'?'*16 pi?16传给相应的解析函数处理,解析函数处理后,将其转为z3中的约束。核心就是使用Extact函数(用于从一个位向量(BitVector)中提取一段连续的比特位)。

def get_one_symbolic(out, index):# 参数检查略out = out.zfill(32)guess_state = BitVec(f'symbolic_guess_{index}', 32)out = out[::-1]for i, bit in enumerate(out):if bit != '?':self.solver.add(Extract(i, i, guess_state) == bit)return guess_state

关于这部分工作,在查找资料的时候发现也有前人做了相应的开源:https://github.com/icemonster/symbolic_mersenne_cracker

2.2 应用一:完全推导内部状态

在上述symbolic_mersenne_cracker项目中给出的test,就是一个完全推导内部状态的例子:

def test():'''This test tries to clone Python random's internal state, given partial output from getrandbits'''r1 = Random()ut = Untwister()for _ in range(1337):random_num = r1.getrandbits(16)#Just send stuff like "?11????0011?0110??01110????01???"#Where ? represents unknown bitsut.submit(bin(random_num)[2:] + '?'*16)r2 = ut.get_random()for _ in range(624):assert r1.getrandbits(32) == r2.getrandbits(32)logger.debug('Test passed!')

通过1248个16位的随机数,能完全恢复内部的状态,从而克隆出一个完全等价的伪随机数生成器。

2.3 应用二:部分推导,复原部分历史随机数生成

修改Untwister模块获取random的代码,改为恢复成给定seed后的状态(默认是给出后面输出的状态):

主要修改点为,参考[3]:

  • seed后的index为N=624。
    在这里插入图片描述
  • seed后的第一个state为0x80000000。
    在这里插入图片描述
from untwister import Untwister as _Untwister
class Untwister(_Untwister):def __init__(self):super().__init__()self.first_MT = self.MTself.index = 624def get_random(self):self.solver.add(self.first_MT[0] == 0x80000000)self.solver.check()model = self.solver.model()state = [model[x].as_long() if model[x] is not None else 0for x in self.first_MT]result_state = (3, tuple(state+[624]), None)rand = random.Random()rand.setstate(result_state)return rand

这样得到random就是seed后的 random能够复原部分历史的随机数生成

ut = Untwister()
r1 = random.Random()
my_r = [r1.randint(0, 6) for i in range(666)]for i in my_r:res = bin(i)[2:].zfill(3) + "?" * 29ut.submit(res)ut_rand = ut.get_random()for i in my_r:assert i == ut_rand.randint(0, 6)

3.生成等价种子

在只得到部分截断连续随机数输出的条件下,想完整的恢复种子是不太可能的,但是可以分析random模块种子的初始化过程,从而生成一个与原始种子不相同,但播种后后续部分输出相同的种子

分析random底层seed相关的初始化部分:

/* initializes mt[N] with a seed */
static void
init_genrand(RandomObject *self, uint32_t s)
{int mti;uint32_t *mt;mt = self->state;mt[0]= s;for (mti=1; mti<N; mti++) {mt[mti] =(1812433253U * (mt[mti-1] ^ (mt[mti-1] >> 30)) + mti);/* See Knuth TAOCP Vol2. 3rd Ed. P.106 for multiplier. *//* In the previous versions, MSBs of the seed affect   *//* only MSBs of the array mt[].                                *//* 2002/01/09 modified by Makoto Matsumoto                     */}self->index = mti;return;
}/* initialize by an array with array-length */
/* init_key is the array for initializing keys */
/* key_length is its length */
static void
init_by_array(RandomObject *self, uint32_t init_key[], size_t key_length)
{size_t i, j, k;       /* was signed in the original code. RDH 12/16/2002 */uint32_t *mt;mt = self->state;init_genrand(self, 19650218U);i=1; j=0;k = (N>key_length ? N : key_length);for (; k; k--) {mt[i] = (mt[i] ^ ((mt[i-1] ^ (mt[i-1] >> 30)) * 1664525U))+ init_key[j] + (uint32_t)j; /* non linear */i++; j++;if (i>=N) { mt[0] = mt[N-1]; i=1; }if (j>=key_length) j=0;}for (k=N-1; k; k--) {mt[i] = (mt[i] ^ ((mt[i-1] ^ (mt[i-1] >> 30)) * 1566083941U))- (uint32_t)i; /* non linear */i++;if (i>=N) { mt[0] = mt[N-1]; i=1; }}mt[0] = 0x80000000U; /* MSB is 1; assuring non-zero initial array */
}

用python分别实现:

from z3 import *
prng_N = 624def u32(x):return x & 0xffffffffdef init_genrand(s):mt = [0 for _ in range(prng_N)]mt[0] = BitVecVal(s, 32)mti = 1while mti < prng_N:mt[mti] = u32(1812433253 * (mt[mti-1] ^ LShR(mt[mti-1], 30)) + mti        )mti += 1return mt, mtidef init_by_array(init_key):key_length = len(init_key)mt, mti = init_genrand(19650218)i, j = 1, 0k = prng_N if prng_N > key_length else key_lengthwhile k:mt[i] = u32((mt[i] ^ ((mt[i-1] ^ LShR(mt[i-1], 30)) * 1664525)) + init_key[j] + j)i, j = i + 1, j + 1if i >= prng_N:mt[0] = mt[prng_N-1]i = 1if j >= key_length:j = 0k -= 1k = prng_N - 1while k:mt[i] = u32((mt[i] ^ ((mt[i-1] ^ LShR(mt[i-1], 30)) * 1566083941)) - i)i += 1if i >= prng_N:mt[0] = mt[prng_N-1]i = 1k -= 1mt[0] = 0x80000000return mt

可以发现random模块的流程:

  • 如果输入的种子是一个小于32位的数,就会直接把这个种子作为mt[0],然后生成后续的所有状态。
  • 如果输入的种子是一个大于32位的书,就会把输入的种子变成一个数组init_key,每个数组是一个32位数,用这个数组进行一些位操作得到最终的状态,并把第一个状态设置为0x80000000

假设我们依据2.3得到了一组可能得初始状态(seed后的)init_rand_state,那么我们要构造一个等价种子,使得播种后能产生指定的输出,思路如下:

  • 将种子设为长为624的z3向量数组seed_vecs
  • 用python重写底层c的init_by_arrayinit_genrand函数。
  • 将向量数组传入init_by_array函数中参与运算。
  • 将这些参与运算完后的向量与init_rand_state做等价约束传入z3中。
  • z3解方程,得到可能得seed_vecs
  • seed_vecs拼接成一个大数,传入python的random模块中。

通过这些步骤,能得到满足上述要求的种子。代码如下:

def recover_seed(outputs):ut = Untwister()for output in outputs:ut.submit(output)ut_rand = ut.get_random()ut_rand_state = ut_rand.getstate()[1][:-1]seed_vecs = [BitVec(f"seed_{i}", 32) for i in range(624)]seed_rand_state = init_by_array(seed_vecs)s = Solver()for a, b in zip(seed_rand_state, ut_rand_state):s.add(a == b)s.check()model = s.model()seed_init_key = [model[x].as_long() if model[x] is not None else 0for x in seed_vecs]seed_rand_seed = sum([x * (2 ** (idx * 32))for idx, x in enumerate(seed_init_key)])return seed_rand_seed

4.完整代码及工具封装

有关直接对MT19937逆向的代码在第1章已经完全给出,这里仅给出涉及符号执行的代码。

from random import Random
from untwister import Untwister as _Untwister
from z3 import *
from randcrack import RandCrack as _RandCrackprng_N = 624def u32(x):return x & 0xffffffffdef init_genrand(s):mt = [0 for _ in range(prng_N)]mt[0] = BitVecVal(s, 32)mti = 1while mti < prng_N:mt[mti] = u32(1812433253 * (mt[mti-1] ^ LShR(mt[mti-1], 30)) + mti# 1812433253 * (mt[mti-1] ^ (mt[mti-1] >> 30)) + mti)mti += 1return mt, mtidef init_by_array(init_key):key_length = len(init_key)mt, mti = init_genrand(19650218)i, j = 1, 0k = prng_N if prng_N > key_length else key_lengthwhile k:mt[i] = u32((mt[i] ^ ((mt[i-1] ^ LShR(mt[i-1], 30)) * 1664525)) + init_key[j] + j)i, j = i + 1, j + 1if i >= prng_N:mt[0] = mt[prng_N-1]i = 1if j >= key_length:j = 0k -= 1k = prng_N - 1while k:mt[i] = u32((mt[i] ^ ((mt[i-1] ^ LShR(mt[i-1], 30)) * 1566083941)) - i)i += 1if i >= prng_N:mt[0] = mt[prng_N-1]i = 1k -= 1mt[0] = 0x80000000return mtclass RandCrack(_RandCrack):def __init__(self):super().__init__()def get_random(self):state = []for i in self.mt[:624]:b = ''for j in i:b += str(j)state.append(int(b, 2))rand = Random()rand.setstate((3, tuple(state+[0]), None))return randclass Untwister(_Untwister):def __init__(self):super().__init__()self.first_MT = self.MTself.index = 624def get_random_init(self):self.solver.add(self.first_MT[0] == 0x80000000)print(self.solver.check())model = self.solver.model()state = [model[x].as_long() if model[x] is not None else 0for x in self.first_MT]result_state = (3, tuple(state+[624]), None)rand = Random()rand.setstate(result_state)return randdef recover_seed(outputs):ut = Untwister()for output in outputs:ut.submit(output)ut_rand = ut.get_random_init()ut_rand_state = ut_rand.getstate()[1][:-1]seed_vecs = [BitVec(f"seed_{i}", 32) for i in range(624)]seed_rand_state = init_by_array(seed_vecs)s = Solver()for a, b in zip(seed_rand_state, ut_rand_state):s.add(a == b)s.check()model = s.model()seed_init_key = [model[x].as_long() if model[x] is not None else 0for x in seed_vecs]seed_rand_seed = sum([x * (2 ** (idx * 32))for idx, x in enumerate(seed_init_key)])return seed_rand_seed# 根据部分输出恢复可能的种子
def test_recover_seed():# 预设初始种子my_seed = 1173791373my_rand = Random(my_seed)my_outputs = [my_rand.randint(0, 6) for i in range(1000)]my_outputs_sym = [bin(i)[2:].zfill(3) + '?' * 29 for i in my_outputs]guess_seed = recover_seed(my_outputs_sym)guess_rand = Random(guess_seed)assert my_seed != guess_seed# testfor i in range(1000):assert my_outputs[i] == guess_rand.randint(0, 6)print("test recover seed pass!")def test_randcrack():rc = RandCrack()r = Random()for i in range(624):rc.submit(r.getrandbits(32))my_rand = rc.get_random()for i in range(1000):assert r.getrandbits(32) == my_rand.getrandbits(32), f"index: {i}"print("test randcrack pass!")def test_ut():ut = Untwister()r1 = Random()my_r = [r1.randint(0, 6) for i in range(666)]for i in my_r:res = bin(i)[2:].zfill(3) + "?" * 29ut.submit(res)ut_rand = ut.get_random_init()for i in my_r:assert i == ut_rand.randint(0, 6)print("test Untwister pass!")if __name__ == '__main__':test_randcrack()test_ut()#test_recover_seed()

上述代码的能力:

  • 1.若得到完整且连续的624个随机数,使用randcrack即可得到一个完全克隆的random。
  • 2.若得到不完整且连续的若干个随机数(数量很大),使用Untwister有概率能恢复完整的内部状态。
  • 3.若得到不完整且连续的若干个随机数(数量不大),使用Untwister的get_random_init能恢复seed后的状态,能够复原历史的随机数生成。
  • 4.在3的基础上,能够恢复一个种子,该种子在seed后能重现给定随机数的生成。

后续考虑封装成pip工具包… 但是封装成工具包涉及易用性和容错性的问题,貌似工作量不小。


参考

  • [1] https://zh.wikipedia.org/zh-hans/%E6%A2%85%E6%A3%AE%E6%97%8B%E8%BD%AC%E7%AE%97%E6%B3%95
  • [2] https://github.com/python/cpython/blob/3.8/Modules/_randommodule.c
  • [3] https://imp.ress.me/blog/2022-11-13/seccon-ctf-2022#janken-vs-kurenaif

这篇关于MT19937在连续输出存在截断的情况下利用z3符号执行推导内部状态以及等价种子的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/383806

相关文章

hdu1043(八数码问题,广搜 + hash(实现状态压缩) )

利用康拓展开将一个排列映射成一个自然数,然后就变成了普通的广搜题。 #include<iostream>#include<algorithm>#include<string>#include<stack>#include<queue>#include<map>#include<stdio.h>#include<stdlib.h>#include<ctype.h>#inclu

hdu1565(状态压缩)

本人第一道ac的状态压缩dp,这题的数据非常水,很容易过 题意:在n*n的矩阵中选数字使得不存在任意两个数字相邻,求最大值 解题思路: 一、因为在1<<20中有很多状态是无效的,所以第一步是选择有效状态,存到cnt[]数组中 二、dp[i][j]表示到第i行的状态cnt[j]所能得到的最大值,状态转移方程dp[i][j] = max(dp[i][j],dp[i-1][k]) ,其中k满足c

poj2406(连续重复子串)

题意:判断串s是不是str^n,求str的最大长度。 解题思路:kmp可解,后缀数组的倍增算法超时。next[i]表示在第i位匹配失败后,自动跳转到next[i],所以1到next[n]这个串 等于 n-next[n]+1到n这个串。 代码如下; #include<iostream>#include<algorithm>#include<stdio.h>#include<math.

uva 10014 Simple calculations(数学推导)

直接按照题意来推导最后的结果就行了。 开始的时候只做到了第一个推导,第二次没有继续下去。 代码: #include<stdio.h>int main(){int T, n, i;double a, aa, sum, temp, ans;scanf("%d", &T);while(T--){scanf("%d", &n);scanf("%lf", &first);scanf

XTU 1233 n个硬币连续m个正面个数(dp)

题面: Coins Problem Description: Duoxida buys a bottle of MaiDong from a vending machine and the machine give her n coins back. She places them in a line randomly showing head face or tail face o

状态dp总结

zoj 3631  N 个数中选若干数和(只能选一次)<=M 的最大值 const int Max_N = 38 ;int a[1<<16] , b[1<<16] , x[Max_N] , e[Max_N] ;void GetNum(int g[] , int n , int s[] , int &m){ int i , j , t ;m = 0 ;for(i = 0 ;

顺序表之创建,判满,插入,输出

文章目录 🍊自我介绍🍊创建一个空的顺序表,为结构体在堆区分配空间🍊插入数据🍊输出数据🍊判断顺序表是否满了,满了返回值1,否则返回0🍊main函数 你的点赞评论就是对博主最大的鼓励 当然喜欢的小伙伴可以:点赞+关注+评论+收藏(一键四连)哦~ 🍊自我介绍   Hello,大家好,我是小珑也要变强(也是小珑),我是易编程·终身成长社群的一名“创始团队·嘉宾”

hdu3006状态dp

给你n个集合。集合中均为数字且数字的范围在[1,m]内。m<=14。现在问用这些集合能组成多少个集合自己本身也算。 import java.io.BufferedInputStream;import java.io.BufferedReader;import java.io.IOException;import java.io.InputStream;import java.io.Inp

从状态管理到性能优化:全面解析 Android Compose

文章目录 引言一、Android Compose基本概念1.1 什么是Android Compose?1.2 Compose的优势1.3 如何在项目中使用Compose 二、Compose中的状态管理2.1 状态管理的重要性2.2 Compose中的状态和数据流2.3 使用State和MutableState处理状态2.4 通过ViewModel进行状态管理 三、Compose中的列表和滚动

AI(文生语音)-TTS 技术线路探索学习:从拼接式参数化方法到Tacotron端到端输出

AI(文生语音)-TTS 技术线路探索学习:从拼接式参数化方法到Tacotron端到端输出 在数字化时代,文本到语音(Text-to-Speech, TTS)技术已成为人机交互的关键桥梁,无论是为视障人士提供辅助阅读,还是为智能助手注入声音的灵魂,TTS 技术都扮演着至关重要的角色。从最初的拼接式方法到参数化技术,再到现今的深度学习解决方案,TTS 技术经历了一段长足的进步。这篇文章将带您穿越时