今まで64ビットアプリケーションをがっつり解析することがなかったので詳しく調べていませんでしたが。
Windowsでも、32ビットアプリケーションと64ビットアプリケーションでは引数の渡し方が異なるので、今回はそれのメモです。
ええ、たまにしかやらないから本人が忘れるので覚書です。
x64 での呼び出し規則
https://docs.microsoft.com/ja-jp/cpp/build/x64-calling-convention?view=vs-2019
まあ、公式に解説が出ているので、それ読んで理解すれば終了なんですけどね。
しかし!例によってイマイチ理解度が低くて勘違いしそうだったのでメモしておくというわけです。
32ビット(x86)の関数呼び出しの場合
基本的にはスタック経由で呼び出されます。
スタックへの格納は、関数コール時にSPが差しているアドレスの4バイトの値が第1引数となり、第2引数以降はアドレスの大きい方に格納されます。
例えば、SPが0x00001000なら
0x00001000~0x00001003 第1引数
0x00001004~0x00001007 第2引数
0x00001008~0x0000100B 第3引数
:
:
となります。
このため、パラメータを push で格納している場合、関数最後の引数から順に push しています。
最後に push した値が第1引数になるので、解析時にはそこを間違えないように。
ただし、最近のものでは、これ以外の方法を使っているケースもあるので注意が必要です。
それは、「呼び出し規約」で設定された方法により変わります。
「呼び出し規約」は、Visual C++ ではプロジェクトのプロパティのうち、「C/C++」の「詳細設定」に「呼び出し規約」があり、この設定によります。
Visual C++ のデフォルトは __cdecl となっています。
呼び出し規約は、x86では以下があります。
- __cdecl
- __stdcall
- __fastcall
- __vectorcall
詳しくは以下に書いてあります。
/Gd、/Gr、/Gv、/Gz (呼び出し規約)
https://docs.microsoft.com/ja-jp/cpp/build/reference/gd-gr-gv-gz-calling-convention?view=vs-2019
x86に多いのは、 __cdecl と __stdcall です。
デフォルトが __cdecl になっており、呼び出し規約を注意している人も少ないので、割とこれが多いと思います。
__cdecl と __stdcall は、ともにスタックを使って引数を受け渡しすることに違いはありません。
問題は、関数の実行後、スタックを関数呼び出し前の状態に戻す処理をするわけですが、__cdecl は関数の呼び出し側がスタックをクリアします。一方、 __stdcall は関数の呼び出された側がスタックをクリアします。
そして、注意が必要なのが、WindowsのWin32APIは __stdcall です。
これは、いくらコンパイラの設定を変えても、呼び出される側 (OSのdll等) が __stdcall になっているため、変わることはないと思います。
このため、自身で実装した関数のコール時のスタックのクリアとAPIのコール時のスタックのクリアが異なることがあることに注意が必要です。
どうせ辻褄が合うようにしてるんだから気にしない、とかイワナイ。
なお、引数、リターンアドレス、ローカル変数に関するスタックの動きは、バッファオーバーランによるリターンアドレスの書き換えである程度書いていますので、参考にしていただければと思います。
また、 __fastcall と __vectorcall は、引数の受け渡しにレジスタを使っています。
詳しくは調べれていないのですが、 push/pop などでメインメモリから毎回値を受け渡しをするより、レジスタで受け渡しをしたほうがオーバーヘッドが少ない、という考え方だと思われます。
__fastcallでは、最初の2つの引数が整数値型(ポインタも含む)の場合、ecx、edx レジスタに格納されて渡されるようになります。
__vectorcallでは、ベクター型(浮動小数点、または SIMD ベクター型)をレジスタで渡すことができるようになります。これは使ったことがないので、ちょっとぱっと見の説明が分かりませんでした(汗)。
解析側でこれを見たことはまだないのですが、もし使っている場合は、関数コール直前にスタックではなくレジスタにパラメータを設定しているため、パラメータを見落とさないよう注意が必要です。
なお、 __fastcall と __vectorcall の違いは、パラメータに浮動小数点のレジスタ (xmm) を使うかどうかの違いのようです。
(読んでいる限りは。実際には使ったことも解析で出合ったこともないですが。)
うーんしまった。
x64の話を書こうとしていたのに、x86で結構な文章量になってしまった。
無計画に、予備知識うんちくをメモろうとするからこうなる・・・。
でも、ここ、試験にでるからね!
え?「なんの試験だよw」って?
GIACのGREMの出題範囲だからね?受ける人は押さえておかないとダメよ?
64ビット(x64)の関数呼び出しの場合
冒頭に紹介した Microsoft の記事のとおり・・・なのですが、分かりにくい点があるのでそこを記載します。
引数は、第1引数から第4引数はレジスタで渡すようになっています。
レジスタは、 rcx、rdx、r8、r9 の順で使われます。
しかし、引数が5つ以上ある場合はどうするのでしょうか?
公式には、「整数値は、左から右の順序で、それぞれ RCX、RDX、R8、および R9 で渡されます。 5番目以降の引数は、スタックで渡されます。」と明記しているので、スタックを使うことになります。
問題は、スタックの「どこに?」が分かりにくいんです。
説明には、
最初の 4つを超えるすべてのパラメーターは、呼び出しの前に、シャドウストアの後にスタック上に格納する必要があります。
という文言があります。
では、シャドウストアとはなんぞ?ということですが。この文言よりも前に言及があります。
呼び出し履歴には、呼び出し先がそのレジスタを保存するためのシャドウストアとして領域が割り当てられます。
今度は「呼び出し履歴」なるものがでてくるのですが。
これについての言及が他になく、結局何言ってるのかよくわからなかったりします。
・・・と、ここでよく考えると、最近のMSのこの手のオンラインマニュアルは、機械翻訳していることが多いということを思い出しました。
つまり、機械翻訳が謎の単語を爆誕させているんじゃないかと。
と、いうわけで、英語の原文をあさってみました。
x64 calling convention
https://docs.microsoft.com/en-us/cpp/build/x64-calling-convention?view=vs-2019
Space is allocated on the call stack as a shadow store for callees to save those registers.
(中略)
Any parameters beyond the first four must be stored on the stack after the shadow store prior to the call.
雑に訳すと以下のとおり。
呼び出し先がこれらのレジスタを保存できるように、シャドウストアとして領域がコールスタック上に割り当てられる。
(中略)
最初の4つの引数を超えるパラメータは、(関数の)呼び出し前にシャドウストアの後にスタックに格納する必要がある。
・・・なんか前半部分がやっぱり訳がおかしかったみたいで。
つまり、引数のうち、1~4番目はレジスタに格納されるが、呼び出し側がその処理でその値をスタックに置くかもしれないから、引数の分スタック上に仮のストア領域(=シャドウストア)を確保する、というルールがあり、第5引数はその後ろに格納しろ、ってことのようです。
具体的には、5つ以上引数がある場合、関数コール時のスタックポインタ(SP)の指すアドレスから4つの引数分、つまり8バイト × 4 = 32バイト(x64では1引数は8バイト)はシャドウストアというルールとして確保し、その後ろに第5引数以降を置け、ということです。
・・・ごめん、最初に機械翻訳の日本語の文章読んで、全然わかりませんでした。
実際のコードで見てみると、以下の図のようになっています。
上図は、NtAllocateVirtualMemory関数を呼び出す例です。
NtAllocateVirtualMemory関数は、6つの引数を使用します。
RIP(インストラクションポインタ)がアドレス 0x00000000100011BE を指しており、このAPIを実行する直前です。
第1~4引数は、それぞれ rcx、rdx、r8、r9 に格納されています。
第5、6引数はスタックに格納されています。スタックポインタ(RSP)は 0x000000000014D670 を指しています。
ここで 0x000000000014D670 の第1~4引数分の領域、32バイトがシャドウストアになります。
このため、第5引数は 0x000000000014D670 + 0x20 (32バイト) = 0x000000000014D690 に格納されます。
第6引数は、その後ろの0x000000000014D698 に格納されます。
このため、この例では、NtAllocateVirtualMemory関数のパラメータは以下のとおりとなります。
- 第1引数:rcx = 0xFFFFFFFFFFFFFFFF
- 第2引数:rdx = 0x000000000014D770
- 第3引数:r8 = 0x0000000000000000
- 第4引数:r9 = 0x000000000014D6A0
- 第5引数:rsp + 0x20 = 0x0000000000001000
- 第6引数:rsp + 0x28 = 0x0000000000000004
これが、Windowsにおけるx64モードでの一般的な関数呼び出し時のパラメータの確認方法となります。
x64でも、x86同様に呼び出し規約は複数ありますが、x86ほど種類は無いようです。
x64では、 __cdecl と __stdcall オプションは無視され、x64呼び出し規約が利用されます。
(詳しくはMicrosoft公式の __cdecl と __stdcall の記事を参照。)
さいごに
Windows環境の実行ファイルをリバースエンジニアリングをする場合、関数の分析は必須になります。
関数やAPIでは、与えるパラメータによって挙動が大きく変わるため、そのパラメータの確認は非常に重要です。
逆にいえば、関数呼び出し時のパラメータを間違えてしまっては、分析失敗の大きな要因になりかねません。
今回は、そのパラメータを間違えないよう、呼び出し規約を一通り確認してみた、ということです。
Windowsの実行ファイルのリバースエンジニアリングをやろうとしている人は必須知識、経験者でも規約の種類の復習や64ビット環境へ手を伸ばす場合の32ビットとの違いの確認すべき知識となるかと思います。
今後のリバースエンジニアリングライフ(そんなのあるの??)に役立てば幸いです。
・・・今後のライフといえば、どうでもいいけど、艦〇れの梅雨・夏イベがまだ E-1(甲) しか終わってナス。
後段実装までに E-2 くらいまでは終わらせておかないと。
そうそう、ウチの鎮守府に、先日初めてランカー報酬が届いたですよ。
・・・ホントにどうでもいいわ!
(相変わらず最後に締まりがない落書きブログ)