web-dev-qa-db-ja.com

Math.random()の数値を予測しますか?

私は Math.random()のドキュメント を読んでいて、次のメモを見つけました。

Math.random()は暗号的に安全な乱数を提供しません。セキュリティに関することには使用しないでください。代わりにWeb Crypto APIを使用し、より正確にはwindow.crypto.getRandomValues()メソッドを使用します。

randomの呼び出しで生成される数値を予測することはできますか?もしそうなら-これはどのように行うことができますか?

34
Abe Miessler

実際、Math.random()は暗号的に安全ではありません。


Math.random()の定義

ES6仕様 でのMath.random()の定義は、JavaScriptエンジンでの関数の実装に関して多くの自由を残しました:

実装に依存するアルゴリズムまたは戦略を使用して、ランダムにまたは疑似ランダムにその範囲全体にわたってほぼ均一な分布で選択された、0以上1未満の正の符号を持つ数値を返します。この関数は引数を取りません。

異なるコードレルム用に作成された各_Math.random_関数は、連続する呼び出しから異なる値のシーケンスを生成する必要があります。

それでは、最も一般的なJavaScriptエンジンがどのように実装したかを見てみましょう。


Xorshift128 +XorShift乱数ジェネレータ の1つであり、暗号化されていない最も高速な乱数ジェネレータの1つです。

ただし、上記の実装のいずれかに攻撃があるかどうかはわかりません。しかし、これらの実装はごく最近のものであり、他の実装(および脆弱性)は過去に存在しており、ブラウザー/サーバーが更新されていない場合でも存在する可能性があります。

更新: douggard's answer は、誰かがXorShift128 +の状態を回復し、Math.random()値を予測する方法を説明しています。


V8のMWC1616アルゴリズム

2015年11月、Mike Maloneはブログ投稿で V8のMWC1616アルゴリズムの実装がどういうわけか壊れていた と説明しました: this test または this one V8ベースのブラウザーを使用している場合。 V8チームが対応しました 、Chromium 49(2016年1月15日)およびChrome 49(2016年3月8日)で修正をリリースしました。

このペーパー 2009年に完成し、以前のMath.random()(MWC1616バージョン)の出力に基づいてV8のPRNGの状態を判断する方法を説明しました。

これは、(出力が連続していない場合でも)実装する Pythonスクリプト です。

これは、Node.jsで構築された賭けサイトである CSGOJackbotに対する現実世界の攻撃 で悪用されています。攻撃者は、この脆弱性をからかうだけの十分な公平性を持っていました。


区画化の欠如

ES6以前は、 Math.random()定義 は、個別のページが個別の値のシーケンスを生成する必要があることを指定していませんでした。

これにより、攻撃者はいくつかの乱数を生成し、PNRGの状態を判別し、ユーザーを脆弱なアプリケーション(機密情報にMath.random()を使用する)にリダイレクトし、Math.random()がどの番号を使用しているかを予測できました戻ります。 このブログ投稿 は、その方法に関するコードをいくつか示しています(Internet Explorer 8以下)。

ES6仕様 (2015年6月17日に標準として承認されました)により、ブラウザーはこのケースを正しく処理できます。


ひどく選ばれた種子

シーケンスを初期化するために選択されたシードを推測することにより、攻撃者はシーケンス内の数を予測することもできます。 2012年の Facebookで使用されている 以来、これは現実のシナリオでもあります。


このペーパー は2008年に公開されたもので、ブラウザーのランダム性の欠如のおかげで情報をリークするさまざまな方法を説明しています。


ソリューション

まず第一に、常にブラウザ/サーバーが定期的に更新されていることを確認してください。

次に、必要に応じて暗号化関数を使用する必要があります。

どちらもOSレベルのエントロピーに依存しており、暗号的にランダムな値を取得できます。

35
Benoit Esnard

Z3の定理証明を使用してこれらを攻撃できます。宝くじシミュレーターで値を予測するために Pythonでこのような攻撃を実装 をしました。

前述したように、現在XorShift128 +がほとんどの場所で使用されているため、攻撃されています。まず、通常のアルゴリズムを実装して理解できるようにします。

def xs128p(state0, state1):
    s1 = state0 & 0xFFFFFFFFFFFFFFFF
    s0 = state1 & 0xFFFFFFFFFFFFFFFF
    s1 ^= (s1 << 23) & 0xFFFFFFFFFFFFFFFF
    s1 ^= (s1 >> 17) & 0xFFFFFFFFFFFFFFFF
    s1 ^= s0 & 0xFFFFFFFFFFFFFFFF
    s1 ^= (s0 >> 26) & 0xFFFFFFFFFFFFFFFF 
    state0 = state1 & 0xFFFFFFFFFFFFFFFF
    state1 = s1 & 0xFFFFFFFFFFFFFFFF
    generated = (state0 + state1) & 0xFFFFFFFFFFFFFFFF

    return state0, state1, generated

アルゴリズムは2つの状態変数XORを取り込んでシフトし、更新された状態変数の合計を返します。また、各エンジンが、返されたuint64を取得してdoubleに変換する方法も重要です。この情報は、各実装のソースコードを掘り下げて見つけました。

# Firefox (SpiderMonkey) nextDouble():
# (Rand_uint64 & ((1 << 53) - 1)) / (1 << 53)

# Chrome (V8) nextDouble():
# ((Rand_uint64 & ((1 << 52) - 1)) | 0x3FF0000000000000) - 1.0

# Safari (WebKit) weakRandom.get():
# (Rand_uint64 & ((1 << 53) - 1) * (1.0 / (1 << 53)))

それぞれ少しずつ異なります。次に、Math.random()によって生成されたdoubleを取得し、アルゴリズムによって生成されたuint64のいくつかの下位ビットを回復できます。

次に、Z3にコードを実装して、コードを実行して状態を解決できるようにします。詳細については、Githubリンクを参照してください。これは通常のコードと非常に似ていますが、下位ビットがブラウザから復元された下位ビットと等しくなければならないことをソルバーに伝えます。

def sym_xs128p(slvr, sym_state0, sym_state1, generated, browser):
    s1 = sym_state0 
    s0 = sym_state1 
    s1 ^= (s1 << 23)
    s1 ^= LShR(s1, 17)
    s1 ^= s0
    s1 ^= LShR(s0, 26) 
    sym_state0 = sym_state1
    sym_state1 = s1
    calc = (sym_state0 + sym_state1)

    condition = Bool('c%d' % int(generated * random.random()))
    if browser == 'chrome':
        impl = Implies(condition, (calc & 0xFFFFFFFFFFFFF) == int(generated))
    Elif browser == 'firefox' or browser == 'safari':
        # Firefox and Safari save an extra bit
        impl = Implies(condition, (calc & 0x1FFFFFFFFFFFFF) == int(generated))

    slvr.add(impl)
    return sym_state0, sym_state1, [condition]

連続して生成された3つのdoubleをZ3に供給すると、状態を回復できるはずです。以下はメイン関数の抜粋です。 Z3の64ビット整数(不明な状態変数)の2つでシンボリックに実行されるXorShift128 +アルゴリズムを呼び出し、回復されたuint64から下位(52または53)ビットを提供します。

それが成功した場合、ソルバーはSATISFIABLEを返し、解決した状態変数を取得できます。

    for ea in xrange(3):
        sym_state0, sym_state1, ret_conditions = sym_xs128p(slvr, sym_state0, sym_state1, generated[ea], browser)
        conditions += ret_conditions

    if slvr.check(conditions) == sat:
        # get a solved state
        m = slvr.model()
        state0 = m[ostate0].as_long()
        state1 = m[ostate1].as_long()

パワーボールシミュレーターで当選した宝くじの数を予測するためにこの方法を使用することに焦点を当てた、もう少し詳細な記述 here があります。

21
douggard

Math.random(および他の同様の関数)はシードから始まり、新しい数値を作成します。もちろん、アルゴリズムはそのように調整されているため、ユーザーにはランダムに見えます。しかし実際のところ、ランダム性の本当の原因はどこにもありません。ジェネレーターの内部状態(100%確定的ソフトウェアであり、特別なものは何もない)がわかっている場合、all将来(およびアルゴリズムによっては過去)で生成された数値がわかります。

「実際の」ランダムな発生源は、放射性粒子の崩壊の測定のようなもの、より現実的な用語では、あらゆる種類の電気的ホワイトノイズ、またはより実際には、ユーザーがマウスを動かしたり、キーを押したときのわずかなずれのようなものです。など。 Math.randomには何もありません。

Math.randomはこのように設計されており(ほとんどの言語/ライブラリのrandom関数のように)、同じシードから「ランダムな」数値の文字列を復元できるプロパティは、実際には便利な機能です多くの場合。セキュリティのためだけではありません。

2
AnoE