通过前一篇文章的学习了解到IRP是Windows系统中用于表达一个I/O请求的核心数据结构,当内核模式代码要发起一个I/O请求时,它会构造一个IRP,用于在处理该I/O请求的过程中代表该请求。
IRP结构
IRP对象从一个I/O请求被发起时开始存在,一直到该I/O请求被完成或者取消为止,在此过程中,会有多方操纵此IRP对象,包括I/O管理器、即插即用管理器、电源管理器以及一个或多个驱动程序等。Windows I/O系统本质上支持异步I/O请求,所以,IRP对象必须携带足够多的环境信息,以便能够描述一个I/O请求的所有状态。下面来研究一下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 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
| 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; PIO_STATUS_BLOCK UserIosb; PKEVENT UserEvent; union { struct { union { PIO_APC_ROUTINE UserApcRoutine; PVOID IssuingProcess; }; PVOID UserApcContext; } 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;
|
根据注释,可以大致了解IRP结构各个字段的含义及作用,这里主要介绍几个接下来会用到的:
AssociatedIrp.SystemBuffer:根据定义,可以发现它是一个指向系统地址空间缓冲区的指针。这个系统地址空间缓冲区又是什么?在前一篇中,我们曾介绍过,在创建完设备对象后,需要设置设备对象的Flags字段,也就是设置数据传输方式。而这个SystemBuffer字段,就是在采用缓冲区方式读写(DO_BUFFERED_IO)时,指向的内核空间中分配的一块用于数据复制、交换的内存
MdlAddress:和SystemBuffer类似,这个字段也是在通过缓冲区处理I/O请求时,与设置的数据传输方式有关,这个字段在设备对象采用直接方式读写(DO_DIRECT_IO)时有效。当使用这种方式进行数据读写时,I/O请求的发起者调用IoAllocateMdl函数申请一个MDL(Memory Descriptor List,内存描述符链表),将调用者指定的缓冲区的物理页面构成一个MDL,以便于设备驱动程序使用DMA方式来传输数据。这个字段就是记录了一个I/O请求所使用的MDL。
UserBuffer:同上。当设备对象采用的是默认方式读写(NEITHER_IO)时,就会使用这个字段。此时I/O管理器或者I/O请求的发起者不负责缓冲区管理工作,而由驱动程序自行决定该如何使用缓冲区。其中输出缓冲区的指针放在该字段内,缓冲区本身不做任何处理。
IoStatus:I/O操作的状态。这个字段是一个_IO_STATUS_BLOCK结构体
1 2 3 4 5 6 7 8 9
| typedef struct _IO_STATUS_BLOCK { union { NTSTATUS Status; PVOID Pointer; }; ULONG_PTR Information; } IO_STATUS_BLOCK, *PIO_STATUS_BLOCK;
|
1)Status:表示IRP的完成状态,如果三环程序调用完后发生了错误,想通过GetLastError函数来获取错误码,实际上获取到的就是这个Status的值,也就是说,我们自己在驱动中编写特定IRP对应的派遣函数的话,是可以设置它的错误码的。
2)Information:这个数,决定了返回给3环多少数据。某些3环函数,会传入一部分数据进来(IN类型的参数),也会接收一部分数据(OUT类型的参数)。例如,3环传进来一个CHAR数组,有8个元素,但是我们在该函数的派遣函数中设置的Information的值是2,最后这个数组返回到3环时,就只有2个元素了。具体可以参考后面的程序演示部分。
栈单元
实际上,IRP数据结构仅仅是一个I/O请求的固定描述部分,另一部分是一个或者多个栈单元。每个栈单元针对单个驱动程序,I/O管理器在处理一个I/O请求时,根据目标设备对象(DeviceObject)的StackSize 域,可以知道最多有多少个驱动程序需要参与到该I/O请求的处理过程中。下面来看一下栈单元这个结构:
看上去,这个结构并不复杂,但实际上要注意一下Parameters这个域,这是一个联合体,包含了不同IRP对应的3环函数原型所需的参数,一起来看一下:
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
| union { struct { PIO_SECURITY_CONTEXT SecurityContext; ULONG Options; USHORT POINTER_ALIGNMENT FileAttributes; USHORT ShareAccess; ULONG POINTER_ALIGNMENT EaLength; } Create; struct { PIO_SECURITY_CONTEXT SecurityContext; ULONG Options; USHORT POINTER_ALIGNMENT Reserved; USHORT ShareAccess; PNAMED_PIPE_CREATE_PARAMETERS Parameters; } CreatePipe; struct { PIO_SECURITY_CONTEXT SecurityContext; ULONG Options; USHORT POINTER_ALIGNMENT Reserved; USHORT ShareAccess; PMAILSLOT_CREATE_PARAMETERS Parameters; } CreateMailslot; struct { ULONG Length; ULONG POINTER_ALIGNMENT Key; ULONG Flags; LARGE_INTEGER ByteOffset; } Read; struct { ULONG Length; ULONG POINTER_ALIGNMENT Key; ULONG Flags; LARGE_INTEGER ByteOffset; } Write; struct { ULONG Length; PUNICODE_STRING FileName; FILE_INFORMATION_CLASS FileInformationClass; ULONG POINTER_ALIGNMENT FileIndex; } QueryDirectory; struct { ULONG Length; ULONG POINTER_ALIGNMENT CompletionFilter; } NotifyDirectory; struct { ULONG Length; ULONG POINTER_ALIGNMENT CompletionFilter; DIRECTORY_NOTIFY_INFORMATION_CLASS POINTER_ALIGNMENT DirectoryNotifyInformationClass; } NotifyDirectoryEx; struct { ULONG Length; FILE_INFORMATION_CLASS POINTER_ALIGNMENT FileInformationClass; } QueryFile; struct { ULONG Length; FILE_INFORMATION_CLASS POINTER_ALIGNMENT FileInformationClass; PFILE_OBJECT FileObject; union { struct { BOOLEAN ReplaceIfExists; BOOLEAN AdvanceOnly; }; ULONG ClusterCount; HANDLE DeleteHandle; }; } SetFile; struct { ULONG Length; PVOID EaList; ULONG EaListLength; ULONG POINTER_ALIGNMENT EaIndex; } QueryEa; struct { ULONG Length; } SetEa; struct { ULONG Length; FS_INFORMATION_CLASS POINTER_ALIGNMENT FsInformationClass; } QueryVolume; struct { ULONG Length; FS_INFORMATION_CLASS POINTER_ALIGNMENT FsInformationClass; } SetVolume; struct { ULONG OutputBufferLength; ULONG POINTER_ALIGNMENT InputBufferLength; ULONG POINTER_ALIGNMENT FsControlCode; PVOID Type3InputBuffer; } FileSystemControl; struct { PLARGE_INTEGER Length; ULONG POINTER_ALIGNMENT Key; LARGE_INTEGER ByteOffset; } LockControl; struct { ULONG OutputBufferLength; ULONG POINTER_ALIGNMENT InputBufferLength; ULONG POINTER_ALIGNMENT IoControlCode; PVOID Type3InputBuffer; } DeviceIoControl; struct { SECURITY_INFORMATION SecurityInformation; ULONG POINTER_ALIGNMENT Length; } QuerySecurity; struct { SECURITY_INFORMATION SecurityInformation; PSECURITY_DESCRIPTOR SecurityDescriptor; } SetSecurity; struct { PVPB Vpb; PDEVICE_OBJECT DeviceObject; } MountVolume; struct { PVPB Vpb; PDEVICE_OBJECT DeviceObject; } VerifyVolume; struct { struct _SCSI_REQUEST_BLOCK *Srb; } Scsi; struct { ULONG Length; PSID StartSid; PFILE_GET_QUOTA_INFORMATION SidList; ULONG SidListLength; } QueryQuota; struct { ULONG Length; } SetQuota; struct { DEVICE_RELATION_TYPE Type; } QueryDeviceRelations; struct { const GUID *InterfaceType; USHORT Size; USHORT Version; PINTERFACE Interface; PVOID InterfaceSpecificData; } QueryInterface; struct { PDEVICE_CAPABILITIES Capabilities; } DeviceCapabilities; struct { PIO_RESOURCE_REQUIREMENTS_LIST IoResourceRequirementList; } FilterResourceRequirements; struct { ULONG WhichSpace; PVOID Buffer; ULONG Offset; ULONG POINTER_ALIGNMENT Length; } ReadWriteConfig; struct { BOOLEAN Lock; } SetLock; struct { BUS_QUERY_ID_TYPE IdType; } QueryId; struct { DEVICE_TEXT_TYPE DeviceTextType; LCID POINTER_ALIGNMENT LocaleId; } QueryDeviceText; struct { BOOLEAN InPath; BOOLEAN Reserved[3]; DEVICE_USAGE_NOTIFICATION_TYPE POINTER_ALIGNMENT Type; } UsageNotification; struct { SYSTEM_POWER_STATE PowerState; } WaitWake; struct { PPOWER_SEQUENCE PowerSequence; } PowerSequence; #if ... struct { union { ULONG SystemContext; SYSTEM_POWER_STATE_CONTEXT SystemPowerStateContext; }; POWER_STATE_TYPE POINTER_ALIGNMENT Type; POWER_STATE POINTER_ALIGNMENT State; POWER_ACTION POINTER_ALIGNMENT ShutdownType; } Power; #else struct { ULONG SystemContext; POWER_STATE_TYPE POINTER_ALIGNMENT Type; POWER_STATE POINTER_ALIGNMENT State; POWER_ACTION POINTER_ALIGNMENT ShutdownType; } Power; #endif struct { PCM_RESOURCE_LIST AllocatedResources; PCM_RESOURCE_LIST AllocatedResourcesTranslated; } StartDevice; struct { ULONG_PTR ProviderId; PVOID DataPath; ULONG BufferSize; PVOID Buffer; } WMI; struct { PVOID Argument1; PVOID Argument2; PVOID Argument3; PVOID Argument4; } Others; } Parameters;
|
那这个Parameters该如何用呢?举个例子,假设3环程序调用了DeviceIoControl函数,在0环,就会构造一个IRP_MJ_DEVICE_CONTROL这个类型的IRP,然后我们就可以构建它的派遣函数了。在派遣函数中,当我们获得了当前驱动的栈单元时,就可以通过如下语句访问3环函数DeviceIoControl的参数了,例如:
1 2 3 4 5
| PIO_STACK_LOCATION pStackLocation = IoGetCurrentIrpStackLocation(pIrp);
ULONG InputBufferLength = pStackLocation->Parameters.DeviceIoControl.InputBufferLength; ULONG FsControlCode = pStackLocation->Parameters.DeviceIoControl.IoControlCode;
|
其中,IoGetCurrentIrpStackLocation函数,将Irp指针传进去,可以获取当前驱动程序对应的栈单元。接着就可以通过栈单元获取我们想要的参数了
3环与0环通信(升级)
操作码
在了解了上述知识后,我们就可以对前一篇文章中的代码进行一次升级,更清晰的看到3环和0环的信息交互过程。在此之前,我们需要了解一个操作码。本次实验会在3环程序中新增一个DeviceIoControl函数,因为这个函数能既有传入的参数,也有输出的参数,可以比较直观的看明白3环和0环交互的数据。具体定义如图:
其中需要解释一下的,就是这个dwIoControlCode参数,这个就相当于Switch语句中传入的那个参数,用来判断程序执行流程用的,当操作码不同时,执行的功能也不同,操作码定义如下:
1 2
| #define OPCODE1 CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800, METHOD_BUFFERED, FILE_ANY_ACCESS) #define OPCODE2 CTL_CODE(FILE_DEVICE_UNKNOWN, 0x900, METHOD_BUFFERED, FILE_ANY_ACCESS)
|
CTL_CODE函数,会接收这四个参数,并通过某一种算法,生成一个四字节的操作码,3环和0环中用的是同一套操作码,其中第二个参数,这个值必须选定一个大于等于0x800的值,之前的值由系统保留使用。
新增代码
本次实验新增的代码,就是在3环程序增加了DeviceIoControl这个函数,以及相应的驱动增加了派遣函数。具体变化如下:
Ring3新增部分:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| #define OPCODE1 CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800, METHOD_BUFFERED, FILE_ANY_ACCESS) #define OPCODE2 CTL_CODE(FILE_DEVICE_UNKNOWN, 0x900, METHOD_BUFFERED, FILE_ANY_ACCESS)
getchar(); char pInputBuffer[20] = {1, 2, 4, 8, 16, 32, 64, 0}; char pOutputBuffer[20] = {0}; DWORD dwReturnSize = 0; BOOL bDIC = DeviceIoControl(hDevice, OPCODE2, pInputBuffer, 8, pOutputBuffer, 20, &dwReturnSize, NULL); if(bDIC != 0){ printf("ReturnSize: %x\n", dwReturnSize); printf("OutputBuffer: "); for(int i = 0; i < dwReturnSize; i++){ printf("%x ", pOutputBuffer[i]); } } else { printf("Communicate Failed!\n"); return -1; } printf("\nRing3 And Ring0 Communicate Success!\n");
|
代码中传入一个初始化了8个字节的数组,并且用另一个空数组来接收0环的数据,DeviceIoControl执行完后,根据返回的长度大小,以及返回的Buffer,来打印返回的数据。
Ring0新增部分:
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
| #define OPCODE1 CTL_CODE(FILE_DEVICE_UNKNOWN,0x800,METHOD_BUFFERED,FILE_ANY_ACCESS) #define OPCODE2 CTL_CODE(FILE_DEVICE_UNKNOWN,0x900,METHOD_BUFFERED,FILE_ANY_ACCESS)
NTSTATUS IrpDeviceControlProc(PDEVICE_OBJECT pDeviceObj, PIRP pIrp) { DbgPrint("Irp DeviceControl Dispatch Function...\n");
PVOID pSystemBuffer = pIrp->AssociatedIrp.SystemBuffer;
PIO_STACK_LOCATION pStackLocation = IoGetCurrentIrpStackLocation(pIrp); ULONG InputBufferLength = pStackLocation->Parameters.DeviceIoControl.InputBufferLength; ULONG FsControlCode = pStackLocation->Parameters.DeviceIoControl.IoControlCode;
switch (FsControlCode) { case OPCODE1: DbgPrint("不打印操作码"); pIrp->IoStatus.Status = STATUS_SUCCESS; pIrp->IoStatus.Information = 2; break; case OPCODE2: DbgPrint("操作码:%x\n", FsControlCode); for (UINT32 i = 0; i < InputBufferLength; i++) { DbgPrint("Ring3 Data: %x\n", ((PUCHAR)pSystemBuffer)[i]); } pIrp->IoStatus.Status = STATUS_SUCCESS; pIrp->IoStatus.Information = 5; break; } IoCompleteRequest(pIrp, IO_NO_INCREMENT); return STATUS_SUCCESS; }
|
来简要看一下派遣函数的执行流程:
由于在设备对象的Flags字段定义过缓冲区读取的类型是(DO_BUFFERED_IO),因此我们可以直接从AssociatedIrp.SystemBuffer中读取3环传入的数据,也就是DeviceIoControl中pInputBuffer参数指向的数据。
通过IoGetCurrentIrpStackLocation函数获取到栈单元,再通过栈单元获取到Parameters中DeviceIoControl结构体里对应的参数
1 2 3 4 5 6
| struct { ULONG OutputBufferLength; ULONG POINTER_ALIGNMENT InputBufferLength; ULONG POINTER_ALIGNMENT IoControlCode; PVOID Type3InputBuffer; } DeviceIoControl;
|
这里我们仅取操作码IoControlCode,用于判断执行流程;以及InputBufferLength,用于打印传入数据
然后就是根据操作码的不同,执行不同的流程了:
1)操作码1:不做任何操作,向3环返回2字节大小的数据
2)操作码2:打印操作码的值;根据传入数据的长度,打印传入的数据;向3环返回5字节大小的数据
参考链接
参考书籍:
《Windows内核原理与实现》
参考文章:
- https://www.cnblogs.com/LittleHann/p/3450436.html
- https://www.cnblogs.com/lfls128/p/4982309.html
- https://blog.csdn.net/qq_41988448/article/details/103519478
参考笔记:
Joney,张嘉杰
参考文档:
- https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/ns-wdm-_irp#irp_mj_read
- https://docs.microsoft.com/zh-cn/windows-hardware/drivers/ddi/wdm/ns-wdm-_io_stack_location
- https://docs.microsoft.com/zh-cn/windows-hardware/drivers/ddi/wdm/nf-wdm-iogetcurrentirpstacklocation
- https://docs.microsoft.com/zh-cn/windows-hardware/drivers/ddi/wdm/ns-wdm-_io_status_block
- https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/nf-wdm-iocreatedevice
关于更新
上周过的比较迷,不知道该干什么,因为进度远远落后于计划,日更变得困难了起来,白天完善代码,晚上也来不及更新博客。自身的确有很大问题,比起刚开始的热情,现在没那么有干劲了,人放松了很多,这是个不好的现象。实际上,经历了清明三天的假期,我也逐渐看清了自己。较差的自制力,学习时无法集中注意力等。看着四哥的RMI文章都已经更新到第九篇了,真的自愧不如,我才20出头,却如此懈怠,懒惰。现在已经是4月19号的下午了,而这篇文章原本是13号该更新的(按照日更的进度)。而我目前为止,已经完善的代码仅有SSDT Hook,Inline Hook的代码,在UnHook方面还有一定的瑕疵,还未能完好的解决。日更,不那么现实了,当然,更多的是自身的原因,如果不再日更,就更不知道自己还能不能坚持下来了。但这的确不该放弃,毕竟博客还是一个沉淀知识,分享知识的地方。
这几天,我曾考虑过,写点别的,内容较少的,来维持日更的状态,但是比较恐怖的是,我发现自己并没有足够的内容来写,自身真正掌握的知识太少了,这是非常恐怖的,我也学了这么久计算机了,但是发现,在很多领域,仅仅只是知道点皮毛而已,完全没有知识的积淀。所以,需要提升技术栈的同时也提升深度。
最后一个是,基本上更新完SSDT和Inline Hook这两篇后,驱动剩下的东西就不多了。接下来还有多核同步,句柄表以及APC。中级上的部分也就结束了,但是APC需要分析大量的内核函数,内容巨多,看到群友张嘉杰已经分析完了,真的是非常厉害。到了中级下,没了线上的视频,就得自己探索和扩展一些内容了。我计划在下周更新完内核重载后,就考虑更一些Android或者Web相关的基础内容了。不过由于决定不再日更了,后面内容可能也会更冗长了,但是会更细致一些