《Windows内核安全与驱动编程》-第五章第一节学习

应用与内核通信

5.1 内核方面的编程

5.1.1 生成控制设备

​ 一个驱动要和应用程序通信,要利用设备对象。设备对象和分发函数构成了整个内核体系的基本框架。设备对象可以在内核中暴露出来给应用层,应用层可以像操作文件一样操作它。一般而言,用于和应用程序通信的设备往往用来“控制”这个内核驱动,所以往往称之为“控制设备对象”。

​ 生成设备可以使用函数 IoCreateDevice 。这个函数的原型如下:

NTSTATUS IoCreateDevice(
    //可以从 DriverEntry 的参数中获得
	IN PDRIVER_OBJECT DriverObject,
    // 设备扩展的大小
    IN ULONG DeviceExtensionSize,
    // 设备名,一般都提供一个
    IN PUNICODE_STRING DeviceName OPTIONAL,
    //设备类型, Windows已经规定了一系列设备类型
    IN DEVICE_TYPE DeviceTpye,
    //需要填写的一组设备属性
    IN ULONG DeviceCharacteristics,
    //设备是否为独占设备。表示同一时刻是否只能被打开一个句柄。一般都不会设置为独占,但是安全软件显然只希望被病毒控制,所以可能会设为独占。
    IN BOOLEAN Exclusive,
    //返回结果
    OUT PDEVICE_OBJECT *DeviceObject
);

​ 使用 IoCreateDevice 生成的控制设备对初学者来说会遇到一个潜在问题。即安全属性的问题。设备具有默认的安全属性,会导致必须要有管理员权限的进程才可以打开它。因此,我们可以使用另一个函数来强迫生成一个任何用户都可以打开的设备。但是这样不安全。

NTSTATUS IoCreateDeviceSecure(
    //可以从 DriverEntry 的参数中获得
	IN PDRIVER_OBJECT DriverObject,
    // 设备扩展的大小
    IN ULONG DeviceExtensionSize,
    // 设备名,一般都提供一个
    IN PUNICODE_STRING DeviceName OPTIONAL,
    //设备类型, Windows已经规定了一系列设备类型
    IN DEVICE_TYPE DeviceTpye,
    //需要填写的一组设备属性
    IN ULONG DeviceCharacteristics,
    //设备是否为独占设备。表示同一时刻是否只能被打开一个句柄。一般都不会设置为独占,但是安全软件显然只希望被病毒控制,所以可能会设为独占。
    IN BOOLEAN Exclusive,
    //对设备对象的安全设置
    IN PCUNICODE_STRING DefaultSDDLString,
    //设备对象的GUID,全球唯一标识符
    IN LPCGUID DeviceClassGuid,
    //返回创建到的设备对象结果
    OUT PDEVICE_OBJECT *DeviceObject
);

DefautSDDLString 的设置可以从 WDK 的帮助中拷贝一个可以支持任何用户打开设备的字符串。DeviceClassGuid 理论上需要由微软提供的函数 CoCreateGuid 来生成(只是调用该函数一次,得到一个设备的GUID,而不是每次执行时都生成一个)。

//接收返回结果
DEVICE_OBJECT g_cdo = NULL;
//生成可以被任何用户操控的设备对象
NTSTATUS DriverEntry(PDRIVER_OBJECT driver,PUNICODE_STRING reg_path)
{
	NTSTATUS status;
	ULONG i;
    UCHAR mem[256] = { 0 };

	// 生成一个控制设备。然后生成符号链接。
	UNICODE_STRING sddl = RTL_CONSTANT_STRING(L"D:P(A;;GA;;;WD)");//该字符串为安全符设置
	UNICODE_STRING cdo_name = RTL_CONSTANT_STRING(L"\\Device\\cwk_3948d33e");//设备名
	UNICODE_STRING cdo_syb = RTL_CONSTANT_STRING(CWK_CDO_SYB_NAME);

    KdBreakPoint();

	// 生成一个控制设备对象。
	status = IoCreateDeviceSecure(
		driver,
		0,&cdo_name,
		FILE_DEVICE_UNKNOWN,
		FILE_DEVICE_SECURE_OPEN,
		FALSE,&sddl,
		(LPCGUID)&CWK_GUID_CLASS_MYCDO,
		&g_cdo);
	if(!NT_SUCCESS(status))
		return status;
		...
}

g_cdo是一个全局变量。一般而言,控制设备生成之后都保存在全局变量中。因为一个驱动程序只有一个控制设备,简单的保存在全局变量里容易在其他函数中识别。

5.1.2 控制设备的名字和符号链接

​ 设备对象可以是没有名字的。但是控制设备需要有一个名字,这样它才会暴漏出来供其他程序打开与之通信。设备的名字可以在调用 IoCreateDeviceIoCreateDeviceSecure 时指定。此外,应用层是无法直接通过设备的名字来打开对象的,为此必须要建立一个暴露给应用层的符号链接。符号链接就是记录一个字符串对应到另一个字符串的一种简单结构。生成符号连接的函数是:

NTSTATUS IoCreateSymbolicLink(
	IN PUNICODE_STRING SymbolicLinkName,//符号连接名
	IN PUNICODE_STRING DeviceName		//设备名
);

​ 一般而言这个函数都会成功。不过,如果一个符号连接的名字已经在系统里存在了,那么这个函数就会失败。符号链接的名字是在Windows中全局存在的。

​ 所以用符号连接是不太稳妥的方式,因为存在重名的可能性。最稳妥的方法是使用 GUID 的方式来访问设备。要进一步详细了解设备对象的访问方式,需要参考专业的硬件驱动开发书籍。

5.1.3 控制设备的删除

​ 既然在驱动中生成了控制设备及符号链接,那么在驱动卸载时就应该删除它们;否则符号链接就会一直存在。删除示例代码如下:

#define CWK_CDO_SYB_NAME L"\\??\\slbkcdo_3948d334" //符号链接名

NTSTATUS DriverEntry(PDRIVER_OBJECT driver,PUNICODE_STRING reg_path)
{
    ...
    //在DriverEntry中,设置了 cwkUnload 即卸载函数
    driver-DriverUnload = ckwUnload;
}
void cwkUnload(PDRIVER_OBJECT driver)
{
    UNICODE_STRING cdo_syb = RTL_CONSTANT_STRING(SLBKCDO_SYB_NAME);
    //如果没有该设备对象则出错
    ASSERT(g_cdo != NULL);
    //依次删除
    IoDeleteSymblicLink(&cdo_syb);
    IoDeleteDevice(g_cdo)
}
5.1.4 分发函数

​ 分发函数是一组用来处理发送给设备对象的请求的函数。分发函数是设置在驱动对象上的。即每一个驱动都有一组自己的分发函数。Windows 的 IO 管理器在收到请求时,会根据请求发送的目标,也就是一个设备对象,来调用这个设备对象所丛书的驱动对象上对应的分发函数。

​ 不同的分发函数处理不同的请求。在本章中,作者使用到了三种请求:

  • 打开(Create): 在试图访问一个设备对象之前,必须先用打开请求“打开”它。只有得到成功的返回,才可以发送其他的请求。

  • 关闭(Close): 在结束访问一个设备对象之后,发送关闭请求将它关闭。关闭之后就必须再次打开才能访问。

  • 设备控制(Device Control) : 设备控制请求是一种既可以用来输入(从应用到内核)。 又可以用来输出 (从内核到应用) 的请求。因此很适合本节的需求。

    一个标准的分发函数原型如下:

    NTSTATUS cwwDispatch(
    	IN PDEVICE_OBJECT dev,
    	IN PIRP irp
    	)
    

    ​ 其中的 dev 就是请求要发送给的目标对象: irp 则是代表请求内容的数据结构的指针。无论如何,分发函数必须先首先设置驱动对象,这个工作一般在 DriverEntry 中完成。

    NTSTATUS DriverEntry(PDRIVER_OBJECT driver,PUNICODE_STRING reg_path)
    {
        ...
    	// 所有的分发函数都设置成一样的。
    	for(i=0;i<IRP_MJ_MAXIMUM_FUNCTION;i++)
    	{
    		driver->MajorFunction[i] = cwkDispatch;
    	}
        ...
    }
    

    在该代码片段中,MajorFunction是一个函数指针数组,保存所有分发函数的指针。以上代码都设置成同一种函数,实际中可以设置不同的分发函数。

5.1.5 请求的处理

​ 在分发函数中处理请求的第一步是获取当前栈空间。请求的栈空间结构是适应于 Windows 内核驱动中设备对象的栈结构的。但是这不是本书的重点。

  • 打开请求的主功能号是 IRB_MJ_CREATE
  • 关闭请求的主功能号是 IRP_MJ_CLSOE
  • 设备请求控制的功能号是 IRP_MJ_DEVICE_CONTROL

​ 请求的当前栈空间可以用 IoGetCurrentIrpStackLocation 取得,然后根据主功能号做出不同的处理。代码如下:

NTSTATUS cwkDispatch(		
			      IN PDEVICE_OBJECT dev,
			      IN PIRP irp)
{
    //获取当前栈空间
    PIO_STACK_LOCATION  irpsp = IoGetCurrentIrpStackLocation(irp);
    NTSTATUS status = STATUS_SUCCESS;
    ULONG ret_len = 0;
    while(dev == g_cdo) 
    {
        // 如果这个请求不是发给g_cdo的,那就非常奇怪了。
        // 因为这个驱动只生成过这一个设备。所以可以直接
        // 返回失败。
	    if(irpsp->MajorFunction == IRP_MJ_CREATE || irpsp->MajorFunction == IRP_MJ_CLOSE)
	    {
            // 生成和关闭请求,这个一律简单地返回成功就可以
            // 了。就是无论何时打开和关闭都可以成功。
            break;
	    }
    	
        if(irpsp->MajorFunction == IRP_MJ_DEVICE_CONTROL)
	    {
		    // 处理DeviceIoControl。
            PVOID buffer = irp->AssociatedIrp.SystemBuffer;  
            ULONG inlen = irpsp->Parameters.DeviceIoControl.InputBufferLength;
            ULONG outlen = irpsp->Parameters.DeviceIoControl.OutputBufferLength;
		    ULONG len;
		    switch(irpsp->Parameters.DeviceIoControl.IoControlCode)
		    {
            case CWK_DVC_SEND_STR:
                ASSERT(buffer != NULL);
                ASSERT(inlen > 0);
                ASSERT(outlen == 0);
                DbgPrint((char *)buffer);
                // 已经打印过了,那么现在就可以认为这个请求已经成功。
                break;
            case CWK_DVC_RECV_STR:
            default:
                // 到这里的请求都是不接受的请求。未知的请求一律返回非法参数错误。
                status = STATUS_INVALID_PARAMETER;
                break;
            }
        }
        break;
    }
    // 到这里的请求都是不接受的请求。未知的请求一律返回非法参数错误。
	irp->IoStatus.Information = ret_len;
	irp->IoStatus.Status = status;
	IoCompleteRequest(irp,IO_NO_INCREMENT);
	return status;
}
明日计划

继续学习驱动编程。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值