0%

驱动的hook

相比Win32API的hook,这个貌似更加的流氓一些,其实原理是这样子的(下面的例子用OpenProcess举例),在我们调用OpenProcess函数的时候会调用ZwOpenProcess函数,还是通过汇编代码分析一下吧(随便找个程序进去就行),这样更清楚:

1
2
3
4
5
73975A00 >  8BFF            mov edi,edi                              ; OpenProc.<ModuleEntryPoint>
73975A02 55 push ebp
73975A03 8BEC mov ebp,esp
73975A05 5D pop ebp ; kernel32.73978494
73975A06 - FF25 F8149E73 jmp dword ptr ds:[<&api-ms-win-core-proc>; KernelBa.OpenProcess

可以看到,我们在jmp之前做了点迷惑操作,具体为什么这么做,不需要了解,但是可以注意到,我们跳转到了KernelBa.OpenProcess里面,我们跟进去:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
754B3C70 >/$  8BFF          mov edi,edi                              ;  OpenProc.<ModuleEntryPoint>
754B3C72 |. 55 push ebp
754B3C73 |. 8BEC mov ebp,esp
754B3C75 |. 83EC 24 sub esp,0x24
754B3C78 |. 8B45 10 mov eax,[arg.3]
754B3C7B |. 33C9 xor ecx,ecx ; OpenProc.<ModuleEntryPoint>
754B3C7D |. 8945 F4 mov [local.3],eax
754B3C80 |. 8B45 0C mov eax,[arg.2]
754B3C83 |. F7D8 neg eax
754B3C85 |. 894D F8 mov [local.2],ecx ; OpenProc.<ModuleEntryPoint>
754B3C88 |. C745 DC 18000>mov [local.9],0x18
754B3C8F |. 1BC0 sbb eax,eax
754B3C91 |. 894D E0 mov [local.8],ecx ; OpenProc.<ModuleEntryPoint>
754B3C94 |. 83E0 02 and eax,0x2
754B3C97 |. 894D E4 mov [local.7],ecx ; OpenProc.<ModuleEntryPoint>
754B3C9A |. 8945 E8 mov [local.6],eax
754B3C9D |. 8D45 F4 lea eax,[local.3]
754B3CA0 |. 50 push eax
754B3CA1 |. 8D45 DC lea eax,[local.9]
754B3CA4 |. 894D EC mov [local.5],ecx ; OpenProc.<ModuleEntryPoint>
754B3CA7 |. 50 push eax
754B3CA8 |. FF75 08 push [arg.1]
754B3CAB |. 8D45 FC lea eax,[local.1]
754B3CAE |. 894D F0 mov [local.4],ecx ; OpenProc.<ModuleEntryPoint>
754B3CB1 |. 50 push eax
754B3CB2 |. FF15 70385775 call dword ptr ds:[<&ntdll.NtOpenProcess>; ntdll.ZwOpenProcess

前面的还是不需要关注,还是最后一句话,call了ntdll.NtOpenProcess,那么我们继续跟进这个ntdll.NtOpenProcess这个函数:

1
2
3
7707AB30 >  B8 26000000     mov eax,0x26
7707AB35 BA 60F10877 mov edx,ntdll.7708F160
7707AB3A FFD2 call edx ; OpenProc.<ModuleEntryPoint>

可以看到,我们又call了一下edx,然后就返回了,这个edx是多少呢,是0x7708F160,我们继续看这个地址:

1
7708F160  - FF25 28B21277   jmp dword ptr ds:[Wow64Transition]       ; wow64cpu.76FA7000

这是最深的一层了,没必要跟进了,其实这个函数通过汇编的分析,可以简单的得到一个结论,就是他需要一个eax的值,在我们调用OpenProcess的时候,这个eax的值是0x26,我截个图大家看看上一层:
eax
发现在上一层有好多类似的代码,都是赋值eax,然后跳到这个Wow64Transition函数中去,这里我讲解的是SSDT,也就是kernel的(不是GDI和user32的)
SSDT就是系统服务描述符表,主要作用就是将我们的用户层API与内核层的Nt函数进行一一的对应,里面会存放一个指向这个NT开头函数的地址:
说白了就是OpenProcess其实调用了ZwOpenProcess,然后通过eax的值在SSDT表中指向了NtOpenprocess函数,最终调用内核层的代码,Wow64Transition这个函数应该是从用户层跳转的一个函数,这里不做过多的分析。

简单的分析到这里,我们可以画一张图来理顺一下思路(本人画图技术不精,PS一直都是很混)
paint
在之前我们知道用户层的hook分为两种方式,一种是修改PE导入表,另一种是inlinehook,那么这里也主要分为这两种:

  1. 修改SSDT表的OpenProcess函数的地址,指向我们hook的地址
  2. 修改函数的跳转

为了简单起见,我主要讲解的是第一种修改SSDT表的方式。

这里我们开发一个hook的驱动程序,我准备的环境是win7,vs2013,wdk,ddk标准的开发环境,虽然有一点点的老了,但是还是可以用的,如果不熟悉搭建环境流程的和我以前学驱动的时候一样的话呢,那么可以联系我,我将这个环境配置到了VM虚拟机中,你只需要下载虚拟机导入就可以直接使用我配置好的环境。

首先我们导入ntddk.h

1
#include <ntddk.h>

对了还有一点要说的是,我用的是C语言写的,我建议使用C语言,不要用CPP了,CPP写起来我总是遇到问题!!!虽然高级属性少了,但是会避免一些不必要的问题。
定义NTOpenProcess的原型:

1
2
3
4
5
6
typedef NTSTATUS(*pfnNtOpenProcess)(
PHANDLE,
ACCESS_MASK,
POBJECT_ATTRIBUTES,
PCLIENT_ID
);

定义一个全局变量,用来保存我们的被hook前真实的NtOpenprocess地址,这里为什么我没用到设备扩展,因为我们不是一个真实的wdm驱动,而是一个测试的nt驱动,为了简单,我就不创建驱动对象了。

1
pfnNtOpenProcess OldNtOpenProcess;

下面是重点:

1
2
3
4
5
6
7
8
typedef struct _SERVICE_DESCRIPTOR_TABLE {
PVOID ServiceTableBase;//System Service Dispatch Table 的基地址
PVOID ServiceCounterTableBase;//包含着 SSDT 中每个服务被调用次数的计数器。这个计数器一般由sysenter 更新
ULONG NumberOfServices;//由 ServiceTableBase 描述的服务的数目
PUCHAR ParamTableBase;//包含每个系统服务参数字节数表的基地址-系统服务参数表
}SERVICE_DESCRIPTOR_TABLE, *PSERVICE_DESCRIPTOR_TABLE;

__declspec(dllimport) SERVICE_DESCRIPTOR_TABLE KeServiceDescriptorTable;

主要讲解这一块,我们定义SSDT的结构,这个微软给了,我直接copy别人的,下面的是ntoskrnl.lib导出的一个全局变量,这个变量里面就是我们要修改的宏大的SSDT表!

定义我们的NtOpenprocess函数:

1
2
3
4
NTSTATUS MyNtOpenProcess(PHANDLE ProcessHandle, ACCESS_MASK DesireAccess, POBJECT_ATTRIBUTES ObjectAttributes, PCLIENT_ID ClientID)
{
return STATUS_ACCESS_DENIED;
}

可以看到,我什么都不做,只是返回一个连接拒绝,当然我只是为了简单,其实真正的做法应该是我们遍历我们保护的进程,得到PID,然后如果PID等于要保护的话呢我们就返回拒绝,不是我们就让他执行原先的Nt函数。

下面就是hook代码,我参考别人的,我下面简单解释一下他的做法是什么:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
PVOID HookSSDTFunction(PVOID OldFunction, PVOID HookFuction)
{
KdPrint(("Enter HookSSDTFunction \n"));
PMDL pMdl = MmCreateMdl(NULL, KeServiceDescriptorTable.ServiceTableBase,KeServiceDescriptorTable.NumberOfServices * 4);
if (!pMdl)
{
KdPrint(("Hook SSDT Failed!\n"));
return 0;
}
MmBuildMdlForNonPagedPool(pMdl);
pMdl->MdlFlags |= MDL_MAPPED_TO_SYSTEM_VA;
PLONG pMdlLocked = (PLONG)MmMapLockedPages(pMdl, KernelMode);
for (ULONG i = 0; i < KeServiceDescriptorTable.NumberOfServices;i++)
{
if ((LONG)pMdlLocked[i] == (LONG)OldFunction)
{
InterlockedExchange(&pMdlLocked[i], (LONG)HookFuction);
break;
}
}
MmUnmapLockedPages(pMdlLocked, pMdl);
IoFreeMdl(pMdl);
return OldFunction;
}

首先我们创建一块mdl空间,SSDT表的基地址到SSDT表的结尾,大小的话呢就是SSDT个数的4倍,主要是一个指针四个字节,我们用的32位的操作系统。
然后更新我们的MDL空间在非分页内存上,然后再锁住,防止蓝屏这个小可爱。
然后遍历SSDT表判断是不是我们要hook的地址,如果是的话呢我们就用这种原子级别的数据交换,修改要hook的函数,最终解锁MDL,并且释放,返回我们之前的函数地址,为了在驱动卸载的时候取消hook。

1
2
3
4
5
6
7
VOID hook(PCWSTR sz)
{
UNICODE_STRING strToFind;
RtlInitUnicodeString(&strToFind, sz);
PVOID AddToFind = MmGetSystemRoutineAddress(&strToFind);
OldNtOpenProcess = (pfnNtOpenProcess)HookSSDTFunction(NtOpenProcess, MyNtOpenProcess);
}

这段代码为了验证,但是实际上关键的就是OldNtOpenProcess = (pfnNtOpenProcess)HookSSDTFunction(NtOpenProcess, MyNtOpenProcess);,MmGetSystemRoutineAddress这个函数是通过字符串获得地址,这个也就是XT这种工具可以帮助我们取消hookSSDT表的方法函数。
可以看到我们将之前的NtOpenprocess hook 成为了我们的函数。

驱动加载函数:

1
2
3
4
5
6
7
8
9
10
#pragma INITCODE
//extern "C"
NTSTATUS DriverEntry(IN PDRIVER_OBJECT pDriverObject,
IN PUNICODE_STRING pRegistryPath)
{
NTSTATUS status = STATUS_SUCCESS;
pDriverObject->DriverUnload = DriverUnLoad;
hook(L"NtOpenProcess");
return status;
}

很简单,驱动一加载我们就hook,也不创建别的东西了。

驱动卸载函数:

1
2
3
4
5
#pragma PAGECODE
VOID DriverUnLoad(IN PDRIVER_OBJECT pDriverObject)
{
HookSSDTFunction(MyNtOpenProcess, OldNtOpenProcess);
}

也是,卸载的话呢设备对象也不管了,直接取消hook就可以了。

代码其实比较的简单,关键的也就没几行。
看一下在32位上实际的操作是什么样子的:
hook
可以看到XT已经检测出来hook了,并且我们现在如果要用管理员方式运行任何东西,都会提示这个样子的对话框:
hook
这个对话框是不是很熟悉,杀毒软件经常这么搞。
unhook
卸载之后XT也就给我们提示了,没有hook了。

过掉驱动

我们知道原理之后,我们只需要写一个简单的驱动,通过MmGetSystemRoutineAddress函数修改回我们之前的OpenProcess地址就可以了,在这里Wker给大家一些问题以及思路:

  1. 我们修改回来的话呢,驱动其实可以设置一个DPC的时钟,一直监视我们的NtOpenprocess的地址,如果发现被改回来的话呢还是可以进行设置回来的。
    1. 解决办法:用汇编修改新的NtOpenprocess代码,让他直接跳转到的旧的NtOpenProcess
  2. 如果我们用了汇编修改方法,他可以检测这个内存单元的字节,如果存在跳转,并且不是他之前的字节的话呢,他还是可以进行保护

留一个问题,希望读者可以通过思考得出如何过掉他的第二种保护手法。

那种内联的hook方法我就不讲了,这个的话呢还要牵扯到一些比较底层的东西,牵扯到CR0的一些问题,给大家一段代码,从网上摘录的,其实也比较简单,就是对CR0的位进行了修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//禁用写保护,wp=0
VOID PROTECT()
{
__asm
{
cli ;
mov eax, cr0
and eax, ~0x10000
mov cr0, eax
}
}
//启用写保护,wp=1
VOID UN_PROTECT()
{
__asm
{
mov eax, cr0
or eax, 0x10000
mov cr0, eax
sti ;
}
}