なのは完売 とある関数の電脳戦 (じょうほうせん とある関数のバトルプログラム)
カーネル/VM Advent Calendar の記事です。
それは、小さなメモリチップの中で繰り広げられる電脳戦のお話。
ひと足早いなのは完売(じょうほうせん)を楽しみましょう。
特定の処理の中に、自分の任意の処理を挟む行為をフックと呼びますが、フックから自分のプロセスを守りぬく方法はあるのでしょうか?
windows環境でのanti hookのお話をします。
DLL Injection Hook
windowsではさまざまなフック形式を用いることができます。
ほとんどのフックにおいて、最終的には自分のフック用DLLを相手のプロセスに叩きこんで、そこを橋頭堡として、フックを行います。
これはOSのプロセスのメモリ保護をかいくぐるのにフックDLLが便利だからです。
(ついでに8086のジャンプが相対アドレスってゆーのもありそうですがね。)
これらのフックは、DLLを対象プロセスに挿入するので、DLL INJECTIONとも呼ばれています。
フックするプロセスは、APIの SetWindowsHookExや CreateRemoteThread によって、対象プロセスへ DLLマッピングを行います。
早速、サンプルを見て行きましょう。
攻撃をするドSくん と 攻撃を受ける ドMくんです。
ドSくんは、ドMくんに CreateRemoteThread API を使い、ドS攻撃用DLL.dll というフック用DLLを挿入します。ねじ込んだるー。
ドS攻撃用DLL.DLLによって、ドMくんのプロセスに干渉します。
今回は、ドMくんの「なのフェイの薄い本ください」という文字列を「なのは完売」という文字列に書き換えます。
この程度であれば、フックなんて使わなくてもできるのですが、サンプルということで、お願いします。
実際動かしてみましょう。
ドSくんとドMくんを起動して、ドSくんのボタンをクリックすると、ドMくんのステータスが「なのフェイの薄い本ください」から「なのは完売」に変わります。
サンプルプログラム 「ドS攻撃CreateRemoteThreadとDLL_Injection」と「ドMやられ放題」による実演。
攻撃のキモの部分を見てみましょう。
ここで、ドSくんは、ドMくんのプロセスに、ドS攻撃用DLL.DLLをねじ込んでいます。らめー。
//CreateRemoteThread で ドM を攻撃する xreturn::r<bool> Attach() { HMODULE kernel32dll = ::GetModuleHandle("Kernel32"); //埋め込むフック用のdll char injectionDLLName[MAX_PATH]; if( !GetModuleFileName( g_Instance ,injectionDLLName,_MAX_PATH) ) { DWORD lastError = ::GetLastError(); return xreturn::windowsError(std::string() + "GetModuleFileNameに失敗。" , lastError); } _tcscpy(_tcsrchr(injectionDLLName, '\\') + 1, "ドS攻撃用DLL_CreateRemoteThread.dll" ); //ドMくんのプロセスを探します。 auto processID = findTargetProcess(); if (processID.isError()) { DWORD lastError = ::GetLastError(); return xreturn::windowsError(std::string() + "GetModuleFileNameに失敗。" , lastError); } if ( (DWORD)processID == 0) { return xreturn::error(std::string() + "攻撃対象がいません" ,0); } //ドMくんのプロセスをひらきます。 HANDLE processHandle = OpenProcess( PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_READ | PROCESS_VM_WRITE , FALSE,processID ); if (!processHandle) { DWORD lastError = ::GetLastError(); return xreturn::windowsError(std::string() + "OpenProcessに失敗。" , lastError); } //文字列用のメモリを確保するために、ドMくん名義でメモリを確保しておきます。 //おれおれドMなんだけどメモリをちょっと振り込んでくれないかなー? void * remoteMemory = VirtualAllocEx( processHandle, NULL , lstrlen(injectionDLLName) , MEM_COMMIT, PAGE_EXECUTE_READWRITE ); if (!remoteMemory) { DWORD lastError = ::GetLastError(); CloseHandle(processHandle); return xreturn::windowsError(std::string() + "VirtualAllocExに失敗。" , lastError); } ::WriteProcessMemory(processHandle, remoteMemory, (void*)injectionDLLName,lstrlen(injectionDLLName),NULL); //ターゲットプロセス上にスレッドを作成し、dllを注入します。 HANDLE remoteThreadHandle; //フック remoteThreadHandle = ::CreateRemoteThread( processHandle, NULL, 0, (LPTHREAD_START_ROUTINE) ::GetProcAddress(kernel32dll,"LoadLibraryA"), remoteMemory, 0, NULL ); if( !remoteThreadHandle ) { DWORD lastError = ::GetLastError(); ::VirtualFreeEx( processHandle, injectionDLLName, lstrlen(injectionDLLName) , MEM_RELEASE ); CloseHandle(processHandle); return xreturn::windowsError(std::string() + "CreateRemoteThreadに失敗。" , lastError); } //注入スレッドが終了するまで待ちます. ::WaitForSingleObject( remoteThreadHandle, INFINITE ); DWORD hLibModule; ::GetExitCodeThread( remoteThreadHandle, &hLibModule ); ::VirtualFreeEx( processHandle, injectionDLLName, lstrlen(injectionDLLName) , MEM_RELEASE ); ::CloseHandle( processHandle ); return true; }
フック用のDLLを対象プロセスにねじ込んだあとは、DLLの中の任意のコードを動作させることができます。
そのため、フック対象プロセスを蹂躙し放題です。
(こっ、こんなDLLなんかにビクンビクン。)
たとえドMでも、よくしらない相手に好き放題されるのは悔しいものです。
これは、ドMくんでも、くやしいですね。悔しいはず無いですわよね。
悔しいので、対抗策を考えます。
anti DLL Injection
結局のところ、自分のプロセス内で LoadLibraryA を呼ばれているわけですから、それを呼ばせないようにすればいいわけです。
LoadLibrary が素直すぎて、ドSくんからの攻撃を素直に実行しているのが悪いのです。
つまり、ドSくんからフック用のDLLをねじ込まれるよりも早く、 自分のLoadLibrary API を先にフックして、ドSくんの攻撃だったらロードしない、といった、賢いLoadLibrary関数に作り替えてしまうのです。
API フックの方法はいろいろありますが、今回は 自作のsexyhookでやってみます。
sexyhookもひっそりとバージョンアップを重ねているのだ。
sexyhookのgithubはこっち。
こんな感じです。
//LoadLibraryAを保護する. SEXYHOOK_BEGIN(int,SEXYHOOK_STDCALL,SEXYHOOK_DYNAMICLOAD ("kernel32.dll","LoadLibraryA"),( const char * lpLibFileName )) { //ドSくんのDLLだけロードさせない. if ( strstr(lpLibFileName , "ドS" )) { //ドSって入っているから、ドSくんのdllだろうから、ロード拒否. MessageBox( g_MainWnd , "ドSくんからDLLを挿し込まれそうになったので防御しました" , "バリアー" , 0); return 1; } //それ以外はロードする. //全部のDLLを拒否してはいけない。 //gdiplus や winsock など遅延読み込みされるAPIがあるため、そいつらまで殺してしまう。 //一時的にフックを解除 SEXYHOOK_UNHOOK(); //元の関数を呼び出す return CallOrignalFunction(lpLibFileName); } SEXYHOOK_END_AS() g_LoadLibraryAHook;
これで、ドSくんがDLLをねじ込もうとしても、スルーできます。
これをバリアと名づけましょう。手をクロスしてバリアーーー!!
ドMくんのバリアーボタンをクリックすると、この処理が行われるようになっています。
試してしみましょう。
サンプルプログラム 「ドS攻撃CreateRemoteThreadとDLL_Injection」と「ドMやられ役_Anti_DLL_Injection_LoadLibraryA」による実演
バリアーを展開すると、今度はフックされないよ。
見事に、ドSくんの攻撃を受け流すことが出来ました。
ステータスは「なのフェイの薄い本ください」であり、フックされて書き換えられる「なのは完売」ではありません。
やったね☆
だが、これだけでは完璧とは言えない
これで、フックを受け流すことが出来ました。
ですが、LoadLibray関数は複数種類ありますよね。
LoadLibrayA LoadLibrayW LoadLibrayExA LoadLibrayExW です。
今回は、相手がLoadLibrayAを呼ぶので、これだけ対応しましたが、ドSくん以外の攻撃者はどれを使ってくるかわかりません。
また、 SetWindowsHookEx APIによるフックは LoadLibraryのフックだけでは防げないようです。
デモを見てみましょう。
サンプルプログラム 「ドS攻撃SetHookWindowsExでDLL_Injection」と「ドMやられ役_Anti_DLL_Injection_LoadLibraryA」による実演
SetHookWindowsEx API によるフックでは、 dll内に実装した SetHookWindowsEx を呼び出すことで、 windows OS から DLLが各プロセスにマッピングされます。
LdrLoadDll
こうなって、全部に対応する・・・のは少々めんどいですね。
実は、LdrLoadDllという、 LoadLibray関数の元締め的関数がいるのです。
LdrLoadDllをフックすれば、すべての LoadLibray関数 をフックすることができます。
ついでに、 SetWindowsHookEx にも対応できちゃいます。
早速やってみましょう。
//LoadLibrary* 系すべてでバリアを使う. SEXYHOOK_BEGIN(int,SEXYHOOK_STDCALL,SEXYHOOK_DYNAMICLOAD ("NTDLL.dll","LdrLoadDll"),(PWCHAR PathToFile,ULONG Flags,PUNICODE_STRING ModuleFileName,PHANDLE ModuleHandle)) { //気に食わないdllはロードさせない. if (wcsstr(ModuleFileName->Buffer ,L"ドS" ) != NULL) { //ドSって入っているから、ドSくんのdllだろうから、ロード拒否. MessageBox( g_MainWnd , "ドSくんからDLLを挿し込まれそうになったので防御しました" , "バリアー" , 0); return 0; //ロード禁止 } //それ以外はふつーにロードするよ //一時的にフックを解除 SEXYHOOK_UNHOOK(); //元の関数を呼び出す return CallOrignalFunction(PathToFile,Flags,ModuleFileName,ModuleHandle); } SEXYHOOK_END_AS() g_LdrLoadDllHook;
これだけです。簡単ですね。
デモで挙動を見てみましょう。
サンプルプログラム 「ドS攻撃SetHookWindowsExでDLL_Injection」と「ドMやられ役_Anti_DLL_Injection_完全版」による実演
やりました。
これで、どのDLLをロードして、どのDLLをロードしないかという決定権を自プロセスが握ることが出来ました。
あとは、この中で、DLLの名前によって、ホワイトリストによるフィルタリングをするにしろ、ブラックリストによるフィルタリングをするにしろ自由に作ることができます。
やったね☆。
これで、DLL Injectionによるフックからは身を守れるようになりました。
めでした。めでした・・・・・ってまだ続くよ。
余談 LdrLoadDll ですべて拒否してはいけない。
海外のフォーラムでは LdrLoadDll を書き変えて CHAR pRet[]={0xC3}; つまり ret 命令にしたりしています。
これでもいいんですが、 これでは、winsock や gdiplus といった遅延読み込みする dll も、読み込みを拒絶してしまい、プログラムがまともに動かないことがあります。
そういうDLLを使っている場合は、今回の例みたいに、DLLの名前でロードするかどうかを決める処理を入れる必要があります。
anti CreateRemoteThread
前回は、橋頭堡として DLL injectionしてくる奴だったので、LdrLoadDll で逃げれました。
ですが、DLL injectionしてこないフックだったらどうでしょうか?
CreateRemoteThread で直接アセンブラなどを叩きこんでくるフックルーチンです。
そんなことできるの?って話しですが、できます。
フック用DLLを作るのがOSのメモリ保護を出しぬいてフック用関数楽に書きたいためであって、フックにDLLは必須ではありません。
だから、DLLなんかなくても当然フックできます。
サンプルプログラム 「ドS攻撃CreateRemoteThreadだけ」と「ドMやられ役_Anti_DLL_Injection_完全版」による実演。
DLL Injectionは完璧に守れていた 「ドMやられ役_Anti_DLL_Injection_完全版」の無敵のバリアが突破されてしまいました。
ネクロフォビア「む、無敵のバリアが。」
助けて、金ピカ。
さて、どうしましょう。
CreateRemoteThreadをフックして・・・・というのは無意味です。
自プロセスのCreateRemoteThread関数が呼ばれるわけではないですからね。
だったら、全プロセスのCreateRemoteThreadを先にフックしておいて・・・・って考えるのはナンセンスです。
それって、自分へのフック攻撃を避けるために、相手をフックするわけですよね。
やはり、ここは専守防衛です。
日本人なら専守防衛です。
自分からは手を出さない。
ただし自衛のための戦力は保有するのです。
専守防衛のポリシーに則り、自プロセス内だけで、CreateRemoteThreadを何とかしましょう。
スレッドってどうやって呼ばれるんだろう?
デバッガで呼び出し履歴などを見ていくと、 Thread は、以下のメソッドから呼ばれています。
kernel32.dll!@BaseThreadInitThunk@12() + 0x12 バイト ntdll.dll!___RtlUserThreadStart@8() + 0x27 バイト ntdll.dll!__RtlUserThreadStart@8() + 0x1b バイト
つまり、ここのどこかに処理を噛ますことが出来れば、スレッド実行に干渉できるわけです。
今回は、 BaseThreadInitThunk をフックしてみます。
やってみよう。
DWORD WINAPI USOThread(LPVOID lpParameter) { MessageBox( g_MainWnd , "CreateRemoteThreadを検出したので無力化しました。\r\n" , "CreateRemoteThread検出" , 0); return 0; } SEXYHOOK_BEGIN(void,SEXYHOOK_STDCALL,SEXYHOOK_DYNAMICLOAD ("kernel32.dll","BaseThreadInitThunk"),(LPVOID a1,LPVOID a2 )) { void * _ecx = NULL; void * _edx = NULL; _asm { mov _ecx , ecx; //何かのフラグに利用しているらしい mov _edx , edx; //スレッド内で実行する関数のアドレス } //ここで嘘の関数を指定できる。 _edx = (void*)USOThread; //一時的にフックを解除 SEXYHOOK_UNHOOK(); _asm { push a2; push a1; mov ecx,_ecx; mov edx,_edx; //元の関数を呼び出す call CallOrignalFunction; } } SEXYHOOK_END_AS() g_BaseThreadInitThunk;
これで、スレッドの動作開始に干渉できます。
が、これには一つ問題があります。
sexyhookでは、UNHOOKするときにフックしたメモリ空間を元に戻しているのですが、ここはスレッドで動作しています。
メモリは一つしかないのに、スレッドは複数個ある可能性があります。
つまり、スレッド競合の問題が発生します。
同時に、メモリを書き換えることがあるかもしれません。
よって、ちょっとしたハックを行います。
DWORD WINAPI USOThread(LPVOID lpParameter) { MessageBox( g_MainWnd , "CreateRemoteThreadを検出したので無力化しました。\r\n" , "CreateRemoteThread検出" , 0); return 0; } SEXYHOOK_BEGIN(void,SEXYHOOK_STDCALL,SEXYHOOK_DYNAMICLOAD ("kernel32.dll","BaseThreadInitThunk"),(LPVOID threadParama,LPVOID nazo )) { //このレジスタの値は壊してはいけない void * _ecx = NULL; void * _edx = NULL; _asm { mov _ecx , ecx; //何かのフラグに利用しているらしい mov _edx , edx; //スレッド内で実行する関数のアドレス } //選別ルーチン _edx = (void*)USOThread; //オリジナルの関数 +5 はsexyhookが破壊している領域を飛ばすためアドレスをずらす. void* orignalFunction = (void*)(((unsigned long) sexyhookThis->getOrignalFunctionAddr() ) + 5); //なりジナルの BaseThreadInitThunk を呼び出します。 _asm { push nazo; push threadParama; mov eax,orignalFunction; //ebp esp を下で書き換えるため ローカル変数にアクセスできなくなるので、eaxに保持. mov ecx,_ecx; mov edx,_edx; //この関数をなかった事にしたいので、 スタックフレームを復元する. mov esp,ebp //ebpの復元 pop ebp //BaseThreadInitThunk のプロローグ mov edi,edi push ebp mov ebp,esp //元のルーチンに飛ばす. jmp eax //74AB338F jne @BaseThreadInitThunk@12+15h (74AB620Ah) ここに飛ばす. } } SEXYHOOK_END_AS() g_BaseThreadInitThunk
sexyhookにアンフックさせるのではなく、自分の手でメモリアドレスを計算してプロセスをつないでいます。
これでスレッド競合の問題はなくなります。
本当はsexyhookにこの機能を搭載したいのですがねー。
そして完成へ
まとめます。
DLL INJECTION だけを防止するコードはこんな感じです。
//LoadLibrary* 系すべてでバリアを使う. SEXYHOOK_BEGIN(int,SEXYHOOK_STDCALL,SEXYHOOK_DYNAMICLOAD ("NTDLL.dll","LdrLoadDll"),(PWCHAR PathToFile,ULONG Flags,PUNICODE_STRING ModuleFileName,PHANDLE ModuleHandle)) { //気に食わないdllはロードさせない. if (wcsstr(ModuleFileName->Buffer ,L"ドS" ) != NULL) { //ドSって入っているから、ドSくんのdllだろうから、ロード拒否. MessageBox( g_MainWnd , "ドSくんからDLLを挿し込まれそうになったので防御しました" , "バリアー" , 0); return 0; //ロード禁止 } //それ以外はふつーにロードするよ //一時的にフックを解除 SEXYHOOK_UNHOOK(); //元の関数を呼び出す return CallOrignalFunction(PathToFile,Flags,ModuleFileName,ModuleHandle); } SEXYHOOK_END_AS() g_LdrLoadDllHook;
CreateRemoteThread だけを防止するコードはこんな感じです。
DWORD WINAPI USOThread(LPVOID lpParameter) { MessageBox( g_MainWnd , "CreateRemoteThreadを検出したので無力化しました。\r\n" , "CreateRemoteThread検出" , 0); return 0; } SEXYHOOK_BEGIN(void,SEXYHOOK_STDCALL,SEXYHOOK_DYNAMICLOAD ("kernel32.dll","BaseThreadInitThunk"),(LPVOID threadParama,LPVOID nazo )) { //このレジスタの値は壊してはいけない void * _ecx = NULL; void * _edx = NULL; _asm { mov _ecx , ecx; //何かのフラグに利用しているらしい mov _edx , edx; //スレッド内で実行する関数のアドレス } //選別ルーチン //VirtualAlloc されたメモリの判別が難しい。 //とりあえず、 VirtualAllocする人は、 PAGE_EXECUTE_READWRITE していたりしたら、 コード領域なのにWRITEができるーっていうことで落とすことにした。 //相手が対策してきたら無意味なので、、、根本的な対処を考えないといけない。 if (!IsBadWritePtr(_edx , 1)) { //嘘のスレッド関数を渡してあげる。 _edx = (void*)USOThread; } //オリジナルの関数 +5 はsexyhookが破壊している領域を飛ばすためアドレスをずらす. void* orignalFunction = (void*)(((unsigned long) sexyhookThis->getOrignalFunctionAddr() ) + 5); //なりジナルの BaseThreadInitThunk を呼び出します。 _asm { push nazo; push threadParama; mov eax,orignalFunction; //ebp esp を下で書き換えるため ローカル変数にアクセスできなくなるので、eaxに保持. mov ecx,_ecx; mov edx,_edx; //この関数をなかった事にしたいので、 スタックフレームを復元する. mov esp,ebp //ebpの復元 pop ebp //BaseThreadInitThunk のプロローグ mov edi,edi push ebp mov ebp,esp //元のルーチンに飛ばす. jmp eax //74AB338F jne @BaseThreadInitThunk@12+15h (74AB620Ah) ここに飛ばす. } } SEXYHOOK_END_AS() g_BaseThreadInitThunk
SetWindowsHookEx , CreateRemoteThread の両方に対応しようとすると、この2つを実装します。
片方だけでは、それぞれしか防ぐことができません。
このコードで、自分のプロセスを邪悪なフックから守ることが出来ました。
フックするプログラムがどんな手でこようと、フックされることはないでしょう。(カーネルサイドでのフックを除く?)
デモ
サンプルプログラム 「すべてを実装したドMやられ役_Anti_ALLHook無敵のバリア」がすべてのドS攻撃に耐えていることを実演。
こいつもうドMぢゃない。
攻性防壁
さて、フックから自プロセスを守ることはできたのですが、やられぱなしもイヤですから、何か反撃の方法を考えてみましょう。
攻性防壁みたいなもんです。
我々をフックしようとした代償がいかに高くつくかを相手のプロセスに思い知らせてやりましょう。
しかし、これが結構難しいのです。
まず、反撃するには、攻撃した相手が分からないといけないのですが、フックの場合、誰にやられたかという情報を得ることがすごく難しいのです。
DLL INJECTIONでは、挿入されようとしたDLLのファイル名がわかります。
なのでこのDLLの情報、製作者やファイルのパス名から、やり返す方法はあるかもしれません。
ですが、DLL INJECTIONを指示した直接のプロセス名はわかりません。
推測することしかできないのです。
また、挿入されたようとしたDLLをむやみに削除するプログラムを作るのは、あまりおすすめしません。
フックDLLはシステムに深く食い込んでいる可能性があり、ヘタにフックDLLを消去すると、出来の悪いフックプログラムによっては、システム全体が不安定になる可能性があります。
よって、ここではフック仕掛けてきたDLLがある場所にあるexeでドSくんのものだったら、そのプロセスを終了する(TerminateProcess)するようにしてみました。
CreateRemoteThread では、相手の名前すらも何もわかりません。
ただ、ちょっとだけ嫌がらせをすることはできます。
CreateRemoteThread を呼ぶルーチンは以下のようになっていることがあります。
HANDLE remoteThreadHandle; remoteThreadHandle = ::CreateRemoteThread( processHandle, NULL, 0, (LPTHREAD_START_ROUTINE) remoteCodeMemory, remoteDataMemory, 0, NULL ); //注入スレッドが終了するまで待ちます. ::WaitForSingleObject( remoteThreadHandle, INFINITE );
ふつーのフックルーチンだと、フック処理は一瞬で終わるので、マルチスレッドをまともに実装するのではなく、こーゆー手抜きな実装になっていることが多いのです。
もし、CreateRemoteThread されたスレッドが長時間応答を返さなかったら、どうなるでしょうか?
フックしたプロセスはフリーズしますね。
と、いうわけで、 CreateRemoteThread してきたプログラムには、 3日間寝とけ Sleep(1000 * 60*60*24*3) とでも返してあげましょう。
DWORD WINAPI USOThread(LPVOID lpParameter) { MessageBox( g_MainWnd , "CreateRemoteThreadを検出したので、相手を冬眠させます。\r\n" , "お休み" , 0); ::Sleep(1000 * 60 * 60 * 24 * 3); return 0; }
これで、ぐっすりオネンネしてくれます。
余計な悪さはできないでしょう。
デモをやってみましょう。
サンプルプログラム 「ドMやられ役_Anti_Hook_ReactiveBarrier攻性防壁」と各フックプログラムです。
(なお、反撃するコードなどは法令を守って楽しく実装しましょう。)
まとめ
アンチフックについて解説しました。
また、余興としてフックしてきた奴らにやり返す攻性防壁を作って見ました。