Signature scanで欲しい値があるメモリアドレスを見つける方法

Signature scan はゲームプログラムの特定のバイナリ値の並びを使ってメモリ検索を行うことで、そのプログラムが書き込んているアドレスを特定する方法です
 Signature は Array of Bite、AOB などと書かれていることもあります
Sig scanの概要


Signature scan を使用するメリット
asl では基本的にはメモリから必要な値を読み取ってタイマー操作の条件判定に利用しますが、欲しい値のアドレスはほとんどの場合はゲームを起動するたびに、ものによってはタイトル画面に戻るたびに変わってしまうため、アドレスが変わっても追跡できる検索方法が必要になります
 基本的な検索方法にポインタスキャンがあり、これは変わらないアドレスを探し出してそのアドレスを基準にして欲しい値のアドレスをたどる方法ですが、ゲームによってはポインタスキャンではうまくいかない場合があります

そこで Signature scan(以降、Sig scan)という検索方法を使えば、ポインタスキャンではうまくいかない場合でも欲しい値にたどり着くことができるかもしれません
 ポインタスキャンと比べてより多くの知識が必要で、asl の記述量が増えるため、ハードルは高いと思います
 が、ポインタスキャンでは失敗しても Sig scan なら成功する可能性があったり、ゲームのバージョンが変わっても asl の修正が不要になる可能性があったりと、苦労に見合うだけのメリットがあると思います

ここでは Cheat Engine を使用したクラスの構造解析機能を利用したポインタスキャンの知識があることを前提に説明します

英語の資料ですが、Short signature scanning introduction & guide は Sig scan の概要と基本的な手順についてとても分かり易く書かれているので、参考資料としておすすめです

1. 欲しいアドレスへのポインタを含む命令を探す
ポインタを含む命令を探す
Sig scan はプログラムの命令のバイト列を利用した検索方法であり、検索に使うための命令は自分で探し出す必要があります

クラスの構造解析機能を利用したポインタスキャンと同様の手順で、欲しい値が含まれるフィールドを探し出します
 ついでにそのフィールドのアドレスも調べておくと、検索に使用するバイト値を探し出すときに楽になる場合もあります
フィールドのアドレス

同じクラスに含まれるメソッド一覧の中からフィールドにアクセスしていそうなものに当たりを付けて、右クリックメニューから Jit(Just in time、実行時にコンパイルする)を選択します
 Awake() メソッドを優先的に探すと、フィールドへのアドレス・ポインタが見つかる可能性が高めな印象

Memory Viewer が開いてアセンブリ言語化されたメソッドが表示されるので、Bytes や Opcode、Comment を見て「アドレス・ポインタを表すバイト」を探します
 ポインタの場合は、Memory Viewer の下半分の部分を使って、右クリック Goto Address でポインタが示すアドレスをたどってみます
 どこがアドレス・ポインタなのか分からない場合は、こちらの資料の 0.1, 3.2b, 3.2c, 3.2d の辺りを参考にしてみてください

ポインタを含む命令を見つける
事前に探しておいたフィールドのアドレスが出てきたら次の段階に進むことができます
 ポインタを複数回経由して目的のフィールドのアドレスにたどり着く場合もあるので、ポインタが出てきたら全てたどってみる、くらいのつもりで根気よく

違っていたなら、アセンブリ言語化されたメソッドから次のアドレス・ポインタを探して、欲しいフィールドへのアドレスにつながっているか確認する作業を繰り返します

欲しいフィールドへのアドレス・ポインタが当たりを付けたメソッドに含まれていなかった場合は、他のメソッドを選んで同様にします

クラスに含まれる全てのメソッドを調べても欲しいフィールドへのアドレス・ポインタが見つからなかった場合は、そのクラスをフィールドメンバに持つ他のクラスを探して、そちらのクラスのメソッドを調べるとアドレスが見つかることがあります

2. Signature の作成
Signature の作成
探し出した命令を加工した、検索のための文字列が Signature です
 命令文に含まれるアドレス・ポインタの値は毎回変わる可能性の高い値なので、変わる可能性のある部分を ? に書き換えます
 ? は 16 進数 0 - f のどれか一つを表すワイルドカードです

バイト列をコピー
欲しいフィールドへのアドレス・ポインタを含むアセンブリ語の行から数行を範囲選択して、右クリック → Copy to clipboard → Bytes only を選び、バイト列をコピーします

メモ帳などに張り付けて、アドレス・ポインタの部分を ?? ?? ?? ?? のように書き換え、書き換えたバイト列をコピーします

Signature で検索
Cheat Engine 本体の検索で、Scan Type に Array of byte を選択し、一部を ?? に書き換えたバイト列を張り付けて検索します

検索結果が 1 件だけ表示されれば成功です

2 件以上ヒットした場合、次のような対策を行います
・バイト列の選択範囲を広げてみる
・アセンブリ言語から他の行を探しなおす

もし、同じゲームの別バージョンもあるならば、そちらのバージョンでも検索結果が 1 件だけになるかを確認しておくとより確実です

検索結果が 0 件になる場合、次のような対策を行います
・バイト列に含まれる「レジスタのオペランド」も ?? に書き換えてみる
・アセンブリ言語から他の行を探しなおす

どこがレジスタのオペランドなのか分からない場合は、こちらの資料の 0.1, 3.2b, 3.2c, 3.2d の辺りを参考にしてみてください

ここで作成した、バイト列の一部を ?? に書き換えた、検索で 1 件だけヒットする文字列が Signature です

3. Sig scan に対応した asl の記述
既存の asl を見ると、大きく分けてマルチスレッドを利用する・しないという 2 通りの書き方が存在しています
 マルチスレッドを利用する・しないことによるメリット・デメリットについては、私自身が理解できていないため、ここではおおまかな違いにしか触れません

どちらの場合でも、記述する基本的な内容は変わりません

まずは SigScanTarget(ポインタの先頭からのバイト数, Signature のバイト列) でスキャンターゲットを定義します
 第 1 引数は Signature に含まれるポインタの位置を先頭からのバイト数、先頭は 0x0 で、この例の場合は 0x1 バイト目なので 0x1 です

SignatureScanner.Scan(SigScanTarget) で Sig scan を行いポインタを見つけたら、そのポインタを渡して新しい MemoryWatcher<T>(ポインタ) を作って、update で MemoryWatcher の更新を行います
 <T> はその MemoryWatcher で取得しようとしている値の型で、ポインタを複数経由する場合は DeepPointer を利用します

ポインタが見つかった前提で動くので、ポインタが見つからなかった場合の記述も必要です
 特にマルチスレッドを利用する書き方の場合は、update で更新する際に「スレッドが終了していたら更新」するように注意が必要です

特別な意図がなければ、全ての MemoryWatcher を更新するために全ての MemoryWatcher.Update() を呼び出す必要があります
 MemoryWatcher がひとつふたつくらいなら個別に Update() を呼び出しても手間ではないのですが、数が多くなると大変になってきます

そこで MemoryWatcherList を利用します
 このリストに複数の MemoryWatcher を登録しておくと、MemoryWatcherList.UpdateAll() を呼び出すことで登録されたすべての MemoryWatcher.Update() を呼び出すことができます

MemoryWatcher の値を利用する際は Current プロパティか Old プロパティの値を取得するということに注意が必要です
 つまり、vars.MemoryWatcher.Current とか vars.MemoryWatcher.Old という書き方にする必要があるということで、私はこのプロパティの書き忘れを何度もやってしまっています

マルチスレッドを利用しない場合の例
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
startup
{
  // Signatureの定義
  vars.scanTarget = new SigScanTarget(0x1, 
    "B8 ?? ?? ?? ?? 89 38 C6 87 ?? ?? ?? ?? 01 8D 45 9C 89 04 24");
}

init
{
  // Sig scan
  IntPtr ptr = IntPtr.Zero;
  foreach (var page in game.MemoryPages(true).Reverse())
  {
    var scanner = new SignatureScanner(game, page.BaseAddress, (int)page.RegionSize);

    if (ptr == IntPtr.Zero && 
      (ptr = scanner.Scan(vars.scanTarget)) != IntPtr.Zero)
      print("ptr : " + ptr.ToString("x"));

    if (ptr != IntPtr.Zero)
      break; // ポインタが見つかったらbreak
  }

  if (ptr != IntPtr.Zero)
  {
    // ポインタが見つかっていればメモリウォッチャーを定義
    print("-- Sig scan in init success --");

    vars.Depth = new MemoryWatcher<int>(new DeepPointer(ptr, 0x0, 0x04));
    vars.Select = new MemoryWatcher<int>(new DeepPointer(ptr, 0x0, 0x08));

    // 必要に応じてメモリウォッチャーリストを定義
    vars.watchers = new MemoryWatcherList()
    {
      vars.Depth,
      vars.Select
    };
  }
  else
  {
    // ポインタが見つかっていなければ例外を投げる
    // スキャンが成功するまでinitが呼ばれ続ける
    Thread.Sleep(1000);
    throw new Exception("-- Sig scan in init fail --");
  }
}

update
{
  // メモリウォッチャー(リスト)の更新
  vars.watchers.UpdateAll(game);
}
マルチスレッドを利用しない場合は Sig scan が成功するまで init が繰り返し呼び出されます


マルチスレッドを利用する場合の例
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
startup
{
  // Signatureの定義
  vars.scanTarget = new SigScanTarget(0x1, 
    "B8 ?? ?? ?? ?? 89 38 33 F6 EB 27 8B C0");
}

init
{
  // Sig scan thread
  vars.tokenSource = new CancellationTokenSource();
  vars.token = vars.tokenSource.Token;
  vars.threadScan = new Thread(() =>
  {
    IntPtr ptr = IntPtr.Zero;

    while (!vars.token.IsCancellationRequested)
    {
      foreach (var page in game.MemoryPages())
      {
        var scanner = new SignatureScanner(game, page.BaseAddress, (int)page.RegionSize);

        if (ptr == IntPtr.Zero && 
          (ptr = scanner.Scan(vars.scanTarget)) != IntPtr.Zero)
          print("ptr : " + ptr.ToString("x"));

        if (ptr != IntPtr.Zero)
          break; // ポインタが見つかったらbreak
      }

      if (ptr != IntPtr.Zero)
      {
        // ポインタが見つかっていればメモリウォッチャーを定義
        vars.nowGameOver = new MemoryWatcher<bool>(new DeepPointer(ptr, 0x0, 0x89));
        vars.nowPause = new MemoryWatcher<bool>(new DeepPointer(ptr, 0x0, 0x8a));

        // 必要に応じてメモリウォッチャーリストを定義
        vars.watchers = new MemoryWatcherList()
        {
          vars.nowGameOver,
          vars.nowPause
        };
        print("-- Sig scan in thread done --");
        break; // while
      }
      // ポインタが見つかっていなければ一定時間休んで再スキャン
      Thread.Sleep(1000);
    }
    print("-- Exit thread scan --");
  });
  vars.threadScan.Start();
}

update
{
  if (!vars.threadScan.IsAlive)
    vars.watchers.UpdateAll(game); // スレッドが終了していれば更新
}

exit
{
  vars.tokenSource.Cancel();
}
shutdown
{
  vars.tokenSource.Cancel();
}
マルチスレッドを利用する場合は、スキャンが失敗していても init 以降の処理が実行されます


ここから先はポインタスキャンを使った場合などと同様、普通に asl を書くだけです

0 件のコメント:

コメントを投稿