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 相对寻址的指令”,需要注意的是条件跳转指令是不支持的,跳转到我们所覆盖的指令的地址范围内的指令不支持。其它的指令直接拷贝即可,因为其执行效果与指令运行的位置无关。
上面这段代码将‘跳转到原函数被覆盖指令的后一条指令的代码’放到了重新生成的被覆盖的指令的后面。在执行完新生成的替代代码之后,代码将跳转到原来的位置继续执行。
引用