当前位置 博文首页 > KOOKNUT的博客:Ring3与Ring0之间的通信方式(Windows内核学习笔
我们写一个Ring0的代码,有时候需要和Ring3进行交互,比如我们做一个进程防杀的驱动保护,那我们可以通过Ring3的进程,发送请求告诉驱动层,究竟哪些进程属于白名单,哪些属于黑名单,而在通信过程中,涉及到了应用层与内核层之间通信的一些数据传输方法,我在这里简单做一个学习笔记:
如果一个驱动要和应用程序通信,首先需要生成一个设备对象(DeviceObject),设备对象可以通过某种方式在内核中暴露出来给用户层,应用层就可以像操作文件一样操作它。而在内核层创建一个设备对象,Windows也提供给了我们底层函数接口:
可以用这个底层函数创建一个与Ring3进行通信的控制设备对象。使用这个函数需要注意,它生成的设备对象具有默认的安全属性,需要有管理员权限的进程才可以打开这个设备对象。对于我们用来通信的控制设备来说,肯定是需要一个设备名称的,上面我们还提到,设备名是无法直接被用户层所打开的,需要一些特殊的操作,而这个操作具体就是,我们为当前设备对象创建一个符号链接名,用来被Ring3层打开。而这些名称的定义也有一个确定的规范:
#define DEVICE_OBJECT_NAME L"\\Device\\Kt_DeviceObjectName" //驱动之间用的 命名规范就是这样,最后'\\'后面的字符串是可以自己定义的
#define DEVICE_LINK_NAME L"\\DosDevices\\Kt_DeviceLinkName" //Ring3和Ring0之间通信
RtlInitUnicodeString(&DeviceObjectName, DEVICE_OBJECT_NAME);
//创建与Ring3层通信的控制设备对象 也称之为CDO Control Device Object
Status = IoCreateDevice(
DriverObject,
0,
&DeviceObjectName,
FILE_DEVICE_UNKNOWN,
FILE_DEVICE_SECURE_OPEN,
FALSE,//是否独占,若独占,在某一时刻只能被打开一个句柄
&DeviceObject
);
if (!NT_SUCCESS(Status))
{
return Status;
}
RtlInitUnicodeString(&DeviceLinkName, DEVICE_LINK_NAME);
//为了和Ring3层进行通信,创建一个符号链接名
Status = IoCreateSymbolicLink(
&DeviceLinkName,
&DeviceObjectName
);
if (!NT_SUCCESS(Status))
{
//如果符号链接名创建失败,则直接over
IoDeleteDevice(DeviceObject);
return Status;
}
驱动卸载时候,记得销毁设备链接名,删除创建的设备对象。
应用层需要连接符号链接名时,使用文件操作的API CreateFile函数即可:
//通过Ring0的设备对象的设备链接名进行打开获取设备对象句柄
DeviceHandle = CreateFile(
_T("\\\\.\\Kt_DeviceLinkName"),//需要转义字符\\.\Kt_DeviceLinkName
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
NULL,
OPEN_EXISTING,
FILE_FLAG_NO_BUFFERING,
NULL);
一旦连接设备成功,则需要像设备发送设备请求:
//CreateFile失败函数返回不是NULL,而是INVALID_HANDLE_VALUE
if (DeviceHandle != INVALID_HANDLE_VALUE)
{
//向设备请求IO
BOOL IsOk = DeviceIoControl(
DeviceHandle,
MY_IOCTL_CODE, //自定义的某种IO请求方式
InputBuffer,
InputBufferLength,
OutBuffer,
OutBufferLength,
&v1,
NULL);
}
经过前面的一些铺垫,接下来我们进入今日正题,来看一下自定义控制码的方法:
#define MY_IOCTL_CODE \
CTL_CODE \
( \
FILE_DEVICE_UNKNOWN,\ //未知的类型
0x911, \ //生成功能号的核心数字,并且不大于0xfff,0x000~0x7ff被微软预留
METHOD_NEITHER, \ //数据传输的方式,重点
FILE_ANY_ACCESS \ //文件操作的权限
)
我们可以用上面这种方式,来设置一个自己的设备控制码,CTL_CODE是一个SDK的宏:
#define CTL_CODE( DeviceType, Function, Method, Access ) ( \
((DeviceType) << 16) | ((Access) << 14) | ((Function) << 2) | (Method) \
)
其中用来区分数据是以何种方式传输到内核层的参数,是第三参数,其总共有三种方式(直接输入和输出归结为直接方式),分别是:
#define METHOD_BUFFERED 0 //缓冲区
#define METHOD_IN_DIRECT 1 //直接输入方式
#define METHOD_OUT_DIRECT 2 //直接输出方式
#define METHOD_NEITHER 3 //原始方式
METHOD_BUFFERED:
若使用这种方式传输数据,那么我们的数据将会通过PIRP->AssociatedIrp.SystemBuffer来进行用户层输入与输出数据的缓冲,Ring0将数据进行拷贝,而不是直接对Ring3层地址进行访问,所以这种方式比较安全。
METHOD_IN_DIRECT/METHOD_OUT_DIRECT:
使用这种方式传输数据,我们的输入也将会通过PIRP->AssociatedIrp.SystemBuffer来进行用户层输入数据的缓冲,而输出数据是以MDL映射的方式,锁定用户区的内存,直到Ring0完成I/O请求之后,Ring3层才可以访问这块内存,也算是相对安全的一种方式。IN和OUT的区别是对于打开设备的权限,当只读打开,使用METHOD_IN_DIRECT成功,METHOD_OUT_DIRECT失败。如果读写权限,则都可以。
METHOD_NEITHER:
使用这种方式传输数据,我们通过PIO_STACK_LOCATION->Parameters.DeviceIoControl.Type3InputBuffer获取用户层输入地址,输出数据地址通过PIRP->UserBuffer来存放。使用这种方式时候,驱动可以直接对用户层地址进行读写,所以一定要注意对用户区提供的地址进行检查(小心蓝屏),看看参数是否合法。使用ProbeForRead和ProbeForWrite函数来进行地址校验。
还有一篇博文写的特别好,图文并茂,给出链接:
https://www.cnblogs.com/lsh123/p/7354573.html
“世人见我恒殊调,闻余大言皆冷笑。”–李白
参考书籍:
《Windows内核安全与驱动开发》