ゲーム(Undertale)のメモリを覗いてHP値を監視する

Table of Contents

【追記】この記事はクソだ

http://cheatwhatever.com/blog-entry-20.html
Cheat Engineには、下記手順を自動でやってくれるポインタスキャン機能がある。
あくまで以下の内容は「こういう事をやってるんだ、へー」程度にとどめてほしい。

がいよう

エミュレータで動作するゲームと違い、PCゲームのチートは読み出したいメモリアドレスが一意である保証は無い。
これはただ一度だけ数値を書き換えたい場合と違い、「常にHPの値を監視して変動があったか判定する」ようなケースで問題になる。

今回はPCゲーム「Undertale」を例に、この問題を解決する。
具体的には、

  1. HPを指す動的なメモリを指す、静的なメモリを見つける
  2. 別Processのメモリを読み出すようなプログラムをC++で書く

の2ステップだ。

Cheat Engineでメモリを特定する

HPのメモリを探す

兎にも角にも、まずはHPを指すメモリを探してみよう。
Undertaleを戦闘画面で止めておき、Cheat EngineでHPの値を検索する。

file

この時Value TypeをAll、Fast Scanをオフにしておくのがコツだ。
HPの値が何型なのかはこの段階でわかっていないし、Fast Scanはキリの悪いメモリ番地を読み飛ばす機能なので最初はオフにしたほうが良い。

この段階でダメージを受け、Next Scanをかけると…

file

いとも簡単に特定できた。
念の為更にダメージを受けて、19AC4B40:d がHPのアドレスで確定だ。
:dというのはまぁあんま気にしなくていい(あてにならないので)。

HPのメモリへのポインタを探す

今、丁度メモリアドレスが緑字と黒字で表示されているが、緑のものはStatic Addressであるという意味だ。
つまり、緑字の変数は起動し直してもアドレスが変わらない事を意味する。

一方今見つけ出したHPメモリは黒字で書いてあるので、動的アドレスであることが分かる。
ゲームを再起動、どころか作業に手間取るだけでアドレスの番地が変更されうるわけだ。

じゃあ何でプログラムはHPの値を知ってるのかと言うと、このメモリアドレスを保存している静的アドレスが何処かにいるからだ。
ただし、19AC4B40という値を直接保存しているポインタが存在する保証は無い。

Cの配列や構造体は、アラインメントの概念があるものの、基本的に連続な領域に変数が確保される。

つまりある変数に対し、配列or構造体の先頭アドレス+ハードコードされたオフセット値 でアクセスされてる可能性があるわけだ。

というわけで、これから「19AC4B40という値を保持しているポインタ」を探しに行く。

file

「HEX」にチェックを入れた上でNew Scan。5つ程候補が出てきたが、ゲーム画面をアクティブにした瞬間一つを除いて0になった。

HPメモリへのポインタは07C00EE8で確定…しかし、これも動的アドレスのようだ。
もう一度、07C00EE8でNew Scanしてみると…該当無し。

これが上記の、「アドレス+オフセット値でアクセスされているケース」だ。

実装としては、07C00EE8が配列の10番目とかに配置されてるケースを想像してみて欲しい。
その時arr[10]としてアクセスする時の機械語は、「arrの先頭番地+ポインタ10個分」の番地に飛ぶはずだ。
この「arrの先頭番地」「ポインタn個分」がそれぞれ欲しいわけだ。

アセンブラ完全に理解した

アドレス一覧から07C00EE8を右クリックし、「Find out what accesses this address」を選んで、このアドレスに触りに行ったアセンブラを監視するモードに入る。

するとすぐに3つの候補が見つかった。
ダブルクリックして内訳を見てみると、

file

the value of the pointer needed to find this address is probably 07C00590(このポインタの値を算出するのに、07C00590を使ってるかも) というありがたい表現が見つかる。
他の2つは「255」「19AC~」でなんか直感的に違う気がする(255というのはポインタらしくない数字だし、後者はHPのメモリなので既出の情報だ)のでパス。

ここでアセンブラを少し読んで見る。
mov A [B]は、Aの値をBのアドレスにぶっこむという意味なので、「edi+ecx*4+08」はメモリアドレスということになる。

更に、この値が07C00EE8になるということで、計算すると
7c00EE8 – 7c00590 = 0x958
がHPへの参照の参照という事になる。この0x958をメモしておく
ちなみに下にレジスタの値が出てるが、これはひとしきり計算が終わった後の値なので話半分に聞くべきである。ecx*4+08を真面目に計算したりしてはいけない(一敗)。

同じ手順で、7c00EE8への参照を探していく。
ぐだぐだしているとメモリアドレスが変わっちまうので、手早く作業していこう。
最終的に、下記の結果が得られる。

p0=静的アドレス:0x00808950の値

p1=p0 + 0x44

p2=p1 + 0x10

p3=p2 +0x958

p4=HPメモリへのポインタ

HPメモリ

ここで、「静的アドレスと各オフセットの値」は変わらないのだった。
よって、0x00808950から毎回ポインタを辿れば、再起動してもHPメモリへ干渉できるわけだ。

実際にコード書く

他processへの干渉はそんなに難しくない。
下記が最も参考になる。
http://peryaudo.hatenablog.com/entry/20100516/1273998518

記事によっては、「プロセスのベースアドレスが必要」との記述を見かけるが、そんな事はない。
そのあたりをよしなにやってくれるのがWinAPIだからだ。
よってReadProcessMemory関数を動かせる所までは省略する。

ポインタを辿る

この手の低級なコードはC++が強い。
以下ソースコードで、説明はコメントに譲る。

// undertaleからHPを引っこ抜く
int getHp(HANDLE hProcess) {
    BYTE buf[8];
    unsigned __int64 offset[4] = { 0x00808950,0x44,0x10,0x958 };
    // ポインタ4枚分貫通した先に実数値がある
    // ポインタは4バイト、実数値は8バイトで読まないといけないので
    // まずポインタだけめくっちゃう   

    // 今現在のポインタ
    unsigned __int64 temp = 0;
    for (int i = 0; i < 4; i++) {
        if (ReadProcessMemory(hProcess, (LPCVOID)(temp + offset[i]), (LPVOID)buf, 4, NULL)) {
            // 引っこ抜いてきた値を組み立てる
            // 1バイトずつ逆に入ってる(最上位1バイトが[3]におる)故
            temp = (buf[3] << 24) | (buf[2] << 16) | (buf[1] << 8) | (buf[0] << 0);
            //std::cout << "次→" << std::hex << temp << '\n';
        }
    }

    // 最後に8バイト抜いてDoubleキャスト
    // キャストと言いつつ実際はunion使ってやるだけ
    union { double val; unsigned __int64 bytes; } p;
    p.bytes = 0;

    if (ReadProcessMemory(hProcess, (LPCVOID)temp, (LPVOID)buf, 8, NULL)) {
        for (int i = 0; i < 8; i++) {
            // 流石に8個の配列逆順にするの手書きはつらい。
            p.bytes |= (unsigned __int64)buf[i] << (8 * i);
        }
        //std::cout << p.val << "\n";
    }
    return (int)p.val;
}

とまぁこんな具合。これでPCゲームのHPを監視出来るぞ。

PCゲームのHPを監視してどうするかは言わん。言わんからな!!

カテゴリー: IT パーマリンク

ゲーム(Undertale)のメモリを覗いてHP値を監視する への2件のフィードバック

  1. 匿名 のコメント:

    アンテ実況配信みてました。次回アンダイン戦のようですけど、Gルートのアンダインはほんとにつよいので何回もHPが0になります。腕が焼き切れるかもしれないので、そこそこのところで切り上げることを強くお勧めします。何かあっても視聴者たちは救急車を呼びたくても呼べないので、、

    • drroot のコメント:

      魚人野郎は何とかなったけどSANSはなまらひどい目にあったよ!!!!!!!!!!!!!

      だが俺の勝ちだ
      観てくれてありがとうな

匿名 へ返信する コメントをキャンセル

メールアドレスが公開されることはありません。 が付いている欄は必須項目です