シリーズ 乱数生成器の原理3 Math.random()をハッキングしてみた

本田 顕 / 2025年3月25日

はじめに

こんにちは!エヌズクリエイツ開発チームの本田です。今回は乱数生成器の第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/python3
import z3
import struct
import sys
sequence = [
  # 予測したい乱数のリスト
]
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_state0
 se_s0 = se_state1
 se_state0 = se_s0
 se_s1 ^= se_s1 << 23
 se_s1 ^= z3.LShR(se_s1, 17) # Logical shift instead of Arthmetric shift
 se_s1 ^= se_s0
 se_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 -= 1
 print(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(暗号学的に安全な疑似乱数生成器)という、予測が困難な仕組みを採用した乱数生成であり、セキュリティが問題になる場面でも安全に使うことができます。

今回は以上です!それでは

最後までお読みいただき、ありがとうございます!
エヌズクリエイツでは共に働ける仲間を募集しています!
少しでも興味がある方はこちらから覗いてみてね

同じテーマの記事

高橋 実玖 / 2026年1月30日

音声入力を仕事に取り入れてみた話

坂本 結 / 2026年1月16日

「これどこ?」をなくす。相手の時間を奪わない工夫

山本 明子 / 2025年12月19日

余白のデザインがユーザー体験を変える

坂本 結 / 2025年11月21日

確認ミスを減らす!誰でもできる仕組みづくり