Windows内核漏洞初探

by Netfairy - 2015-09-11

0x00 前言

传统的Ring3层漏洞由于多种保护技术的结合使得利用越来越困难,这时候,攻击者把目光转到内核也就顺理成章了。内核代码运行在Ring0,拥有最高的特权级,可以执行所有的指令,包括特权指令。所以,一旦内核出现漏洞,危害是极其严重的。一旦攻击者成功利用内核漏洞执行了shellcode,因为这些代码运行在Ring0,所以攻击者可以做任何事。内核漏洞一般也叫驱动漏洞,驱动一般以.sys为后缀,因为我们的机器包含了各种各样的外设,而这些外设和机器的通信是通过驱动来交互的,还有杀毒软件等一些程序也自带了驱动。本文我们把目光聚焦在驱动上,分析驱动漏洞的表现形式及利用技术。


0x01 关于驱动程序

驱动程序一般指的是设备驱动程序(Device Driver),是一种可以使计算机和设备通信的特殊程序。相当于硬件的接口,操作系统只有通过这个接口,才能控制硬件设备的工作,假如某设备的驱动程序未能正确安装,便不能正常工作。下面来介绍驱动程序的框架,学习一门语言,我看到的第一个例子总是 " Hello world ",因此,我还还是以hello world来介绍驱动编程,但是注意,本文不是讲如何编写驱动程序,如果你想了解更多关于驱动编程,也叫内核编程,我推荐:《天书夜读》。现在,让我们开始看第一个驱动程序吧:

/********************************************************************
	时间:		2015/9/11
	文件: 		helloworld.c
	作者:		Netfairy
*********************************************************************/
#include <NTDDK.h>
//创建的设备对象指针
PDEVICE_OBJECT p_DeviceObject;

//驱动卸载函数
VOID DriverUnload( IN PDRIVER_OBJECT  driverObject )
{
	//什么都不做,只是打印一句话
	KdPrint(("驱动卸载,再见!\n"));
} 

//驱动派遣例程函数
NTSTATUS DrvDispatch(IN PDEVICE_OBJECT driverObject,IN PIRP pIrp)
{ 
	
	//设置IRP状态
	pIrp->IoStatus.Status=STATUS_SUCCESS;

	//设置IRP操作字节数
	pIrp->IoStatus.Information=0;

	//完成IRP的处理
	IoCompleteRequest(pIrp,IO_NO_INCREMENT);

	return STATUS_SUCCESS;
}

//驱动入口函数(类似于main或WinMain)
NTSTATUS DriverEntry( IN PDRIVER_OBJECT  driverObject, IN PUNICODE_STRING  registryPath )
{ 

	NTSTATUS       ntStatus;
	UNICODE_STRING devName;
	UNICODE_STRING symLinkName;
	int i=0; 

	//打印一句调试信息
	KdPrint(("Hello world!!!\n"));

	//设置该驱动对象的卸载函数
	//driverObject->DriverUnload = DriverUnload; 

	//创建设备 
	RtlInitUnicodeString(&devName, L"\\Device\\HelloWorld");
	ntStatus = IoCreateDevice( driverObject,0,&devName,FILE_DEVICE_UNKNOWN,0, TRUE,&p_DeviceObject );
	
	//创建符号链接  
	RtlInitUnicodeString(&symLinkName,L"\\DosDevices\\HelloWorld");
	ntStatus = IoCreateSymbolicLink( &symLinkName,&devName );
	
	//设置该驱动对象的派遣例程函数
	for (i = 0; i < IRP_MJ_MAXIMUM_FUNCTION; i++)
	{
		driverObject->MajorFunction[i] = DrvDispatch;
	}
	//返回成功结果
	return STATUS_SUCCESS;
}

程序很简单:首先看


ntStatus = IoCreateDevice( driverObject,0,&devName,FILE_DEVICE_UNKNOWN,0, TRUE,&p_DeviceObject );
驱动本质是用来完成一定任务的,那么它必然要能和Ring3进行交互,所以在驱动中调用此函数来创建设备对象,标示这个驱动本身。


下面接着看


ntStatus = IoCreateSymbolicLink( &symLinkName,&devName )
IoCreateSymbolicLink函数创建一个符号链接,以后跟驱动进行交互就使用这个符号链接名HelloWorld。驱动处理程序发来的消息,是通过



driverObject->MajorFunction[i] = DrvDispatch;
派遣的,有点类似WIN32编程的消息派遣,由于这个程序简单,我把全部的消息都派遣给DrvDispatch这个函数处理。更多请参考http://www.programlife.net/io_stack_location-irp.html。这个驱动加载后会输出 HelloWorld,请用Dbgview.exe观察输出而不是控制台。



0x02 内核漏洞介绍

关于内核漏洞的分类我没有找到权威的说法,所以本文也不打算介绍内核漏洞的类型。我只讲很常见一种,也是下来我要演示的:任意地址写任意数据。这个漏洞允许攻击者在任意地址(包括内核地址)写任意数据,这是很严重的,想想假如我们在驱动程序中把某个地址覆写为我们shellcode的地址,然后想办法跳到这个地址执行,由于驱动运行在Ring0....为了演 示内核漏洞,我决定用0day安全:软件漏洞分析技术里面的一个例子。原代码如下:

/********************************************************************
	created:	2010/12/06
	filename: 	D:\0day\ExploitMe\exploitme.c
	author:		shineast
	purpose:	Exploit me driver demo 
*********************************************************************/
#include <ntddk.h>

#define DEVICE_NAME L"\\Device\\ExploitMe"
#define DEVICE_LINK L"\\DosDevices\\DRIECTX1"
#define FILE_DEVICE_EXPLOIT_ME 0x00008888
#define IOCTL_EXPLOIT_ME (ULONG)CTL_CODE(FILE_DEVICE_EXPLOIT_ME,0x800,METHOD_NEITHER,FILE_WRITE_ACCESS)

//创建的设备对象指针
PDEVICE_OBJECT g_DeviceObject;

/**********************************************************************
 驱动派遣例程函数
	输入:驱动对象的指针,Irp指针
	输出:NTSTATUS类型的结果
**********************************************************************/
NTSTATUS DrvDispatch(IN PDEVICE_OBJECT driverObject,IN PIRP pIrp)
{ 
	PIO_STACK_LOCATION pIrpStack;//当前的pIrp栈
	PVOID Type3InputBuffer;//用户态输入地址
	PVOID UserBuffer;//用户态输出地址 
	ULONG inputBufferLength;//输入缓冲区的大小
	ULONG outputBufferLength;//输出缓冲区的大小 
	ULONG ioControlCode;//DeviceIoControl的控制号
	PIO_STATUS_BLOCK IoStatus;//pIrp的IO状态指针
	NTSTATUS ntStatus=STATUS_SUCCESS;//函数返回值 

	//获取数据
	pIrpStack = IoGetCurrentIrpStackLocation(pIrp);
	Type3InputBuffer = pIrpStack->Parameters.DeviceIoControl.Type3InputBuffer;
	UserBuffer = pIrp->UserBuffer;
	inputBufferLength = pIrpStack->Parameters.DeviceIoControl.InputBufferLength; 
	outputBufferLength = pIrpStack->Parameters.DeviceIoControl.OutputBufferLength; 
	ioControlCode = pIrpStack->Parameters.DeviceIoControl.IoControlCode;
	IoStatus=&pIrp->IoStatus;
	IoStatus->Status = STATUS_SUCCESS;// Assume success
	IoStatus->Information = 0;// Assume nothing returned

	//根据 ioControlCode 完成对应的任务
	switch(ioControlCode)
	{
	case IOCTL_EXPLOIT_ME: 
		if ( inputBufferLength >= 4 && outputBufferLength >= 4 )
		{
			*(ULONG *)UserBuffer = *(ULONG *)Type3InputBuffer;
			IoStatus->Information = sizeof(ULONG);
		}
		break;
	}  

	//返回
	IoStatus->Status = ntStatus; 
	IoCompleteRequest(pIrp,IO_NO_INCREMENT);
	return ntStatus;
}
/**********************************************************************
 驱动卸载函数
	输入:驱动对象的指针
	输出:无
**********************************************************************/
VOID DriverUnload( IN PDRIVER_OBJECT  driverObject )
{ 
	UNICODE_STRING symLinkName; 
	KdPrint(("DriverUnload: 88!\n")); 
	RtlInitUnicodeString(&symLinkName,DEVICE_LINK);
	IoDeleteSymbolicLink(&symLinkName);
	IoDeleteDevice( g_DeviceObject ); 
} 
/*********************************************************************
 驱动入口函数(相当于main函数)
	输入:驱动对象的指针,服务程序对应的注册表路径
	输出:NTSTATUS类型的结果
**********************************************************************/
NTSTATUS DriverEntry( IN PDRIVER_OBJECT  driverObject, IN PUNICODE_STRING  registryPath )
{ 
	NTSTATUS       ntStatus;
	UNICODE_STRING devName;
	UNICODE_STRING symLinkName;
	int i=0; 
	//打印一句调试信息
	KdPrint(("DriverEntry: Exploit me driver demo!\n"));
	//创建设备 
	RtlInitUnicodeString(&devName,DEVICE_NAME);
	ntStatus = IoCreateDevice( driverObject,
		0,
		&devName,
		FILE_DEVICE_UNKNOWN,
		0, TRUE,
		&g_DeviceObject );
	if (!NT_SUCCESS(ntStatus))
	{
		return ntStatus;  
	}
	//创建符号链接  
	RtlInitUnicodeString(&symLinkName,DEVICE_LINK);
	ntStatus = IoCreateSymbolicLink( &symLinkName,&devName );
	if (!NT_SUCCESS(ntStatus)) 
	{
		IoDeleteDevice( g_DeviceObject );
		return ntStatus;
	}
	//设置该驱动对象的卸载函数
	driverObject->DriverUnload = DriverUnload; 
	//设置该驱动对象的派遣例程函数
	for (i = 0; i < IRP_MJ_MAXIMUM_FUNCTION; i++)
	{
		driverObject->MajorFunction[i] = DrvDispatch;
	}
	//返回成功结果
	return STATUS_SUCCESS;
}





除去注释信息,实际上也没有多少代码。这个驱动存在任意地址写任意数据漏洞。我们看到驱动首先创建了ExploitMe这个设备,然后把所有的信息都交给DrvDispatch函数处理。在DrvDispatch内部

Type3InputBuffer = pIrpStack->Parameters.DeviceIoControl.Type3InputBuffer;
获取用户态输入缓冲区的地址,用户态程序传给驱动的数据就保存在这个地址。


outputBufferLength = pIrpStack->Parameters.DeviceIoControl.OutputBufferLength;
获取用户态输出缓冲区地址,驱动向这个地址填充数据返回给用户态程序,完成交互。



ioControlCode = pIrpStack->Parameters.DeviceIoControl.IoControlCode;
IoControlCode是用户态传给驱动去执行某一子分支的代码,所以接下来我们看到


switch(ioControlCode)
IoControlCode进行识别,然后执行相应的分支,因此,用户态程序传过来的IoControlCode和驱动定义的IoControlCode必须对应,否则驱动无法识别。那么,驱动定义的IoControlCode都有哪些呢?在这个例子中,我们只看到了 IOCTL_EXPLOIT_ME。关于它的十六进制数值计算,请看


#define IOCTL_EXPLOIT_ME (ULONG)CTL_CODE(FILE_DEVICE_EXPLOIT_ME,0x800,METHOD_NEITHER,FILE_WRITE_ACCESS)
并且用我写的一个小工具计算


1.png

下面看驱动如何处理8888a003这个控制代码

if ( inputBufferLength >= 4 && outputBufferLength >= 4 )
		{
			*(ULONG *)UserBuffer = *(ULONG *)Type3InputBuffer;
			IoStatus->Information = sizeof(ULONG);
		}
		break;
首先判断输入输出缓冲区长度,如果长度都大于4,那么就把输入缓冲区的前四字节内容赋值给输出缓冲区,由于驱动对输入输出缓冲区没有任何检查,且输入输出缓冲区是是我们用户层程序传给驱动的,那么我们可以传递任何值。假设我们输入缓冲区前四个字节是shellcode的地址,输出缓冲区是指向函数A的地址。当我们给驱动发送8888a003这个控制代码发送数据时,原本保存着函数A地址,现在被shellcode地址覆盖了,此时我们再调用函数A,我们的shellcode就被激活。一个任意地址写任意数据漏洞发生了。



0x03 实践ExploitMe漏洞利用 (完整代码见 0day安全:内核漏洞利用技技术)

关于这个驱动的利用书上已经讲的很清楚了,需要说明的是,在我的机器上,这个Exploit并没有执行成功(如果你幸运的话,或许可以)为了证明我们的shellcode确实执行了。我在Exploit开头定义了一个全局变量

//验证shellcode执行成功的标志
int flag=0;
在Ring0Shellcode把flag修改为 88,最后在程序末尾打印flag


printf("flag=%d\n",flag);  //验证shellcode是否调用成功
	if(flag==88)
	{
		printf("Ring0ShellCode执行成功\n");
	}
如果Ring0Shellcode执行成功,应该能看到 Ring0ShellCode执行成功 输出。好,下面我们试一下,首先把exploitme.sys加载到系统(请用虚拟机)


2.png


然后把Exploit编译出来,放到测试机器上,运行,在我的机器上,结果


3.png


0X04 参考

0day安全:软件漏洞分析技术

暗战亮剑-软件漏洞发掘与安全

内核漏洞的利用与防范

IO_STACK_LOCATION与IRP的一点笔记:http://www.programlife.net/io_stack_location-irp.html