63変数連立2次方程式が解ければTinyMTのstateは連続した64個の乱数値の下位2bitから得られる

Pokémon RNG Advent Calendar 2016 13日目の記事です。

疑似乱数TinyMTにおいて得られた乱数値から乱数生成器の状態を逆算する話です。

SM孵化乱数列を計算する : ただの雑記byさきによってTinyMTのstateは連続した127個の乱数値の下位1bitがあれば得られることが示されました。

これを連続した64個の乱数値の下位2bitから得られるともっと楽できそうです。

これは63変数63本のF_2係数連立2次方程式が解ければ可能です。
たとえば、連続する64個の下位2bitが01, 00, 00, …, 00であるstateは次の連立方程式を解けば得られます。

あとは、この方程式のグレブナー基底を求めるなり、SAT-based Constraint Solverにかけるなりして解けばいいですね!
どちらも試しましたが、変数の個数の指数のオーダーの時間がかかって63変数は無理そうでした!

ソースコードは以下の通り。SageMathCloudで動かすことができます。

mat1 = 0x8f7011ee
mat2 = 0xfc78ff1f
tmat = 0x3793fdff

F2 = GF(2)

# 整数を32次元F2ベクトルに変換する
def int_to_f2(x):
    return vector([F2((x >> i) & 1) for i in range(32)])

# 整数列をF2ベクトルに変換する
def ints_to_f2(xs):
    return vector([F2((xs[floor(i / 32)] >> (i % 32)) & 1) for i in range(32*len(xs))])

# 32次元F2ベクトルを整数に変換する
def f2_to_int(vec):
    x = 0
    for i in range(32):
        x |= Integer(vec[i]) << i
    return x

# F2ベクトルを整数列に変換する
def f2_to_ints(vec):
    return [f2_to_int(vec[i*32:i*32+32]) for i in range(floor(len(vec) / 32))]

# 行列を横につなげる
def join_matrix_horizontal(mats):
    m = mats[0].nrows()
    n = mats[0].ncols()
    return matrix([[mats[floor(j / n)][i, j % n] for j in range(n*len(mats))] for i in range(m)])

# 行列を縦につなげる
def join_matrix_vertical(mats):
    m = mats[0].nrows()
    n = mats[0].ncols()
    return matrix([[mats[floor(i / m)][i % m, j] for j in range(n)] for i in range(m*len(mats))])

# 行列からそのi番目の行だけを得る
def nth_row(mat, i):
    m = mat.nrows()
    n = mat.ncols()
    return matrix([[mat[i, j] for j in range(n)]])

# 128次元ベクトルを32次元ベクトルを4つつなげたものと思ってそのi番目をとってくるという変換の行列
def elem_matrix(i):
    mats = [Mat(GF(2), 32, 32).zero() for j in range(4)]
    mats[i] = Mat(GF(2), 32, 32).identity_matrix()
    return join_matrix_horizontal(mats)

# 真なら1、偽なら0を返す
def xi(x):
    if x:
        return 1
    else:
        return 0

# 32次元ベクトルに対して右シフトを表す行列
def rshift_matrix(k):
    return matrix([[F2(xi(j - i == k)) for j in range(32)] for i in range(32)])

# 32次元ベクトルに対して左シフトを表す行列
def lshift_matrix(k):
    return rshift_matrix(-k)

# 32次元ベクトルに対して第kビットを得るという変換の行列
def get_bit_matrix(k):
    return matrix([[F2(xi(j == k)) for j in range(32)]])

get_0bit = get_bit_matrix(0)

# n次元ベクトルをn×1行列に変換する
def vect_to_matrix(vec):
    return matrix([[vec[i]] for i in range(len(vec))])

# 32次元ベクトルに対してaとandをとるという変換を表す行列
def mask_matrix(a):
    return matrix([[F2(a[i] * xi(i == j)) for j in range(32)] for i in range(32)])

# 128次元ベクトルから調律した32次元ベクトルを返す変換を表す行列
def temper_matrix():
    t0 = elem_matrix(3)
    t1 = elem_matrix(0)
    t2 = rshift_matrix(8) * elem_matrix(2)
    return t0 + (t1 + t2) + vect_to_matrix(int_to_f2(tmat)) * get_0bit * (t1 + t2)

# 128次元ベクトルから調律に使われるt1の値を返す変換を表す行列
def temper_t1_matrix():
    return elem_matrix(0)

# 128次元ベクトルから調律に使われるt2の値を返す変換を表す行列
def temper_t2_matrix():
    return rshift_matrix(8) * elem_matrix(2)

temper = temper_matrix()
temper_t1 = temper_t1_matrix()
temper_t2 = temper_t2_matrix()

# 128次元のstateから次のstateを得る変換を表す行列
def next_state_matrix():
    y = elem_matrix(3)
    x = mask_matrix(int_to_f2(0x7fffffff)) * elem_matrix(0) + elem_matrix(1) + elem_matrix(2)
    x += lshift_matrix(1) * x
    y += rshift_matrix(1) * y + x
    st0 = elem_matrix(1)
    st1 = elem_matrix(2) + vect_to_matrix(int_to_f2(mat1)) * get_0bit * y
    st2 = x + lshift_matrix(10) * y + vect_to_matrix(int_to_f2(mat2)) * get_0bit * y
    st3 = y
    return join_matrix_vertical([st0, st1, st2, st3])

next_state = next_state_matrix()

# 128次元ベクトルを4つの32次元ベクトルをつなげたものとみたとき第0要素の最上位bitを無視して127bitを得るという変換の行列
def from_128bit_to_127bit_matrix():
    m = [[F2(0) for j in range(128)] for i in range(127)]
    for i in range(31):
        m[i][i] = F2(1)
    for i in range(31, 127):
        m[i][i+1] = F2(1)
    return matrix(m)

# 逆に127bitから1bit水増しして128次元ベクトルを返すという変換を表す行列
def from_127bit_to_128bit_matrix():
    m = [[F2(0) for j in range(127)] for i in range(128)]
    for i in range(31):
        m[i][i] = F2(1)
    for i in range(31, 127):
        m[i+1][i] = F2(1)
    return matrix(m)

from_128bit_to_127bit = from_128bit_to_127bit_matrix()
from_127bit_to_128bit = from_127bit_to_128bit_matrix()

# 127次元のstateから次のstateを得る変換を表す行列
next_state_127 = from_128bit_to_127bit * next_state * from_127bit_to_128bit

# 127次元のstateから連続する127個の乱数のそれぞれ最下位bitを得る変換の行列
def from_state_to_random_stream_matrix():
    mats = [None for i in range(127)]
    step = Mat(GF(2), 127, 127).identity_matrix()
    A = get_0bit * temper * from_127bit_to_128bit
    for i in range(127):
        mats[i] = A * step
        step *= next_state_127
    return join_matrix_vertical(mats)

# stateから調律後の第1bitの連続した127個分を得る
def from_state_to_tempered_bit1_stream_matrix():
    mats = [None for i in range(127)]
    step = Mat(GF(2), 127, 127).identity_matrix()
    A = get_bit_matrix(1) * temper * from_127bit_to_128bit
    for i in range(127):
        mats[i] = A * step
        step *= next_state_127
    return join_matrix_vertical(mats)

# stateから調律に使われるt1の第0bitの連続した127個分を得る
def from_state_to_t1_stream_matrix():
    mats = [None for i in range(127)]
    step = Mat(GF(2), 127, 127).identity_matrix()
    A = get_0bit * temper_t1 * from_127bit_to_128bit
    for i in range(127):
        mats[i] = A * step
        step *= next_state_127
    return join_matrix_vertical(mats)

# stateから調律に使われるt2の第0bitの連続した127個分を得る
def from_state_to_t2_stream_matrix():
    mats = [None for i in range(127)]
    step = Mat(GF(2), 127, 127).identity_matrix()
    A = get_0bit * temper_t2 * from_127bit_to_128bit
    for i in range(127):
        mats[i] = A * step
        step *= next_state_127
    return join_matrix_vertical(mats)

from_state_to_random_stream = from_state_to_random_stream_matrix()

# 127個の連続した第0bitからstateを得る行列 (逆行列を計算)
from_random_stream_to_state = from_state_to_random_stream.inverse()

from_state_to_tempered_bit1_stream = from_state_to_tempered_bit1_stream_matrix()
from_state_to_t1_stream = from_state_to_t1_stream_matrix()
from_state_to_t2_stream = from_state_to_t2_stream_matrix()

# 連立一次方程式の解のパラメータから解を得る変換の行列
def from_param_to_state_matrix(solution0, basis):
    m = [[0 for j in range(len(basis)+1)] for i in range(127)]
    for i in range(127):
        for j in range(len(basis)+1):
            if j == 0:
                m[i][j] = solution0[i]
            else:
                m[i][j] = basis[j-1][i]
    return matrix(m)

# 係数がベクトルaの成分の一次式を文字列で返す
def poly(a):
    poly = []
    for i in range(len(a)):
        if a[i] != 0:
            if i == 0:
                poly.append("1")
            else:
                poly.append("x"+str(i-1))
    if poly != []:
        return "+".join(poly)
    else:
        return "0"

# a, b, cから決まる方程式を文字列で返す
def equation(a, b, c):
    return poly(a) + "+(" + poly(b) + ")*(" + poly(c) + ")=0"

# 乱数列の最下位bitが1,0,…,0,…(最初に1、続いて0が63個)であるようなstateを求めるという一次方程式を解き、解空間(solution1, basis)を得る
I = matrix([[xi(i == j) for j in range(127)] for i in range(64)])
vec = vector([xi(i == 0) for i in range(64)])
solution0 = (I * from_random_stream_to_state).solve_right(vec)
basis = kernel(((I * from_random_stream_to_state).transpose())).basis()

from_param_to_state = from_param_to_state_matrix(solution1, basis)

for i in range(len(basis)):
    a = list(nth_row(from_state_to_tempered_bit1_stream, i) * from_param_to_state)[0]
    b = list(nth_row(from_state_to_t1_stream, i) * from_param_to_state)[0]
    c = list(nth_row(from_state_to_t2_stream, i) * from_param_to_state)[0]
    print(equation(a, b, c))

得られた連立方程式は以下にアップロードしてあります。

筆者: oupo (連絡先: oupo.nejiki@gmail.com)