はじめに
こんにちは!エヌズクリエイツ開発チームの本田です。今回は乱数生成器の第3弾として、学習がてら実際にJavaScriptのMath.random()をハッキングしてみたいと思います。
Math.random()の仕組み
JavaScriptやNode.jsのMath.random()はPRNGに該当し、V8というエンジンを使って生成されています。
実際にブラウザ上で、Math.random()を使ってみると添付画像のような結果になります。

これらは、一見ランダムに見える数字が並んでいますが、前回までに触れてきたように、一定のルール・周期に従って同じ数字が生成されています。今回はこの添付画像の数字の続きを、Math.random()を使うことなく、予測します。
V8のアルゴリズム
V8エンジンの実装を実際に追って行くとXorShift128という関数によってMath.random()で出力する値が決まっていることがわかります。

具体的にいうと、初期値(シード)を時刻として、排他的論理和とシフト演算を組み合わせて乱数が決定されています。このままV8の実装を追いかけてMath.random()の計算を明らかにすることもできますが、実はもっと簡単にハッキングする方法があります。(生成AIじゃないですよ)
Z3を使ってみる
Z3というSMT (Satisfiability Modulo Theories) ソルバーを使うと、ある複雑な数学的条件に対する解を一発で求めることができます。Math.random()の計算方法もこのツールを使えば簡単に求めることがきます。
さっそくz3をlinux環境にインストールします。

これを使った実際の実装がこちらです
#!/usr/bin/python3import z3import structimport syssequence = [# 予測したい乱数のリスト]sequence = sequence[::-1]solver = z3.Solver()# V8に合わせて64ビットのステートを作るse_state0, se_state1 = z3.BitVecs(“se_state0 se_state1”, 64)for i in range(len(sequence)):”””V8の計算ロジックと同じ論理和、シフト演算を実行”””se_s1 = se_state0se_s0 = se_state1se_state0 = se_s0se_s1 ^= se_s1 << 23se_s1 ^= z3.LShR(se_s1, 17) # Logical shift instead of Arthmetric shiftse_s1 ^= se_s0se_s1 ^= z3.LShR(se_s0, 26)se_state1 = se_s1# long long型にフォーマットfloat_64 = struct.pack(“d”, sequence[i] + 1)u_long_long_64 = struct.unpack(“<Q”, float_64)[0]# 下位52ビットを取り出すmantissa = u_long_long_64 & ((1 << 52) – 1)# Z3ソルバーに制約を追加solver.add(int(mantissa) == z3.LShR(se_state0, 12))# Z3ソルバーのチェックif solver.check() == z3.sat:model = solver.model()states = {}for state in model.decls():states[state.__str__()] = model[state]state0 = states[“se_state0”].as_long()# V8の乱数生成ロジックを再現u_long_long_64 = (state0 >> 12) | 0x3FF0000000000000# floatに変換float_64 = struct.pack(“<Q”, u_long_long_64)next_sequence = struct.unpack(“d”, float_64)[0]next_sequence -= 1print(next_sequence)
ハッキングしてみる
では、実際にNode.js上のMath.randomの実行結果とハッキング結果を比較してみましょう
まずNode.jsの実行結果を見ます

今回は6番目に来る乱数を予測するとして、0.3663883640192922の後に0.8203740823567165が算出されれば正解です。
では、ハッキングプログラムを実行して6番目にくる乱数を確認します。

done!!
0.8203740823567165が算出されたので、Math.random()で生成された乱数をpythonを使って予測することができました!
最後に
こんな感じで、みなさんお馴染みのMath.random()は簡単にハッキングすることができてしまいます。セキュリティの考慮が必要なものやゲームアプリでチートされたくない場合はMath.random()を使うことは避けましょう。代わりに、Node.jsであればcrypto.randomBytes()、生のJavaScriptであればwindow.crypto.getRandomValues()を使いましょう
これらはCSPRNG(暗号学的に安全な疑似乱数生成器)という、予測が困難な仕組みを採用した乱数生成であり、セキュリティが問題になる場面でも安全に使うことができます。
今回は以上です!それでは









