メモリ書き換え メモ その2
前回の記事
指定のアドレスのメモリイメージを書き換えることができた。
今回の記事
MakeInlineの解説とx64化
MakeInlineについて
MakeInlineは下記のassembly.hppで定義されている一連の関数のことである。
EU4dll/assembly.hpp at master · matanki-saito/EU4dll · GitHub
4つ定義があるが、それらはすべて下記に行く。
EU4dll/assembly.hpp at cebec8e139b69d3fe0b4e4c461ffb541f36960e9 · matanki-saito/EU4dll · GitHub
使い方としては次のようになる。最初に割り込ませたい処理を入れるためのStructもしくはclassを用意する。処理自体は()のオーバーライドとして用意する。引数には割り込ませた直前のすべての汎用レジスタとフラグが格納された構造体の先頭アドレスが渡される。
構造体(injector::reg_pack)の構造は以下のようになっている。これらの順序はあとで説明するpushfdとpushadの順序に依存する。
EU4dll/assembly.hpp at cebec8e139b69d3fe0b4e4c461ffb541f36960e9 · matanki-saito/EU4dll · GitHub
実際に用意したコードを指定アドレスにinjectする場合は、下記のようにする。テンプレートに構造体を指定し、第一引数にアドレスを渡す。第二引数を入れると、1から2の間をNOPで埋めるため、call先から戻ってきたときの動作の見通しが良くなる。
]
MakeInlineの動作
MakeInlineはテンプレートであることを除くと単純な動作をする。まず指定アドレスに対してMakeCALLでinjector_asm::make_reg_pack_and_call関数へのバイパスを実行する(下記)。
EU4dll/assembly.hpp at cebec8e139b69d3fe0b4e4c461ffb541f36960e9 · matanki-saito/EU4dll · GitHub
injector_asm::make_reg_pack_and_callは__declspec(naked)のインラインアセンブラで、これは見たまま実行される。
callの直後にpushfd(参考1)とpushad(参考2)が実行されてフラグとすべての汎用レジスタがスタックに積まれ、call命令によってW::call関数が呼び出される。call命令の直前にespをスタックに積んでいるのは引数をスタック渡し(cdecl)をするためだ。これはx64化で重要になる。
引数となるespは最後に積んだ汎用レジスタの先頭を示すアドレスに等しいので、call先でreg_packにマッピングすれば汎用レジスタとフラグが読める。
EU4dll/assembly.hpp at cebec8e139b69d3fe0b4e4c461ffb541f36960e9 · matanki-saito/EU4dll · GitHub
下記はcall先のアセンブリコードである。
矢印にある88 45 08
はmov eax,DWORD PTR [ebp+0x8]
であるから(参考3)正しく引数(regs)として伝わっていることがわかる。その下ではpush eaxが呼ばれてinjectしたいコードに対してregsがスタック渡しされていることがわかる。
injectしたいコード先でregsを変更すると、それはスタックに積んであるデータを変更したことになる。inject処理が終了し、wrapperの処理も終了し、インラインアセンブラに戻ってきた後にpopadとpopfdが実行され、変更されたデータは汎用レジスタとフラグに反映される。
MakeInlineの仕組み
このコードはtemplate、インラインアセンブラ、callするだけのwrapperが組み合わさっているため複雑である。
インラインアセンブラ
まずvoid __declspec(naked)によるインラインアセンブラは汎用レジスタとフラグを壊さないために使用される。それを実現する方法はこれ以外で存在しない。
wrapper
wrapperの使用は冗長に見える。下記のようにテンプレートをそのまま渡してしまえばよいのではないか?
そしてstructにcallを置いて処理を書けばよい?
この処理は実際に動作する。しかしcallにあるstaticは外すことができない。外してもビルドは通るが実行してターゲットのボタンを押すとCTDする。これはインラインアセンブラから呼び出される時はcdecl想定であるが、受け取り側はthiscall想定になっているためである。
wrapperはインラインアセンブラからの呼び出し(cdecl)をクッションすることでインスタンスの関数の呼び出し(thiscall)に処理をつなげる役目をしているともいえる。インスタンス化する必要がないのであれば、上記の処理に書き換えても問題ない。
テンプレート
インラインアセンブラでテンプレートを使用するとどうなるかという疑問がある。二つのstructを用意する。
それぞれを別の場所にInjectionする。
これをビルドしてjmpテーブルを確認すると下記のようになっていることがわかる。インラインアセンブラコードはテンプレートに応じて別に用意されている(1)。そこから呼び出されるwrapperもそれぞれで用意されていることがわかる(2)。
コードを見てもインラインアセンブラコードは別に存在していることが確認できる。このことからテンプレートがコンパイル時に解決されていることがわかる。これは後述するx64化で大きく問題になる。
x64化の課題
x64化するのには下記の問題がある。
- __declspec(naked)が使えない
- インラインアセンブラ(_asm)が使えない
- pushfd/popfdが使えない
- pushad/popadが使えない
- フラグが拡張されている
- 汎用レジスタが拡張されている
- 呼び出し規約にcdeclが指定できない
3~7について
3はpushfq/popfqを使えばよい。4はpush rax, push rbx, ... とすべてに対して実施する。5は参考4を見て拡張する。6は4でpushした順番で定義すればよい。7はcdeclをつけても無視される(fastcallになる。参考5)。rcxでrbpを受け渡すしかない。
1,2について
インラインアセンブラのコードはコンパイル時にテンプレートの数だけジェネレートされるため、前回と同じようにasmファイルにアセンブリを移動する方法では要件を満たせない。masmでテンプレートの機能を探したが、見つけられなかった。
最終的にインラインアセンブラを実装できれば要件を満たせると考え、w_make_reg_pack_and_callにダミーの処理を入れて置いて、
WriteMemoryを使ってその処理を書き換えた。関数アドレス(injectorAddress)を取得してもそれはjmpテーブルのアドレスになるため、GetBranchDestinationを使って、実際のアドレスを取得している。
funcAddressはwrapperのアドレスであり、レジスタの退避が行われた後にMakeCallされる。
動作を確認する
実際に上記の変更を行い、ブレークポイントを置いて動作を確認すると、コードが書き換わっていることがわかる。
途中、call前にpush rspしている箇所があるが、それはcall先で下記のようになっていたためである。rspよりも前に対して上書きしている。さらに不明な0x20の領域がとられている。これはおそらく参考6のhome spaceと呼ばれるものである。