设为首页 加入收藏

TOP

Windows下x86和x64平台的Inline Hook介绍(一)
2023-07-23 13:27:21 】 浏览:211
Tags:Windows x86 x64 平台的 Inline Hook 介绍

前言

我在之前研究文明6的联网机制并试图用Hook技术来拦截socket函数的时候,熟悉了简单的Inline Hook方法,但是由于之前的方法存在缺陷,所以进行了深入的研究,总结出了一些有关Windows下x86和x64架构程序的Inline Hook方法。

本文使用的方法并非最优,也没有保证安全,但是用较少的代码实现了所需的功能,非常适合用来学习Inline Hook的基本原理和一般的使用方法。

由于本文是在Windows平台下的,所以你需要对Windows系统的机制需要有一定的了解;同时本文的代码基于C语言(当然C++编译器也可以编译),所以你应该要有C语言的基础(尤其是对指针的理解);此外,你还需要有一定的8086汇编(如果x86和x64更好)基础,因为本文涉及到部分汇编指令。

本文假定你对以上这些内容有一定基础,但并不非常熟悉,如果你完全了解,可以适当跳过部分内容。

如果你对更高级的内容有兴趣,本文后面也会对这些东西做一个介绍,有兴趣可以进一步了解。

在开始之前,先说明一下本文所有提到的完整代码都可以在这个链接找到:https://gitcode.net/PeaZomboss/miscellaneous/-/tree/main/230131-inlinehook

正文

Windows下的Hook机制,最早是用来在提供类似于DOS下的中断机制,当然还有更多其他功能。Hook技术有许许多多的分类,本文所用的就是其中一种:Inline Hook。

所谓Inline Hook,一般是修改一个函数头部的代码,使其跳转到指定的地址。这样当调用这个函数的时候,实际上执行的是我们设定的代码。

正因为如此,我们可以用Hook技术来拦截操作系统的API,或者某个软件的关键函数,然后拦截获取信息或者修改其内容,从而达到我们的目的。比如微信QQ的防撤回就是这样实现的,游戏对战平台也一般是这样做的。

后面要介绍两种Inline Hook的方法。其中第一种比较简单,但效果较差,尤其是在x64和多线程的情况下;而第二种效果好,尤其是x64以及多线程的情况下,但是操作较为复杂。

而许多更高级的功能基本就是在第二种方法的基础上扩展的。

为了方便演示,我选择了kernelbase.dll的函数WriteConsoleA,因为这个函数可以直接在控制台输出一段指定字符串,便于我们查看Hook的效果。

如果你通过windows.h头文件导入WriteConsoleA这个函数,会发现它调用了kernel32.dll的WriteConsoleA而不是kernelbase.dll的,这个你可以去反汇编看看,但是在kernel32.dll内部,你会发现函数头部就是一句jmp指令,而真正执行的是kernelbase.dll里的函数,所以一般选择要Hook的函数的时候,如果这个函数头部是一句跳转指令,则去修改跳转过去的地址。

简单的Hook

这部分Hook方法是最简单的,对于x86和x64仅有汇编指令的不同,但根本逻辑是完全一样的。

这种方法之所以简单,是因为不需要什么复杂的操作和概念,只要简单修改函数的头部代码,然后需要调用原来的代码的时候再给他改回去就行了。

但是因为要改来改去的,所以在多线程的情况下会遇到问题,这个在之后讨论。

x86

对于x86的Hook,方法比较简单,使用一句跳转指令就可以了:

jmp addr_diff

由于jmp指令有好多种用法,我们这里用的是寻址范围±2G的指令,所以编译成机器码有5个字节,第一个字节是0xE9,剩下4个字节是目标地址相对当前EIP的差值。

比如被Hook的函数地址是7FF01000,我们就修改7FF01000处的代码,使其跳转到我们00401000处代码,代码如下:

...
00401000  ???
...
7FF01000  E9 FBFF4F80  jmp 00401000
7FF01005  ???
...

注意这里的FBFF4F80,实际上是用小端表示的0x804FFFFB,记得刚刚说的吧,是目标地址相对当前EIP的差值。在执行7FF01000这一句的时候,EIP已经不是7FF01000了,而是7FF01005,因为EIP始终指向当前执行指令的下一个指令。

我们可以计算得出0x7FF01005+0x804FFFFB=0x100401000,由于EIP是32位寄存器,所以实际上执行这一句后EIP就会被设为00401000,这样就使得代码执行到了我们的地方了。

所以我们可以得出这样一个计算公式,假定被我们Hook的代码地址是addr_hook,而我们替换的地址是addr_fake,那么跳转语句jmp addr_diff的addr_diff=addr_hook-addr_fake-5。

代入刚刚的数据,0x804FFFFB=0x00401000-0x7FF01000-0x5,只取低32位,可以发现这个等式成立。

那么方法就很简单了,我们只要知道被Hook函数的地址,用来替换的函数的地址,即可计算出修改的指令,当然修改之前要先保存一下原来的指令,以便到时候改回去。具体操作在后面的实例讲解会有说明。

x64

对于x64来说,除了头部修改的字节数和跳转的指令不同,其余和x86的情况完全一致。

不过这个汇编指令就不能再像x86一样简单用jmp指令了,因为似乎没有一个jmp指令可以跨大于±2G的内存地址空间。

作为jmp的替代,我们可以用寄存器寻址或者压栈配合ret指令实现同样的效果:

mov rax, address
jmp rax

或者

mov rax, address
push rax
ret

以上两段代码效果一样,而且都占用12个字节,但缺点一致——会改变寄存器的值。

由于改变寄存器的值可能会影响程序运行结果,我们可以用如下代码避免这种情况:

push address.low
mov dword [rsp+4], address.high
ret

注意这里的address.low表示地址的低4字节,address.high表示地址的高4字节。

这段代码的原理是在x64汇编中,push指令只能处理4个字节的立即数,但是由于栈是8字节对齐的,所以执行第一句指令的时候,栈里会压入8字节内容,其中低4字节就是push的值,而高4字节会补0,此时我们可以通过rsp寄存器间接寻址再把那高4字节立即数放入栈里。

相对之前的两段代码,这段代码的好处是不会修改寄存器,不过缺点是指令长度要多2个字节。不过为了确保不会出现问题,我们就选择这个方法。

实例

首先看一下微软文档关于WriteConsoleA这个函数的原型说明:

BOOL WINAPI WriteConsole(
  _In_             HANDLE  hConsoleOutput,
  _In_       const VOID    *lpBuffer,
  _In_             DWORD   nNumberOfCharsToWrite,
  _Out_opt_        LPDWORD lpNumberOfCharsWritten,
  _Reserved_       LPVOID  lpReserved
);

注意这个函数原型就是一个宏,在Unicode下实际调用的是WriteConsoleW,ANSI下则是WriteConsoleA。推荐是直接调用Wri

首页 上一页 1 2 3 4 5 6 下一页 尾页 1/6/6
】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
上一篇win64环境下监听键盘操作并发送MQ.. 下一篇修改Windows远程桌面3389端口

最新文章

热门文章

Hot 文章

Python

C 语言

C++基础

大数据基础

linux编程基础

C/C++面试题目