EasyHook 中部分函数的实现分析—-申请钩子

EasyHook 中部分函数的实现分析—-申请钩子

“easyhook库代码简要分析http://gslab.qq.com/article-245-1.html”这篇文章对EasyHook 整体的流程和使用有比较清晰的介绍,下面我主要是具体分析其中的一部分函数。最好在对EasyHook 有一定的了解之后再阅读本文,可以先阅读上链接的文章。

EasyHook 中申请钩子的原理介绍

  • 函数原型
    内部使用的函数,为给定的入口函数申请一个hook结构。
    准备将目标函数的所有调用重定向到目标函数,但是尚未实施hook。
EASYHOOK_NT_INTERNAL LhAllocateHook(
            void* InEntryPoint,
            void* InHookProc,
            void* InCallback,
            LOCAL_HOOK_INFO** OutHook,
            ULONG* RelocSize)
  • 参数说明
           InEntryPoint—如果入口点不能hook,将返回STATUS_NOT_SUPPORTED
           InHookProc—- 与入口点完全匹配的替代函数。
           InCallback—-hook后可以从LhBarrierGetCallback 得到的回调函数
           OutHook—-返回一个Hook 结构,包含:已经申请好的跳板函数,重定位的入口指针。
            RelocSize—- 入口点重定向指令的大小
  • 返回值
    STATUS_NO_MEMORY
    无法在目标入口点周围申请内存
    STATUS_NOT_SUPPORTED
    目标入口点包含不支持的指令。
    STATUS_INSUFFICIENT_RESOURCES
    同时叠加在这个函数上的hook 数目太多。

  • 敲黑板
           这个函数是EasyHook 挂钩操作的一个最最基本也是最最重要的函数,它决定了这个开源工具的实用性以及稳定性。我尽量按照我的理解来讲述这个函数的实现过程。

       我们都知道,EasyHook 实现的HOOK算是一种InlineHook,那么InlineHook 的通用的做法是什么呢?EasyHook 的特殊性在哪里?

InlineHook 的通常做法:http://blog.csdn.net/qq_18218335/article/details/76262918
最好首先阅读我之前写的关于hook 的方法的介绍的文章,对hook 的方法有基本的理解。
EasyHook 使用的是相对来说最复杂的一种hook 的方法,我们需要研究的就是它针对目标函数的前几条指令的各种情况,如何处理。以及如何在不同的地址上模拟执行目标函数的前几条指令的。总的来说,如果被覆盖的指令的执行效果与执行指令的地址(RIP/EIP)有关的话,就需要特殊处理,如果与RIP/EIP 无关,直接拷贝即可

申请hook 时对于目标函数前几条指令的不同处理

  • 函数声明
EASYHOOK_NT_INTERNAL LhRelocateEntryPoint(
                UCHAR* InEntryPoint,
                ULONG InEPSize,
                UCHAR* Buffer,//用于存储转化后的模拟执行目标函数前几个指令的指令
                ULONG* OutRelocSize);// 转化后的替代代码的长度
  • 代码分析
while(pOld < InEntryPoint + InEPSize)
{
        b1 = *(pOld);
        b2 = *(pOld + 1);
        OpcodeLen = 0;
        AbsAddr = 0;
        IsRIPRelative = FALSE;
        ...
}

       循环处理目标函数开始所有收到影响的指令,下面的代码都是在这个循环里面的

    // 检查指令前缀
        switch(b1)
        {
            case 0x67: // 地址大小重写前缀,后面的代码为16 位代码,我们不考虑
                       //  关于指令前缀:http://wiki.osdev.org/X86-64_Instruction_Encoding#Operand-size_and_address-size_override_prefix
                bCurrent16 = TRUE;
                // 标记当前指令包含0x67 前缀,处理下一个指令
                pOld++;
                continue;
            /*
                不用管 0x66 前缀[操作数/数据-大小前缀],因为我们仅仅需要直到当前是否为 [地址大小重写] 前缀
                0x 66 指令通常毫不改变地复制(除了 64-bit rip 相对地址),此时我们仅仅调整地址
            */
        }

       如果当前指令的第一个字节为0x67 的话,其代表的是地址大小重写前缀,当我们在Win32 程序进行16位地址操作时就会出现这样的前缀。关于这个前缀没有仔细研究,暂时忽略吧。

// 得到相对地址的值
        switch(b1)
        {
            case 0xE9: // jmp 16位 立即数/32位 立即数
            {
                /* 
                   当且仅当这个指令是入口的第一个指令时满足条件
                */
                if(pOld != InEntryPoint)
                    THROW(STATUS_NOT_SUPPORTED, L"Hooking far jumps is only supported if they are the first instruction.");
            }
            case 0xE8: // call 16-bit 立即数 / 32-bit 立即数
            {
                if(bCurrent16)// 16 位跳转,做过一个实验,测试当有0x67 前缀与没有0x67 前缀,指令的执行逻辑是相同的,可能是我对于这个前缀的理解不对,暂时不研究这个问题
                {
                    AbsAddr = *((__int16*)(pOld + 1));
                    OpcodeLen = 3;
                }
                else
                {
                    AbsAddr = *((__int32*)(pOld + 1));
                    OpcodeLen = 5;// 通常的call 指令,指令长度为5
                }
            }break;

            case 0xEB: // jmp 8 位立即数
            {
                AbsAddr = *((__int8*)(pOld + 1));
                OpcodeLen = 2;
            }break;
        /*
            条件跳转指令是不支持的
        */
            case 0xE3: // jcxz imm8
            {
                THROW(STATUS_NOT_SUPPORTED, L"Hooking near (conditional) jumps is not supported.");
            }break;
            case 0x0F:
            {
                if((b2 & 0xF0) == 0x80) // jcc imm16/imm32
                    THROW(STATUS_NOT_SUPPORTED,  L"Hooking far conditional jumps is not supported.");
            }break;
        }// switch(b1)

       我们看到上面的这个处理整体上是查找跳转以及call 指令的,如果开头是0xE9 即表示跳转指令的时候,该指令必须是函数的第一个指令,如果是call 指令的话,分为两种情况,一种是有0x67 指令前缀,一种是没有,对于0x67 前缀理解的不够现在暂且不谈,如果是普通的call 指令,这里得到了一个相对的偏移值放在了AbsAddr 中。如果代码中包含了条件跳转指令,将报错,因为此时代码的运行状态是不确定的,因此不支持。


        // 转换得到 mov eax,绝对地址
        if(OpcodeLen > 0)
        {
            AbsAddr += (POINTER_TYPE)(pOld + OpcodeLen);

#ifdef _M_X64
            *(pRes++) = 0x48; // 一种指令前缀,扩展使用64位操作数
#endif
            *(pRes++) = 0xB8;               // mov eax,
            *((LONGLONG*)pRes) = AbsAddr;   //          address

            pRes += sizeof(void*);

            // points into entry point?
            if((AbsAddr >= (LONGLONG)InEntryPoint) && (AbsAddr < (LONGLONG)InEntryPoint + InEPSize))
                /* 不支持跳转到我们自己写的跳转指令内部的操作 */
                THROW(STATUS_NOT_SUPPORTED, L"Hooking jumps into the hooked entry point is not supported.");

            // 插入 替代代码
            switch(b1)
            {
            case 0xE8: // call eax
                {
                    *(pRes++) = 0xFF;
                    *(pRes++) = 0xD0;
                }break;
            case 0xE9: // jmp eax
            case 0xEB: // jmp imm8
                {
                    *(pRes++) = 0xFF;
                    *(pRes++) = 0xE0;
                }break;
            }
            *OutRelocSize = (ULONG)(pRes - Buffer);
        }
        // 没有跳转指令,修正 RIP 相关的指令
        else
        {
            // 查看是否有RIP 相对寻址的指令,如果有的话,修正这些指令。
            FORCE(LhRelocateRIPRelativeInstruction((ULONGLONG)pOld, (ULONGLONG)pRes, &IsRIPRelative));
        }

       我们看到,当OpcodeLen > 0 也就代表当前指令为跳转或者call这类改变函数流程的指令的时候,我们会构建一个等价的跳转或者是call 指令,其中使用的是eax存储这个目标地址,当当前平台为64位,这里使用了一个指令前缀0x48,用于扩展访问64-bit的值;如果没有跳转指令,调用LhRelocateRIPRelativeInstruction函数。

EASYHOOK_NT_INTERNAL LhRelocateRIPRelativeInstruction(
            ULONGLONG InOffset,
            ULONGLONG InTargetOffset,
            BOOL* OutWasRelocated)
{
/*
Description:

    [若给定的指令是RIP 相关?重置它:什么都不做]
    [只支持 64-bit]
Parameters:

    - InOffset

        The instruction pointer to check for RIP addressing and relocate.

    - InTargetOffset

        The instruction pointer where the RIP relocation should go to.
        Please note that RIP relocation are relocated relative to the
        offset you specify here and therefore are still not absolute!

    - OutWasRelocated

        TRUE if the instruction was RIP relative and has been relocated,
        FALSE otherwise.
*/

#ifndef _M_X64
    return FALSE;   // 只有X64 存在RIP 相对寻址
#else
#ifndef MAX_INSTR
#define MAX_INSTR 100
#endif
    NTSTATUS            NtStatus;
    CHAR                Buf[MAX_INSTR];
    ULONG               AsmSize;
    ULONG64             NextInstr;
    CHAR                Line[MAX_INSTR];
    LONG                Pos;
    LONGLONG            RelAddr;
    LONGLONG            MemDelta = InTargetOffset - InOffset;//增量

    ULONGLONG           RelAddrOffset = 0;
    LONGLONG            RelAddrSign = 1;

    ASSERT(MemDelta == (LONG)MemDelta,L"reloc.c - MemDelta == (LONG)MemDelta");

    *OutWasRelocated = FALSE;

    /*
        BYTE t[10] = {0x8b, 0x05, 0x12, 0x34, 0x56, 0x78};
        udis86 outputs: 0000000000000000 8b0512345678     mov eax, [rip+0x78563412]     // 一个示例代码
    */
    // 反汇编当前指令
    if(!RTL_SUCCESS(LhDisassembleInstruction((void*)InOffset, &AsmSize, Buf, sizeof(Buf), &NextInstr)))
        THROW(STATUS_INVALID_PARAMETER_1, L"Unable to disassemble entry point. ");

    // 查看当前指令中是否有rip 相对寻址的指令]
    Pos = RtlAnsiIndexOf(Buf, '[');
      if(Pos < 0)
        RETURN;

    if (Buf[Pos + 1] == 'r' && Buf[Pos + 2] == 'i' && Buf[Pos + 3] == 'p' &&  (Buf[Pos + 4] == '+' || Buf[Pos + 4] == '-'))
    {
        // 找到了rip 相对指令
        /*
          Support negative relative addresses
          支持负的相对地址
          https://easyhook.codeplex.com/workitem/25592
            e.g. Win8.1 64-bit OLEAUT32.dll!VarBoolFromR8
            Entry Point:
              66 0F 2E 05 DC 25 FC FF   ucomisd xmm0, [rip-0x3da24]   IP:ffc46d4
            Relocated:
              66 0F 2E 05 10 69 F6 FF   ucomisd xmm0, [rip-0x996f0]   IP:100203a0
        */
        if (Buf[Pos + 4] == '-')
            RelAddrSign = -1;

        Pos += 4;
        // parse content
        if (RtlAnsiSubString(Buf, Pos + 1, RtlAnsiIndexOf(Buf, ']') - Pos - 1, Line, MAX_INSTR) <= 0)
            RETURN;

        // Convert HEX string to LONGLONG
        RelAddr = RtlAnsiHexToLongLong(Line, MAX_INSTR);
        if (!RelAddr)
            RETURN;

        // Apply correct sign
        RelAddr *= RelAddrSign;

        if(RelAddr != (LONG)RelAddr)
            RETURN;
        // 现在我们得到了rip + RelAddr 中的 RelAddr【正/负】 的值
        /*
          Ensure the RelAddr is equal to the RIP address in code
          确保RelAddr 等于 RIP 地址
          https://easyhook.codeplex.com/workitem/25487
          Thanks to Michal for pointing out that the operand will not always 
          be at *(NextInstr - 4)
          e.g. Win8.1 64-bit OLEAUT32.dll!GetVarConversionLocaleSetting 
              Entry Point:
                 83 3D 【71 08 06 00 00】    cmp dword [rip+0x60871], 0x0  IP:ffa1937
              Relocated:
                 83 3D 【09 1E 0B 00 00】    cmp dword [rip+0xb1e09], 0x0  IP:ff5039f
        */
        // 找到存储相对地址的地方
        for (Pos = 1; Pos <= NextInstr - InOffset - 4; Pos++) {
            if (*((LONG*)(InOffset + Pos)) == RelAddr) {
                if (RelAddrOffset != 0) {
                    // More than one offset matches the address, therefore we can't determine correct offset for operand
                    // 不仅有一个匹配的地址,因此我们不能决定正确的偏移for[操作数]
                    // 这个可能性基本没有???一个指令长度最大是有限度的,然后在里面有同样的两个地址???
                    RelAddrOffset = 0;  
                    break;
                }

                RelAddrOffset = Pos;
            }
        }

        if (RelAddrOffset == 0) {
            THROW(STATUS_INTERNAL_ERROR, L"The given entry point contains a RIP-relative instruction for which we can't determine the correct address offset!");
        }

        /*
            重置这个指令
        */
        // Adjust the relative address
        RelAddr = RelAddr - MemDelta;// InTargetOffset - InOffset;
        // Ensure the RIP address can still be relocated
        if(RelAddr != (LONG)RelAddr)
            THROW(STATUS_NOT_SUPPORTED, L"The given entry point contains at least one RIP-Relative instruction that could not be relocated!");

        // 拷贝指令到 目标地址
        RtlCopyMemory((void*)InTargetOffset, (void*)InOffset, (ULONG)(NextInstr - InOffset));
        // 利用上面找到的偏移修正 rip 相对地址
        *((LONG*)(InTargetOffset + RelAddrOffset)) = (LONG)RelAddr;

        *OutWasRelocated = TRUE;
    }

    RETURN;

THROW_OUTRO:
FINALLY_OUTRO:
    return NtStatus;
#endif
}

       修正RIP 相对寻址总的来说就是根据指令是’+’或者’-‘,以及老RIP 相对寻址指令与 新指令位置的差值生成新的等价指令的过程。注释写的比较明白,这里就不再过多的解释了。

        // 与前面 对应,pOld 需要-- 操作
        if (bCurrent16) pOld--;

        // 找到下一个指令
        FORCE(InstrLen = LhGetInstructionLength(pOld));

        // 没有找到跳转指令,直接
        if(OpcodeLen == 0)
        {
            // 不是RIP 相关的指令,直接拷贝这个指令

            if(!IsRIPRelative) // RIP 指令相关的指令已经在上面的处理中拷贝到了pRes;
                RtlCopyMemory(pRes, pOld, InstrLen);

            pRes += InstrLen;
        }

        pOld += InstrLen;// 转移到了下一个指令
        IsRIPRelative = FALSE;
        bCurrent16 = FALSE;

       到这里我们就可以清晰的认识这个函数对于目标函数前几个指令的处理过程了。特殊的指令需要处理,主要包括:“绝对jmp 指令、call 指令,RIP 相对寻址的指令”,需要注意的是条件跳转指令是不支持的,跳转到我们所覆盖的指令的地址范围内的指令不支持。其它的指令直接拷贝即可,因为其执行效果与指令运行的位置无关。

    // 在重定向入口点增加跳转指令,将继续执行原方法的下一个指令
    // 64-bit 驱动 为绝对跳转
#ifdef X64_DRIVER

    // 得到入口点的第一个指令的下一个指令的地址
    RelAddr = (LONGLONG)(Hook->TargetProc + Hook->EntrySize);
    // 修正的入口点函数的下一个指令为:跳转到本来的入口点函数的下一个指令的地址
    RtlCopyMemory(Hook->OldProc + *RelocSize, Jumper_x64, X64_DRIVER_JMPSIZE);
    // Set address to be copied into RAX
    RtlCopyMemory(Hook->OldProc + *RelocSize + X64_DRIVER_JMPADDR_OFFSET, &RelAddr, 8);

#else
    // 其它的都是相对跳转
    // 相对跳转的公式:p2-(p1+本指令的字节数)=后面填充的相对地址
    RelAddr = (LONGLONG)(Hook->TargetProc + Hook->EntrySize) - ((LONGLONG)Hook->OldProc + *RelocSize + 5);

    if(RelAddr != (LONG)RelAddr)
        THROW(STATUS_NOT_SUPPORTED, L"The given entry point is out of reach.");

    Hook->OldProc[*RelocSize] = 0xE9;   // 执行完前面的指令完之后就跳转到原来的函数去执行

    RtlCopyMemory(Hook->OldProc + *RelocSize + 1, &RelAddr, 4);

#endif

       上面这段代码将‘跳转到原函数被覆盖指令的后一条指令的代码’放到了重新生成的被覆盖的指令的后面。在执行完新生成的替代代码之后,代码将跳转到原来的位置继续执行
       有什么错误的地方,恳请各位不吝赐教。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
目前最好的EasyHook的完整Demo程序,包括了Hook.dll动态库和Inject.exe注入程序。 Hook.dll动态库封装了一套稳定的下钩子的机制,以后对函数钩子,只需要填下数组表格就能实现了,极大的方便了今后的使用。 Inject.exe是用MFC写的界面程序,只需要在界面上输入进程ID就能正确的HOOK上相应的进程,操作起来非常的简便。 这个Demo的代码风格也非常的好,用VS2010成功稳定编译通过,非常值得下载使用。 部分代码片段摘录如下: //【Inject.exe注入程序的代码片段】 void CInjectHelperDlg::OnBnClickedButtonInjectDllProcessId() { ////////////////////////////////////////////////////////////////////////// //【得到进程ID值】 UINT nProcessID = 0; if (!GetProcessID(nProcessID)) { TRACE(_T("%s GetProcessID 失败"), __FUNCTION__); return; } ////////////////////////////////////////////////////////////////////////// //【得到DLL完整路径】 CString strPathDLL; if (!GetDllFilePath(strPathDLL)) { TRACE(_T("%s GetDllFilePath 失败"), __FUNCTION__); return; } ////////////////////////////////////////////////////////////////////////// //【注入DLL】 NTSTATUS ntStatus = RhInjectLibrary(nProcessID, 0, EASYHOOK_INJECT_DEFAULT, strPathDLL.GetBuffer(0), NULL, NULL, 0); if (!ShowStatusInfo(ntStatus)) { TRACE(_T("%s ShowStatusInfo 失败"), __FUNCTION__); return; } } //【Hook.dll动态库的代码片段】 extern "C" __declspec(dllexport) void __stdcall NativeInjectionEntryPoint(REMOTE_ENTRY_INFO* InRemoteInfo) { if (!DylibMain()) { TRACE(_T("%s DylibMain 失败"), __FUNCTION__); return; } } FUNCTIONOLDNEW_FRMOSYMBOL array_stFUNCTIONOLDNEW_FRMOSYMBOL[]= { {_T("kernel32"), "CreateFileW", (void*)CreateFileW_new}, {_T("kernel32"), "CreateFileA", (void*)CreateFileA_new}, {_T("kernel32"), "ReadFile", (void*)ReadFile_new} }; BOOL HookFunctionArrayBySymbol() { /////////////////////////////////////////////////////////////// int nPos = 0; do { /////////////////////////////// FUNCTIONOLDNEW_FRMOSYMBOL* stFunctionOldNew = &g_stFUNCTIONOLDNEW_FRMOSYMBOL[nPos]; if (NULL == stFunctionOldNew->strModuleName) { break; } /////////////////////////////// if (!HookFunctionBySymbol(stFunctionOldNew->strModuleName, stFunctionOldNew->strNameFunction, stFunctionOldNew->pFunction_New)) { TRACE(_T("%s HookFunctionBySymbol 失败"), __FUNCTION__); return FALSE; } } while(++nPos); /////////////////////////////////////////////////////////////// return TRUE; } HANDLE WINAPI CreateFileW_new( PWCHAR lpFileName, DWORD dwDesiredAccess, DWORD dwShareMode, LPSECURITY_ATTRIBUTES lpSecurityAttributes, DWORD dwCreationDisposition, DWORD dwFlagsAndAttributes, HANDLE hTemplateFile ) { TRACE(_T("CreateFileW_new. lpFileName = %s"), lpFileName); return CreateFileW( lpFileName, dwDesiredAccess, dwShareMode, lpSecurityAttributes, dwCreationDisposition, dwFlagsAndAttributes, hTemplateFile); }
Windows钩子函数是一种Windows API机制,它允许程序在操作系统拦截和监视特定事件或消息。这些事件或消息可以是键盘、鼠标、消息队列等。钩子函数通常用于记录用户输入,或者在特定条件下触发自定义操作。 实现Windows钩子函数需要以下步骤: 1. 定义钩子函数 钩子函数是一个回调函数,当特定事件或消息发生时,操作系统将调用该函数钩子函数需要根据钩子类型和事件类型进行定义,例如键盘钩子函数可以监视按键事件,鼠标钩子函数可以监视鼠标事件等。 2. 安装钩子 安装钩子需要使用`SetWindowsHookEx`函数。该函数需要三个参数:钩子类型、钩子函数地址、以及钩子函数所属进程的句柄。钩子类型可以是全局钩子或局部钩子,具体取决于监视的事件或消息。 3. 卸载钩子 卸载钩子需要使用`UnhookWindowsHookEx`函数。该函数需要一个参数,即之前安装钩子时返回的句柄。 下面是一个示例键盘钩子函数实现: ```c++ LRESULT CALLBACK KeyboardHook(int nCode, WPARAM wParam, LPARAM lParam) { if (nCode >= 0) { // 拦截到键盘事件 PKBDLLHOOKSTRUCT p = (PKBDLLHOOKSTRUCT)lParam; if (wParam == WM_KEYDOWN) { // 按键按下事件 // 处理按键事件 } } return CallNextHookEx(NULL, nCode, wParam, lParam); // 调用下一个钩子 } int main() { HHOOK hook = SetWindowsHookEx(WH_KEYBOARD_LL, KeyboardHook, NULL, 0); // 安装键盘钩子 // ... UnhookWindowsHookEx(hook); // 卸载键盘钩子 return 0; } ``` 在上面的示例,`KeyboardHook`函数是一个键盘钩子函数,它拦截键盘事件并进行处理。在`main`函数,使用`SetWindowsHookEx`函数安装了一个全局键盘钩子,并使用`UnhookWindowsHookEx`函数卸载了该钩子
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值