5

バックグラウンド

私は、インジェクション dll を介して、インジェクト プロセスが任意の dll にしようとしている任意のメソッド呼び出しを正常にインターセプトできるレガシー製品に取り組んでいます。特に、gdi32.dll ライブラリ。残念ながら、64 ビット アプリケーションに組み込むと機能しません。注目のトピックになり、機能をアップグレードする時が来ました。また、残念なことに、ソースにはコメントがありません (典型的な >:-<)。見た目から、これを書いた人は x86 命令セットにかなり精通していました。私は何年も組み立ての仕事をしていませんでした。私がやったときはモトローラの組み立てでした。

インターネットを精査した後、インテルの従業員からこの記事に出くわしました。私たちのソース コードがこの記事より約 7 年も前に作成されていなかったとしたら、これはまさに、NoComments 氏の開発者が API メソッドのインターセプトを実行する方法を学んだ場所であると言えます。それくらい手順は似ています。この記事は、前述の Web サイトからもリンクされている素敵な pdf ( Intercepting System API calls ) にも要約されています。

問題

インテルの Web ページのリンクで提供されている例を本当に理解したいので、64 ビット シナリオのソリューションを作成する際に良いクラックを取ることができます。それは十分に文書化されており、理解するのが少し簡単です。以下は、InterceptAPI() ルーチンの抜粋です。「//#」で示される独自のコメントを追加しました(元のコメントは標準の「//」で示されます)。そこで、私が知っていると思うことと知らないことを説明します。

BOOL InterceptAPI(HMODULE hLocalModule, const char* c_szDllName,
    const char* c_szApiName, DWORD dwReplaced, DWORD dwTrampoline, int offset) 
{ 
    //# Just a foreword.  One of the bigger mysteries of this routine to me is
    //# this magical number 5 and the offset variable.  Now I'm assuming, that
    //# there are 5 bytes at the beginning of every method that are basically 
    //# there to set up some sort of pre-method-jump context switch, since its
    //# about to leave the current method and jump to another.  So I'm guessing
    //# that for all scenarios, the minimum number of bytes is 5, but for some
    //# there may be more than 5 bytes so that's what the "offset" variable is
    //# for. In the aforementioned article, the author writes "One additional 
    //# complication exists, in that the sixth byte of the original code may be
    //# part of the previous instruction. In that case, the function overwrites
    //# part of the previous instruction and then crashes."  So some method
    //# starting code contains multi-byte opcodes while others don't apparently.
    //# And if you don't know the instruction set well enough, I'm guessing
    //# you'll just have to figure it out by trial and error.
    int i; 
    DWORD dwOldProtect;

    //# Fetching the address of the method that we want to capture and reroute
    //# Example: c_szDllName="user32",   c_szApiName="SelectObject"
    DWORD dwAddressToIntercept = (DWORD)GetProcAddress( 
        GetModuleHandle((char*)c_szDllName), (char*)c_szApiName); 


    //# Storing address of method we are about to intercept in another variable
    BYTE *pbTargetCode = (BYTE *) dwAddressToIntercept;

    //# Storing address of method we are going to use to take the place of the 
    //# intercepted method in another variable.
    BYTE *pbReplaced = (BYTE *) dwReplaced; 

    //# "Trampoline" appears to be a "Microsoft Detours" term, but its basically
    //# a pointer so that we can get to the original "implementation" of the method
    //# we are intercepting.  Most of the time your replacement function will
    //# want to call the original function so this is pretty important.  What its
    //# pointing to must already be pre allocated by the caller.  The author of
    //# the aforementioned article states "Prepare a dummy function that has the
    //# same declaration that will be used as the trampoline. Make sure the dummy
    //# function is more than 10 bytes long." I believe I'd prefer allocating this
    //# memory within this function itself just to make using this InterceptAPI()
    //# method easier, but this is the implementation as it stands.
    BYTE *pbTrampoline = (BYTE *) dwTrampoline; 


    // Change the protection of the trampoline region 
    // so that we can overwrite the first 5 + offset bytes.
    //# This is voodoo magic to me, but I'm guessing you just can't hop on the
    //# stack and start changing execute instructions without ringing some
    //# alarms, so this makes sure the alarms don't ring. Here we are allowing
    //# permissions so we can change the bytes at the beginning of our
    //# trampoline method.
    VirtualProtect((void *) dwTrampoline, 5+offset, PAGE_WRITECOPY, &dwOldProtect); 

    //# More voodoo magic to me, but this appears to be a way to copy over extra
    //# opcodes that may be needed.  Some opcodes are multi byte I believe so this
    //# is where you can make sure you don't miss them.
    for (i=0;i<offset;i++) 
        *pbTrampoline++ = *pbTargetCode++; 

    //# Resetting the pbTargetCode pointer since it was modified it in the above
    //# for loop.
    pbTargetCode = (BYTE *) dwAddressToIntercept; 


    // Insert unconditional jump in the trampoline.
    //# This is pretty understandable.  0xE9 the x86 JMP command.  I looked
    //# this up in Intel's documentation and it can be followed by a 16-bit
    //# offset or a 32-bit offset. The 16-bit version is not supported in 64-bit
    //# architecture but lets just hope they are all 32-bit and that this does
    //# indeed do what it is intended in 64-bit scenarios
    *pbTrampoline++ = 0xE9;        // jump rel32 

    //# So basically here it looks like we are following up our jump command with
    //# the address its supposed to jump too.  This is a relative offset, that's why
    //# we are subtracting pbTargetCode and pbTrampoline.  Also, since JMP opcodes
    //# jump relative to the address AFTER the jump address, that's why we are
    //# adding 4 to pbTrampoline.  Also, offset is added to pbTargetCode because we
    //# advanced the pointers in the for loop above an "offset" number of bytes.
    *((signed int *)(pbTrampoline)) = (pbTargetCode+offset) - (pbTrampoline + 4); 

    //# Not quite sure why we are changing the permissions on the trampoline function
    //# again, but looks like we are making it executable here.  Maybe this is the
    //# last thing we have to do before it is actually callable and usable.
    VirtualProtect((void *) dwTrampoline, 5+offset, PAGE_EXECUTE, &dwOldProtect); 


    // Overwrite the first 5 bytes of the target function 
    //# It seems we are now setting permissions so we can modify the original
    //# intercepted routine.  It is still pointing to its original code so we
    //# need to eventually redirect it.
    VirtualProtect((void *) dwAddressToIntercept, 5, PAGE_WRITECOPY, &dwOldProtect); 

    //# This will now instruct the original method to instead jump to the next
    //# address it sees on the stack.
    *pbTargetCode++ = 0xE9;        // jump rel32

    //# this is the address we want our original intercepted method to jump to.
    //# Where its jumping to will have the code of our replacement method.
    //# The "+ 4" is because the jump occurs relative to the address of the
    //# NEXT instruction after the 4byte address.
    *((signed int *)(pbTargetCode)) = pbReplaced - (pbTargetCode +4); 

    //# Changing the permissions of our original intercepted routine back to execute
    //# permissions so it can be called by other methods.
    VirtualProtect((void *) dwAddressToIntercept, 5, PAGE_EXECUTE, &dwOldProtect); 


    // Flush the instruction cache to make sure  
    // the modified code is executed.
    //# I guess this is just to make sure that if any instructions from the old
    //# state of the methods we changed, have wound up in cache, that it gets
    //# purged out of there before it gets used.
    FlushInstructionCache(GetCurrentProcess(), NULL, NULL); 

    return TRUE; 
} 

このコードで何が起こっているのか、かなりよく理解していると思います。したがって、百万ドルの質問は次のとおりです。これが 64 ビット プロセスで機能しないのはどうですか? 私が最初に思ったのは、「まあ、アドレスは 8 バイトになっているはずだから、それは何か問題があるに違いない」ということでした。しかし、JMP コマンドはまだ相対 32 ビット アドレスしか使用しないため、64 ビット プロセスで 32 ビット アドレスを使用してもオペ コードは有効であると思います。それ以外に私が信じている唯一のことは、メソッド呼び出しの最初にある魔法の 5 バイトが、実際には他の魔法の数であるということです。誰かがより良い洞察を得ましたか?

注: 「Microsoft Detours」や「EasyHook」など、他にもいくつかのソリューションがあることは知っています。前者は高すぎるので、現在後者を検討していますが、今のところ期待外れです。ということで、今回は特にこの話題に絞って議論したいと思います。私はそれが興味深いだけでなく、私の問題に対する最良の解決策でもあると思います. したがって、「この投稿については特に何も知りませんが、代わりに {サードパーティのソリューションをここに挿入} を試してください」という質問はやめてください。

4

2 に答える 2

1

あなたの例ではうまくいかないことがたくさんあります。

1)PAGE_WRITECOPYに対してVirtualProtectを実行していますが、失敗します。VirtualProtectをPAGE_EXECUTE_READWRITEにしたいとします。

2)e9形式のjmp命令を使用しているため、フックしようとしているdllから「シム」が4 GB以上離れている場合、パッチジャンプは機能しません。

3)VirtualProtectを元に戻すと、PAGE_EXECUTE_READではなくPAGE_EXECUTEで保護されます。実際には、最初のVirtualProtectから取得したflProtectを実際に使用して、元に戻す必要があります。

ちなみに「魔法の数5」は、E9ジャンプ命令オペコードのサイズです。つまり、E9がバイトで、その後にDWORDがオフセットとして続きます。

トランポリンは、コード内から元のAPIにコールバックできるようにするためのものです(つまり、CreateFileWをシミングしている場合、シム内からCreateFileWを呼び出すことはできません。そうしないと、シムを呼び出すことになります)。

FlushInstructionCacheの呼び出しは、x86/x64には影響しません。削除する必要があります。

于 2012-08-16T18:34:28.813 に答える
1

提案されたコードは Microsoft プラットフォームを対象としているように見えるため、 Detoursを使用することをお勧めします。Detours を使用すると、トランポリンは 32 および 64 ビット システムで動作します。

于 2012-05-31T18:07:16.687 に答える