pop↑ push↓

☆ (ゝω・)v

メモリ書き換え メモ その1

前回の記事

実行したexe(PEフォーマット)が展開されたメモリイメージから特定のバイトパターンを検索して、そこのアドレスを見つけることができた。 popush.hatenablog.com

今回の記事

指定のアドレスのメモリイメージを書き換える。

injector

DLLはメモリイメージを書き換えるのに下記(以下injector)を使用している。

EU4dll/include/injector at master · matanki-saito/EU4dll · GitHub

injectorのCopyrightを見ると、作者はLINK/2012氏のようだ。そのものは見つけられなかったが、GTAに関するコードがあったので下記が元だと思う。

github.com

EU4Dllはinjectorの下記3つの関数だけを利用している。

x64でビルドが通るようにする

injectorにはインラインアセンブラと_declspec(naked)が入っているため、x64では動かない。さらにM_IX86マクロ(参考1)ガードがかかっているため、コンパイルもできない。static_assertでサイズもチェックしているという念の入れようである。

EU4dll/assembly.hpp at master · matanki-saito/EU4dll · GitHub

アセンブラコードはファイルにあるMakeInline関数を使うためだけに必要なのでここでは無理やりビルドできるようにする。

  • asmブロックごと削除する
  • __declspec(naked)を消しておく。
  • マクロは_M_X64などとしておく。
  • static_assertは適当にコメントアウトするか4を8にする。

これでx64でビルドが通るようになる。これらのコードの解説とx64化は次回があればその時に行う。ただしこのままではMakeJMPは正しく動かないので次章以降で修正する。

ターゲットプログラムを作る

HOI4でinjectorの実験をするのは大変なので、MFCでx64の適当なターゲットプログラムを作った。ボタンを押すとテキストエリアにテキストが入る。injectorを使って別のテキストが入るようにする予定。 f:id:saito-matanki:20190901031125p:plain

f:id:saito-matanki:20190901031744p:plain

InitInstanceにLoadLibraryを入れてd3d9.dllを使ったDLL injectionをできるようにした。

f:id:saito-matanki:20190901031930p:plain

作ったプログラムを解析して、inject位置を確認しておく。 f:id:saito-matanki:20190901032514p:plain

WriteMemoryをつかう

WriteMemory関数を使ってテキスト(ボタン1)そのものを書き換える。テキストそのものは.rdataセグメントにUTF-16 LEとして置いてある。 f:id:saito-matanki:20190901033241p:plain

find_patternは.rdataも検索可能なので、マッチさせて開始アドレスを割り出しておく。WriteMemoryは構造体をそのまま書き込むため、TCHARが入ったものを用意しておく。あとはinjector::WriteMemoryのテンプレートに構造体、書き込み対象の開始アドレスとデータを引数に渡せばそのまま書き込まれる。

f:id:saito-matanki:20190901034111p:plain

ボタン1を押すと、テキストが変更されていることがわかる。

f:id:saito-matanki:20190901040514p:plain

WriteMemoryの仕組み

WriteMemoryはWriteObjectのWrapperになっている。

EU4dll/injector.hpp at cebec8e139b69d3fe0b4e4c461ffb541f36960e9 · matanki-saito/EU4dll · GitHub

3番目の引数の値によって、処理開始時にscoped_unprotectのコンストラクタがVirtualProtect(参考2)を使って対象のアドレス領域の書き込み権限が変更される。この変更は一時的でデストラクタによって元に戻る

EU4dll/injector.hpp at cebec8e139b69d3fe0b4e4c461ffb541f36960e9 · matanki-saito/EU4dll · GitHub

書き込み対象のアドレスはmemory_pointer_tr(union)でラップされて受け取られ、それがget()されるときにさらにauto_pointer(union)でラップされる。

EU4dll/injector.hpp at master · matanki-saito/EU4dll · GitHub

最終的には前回と同じようにアドレスはテンプレートでreinterpret_castされて、そのアドレスを開始位置としたテンプレートの変数になる。

EU4dll/injector.hpp at cebec8e139b69d3fe0b4e4c461ffb541f36960e9 · matanki-saito/EU4dll · GitHub

MakeJMPをつかう

MakeJMPを使うと、指定のアドレスにjmp命令を入れることができる。jmpを使って処理をDLLに移してretで戻せば処理をバイパスできる。 f:id:saito-matanki:20190901041031p:plain

WriteMemoryと同じくパターンから開始アドレスを見つけておき、戻りたいアドレス(開始アドレスから+14したところ)を用意しておく。さらにasmでバイパスした先の処理を作っておき、テキストも用意しておく。 f:id:saito-matanki:20190901041409p:plain f:id:saito-matanki:20190901041449p:plain

MakeJMPにそれらを渡せば上記のバイパスが実現できる。

f:id:saito-matanki:20190901041612p:plain

ボタン2を押すと、テキストが変更されていることがわかる。

f:id:saito-matanki:20190901041657p:plain

MakeJMPの仕組み

仕組みは単純で、最初に解説したWriteMemoryでアドレスにオペコードとしてJMP命令(参考3のE9 cdの相対ニアジャンプ。cd等の意味は参考4を参照。もっと詳細は参照5の3.1.1.1章を参照)を書き込み、

EU4dll/injector.hpp at cebec8e139b69d3fe0b4e4c461ffb541f36960e9 · matanki-saito/EU4dll · GitHub

jmp元アドレス(jmp命令が終了したアドレス。RIP。開始アドレスにオペコードの1バイトとオペランドの4バイトを足したもの)からjmp先のアドレスまでのオフセットをオペランドとして書き込んでいるだけである。

EU4dll/injector.hpp at cebec8e139b69d3fe0b4e4c461ffb541f36960e9 · matanki-saito/EU4dll · GitHub

MakeJMPをx64に対応させる

オペコードE9を使った相対ニアジャンプはオフセットが32bitまでのため、jmp元とjmp先が32bitを超えて離れていた場合は使えない場合がある。そのような場合のため、ジャンプ先のアドレスをレジスタかメモリ(r/m)で64bitを直接指定するオペコード(FF /4)を使うように変更する。

f:id:saito-matanki:20190901050506p:plain

ここで命令バイト列全体を確認する(参考6より引用)。

f:id:saito-matanki:20190901045227p:plain

オペコードにある /4というのはdigitであって(参考6)、上記のModR/Mにある3~5bitまでのReg/Opcodeである。/4なので100bになる。

f:id:saito-matanki:20190901045749p:plain

次に残りのModとR/Mを決める必要があるが、これは参考資料7の表を見るのが早い(下図参照)。今レジスタではなくメモリで指定したいので、[RIP + disp32]を選び、r/mに101b、modに00bが決定する。ModR/M全体だと00-100-101で0x25になる。

f:id:saito-matanki:20190901050353p:plain

32bit分のdisplacementでRIPからjmp先のアドレスが入っているメモリ位置までのオフセットを決めるが、これは命令のすぐ後ろにjmp先のアドレスを置くため、0にしておく。

以上をMakeJMPで書くと下記のようなコードになる。

f:id:saito-matanki:20190901051430p:plain

MakeCALL

上記のようにすぐ後ろに戻ってくるのであれば、それはjmpではなくてcall(参考8)がふさわしい。MakeCALLを使う。 EU4dll/injector.hpp at cebec8e139b69d3fe0b4e4c461ffb541f36960e9 · matanki-saito/EU4dll · GitHub

MakeCALLもMakeJMPと同じようにx64のための修正が必要になる。

f:id:saito-matanki:20190902011441p:plain

注意点があり上記を使うと、call命令の後ろ、つまりcall先アドレスが入っている場所に戻ってきてしまう。したがってアセンブラ側で戻り先アドレスを修正をする必要がある。下記の例ではxchgでraxに戻り先アドレスを入れて、8バイト進めた場所に戻るようにしている。

f:id:saito-matanki:20190902011909p:plain

参考

  1. 定義済みマクロ | Microsoft Docs
  2. API 関数解説
  3. JMP — Jump
  4. Assembly Programming on x86-64 Linux (06)
  5. Intel® 64 and IA-32 Architectures Software Developer Manuals | Intel® Software
  6. 命令エンコード - panda's tech note
  7. ModR/M ‐ 通信用語の基礎知識
  8. CALL — Call Procedure