简析利用调试寄存器实现内核函数的HOOK
来源:安全焦点
作者:yykingking (yykingking_at_126.com)
某些RK,木马会经常HOOK一些关键函数从而达到隐藏等目的,而相应的ARK检测软件也会通常会先恢复这些关键函数的HOOK(譬如利用硬盘文件恢复),然后再调用来检测RK,这样就可以检测出某些隐藏.下面就介绍利用调试器实现某些内核函数的HOOK.
Intel386以后的系列CPU增加了8个32位的调试寄存器,从Dr0到Dr7,方便调试使用.如果设置了相应的调试信息,在条件满足的情况下将会发生 1 号(DB例外)中断,CPU就会陷入中断例程,执行中断代码,我们的HOOK目的就可以通过这个实现.
首先看下面百度出来的对寄存器组的使用方法的解释:
这八个寄存器中由四个用于断点,两个用于控制,另两个保留未用。对这八个寄存器的访问,只能在0级特权级进行。在其它任何特权级对这八个寄存器中的任意一个寄存器进行读或写访问,都将产生无效操作码异常。此外,这八个寄存器还可用DR6及DR7中的BD位和GD位进行进一步的保护,使其即使是在0级也不能进行读出或写入。
对这些寄存器的访问使用通常的MOV指令:
MOV reg Dri 该指令将调试寄存器i中的内容读至通用寄存器reg中;
MOV Dri reg 该指令将通用寄存器reg中的内容写至调试寄存器i中。此处i的取值可以为0至7中的任意值。
这些寄存器的功能如下:
DR0—DR3 寄存器DR0—DR3包含有与四个断点条件的每一个相联系的线性地址(断点条件则在DR7中)。因为这里使用的是线性地址,所以,断点设施的操作,无论分页机制是否启用,都是相同的。
DR4—DR5 保留。
DR6是调试状态寄存器。当一个调试异常产生时,处理器设置DR6的相应位,用于指示调试异常发生的原因,帮助调试异常处理程序分析、判断,以及作出相应处理。
DR7是调试控制寄存器。分别对应四个断点寄存器的控制位,对断点的启用及断点类型的选择进行控制。所有断点寄存器的保护也在此寄存器中规定。(下面这图怎么也改不好,大家还是去百度吧)
|---------------|----------------|
Dr6 | |BBB BBB B |
| TSD 3 2 1 0 |
| --------------|----------------|
Dr7 |RWE LEN … RWE LEN | G GLGLGLGLGL |
| 3 3 … 0 0 | D EE33221100 |
|---------------|----------------|
31 15 0
DR6各位的功能
B0—B3(对应0-3位) 当断点线性地址寄存器规定的条件被检测到时,将对应的B0—B3位置1。置位B0—B3与断点条件是否被启用无关。即B0—B3的某位被置1,并不表示要进行对应的断点异常处理。
BD(13位) 如下一条指令要对八个调试寄存器之一进行读或写时,则在指令的边界BD位置1。在一条指令内,每当即将读写调试寄存器时,也BD位置1。BD位置1与DR7中GD位启用与否无关。
BS(14位) 如果单步异常发生时,BS位被置1。单步条件由EFLAGS寄存器中的TF位启用。如果程序由于单步条件进入调试处理程序,则BS位被置1。与DR6中的其它位不同的是,BS位只在单步陷阱实际发生时才置位,而不是检测到单步条件就置位。
BT(15位) BT位对任务切换导致TSS中的调试陷阱位被启用而造成的调试异常,指示其原因。对这一条件,在DR7中没有启用位。
DR6中的各个标志位,在处理机的各种清除操作中不受影响,因此,调试异常处理程序在运行以前,应清除DR6,以避免下一次检测到异常条件时,受到原来的DR6中状态位的影响。
DR7各位的功能
LEN LEN为一个两位的字段,用以指示断点的长度。每一断点寄存器对应一个这样的字段,所以共有四个这样的字段分别对应四个断点寄存器。LEN的四种译码状态对应的断点长度如下
LEN 说明
0 0 断点为一字节
0 1 断点为两字节
1 0 保留
1 1 断点为四字节
这里,如果断点是多字节长度,则必须按对应多字节边界进行对齐。如果对应断点是一个指令地址,则LEN必须为00
RWE RWE也是两位的字段,用以指示引起断点异常的访问类型。共有四个RWE字段分别对应四个断点寄存器,RWE的四种译码状态对应的访问类型如下
RWE 说明
0 0 指令
0 1 数据写
1 0 保留
1 1 数据读和写
GE/LE GE/LE为分别指示准确的全局/局部数据断点。如果GE或LE被置位,则处理器将放慢执行速度,使得数据断点准确地把产生断点的指令报告出来。如果这些位没有置位,则处理器在执行数据写的指令接近执行结束稍前一点报告断点条件。建议读者每当启用数据断点时,启用LE或GE。降低处理机执行速度除稍微降低一点性能以外,不会引起别的问题。但是,对速度要求严格的代码区域除外。这时,必须禁用GE及LE,并且必须容许某些不太精确的调试异常报告。
L0—L3/G0—G3 L0—L3及G0—G3位
分别为四个断点寄存器的局部及全局启用信号。如果有任一个局部或全局启用位被置位,则由对应断点寄存器DRi规定的断点被启用。
GD GD位启用调试寄存器保护条件。注意,处理程序在每次转入调试异常处理程序入口处清除GD位,从而使处理程序可以不受限制地访问调试寄存器。
前述的各个L位(即LE,L0—L3)是有关任务的局部位,使调试条件只在特定的任务启用。而各个G位(即GD,G0—G3)是全局的,调试条件对系统中的所有任务皆有效。在每次任务切换时,处理器都要清除L位。
如果你耐心把上面的信息看完了,基本上也就应该明白了.其实我们可以利用调试寄存器做的不只是函数的HOOK,也可以进行I/O的HOOK,下面要说的是在指定的内核函数上下指令执行断点,然后挂接1号中断实现HOOK(有很多种方法可以实现,看你自己喜欢哪种了).
首先把DR0寄存器设置为要挂接的内核函数的地址(譬如ZwCreateFile),然后修改 Dr7的L0和G0(第0和第1位)都为1, Dr7的 R/W0(16.17位) 为00, LEN0(18.19位)位为00,这样当CPU执行到ZwCreateFile地址的时候,就会进入1号中断例程.
然后我们应该去修改IDT表,将1号中断指向我们的处理程序.
接着需要考虑在中断例程我们要做的事情,关于中断时CPU具体做了什么,大家可以去搜索.下面只介绍我们所关心的在内核空间发生DB例外时的情况:中断发生时,依次将 EFLAGS,CS,EIP压入堆栈,然后进入中断程序(由于中断地址本来就在内核空间,所以不需要切换堆栈).在中断处理程序中作出相应的处理,然后使用 iretd 指令退出中断.( iretd 指令: 依次将堆栈弹出到 EIP,CS,EFLAGS),我们可以通过修改堆栈中EIP的值,在中断返回时跳转实现HOOK.
上面就是主要的内容,但是还有点问题.Windows在KiFastCall和线程切换时会修改Drx的值,为了防止我们的断点被清除,可以利用 Dr7:GD位保护寄存器,这样任何对调试寄存器的操作(读和写)都会产生DB例外然后进入1号中断例程.这样,我们在中断例程中又需要利用Dr6的标识位处理那些因为操作调试寄存器产生的例外(关于这个我只是简单的跳过了那些对DRX操作的代码,并没有详细分析).
下面可以看详细的实现代码:
简单实现HOOK下ZwCreateFile,XP下,其他系统慎用
/*
drxhook.h
Written By [email protected]
*/
#ifndef _DRX_HOOK
#define _DRX_HOOK
#include <ntddk.h>
typedef unsigned long DWORD;
typedef unsigned char BOOL;
#pragma pack(push,1)
typedef struct _idtr
{
//定义中断描述符表的限制,长度两字节;
short IDTLimit;
//定义中断描述服表的基址,长度四字节;
unsigned int IDTBase;
}IDTR,*PIDTR;
typedef struct _IDTENTRY
{
unsigned short LowOffset;
unsigned short selector;
unsigned char unused_lo;
unsigned char segment_type:4; //0x0E is an interrupt gate
unsigned char system_segment_flag:1;
unsigned char DPL:2; // descriptor privilege level
unsigned char P:1; /* present */
unsigned short HiOffset;
} IDTENTRY,*PIDTENTRY;
#pragma pack(pop)
DWORD GetDBEntry();
void HookDBInt();
void UnHookDBInt();
#endif
/*
drxhook.cpp
Written By [email protected]
*/
#include "drxhook.h"
DWORD g_OldDBEntry;
IDTR g_IDTR;
DWORD g_OldCreateFile;
DWORD g_HookNumber = 0;
DWORD g_CR0;
BOOL g_bExit;
void ReLoadCR0AndSti()
{
__asm
{
push eax
mov eax, g_CR0
mov cr0, eax
pop eax
sti
}
}
void CliAndDisableWPBit()
{
__asm
{
cli
push eax
mov eax, cr0
mov g_CR0, eax
and eax, 0xFFFEFFFF
mov cr0, eax
pop eax
}
}
void PrintHook()
{
DbgPrint(" Now Get In ZwCreateFile Hook: %d…Pid: %d…\n", g_HookNumber++, (DWORD)PsGetCurrentProcessId());
}
__declspec(naked) void NewZwCreateFile()
{
__asm
{
pushfd; // 仅仅适合于 XP 操作系统
call PrintHook;
popfd;
mov eax,0x25;
jmp g_OldCreateFile;
}
}
void SetHB() // set hardware breakpoint 设置硬件断点
{
__asm
{
mov eax, ZwCreateFile; // 想要挂接的函数或者地址
mov dr0, eax;
mov eax, dr7;
or eax, 0x2703; // 也要修改 dr7:GD 位,以免DrX被操作系统或其他程序修改
and eax, 0xfff0ffff;
mov dr7, eax;
}
}
__declspec(naked) void NewDBEntry()
{
__asm
{
pushfd;
push eax;
mov eax, dr6;
test eax, 0x2000;
jz NOT_EDIT_DRX;
// 以下是如果有对DRX的操作的简单处理,如有需要可以修改
// 我只是简单的跳过这些指令
and eax, 0xFFFFDFFF;
mov dr6, eax; // 清除DR6的标志
cmp g_bExit, 0;
jnz MY_DRV_EXIT; // 驱动 Unload
mov eax, [esp+8]; // 获取堆栈中的 EIP
add eax, 3; // 由于所有对 DRX 的操作全都是3个字节的
mov [esp+8], eax; // 修改 EIP ,跳过当前指令,返回时执行下条指令
jmp MY_INT_END;
NOT_EDIT_DRX:
mov eax, dr6;
test eax, 0x1;
jz SYS_INT; // 如果不是Dr0 产生的中断,则跳回原系统中断
mov eax, [esp+8];
cmp eax, ZwCreateFile; // 判断一下是不是 ZwCreateFile 的线性地址
jnz SYS_INT;
mov eax, NewZwCreateFile;
mov [esp+8],eax; // 修改堆栈中的 EIP ,实现返回时跳转
MY_INT_END:
mov eax, dr7;
or eax, 0x2000; // 恢复 GD 位
mov dr7, eax;
MY_DRV_EXIT: // 整个驱动 UnLoad 时,不恢复 Dr7
pop eax;
popfd;
iretd;
SYS_INT:
pop eax;
popfd;
jmp g_OldDBEntry;
}
}
DWORD GetDBEntry()
{
PIDTENTRY IdtEntry;
DWORD Entry;
__asm sidt g_IDTR;
IdtEntry = (PIDTENTRY)(g_IDTR.IDTBase + 8);
Entry = IdtEntry->HiOffset << 16;
Entry |= IdtEntry->LowOffset;
return Entry;
}
void HookDBInt()
{
DWORD NewEntry;
PIDTENTRY IdtEntry;
NewEntry = (DWORD)NewDBEntry;
g_OldCreateFile = (DWORD)ZwCreateFile + 5; // 新的要跳转过去的地址
g_OldDBEntry = GetDBEntry();
IdtEntry = (PIDTENTRY)(g_IDTR.IDTBase + 8);
CliAndDisableWPBit();
IdtEntry->LowOffset = (USHORT)NewEntry;
IdtEntry->HiOffset = (USHORT)( NewEntry >> 16 );
ReLoadCR0AndSti();
SetHB();
g_bExit = FALSE;
return;
}
void UnHookDBInt()
{
PIDTENTRY IdtEntry;
DWORD Entry;
__asm sidt g_IDTR;
IdtEntry = (PIDTENTRY)(g_IDTR.IDTBase + 8);
CliAndDisableWPBit();
g_bExit = TRUE;
__asm mov eax, dr7; // 产生一次例外并且清除Dr7:GD
if ( g_OldDBEntry != 0 )
{
IdtEntry->LowOffset = (USHORT)g_OldDBEntry;
IdtEntry->HiOffset = (USHORT)( g_OldDBEntry >> 16 );
}
ReLoadCR0AndSti();
DbgPrint(" UnLoad drx hook..\n");
return;
}
NTSTATUS DriverUnload(IN PDRIVER_OBJECT DriverObject)
{
UnHookDBInt();
return STATUS_SUCCESS;
}
NTSTATUS DriverEntry(
IN PDRIVER_OBJECT DriverObject,
IN PUNICODE_STRING RegistryPath
)
{
HookDBInt();
DriverObject->DriverUnload = DriverUnload;
DbgPrint("Load drxhook Driver Ok…\n");
return STATUS_SUCCESS;
}
/***********************/
以上代码实现了简单的ZwCreateFile 函数的HOOK,可以拿DbgView查看效果.
由于本人水平有限,代码中难免有错误出现,希望指正.
同时也希望各位牛人来指点,[email protected]
/**************下面是罗嗦几句********************/
1.这个方法呢首先不怎么实用,因为你用了调试寄存器后某些壳也想用,因此就冲突了,可能会使某些东西失效,不如传统的HOOK好用(据说利用缺页中断 HOOK也比较好用,没试过)。还有人认为呢这个HOOK虽然是HOOK成功了,但是还得HOOK中断向量,没有必要。其实呢,只是多了种思路罢了,多给大家提供一些想法而已。
2.其次呢,这个方法是我在调试某ARK时想到的,这个ARK的作者说他们会恢复函数的inline hook然后才去调用(大面积的恢复,甚至是整个文件的恢复),于是我就用调试器在该函数上下断点,结果自然是没有断下了,因为下的 (0xcc)断点被恢复了。于是就索性下了个硬件断点,这下就断住了,然后呢就想到了拿这个东西来HOOK。然后就去网上搜资料,发现不少人还是稍微提到过这个方法的,包括 vxk,xikug,都说过。