编写驱动的前缀步骤(环境,配置)已在一给出;

这里再进一步给出详细需要配置的内容:

  1. 将配置里的选项设置为所有平台,所有配置;
  2. 将CPP里面的常规里的将警告视为错误关闭;
  3. 将链接器里面的常规里的警告视为错误关闭;
  4. 在driverSetting里的常规里面更改驱动平台(Win7);
  5. 在工具->代码片段管理器可以导入自己的代码片段展开(每次都写驱动框架麻烦)
    先找到一个代码片文件,复制一份自己改,改展开语句以及展开内容即可,然后导入;
    这一步可有可无,主要是图方便;

其次,我这里是在虚拟机win10里进行编写驱动,然后放到win7里调试,如果要让物理机的windbg找到源码,需要将win10里的工程文件拷贝一份出来,并给windbg路径!

如果不知道怎么给windbg路径,其实windbg是会自动寻找路径的,而自动寻找路径,这个路径是被写入到了debug版本的生成的驱动文件里面的,可以用十六进制编辑器打开找找(定位到pdb文件),然后在物理机的对应路径上放置相同文件即可,此时windbg会自动寻找源码(有符号的调试状态);

将windbg里面的view -> Options -> Evaluate on hover 勾选上,可以直接鼠标放在变量上查看源码变量的内容;

一个驱动的框架写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include<ntifs.h>

//卸载函数
VOID DriverUnload(PDRIVER_OBJECT pDriver)
{
}

//驱动入口 main
NTSTATUS DriverEntry(PDRIVER_OBJECT pDriver, PUNICODE_STRING pReg)
{
pDriver->DriverUnload = DriverUnload;
return STATUS_SUCCESS;
}

认识驱动对象

在我们编写驱动的时候,主函数传入了一个驱动对象(pDriver);

它的结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Local var @ 0x937e5bcc Type _DRIVER_OBJECT*
0x8678ef38
+0x000 Type : 0n4
+0x002 Size : 0n168
+0x004 DeviceObject : (null) //管理的设备对象,是一个单向链表
+0x008 Flags : 0x13
+0x00c DriverStart : 0xa9097000 Void
+0x010 DriverSize : 0x6000
+0x014 DriverSection : 0x886c6348 Void //驱动的信息,以及所有驱动信息的循环链
+0x018 DriverExtension : 0x8678efe0 _DRIVER_EXTENSION
+0x01c DriverName : _UNICODE_STRING "\Driver\MyDriver2"
+0x024 HardwareDatabase : 0x8436e270 _UNICODE_STRING "\REGISTRY\MACHINE\HARDWARE\DESCRIPTION\SYSTEM"
+0x028 FastIoDispatch : (null)
+0x02c DriverInit : 0xa909b000 long MyDriver2!GsDriverEntry+0
+0x030 DriverStartIo : (null)
+0x034 DriverUnload : 0xa9098020 void MyDriver2!DriverUnload+0
+0x038 MajorFunction : [28] 0x840b3cfc long nt!IopInvalidDeviceRequest+0 //驱动回调

在运行起来以后,驱动对象的属性被赋予了很多的值;

其中,驱动的生命周期可以被分成:注册->启动->结束->销毁;

注册实际上就是把这个驱动写入一些信息到注册表,其中有它的启动方式(自启动还是手动启动),类型(驱动,服务,dll);

以及被启动的次数等等信息;

启动就是把这个sys文件放到内存里作为进程跑起来;

结束就是销毁进程;

销毁就是删除注册表项;

这个 _DRIVER_EXTENSION 属性,里面存放了这个驱动的服务号名称;

通过windows命令 net stop / net start name 也可以进行对驱动的启动和结束;

驱动其实类似于dll,他把它的模块放到了高地址,也就是共享内核内存中;

对于第二个参数类型,PUNICODE_STRING,它是一个结构体,用于内核中的字符串处理,使用%wZ格式化打印;

在内核中处理字符串一般是unicode宽字节,所有在和三环进行数据交换的时候需要对字符串进行转换;

返回值类型NTSTATUS是一个四字节int,它是返回码,小于0是失败,大于等于0是成功,比如c0005;

在写内核调用函数的时候,必须判断返回状态,使用函数NT_SUCCESS,其本质是判断状态大于等于0;

字符串操作API

引入头文件: <ntstrsafe.h>

函数 作用 参数 返回值
RtlInitUnicodeString(a1,a2); 初始化PUNICODE_STRING类型字符串 a1是PUNICODE_STRING类型字符串地址,a2是一个宽字节WCHAR类型的指针 NT状态码
RtlAnsiStringToUnicodeString(a1,a2,TRUE) 将窄字节字符串转换为宽字节 a1是宽字节地址,a2是窄字节地址 NT状态码
RtlUnicodeStringToAnsiString(a1,a2,TRUE) 和上面一个是反着的 和上面一个是反着的 NT状态码
RtlStringCbPrintfA(a1,a2,a3,…) 格式化ascii字符串,并保存,如果要格式化宽字节字符串,则后缀A改为W即可 a1是保存的地址,a2是字符串长度,a3是格式化字符串,后面跟参数 NT状态码
RtlCompareUnicodeString(a1,a2,a3) 比较unicode字符串相同与否 a1,a2都是待比较数据,a3设置为true就忽略大小写,false就完全匹配 NT状态码

其他的一些内核API操作

函数 作用 参数 返回值
ExAllocatePool(a1,a2) 申请内存malloc平替 a1是类型,只要记两个:NonPagedPool(有执行属性),PagedPool(无执行属性),a2是大小 申请空间指针
ExFreePoll(a1) 释放内存 a1为释放地址 空
PsCreateSystemThread(a1,a2) 内核创建线程 和createthread填法是一样的,除了第一个参数变为输出的句柄值,第二个为 THREAD_ALL_ACCESS NT状态码

驱动中的链表

有个类型为 LIST_ENTRY,它的结构中只有一个next指针,没有数据域;

使用的时候自己构造节点结构体,然后将其作为属性放入使用;

内核中常用双向循环链表;

一个void返回函数: InitializeListHead(a1),初始化链表结构,其本质是让他自己指向自己;

创建节点也需要初始化一下,配合它自带的API使用;

1
2
3
4
5
6
7
8
//一些内核中的操作链表的API
//头插尾插
InsertHeadList(a1,node);
InsertTailList(a1,node);
//移除节点
RemoveEntryList(node);
//移除该节点的下一个节点
RemoveHeadList(node);

一般在初始化的时候,反汇编会生成一个比较类似特征的东西:

1
2
3
lea eax, [ebp...]  //把地址拿给eax
mov [ebp...], eax //前驱后驱都指向自己
mov [ebp...], eax

以及在进行插入操作的时候,也会有如上的内容;

驱动隐藏

在驱动扫描/遍历里,基本上都是对目录下的驱动名进行遍历,以及获取驱动对象里面的信息进行确认;

在驱动对象里有一个属性叫做 DriverSection,里面存放了该驱动的一些信息,是一个链表结构的节点;

其结构内容如下:

它的结构体也是和三环的 LDR DATA 很像,前面的属性几乎是套用的,可能后面的属性对应不上;

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
1: kd> dt _LDR_DATA_TABLE_ENTRY 0x87d0bc98
ntdll!_LDR_DATA_TABLE_ENTRY
+0x000 InLoadOrderLinks : _LIST_ENTRY [ 0x83f61730 - 0x87d466e8 ]
+0x008 InMemoryOrderLinks : _LIST_ENTRY [ 0xffffffff - 0xffffffff ]
+0x010 InInitializationOrderLinks : _LIST_ENTRY [ 0x87d0bca4 - 0x0 ]
+0x018 DllBase : 0x9b7c6000 Void
+0x01c EntryPoint : 0x9b7ca000 Void
+0x020 SizeOfImage : 0x6000
+0x024 FullDllName : _UNICODE_STRING "\??\C:\Users\SecondBC\Desktop\MyDriver2.sys"
+0x02c BaseDllName : _UNICODE_STRING "MyDriver2.sys"
+0x034 Flags : 0x49104000
+0x038 LoadCount : 1
+0x03a TlsIndex : 0
+0x03c HashLinks : _LIST_ENTRY [ 0x0 - 0x7f8f ]
+0x03c SectionPointer : (null)
+0x040 CheckSum : 0x7f8f
+0x044 TimeDateStamp : 0x46109f52
+0x044 LoadedImports : 0x46109f52 Void
+0x048 EntryPointActivationContext : (null)
+0x04c PatchInformation : 0xfffffffe Void
+0x050 ForwarderLinks : _LIST_ENTRY [ 0x0 - 0x6000 ]
+0x058 ServiceTagLinks : _LIST_ENTRY [ 0x68175a79 - 0x79004d ]
+0x060 StaticLinks : _LIST_ENTRY [ 0x720044 - 0x760069 ]
+0x068 ContextInformation : 0x00720065 Void
+0x06c OriginalBase : 0x2e0032
+0x070 LoadTime : _LARGE_INTEGER 0x00000073`00790073

这里面有驱动名称,驱动入口点,PE文件镜像地址,以及第一个属性:链表节点的前驱后驱,它是一个循环双向链表;

这个链表串联了所有的驱动的DriverSection;

驱动遍历

所以可以通过这个链表来进行对所有驱动的遍历;

我们可以写一个遍历的代码如下:

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
27
28
29
30
31
32
33
34
35
36
37
38
39
#include<ntifs.h>

//DriverSection结构
typedef struct _DriverSection {
LIST_ENTRY Link; // 指向下一个 _LDR_DATA_TABLE_ENTRY 的 InLoadOrderLinks
LIST_ENTRY InMemoryOrderLinks;
LIST_ENTRY InInitializationOrderLinks;
PVOID DllBase; // 模块基地址
PVOID EntryPoint; // 入口点
ULONG SizeOfImage; // 驱动大小
UNICODE_STRING FullsysName; // 模块全路径
UNICODE_STRING sysName; // 模块名
}DriverSection, *PDriverSection;


//卸载函数
VOID DriverUnload(PDRIVER_OBJECT pDriver)
{

}

//驱动入口 main
NTSTATUS DriverEntry(PDRIVER_OBJECT pDriver, PUNICODE_STRING pReg)
{
DbgBreakPoint();

PDriverSection myDriverSection = pDriver->DriverSection;
int num = 0;

//Link和整个结构体共享地址
//同时,myDriverSection->Link.Flink是最后一个节点,也就是null节点,所以还要指向下一个回到第一个节点
for (PDriverSection p = (myDriverSection->Link.Flink)->Flink; p != myDriverSection->Link.Flink; p = p->Link.Flink)
{
DbgPrintEx(77, 0, "[%d] DriverName: %wZ\n", ++num, &p->sysName);
}

pDriver->DriverUnload = DriverUnload;
return STATUS_SUCCESS;
}

此时我们可以在这个遍历的过程中,去和对应驱动名进行匹配,当匹配到了以后,使用API函数进行节点摘除,此时用这种方式遍历的话,就扫描不到这个被摘除的节点了;

隐藏自身

但很可惜,PChunter的扫描方式不是这样,就算从链表中摘除以后,它还是能够检测到我们的驱动!

实际上对于PChunter而言,要绕过它的检测只需要把驱动对象中的DriverSection字段和DriverInit字段给抹除或者更改掉即可;

尝试隐藏自身驱动:

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#include<ntifs.h>

//DriverSection结构
typedef struct _DriverSection {
LIST_ENTRY Link; // 指向下一个 _LDR_DATA_TABLE_ENTRY 的 InLoadOrderLinks
LIST_ENTRY InMemoryOrderLinks;
LIST_ENTRY InInitializationOrderLinks;
PVOID DllBase; // 模块基地址
PVOID EntryPoint; // 入口点
ULONG SizeOfImage; // 驱动大小
UNICODE_STRING FullsysName; // 模块全路径
UNICODE_STRING sysName; // 模块名
}DriverSection, *PDriverSection;


void DriverHide(PDRIVER_OBJECT pDriver)
{
DbgPrintEx(77, 0, "Now in thread\n");
LARGE_INTEGER count = {0};
//-10000是单位,20000是20000ms
count.QuadPart = -10000 * 20000;
//延迟函数
KeDelayExecutionThread(KernelMode, FALSE, &count);
PDriverSection mySection = pDriver->DriverSection;


DbgPrintEx(77, 0, "HideDriver: %wZ\n", &mySection->sysName);


//断链
RemoveEntryList(&mySection->Link);
//抹除
pDriver->DriverInit = NULL;
pDriver->DriverSection = (mySection->Link.Flink)->Flink;
//减少引用次数
ObDereferenceObject(pDriver);

return;
}

//卸载函数
VOID DriverUnload(PDRIVER_OBJECT pDriver)
{

}

//驱动入口 main
NTSTATUS DriverEntry(PDRIVER_OBJECT pDriver, PUNICODE_STRING pReg)
{
DbgBreakPoint();

//不能在驱动入口进行抹除
//驱动入口结束会用到驱动对象里面的一些属性
//所以要创建线程延迟抹除!

HANDLE thread = 0;
if (PsCreateSystemThread(&thread, THREAD_ALL_ACCESS, 0, 0, 0, DriverHide, pDriver) >= 0)
{
//关闭线程句柄
NtClose(thread);
}

pDriver->DriverUnload = DriverUnload;
return STATUS_SUCCESS;
}

至此,PChunter就找不到我们的驱动了,同时使用加载该驱动的KmdManager也找不到驱动进行卸载了;

那么如果要对指定的驱动进行隐藏呢?

指定名称隐藏

首先我们能够获取对应驱动的DriverSection,但是拿不到对应驱动的驱动对象;

WIN api实际上提供了一个用名称获取驱动对象的函数:

1
2
3
4
5
6
7
8
9
10
11
NTKERNELAPI NTSTATUS ObReferenceObjectByName(  
IN PUNICODE_STRING ObjectName, //查找名称
IN ULONG Attributes, //属性 FILE_ALL_ACCESS
IN PACCESS_STATE PassedAccessState OPTIONAL, // NULL
IN ACCESS_MASK DesiredAccess OPTIONAL, // NULL
IN POBJECT_TYPE ObjectType, //类型,填写下面extern的类型即可
IN KPROCESSOR_MODE AccessMode, //KernelMode
IN OUT PVOID ParseContext OPTIONAL, //NULL
OUT PVOID *Object //输出参数,也就是对应的驱动对象
);
extern POBJECT_TYPE * IoDriverObjectType;

由于它没有文档化,所以用的时候要自己定义一遍;

那么就可以用如下代码进行对PChunter驱动的隐藏,这样有些东西扫描PChunter的时候就找不到了:

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
#include<ntifs.h>

//DriverSection结构
typedef struct _DriverSection {
LIST_ENTRY Link; // 指向下一个 _LDR_DATA_TABLE_ENTRY 的 InLoadOrderLinks
LIST_ENTRY InMemoryOrderLinks;
LIST_ENTRY InInitializationOrderLinks;
PVOID DllBase; // 模块基地址
PVOID EntryPoint; // 入口点
ULONG SizeOfImage; // 驱动大小
UNICODE_STRING FullsysName; // 模块全路径
UNICODE_STRING sysName; // 模块名
}DriverSection, *PDriverSection;

NTKERNELAPI NTSTATUS ObReferenceObjectByName(
IN PUNICODE_STRING ObjectName, //查找名称
IN ULONG Attributes, //属性 FILE_ALL_ACCESS
IN PACCESS_STATE PassedAccessState OPTIONAL, // NULL
IN ACCESS_MASK DesiredAccess OPTIONAL, // NULL
IN POBJECT_TYPE ObjectType, //类型,填写下面extern的类型即可
IN KPROCESSOR_MODE AccessMode, //KernelMode
IN OUT PVOID ParseContext OPTIONAL, //NULL
OUT PVOID *Object //输出参数,也就是对应的驱动对象
);
extern POBJECT_TYPE *IoDriverObjectType;

void DriverHide(PDRIVER_OBJECT pDriver)
{
LARGE_INTEGER count = {0};
//-10000是单位,10000是10000ms
count.QuadPart = -10000 * 10000;
//延迟函数
KeDelayExecutionThread(KernelMode, FALSE, &count);
PDriverSection mySection = pDriver->DriverSection;

DbgPrintEx(77, 0, "HideDriver: %wZ\n", &mySection->sysName);

//断链
RemoveEntryList(&mySection->Link);
pDriver->DriverInit = NULL;
pDriver->DriverSection = (mySection->Link.Flink)->Flink;
//减少引用次数
ObDereferenceObject(pDriver);

return;
}

//卸载函数
VOID DriverUnload(PDRIVER_OBJECT pDriver)
{

}

//驱动入口 main
NTSTATUS DriverEntry(PDRIVER_OBJECT pDriver, PUNICODE_STRING pReg)
{
pDriver->DriverUnload = DriverUnload;

//ObReferenceObjectByName给到的名称是目录下的,且没有后缀名
UNICODE_STRING objname = { 0 };
RtlInitUnicodeString(&objname, L"\\driver\\View++");

//找驱动对象
PDRIVER_OBJECT aimDriver = 0;
NTSTATUS status = ObReferenceObjectByName(&objname, FILE_ALL_ACCESS, NULL, NULL, *IoDriverObjectType, KernelMode, NULL, &aimDriver);
if (status< 0)
{
DbgPrintEx(77, 0, "name %wZ\n", &objname);
DbgPrintEx(77, 0, "find Name fail! status: %p\n",status);
return STATUS_SUCCESS;
}

//不能在驱动入口进行抹除
//驱动入口结束会用到驱动对象里面的一些属性
//所以要创建线程延迟抹除!
HANDLE thread = 0;
if (PsCreateSystemThread(&thread, THREAD_ALL_ACCESS, 0, 0, 0, DriverHide, aimDriver) >= 0)
{
//关闭线程句柄
NtClose(thread);
}

return STATUS_SUCCESS;
}

蓝屏调试

在编程过程中会遇到大量的蓝屏;

如何利用windbg高效调试蓝屏呢?

1
2
3
4
5
!analize -v //蓝屏后直接输,详细检测
k //查看栈
kv //详细栈
dds esp值 //查看栈树形排列后的值
.cxr 地址 //恢复上下文信息

蓝屏后一般会给5个值;

一个是bugcheck值,另外4个是参数,第一个参数一般都是发生错误的代码(c00005一类),第二个参数是死掉的地址,第三个参数是陷入框架地址(trap frame);

一般就先查看这些参数对应结构,显示他们的结构表,以及对应地址的内容进行检查;

之后查看栈结构;

在kv中也有可能发现其他的trap frame地址;

输入指令: .trap trapFrame地址

可以获取陷入框架时的上下文信息;

检查最后调用的函数,可以是死掉时候,也可以是三环调用的时候;

看一下这个函数的功能,看一下死掉的点,大概推测出传入的参数是出了什么问题;

并对比源码进行进一步测试;

IO通信

驱动设计而来的作用就是在硬件和软件之间架起一座桥梁,全称都是设备驱动程序;

它是用来管理设备的一个软件程序,方便操作系统和用户对设备的使用,就像是一个接口;

在r3,用户无法直接访问驱动,但是可以请求一个设备(资源)进行使用(使用设备提供的服务);

这样的关系就类似于一个exe进程管理了若干个窗口,另外的exe可以打开多个窗口进行信息处理一样:一个驱动管理多个设备,另外的exe可以打开多个设备进行享受服务资源;

类似的,窗口拥有回调函数,设备也拥有对应的回调函数;

在编程中,设备的概念被抽象为了一个虚拟的对象,即驱动对象管理设备对象,设备对象也拥有自己的路径和符号链接;

需要强调的是,一个驱动程序可以没有设备,也可以有虚拟的设备,不需要硬性要求设备一定是硬件;

用Linux的一种更通俗的理解:设备即是IO总线上插入进来的一个文件,用户层面的进程想要访问,只有通过驱动进行对接,因为驱动是这些文件的管理者;

因为我并不了解硬件设备的具体原理和构成,我这里做出一个猜测方便我理解:正常驱动开发也就是需要将需要接入电脑的设备的一些操作信号和指令转换为操作系统看得懂的指令(操作系统定义的统一接口),这样在操作系统中的进程使用该类资源时,发出的信号才能转换为对应的硬件指令;

接下来我会写一个驱动创建设备的过程,类似于以前r3写windows窗口编程,并用r3的程序去访问它;

第一步创建设备对象

在入口点写入如下内容即创建一个设备,其函数原型也如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
NTSTATUS IoCreateDevice(
[in] PDRIVER_OBJECT DriverObject,
[in] ULONG DeviceExtensionSize,
[in, optional] PUNICODE_STRING DeviceName,
[in] DEVICE_TYPE DeviceType,
[in] ULONG DeviceCharacteristics,
[in] BOOLEAN Exclusive,
[out] PDEVICE_OBJECT *DeviceObject
);
#define MY_DEVICE_NAME L"\\device\\SecondBC"

//第一步创建设备对象
NTSTATUS stCode = STATUS_SUCCESS;
UNICODE_STRING myDeviceName;
RtlInitUnicodeString(&myDeviceName, MY_DEVICE_NAME);
PDEVICE_OBJECT pDevice;

stCode = IoCreateDevice(pDriver, 0, &myDeviceName, FILE_DEVICE_UNKNOWN, FILE_DEVICE_SECURE_OPEN, FALSE, &pDevice);
if (stCode < 0)
{
DbgPrintEx(77, 0, "[bc]: Create Device Error, code: %lx\n", stCode);
return stCode;
}

函数第一个参数为设备绑定的驱动对象,第二个参数在此次不使用填0,第三个参数为设备名称,用于r3访问设备,第四个参数为设备类型,因为我们是没有硬件的虚拟设备,所以填UNKNOWN,第五个参数为设备属性,这里默认填OPEN,第六个参数为是否是独占资源,即是否一个进程在使用时,其他进程无法使用,这里填否,第七个参数是返回的设备对象;

此时我们就创建好了这个驱动对象;

第二步创建符号链接

1
2
3
4
5
6
7
8
9
10
11
12
#define MY_SYM_NAME L"\\??\\SecondBC" //r3: "\\\\.\\name"	

//第二步创建符号链接
UNICODE_STRING mySymName;
RtlInitUnicodeString(&mySymName, MY_SYM_NAME);
stCode = IoCreateSymbolicLink(&mySymName, &myDeviceName);
if (stCode < 0)
{
IoDeleteDevice(pDevice);
DbgPrintEx(77, 0, "[bc]: Create Symbol Name Error, code: %lx\n", stCode);
return stCode;
}

类比窗口编程中的注册;

第三步设置设备标志

1
2
3
//第三步设置设备标志
pDevice->Flags &= ~DO_DEVICE_INITIALIZING;
pDevice->Flags |= DO_BUFFERED_IO;

当创建驱动对象后,flag中的初始化位是存在的,需要手动给它消掉,不然有些操作系统无法对这个设备进行通信;

同时设置它的内存模式;

第四步创建回调函数

在驱动对象的结构里,有一个属性叫做:

1
PDRIVER_DISPATCH MajorFunction[IRP_MJ_MAXIMUN_FUNCTION + 1];

它是这个驱动的回调函数数组,也就是用于处理通信的函数;

它的下标值有如下内容,对应不同的执行时机:

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
27
28
29
30
31
32
33
34
35
//
// Define the major function codes for IRPs.
//
//函数里的file指的是设备/网络/文件/管道,等等,不单指文件
//Zw也可以替换为Nt NtCreateFile
#define IRP_MJ_CREATE 0x00 //创建或打开设备时 ZwCreateFile/ZwOpenFile 执行时
#define IRP_MJ_CREATE_NAMED_PIPE 0x01 //创建命名管道时 ZwCreateFile/ZwOpenFile
#define IRP_MJ_CLOSE 0x02 //关闭设备时 CloseHandle/ZwClose
#define IRP_MJ_READ 0x03 //读设备时 ZwReadFile
#define IRP_MJ_WRITE 0x04 //写设备时 ZwWriteFile
#define IRP_MJ_QUERY_INFORMATION 0x05 //查询设备信息时 ZwQueryInformationFile
#define IRP_MJ_SET_INFORMATION 0x06 //设置设备信息时 ZwSetInformationFile
#define IRP_MJ_QUERY_EA 0x07 //查询EA时 ZwQueryInformationFile
#define IRP_MJ_SET_EA 0x08 //设置EA时 ZwSetInformationFile
#define IRP_MJ_FLUSH_BUFFERS 0x09 //刷新内存 ZwFlushBuffersFile
#define IRP_MJ_QUERY_VOLUME_INFORMATION 0x0a //查询V信息 ZwQueryVolumeInformationFile
#define IRP_MJ_SET_VOLUME_INFORMATION 0x0b //设置V信息 ZwSetVolumeInformationFile
#define IRP_MJ_DIRECTORY_CONTROL 0x0c
#define IRP_MJ_FILE_SYSTEM_CONTROL 0x0d
#define IRP_MJ_DEVICE_CONTROL 0x0e //设备控制 NtDeviceIoControlFile,最容易检测
#define IRP_MJ_INTERNAL_DEVICE_CONTROL 0x0f
#define IRP_MJ_SHUTDOWN 0x10
#define IRP_MJ_LOCK_CONTROL 0x11
#define IRP_MJ_CLEANUP 0x12 //调用Close时会先调用一遍CleanUp
#define IRP_MJ_CREATE_MAILSLOT 0x13
#define IRP_MJ_QUERY_SECURITY 0x14
#define IRP_MJ_SET_SECURITY 0x15
#define IRP_MJ_POWER 0x16 //电源策略 ZwShutdownSystem
#define IRP_MJ_SYSTEM_CONTROL 0x17
#define IRP_MJ_DEVICE_CHANGE 0x18
#define IRP_MJ_QUERY_QUOTA 0x19 //查询指标 ZwQueryQuotaInformationFile
#define IRP_MJ_SET_QUOTA 0x1a //设置指标 ZwSetQuotaInformationFile
#define IRP_MJ_PNP 0x1b
#define IRP_MJ_PNP_POWER IRP_MJ_PNP // Obsolete....
#define IRP_MJ_MAXIMUM_FUNCTION 0x1b

把这些回调执行触发的一些函数列出来的原因是,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
25
26
27
28
29
30
31
32
33
//回调函数
NTSTATUS MyCreateCallBack(
_In_ struct _DEVICE_OBJECT *DeviceObject,
_Inout_ struct _IRP *Irp
)
{
DbgPrintEx(77, 0, "[bc]: %s", __FUNCTION__);
IoCompleteRequest(Irp, 0);
return STATUS_SUCCESS;
}
NTSTATUS MyCloseCallBack(
_In_ struct _DEVICE_OBJECT *DeviceObject,
_Inout_ struct _IRP *Irp
)
{
DbgPrintEx(77, 0, "[bc]: %s", __FUNCTION__);
IoCompleteRequest(Irp, 0);
return STATUS_SUCCESS;
}
NTSTATUS MyControlCallBack(
_In_ struct _DEVICE_OBJECT *DeviceObject,
_Inout_ struct _IRP *Irp
)
{
DbgPrintEx(77, 0, "[bc]: %s", __FUNCTION__);
IoCompleteRequest(Irp, 0);
return STATUS_SUCCESS;
}

//第四步设置回调函数
pDriver->MajorFunction[IRP_MJ_CREATE] = MyCreateCallBack;
pDriver->MajorFunction[IRP_MJ_CLOSE] = MyCloseCallBack;
pDriver->MajorFunction[IRP_MJ_DEVICE_CONTROL] = MyControlCallBack;

这三个是必须要覆写的,其他地方的回调函数系统都给了一个空白模板;

此时第四步已经完成;

在这之后要在驱动卸载函数里面将分配的设备进行卸载;

第五步释放设备

1
2
3
4
5
6
7
8
9
if (pDriver->DeviceObject)
{
//先释放符号链接
UNICODE_STRING mySymName;
RtlInitUnicodeString(&mySymName, MY_SYM_NAME);
IoDeleteSymbolicLink(&mySymName);

IoDeleteDevice(pDriver->DeviceObject);
}

驱动完整代码

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
#include<ntifs.h>

#define MY_DEVICE_NAME L"\\device\\SecondBC"
#define MY_SYM_NAME L"\\??\\SecondBC" //r3: "\\\\.\\name"

//回调函数
NTSTATUS MyCreateCallBack(
_In_ struct _DEVICE_OBJECT *DeviceObject,
_Inout_ struct _IRP *Irp
)
{
DbgPrintEx(77, 0, "[bc]: %s\n", __FUNCTION__);
IoCompleteRequest(Irp, 0);
return STATUS_SUCCESS;
}
NTSTATUS MyCloseCallBack(
_In_ struct _DEVICE_OBJECT *DeviceObject,
_Inout_ struct _IRP *Irp
)
{
DbgPrintEx(77, 0, "[bc]: %s\n", __FUNCTION__);
IoCompleteRequest(Irp, 0);
return STATUS_SUCCESS;
}
NTSTATUS MyControlCallBack(
_In_ struct _DEVICE_OBJECT *DeviceObject,
_Inout_ struct _IRP *Irp
)
{
DbgPrintEx(77, 0, "[bc]: %s\n", __FUNCTION__);
IoCompleteRequest(Irp, 0);
return STATUS_SUCCESS;
}

//卸载函数
VOID DriverUnload(PDRIVER_OBJECT pDriver)
{
if (pDriver->DeviceObject)
{
//先释放符号链接
UNICODE_STRING mySymName;
RtlInitUnicodeString(&mySymName, MY_SYM_NAME);
IoDeleteSymbolicLink(&mySymName);

IoDeleteDevice(pDriver->DeviceObject);
}
}

//驱动入口 main
NTSTATUS DriverEntry(PDRIVER_OBJECT pDriver, PUNICODE_STRING pReg)
{
//第一步创建设备对象
NTSTATUS stCode = STATUS_SUCCESS;
UNICODE_STRING myDeviceName;
RtlInitUnicodeString(&myDeviceName, MY_DEVICE_NAME);
PDEVICE_OBJECT pDevice;

stCode = IoCreateDevice(pDriver, 0, &myDeviceName, FILE_DEVICE_UNKNOWN, FILE_DEVICE_SECURE_OPEN, FALSE, &pDevice);
if (stCode < 0)
{
DbgPrintEx(77, 0, "[bc]: Create Device Error, code: %lx\n", stCode);
return stCode;
}

//第二步创建符号链接
UNICODE_STRING mySymName;
RtlInitUnicodeString(&mySymName, MY_SYM_NAME);
stCode = IoCreateSymbolicLink(&mySymName, &myDeviceName);
if (stCode < 0)
{
IoDeleteDevice(pDevice);
DbgPrintEx(77, 0, "[bc]: Create Symbol Name Error, code: %lx\n", stCode);
return stCode;
}

//第三步设置设备标志
pDevice->Flags &= ~DO_DEVICE_INITIALIZING;
pDevice->Flags |= DO_BUFFERED_IO;

//第四步设置回调函数
pDriver->MajorFunction[IRP_MJ_CREATE] = MyCreateCallBack;
pDriver->MajorFunction[IRP_MJ_CLOSE] = MyCloseCallBack;
pDriver->MajorFunction[IRP_MJ_DEVICE_CONTROL] = MyControlCallBack;

pDriver->DriverUnload = DriverUnload;
return STATUS_SUCCESS;
}

r3访问设备

注意要设置r3的属性->C->代码生成->运行时库->mtd;

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
27
28
29
30
31
32
33
34
35
#include<Windows.h>
#include<iostream>
using namespace std;

#define MY_SYM_NAME "\\\\.\\SecondBC"

int _tmain(int argc, _TCHAR* argv[])
{
//打开驱动
HANDLE hDevice = CreateFileA(MY_SYM_NAME,GENERIC_READ|GENERIC_WRITE,FILE_SHARE_READ|FILE_SHARE_WRITE,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL);

if(!hDevice)
{
printf("打开设备失败!");
}
else
{
system("pause");

ULONG inbuff = 0;
ULONG outbuff = 0;
ULONG retlen = -1;
//设备通信
BOOL istrue = DeviceIoControl(hDevice,0,&inbuff,4,&outbuff,4,&retlen,0);
if(!istrue)
{
printf("设备通信失败!");
}
system("pause");
//关闭设备
CloseHandle(hDevice);
}
system("pause");
return 0;
}

结果如下:

结果

上面是一个通用编程框架,类似于r3的消息窗口编程;

设备/请求包结构

一般硬件设备都会附加一些过滤设备,处理信息的逻辑则是:

CPU发送一个irp包给到一层设备,处理完后往下传递,依次处理;

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
ntdll!_DEVICE_OBJECT
+0x000 Type : Int2B
+0x002 Size : Uint2B
+0x004 ReferenceCount : Int4B
+0x008 DriverObject : Ptr32 _DRIVER_OBJECT //归属于的驱动对象
+0x00c NextDevice : Ptr32 _DEVICE_OBJECT //一个链,连接下一个设备对象
+0x010 AttachedDevice : Ptr32 _DEVICE_OBJECT //附加的设备(过滤设备,HOOK作用)
+0x014 CurrentIrp : Ptr32 _IRP //io request package 所有的IO信息交换包
+0x018 Timer : Ptr32 _IO_TIMER
+0x01c Flags : Uint4B
+0x020 Characteristics : Uint4B
+0x024 Vpb : Ptr32 _VPB
+0x028 DeviceExtension : Ptr32 Void
+0x02c DeviceType : Uint4B
+0x030 StackSize : Char //设备栈大小
+0x034 Queue : <unnamed-tag>
+0x05c AlignmentRequirement : Uint4B
+0x060 DeviceQueue : _KDEVICE_QUEUE
+0x074 Dpc : _KDPC
+0x094 ActiveThreadCount : Uint4B
+0x098 SecurityDescriptor : Ptr32 Void
+0x09c DeviceLock : _KEVENT
+0x0ac SectorSize : Uint2B
+0x0ae Spare1 : Uint2B
+0x0b0 DeviceObjectExtension : Ptr32 _DEVOBJ_EXTENSION
+0x0b4 Reserved : Ptr32 Void

IRP结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
ntdll!_IRP
+0x000 Type : Int2B
+0x002 Size : Uint2B
+0x004 MdlAddress : Ptr32 _MDL
+0x008 Flags : Uint4B
+0x00c AssociatedIrp : <unnamed-tag> //r3-r0通信的数据缓冲区在这,是个联合体
+0x010 ThreadListEntry : _LIST_ENTRY
+0x018 IoStatus : _IO_STATUS_BLOCK
+0x020 RequestorMode : Char //可以看出来是3环的请求还是0环的请求
+0x021 PendingReturned : UChar
+0x022 StackCount : Char //栈指针 有多少层栈 过滤设备的数量 (用于跳过过滤设备)
+0x023 CurrentLocation : Char //当前设备栈索引
+0x024 Cancel : UChar
+0x025 CancelIrql : UChar
+0x026 ApcEnvironment : Char
+0x027 AllocationFlags : UChar
+0x028 UserIosb : Ptr32 _IO_STATUS_BLOCK
+0x02c UserEvent : Ptr32 _KEVENT
+0x030 Overlay : <unnamed-tag>
+0x038 CancelRoutine : Ptr32 void
+0x03c UserBuffer : Ptr32 Void
+0x040 Tail : <unnamed-tag>

在回调里,要操作一个比较重要的内容,即是设备栈,会用到设备栈里面的参数;

使用以下代码获取设备栈:

1
2
3
4
PIO_STACK_LOCATION ioStack = IoGetCurrentIrpStackLocation(Irp);
//获取参数,参数是个联合体,对应不同回调功能
//对应触发函数的参数,如NtDeviceIoControlFile
ioStack->Parameters.[联合体名称,如DeviceIoControl].[对应调用函数的参数名称]

获取传递操作数:

1
Irp->AssociatedIrp.SystemBuffer;

使用设备读写r3进程内存

r3方向这么写:

1
2
3
4
5
DWORD inbuff = 0xf00dbc;
DWORD outbuff = 0;
DWORD retlen = -1;
//设备通信
BOOL istrue = DeviceIoControl(hDevice, 0xbc, &inbuff, 4, &outbuff, 4, &retlen, 0);

驱动的回调函数这么写:

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
27
28
29
NTSTATUS MyControlCallBack(
_In_ struct _DEVICE_OBJECT* DeviceObject,
_Inout_ struct _IRP* Irp
)
{
DbgPrintEx(77, 0, "[bc]: %s\n", __FUNCTION__);

//获取设备栈
PIO_STACK_LOCATION ioStack = IoGetCurrentIrpStackLocation(Irp);

// 0xbc 为我们自定义的操作码
if (ioStack->Parameters.DeviceIoControl.IoControlCode == 0xbc)
{
DWORD input = 0;
memcpy(&input, Irp->AssociatedIrp.SystemBuffer, ioStack->Parameters.DeviceIoControl.InputBufferLength);
DbgPrintEx(77, 0, "[bc]: input: 0x%x\n", input);
DWORD output = 0xdeadbc02;
memcpy(Irp->AssociatedIrp.SystemBuffer, &output, ioStack->Parameters.DeviceIoControl.OutputBufferLength);
//需要给Irp的IO状态返回信息,返回值是输出长度
Irp->IoStatus.Information = ioStack->Parameters.DeviceIoControl.OutputBufferLength;
}
//也要记得给Irp的IO状态设置为成功
//因为要考虑到Irp可能是会经过多层设备的
//Irp上面的IoStatus才是最终的返回状态
Irp->IoStatus.Status = STATUS_SUCCESS;

IoCompleteRequest(Irp, 0);
return STATUS_SUCCESS;
}

可以发现,读写在驱动端都是使用一个缓冲区,即:

1
Irp->AssociatedIrp.SystemBuffer;

结果如下:

读写结果

可以劫持别的设备的回调表,通过打开别的设备来执行自己的函数;

两个检测点:一个是驱动遍历链表,一个是驱动回调函数表;

注意 IoControlCode 控制码windows官方有定义:

1
2
3
#define CTL_CODE( DeviceType, Function, Method, Access ) (                 \
((DeviceType) << 16) | ((Access) << 14) | ((Function) << 2) | (Method) \
)

第一个是设备类型,第二个是自定义码,一般从0x800开始;第四个是权限位;

第三个是缓冲区映射方法,有三种,一个是复制粘贴,二个是物理页共享(内存映射),三个是直写(直接操作r3地址)

针对的是r3和驱动里面的缓冲区指针的关系;

补充一个内容:驱动回调,只会在被当前驱动管理的设备被调用时,才会触发;其他设备被调用不会触发;

对于驱动回调的写法,用Write类型可以代替Control类型,只不过写r3内存需要二级指针;但是Read类型代替不了,因为数据传不到驱动;

使用二级指针让r3和驱动同时操作同一块内存要注意一点,这个数据区需要在栈上,这样不会导致缺页;

无模块注入

对于驱动断链,在win10上会有bug,会判断到驱动链表异常,从而蓝屏;

另外一种隐藏驱动的方法是无模块注入,我们会实现一个驱动注入器;

获取内核模块基址和大小

首先是一个函数,用于获取内核模块的基址和大小:

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
ULONG_PTR QuerrySysModule(char* moduleName, PULONG_PTR moduleSize)
{
RTL_PROCESS_MODULES sysModuleTable = { 0 };
ULONG_PTR moduleBase = 0;
ULONG_PTR moduleLen = 0;
ULONG retLen = 0;
//获取操作系统的内核模块表
NTSTATUS st = ZwQuerySystemInformation(11, &sysModuleTable, sizeof(sysModuleTable), &retLen);
//第一次长度不够
if (!NT_SUCCESS(st) && st == STATUS_INFO_LENGTH_MISMATCH)
{
PRTL_PROCESS_MODULES psModuleTable = ExAllocatePool(NonPagedPool, retLen + sizeof(RTL_PROCESS_MODULES));
memset(psModuleTable, 0, retLen + sizeof(RTL_PROCESS_MODULES));
//二次查询
st = ZwQuerySystemInformation(11, psModuleTable, retLen + sizeof(RTL_PROCESS_MODULES), &retLen);
if (!NT_SUCCESS(st))
{
ExFreePool(psModuleTable);
return moduleBase;
}

//直接取核心内核模块
if (_stricmp(moduleName, "ntkrnlpa.exe") == 0 || _stricmp(moduleName, "ntoskrnl.exe") == 0)
{
moduleBase = psModuleTable->Modules[0].ImageBase;
moduleLen = psModuleTable->Modules[0].ImageSize;

if (moduleSize)
{
*moduleSize = moduleLen;
}
ExFreePool(psModuleTable);
return moduleBase;
}

//遍历表
for (int i = 1; i < psModuleTable->NumberOfModules; i++)
{
PRTL_PROCESS_MODULE_INFORMATION pModule = &(psModuleTable->Modules[i]);

PUCHAR baseName = (pModule->FullPathName + pModule->OffsetToFileName);
if (_stricmp(moduleName, baseName) == 0)
{
moduleBase = psModuleTable->Modules[i].ImageBase;
moduleLen = psModuleTable->Modules[i].ImageSize;

if (moduleSize)
{
*moduleSize = moduleLen;
}
ExFreePool(psModuleTable);
return moduleBase;
}

}
ExFreePool(psModuleTable);
}

return moduleBase;
}

用到的宏定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
typedef struct _RTL_PROCESS_MODULE_INFORMATION {
HANDLE Section; // Not filled in
PVOID MappedBase; // Base address of the mapped module
PVOID ImageBase; // Base address of the module in memory
ULONG ImageSize; // Size of the module in memory
ULONG Flags; // Flags associated with the module
USHORT LoadOrderIndex; // Load order of the module
USHORT InitOrderIndex; // Initialization order of the module
USHORT LoadCount; // Number of times the module is loaded
USHORT OffsetToFileName; // Offset to the module's file name
UCHAR FullPathName[256]; // Full path of the module
} RTL_PROCESS_MODULE_INFORMATION, * PRTL_PROCESS_MODULE_INFORMATION;
typedef struct _RTL_PROCESS_MODULES {
ULONG NumberOfModules; // Total number of modules
RTL_PROCESS_MODULE_INFORMATION Modules[1]; // Array of module information
} RTL_PROCESS_MODULES, * PRTL_PROCESS_MODULES;

NTSTATUS NTAPI ZwQuerySystemInformation(
_In_ ULONG SystemInformationClass,
_Inout_ PVOID SystemInformation,
_In_ ULONG SystemInformationLength,
_Out_opt_ PULONG ReturnLength
);

注入驱动数组化

我们需要把要注入的驱动模块进行数组化,使用注入器将数组直接写入到内存,在内存中直接运行驱动的DriverMain,实现不落地,不注册。自然驱动链表里面不会出现我们要挂载的驱动模块,其他地方也不会记录这样一个驱动模块,所谓无模块注入;

所以我们要实现一个驱动数组化的工具:

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
#include <iostream>
#include <Windows.h>
using namespace std;

BYTE box[10] = { 0xbc,0x02,0xde,0xad,0xbe,0xef,0x12,0x34,0x56,0x78 };

int main(int argc, char* argv[], char** env)
{
if (argc < 4)
{
printf("用法: BuildFile path_aim_file out_file var_name\n");
return 0;
}



FILE *file;

fopen_s(&file, argv[1], "rb");
if (file == NULL)
{
printf("打开文件失败\n");
return -1;
}

//求长度
fseek(file, 0, SEEK_END);
int fileLen = ftell(file);
rewind(file);

//写入到内存
BYTE* fileData = (BYTE*)malloc(fileLen);
memset(fileData, 0, fileLen);
fread_s(fileData, fileLen, fileLen, 1, file);

fclose(file);
file = NULL;
//写出
fopen_s(&file, argv[2], "wb");
if (file == NULL)
{
printf("创建文件失败\n");
return -2;
}

char buffer[100] = { 0 };
sprintf_s(buffer, 99, "unsigned char %s[%d] = { ", argv[3], fileLen);
fputs("#pragma once\r\n", file);
fputs(buffer, file);

for (int i = 0; i < fileLen; i++)
{
char code[10] = { 0 };

if (i % 16 == 0)
{
fputs("\r\n\t", file);
}

fileData[i] ^= box[i % sizeof(box)];

sprintf_s(code, 10, "0x%02X, ", fileData[i]);
fputs(code, file);

}
fputs("\r\n};\r\n", file);
fclose(file);

return 0;
}

其能力为:读目标驱动,转换为数组,写入到注入器的项目中,作为头文件使用;

把驱动数组进行简单异或加密,更增隐秘性;

制作一份批处理在注入器编译前阶段自动执行:

1
2
3
4
5
6
7
8
set "workpath=%cd%"
cd ../
set "mainpath=%cd%"
set "genPath=%mainpath%\Release"
set "aimPath=%genPath%\LoadTest.sys"
set "buildPath=%genPath%\BuildFile.exe"
cd %workpath%
%buildPath% %aimPath% sys.h sysData

当编译注入器的时候,会先生成目标驱动(LoadTest.sys),然后利用转换工具转换为头文件,然后才生成注入器;

PE文件老三样处理

注入的驱动模块需要和正常PE文件一样,在内存中正常运行;

所以需要拉伸PE段,进行重定位,修复IAT表;

进行重定位和修复IAT的具体实现如下:

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
ULONG64 ExportGetAddrByName(PUCHAR imageBase, PUCHAR funcName)
{
ULONG64 funcAddr = NULL;
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)imageBase;
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(imageBase + pDos->e_lfanew);

PIMAGE_DATA_DIRECTORY pDir = &(pNt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]);
if (!(pDir->VirtualAddress)) return funcAddr;

PIMAGE_EXPORT_DIRECTORY pExport = (PIMAGE_EXPORT_DIRECTORY)(pDir->VirtualAddress + imageBase);
int* funcTable = pExport->AddressOfFunctions + imageBase;
int* nameTable = pExport->AddressOfNames + imageBase;
short* ordinalTable = pExport->AddressOfNameOrdinals + imageBase;

for (int i = 0; i < pExport->NumberOfNames; i++)
{
char* name = nameTable[i] + imageBase;
int ordinal = -1;
if (!strcmp(name, funcName))
{
ordinal = ordinalTable[i];
}

if (ordinal != -1)
{
funcAddr = funcTable[ordinal] + imageBase;
break;
}
}
return funcAddr;
}

void FixImports(PUCHAR memImage)
{
ULONG64 funcAddr = NULL;

PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)memImage;
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(memImage + pDos->e_lfanew);

PIMAGE_DATA_DIRECTORY pDir = &(pNt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]);
if (!(pDir->VirtualAddress)) return;

PIMAGE_IMPORT_DESCRIPTOR pImport = (PIMAGE_IMPORT_DESCRIPTOR)(pDir->VirtualAddress + memImage);
LPSTR libName = NULL;

while (pImport->Name)
{
libName = (PCHAR)(pImport->Name + memImage);
ULONG64 moduleSize = 0;
ULONG64 hModule = 0;
hModule = QuerrySysModule(libName, &moduleSize);
if (hModule || (_stricmp(libName, "hal.dll") == 0))
{
PIMAGE_THUNK_DATA pIat = (PIMAGE_THUNK_DATA)(pImport->FirstThunk + memImage);
PIMAGE_THUNK_DATA pInt = (PIMAGE_THUNK_DATA)(pImport->OriginalFirstThunk + memImage);

for (; pInt->u1.ForwarderString; pIat++, pInt++)
{
// 判断是否有名字
if (pInt->u1.AddressOfData & IMAGE_ORDINAL_FLAG32)
{
// 32位序号导入
}
else if (pInt->u1.AddressOfData & IMAGE_ORDINAL_FLAG64)
{
// 64位序号导入
}
//名字导入
else
{
PIMAGE_IMPORT_BY_NAME stru = (PIMAGE_IMPORT_BY_NAME)(pInt->u1.AddressOfData + memImage);

if ((_stricmp(libName, "hal.dll") == 0) || (_stricmp(libName, "ntoskrnl.exe") == 0) || (_stricmp(libName, "ntkrnlpa.exe") == 0))
{
STRING anFuncName = { 0 };
UNICODE_STRING unFuncName = { 0 };
RtlInitString(&anFuncName, stru->Name);
RtlAnsiStringToUnicodeString(&unFuncName, &anFuncName, TRUE);

funcAddr = MmGetSystemRoutineAddress(&unFuncName);
RtlFreeUnicodeString(&unFuncName);
}
else
funcAddr = ExportGetAddrByName(hModule, stru->Name);
if (funcAddr)
{
pIat->u1.Function = (ULONG_PTR)funcAddr;
}
else
{
//fail
return;
}
}
}
}
else
{
//fail
return;
}
pImport++;
}
}

void Relocate(PUCHAR memImage)
{
typedef struct _IMAGE_RELOC
{
UINT16 Offset : 12;
UINT16 Type : 4;
}IMAGE_RELOC, * PIMAGE_RELOC;

PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)memImage;
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(memImage + pDos->e_lfanew);

PIMAGE_DATA_DIRECTORY pDir = &(pNt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC]);
if (!(pDir->VirtualAddress)) return;

PIMAGE_BASE_RELOCATION pReloc = (PIMAGE_BASE_RELOCATION)(memImage + pDir->VirtualAddress);

while (pReloc->SizeOfBlock)
{
//取word类型个数
UINT32 reCount = (pReloc->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(UINT16);
PIMAGE_RELOC begin = (PIMAGE_RELOC)((PUINT8)pReloc + sizeof(IMAGE_BASE_RELOCATION));

//遍历每个小数
for (int i = 0; i < reCount; i++)
{
//有效位判断
//32位
if (begin[i].Type == IMAGE_REL_BASED_HIGHLOW)
{
//获取小块对应地址
PUINT32 address = (PUINT32)(begin[i].Offset + pReloc->VirtualAddress + memImage);
//重定位
UINT32 delta = (*address - pNt->OptionalHeader.ImageBase) + memImage;
*address = delta;
}
//64位
else if (begin[i].Type == IMAGE_REL_BASED_DIR64)
{
PUINT64 address = (PUINT64)(begin[i].Offset + pReloc->VirtualAddress + memImage);
UINT64 delta = (*address - pNt->OptionalHeader.ImageBase) + memImage;
*address = delta;
}
}
pReloc = (PIMAGE_BASE_RELOCATION)((PUINT8)pReloc + pReloc->SizeOfBlock);
}
//基地址修复
pNt->OptionalHeader.ImageBase = (ULONG64)memImage;
}

加载驱动模块的核心函数:

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
27
28
29
30
31
32
33
34
35
36
37
void LoadPE(unsigned char* PEimage)
{
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)PEimage;
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(PEimage + pDos->e_lfanew);
PIMAGE_SECTION_HEADER pSection = IMAGE_FIRST_SECTION(pNt);

//内存大小
ULONG_PTR sizeOfImage = pNt->OptionalHeader.SizeOfImage;

//给到头
PUCHAR memImage = (PUCHAR)ExAllocatePool(NonPagedPool, sizeOfImage);
memset(memImage, 0, sizeOfImage);
memcpy(memImage, PEimage, pNt->OptionalHeader.SizeOfHeaders);

//拉伸节区
for (int i = 0; i < pNt->FileHeader.NumberOfSections; i++)
{
if(pSection->SizeOfRawData != 0)
memcpy((memImage + pSection->VirtualAddress), (PEimage + pSection->PointerToRawData), pSection->SizeOfRawData);

pSection++;
}

//重定位
Relocate(memImage);

//修复IAT
FixImports(memImage);

//修复Cookie 加壳的不用做这一步
FixCookie(memImage);

//调用入口点
PDRIVER_INITIALIZE entryPoint = (PDRIVER_INITIALIZE)(pNt->OptionalHeader.AddressOfEntryPoint + memImage);
entryPoint(memImage, NULL);

}

其中,fixCookie函数实现如下所示:

1
2
3
4
5
6
7
8
9
10
11
void FixCookie(PUCHAR memImage)
{
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)memImage;
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(memImage + pDos->e_lfanew);

PIMAGE_DATA_DIRECTORY pDir = &(pNt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG]);
if (!(pDir->VirtualAddress)) return;

PIMAGE_LOAD_CONFIG_DIRECTORY config = (PIMAGE_LOAD_CONFIG_DIRECTORY)(pDir->VirtualAddress + memImage);
*(PULONG_PTR)(config->SecurityCookie) += 10;
}

其目的是绕过win10的蓝屏处理逻辑,所以需要修改SecurityCookie的值;

注入器入口

注入器入口如下所示:

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
27
28
#include <ntifs.h>
#include "Tools.h"
#include "LoadDriver.h"
#include "sys.h"
#define INITCODE code_seg("INIT");

unsigned char box[10] = { 0xbc,0x02,0xde,0xad,0xbe,0xef,0x12,0x34,0x56,0x78 };

#pragma INITCODE //把主要代码放到INIT段,INIT执行完后会自动释放,dump内存的找不到
NTSTATUS DriverEntry(PDRIVER_OBJECT pDriver, PUNICODE_STRING pReg)
{
//DbgBreakPoint();
int fileLen = sizeof(sysData);
unsigned char* sys = ExAllocatePool(PagedPool, fileLen);
memcpy(sys, sysData, fileLen);

for (int i = 0; i < fileLen; i++)
{
sys[i] ^= box[i % sizeof(box)];
}

LoadPE(sys);

ExFreePool(sys);

//直接返回失败,操作系统自动卸载加载器
return STATUS_UNSUCCESSFUL;
}

首先把DriverEntry放到INIT段,执行后直接释放,dump内存发现不了注入器;

其次,注入器的入口返回失败,注入器驱动不会持久化驻留,查不到注入器;

其功能为:获取目标驱动数组(sysData,通过 #include “sys.h”),进行相应的解密操作,然后进行内存加载;

若要进一步增加隐蔽性,还可以给注入器实现自删除功能,并把目标驱动入口也写到INIT段,在注入器调用入口以后,手动抹除INIT段,以及模块镜像的头部信息;

同时也可以删除注入器的注册表项,使用如下API即可:

1
2
RtlDeleteRegistryValue  //删属性字段
zwDeleteKey //删目录项

系统调用

研究32位下从r3到r0的一个路径;

调用图

从r3到r0只有两条路径,一条系统调用,一条异常/中断处理;

调用一个API函数时,会先经过 kernel32.dll -> kernelbase.dll -> ntdll.dll;

ntdll中的函数会进行ssdt(位于内核模块ntoskrnl.exe中)查表,进而访问ntoskrnl.exe中的函数,操作其参数(驱动句柄/设备句柄)控制第三方驱动进行函数调用;

最后会经过hal.dll(一个硬件层抽象,操作系统启动时会访问CPU以挂载相对应的hal.dll)以访问CPU以及处理硬件信号;

还有user32.dll以及gdi32.dll,他们最终会落在win32k.sys驱动里,这一条调用路径是专门处理GUI的;

当路径走到ntdll时,是如下一个代码状态:

ntdll.ZwOpenProcess

先补充,Zw开头和Nt开头是同一个函数的不同导出名;

第一行的 mov eax, BEh 是一个服务号,0x1000 以下是通过 ntdll.dll 过来的,0x1000 以上是通过 user32.dll,gdi32.dll,win32u.dll 过来的;

服务号与上 0xfff 才是服务索引;

第二行的 mov edx, 7FFE0300h 是一块r3和r0共享的内存区域;

在r3下的宏叫做:SharedUserData, 在r0下的宏叫做: _KUSER_SHARED_DATA;

在r3下的地址为 7ffe0000, 在r0下的地址为 ffdf0000,两者是同一个物理页;

结构如下:

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
0: kd> dt _KUSER_SHARED_DATA ffdf0000
ntdll!_KUSER_SHARED_DATA
+0x000 TickCountLowDeprecated : 0
+0x004 TickCountMultiplier : 0xf99a027
+0x008 InterruptTime : _KSYSTEM_TIME
+0x014 SystemTime : _KSYSTEM_TIME
+0x020 TimeZoneBias : _KSYSTEM_TIME
+0x02c ImageNumberLow : 0x14c
+0x02e ImageNumberHigh : 0x14c
+0x030 NtSystemRoot : [260] "C:\Windows"
+0x238 MaxStackTraceDepth : 0
+0x23c CryptoExponent : 0
+0x240 TimeZoneId : 0
+0x244 LargePageMinimum : 0x200000
+0x248 Reserved2 : [7] 0
+0x264 NtProductType : 1 ( NtProductWinNt )
+0x268 ProductTypeIsValid : 0x1 ''
+0x26c NtMajorVersion : 6
+0x270 NtMinorVersion : 1
+0x274 ProcessorFeatures : [64] ""
+0x2b4 Reserved1 : 0x7ffeffff
+0x2b8 Reserved3 : 0x80000000
+0x2bc TimeSlip : 0
+0x2c0 AlternativeArchitecture : 0 ( StandardDesign )
+0x2c4 AltArchitecturePad : [1] 0
+0x2c8 SystemExpirationDate : _LARGE_INTEGER 0x0
+0x2d0 SuiteMask : 0x110
+0x2d4 KdDebuggerEnabled : 0x3 ''
+0x2d5 NXSupportPolicy : 0x2 ''
+0x2d8 ActiveConsoleId : 1
+0x2dc DismountCount : 0
+0x2e0 ComPlusPackage : 0xffffffff
+0x2e4 LastSystemRITEventTickCount : 0x26f84
+0x2e8 NumberOfPhysicalPages : 0xbff6e
+0x2ec SafeBootMode : 0 ''
+0x2ed TscQpcData : 0 ''
+0x2ed TscQpcEnabled : 0y0
+0x2ed TscQpcSpareFlag : 0y0
+0x2ed TscQpcShift : 0y000000 (0)
+0x2ee TscQpcPad : [2] ""
+0x2f0 SharedDataFlags : 0xe
+0x2f0 DbgErrorPortPresent : 0y0
+0x2f0 DbgElevationEnabled : 0y1
+0x2f0 DbgVirtEnabled : 0y1
+0x2f0 DbgInstallerDetectEnabled : 0y1
+0x2f0 DbgSystemDllRelocated : 0y0
+0x2f0 DbgDynProcessorEnabled : 0y0
+0x2f0 DbgSEHValidationEnabled : 0y0
+0x2f0 SpareBits : 0y0000000000000000000000000 (0)
+0x2f4 DataFlagsPad : [1] 0
+0x2f8 TestRetInstruction : 0xc3
+0x300 SystemCall : 0x77736c00
+0x304 SystemCallReturn : 0x77736c04
+0x308 SystemCallPad : [3] 0
+0x320 TickCount : _KSYSTEM_TIME
+0x320 TickCountQuad : 0x36d1
+0x320 ReservedTickCountOverlay : [3] 0x36d1
+0x32c TickCountPad : [1] 0
+0x330 Cookie : 0x44e5bd65
+0x334 CookiePad : [1] 0
+0x338 ConsoleSessionForegroundProcessId : 0n3324
+0x340 DEPRECATED_Wow64SharedInformation : [16] 0
+0x380 UserModeGlobalLogger : [16] 0
+0x3a0 ImageFileExecutionOptions : 0
+0x3a4 LangGenerationCount : 1
+0x3a8 Reserved5 : 0
+0x3b0 InterruptTimeBias : 0
+0x3b8 TscQpcBias : 0
+0x3c0 ActiveProcessorCount : 8
+0x3c4 ActiveGroupCount : 1
+0x3c6 Reserved4 : 0
+0x3c8 AitSamplingValue : 0
+0x3cc AppCompatFlag : 1
+0x3d0 DEPRECATED_SystemDllNativeRelocation : 0
+0x3d8 DEPRECATED_SystemDllWowRelocation : 0
+0x3dc XStatePad : [1] 0
+0x3e0 XState : _XSTATE_CONFIGURATION

在偏移300h的地方,是:

1
+0x300 SystemCall       : 0x77736c00

在windows的符号中,SystemCall的符号为: _KiFastSystemCall;

在这个回调里面,首先会把r3的返回地址,也就是ZwOpenProcess的地址给到edx;

然后执行 systementer 指令;

进入内核以后,栈会切换;所以在内核中要访问参数,需要通过edx进行访问;

systementer

这条指令做了什么?

  1. 将 SYSENTER_CS_MSR 的值装载到 cs 寄存器(查MSR寄存器,把cs寄存器替换成r0的cs)
  2. 将 SYSENTER_EIP_MSR 的值装载到 eip 寄存器
  3. 将 SYSENTER_CS_MSR 的值 +8 并装载到 ss 寄存器(ss替换)
  4. 将 SYSENTER_ESP_MSR 的值装载到 esp 寄存器
  5. 将特权级切换到 R0
  6. 如果 EFLAGS 寄存器的 VM 标志被置位,则清除该标志
  7. 开始执行指定的 R0 代码

相对应的,还有一条退出指令: SYSEXIT;

它做的事情:

  1. 将 SYSENTER_CS_MSR 的值 +16 装载到 cs 寄存器
  2. 将 edx 的值装载到 eip 寄存器
  3. 将 SYSENTER_CS_MSR 的值 +24 装载到 ss 寄存器
  4. 将 ecx 的值装载到 esp 寄存器
  5. 切换特权回 R3
  6. 继续执行 R3 代码

MSR寄存器的一部分会存储 “内核上下文 ” ;

实际上 fs 段寄存器也会进行修改;

因为 fs 在32位下的 r3 模式是指向 线程TEB;进入内核后就没有意义了;

R0状态下,fs会指向一个 CPU 信息结构: _KPCR;

进 R0 也会把寄存器上下文信息进行保存,会保存到 KTRAP_FRAME 结构里;

SSDT表(不和UI相关)导出名叫 _KeServiceDescriptorTable, SSSDT表(和UI相关)的导出名叫 _KeServiceDescriptorTableShadow;

32位下,每一个表项只有16个字节,表项前4个字节是函数表,第二个4字节是统计相关(已弃用),第三个4字节是函数表中有多少个函数,第四个4字节是参数个数相关的指针(这个指针 + 服务索引得到的第一个字节,存放的是调用函数参数使用的总字节大小);

两个表的前16个字节是一样的;

(函数表指针 + 服务索引 * 4) -> 得到内核模块中的函数地址;(内核模块中的函数名倒是和ntdll的函数名一样)

r3的参数也会被拷贝复制一份到r0的栈用于构建栈帧,之后直接call 从ssdt或者sssdt表中找到的地址;

总结:systementer 做了哪些事情?

  1. 从MSR寄存器读值,直接跳转到 _KiFastCallEntry(内核区域)(硬件直接改动的EIP的值)
  2. 修改CS,FS,SS(CS,SS是指令硬件直接实现,FS是内核部分代码实现)
  3. 线程栈切换内核栈(内核部分代码实现)(有硬件指令实现,但是实际上没有用上,实际上起作用的是内核代码部分)
  4. 关中断(内核代码实现)
  5. 保存寄存器上下文(KTRAP_FRAME)(包括EFLAGS)(内核部分代码实现)
  6. 保存旧的异常链(SEH)(内核部分代码实现)
  7. 构建内核栈帧(内核部分代码实现)
  8. 开中断(内核代码实现)
  9. 复制参数到内核栈(内核部分代码实现)
  10. 根据服务号查表,进行调用(内核部分代码实现)(直接 CALL 寄存器)

就挺像和挺有408上中断隐指令(关中断,保存上下文,找到函数入口)的味道;

IRQL机制

微软模拟的一套硬件中断优先级机制;

它分很多个等级,当设置的 IRQL 大于当前的 IRQL 等级,那么立即中断,执行高等级代码;

如果设置 IRQL 是小于等于当前的 IRQL 等级,那么必须等自己运行完毕,退回设置的 IRQL 之后才执行;

最低为0,1为高等级,在r3下,IRQL等级永远为0;

调用返回

所以在函数调用结束后,会先判断是由 R0 还是 R3 发起的函数调用,选择不同的路径返回;(顺便一提,R3 和 R0 发起的函数调用入口处部分的代码也不同,走两条路径,上面 systementer 的分析是基于 R3 进入 R0的)

然后判断 IRQL 等级是否为0,不为0蓝屏;

之后再判断是否线程被附加,如果被附加,返回R3的话,会导致蓝屏;

接着就是关中断准备返回;

关中断之后会先进行性能统计,然后进行APC派发(后续详解),然后开始真正的还原;

还原步骤:

  1. 还原异常链(内核代码实现)
  2. 修复FS(内核代码实现)
  3. 还原线程栈(内核代码实现)
  4. 恢复寄存器上下文(内核代码实现)
  5. 跳转到 _KiFastSystemCallRet(内核代码实现)(位于ntdll模块,就在 _KiFastSystemCall 下方 )

由上可知r3进入r0的系统调用一定会通过SSDT或者SSSDT表进行内核函数调用;

SSDT HOOK

只针对32位系统的win7;

win10的防护和限制还挺多,后续学习中攻克;

SSDT 称呼应该是 System Service Descriptor Table,系统服务描述表,不要和操作系统那本书里面的SDT表(系统设备表)记混了;

HOOK这个表其实很简单,主要是怎么去过写保护,这个表所在的页是只读的;

在 Win7 上有N种姿势,其中我知道的就有: 改CR0寄存器的WP位,手动改这个表对应页的PTE属性,还有MDL;

这次写法就使用MDL;

先介绍下MDL:一种内存映射方法(至少目前使用的效果是这样);

它的工作原理其实和第二种姿势类似,查找给到的虚拟地址对应的物理页,大小自己给,给多少就找几个物理页。然后给这些找到的物理页创建页表项,映射新的虚拟地址。新映射的虚拟地址和原来的虚拟地址共享同个物理页,但是PTE的属性不一样;

先来MDL操作的函数实现:

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
27
28
29
30
31
32
33
34
35
36
PVOID MdlMap(PVOID aimAddr, ULONG size, KPROCESSOR_MODE accessMode, PMDL *retMdl)
{
//创建映射结构体
PMDL pmdl = IoAllocateMdl(aimAddr, size, NULL, NULL, NULL);
if (!pmdl) return NULL;
PVOID map = NULL;
BOOLEAN isLock = FALSE;
__try
{
//锁页
MmProbeAndLockPages(pmdl, accessMode, IoReadAccess);
isLock = TRUE;
//映射
map = MmMapLockedPagesSpecifyCache(pmdl, accessMode, MmNonCached, NULL, NULL, NormalPagePriority);
}
__except (1)
{
map = NULL;
}
//失败处理
if (!map)
{
if(isLock) MmUnlockPages(pmdl);
IoFreeMdl(pmdl);
return NULL;
}
*retMdl = pmdl;
return map;
}

VOID MdlUnmap(PVOID mapAddr, PMDL pmdl)
{
MmUnmapLockedPages(mapAddr, pmdl);
MmUnlockPages(pmdl);
IoFreeMdl(pmdl);
}

一个对应映射,一个卸载映射,返回的map就是新映射的虚拟地址;aimAddr是需要去映射的目标地址;

然后是获取SSDT表结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typedef struct _SSDTItem
{
PULONG_PTR funcTable;
ULONG_PTR unUse;
ULONG_PTR funcNum;
PUCHAR paramSizeTable;
}SSDTItem, * PSSDTItem;

typedef struct _SSDTService
{
SSDTItem ssdt;
SSDTItem sssdt;
}SSDTService, * PSSDTService;

EXTERN_C PSSDTService KeServiceDescriptorTable;

前面我们用文字说明过,SSDT表项的内容;

接下来是完整的代码:

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
#include<ntifs.h>

typedef struct _SSDTItem
{
PULONG_PTR funcTable;
ULONG_PTR unUse;
ULONG_PTR funcNum;
PUCHAR paramSizeTable;
}SSDTItem, * PSSDTItem;

typedef struct _SSDTService
{
SSDTItem ssdt;
SSDTItem sssdt;
}SSDTService, * PSSDTService;

EXTERN_C PSSDTService KeServiceDescriptorTable;

typedef NTSTATUS(NTAPI* PFN_NTOPENPROCESS)(
PHANDLE ProcessHandle,
ACCESS_MASK DesiredAccess,
POBJECT_ATTRIBUTES ObjectAttributes,
PCLIENT_ID ClientId
);

PFN_NTOPENPROCESS g_oriNtOpenProcess = NULL;
PULONG_PTR g_map = NULL;
PMDL g_pmdl = NULL;

//hook函数
NTSTATUS NTAPI myNtOpenProcess(
PHANDLE ProcessHandle,
ACCESS_MASK DesiredAccess,
POBJECT_ATTRIBUTES ObjectAttributes,
PCLIENT_ID ClientId
)
{
DbgPrintEx(77, 0, "[bc] ntopenprocess hook happen.\n");
return g_oriNtOpenProcess(ProcessHandle, DesiredAccess, ObjectAttributes, ClientId);
}

PVOID MdlMap(PVOID aimAddr, ULONG size, KPROCESSOR_MODE accessMode, PMDL *retMdl)
{
//创建映射结构体
PMDL pmdl = IoAllocateMdl(aimAddr, size, NULL, NULL, NULL);
if (!pmdl) return NULL;
PVOID map = NULL;
BOOLEAN isLock = FALSE;
__try
{
//锁页
MmProbeAndLockPages(pmdl, accessMode, IoReadAccess);
isLock = TRUE;
//映射
map = MmMapLockedPagesSpecifyCache(pmdl, accessMode, MmNonCached, NULL, NULL, NormalPagePriority);
}
__except (1)
{
map = NULL;
}
if (!map)
{
if(isLock) MmUnlockPages(pmdl);
IoFreeMdl(pmdl);
return NULL;
}
*retMdl = pmdl;
return map;
}

VOID MdlUnmap(PVOID mapAddr, PMDL pmdl)
{
MmUnmapLockedPages(mapAddr, pmdl);
MmUnlockPages(pmdl);
IoFreeMdl(pmdl);
}

VOID DriverUnload(PDRIVER_OBJECT pDriver)
{
g_map[0xbe] = (ULONG_PTR)g_oriNtOpenProcess;
MdlUnmap(g_map, g_pmdl);
}

NTSTATUS DriverEntry(PDRIVER_OBJECT pDriver, PUNICODE_STRING pReg)
{
//MDL
g_map = MdlMap(KeServiceDescriptorTable->ssdt.funcTable, PAGE_SIZE, KernelMode, &g_pmdl);

//hook
g_oriNtOpenProcess = (PFN_NTOPENPROCESS)(g_map[0xbe]);
g_map[0xbe] = (ULONG_PTR)myNtOpenProcess;

//unhook
pDriver->DriverUnload = DriverUnload;
return STATUS_SUCCESS;
}

实际使用中,0xbe这种函数服务号对于每种windows版本肯定不一样,所以不能写死,要手动读取system32下的ntdll.dll,获取它的导出表,找到每个导出函数的地址;

前面我们逆向分析过,ntdll中每个函数长这个样子:

ntdll.ZwOpenProcess

所以,在找到每个函数地址后,地址+1的位置就是服务号;