pop↑ push↓

☆ (ゝω・)v

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

前回の記事

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

popush.hatenablog.com

今回の記事

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を用意する。処理自体は()のオーバーライドとして用意する。引数には割り込ませた直前のすべての汎用レジスタとフラグが格納された構造体の先頭アドレスが渡される。

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

構造体(injector::reg_pack)の構造は以下のようになっている。これらの順序はあとで説明するpushfdとpushadの順序に依存する。

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

実際に用意したコードを指定アドレスにinjectする場合は、下記のようにする。テンプレートに構造体を指定し、第一引数にアドレスを渡す。第二引数を入れると、1から2の間をNOPで埋めるため、call先から戻ってきたときの動作の見通しが良くなる。

f:id:saito-matanki:20190908231624p:plain]

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 08mov eax,DWORD PTR [ebp+0x8]であるから(参考3)正しく引数(regs)として伝わっていることがわかる。その下ではpush eaxが呼ばれてinjectしたいコードに対してregsがスタック渡しされていることがわかる。

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

injectしたいコード先でregsを変更すると、それはスタックに積んであるデータを変更したことになる。inject処理が終了し、wrapperの処理も終了し、インラインアセンブラに戻ってきた後にpopadとpopfdが実行され、変更されたデータは汎用レジスタとフラグに反映される。

MakeInlineの仕組み

このコードはtemplate、インラインアセンブラ、callするだけのwrapperが組み合わさっているため複雑である。

インラインアセンブラ

まずvoid __declspec(naked)によるインラインアセンブラは汎用レジスタとフラグを壊さないために使用される。それを実現する方法はこれ以外で存在しない。

wrapper

wrapperの使用は冗長に見える。下記のようにテンプレートをそのまま渡してしまえばよいのではないか?

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

そしてstructにcallを置いて処理を書けばよい?

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

この処理は実際に動作する。しかしcallにあるstaticは外すことができない。外してもビルドは通るが実行してターゲットのボタンを押すとCTDする。これはインラインアセンブラから呼び出される時はcdecl想定であるが、受け取り側はthiscall想定になっているためである。

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

wrapperはインラインアセンブラからの呼び出し(cdecl)をクッションすることでインスタンスの関数の呼び出し(thiscall)に処理をつなげる役目をしているともいえる。インスタンス化する必要がないのであれば、上記の処理に書き換えても問題ない。

テンプレート

インラインアセンブラでテンプレートを使用するとどうなるかという疑問がある。二つのstructを用意する。 f:id:saito-matanki:20190909153006p:plain

それぞれを別の場所にInjectionする。

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

これをビルドしてjmpテーブルを確認すると下記のようになっていることがわかる。インラインアセンブラコードはテンプレートに応じて別に用意されている(1)。そこから呼び出されるwrapperもそれぞれで用意されていることがわかる(2)。

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

コードを見てもインラインアセンブラコードは別に存在していることが確認できる。このことからテンプレートがコンパイル時に解決されていることがわかる。これは後述するx64化で大きく問題になる。

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

x64化の課題

x64化するのには下記の問題がある。

  1. __declspec(naked)が使えない
  2. インラインアセンブラ(_asm)が使えない
  3. pushfd/popfdが使えない
  4. pushad/popadが使えない
  5. フラグが拡張されている
  6. 汎用レジスタが拡張されている
  7. 呼び出し規約に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にダミーの処理を入れて置いて、

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

WriteMemoryを使ってその処理を書き換えた。関数アドレス(injectorAddress)を取得してもそれはjmpテーブルのアドレスになるため、GetBranchDestinationを使って、実際のアドレスを取得している。 f:id:saito-matanki:20190909194539p:plain

funcAddressはwrapperのアドレスであり、レジスタの退避が行われた後にMakeCallされる。

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

動作を確認する

実際に上記の変更を行い、ブレークポイントを置いて動作を確認すると、コードが書き換わっていることがわかる。

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

途中、call前にpush rspしている箇所があるが、それはcall先で下記のようになっていたためである。rspよりも前に対して上書きしている。さらに不明な0x20の領域がとられている。これはおそらく参考6のhome spaceと呼ばれるものである。

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

参考

  1. PUSHF/PUSHFD/PUSHFQ — Push EFLAGS Register onto the Stack
  2. PUSHA/PUSHAD — Push All General-Purpose Registers
  3. Online x86 and x64 Intel Instruction Assembler
  4. 主なx64レジスタまとめ - ゾンビ狩りクラブ
  5. Akihiro Notes: Microsoft x64 呼び出し規約
  6. x64 のデバッグについて – Beyond the Basics – Windows Debugging & Troubleshooting Blog