WinDbg (classic)の古いバージョンでは、fnstenv命令の取得内容のうちFPU Instruction Pointer Offset (FIP)が常に0になってしまうようです。最新のWinDbgでは正しくFIPを取得できました。本記事では検証用コードや検証結果スクリーンショットを紹介します。
fnstenv命令とは
fnstenv命令とは、x87 FPUの状態を指定アドレス以降へ書き込む命令です。32-bit Protected Mode環境では、指定アドレスから24バイト分書き込みます。そのうち12~15バイト目にFPU Instruction Pointer Offset (FIP)が存在します。詳細はIntel® 64 and IA-32 Architectures Software Developer’s Manual Volume 1: Basic Architecture中のFigure 8-9. Protected Mode x87 FPU State Image in Memory, 32-Bit Formatをご参照ください。
FIPとは最後に実行したFPU用命令のアドレスとのことであり、シェルコードが実行中アドレス(=EIPレジスタ内容)を取得するために使用する場合があるとのことです。詳細はシェルコードが置かれているアドレスを得る - ももいろテクノロジーをご参照ください。
各デバッガー経由の実行時にfnstenv命令でFIPを正しく取得できるかどうかの表
本記事冒頭にも書いている通りですが、WinDbgの古いバージョンでは取得できませんでした。確認した結果を以下の表に記述します:
| 使用デバッガー | fnstenv命令でFIPを正しく取得できる? |
|---|---|
| WinDbg (classic) 10.0.18362.1 | ×(常に0になる) |
| WinDbg (classic) 10.0.22621.1778 | ○ |
| WinDbg 1.2306.12001.0 | ○ |
| x64dbg May 25 2023 | ○ |
| IDA Free Version 8.3.230608 | ○ |
| Microsoft Visual Studio Community 2022 Version 17.6.5 | ○ |
| OllyDbg 1.10 | ○ |
| (EXEを直接実行) | ○ |
検証用コードと検証結果スクリーンショット
以下のCソースコードを、Microsoft Visual Studio 2022 Community Version 2022 Version 17.6.5を使用して、Release、x86設定でビルドしました。
// LinkerオプションのAdvancedから「/DYNAMICBASE:NO」を設定している https://learn.microsoft.com/en-us/cpp/build/reference/dynamicbase-use-address-space-layout-randomization #include <stdio.h> // 参考: https://inaz2.hatenablog.com/entry/2014/07/15/023104 シェルコードが置かれているアドレスを得る - ももいろテクノロジー __declspec(naked) void* GetEipTest(void) { __asm { fnop // このGetEipTest関数は、この命令の先頭アドレスを返すはず fnstenv [esp - 28] // fnstenv命令は、指定箇所から28バイト書き込み、そのうちの12~15バイト目が「FPU Instruction Pointer Offset (FIP)」を表す mov eax, [esp - (28 - 12)] ret } } int main(void) { printf("%08p\n", GetEipTest()); return 0; }
ビルド結果のEXEファイルを各種デバッガーで実行した際の、eaxレジスタの内容(=GetEipTest戻り値)を検証したスクリーンショットを以下に示します:








感想
この記事は、WinDbgの古いバージョンを使ってとあるシェルコードをデバッグ実行すると、何故かAccess violation - code c0000005が発生して非常に悩んだ結果、生まれました。デバッガーを使わず直接EXE実行だと問題ないことが尚更混乱に拍車をかけました。まさかデバッガー経由で実行することでバグる場合があるなんて思っていませんでした……。
教訓: デバッガーもなるべく最新バージョンを使いましょう。