windows I/O 模型 主要概念
整个结构会很复杂,比如处理IRP部分:
模型中我们最需要关注这三个概念:
- I/O 请求使用
IRP
从用户空间发送到驱动程序
- I/O 管理器(I/O manager)为所有内核模式驱动程序提供一致的接口。
- 此 I/O 管理器为每个已安装和加载的驱动程序创建一个驱动程序对象( driver object)。
DRIVER_OBJECT
包含许多驱动程序标准例程的入口点的存储。同样重要的是要注意这一点:当 I/O 管理器处理 IRP
时,它会将当前驱动程序的DRIVER_OBJECT
内存地址提供给名为 DriverEntry
的主函数。更详细的描述参考链接1。
内核中需要了解的驱动结构和组件
了解一些重要的结构能帮助我们快速构建需要的payload
。
IRP structure
它是 Input/Output Request Packet
的简称,在WDM.H
定义了标准的NT结构。如下:
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 | typedef struct _IRP {
CSHORT Type ;
USHORT Size;
PMDL MdlAddress;
ULONG Flags;
union {
struct _IRP * MasterIrp;
__volatile LONG IrpCount;
PVOID SystemBuffer;
} AssociatedIrp;
LIST_ENTRY ThreadListEntry;
IO_STATUS_BLOCK IoStatus;
KPROCESSOR_MODE RequestorMode;
BOOLEAN PendingReturned;
CHAR StackCount;
CHAR CurrentLocation;
BOOLEAN Cancel;
KIRQL CancelIrql;
CCHAR ApcEnvironment;
UCHAR AllocationFlags;
union {
PIO_STATUS_BLOCK UserIosb;
PVOID IoRingContext;
};
PKEVENT UserEvent;
union {
struct {
union {
PIO_APC_ROUTINE UserApcRoutine;
PVOID IssuingProcess;
};
union {
PVOID UserApcContext;
_IORING_OBJECT * IoRing;
struct _IORING_OBJECT * IoRing;
};
} AsynchronousParameters;
LARGE_INTEGER AllocationSize;
} Overlay;
__volatile PDRIVER_CANCEL CancelRoutine;
PVOID UserBuffer;
union {
struct {
union {
KDEVICE_QUEUE_ENTRY DeviceQueueEntry;
struct {
PVOID DriverContext[ 4 ];
};
};
PETHREAD Thread;
PCHAR AuxiliaryBuffer;
struct {
LIST_ENTRY ListEntry;
union {
struct _IO_STACK_LOCATION * CurrentStackLocation;
ULONG PacketType;
};
};
PFILE_OBJECT OriginalFileObject;
} Overlay;
KAPC Apc;
PVOID CompletionKey;
} Tail;
} IRP;
|
我们重点关注两个元素:
UserBuffer
字段 :这是从驱动程序返回的数据将被传输到客户端的地方
CurrentStackLocation
: 这是驱动程序将访问从客户端发送的数据的位置。
IO_STACK_LOCATION 结构
该结构体有上面提到的 CurrentStackLocation
指定,其结构可以从这查看https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/ns-wdm-_io_stack_location
。
我们需要关注的两个结构体是:
MajorFunction
: 它是要执行的 I/O 操作的类型。在后面的示例中,将使用 DeviceIoControl
API,它通过调用 IRP_MJ_DEVICE_CONTRO
L 主要函数来访问驱动程序。
Parameters
: 实际就是一堆win api,用来检索用户输入或者数据输入的所在位置。
DEVICE_OBJECT 结构
该结构体位于 IO_STACK_LOCATION结构中,标识驱动程序处理 I/O 请求的逻辑、虚拟或物理设备。
DeviceType
:标识设备类型。生成 IOCTL 控制代码时,此信息非常重要
DriverObject
: 指向 DRIVER_OBJECT 的指针,该指针表示输入到 DriverEntry 例程的驱动程序的加载图像。此成员由 I/O 管理器在成功调用 IoCreateDevice API 时设置。
DRIVER_OBJECT
把上面的结构体组好到一起,就是DRIVER_OBJECT
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | typedef struct _DRIVER_OBJECT {
CSHORT Type ;
CSHORT Size;
PDEVICE_OBJECT DeviceObject;
ULONG Flags;
PVOID DriverStart;
ULONG DriverSize;
PVOID DriverSection;
PDRIVER_EXTENSION DriverExtension;
UNICODE_STRING DriverName;
PUNICODE_STRING HardwareDatabase;
PFAST_IO_DISPATCH FastIoDispatch;
PDRIVER_INITIALIZE DriverInit;
PDRIVER_STARTIO DriverStartIo;
PDRIVER_UNLOAD DriverUnload;
PDRIVER_DISPATCH MajorFunction[IRP_MJ_MAXIMUM_FUNCTION + 1 ];
} DRIVER_OBJECT, * PDRIVER_OBJECT;
|
DeviceObject
: 由 IoCreateDevice 创建
DriverStart
:包含内核中的驱动程序内存位置
DriverName
: 字面意思
MajorFunction
: 调度表,由驱动程序的 DispatchXxx 例程的入口点数组组成
每个驱动程序都包含主要功能代码(major function codes),这些代码告诉驱动程序应执行哪些操作来满足 I/O 请求。所有驱动程序必须至少支持:
- IRP_MJ_CREATE
- IRP_MJ_CLOSE
- IRP_MJ_DEVICE_CONTROL
实例
下面将给一个实际的例子,直接上demo。
核心功能将被放置在vikingdrv2DeviceControl
例程中,该例程有两个参数:
首先把IO_STACK_LOCATION
指针存储到栈上,该结构用来接受用户发来的信息:
- 我们首先验证IOCTL编号
- 如果IOCTL是已知值,我们继续
- 最终我们将有效负载存储在数据变量中
大致结构如下:
1 2 3 4 5 6 7 8 9 | NTSTATUS vikingdrv2DeviceControl(PDEVICE_OBJECT, PIRP Irp) {
auto stack = IoGetCurrentIrpStackLocation(Irp);
switch (stack - >Parameters.DeviceIoControl.IoControlCode) {
case IOCTL_number1: {
auto data = (ThreadData * )stack - >Parameters.DeviceIoControl.Type3InputBuffer;
}
...
}
}
|
然后我们定义名为 DriverEntry
的主函数。如前所述,此函数获取指向DRIVER_OBJECT
结构的指针。为了知道在提供IRP_MJ
代码时必须执行哪个函数,驱动程序必须定义调度例程(dispatch routine)。这里最重要的是使其能够处理IRP_MJ_DEVICE_CONTROL
消息:让它指向 vikingdrv2DeviceControl
函数。
1 2 3 4 5 6 | NTSTATUS DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath) {
/ / 调度例程 dispatch routine
DriverObject - >MajorFunction[IRP_MJ_CREATE] = vikingdrv2CreateClose;
DriverObject - >MajorFunction[IRP_MJ_CLOSE] = vikingdrv2CreateClose;
DriverObject - >MajorFunction[IRP_MJ_DEVICE_CONTROL] = vikingdrv2DeviceControl;
}
|
这就是一个简单基础的驱动实例~,确实很基础,但是能用。
写一个driver
参考自https://leanpub.com/windowskernelprogramming
,写一个简单的驱动程序,需要完成三个功能:
- 等待 IRP
- 当 IRP有一个完整且正确的 IOCTL 结构的时候,展示windows版本信息。
- 更改给定线程的优先级(<=>用户进程)
内核方面
第一步,准备驱动和外界交互部分
驱动程序客户端和驱动程序本身必须具有“通用”说话方式。DeviceIoControl
函数将控制代码直接发送到指定的设备驱动程序,使设备执行相应的操作。这个函数有三个重要的部分:
- 控制代码(在本例中,我们选择 IOCTL 0x800)
- 包含我们数据的输入缓冲区(对本例中是 ThreadData,线程信息)
- 输出缓存
1 2 3 4 5 6 7 | 0x800 , METHOD_NEITHER, FILE_ANY_ACCESS)
struct ThreadData {
ULONG ThreadId;
int Priority;
};
|
第二步,准备内核代码处理驱动客户端发来请求
创建入口点:
DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath)
设置调度例程来处理IRP_MJ_DEVICE_CONTROL
/驱动程序对象:我们将函数命名为vikingdrv2DeviceControl
。
DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = vikingdrv2DeviceControl;
提供device name
和symlink name
名称,然后创建设备对象,以便客户端可以访问驱动程序并打开文件系统句柄。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | UNICODE_STRING devName = RTL_CONSTANT_STRING(L "\\Device\\vikingdrv2" ); / / 驱动名
UNICODE_STRING symLink = RTL_CONSTANT_STRING(L "\\??\\vikingdrv2" ); / / symlink
PDEVICE_OBJECT DeviceObject;
NTSTATUS status = IoCreateDevice(
DriverObject, / / 自定义的驱动对象
0 ,
& devName, / / device name,
FILE_DEVICE_UNKNOWN, / / device type ,
0 , / / characteristics flags,
FALSE,
& DeviceObject / / 输出指针位置
);
if (!NT_SUCCESS(status)) {
KdPrint(( "Failed to create device object (0x%08X)\n" , status));
return status;
}
|
现在我们有一个指向设备对象的指针,通过提供符号链接使用户模式调用方可以访问它。
1 2 3 4 5 6 | status = IoCreateSymbolicLink(&symLink, &devName);
if (!NT_SUCCESS(status)) {
KdPrint(( "Failed to create symbolic link (0x%08X)\n" , status));
IoDeleteDevice(DeviceObject);
return status;
}
|
第三步 驱动核心功能
前面是准备处理请求部分,下面是如何处理它。
首先,我们必须找到我们的堆栈位置(从驱动程序的角度来看),并确认我们的客户端/用户给了我们一个我们能够处理的 IOCTL。
1 2 3 4 | NTSTATUS vikingdrv2DeviceControl(PDEVICE_OBJECT, PIRP Irp) {
auto stack = IoGetCurrentIrpStackLocation(Irp); / / IO_STACK_LOCATION *
switch (stack - >Parameters.DeviceIoControl.IoControlCode) {
case IOCTL_PRIORITY_BOOSTER_SET_PRIORITY: {
|
然后我们处理缓冲区以检索客户端准备的 ThreadData
结构
auto data = (ThreadData*)stack->Parameters.DeviceIoControl.Type3InputBuffer;
最终,我们会处理客户提供的数据。这里我们修改进程的线程优先级:
1 2 3 4 5 | / / 通过pid找线程,
PETHREAD Thread;
status = PsLookupThreadByThreadId(ULongToHandle(data - >ThreadId), &Thread);
/ / 设定新权限
KeSetPriorityThread((PKTHREAD)Thread, data - >Priority);
|
客户端方面
在内核部分:驱动程序正在等待请求。现在客户端代码呢?
首先,让我们创建一个处理用户提供的参数的 main 函数。
1 2 3 4 5 | int main( int argc, const char * argv[]) {
if (argc < 3 ) {
printf( "Usage: Booster <threadid> <priority>\n" );
return 0 ;
}
|
然后使用符号链接打开设备的句柄。
1 2 3 4 5 6 7 8 9 | HANDLE hDevice = CreateFile(L "\\\\.\\vikingdrv2" , GENERIC_WRITE,
FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, 0 , nullptr);
if (hDevice = = INVALID_HANDLE_VALUE)
return Error( "Failed to open device" );
ThreadData data;
data.ThreadId = atoi(argv[ 1 ]); / / 第一个参数
data.Priority = atoi(argv[ 2 ]); / / 第二个参数
|
现在,调用 DeviceIoControl 并在之后关闭设备句柄。
1 2 3 4 5 6 7 8 9 10 11 12 | DWORD returned;
BOOL success = DeviceIoControl(hDevice,
IOCTL_PRIORITY_BOOSTER_SET_PRIORITY, / / control code
&data, sizeof(data), / / 输入缓存和大小
nullptr, 0 , / / 输出缓存和大小
&returned, nullptr);
if (success)
printf( "Priority change succeeded!\n" );
else
Error( "Priority change failed!" );
CloseHandle(hDevice);
|
编译测试
编译此驱动程序会生成一个文件.sys该文件可以作为服务安装:
sc create viking_drv2 type= kernel binpath= C:\viking_driver2.sys
然后禁用签名验证,如果驱动程序已签名,则启用测试签名模式并禁用完整性检查。
1 2 3 | bcdedit - debug on
bcdedit.exe - set TESTSIGNING ON
bcdedit.exe / set nointegritychecks on
|
重启并启动服务。
- 启动进程资源管理器
- 启动CMD.exe
- 启动服务/驱动程序
- 标识线程 ID
- 使用驱动程序客户端修改线程优先级:从 8 到 25
这个例子参考自Windows Kernel Programming
。肯定会运行的,但是问题是目的是提权,而不是修改线程优先级。
优化
寻找线程内存环境:
1 2 | PsLookupProcessByProcessId((HANDLE) * pid, &process);
PsLookupProcessByProcessId((HANDLE) 4 , &system_process);
|
当 PsLookupProcessByProcessId
API 结束时,名为 process
和 system_process
的 PEPROCESS
结构可用,并且包含获取有关请求的 PID(以及系统 pid 编号 4)的信息所需的所有内容。
获取进程 token:
1 2 | targetToken = PsReferencePrimaryToken(process);
systemToken = PsReferencePrimaryToken(system_process);
|
上面所有操作完成之后,提权需要重新写一个客户端,先中止进程:
FindAndReplaceMember((PDWORD_PTR)process, (DWORD_PTR)targetToken, (DWORD_PTR)systemToken, MaxExpectedEprocessSize);
但是不用再启用vs,直接使用ps代替调用:
1 2 3 4 5 6 7 8 9 10 | $myPID = [ int ]$args[ 0 ]
$driverName = "\\.\vikingdrv2sym"
$hDevice = [KGETSYSTEMCLIENT]::CreateFile($driverName, [System.IO.FileAccess]::ReadWrite,
[System.IO.FileShare]::ReadWrite, [System.IntPtr]::Zero, 0x3 , 0x40000080 , [System.IntPtr]::Zero)
[KGETSYSTEMCLIENT]::DeviceIoControl($hDevice, $IOCTL_DRV_QUERY_PROPERTY,
[ref]$myPID, [System.Runtime.InteropServices.Marshal]::SizeOf($myPID), $null, 0 , [ref] 0 , [System.IntPtr]::Zero)|Out - null
|
看看效果
参考链接
- Example I/O Request - The Details
- ns-wdm-_irp
- Kernel/Windows Driver Model
- Understanding the Windows I/O System (Mark E. Russinovich, Kate Chase and Alex Ionescu)
- Writing Dispatch Routines
- Pavel’s book
- 使用PSL fuzzing
- pimp-my-pid
[2022冬季班]《安卓高级研修班(网课)》月薪两万班招生中~