0%

Windows中Hook函数

使用远程线程进行DLL的注入 中我们知道怎么注入,但是注入后我们还可以做更多的事情,比如 HOOK 掉一个函数。微软官方把这个也叫绕行,它就出了一个官方的库 Detours,当然也有一些替代品如:EasyHook

官方的称呼叫做 Detour(绕行),它描述的意思是:汇编指令跳转到了一个不同的位置,最重要的是重定向了执行的流。通常,我们通过这样来将代码绕行到我们自己代码区域内。这样就可以强制让程序来执行我们的代码。

这样做的关键是不要搞坏了 ,我们执行我们自己写的代码,然后跳回到合适的地方。

Mid Function Hooking

Detour 有些时候也会被叫做函数中的 hook,不过如果你只是绕行了一个函数的第一个字节,这就不会叫做函数 hook。将绕行放到一个函数的第一个字节是非常容易被反检测系统检查到,而函数中间 hook 则更难检测到一些。反检测系统很容易检查第一个字节,但并不总是检查函数中的每个字节。如果反检测系统会读取函数的第一个字节,那么我们需要用汇编来编写我们的绕行,这就是一个函数中间 hook。

External Detouring/Hooking

通常,我们会通过 WriteProcessMemory 来将我们的代码写到目标进程中,然后使用这个函数来使已存的代码跳转到我们的代码。External Detouring/Hooking 会更复杂一些,因为我们必须将 shellcode 写到目标进程。在程序内部是很容易的。但在外部,我们必须将汇编代码转换成字节作为shellcode 写到目标进程。

Stolen Bytes

就我们自己的绕行代码覆盖写入的内存区域也叫做 窃取字节,我们知道重写了多少字节很重要,然后这些字节必须在我们绕行后执行,否则就会搞坏栈和寄存器。这些字节并不总是相同,比如你覆盖写入一个8字节的指令,我们的绕行只有 5 字节。这种情况下,我们需要从窃取字节中复制8字节出来。我们不能执行一个指令的 75%。

我们必须复制出窃取字节,然后执行我们自己的代码,最后再执行这些窃取字节来保证不会让程序崩溃。

如果窃取的字节包含了一个 JMP,跳转到相对地址,或者一些其他的相对寻址,它将会失败因为他们不会在一个不同的位置执行。相对地址指下来vyua EIP,因为我们将这些字节复制到了一个不同的内存地址,EIP 就不同了。

Code Caves

这指的是目标进程中未并程序使用的内存区域。通常我们认为它是已分配,但没有包含任何东西,程序却需要。当使用绕行的时候,我们可以将代码写在这个区域而不用分配内存。不用分配内存会更简单些。如果我们使用 Code Caves,内存页必须有执行权限。我们可以通过 VirtualProtect[Ex]来修改它。

在早些时候,code caves 指的是进程分配却没有使用的内存。现在人们都会分配自己的内存,但也这样叫。大多数情况下,这无关紧要,分配我们的内存更容易。如果我们拥有一个写权限的进程,那可能我们就不会遇到反检测的问题。

HOOK

通常和 Detour 换用。当个人认为,hooking 指的是将代码重定向到我们自己的代码;而 detouring 并不一定绕行到我们的代码,只是绕行程序内存的其他位置。

基本的 X86 绕行代码

 bool Detour32(void * src, void * dst, int len)
{
if (len < 5) return false;

DWORD curProtection;
VirtualProtect(src, len, PAGE_EXECUTE_READWRITE, &curProtection);

memset(src, 0x90, len);

uintptr_t relativeAddress = ((uintptr_t)dst - (uintptr_t)src) - 5;

*(BYTE*)src = 0xE9;
*(uintptr_t*)((uintptr_t)src + 1) = relativeAddress;

DWORD temp;
VirtualProtect(src, len, curProtection, &temp);

return true;
}
  • src 想要放 JMP 指令的内存地址
  • dst 想要跳转到的内存地址(可能是我们自己写的代码)
  • len 重写的指令占用的字节数

len 通常是 5 字节,但这依赖于我们要重写的东西,可能会更多。

我们检查最少需要 5 字节是因为 在 X86 中这是 JMP 指令的最小长度。我们将通过重写来销毁的指令最少也是5字节。

VirtualProtect 用来获取要重写区域内存的权限

将我们重写的内存区域初始化为 0x90(这是 NOP)指令,这不是 100% 必要,不过是一个很好的方法。当我们的调试的时候,可以看到 0x90 是否被写入这样就知道在做正确的事情了。

然后我们计算在目标地址和 src 地址间的相对地址(相减,再减去 len)。这个计算的结果是 从我们重写的最后一个字节到我们要跳转到的地址 的偏移量。注意了:我们使用的是相对跳转,而不是绝对跳转,所以必须在运行时计算

0xE9 是 JMP 指令,然后写入偏移。

最后,再重置权限。

我们自己的代码呢?

我们必须在 declspec naked 函数中执行我们的代码,必须保存寄存器和栈这样我们不会搞坏栈和寄存器,必须执行窃取字节。然后必须跳转到 src+len 地址。