2.1.2共享的内核空间

进程的空间被分成两部分:一部分供进程独立使用,称为用户空间;另一部分容纳操作系统的内核,称为内核空间.
在32位系统上,低2GB是用户空间,高2GB是内核空间.
x86架构下r0层的代码才能访问内核空间,普通应用程序都运行在r3层,要
访问r0层的功能
一般通过操作系统提供的一个入口(在该入口调用sysenter指令)
windows的系统进程是名为"System"的进程,这个进程pid在xp下始终为4,调用PsGetCurrentProcessId会发现内核模块中分发函数调用时,当前进程一般都不是System进程.但是DriverEntry函数被调用时,一般都位于系统进程中,因为windows用系统进程来加载内核模块.
2.2.1数据类型
从x86到x64,除了所有的指针从4字节变成了8字节,其它数据类型的字节宽度都没有变化.
2.2.2返回状态
大部分内核API的返回值都是一个返回状态,也就是一个错误码,类型为NTSTATUS.
使用宏NT_SUCCESS()可以判断一个返回值是否成功.
2.2.3字符串
驱动里的字符串用一个结构来容纳,定义如下:
typedef struct _UNICODE_STRING
{
USHORT Length;
USHORT MaximumLength;
PWSTR Buffer;
}UNICODE_STRING *PUNICODE_STRING;
UNICODE_STRING是可以直接打印的(注意是指针,结构本身不能打印).
UNICDE_STRING str = RTL_CONSTANT_STRING(L:"打印内容");
DbgPrint("%wZ",&str);
2.3.1驱动对象
windows内核中,一个驱动,一个设备,一个文件等都是一个对象,每个内核对象都用一个结构体来表示.
驱动对象的结构如下:
typedef struct _DRIVER_OBJECT
{
//结构的类型和大小
CSHORT Type;
CSHORT Size;
//设备对象指针
PDEVICE_OBJECT DeviceObject;
...
//这个内核模块在内核空间中的开始地址和大小
PVOID DiverStart;
ULONG DiverSize;
...
//驱动的名字
UNICODE_STRING DriverName;
...
//快速IO分发函数
PFAST_IO_DISPATCH FastIoDispatch;
...
//驱动的卸载函数
PDRIVER_UNLOAD.DriverUnload;
//普通分发函数
PDRIVER_DISPATCH MajorFunction[IRP_MJ_MAXIMUM_FUNCTION + 1];
}DRIVER_OBJECT;
内核模块并不生成一个进程,只是填写一组回调函数让windows来调用.
2.3.2设备对象
在内核中,大部分消息都以请求(IRP)的方式传递.设备对象(DEVICE_OBJECT)是唯一可以接收请求的实体.
在WDK的wdm.h设备对象的定义如下:
typedef struct DECLSPEC_ALIGN(MEMORY_ALLOCATION_ALIGNMENT)
_DEVICE_OBJECT
{
CSHORT Type;
USHORT Size;
//引用计数
ULONG ReferenceCount;
//这个设备所属的驱动对象
struct _DRIVER_OBJECT *DriverObject;
//下一个设备对象.在一个驱动对象中有n个设备,这些设备用这个指针连接起来作为一个单向链表
string _DEVICE_OBJECT *NextDevice;
//设备类型
DEVICE_TYPE DeviceType;
//IRP栈大小
HAR StackSize;
...
}DEVICE_OBJECT;
当Windows内核向一个设备发送一个请求时,与设备关联驱动对象的分发函数的某一个会被调用.
分发函数的原型如下:
//分发函数数的第一个参数device是请求的目标设备,第二个参数是IRP的指针
NTSTATUS Mydispatch(PDEVICE_OBJECT device,PIRP irp);
2.3.3请求
何为请求?
如果要求网卡发送一个数据包,或者向网卡请求把已经在缓冲区接收到的包读出来,这就是一个请求;
如果读取一个文件从0开始的512字节,这也是一个请求;
如果在磁盘的64MB位置写入长达512个字节的一组数据,这也是一个请求.
这些操作最终在内核中会被IO管理器翻译成请求(IRP或者与之等效的其他形式,比如快速IO调用)发送往某个设备对象
WDK的wdm.h中IRP的结构结构如下:
typedef struct DECLSPEC_ALIGN(MEMORY_ALLOCATION_ALIGNMENT)
_IRP{
//类型和大小
CSHORT Type;
USHORT Size;
//内存描述符链表指针,实际上,这里用来描述一个缓冲区
PMDL MdlAddress;
...
//下面共用体有一个SystemBuffer.这是比MdlAddress稍微简的表示缓冲区的一种方式.
//IRP用MdlAddress还是用SystemBuffer取决于这次请求的IO方式.
union
{
struct _IRP *MasterIrp;
__volatile LONG IrpCount;
PVOID SystemBuffer;
}AssociatedIrp;
//IO状态.一般请求完成之后的返回情况放在这里
IO_STATUS_BLOCK IoStatus;
//IRP栈空间大小
CHAR StackCount;
//IRP当前栈空间数组索引
CHAR CurrentLocation;
....
//用来取消一个未决请求的函数
__volatile PDRIVER_CANCEL CancelRoutine;
//与MdlAddress和SystemBuffer一样都可以表示缓冲区.特性稍有不同.
PVOID UserBuffer;
union
{
...
//发出这个请求的线程
PETHREAD Thread;
...
struct
{
LIST_ENTRY ListEntry;
union
{
//一个IRP栈空间元素
struct _IO_STACK_LOCATION *CurrentStackLocation;
...
};
}Overlay;
...
}Tail;
}IRP,*PIRP;
CurrentLocatiot是
当前IO_STACK_LOCATION的数组索引。索引是从1开始,没有0。当驱动程序准备向次低层驱动程序传递IRP时可以调用IoCallDriver例程,它其中的一个工作是递减当前IO_STACK_LOCATION的索引,使之与下一层的驱动程序匹配。但该索引不会设置成0,如果设置成0,系统将会崩溃。就是说,最底层的驱动程序不会调用IoCallDriver例程
注意所谓的IRP的栈空间.一个IRP往往要传递N个设备才能完成,在传递过程中,可能会一些"中间变换",导致请求的参数变化.为了保存这种参数变化,设定每次"中转"都留一个"栈空间",用来保存中间参数
.CurrentLocation表示当前使用哪个IRP栈空间.
请求的类型如下:
生成请求:主功能号为IRP_MJ_CREATE的IRP
查询请求:主功能号为IRP_MJ_QUERY_INFORMATION的IRP
设置请求:主功能号为IRP_MJ_SET_INFORMATION的IRP
控制请求:主功能号为IRP_MJ_DEVICE_CONTROL的IRP
关闭请求:主功能号为IRP_MJ_CLOSE的IRP
2.4.2常用内核API
| 功能简述 |
ExAllocatePool | 内存分配.相当于C RunTime库里的malloc |
ExFreePool | 内存释放.相当于C RunTime库里的free |
ExAcquireFastMutex | 获取一个快速互斥体,用于多线程环境下的同步 |
ExReleseFastMutex | 释放一个快速互斥体 |
ExRaiseStatus | 抛出一个异常,带有一个错误的status值 |
| 对应的Nt-函数 | 功能简述 |
ZwCreateFile | NtCreateFile | 打开文件或设备 |
ZwWriteFile | NtWriteFile | 写文件或发送写请求给设备 |
ZwReadFile | NtReadFile | 读文件或发送读请求给设备 |
ZwQueryDirectoryFile | NtQueryDirectoryFile | 目录查询 |
ZwDeviceIoControlFile | NtDeviceIoControlFile | 发送设备控制请求 |
ZwCreateKey | NtCreateKey | 打开一个注册表键 |
ZwQueryValueKey | NtQueryValueKey | 读取一个注册表中的值 |
注意:Nt-系列函数在WDK帮助里是查不到的,头文件也没有,但是确实存在,自己声明后就可以使用.
| 功能简述 |
RtlInitUnicodeString | 初始化一个字符串 |
RtlCopyUnicodeString | 拷贝字符串 |
RtlAppendUnicodeToString | 将一个字符串追加到另一个字符串后 |
RtlStringCbPrintf | 将字符串打印到一个字符串中,相当于sprintf |
RtlCopyMemory | 内存数据块拷贝 |
RtlMoveMemory | 内存数据块移动 |
RtlZeroMemory | 内存数据清零 |
RtlCompareMemory | 比较内存 |
RtlGetVersion | 获得当前Windows版本 |
| 功能简述 |
IoCreateFile | 打开文件.这个函数比ZwCreateFile要更加底层 |
IoCreateDevice | 生成一个设备对象 |
IoCallDriver | 发送请求.Io管理器调用这个函数把不同的IRP发送到不同的设备 |
IoCompleteRequest | 完成请求.通知IO管理器这个IRP已经完成了 |
IoCopyCurrentIrpStackLocationToNext | 将当前IRP栈空间拷贝到下一个栈空间 |
IoSkipCurrentIrpStackLocationToNext | 跳过当前IRP的栈空间 |
IoGetCurrentIrpStackLocation | 获得IRP的当前栈空间指针 |
Io系列函数是Io管理器将用户调用的API函数翻译成IRP或者等价请求发送到内核各个不同的设备的关键组件.
2.5 windows的驱动开发模型
VXD windows 9x 上的驱动,已淘汰.
KDM windows NT上的驱动,KDM和WDM都称为传统型驱动.
WDM windows2000时期及之后的驱动,必须满足被要求的特性(如电源管理,即插即用等),不满足的属于NT式驱动(KDM).
WDF 调用了WDF相关内核API的驱动,可调用传统型驱动的API,为传统型的升级版
2.6.1内核编程的主要调用源
假定函数B调用了函数A,则函数B为A的调用者;
继续寻找B的调用者,直到代码可见范围内最用一个调用都函数M,函数M就是函数A调用源
从调用源出发,最后调用函数A的整个路径,称为函数A的调用路径.
单线程的用户态程序只有一个调用源,就是主函数.主函数也有调用者,但是被编译器隐藏了.
内核函数的调用源如下:
- 入口函数DriverEntry和卸载函数DriverUnload.
- 各种分发函数(包括普通分发函数和快速Io分发函数).
- 处理请求时设置的完成函数.即请求完成后被系统调用的回调函数.
- 其它回调函数(各种NDIS回调函数,NDIS是网络相关的一些驱动程序函数)
备注:调用源概念对逆向分析很重要.
2.6.2函数的多线程安全性
函数的多线程安全性是指,一个函数被调用过程中,还未返回时,又被其它线程调用的情况下(函数被重入),函数执行结果的可靠性.
如果是可靠的,则称这个函数是多线程安全的,如果是不可靠的,则称这个函数是非多线程安全的.
多线程安全性的规则:
- 只运行于单线程环境的函数,不需要多线程安全性,可能运行于多线程环境的函数,必须是多线程安全的.
- 如果函数A的所有调用源只运行于同一单线程环境,则A也是只运行在单线程环境的
- 如果函数A的其中一个或多个调用源运行于一个或多个并发的多线程环境,而且调用路径上没有采取多线程序列化成单线程的强制措施(指如互斥体,自旋锁等同步手段),则函数A是运行在多线程环境的.
- 只使用函数内部资源,完全不使用全局变量,静态变量或其它全局性资源的函数是多线程安全的.
- 如果对某个全局变量或者静态变量的所有访问都被强制为同一时刻只有一个线程访问,则使用这个全局变量和静态变量的函数多线程安全性没有影响.
是否保证函数可重入性最终由调用源和调用路径决定.
2.6.3代码的中断级(IRQL级别)
Passive级:被动级,许多比较复杂的内核API都必须在Passive级执行,这一点在WDK的文档上都有说明
Dispatch级:比Passive级别高,只有比较简单的函数能在Dispatch级执行.
调用任何内核API之前,必须查看WDK文档,了解这个内核API的中断级要求.
判断正在编写的代码的中断级:
- 如果调用路径上没有导致中断屁股提高和降低的情况,则一个函数执行时的中断级和他的调用用源中断级相同.
- 如果调用路径上有获取自旋锁,则中断级随之提高;如果调用路径上有释放自旋锁,则中断级随之下降
和多线程安全性一样,当前代码的中断级基本上取决于调用源的中断级和调用路径.
内核代码主要调用源的运行中断级
调用源 | 一般的运行中断级 |
DriverEntry,DriverUnload | Passive级 |
各种分发函数 | Passive级 |
完成函数 | Dispatch级 |
各种NDIS回调函数 | Dispatch级 |
注意:可以强制提高或降低代码的当前中断级.但是windows代码都运行在规范的中断级上,任意的降低中断级会导致产生不可预料结果.
2.6.4WDK中出现的特殊代码
#define IN
//
IN表示这个参数用于输入
#define OUT
//
OUT表示这个参数用于返回结果
#pragma alloc_text(INIT,DriverEntry)
#pragma alloc_text(PAGE,NdisProtOpen)
#pragma alloc_text这个宏指定某个函数的可执行代码编译出来后在sys文件中的位置.
内核模块编译出来后是一个PE格式的sys文件,这个文件代码段(text段)中有不同的节(Section),不同的节被加载到内存中处理情况不同.
INIT节是在初始化完毕后就被释放.PAGE节位于可以进行分页交换的内存空间.如果未用上述预编译指令处理,则代码默认位于PAGELK节,加载后位于不可分页交换的内存空间中.
注意:放在PAGE节的函数不可以在Dispatch级调用,因为这种函数会诱发缺页中断,缺页中断不能在Dispatch级完成.