メモリ書き換え メモ その1
- 前回の記事
- 今回の記事
- injector
- x64でビルドが通るようにする
- ターゲットプログラムを作る
- WriteMemoryをつかう
- WriteMemoryの仕組み
- MakeJMPをつかう
- MakeJMPの仕組み
- MakeJMPをx64に対応させる
- MakeCALL
- 参考
前回の記事
実行したexe(PEフォーマット)が展開されたメモリイメージから特定のバイトパターンを検索して、そこのアドレスを見つけることができた。 popush.hatenablog.com
今回の記事
指定のアドレスのメモリイメージを書き換える。
injector
DLLはメモリイメージを書き換えるのに下記(以下injector)を使用している。
EU4dll/include/injector at master · matanki-saito/EU4dll · GitHub
injectorのCopyrightを見ると、作者はLINK/2012氏のようだ。そのものは見つけられなかったが、GTAに関するコードがあったので下記が元だと思う。
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を使って別のテキストが入るようにする予定。
InitInstanceにLoadLibraryを入れてd3d9.dllを使ったDLL injectionをできるようにした。
作ったプログラムを解析して、inject位置を確認しておく。
WriteMemoryをつかう
WriteMemory関数を使ってテキスト(ボタン1)そのものを書き換える。テキストそのものは.rdataセグメントにUTF-16 LEとして置いてある。
find_patternは.rdataも検索可能なので、マッチさせて開始アドレスを割り出しておく。WriteMemoryは構造体をそのまま書き込むため、TCHARが入ったものを用意しておく。あとはinjector::WriteMemoryのテンプレートに構造体、書き込み対象の開始アドレスとデータを引数に渡せばそのまま書き込まれる。
ボタン1を押すと、テキストが変更されていることがわかる。
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で戻せば処理をバイパスできる。
WriteMemoryと同じくパターンから開始アドレスを見つけておき、戻りたいアドレス(開始アドレスから+14したところ)を用意しておく。さらにasmでバイパスした先の処理を作っておき、テキストも用意しておく。
MakeJMPにそれらを渡せば上記のバイパスが実現できる。
ボタン2を押すと、テキストが変更されていることがわかる。
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)を使うように変更する。
ここで命令バイト列全体を確認する(参考6より引用)。
オペコードにある /4というのはdigitであって(参考6)、上記のModR/Mにある3~5bitまでのReg/Opcodeである。/4なので100bになる。
次に残りのModとR/Mを決める必要があるが、これは参考資料7の表を見るのが早い(下図参照)。今レジスタではなくメモリで指定したいので、[RIP + disp32]を選び、r/mに101b、modに00bが決定する。ModR/M全体だと00-100-101で0x25になる。
32bit分のdisplacementでRIPからjmp先のアドレスが入っているメモリ位置までのオフセットを決めるが、これは命令のすぐ後ろにjmp先のアドレスを置くため、0にしておく。
以上をMakeJMPで書くと下記のようなコードになる。
MakeCALL
上記のようにすぐ後ろに戻ってくるのであれば、それはjmpではなくてcall(参考8)がふさわしい。MakeCALLを使う。 EU4dll/injector.hpp at cebec8e139b69d3fe0b4e4c461ffb541f36960e9 · matanki-saito/EU4dll · GitHub
MakeCALLもMakeJMPと同じようにx64のための修正が必要になる。
注意点があり上記を使うと、call命令の後ろ、つまりcall先アドレスが入っている場所に戻ってきてしまう。したがってアセンブラ側で戻り先アドレスを修正をする必要がある。下記の例ではxchgでraxに戻り先アドレスを入れて、8バイト進めた場所に戻るようにしている。