チート技術2-64bitゲームのチート
前回は自作のMemProgというアプリを使って32bitゲームのメモリ書き換えを行いましたが、今回はうさみみハリケーンというアプリを使用して64bitゲームの書き換え・解析を行ってみたいと思います
まずは実演
うさみみハリケーンというソフトをダウンロードし、UsaMimi64.exeを起動します
プロセス一覧が表示されるので、起動中のゲームを選択します
今回のソンビを銃で撃つ某ホラーゲームを題材にします
銃弾を常にマックスにするチートをやってみましょう
ゲーム画面に移ります
このゲームはNewGameで始めると、しばらくデータを正常にサーチできないため、最初のゾンビに殺されるか一度セーブしてからロードし直しましょう
銃を構え、残りの弾丸数を確認します(ここでは残り9発とします)
Alt+TABでアプリを切り替えます(うさみみハリケーンを起動するときはWindows+Dを押してデスクトップを出します)
メニュー → 検索 → メモリ範囲を選択して検索 を選びます
サーチ画面が出てくるので、
①検索領域タイプをComit-ReadWriteに変更
②データのサイズを4Bytes(DWord)に指定(初期)
③検索する数値を入力(残り銃弾が9発の場合、9を入力)
そして、通常検索実行ボタンを押します
64bit版のアプリのメモリ検索は時間が掛かります(32bitアプリの約32000倍の広さのため)
ただ、使われていない領域は飛ばして検索されるので、かかる時間は数倍程度です
約16万件のデータが見つかりました
この中から目的データを見つけるのは困難なので、検索を繰り返してアドレスを絞り込みます
ゲームに戻って銃を撃ち、残り弾丸を8発にします
うさみみハリケーンの検索画面に切り替え、8を検索します
2回目以降(絞り込み)の検索は高速に行われます
一気に13件まで絞り込めました
もう一度、残り銃弾を7発にしてから検索します
残り1件に絞り込むことができました
検索結果をダブルクリックして、メモリエディタに飛びましょう
これは0218’F0CF0F90という番地(アドレス)に07という数値が書き込まれているという意味になります
1つの値に4Byte使われているため、07 00 00 00が銃弾数を表してます
これはビッグエンディアンという表記法なので、大きい桁ほど後ろにあります
この4Byteを16進数で表すと0x00000007です(00 00 00 07と並び替えてからくっつける)
ビッグエンディアンは1Byte,2Byte,4Byteとデータサイズが変わっても同じ番地で表現できるというメリットがあります
この7のところを9に書き換えて、ゲームに戻ってみましょう
残り銃弾が9発になっているのを確認できるはずです
改造コード
次に改造コードを使って、銃弾が減らないようにしてみましょう
メニュー → 編集 → 改造コード実行 を選択
改造コード実行画面が開きます
0218’F0CF0F90-09と入力し、自動更新にチェック、100ミリ秒に設定し、コード実行を押します。
ゲームに戻ると、いくら銃を撃っても弾が減らなくなっているはずです
コードの内容は説明不要だと思いますが、0218’F0CF0F90番地に09を書き込むという意味になります
問題点
この方法には問題点があって、改造コードの有効期限がキャラクターをプレイしている間だけになります。
セーブデータをロードしなおしたり、操作キャラクターが切り替わったりすると、データの番地が変わってしまい、改造コードが無効になってしまいます。(最悪の場合、他のデータを破壊する)
そのため自作の無限弾丸MODなどを作るときは、ロードしても変わらない番地に書き込まれたアドレスから、何層かアドレスを追跡して、データの番地を特定する必要があります。
アドレス追跡
番地が変わらない固定アドレスを探してみましょう
メモリエディタで先ほどの結果を眺めてみます
0218’F0CF0F90が残り弾丸のアドレスです
キャラクターのステータスや、アイテムデータなどは、この画像のように00 が多く並んでいる場合が多いです。(4Byte使って表現する値が0~9999程度なので)
データはある程度の変数をひとかたまりにした構造体という形で管理されています。
残り弾丸数のアドレス0218’F0CF0F90の右上の0218’F0CF0F84には銃の種類、その隣の0218’F0CF0F88には改造パーツ、その隣は弾丸の種類となっていて、1つのアイテムに関するデータがひとかたまりになっています。
このような構造体が一定のサイズで周期的に繰り返していることも多いです。(構造体配列)
このゲームはアドレスで数珠つなぎになっているので、その例には当たりませんので、アドレス追跡という形でデータを解析してみましょう。
データの癖から、構造体の先頭を見つけます。(これが結構難しい)
一番簡単な方法は、アドレスを順番に検索にかけて検索に掛かればそこが先頭と分かります。
うさみみハリケーンのQWord検索で0x0218F0CF0F90を検索、 0x0218F0CF0F80を検索、 0x0218F0CF0F70を検索としていくと、0218’F0CF0F70が検索に掛かりました。
掛かった場所は0218’F0CF0F20と、すぐ真上にあるデータでした。
0218’F0CF0F20番地には70 0F CF F0 18 02 00 00とあります
これは先ほど説明したビッグエンディアン表記なので並び替えると00000218F0CF0F70となるのが分かります
このように、データとして次のデータへのアドレスが書き込まれているものをポインタと呼びます。
C言語で言うところのポインタと同じものです。
C言語でint64 *addr = 0x218F0CF0F70とあった場合、addrのアドレスが218’F0CF0F20にあって、内容が218’F0CF0F70というわけです
(64bitプログラムというのは8Byteを使ってアドレスを管理するという意味)
固定アドレスを見つけるという作業は、動的に確保されていないローカルの変数を見つける作業になります
GameData *data = new GameData();
とあった場合、ゲームデータは起動するたびにランダムなアドレスになりますが、ゲームデータを入れるポインタ変数dataのアドレスはローカル変数のためアドレスが変わりません
ただ、この変数が動的に確保されるクラスのメンバー変数である場合も多いので、その場合はクラスの階層を何層か辿る必要があります。
アドレス検索でメンバー変数(ポインタ)を見つけて、データの特徴から構造体(クラス)の先頭を特定、そのアドレスを検索といった手順を繰り返します
そして、再起動しても番地が変わらない領域まで辿り着ければ追跡完了です。
このゲームの場合、7FF6’67E10F30が固定アドレスとして見つかりました
(データ階層が思ったより深くて、数時間掛かりましたorz)
.exeファイルのリージョンと同じ、7FF6~から始まる領域が固定アドレスである可能性が高いです
正順で追跡してみる
アドレスが変わらない番地が見つかったので、そこから正順で残り弾丸のデータまで辿ってみます(固定アドレスなので、誰でも再現できます。ゲームを再起動しても大丈夫です)
メニュー → 移動 → 表示アドレスを指定を選択
7FF6’67E10F30にジャンプ
7FF6’67E10F30 に書き込まれているアドレスを選択しポインタジャンプ
(編集 → 選択アドレスをポインタとみなしてアドレス変更 or Ctrl + P)
そこから+0xC8の位置にあるデータにポインタジャンプ(下に12行, 右に8Byte)
さらに+0x38の位置にあるデータにポインタジャンプ(下に3行, 右に8Byte)
さらに+0x10の位置にあるデータにポインタジャンプ
さらに+0x20の位置にあるデータにポインタジャンプ(2キャラ目主人公の場合、0x28)
さらに+0x18の位置にあるデータにポインタジャンプ
さらに+0x10の位置にあるデータにポインタジャンプ
そこがインベントリデータの位置になります。
+0x1Cの位置にはインベントリ数を表す0x14(20)が書き込まれていて
+0x20からは各アイテムへのアドレスが20個連続で続いています
5番目のアドレス0x0218F0CF0F10にポインタジャンプすると、先程の残り弾数のアイテムエリアにたどり着くことができました
あとは、この手順でメモリを書き換えるプログラムを作成すれば、起動するだけで無限弾丸にするMODができます
自作プログラムの抜粋
CProcesMemoryというメモリを読み書きするクラスを作成しm_pmとして使っています
m_pm.Attach(“**.exe”);
m_pm.Read();
とアクセスするだけの簡単なクラスです。
プロセスメモリにアクセスする手順は結構面倒くさいのですが、こうやってクラス化してしまえば、簡単に使い回せます。(これがプログラミングの楽しいところ)
またC++の良いところは、型を自由変換できるところです。DWORD64型の変数をバイト配列に見立てて8Byte読み込むなんてことが簡単にできます。これもビッグエンディアン構造のメリットですね。
>>32(ビットシフト)のところは上位8バイトの値を現在のリージョンの上位8バイトと比較してポインタであるかどうかを判定してます
プログラムを暴走させないために、異常値を見つけたらすぐに終了するようしなければいけません
メモリトラッカーの作成
今回はポインタ追跡に何時間も掛かってしまいました
この作業はある程度パターンが決まっており、うまくいけば自動化できるんじゃないかと思い、ポインタを追跡するプログラムを作ってみることにしました
データの特徴を見抜いたりするのは人間にしかできませんが、マシンスペックを使って全ポインタを列挙してしまえば、データの先頭位置を特定するのは容易です。
幸い64bitプログラムだと上位バイトを比較することで、ポインタかどうかを見分けることができます
完成したプログラムがこちら
検索アドレスと同一リージョン内のポインタを全て列挙し、std::setで管理(数百MBのメモリ飽きが必要)
lower_boundで次の先頭ポインタを高速に見つけ、1つ戻すことで現在データの先頭ポインタを特定します
その繰り返しを再帰的に行うことで、アドレス階層を列挙します
参照も辿ってしまうので16階層で打ち切るようにしています(スタックオーバーフロー対策)
列挙された先頭アドレスのうち、Nestの浅い順にうさみみハリケーンで検索して7F~から始まるアドレスが引っかかればゴールです
今回は8階層目の0x218f1ae3500が 7FF6’67E10F30番地に引っかかりました
アドレスを選択し、追跡ボタンを押すと目的データまでの経路が表示されます。
これでアドレス追跡作業から開放される・・はず
おわりに
今回は64bitゲームのチートからデータ解析、チートツールが出来るまでの流れを簡単に解説しました。
出来上がったツールをネットに公開すると法に触れる可能性があるので、あくまでオフライン限定で個人で楽しむ程度に留めてください。
他者のプレイに影響を与えるゲームでは絶対に行ってはいけません。
チートはあくまで技術であり、ゲームを壊すも楽しいものに変えるも使い方次第です。(料理で言うところの調味料みたいな感じでしょうか)
今回のゲームで例えると、クリア特典でいろんな無限武器があるけど、どうして無限マグナムが無いんだ、あったら楽しいだろうな、みたいなのを実現してくれます。
プレイした感想も、簡単になったからといってつまらなくなるわけではなく、ハクスラガンシューターのように爽快なプレイを楽しめました。
一度クリアしたゲームを、違うシチュエーションでプレイするというのは、まさに調味料ですね。
持っている技術を壊すために使う人をクラッカーと呼び、作るために使う人をハッカーと呼びます。
いつも作る側の人間でありたいです
最近のコメント