CTFにおけるCrypto入門とまとめの1つです。
乱数全般
- PRNG: 疑似乱数ジェネレータ
- HRNG: ハードウェア乱数ジェネレータ。物理プロセス(電子ノイズ、放射性崩壊、熱ノイズなど)を利用して真の乱数を生成できる専用チップを使用する
- 色々な性質
- 無作為性、ランダム性: 統計的偏りがない
- 予測不可能性: 数値の次の発生は過去のシーケンスから推測不可
- 非再現性: 例えばシードから同じシーケンスを再現できたりしない
- カテゴリ
- 弱い疑似乱数 = ランダム性
- 強い疑似乱数 = ランダム性 and 予測不可能性
- 暗号ではこれを使う
- 真の乱数 = ランダム性 and 予測不可能性 and 非再現性
- PRNGの種類
- 線形合同ジェネレータ、LCG
- 線形回帰ジェネレータ
- メルセンヌ・ツイスター
- xorshiftジェネレータ
- WELL発電機ファミリー
- LFSR
- 一方向ハッシュ関数を使う方法
- ANSI X9.17
- CPRNG: 暗号学的に安全な疑似乱数ジェネレータ
- 統計的ランダム性テスト: 特定のシーケンスにおいて、次のビットを50%を超える確率で予測できないことをテスト
- 十分に強力な攻撃に耐えられる必要がある。生成済みのシーケンスから次の乱数が推測できないか
- CPRNGの設計カテゴリ
- CSPRNG: 暗号論的擬似乱数列生成器
- Intel CPUのrdseed, rdrand命令
- Ivy Bridge以降のIntel CPUにはrdrandという命令が追加され、CPU内部の不確かな電子状態を使うことで乱数列を生成する
- std::random_deviceはこれをそのまま使っている
- だが
/dev/urandomはこの命令だけに依存するような作り方をしていない。仮にrdrandが疑似乱数であった場合(つまり、バックドアがあった場合)に判断が付かないためである
- Ivy Bridge以降のIntel CPUにはrdrandという命令が追加され、CPU内部の不確かな電子状態を使うことで乱数列を生成する
/dev/urandomLinuxで乱数が欲しくなったらとりあえずこれを使っておけばいい- 乱数ジェネレータのベンチマークツール dieharder
- 探索空間が小さい
secrets.randbits(17)は全探索可能 - 数学的な(統計的な?)法則
- Benford の法則: 多くの自然数集合における最上位桁は 1 が最も高い確率で出現
- https://gmo-cybersecurity.com/blog/ierae-ctf-2025-writeup-crypto/
- よって、定数mod乱数の統計を取ると、最上位桁が1になる確率が上がる
- Benford の法則: 多くの自然数集合における最上位桁は 1 が最も高い確率で出現
- 用語
- TRNG: 真性乱数生成器
- PRNG: 擬似乱数生成器
- 統計PRNG: 統計的ランダム性テストに合格する数値を生成するように設計。これはランダム性は求められるが、セキュリティは最重要ではない
- 暗号的に安全なPRNG(CSPRNG): ランダム性が予測不可能で攻撃に対する耐性が求められる暗号用途向けに設計されたPRNG
- エントロピー: ランダム性または予測不可能性の大きさを表す
- 乱数を作るときはエントロピーを考えましょう Black Hat 2010 / Defcon 18
- seeding: シードと呼ばれる初期値を安全な暗号関数に与え、ランダムに見える数値列を生成する。シードが同じであれば、常に同じ列を生成する
- ランダムの和
randint(1, 6) + randint(1, 6)とやると6が出やすい(普通にrandint(1,12)するより) https://blog.y011d4.com/20210713-redpwnctf-writeup#yahtzee
LCG, 線形合同法
- 線形合同法を用いた乱数生成 x[i+1]≡Ax[i]+B mod Mという漸化式で表される単純な乱数生成器
- (A,B,M)が分からない場合
- 8個サンプルがあれば完全に復元可能 https://satto.hatenadiary.com/entry/solve-LCG
- 6個でも大体復元可能 https://ctftime.org/writeup/37334
t[i] = s[i+1] - s[i]をそれぞれ計算し、u[i] = t[i + 2] * t[i] - t[i + 1] * t[i + 1]をそれぞれ計算し、u[i]のgcdを取ると大体Mが復元可能- 復元可能かどうかは確率的
- 5個でもできるみたい https://zenn.dev/kurenaif/articles/f9d3f56e1d3235#the_big_five
- gcdで出てくる値がMの整数倍になっていることがあるが、それほど大きい整数倍にはならないだろうので素因数分解してMを持って来る方針も書かれている
- ある時点の出力が得られれば前後すべての出力を計算できるほか、A,B,Mといったパラメータが未知の場合でも、複数の出力からこれらのパラメータを復元することもできる
- CTF crypto 逆引き - ふるつき
- https://www.youtube.com/watch?v=DVZnJG76wdg&t=2s
- kurenaif先生の動画ですべてが理解できる
- A,B,M全てが分からなくても6つの連続する乱数が得られれば解ける
- 実装例
- 記事
- z3でごり押して求めることも場合によってはできるみたい
- 線形計算もできる?
- corCTF 2021 fried_rice https://blog.y011d4.com/20210823-corctf-writeup
- 問題
QCG: Quadratic Congruential Generator, 二次関数合同法?
- https://blog.y011d4.com/20210307-zer0pts-ctf-writeup#easy-pseudo-random
- Pollard generator: v[i+1] = av[i]^2 + bv[1] + cでa=1, b=0のQCG
FSR: フィードバックシフトレジスタ
- 初期状態 a0, a1, ..., an-1のn個の数列を用意
- フィードバック関数 f(ax, ax+1, ..., ax+n-1) を用意
- フィードバック関数を使って、ax, ax+1, ..., ax+n-1からax+nを作成して、状態をax+1, ax+2, ..., ax+nに遷移させて乱数を生成していく
- 出力される乱数
- a0, a1, a2, ... のように乱数が出力されていく場合もあるし
- x0 = sum_{i=0..N-1} ci*ai みたいにある種の式を使って決定される場合もある
LFSR
- LFSRの流れ
- 初期状態Rと、フィードバック関数maskがある
- R&maskをして1各桁の総和(xor和)を取ったものを次の乱数シーケンスの出力rとする
- 状態をR'=R<<1 + rで更新
- 2-3を繰り返す
- 大体maskと乱数列が与えられるのでRを求める問題が出る
- 連続するn個の出力が得られれば特定状態を再現できるので、そこから逆行列を用いて初期状態を特定する
- これはR'の下位1ビットと乱数シーケンスの出力rが一致することから内部状態が漏洩することになるため
- 32bitsのLFSRであれば、連続する32bitsが分かれば、その1つ前の1bitsの状態のみが変数となる方程式が得られて計算できる
r = mask[0]*R[0] + mask[1]*R[1] + ... + mask[31]*R[31]でrとmask全部とRの0~30が既知なのでR[31]が計算できるということ
- 連続してなくてもサンプルが取れれば、z3である程度解けるっぽい?
- フィードバック関数が線形であるということは行列計算に帰着させることが可能になる
- LFSRはGF(2n)上で線形。積は普通に積で、和はxorにしたもの
- 繰り返し二乗法も行けるし、逆行列による計算も可能
- 20220522_LFSRの超難問を解く - Google スライド
- LFSRへの攻撃手法まとめ。
- これを理解できればゴールした感ある
- Berlekamp-Masseyアルゴリズム
- LFSRは線形で逆算可能なので非線形要素を取り入れようという考えがある
- 出力される乱数を単純な線形式ではなく、非線形にする
- フィードバック関数を非線形にする -> NFSR
- 複数のLFSRの出力を非線形に組み合わせる
- 変数を増やして式を作っていくと線形システムにできた N1CTF 2025 - n1fsr
- https://github.com/hash-hash/My-CTF-Challenges/tree/main/n1ctf%202025/n1fsr
- 以下の条件があった
- ビット毎に見ていくと、線形に結合されている場合がある
- 特定のLFSRのサイクルが非常に短い
- 特定のペアのLFSRのサイクルは同じ
- 線形にできれば、連立方程式を用意して、それを行列式にして、計算すれば、解けるという感じ
- Correlation Attack
- 多分全然理解してない。
- 複数のLFSRの出力を組み合わせてキーストリームを生成している場合に有効な攻撃
- LFSRの出力を1つにまとめるためのブール関数に起因する統計的な弱点を利用する
- 全部そうという感じではなく、一部のブール関数に限るっぽい?
- ここでの「相関」とは
- 生成器によって使われるとあるLFSRの出力と、生成器(とブール関数)によって最終的に出力される結果の間の相関
- つまり、どの程度一致しているか
- 一致しすぎていれば、出力の何割かを見れば、とあるLFSRの出力が分かってしまう
- 一致しすぎない傾向であっても逆にすれば出力が分かるので、50%の一致率が理想
- 攻撃PoC https://github.com/M0RC/lfsr-correlation-attack
- 生成器によって使われるとあるLFSRの出力と、生成器(とブール関数)によって最終的に出力される結果の間の相関
- https://ctf-wiki.mahaloz.re/crypto/streamcipher/fsr/nfsr/
- Fast Correlation Attack?
- https://hackmd.io/@giangnd/rJjnnaY50
- https://crypto-writeup-public.hatenablog.com/entry/0CTF%252FTCTF_Quals_2021_%257C_zer0lfsr
- https://blog.bi0s.in/2020/08/02/Crypto/LFSR/InCTFi-20-FaultyLFSR/
- Meier-Staffelbach モデルを使うと、相関が0.53より大きい場合に攻撃が可能らしい。また、タップ数(maskの1が立っている数)が少ないほど効果的らしい。10未満だと理想的らしい
- https://blank-vax.github.io/2019/10/30/LFSR%E5%8E%9F%E7%90%86%E5%8F%8ACTF%E7%9B%B8%E5%85%B3/
- 変数を増やして式を作っていくと線形システムにできた N1CTF 2025 - n1fsr
- sagemathでLFSRの計算をするときはBooleanPolynomialRingが使える
PR = BooleanPolynomialRing(names = [f'x{i}' for i in range(1, 7*2+24+1)])
- いいまとめ
- 問題
- 2018 强网杯 streamgame1
- 2018 CISCN 予選ラウンド oldstreamgame
- CakeCTF 2021 improvisation
- フラグの先頭は決まっていて、それと暗号文から初期状態が取得できるので、残りを復元して解読する問題
- かなり簡潔なソルバ https://mitsu1119.github.io/blog/post/post9/cake/#improvisation
- pbvtf 2020 - Crypto Hammer https://www.jaybosamiya.com/blog/2020/12/10/cryptohammer/
- 一般的なLFSRではないが、視覚化(?)するとLFSRに似ていて、実験すると今回の状況だと等価である実験結果が得られたので、Berlekamp-Masseyアルゴリズムが適用できて状態復元ができた
メルセンヌツイスター, Mersenne Twister, MT, MT19937
- MT19937
from random import getrandbits- https://github.com/toby-bro/Writeups/tree/main/smileyCTF2025/enough/
- 飛び飛びで640ビットの情報からz3で計算して15分かけて初期状態を見つける
- https://github.com/toby-bro/Writeups/tree/main/smileyCTF2025/enough/
- CTF crypto 逆引き - ふるつき
- 624 × 32 ビットの出力を観察すれば内部状態が決定する
- 記事
- z3を使ったクラッカー https://github.com/icemonster/symbolic_mersenne_cracker/blob/main/main.py
- 割と使われているので使えるか考えてみよう
- 飛び飛びでも使える
- ChatGPTが出してきた。嘘かも。MTで作られたランダムなバイト列であるかを判定するのに線形複雑度の計算が使える
- 線形複雑度が一定以下であればMT出力であり、それ以外であれば暗号と判定する
- 線形複雑度はBerlekamp–MasseyでLFSRとして見た時に特性方程式が作れるかを見ることもできる(?)
- 他ライブラリ https://github.com/deut-erium/RNGeesus
- predictor https://github.com/kmyk/mersenne-twister-predictor
- symbolic_mersenne_cracker https://github.com/icemonster/symbolic_mersenne_cracker/tree/main
XorShift
- 最初にstateを定めたあと、以下の処理を1サイクルとして、次の乱数を生成していく。
state = state ^ (state << a); state = state ^ (state >> b); state = state ^ (state << b); - シフトパラメタa,b,cによって乱数の質が変わる。周期性が変わる。
- 13,17,5 -> Xorshiftの論文にもあるパラメタで32ビット用。64ビットに使うには微妙らしい
- 64ビットでは、21,35,4が良い?
- https://www.cepstrum.co.jp/hobby/xorshift/xorshift.html
- 17,9,18だと280が周期になって与えられている試行回数を使うと2周目に突入できて、推測できた
- XorShiftの計算は行列計算に変換し、高速化可能
- 足し算をXORにしている? TokyoWesterns CTF 6th 2020 writeup - XOR and shift encryptor
- WaniCTF 2024 Many Xor Shift https://qiita.com/KowerKoint/items/89d94343e6ceee32645c#cry-many-xor-shift
- もっとちゃんとした解説? https://blog.visvirial.com/articles/575
- 派生のxoshiro256/xoshiro256+/xoshiro256++/xoshiro256**というのもある
実装
乱数は割とライブラリの実装を読んで、内部構造を理解して問題を解くことがある
- C言語
- srand()にtime(NULL)を渡している
- time(NULL)は実行時のUNIX秒を返すので、同じ時間であれば同じシードになる
- srand()にtime(NULL)を渡している
- python
- random.Random()はメルセンヌツイスタ法/MT19937で、予測方法は次の2つ
- シード値を特定する
- 624words(1word=4bytes)以上取得すれば内部状態を復元でき、それ以降は予測可能
- pythonならrandcrackというライブラリを使えば楽
- pipで入れるとoffsetがうまく動かないのでgitで入れる
git clone https://github.com/tna0y/Python-random-module-cracker.gitして、randcrackを同じフォルダに持ってきてfrom randcrack import RandCrackして使う
- pythonならrandcrackというライブラリを使えば楽
- SECCON CTF 2022 Quals - janken vs kurenaif でz3を使ったクラック問題が出た
random.seed(round(time.time(), 2))- やはり、時刻をシードにしてはいけない
- この問題ではサーバ起動時にシード指定とadminアカウント作成を行っていて、アカウント作成ではuuid v1でIDを発行している (
uuid.uuid1()) - uuid1はuuid内部に発行時間を含むのでシードのおおよその推測が可能となる
- UUIDデコーダ - Create UUIDで時刻を抜き出す。UTCでもらえるのでunix秒に直したりして使う
random.seed(int(time.time()))- 時刻をシードにしてはいけない2
- UNIXTIMEの小数点以下を切り捨てた整数をシード
- つまりunix秒
- corCTF 2022 writeup - Attack All Around
__import__('random').getstate()のようにすると現在の乱数の状態を取得でき、乱数推定が可能になる- UTCTF 2023 - ランダムのクラッキング | FireShell セキュリティ チーム
- getrandbitsをして、その後getstateの結果を持ってきて、状態を1つ前の状態に戻して、再度getrandbitsをして最初と一致することを確かめるPoCがある
- UTCTF 2023 - ランダムのクラッキング | FireShell セキュリティ チーム
- https://stackered.com/blog/python-random-prediction/
- 実はpythonのrandomは624個無くても、状況によっては6個とかでも復元できる
- https://github.com/FlagMotori/Nowruz1404/tree/main/crypto/superguesser2
- randomの実装について
- Mersenne Twisterで、624個の32bits数値列からなるstateとindexを持っている
- state[index]にtemperと呼ばれる操作をした値を乱数として生成する
- 乱数生成時はindex++され、index>=624のときはstateの更新が走る
- y011d4さんのstate復元自己実装 https://blog.y011d4.com/20210606-zh3r0-ctf-writeup#twist-and-shout
- random.seedは整数型をシードとして与えた場合はハッシュせず、それを使ってrandomの状態ベクトルに難読化する
- つまり、任意の乱数状態のためのシードを可逆に計算可能
- janken vs kurenaif
- 条件を満たす状態を生成するシードを作る問題
- https://furutsuki.hatenablog.com/entry/2022/11/13/202646
- https://blog.y011d4.com/20221113-seccon-ctf-writeup
- pythonで実装しなおしてくれている + z3
- ImaginaryCTF 2025 - Feistier Network
- Real Mersenne https://github.com/qxxxb/ctf/tree/master/2021/zh3r0_ctf/real_mersenne
- 噂によると...
- 先頭20個の乱数が一致する異なるシードもあるっぽい
- 全部の乱数が一致する異なるシードもあるっぽい
For example 10 and 10+2^329 and 10+2^329+2^648+2^967らしいけどうまくいかなかった
- python random
- numpy, np.random.seedの実装 https://blog.y011d4.com/20210606-zh3r0-ctf-writeup#import-numpy-as-mt
- random.Random()はメルセンヌツイスタ法/MT19937で、予測方法は次の2つ
- php
- uniqidは時刻に基づいてuniqにする
- My-CTF-Challenges/HITCON CTF/2022/yeeclass at master · t510599/My-CTF-Challenges
- 時刻に応じて連番のような感じになるから、サンプルとして値が取れるときは、前後で値を取得すれば、その間は前後の間の値となって全探索可能となる https://github.com/satoki/ctf_writeups/tree/master/%C3%A5ngstromCTF_2023/filestore
- mt_rand関数
- uniqidは時刻に基づいてuniqにする
- javascriptのMath.random()
- 環境によって使っているもの違いそう
- Chrome: むずいけどできるみたい https://gist.github.com/terjanq/e66c2843b5b73aa48405b72f4751d5f8
- Firefoxは簡単? https://github.com/mkutay/spidermonkey-randomness-predictor/blob/main/main.py
- Node.jsのV8はXorShift128+を使っていて、Z3で簡単に状態復元が可能 https://blog.hamayanhamayan.com/#%E4%B9%B1%E6%95%B0%E3%81%AE%E6%8E%A8%E6%B8%AC%E3%81%A8Boundary-Injection%E3%81%AB%E3%82%88%E3%82%8BContent-Type%E3%81%AE%E5%B7%AE%E3%81%97%E8%BE%BC%E3%81%BF
- SafariはJavascriptcore? これもできるっぽい
- シードのエントロピーが小さい場合
- revとかcryptoとかでシードのエントロピーが小さい場合はあらかじめ何通りかのシードで乱数を用意しておいてマッチさせる方法がある
- 例えばシードが最大32bytesくらいのエントロピーの場合、256分の1の24bytes分(16777216通り)の乱数テーブル(最初の数個)を作って、何回もマッチするか試す
- 500回も試せば1回くらい当たるらしい(正確な回数はわからないが、確かにそれっぽい)
- ImaginaryCTF 2022 writeup - st98 の日記帳 - コピーのThe House Always Wins
- 使われる乱数に偏りがないか?
- ImaginaryCTF 2022 writeup - st98 の日記帳 - コピーのotp
- 適当に乱数を作ってみて偏りがないかを試す。偏りがあるなら統計的に何回もデータを収集して復号できるかも
- ImaginaryCTF 2022 writeup - st98 の日記帳 - コピーのotp
- https://b4d.sablun.org/blog/2020-04-20-247ctf-com-web-administrative-orm/
- 内部情報からUUIDv1が生成できるかも
- 必要な情報
- タイムスタンプ
- クロックシーケンス
- ノード情報(サーバーのMACアドレス)