APIを自分で呼びだそう!
はじめに
この記事はカーネル/VM Advent Calendarで書いた記事です。
カーネル/VM Advent Calendarはこちらを参照。
カーネル/VM Advent Calendar : ATND
最初に
カーネル/VM Advent Calendarですがカーネル・VMが一切関係ないこと書きます。
そもそも私にカーネル・VMの話を書けという方が無理です!
ということで内容はAPI呼出しに関して書きます。
対象はWindowsです。
多分32bitでしかもXP以前です。
Vista以降いろいろ変わってるらしく、調べてないのでよくわかりません。
いきなり適当でごめんなさい。。。
一応Windows 7 64bit上(プログラム自体は32bit)で動きました
#実はVista以降でも動く?
API呼出し
さて、そもそもAPIとはApplication Programming Interfaceの略で、アプリケーションからライブラリやOSの機能を使用するときのインターフェースのことです。
アプリケーションを動かすときはAPIを用いて何らかの機能を利用することになります。
例えば画面に文字を表示するのでもWindowsであれば、GUIであればDrawTextAやTextOutA、コンソールであればWriteFileやWriteConsoleOutputCharacterAといったAPIでしょうか。
WindowsのこれらAPIはWin32APIと呼ばれます*1。
これらWin32APIの実体はDLLの中に格納された関数です。
つまり、実行時に実行ファイルとともにDLLも同じアドレス空間にマッピングされ実行されるのです。
このWin32APIを呼び出すには大きく分けて以下の2種類の方法があります。
まず静的リンクの方から。
静的リンクはコンパイル時(より正確にはリンク時?)にリンクされるAPIが決まります。
つまり、実行ファイル中にどのDLLのなんという関数が呼び出されるかが記述されており、実行時にローダによって必要なDLLがロード、リンクされた上で実行されます。
これら、呼び出される関数が列挙されているのが実行ファイル中のインポートセクションというところです。
ここを見るとその実行ファイルが呼び出すAPIの一覧を見ることができます。
通常のAPI呼び出しはこの静的リンクです。
そのため、特別なことをしなくてもWin32APIが通常の関数と同様に呼び出せるわけです。
次に動的リンク。
これはプログラム実行中に動的にDLLをロードして呼び出す方式です。
プログラムの実行コードから呼び出すので実行時までどのAPIを呼び出すかわからない場合に使われます。
実行コードから呼び出すので実行時に決めることができ、プラグインなどの実装に使用されます。
動的リンクを行うには具体的にはLoadLibraryA(もしくはLoadLibraryWやそれぞれのEx版など)とGetProcAddressを使います。
LoadLibrary系でDLLを自プロセスのアドレス空間にマッピングを行い、GetProcAddress関数でDLLがエクスポートしている関数のアドレスを取得します。
静的リンク、動的リンクのより詳しい説明は他のサイトを参照してください。
何をするか
さて、何をするかですが普通にAPIを呼びだそうと思います。
ただし静的リンクを一切使わずに。
つまりインポートセクションを一切持たないプログラムを作ろうということです。
こう書くと動的リンクを使えば簡単にできそうに思えますね。
LoadLibrary系とGetProcAddressを呼び出せばできる、と。
ではLoadLibrary系とGetProcAddressのアドレスはどこでしょう?
通常ではこれらは静的リンクされます。
つまり、最低限これらは静的リンクされるため通常のアプリケーションでは問題なく呼び出せるわけです。
ではインポートセクションを持たないプログラムでどうやってAPIを呼び出すか、これが本題です。
kernel32.dll
さて、Windowsではプログラムをロードするときにインポートセクションに書かれているDLLが同時に読み込まれ、それらの初期化が終わった段階でプログラムが実行されます。
しかし、インポートセクションに記述されていなくても必ずロードされるDLLがあります。
それがkernel32.dllです。
これはプログラムを司るAPIが存在し、プログラムも必ずロードされ、しかも決まったベースアドレスにマッピングされます。
#user32.dllも決まったベースアドレスにマッピングされたと思いますが、これはインポートセクションに記述がある場合で、記述がない場合はロードされなかったと思います
次にLoadLibrary系とGetProcAddress関数ですが、これらの関数はkernel32.dllに実装されています。
つまり、プログラムがロードされればこれらの関数も同じアドレス空間にマッピングされているため、アドレスさえわかれば呼び出すことが可能となります。
実装
では実装です。
とりあえずソースを載せておきます。
#include <windows.h> #define RVAtoVA( type, va, rva ) static_cast<type>( static_cast<void*>( static_cast<char*>( va ) + ( rva ) ) ) typedef FARPROC ( __stdcall *TYPE_GetProcAddress )( HMODULE, LPCSTR ); typedef HMODULE ( __stdcall *TYPE_LoadLibrary )( LPCTSTR ); typedef VOID ( __stdcall *TYPE_ExitProcess )( UINT ); typedef int ( __stdcall *TYPE_MessageBox )( HWND, LPCTSTR, LPCTSTR, UINT ); int cmp( const char *str1, const char *str2 ) { for( ; *str1 && *str1 == *str2 ; ++str1, ++str2 ); return *str1 - *str2; } void *getKernel32Addr( void *base ) { base = reinterpret_cast<void*>( reinterpret_cast<DWORD>( base ) & 0xFFFF0000 ); for( int i = 0 ; i < 5 ; ++i ) { if( static_cast<IMAGE_DOS_HEADER*>( base )->e_magic == IMAGE_DOS_SIGNATURE ) { if( RVAtoVA( PIMAGE_NT_HEADERS32, base, static_cast<PIMAGE_DOS_HEADER>( base )->e_lfanew )->Signature == IMAGE_NT_SIGNATURE ) return base; } base = static_cast<char*>( base ) - 0x10000; } return NULL; } TYPE_GetProcAddress getGetProcAddrFunc( void *base ) { PIMAGE_NT_HEADERS32 peh = RVAtoVA( PIMAGE_NT_HEADERS32, base, static_cast<PIMAGE_DOS_HEADER>( base )->e_lfanew ); PIMAGE_EXPORT_DIRECTORY expdir = RVAtoVA( PIMAGE_EXPORT_DIRECTORY, base, peh->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress ); DWORD num = expdir->NumberOfNames; DWORD *func = RVAtoVA( DWORD*, base, expdir->AddressOfFunctions ); DWORD *name = RVAtoVA( DWORD*, base, expdir->AddressOfNames ); WORD *ord = RVAtoVA( WORD*, base, expdir->AddressOfNameOrdinals ); for( DWORD i = 0 ; i < num ; ++i ) { if( !cmp( RVAtoVA( const char *, base, name[i] ), "GetProcAddress" ) ) return RVAtoVA( TYPE_GetProcAddress, base, func[ord[i]] ); } return NULL; } int KernelVMAdventCalendar() { void *kernelBaseAddr; TYPE_GetProcAddress pGetProcAddress; TYPE_LoadLibrary pLoadLibrary; TYPE_ExitProcess pExitProcess; TYPE_MessageBox pMessageBox; __asm { mov eax, [ebp+4] mov kernelBaseAddr, eax } kernelBaseAddr = getKernel32Addr( kernelBaseAddr ); if( !kernelBaseAddr ) return 0; pGetProcAddress = getGetProcAddrFunc( kernelBaseAddr ); #ifdef UNICODE pLoadLibrary = reinterpret_cast<TYPE_LoadLibrary>( pGetProcAddress( reinterpret_cast<HMODULE>( kernelBaseAddr ), "LoadLibraryW" ) ); #else pLoadLibrary = reinterpret_cast<TYPE_LoadLibrary>( pGetProcAddress( reinterpret_cast<HMODULE>( kernelBaseAddr ), "LoadLibraryA" ) ); #endif // UNICODE #ifdef UNICODE pMessageBox = reinterpret_cast<TYPE_MessageBox>( pGetProcAddress( pLoadLibrary( L"user32.dll" ), "MessageBoxW" ) ); #else pMessageBox = reinterpret_cast<TYPE_MessageBox>( pGetProcAddress( pLoadLibrary( "user32.dll" ), "MessageBoxA" ) ); #endif // UNICODE pMessageBox( NULL, TEXT( "カーネル/VM Advent Calendar 24日目" ), TEXT( "カーネル/VM" ), MB_OK ); pExitProcess = reinterpret_cast<TYPE_ExitProcess>( pGetProcAddress( reinterpret_cast<HMODULE>( kernelBaseAddr ), "ExitProcess" ) ); pExitProcess( 0 ); return 0; }
コンパイル方法
Visual Studioの環境変数がロードされたコマンドプロンプトから以下のコマンドを入力してください。
環境変数のロードはVSをインストールしてあるならスタート→すべてのプログラム→Visual Studio→Visual Studioコマンドプロンプトを開けば必要な環境変数がロードされます。
>cl kvadvent.cpp /link /NODEFAULTLIB /ENTRY:KernelVMAdventCalendar /SUBSYSTEM:WINDOWS
少し解説
エントリーポイントはkernel32.dllから呼び出されます。
つまり、return先アドレスを取得すればkernel32.dllの存在する領域(のどこか)のアドレスが得られます。
このアドレスを取得しているのがエントリーポイント(関数名KernelVMAdventCalendar)のインラインアセンブラのところです。
これを用いて順にアドレスを遡っていってkernel32.dllのベースアドレスを探し出します。
これを行っているのがgetKernel32Addr関数です。
今回はアドレスを順にたどっていますが、前に述べたようにkernel32.dllのマッピングアドレスは決まっているのでこの数値を使うのもひとつの手です。
http://wizardbible.org/17/muffin.txt
Windows アドレス Win95/98 0bff70000h WinME 0bff60000h WinNT 077f00000h Win2000 077e00000h WinXP 07c800000h
ベースアドレス判定やこの後に出てくるエクスポート情報からエクスポート関数を探す方法はPEファイル構造を知っている必要がありますが、この記事を読むような方であればバイナリエディタで実行ファイルを作成するような方々でしょうから省略します。
さて、ベースアドレスを取得できたなら次はGetProcAddress関数を探し出します。
この関数を探すにはDLLのエクスポート情報を利用します。
これを行っているのがgetGetProcAddrFunc関数です。
GetProcAddress関数のアドレスが取得出来ればあとは何でもできますね。
LoadLibrary系が必要だと思われるかもしれませんが、HMODULEの実体はそのモジュールのベースアドレスであり、kernel32.dllのベースアドレスはすでに分かっているためこのアドレスをGetProcAddressのHMODULEに指定し、第二引数に"LoadLibraryA"などとすれば自分で探さなくてもOSが代わりに探してきてくれます。
これでLoadLibrary系とGetProcAddressのアドレスはわかったので後は必要なDLLのモジュールをLoadLibrary系を使ってロードし、GetProcAddressを使ってAPIのアドレスを取得すれば基本的に何でもAPIは呼び出せることになります。
上記例では単にメッセージボックスを出しています。
また、コンパイル時にはいろいろなライブラリがリンクされるのを防ぐために/NODEFAULTLIB をリンカ指定しています。
さらにエントリーポイントをKernelVMAdventCalendarに指定しています。
通常はWinMain関数から始めると思いますが、実際にはWinMain関数の前にランタイムライブラリの初期化などの処理が含まれているため、WinMain関数から始めると余計なコードが含まれてしまいます。
そのため通常のWinMainは使わずに自分でエントリーポイントを定義します。
で?
これができるとどうなるかという話ですが、自分でアドレスを探してくるのでインポートセクションが必要ありません。
つまり実行コードさえあれば何でもできるということになります。
これを用いた何かを書こうと思いましたが時間切れです。
ごめんなさい><
また機会があればやるかもしれません!*2
まとめ
いろいろと肝心な部分を端折ってしまいました。
PEファイルの構造やそれを用いたエクスポート情報の取得などは本来なら実装上でかなり重要な部分になるのですが、完全に説明をすっ飛ばしました。
実はこのあたりの構造がわかるとAPIのフックとかできたりするのですが今回は範囲外なのでこれはまた機会があれば書きたいなぁ。
以上内容あるようで肝心な部分が抜けてる記事でごめんなさい。。。
参考文献
今回参考にさせてもらったサイトを載せておきます。
詳しく知りたい方はリンク先を見てください。
むしろリンク先のほうが詳しいです。。。
インポートセクションを持たずにAPIを使う方法 このネタの元です
自力でAPIを呼び出す ここより詳しく書いてあります
アレ用の何か PEファイル構造が詳しく書いてくれてあります