- Randar は Minecraft Beta 1.8〜1.12.2 において、ブロック破壊時に発生するアイテムドロップ座標を利用してサーバーの
java.util.Random 状態を復元し、他プレイヤーの位置を逆追跡するエクスプロイトである
- 根本原因は、地形・構造物生成と採掘イベントが同じ乱数生成器を共有する RNG再利用 にあり、観測可能なドロップ位置が内部状態の手がかりになる
- アイテムドロップの X/Y/Z オフセットは
World.rand.nextFloat() の3回連続出力なので、48ビットシードの上位24ビットが露出し、LLL格子簡約 によってシードを高速に復元できる
- 復元したシードは逆方向に巻き戻して Woodland Mansion チェック由来の値かどうかを確認し、一致すれば直近でロードされた 1280×1280ブロックの Woodland region を特定できる
- ReplayMod のような過去のパケット記録にドロップデータが残っていれば、サーバーが修正後でも Beta 1.8〜1.12.2 時代の活動位置が事後的に露出する可能性がある
Randarが狙う情報
- 目標は、同じワールドにいる他プレイヤーの ゲーム内座標 を見つけること
- 主な事例として 2b2t が使われている
- 2b2t はルールのない Minecraft の「anarchy」サーバーである
- サーバーマップは 3.6 quadrillion square tiles 規模で、位置の秘匿性がアイテムを守るための重要条件になる
- Randar 以前にも、2b2t には 2018〜2021 年に使われた座標エクスプロイト Nocom があった
脆弱なバージョンとコード上のミス
- 問題は Minecraft Beta 1.8 の 2011 年リリースから 1.12.2 の 2017 年リリースまで存在していた
- 2b2t は 2023 年 8 月 14 日まで 1.12.2 に留まっていた
- 核心的なミスは、
java.util.Random インスタンスを複数のコード経路で不注意に再利用していた点である
- 地形生成と採掘のようなゲーム動作の間で RNG が共有されていた
java.util.Random 自体もセキュリティ用途の RNG ではない
- Minecraft は、同じワールドシードと同じ位置で同じ地形が生成されるよう 決定的生成 を使っている
- この目的に
java.util.Random を使うのは自然である
- 問題は、予測可能であるべきワールド生成用 RNG の操作が、予測不能であるべきイベントにまで影響していたこと
Woodland Mansion チェックとグローバル World.rand
- Woodland Mansion の生成位置を確認する過程で
World.rand が再設定される
- Woodland region は 80×80 チャンク単位で計算される
- その region 内で Mansion が生成されるチャンクを選ぶため、
random.nextInt(60) が4回呼ばれる
- 脆弱なフローは、
World.setRandomSeed(seedX, seedY, seedZ) がグローバルな this.rand に新しいシードを設定し、同じオブジェクトを返す構造にある
- 2b2t 専用の式は次の通り
seed = x * 341873128712 + z * 132897987541 - 4172144997891902323 mod 2^48
setRandomSeed は実際に Woodland Mansion の近くにいるときだけでなく、チャンクロードのたびにチェック目的 で呼び出される
- ディメンションごとの影響は異なる
- Overworld が主な影響対象である
- Nether は構造物生成が常に安全な RNG を使うため安全である
- The End は end city のため初回生成時には影響を受けるが、同じチャンクが後で再ロードされるたびに影響するわけではないため、比較的安全である
アイテムドロップ座標がRNGを露出させる仕組み
- ブロックを採掘すると、アイテムはブロック内部のランダムな位置にドロップする
- たとえばブロック座標が
(10, 20, 30) なら、アイテムは (10.25, 20.25, 30.25) から (10.75, 20.75, 30.75) の間に現れる
- この位置は
world.rand.nextFloat() 3回で X、Y、Z オフセットを決める
- ドロップ座標から元の
nextFloat() 値へ戻すことができる
- float に 0.5 を掛ける処理では指数だけが下がるため情報損失がない
- その後 double に変換され、ブロック座標が加えられて full precision でネットワーク送信される
java.util.Random.nextFloat() は 48 ビットシードを更新したあと、上位 24 ビットを整数として取り出し 2^24 で割る
- LCG の式は
newSeed = oldSeed * 25214903917 + 11 mod 2^48
- 3回連続の float は、3つの連続シードの上位 24 ビットを与える
LLL格子簡約によるシード復元
- 単純な方法は、最初の測定値に合う下位 24 ビット候補
2^24 個をすべて試すことである
- 3つの測定値は次の3値の範囲を与える
seed
nextSeed(seed)
nextSeed(nextSeed(seed))
- この3値を 3 次元の点
(seed, nextSeed(seed), nextSeed(nextSeed(seed))) とみなすと、可能な全シードは格子構造をなす
- 基底ベクトルは次のように取る
(1, a, a^2)
(0, c, 0)
(0, 0, c)
- ここで
a = 25214903917, c = 2^48
- LLL basis reduction は、同じ格子を生成するより短くほぼ直交した基底を求める
- Mathematica の例は
LatticeReduce[{{1, a, a^2}, {0, c, 0}, {0, 0, c}}]
- 結果の基底は
(1270789291, -2446815537, 2154219555), (-2355713969, 1026597795, 4110294631), (-3756485696, -2345310016, -2015749696) である
- 測定キューブの中心を簡約基底空間に変換し、各係数を最も近い整数に丸めると有効な格子点が得られ、最初の座標が復元された内部シードになる
- 最適化された Java コード例では、3つの測定値からおよそ 10ns 程度でシードを復元できる
シードを巻き戻して位置を特定する過程
java.util.Random の LCG は前方向にも後方向にも進められる
- 順方向:
newSeed = oldSeed * 25214903917 + 11 mod 2^48
- 逆方向:
oldSeed = (newSeed - 11) * 246154705703781 mod 2^48
- 復元したシードを逆に巻き戻しながら、各シードが Woodland Mansion チェックで得られるシードになり得るかを確認する
- Minecraft のワールド範囲は -30 million から +30 million ブロックまでである
- 実装では各軸の Woodland region 範囲を -23440 から +23440 としている
- 可能な Woodland region 数は
(23440*2+1)^2、すなわち 2,197,828,161 個 である
- 22 億個の候補をすべて比較する方法は遅く、大きな
HashSet もメモリを大量に使う
- Z 係数
132897987541 は奇数なので mod 2^48 における逆元を持つ
- 逆元は
211541297333629
- これを使えば、全 X 候補 46,881 個だけを走査して Z を計算し、候補 region を求められる
GPUとルックアップテーブル最適化
- X のみを走査する方法も単一シードに対しては現実的だが、多数のボットが毎秒複数ブロックを掘り、各測定ごとに数千 RNG step を調べる場合、低性能 VPS ではリアルタイム処理が難しかった
- その後の実装は CUDA バッチ処理 とルックアップテーブルへ移行した
- ルックアップテーブルのキーは mansion seed の下位 32 ビット
- 値は Woodland region の X 座標
- 下位 32 ビットのキーでは衝突がなく、その理由は分からなかったと記されている
- テーブルは
2^32 エントリ、1 エントリあたり 2 バイトを使うため、およそ 9GB VRAM が必要である
- RTX 3090 では毎秒およそ 1000万 seeds をクラックできた
- 復元結果は、直近でチャンクロードが発生した 1280×1280 ブロックの Woodland region を示し、その精度があれば数分の探索で位置特定が可能である
実サーバーで観測された step 分布
- 理論上、Woodland seed 間の平均間隔は約 128,000 RNG steps である
- 2b2t では、ほとんどが数十 step 以内で Woodland seed を発見できた
- 測定はパケット処理時点の都合で tick の非常に早い時点に行われる
- 多くの場合、直前の tick にチャンクがロードされていた
- 信頼できる測定は最低 4 RNG steps から始まる
- Woodland Mansion のコードが観測前に
rand.nextInt を4回呼ぶためである
- 大きなスパイクは 1354 step の倍数で現れた
- end crystal 爆発または wither skull の可能性が示されている
- end crystal 爆発の場合、ブロック被害計算
16^3-14^3=1352 とサウンド効果 2 回で 1354 step に一致する
ReplayModと事後露出リスク
- サーバーが最新バージョンへ更新されたり RNG 操作が修正されたりしていても、過去データが残っていれば Randar のリスクは残る
- 一部の Minecraft プレイヤーは ReplayMod のようなモッドでパケットを記録している
- 記録ファイルにアイテムドロップが含まれていれば、当時のサーバー RNG 状態を復元できる
- ブロック破壊は非常によく行われる動作なので、記録に含まれている可能性が高い
- Beta 1.8〜1.12.2 で活動していたすべての位置は、サーバーがかなり前に更新されていても露出したものとみなすべきである
- Randar を直接試せるクライアントサイドの Web ツールも提供されている
Randar運用とヒートマップ
- SpawnMasons は、もともと別プロジェクトのために 24/7 で stone/cobblestone を掘っていたアカウントで、アイテムドロップ座標の記録を開始した
- Nocom で使っていた headless Minecraft システムを再利用し、測定値保存のため Postgres データベースを追加した
- RNG 測定値をクラックするソフトウェアは何度も改良され、最終的に async CUDA batch job に落ち着いた
- クラック済み測定値がデータベースに追加されると、heatmap 分析テーブルも更新される
- 全期間、日別、時間別の hit count を保存する
- Plotly Dash UI で特定の時間範囲と granularity を選び、ブラウザで閲覧できるようにする
- Elytra stash hunting によるチャンクロード spam は、複数の distinct hour にわたってロードされた座標だけを考慮する方法で除去する
- 発見した hotspot を追跡するため、簡易な共有 annotation システムも追加した
- Nocom から持ち込んだ Baritone bot により、アイテム stash を盗み整理する工程を AFK 自動化した
保護用 decoy Woodland region
- Randar が常に最新チャンクを正確に特定できるとは限らない
- RNG を逆に巻き戻す際、より新しい decoy Woodland region が先に現れると、最初の一致を返すエクスプロイトは誤検出を起こし得る
- Woodland seed は全体として約 130,000 個の RNG seed に 1 個程度の割合だが、分布には外れ値がある
- 2b2t では、およそ 2万個の Woodland region に1つ が、次の 4 RNG step 以内に別の Woodland region を持つ特殊な hiding property を備えるという
- SpawnMasons はこのような region に stash を構築した
- レンダー距離の関係で保護 region 外のチャンクがロードされないよう、構造物をコンパクトに作った
- decoy 位置には AFK アカウントと小さな base を置き、他の Randar 使用者に decoy 位置を見せることを狙った
- 自前の Randar ログでは、これらの stash は全期間を通じて隣接 Woodland region を誤ってロードしていない「clean」状態だった
- 公開時点では、それらの stash はすでに移設済みだと述べている
完全な例とバージョン別の制約
SPacketSpawnObject から採掘ドロップに見えるアイテムを検出し、ドロップ座標を 3 つの float 測定値へ戻したうえで、LLL ベースのクラックと Woodland region の逆追跡を行う Java 例が提供されている
- 2b2t の実測値例は次の結果を与える
- item drop:
0.41882818937301636, 0.6833633482456207, 0.46088552474975586
- RNG measurements:
5664934 14541261 7076144
- 内部 seed:
95041827771683
- Woodland region:
-12008 0
- 位置範囲:
-15370368,-128 から -15369089,1151
- 図解用の例では Woodland Region
123,456 を見つける
- 最終的な位置範囲は
157312,583552 から 158591,584831
- 元の入力座標
x=157440 z=583680 を含んでいる
- 1.11 より前のバージョンでは、exploitable structure は Woodland Mansion ではなく別の構造物なので、別のコードが必要である
- 1.9 より前では、アイテム位置は double ではなく小数部 5 ビットの fixed-point で送信されるため、アイテム 1 個だけで RNG 状態をクラックするのは現実的でなく、別の測定戦略が必要になる
パッチ方法
- 簡単な方法は、RNG manipulation を無効化するパッチや設定を見つけることである
- 脆弱な実装では
World.setRandomSeed がグローバルな this.rand に seed を設定して返す
public Random setRandomSeed(int seedX, int seedY, int seedZ) {
this.rand.setSeed(seedX * 341873128712L + seedY * 132897987541L + seedZ + this.getWorldInfo().getSeed());
return this.rand;
}
- 完全な防御を望むなら、呼び出しごとに新しい
Random を返すよう変更できる
public Random setRandomSeed(int seedX, int seedY, int seedZ) {
return new Random(seedX * 341873128712L + seedY * 132897987541L + seedZ + this.getWorldInfo().getSeed());
}
n0pf0x の付録: 他の座標探索方式と The End
- n0pf0x は Mason 側の GPU 大規模ルックアップテーブルの代わりに キャッシュベース の座標探索を使っている
- hit が発生すると、その座標と周辺 radius 内の座標を
HashMap に入れる
- 第1パスでは RNG を逆に巻き戻しながら、キャッシュ hit または直前処理シードとの重複を高速に確認する
- 第2パスは第1パスが失敗した場合のみ実行され、前述の高コストな座標探索アルゴリズムを使う
- このキャッシュ方式には、もっともらしくない valid location を飛ばす効果があり、false positive の緩和に役立つ可能性がある
- The End ではチャンク初回生成時にしか RNG に影響しないため、Overworld のように base のチャンクロードを繰り返し観測することは難しい
- The End で頼れる状況は2つある
- base にいたプレイヤーが動き回って未生成チャンクを生成する場合
- base へ向かうプレイヤーが移動経路に新しいチャンク trail を作る場合
- trail は自動識別システムを作れるが、n0pf0x は実装しておらず、視覚的に手動追跡していた
- プレイヤー識別には End Occupancy Tracker(EOT) のアイデアを使う
- tick ごとの RNG 呼び出し数がロード済みチャンク数とある程度相関し、それがそのディメンション内のプレイヤー数につながるという仮定に基づく
- プレイヤー join/leave 直後に RNG 呼び出し数が急増または減少するかを見て、The End にいるプレイヤーを推定する
- EOT は 9b9t でのみテストされており、2b2t のような他サーバー条件では成り立たない可能性がある
- 毎 tick 安定して RNG をサンプリングできる必要がある
- The End のプレイヤー活動量がはるかに多い場合はさらに難しくなる可能性がある
1件のコメント
Hacker Newsのコメント
1999〜2000年に、じゃんけんのコンピュータボット同士が競う International RoShamBo Programming Competition があった [1]
基準ボットは理論上勝てない戦略であるランダム選択をしていたが、いたずら的な参加作の1つは乱数生成器の状態を逆算し、ランダムプレイヤーが出す手を100%の精度で予測するよう設計されていた
修正: そのボットはTim Dierksの「Nostradamus」で、第1回大会の「supermodified」部門の優勝者と宣言された [2]
[1] https://web.archive.org/web/20180719050311/http://webdocs.cs...
[2] https://groups.google.com/g/comp.ai.games/c/qvJqOLOg-oc
実際にはセキュリティ分野に進み、TLS RFCを書き、Googleのセキュリティ部門のプリンシパルエンジニアになった。思い出させてくれてありがとう
https://web.archive.org/web/20180719050236/http://webdocs.cs...
Nostradamusは暗号分野の専門性が高いCerticomのエンジニアリングVPであるTim Dierksが作成し、
random()生成器の内部状態をリバースエンジニアリングして最適プレイヤーに勝った。本人いわく「思ったより簡単でもあり、難しくもあった」とのこと。それでもスポーツマンシップの観点から、他のすべての相手には最適にプレイしたFork BotはDan Egnorが大会の話を聞いて数分後に思いついたアイデアから生まれ、「ライブラリルーチンの使用可」を利用して
fork()で3つのプロセスを起動し、それぞれに別の手を出させ、負けた2つを殺す方式だった。Andreas Junghannsが約10行のコードで実装したが、最初のターン後に3つの手がすべてPsychic Friends Networkに負けてプログラムが終了し、残りの試合は棄権扱いになったPsychic Friends Network はMichael SchatzとRST Corporationの人たちが書いた本当に笑える難読化Cコードで、良いカルマを探す補助関数、星占い相談、スパゲッティと神秘的なピザ料理、
#defineで民主党員を共産主義者にすること、神をundefすることなどを行う。スタックフレームで正確に何をしているのかはまだ解析中だが、メタ・メタ・チーターを相手にしない限り、1試合で+998未満を記録することはないThe Matrixは「Student for Life」という権威ある肩書きを持つDarse Billingsが作成し、「スプーンは存在しない」という単純な原則で全ての相手に満点で勝った
The Matrixはトーナメントプログラムでもあり、他のすべてのアルゴリズム、データ構造、出力ルーチンに完全にアクセスできるため、今後も追い越される可能性は低い。したがってこの部門は解決済みと宣言され、以後の大会では廃止された
結局、その透明性のおかげでセキュリティは改善された
LLL格子基底簡約は、数日前のCVEで偏ったnonceを使ってPuTTYの鍵を破るのにも使われた、まさにそのアルゴリズムだ
tptacekが攻撃を少し説明していて、目を細めればほぼ理解したふりができるcryptopalsの問題にもリンクしている https://news.ycombinator.com/item?id=40045377同じように、SciCraftのMinecraftサーバーには、何か黒魔術のような装置で乱数生成器の状態を決定論的に操作し、毎フレーム特定のブロックに「ランダムな」雷が落ちるようにしてクリーパーのドロップを良くするクリーパー農場があった https://youtu.be/TM7SutJyDCk
3つのケースすべてで必要なのは基礎的な線形代数程度で、それすら多くはない。Kelbyはグラム・シュミットを理解していることを期待しているが、これは学部の線形代数入門の中間試験直前あたりに出てくる内容だ
この記事がどれほど素晴らしいか、言葉では表せない。今週を良いものにしてくれた
後でPythonで追ってみられる、同じ過程のとても簡潔な説明:
https://crypto.stackexchange.com/questions/37836/problem-wit...
https://youtu.be/ZcdN1wCJPqM?t=390
朝起きたら、前の晩にはなかったブロックが空に浮かんでいて、最初は幽霊みたいに霧のように見えたものが、すぐにレッドストーン、オブザーバー、スライムブロックだと分かり、無限TNT落下が見える状況を想像した
サーバーが自分の位置を漏らしたというだけで、そうなる。それでもまだ逃げることはできるし、チェストから必要なものを数秒で取り出して逃げたり、黒曜石の避難所を作ったりする時間はあるかもしれない。でもそれがすべて
精密照準の大砲を作る時間は足りないし、どうせ高度も合わせられない。エリトラとロケットがあれば妨害しに行けるかもしれないが、すぐ16チャンク先には巨大なワールドイーターの穴がある。近くのネザーポータルごとに溶岩トラップを仕掛けてあるのだろうか?
興味深くて笑える乱数生成器問題はいろいろ見てきたが、これは得られるものに比べて最も精巧なエクスプロイトの一つだ。見事な芸術作品のようだ
かなりすごいエクスプロイトだ
バグ悪用が自由なサーバーというアイデアも悪くないし、ゲームのまったく別の段階のように見える
メタバースが実際に起きるなら、単にゲーム内の戦闘メカニクスを使うのではなく、「本当に戦う」というのはこういう姿になりそうだ
かなり昔から複製された高価なアイテムが大量にあるため、PvPは壊すと莫大なダメージを与えるエンドクリスタルをばらまき続ける方式になり、防御は致命的なダメージを吸収する「不死のトーテム」をどれだけ持っているかになった
当然、ハッククライアントはエンドクリスタルの設置、トーテムの再装填、弱い位置と強い位置の識別を自動化し、プレイヤーはその指示に従ってダメージを入れ続ける
その少し前には、サーバーでパッチされるまで即死させる+32,767ダメージのハック剣もあった
ARK: Survival EvolvedとEve Onlineは、数千人規模の巨大クランがメタゲームやバグ悪用に極端に走ることで悪名高い
常にロマンチックなものではない。ARKには、プレイヤーとその複数のSteamアカウントを晒し上げられる仕組みがあり、Great Warの最中にはゲーム内の関係が現実に波及した事例も一部あったようだ
ときには非常に基本的な手口もある。たとえば襲撃を受けたときに巨大な塔を建てて崩し、サーバーにサービス拒否を起こしてクラッシュさせると、サーバーが10〜20分前のバックアップにロールバックされ、活動中のプレイヤーがいる拠点は襲撃するのが非常に難しくなった。かなり古い手口で、何年も前に修正された
Rustには、バグやエクスプロイトをYouTubeで広めて公開することを奨励する方針もあったが、目的は別だった。開発者がより早く気づいてパッチできるようにするためだった。その結果、実際の外部チートを使わなければ悪用がかなり難しい、相当に堅牢なゲームになった
ただし、通常のコントローラーで手動で悪用できるものだけで、実際のハッキングではない。Wobblingというエクスプロイトの一つは2019年に禁止されたが、このゲームは2001年のゲームだ
この話題の動画をさっき見た。ランダム性の源同士が相互作用するときの危険を示す、確かな警告事例で、重要なシステムにも多く当てはまる
性能のためにコード内で乱数生成器を共有することは多いが、こういう話を見ると確かに立ち止まって考えさせられる
更新される地点を十分に観測しにくくなるはずだと思ったからだ。ところがこの事例は、その直感が間違っていたことをかなり印象的かつ面白く示している
この動画がすごい: https://www.youtube.com/watch?v=maMpMOnIJDE
コミュニティがここまで精巧だとは知らなかった
これに驚いたなら、これも見てほしい。頭が吹き飛ぶはずだ
https://www.youtube.com/watch?v=ea6py9q46QU
そして
https://www.youtube.com/watch?v=GaRurhiK-Lk
さらに言えば、この種の乱数生成器クラッキングはゲーム内でも実装されたことがある
https://youtu.be/FPmQ0rnJjNc?si=tTFObcfZ-ILanL_A
驚くことに、Minecraft自体の中に作られた機械であるMess Detectorがある。ブロックのドロップではなく、点火されたTNTの位置を使って乱数生成器の内部状態を予測する
https://www.youtube.com/watch?v=FPmQ0rnJjNc
これは状態侵害拡張攻撃のように見える (https://en.wikipedia.org/wiki/Random_number_generator_attack)
暗号学的に安全な疑似乱数生成器(CSPRNG)ではないPRNGが受けうる攻撃だ
今となっては、ライブラリがPRNGをデフォルトで提供すること自体、あまり安全ではないように感じる。2024年にTLSv1.0やblowfishをデフォルトで許可するのに似ている