detoursが復元なしで元関数を呼び出す仕組み
以前、単純なJMP方式のフックとして紹介したやり方は、元関数を呼び出すために一時アンフックするという実装だった。
図解するとこう。
1.初期状態
2.ExitProcessフック実施後。関数の先頭5バイトがNewExitProcessへのJMPに改竄される。
3.ExitProcessが呼び出された。即座にNewExitProcessにJMPする。
4.NewExitProcessでフックを解除する。これによりExitProcessの先頭5バイトは復元される。
5.NewExitProcessでExitProcessを呼び出す。ExitProcessは実行を完了しNewExitProcessに制御を返す*1
6.NewExitProcessでフックを実施する。
でも問題があって、アンフックしている最中に別スレッドがExitProcessを呼び出すと、当然ながらNewExitProcessを経由しない。また、アンフック中に呼び出されることを阻止する方法もない(と思っている)。したがって2スレッド以上の環境を想定すると、NewExitProcessを100%呼び出す事は保証できない。
一方でDetoursを利用する場合、アンフックすることなくフック前関数を呼び出すことができる。
図解するとこう。
1.初期状態
2.ExitProcessフック実施後。関数の先頭5バイトがNewExitProcessへのJMPに改竄されるとともに、本来のバイト列がOldExitProcess(Trampoline領域)の先頭にコピーされる。OldExitProcessには、そのバイト列の直後にExitProcess+NへのJMPコードが配置される。
3.ExitProcessが呼び出された。即座にNewExitProcessにJMPする。
4.NewExitProcessでOldExitProcessを呼び出す。
5.本来のバイト列を通過後、ExitProcess+NへJMPする。
こうすることでフックを解除せずに元の関数を呼び出している。
ここでポイントなのは ExitProcess+N の部分。Nは可変サイズである。
単純なJMP方式の場合、必ず5バイト(JMP XX XX XX XX)改竄したが、これは本来のバイトを本来の場所に戻すから可能な事。detours方式の場合、本来のバイト列を本来の場所で処理しないため(original codeの場所を見てね)、機械的に5バイトで処理すると意図した命令にならない可能性がある。
たとえば以下はSeAccessCheckの先頭数バイトを抜き出したもの。
; W2K SP4 nt!SeAccessCheck: 804fac2e 55 push ebp 804fac2f 8bec mov ebp,esp 804fac31 53 push ebx 804fac32 33db xor ebx,ebx ; XP SP2 nt!SeAccessCheck: 8056d2f0 8bff mov edi,edi 8056d2f2 55 push ebp 8056d2f3 8bec mov ebp,esp 8056d2f5 53 push ebx ; Vista nt!SeAccessCheck: 8186da06 8bff mov edi,edi 8186da08 55 push ebp 8186da09 8bec mov ebp,esp 8186da0b 83ec0c sub esp,0Ch
このうち、W2K SP2 のものは5バイト切り出し、Trampoline領域に復元しても意図した命令にはならない。
55 push ebp 8bec mov ebp,esp 53 push ebx 33e9 xor ebp,ecx ; e9 がJMPとして解釈されない!
そのため、この方式を採用するには逆アセンブラを実装し、命令を破壊しないようにコピーしなければいけない。detoursの場合、disasm.cppがこの部分に当たる。これを実装しないと汎用性のある形にはならないことになる。