作者 | 榴莲
编辑 | 楌橪
Windows操作系统中存在多种异常处理,我们现在需要的是其中的VEH(VectoredExceptionHandler)异常处理,也就是向量化异常处理。我们之所以可以使用VEH异常来进行HOOK的主要原因,在于两点。其一,VEH异常处理的优先级是高于SEH异常处理的,也就是说可以先手拿到异常,确保不会被其他异常处理流程将异常截获而导致HOOK失败。其二就是在VEH异常处理的回调函数中,可以获取及修改异常发生处的上下文环境,这就意味着我们可以操作的东西会非常多,例如通过上下文环境中的ESP(栈顶指针寄存器)就可以拿到HOOK位置触发异常时的堆栈数据。而我们设置的HOOK位置通常位于函数内部的起始位置,这就意味着我们可以直接通过堆栈里的数据获取到被HOOK函数的参数,并且可以对其进行修改。
在我们之前的文章中,我们已经使用过利用软件断点(int 3 0xCC)触发异常,实现HOOK。
但是这种方法也是有一定缺陷的。例如,如果目标进程具有CRC32一类的完整性检查,int3 软件断点又会修改指令。这样就无法通过完整性检查了。所以,我们这一次,提出一种新的方式。依然是基于VEH异常的,但是可以实现“无痕”的效果。不修改任何一个字节就完成HOOK。那么,这种方式就是基于硬件调试寄存器实现的。也就是硬件断点。因为硬件断点的地址是存储在寄存器里的,所以不会修改内存。
那么在学习具体的HOOK方法之前,我们首先需要了解一下硬件断点的基本知识:
上图就是Intel手册中对于调试断点的说明图,下面我们对其字段进行一定解释:
DR0 - DR3就是用来保存硬件断点的地址的,这个地址是线性地址而不是物理地址,因为CPU是在线性地址被翻译成物理地址之前出处理断点的,也因此,我们在保护模式内不能用调试寄存器对物理内存地址设置断点。
DR4和DR5是保留的,如果调试扩展开启了(CR4的DE位设置成1就是开启了),任何对DR4和DR5的调用都会导致一个非法指令异常#UD,如果调试扩展禁用了,那么DR4和DR5其实就是DR6和DR7的别名寄存器。
DR7寄存器是调试控制寄存器:
R/W0 - R/W3 读写域 四个读写域分别与DR0-DR3寄存器所对应,用来指定被监控地点的访问类型。
占两位,所以有以下四种状态:
00:仅执行对应断点的时候中断(执行断点)
01:仅写数据中断(写入断点)
10:(需要开启CR4的DE【调试扩展】)I/O时中断
11:读写数据都中断,但是读指令除外(访问断点)
LEN0 - LEN3 长度域 四个长度域分别与DR0-DR3寄存器所对应,用来指定监控区域的长度
占两位,所以有以下四种状态:
00:1字节长
01:2字节长
10:8字节长
11:4字节长
如果R/W位是00,那么这里应该设置成0
L0-L3 局部断点启用 分别与DR0-DR3寄存器所对应,对应项为1就是开启断点,为0就是关闭断点,执行后自动清除该位
G0-G3 全部断点启用 分别与DR0-DR3寄存器所对应,对应项为1就是开启断点,为0就是关闭断点,CPU不会主动清除
LE和GE 忽略即可,高版本CPU不用了,486之前才会用
GD启用访问检测,如果GD是1,那么CPU遇到修改DR寄存器的指令,会产生一条异常。
DR6寄存器是调试状态寄存器
B0-B3 分别与DR0-DR3寄存器所对应,如果B0被置1了,那说明R/W0 len0 DR0的条件都被满足了
BD 与DR7的GD位相关联,当CPU发现了需要修改DR寄存器的指令,那么就会停止执行,把BD设置成1,然后交给#DB的处理程序
BS 单步 与EFLAGS里的TF位相关联,如果这一位是1,则表示是单步触发的
BT 任务切换 与任务段相关,TSS的T标志(调试陷阱)相关联,当任务切换,发现下一个TSS的T是1,那么就会中断到调试中断程序里
了解了以上内容之后,我们开始编写具体的无痕HOOK,以MessageBoxA的HOOK为例:
我这里采用的操作系统是Windows 10 20H2(19042.1288),集成开发环境采用的是Visual Studio 2017。那么我们先来创建一个DLL项目。步骤如下:
1.选择新建项目
2:选择Windows桌面->动态链接库(DLL),点击确定
3:注释#include “pch.h”,添加#include <Windows.h>。删除framework.h、pch.h以及pch.cpp文件。
4:配置
4.1 选择属性
4.2 修改运行库以及Spectre缓解,选择应用
4.3 修改预编译头,选择应用
5. 在每一个分支中,添加break,防止DLL注入失败。
6.使用AddVectoredExceptionHandler函数添加一个VEH异常的回调。VEH的回调是一个链表,挂着很多的处理程序。AddVectoredExceptionHandler函数的第一个参数就是用来指定新增VEH处理函数位于链表的哪个位置。如果第一个参数的值是0,那么新增的VEH处理函数将处于整个链表的最后。如果第一个参数的值是一个非0值,那么新增的VEH处理函数就会位于链表的头部。AddVectoredExceptionHandler函数的第二个参数就是指定新增的回调函数
6.1函数的原型可以通过在AddVectoredExceptionHandler的函数上按F12,看到函数的原型。
6.2在红框的位置上,继续F12,就可以看到这个参数的原型,实际上就是一个回调函数的函数指针原型。
6.3然后复制出来,修改成如下样式。去掉typedef,把指针修改成函数名,增加花括号的函数体,异常处理的部分就需要在函数体内实现。
6.4最后,将其添加到DLL_PROCESS_ATTACH中。
6. 获取MessageBoxA的函数地址,并且保存起来
7. 这里我们需要编写一个HOOK函数
8. 硬件断点和软件断点是有区别的,软件断点是修改内存,所以只需要修改一处,但是因为硬件调试寄存器是每个线程一套,所以如果需要HOOK函数而不漏接,那么就是需要对所有线程的函数都下一个硬件断点。具体操作方式如下:
9. 在DLL_PROCESS_ATTACH中调用HOOK函数
10. 同样是因为硬件断点属于线程环境,所以当创建新线程的时候,需要调用SetThreadHook函数对新线程进行HOOK。
11. 接下来,我们需要在异常处理函数内,处理HOOK,首先判断是不是我们自己的HOOK地址
12.处理参数的HOOK
在这部分代码中,之所以需要将EIP + 2。是因为在x86的函数头是如下样式的。
也就是说,我们的断点0xCC实际上就是改在了红框的位置上,替换了8B,而原本的函数中,8BFF的硬编码组成了mov edi,edi,这里实际上别没有什么用,所以我们如果直接跳过这两个硬编码也并不会影响程序的正常执行。
12. 如果不是我们自己的HOOK地址,重新下一次HOOK,防止HOOK失效
13.生成文件
14.取出文件到桌面或其他位置
15.测试HOOK效果
15.1首先写一个目标程序,代码如下
15.2使用注入器(自行编写或网上下载,这里我用的是自己写的)将我们生成的模块注入到目标进程中。
正常情况下:
HOOK后:
到了这里,我们就完成了整个无痕HOOK的代码编写。