当前位置 博文首页 > KOOKNUT的博客:Ring3与Ring0之间的通信方式(Windows内核学习笔

    KOOKNUT的博客:Ring3与Ring0之间的通信方式(Windows内核学习笔

    作者:[db:作者] 时间:2021-07-03 09:22

    我们写一个Ring0的代码,有时候需要和Ring3进行交互,比如我们做一个进程防杀的驱动保护,那我们可以通过Ring3的进程,发送请求告诉驱动层,究竟哪些进程属于白名单,哪些属于黑名单,而在通信过程中,涉及到了应用层与内核层之间通信的一些数据传输方法,我在这里简单做一个学习笔记:
    如果一个驱动要和应用程序通信,首先需要生成一个设备对象(DeviceObject),设备对象可以通过某种方式在内核中暴露出来给用户层,应用层就可以像操作文件一样操作它。而在内核层创建一个设备对象,Windows也提供给了我们底层函数接口:

    • IoCreateDevice

    可以用这个底层函数创建一个与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内核安全与驱动开发》

    cs