avatar

Catalog
初探IRP(原理&程序思路)

通过前一篇文章的学习了解到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这个结构。

看上去结构并不复杂,但其中有很多字段包含了结构体,结构体内又内嵌了结构体和联合体,下面结合官方文档中结构的定义来分析(写入到注释中)

c
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; //IRP类型,等于IO_TYPE_IRP宏
USHORT Size; //IRP大小
PMDL MdlAddress; //该I/O请求的用户缓冲区的MDL,仅用于“直接I/O”类型
ULONG Flags; //用于记录各种标志
union {
struct _IRP *MasterIrp; //若这是一个关联IRP,则指向主IRP
__volatile LONG IrpCount; //若这是一个主IRP,则必须先完成多少个关联IRP
PVOID SystemBuffer; //该操作被缓冲起来,指向系统地址空间缓冲区的地址
} AssociatedIrp;
LIST_ENTRY ThreadListEntry; //链表项,可以加入到线程的未完成I/O请求链表中
IO_STATUS_BLOCK IoStatus; //I/O操作的状态
KPROCESSOR_MODE RequestorMode; //内核模式I/O请求或用户模式I/O请求
BOOLEAN PendingReturned; //未完成返回
CHAR StackCount; //栈单元(IO_STACK_LOCATION)计数
CHAR CurrentLocation; //当前栈单元位置
BOOLEAN Cancel; //该I/O请求是否已被取消
KIRQL CancelIrql; //取消自旋锁在哪级IRQL上被获取
CCHAR ApcEnvironment; //用于当该IRP被初始化时保存APC环境
UCHAR AllocationFlags; //该IRP内存的分配控制标志
PIO_STATUS_BLOCK UserIosb; //用户的I/O状态块
PKEVENT UserEvent; //用户事件对象
union {
struct {
union {
PIO_APC_ROUTINE UserApcRoutine; //当I/O请求完成时执行的APC例程
PVOID IssuingProcess;
};
PVOID UserApcContext; //传递给UserApcRoutine的环境参数
} AsynchronousParameters;
LARGE_INTEGER AllocationSize; //分配块的大小
} Overlay;
__volatile PDRIVER_CANCEL CancelRoutine; //若是可取消的I/O请求,该域包含了取消时调用的例程
PVOID UserBuffer; //调用者(即发起者,往往是3环程序)提供的输出缓冲区地址

//以下Tail联合成员用于当I/O管理器处理该I/O请求时存放各种工作信息
union {
struct {
union {
KDEVICE_QUEUE_ENTRY DeviceQueueEntry; //设备队列项
struct {
PVOID DriverContext[4]; //由驱动程序解释和使用
};
};
PETHREAD Thread; //指向发起者线程的EHTREAD
PCHAR AuxiliaryBuffer; //辅助缓冲区
struct {
LIST_ENTRY ListEntry; //存放到完成队列中的链表项
union {
struct _IO_STACK_LOCATION *CurrentStackLocation; //指向当前栈单元,驱动程序不可直接访问
ULONG PacketType; //Minipacket的类型
};
};
PFILE_OBJECT OriginalFileObject; //指向原始的文件对象
} Overlay;
KAPC Apc; //特殊内核模式APC或发起者的APC
PVOID CompletionKey; //完成键,用于标识在不同文件句柄上的I/O请求
} 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结构体

    c
    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环函数原型所需的参数,一起来看一下:

c
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的参数了,例如:

c
1
2
3
4
5
//获取IO_STACK_LOCATION
PIO_STACK_LOCATION pStackLocation = IoGetCurrentIrpStackLocation(pIrp);
//获取3环函数参数
ULONG InputBufferLength = pStackLocation->Parameters.DeviceIoControl.InputBufferLength;
ULONG FsControlCode = pStackLocation->Parameters.DeviceIoControl.IoControlCode;

其中,IoGetCurrentIrpStackLocation函数,将Irp指针传进去,可以获取当前驱动程序对应的栈单元。接着就可以通过栈单元获取我们想要的参数了

3环与0环通信(升级)

操作码

在了解了上述知识后,我们就可以对前一篇文章中的代码进行一次升级,更清晰的看到3环和0环的信息交互过程。在此之前,我们需要了解一个操作码。本次实验会在3环程序中新增一个DeviceIoControl函数,因为这个函数能既有传入的参数,也有输出的参数,可以比较直观的看明白3环和0环交互的数据。具体定义如图:

其中需要解释一下的,就是这个dwIoControlCode参数,这个就相当于Switch语句中传入的那个参数,用来判断程序执行流程用的,当操作码不同时,执行的功能也不同,操作码定义如下:

c
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新增部分:

c
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)

//Call IRP_MY_DEVICE_CONTROL
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新增部分:

c
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;

//获取IO_STACK_LOCATION
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;
}

来简要看一下派遣函数的执行流程:

  1. 由于在设备对象的Flags字段定义过缓冲区读取的类型是(DO_BUFFERED_IO),因此我们可以直接从AssociatedIrp.SystemBuffer中读取3环传入的数据,也就是DeviceIoControl中pInputBuffer参数指向的数据。

  2. 通过IoGetCurrentIrpStackLocation函数获取到栈单元,再通过栈单元获取到Parameters中DeviceIoControl结构体里对应的参数

    c
    1
    2
    3
    4
    5
    6
    struct {
    ULONG OutputBufferLength;
    ULONG POINTER_ALIGNMENT InputBufferLength;
    ULONG POINTER_ALIGNMENT IoControlCode;
    PVOID Type3InputBuffer;
    } DeviceIoControl;

    这里我们仅取操作码IoControlCode,用于判断执行流程;以及InputBufferLength,用于打印传入数据

  3. 然后就是根据操作码的不同,执行不同的流程了:

    1)操作码1:不做任何操作,向3环返回2字节大小的数据

    2)操作码2:打印操作码的值;根据传入数据的长度,打印传入的数据;向3环返回5字节大小的数据

完整代码及演示见下篇

参考链接

参考书籍:

《Windows内核原理与实现》

参考文章:

  1. https://www.cnblogs.com/LittleHann/p/3450436.html
  2. https://www.cnblogs.com/lfls128/p/4982309.html
  3. https://blog.csdn.net/qq_41988448/article/details/103519478

参考笔记:

Joney,张嘉杰

参考文档:

  1. https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/ns-wdm-_irp#irp_mj_read
  2. https://docs.microsoft.com/zh-cn/windows-hardware/drivers/ddi/wdm/ns-wdm-_io_stack_location
  3. https://docs.microsoft.com/zh-cn/windows-hardware/drivers/ddi/wdm/nf-wdm-iogetcurrentirpstacklocation
  4. https://docs.microsoft.com/zh-cn/windows-hardware/drivers/ddi/wdm/ns-wdm-_io_status_block
  5. 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相关的基础内容了。不过由于决定不再日更了,后面内容可能也会更冗长了,但是会更细致一些

Author: cataLoc
Link: http://cata1oc.github.io/2020/04/14/%E5%88%9D%E6%8E%A2IRP/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.
Donate
  • 微信
    微信
  • 支付寶
    支付寶