本帖最后由 Netfairy 于 2016-1-3 15:33 编辑
0x00 前言 传统的Ring3层漏洞由于多种保护技术的结合使得利用越来越困难,这时候,攻击者把目光转到内核也就顺理成章了。内核代码运行在Ring0,拥有最高的特权级,可以执行所有的指令,包括特权指令。所以,一旦内核出现漏洞,危害是极其严重的。一旦攻击者成功利用内核漏洞执行了shellcode,因为这些代码运行在Ring0,所以攻击者可以做任何事。内核漏洞一般也叫驱动漏洞,驱动一般以.sys为后缀,因为我们的机器包含了各种各样的外设,而这些外设和机器的通信是通过驱动来交互的,还有杀毒软件等一些程序也自带了驱动。本文我们把目光聚焦在驱动上,分析驱动漏洞的表现形式及利用技术。
0x01 关于驱动程序 驱动程序一般指的是设备驱动程序(Device Driver),是一种可以使计算机和设备通信的特殊程序。相当于硬件的接口,操作系统只有通过这个接口,才能控制硬件设备的工作,假如某设备的驱动程序未能正确安装,便不能正常工作。下面来介绍驱动程序的框架,学习一门语言,我看到的第一个例子总是 " Hello world ",因此,我还还是以hello world来介绍驱动编程,但是注意,本文不是讲如何编写驱动程序,如果你想了解更多关于驱动编程,也叫内核编程,我推荐:《天书夜读》。现在,让我们开始看第一个驱动程序吧: [Asm] 纯文本查看 复制代码 /********************************************************************
时间: 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 = DrvDispatch;
}
//返回成功结果
return STATUS_SUCCESS;
}
程序很简单:首先看
[Asm] 纯文本查看 复制代码 ntStatus = IoCreateDevice( driverObject,0,&devName,FILE_DEVICE_UNKNOWN,0, TRUE,&p_DeviceObject ); 驱动本质是用来完成一定任务的,那么它必然要能和Ring3进行交互,所以在驱动中调用此函数来创建设备对象,标示这个驱动本身。
下面接着看 [Asm] 纯文本查看 复制代码 ntStatus = IoCreateSymbolicLink( &symLinkName,&devName )
IoCreateSymbolicLink函数创建一个符号链接,以后跟驱动进行交互就使用这个符号链接名HelloWorld。驱动处理程序发来的消息,是通过
[Asm] 纯文本查看 复制代码 driverObject->MajorFunction = DrvDispatch; 派遣的,有点类似WIN32编程的消息派遣,由于这个程序简单,我把全部的消息都派遣给DrvDispatch这个函数处理。更多请参考http://www.programlife.net/io_stack_location-irp.html。
这个驱动加载后会输出 HelloWorld,请用Dbgview.exe观察输出而不是控制台。
0x02 内核漏洞介绍 关于内核漏洞的分类我没有找到权威的说法,所以本文也不打算介绍内核漏洞的类型。我只讲很常见一种,也是下来我要演示的:任意地址写任意数据。这个漏洞允许攻击者在任意地址(包括内核地址)写任意数据,这是很严重的,想想假如我们在驱动程序中把某个地址覆写为我们shellcode的地址,然后想办法跳到这个地址执行,由于驱动运行在Ring0....为了演 示内核漏洞,我决定用0day安全:软件漏洞分析技术里面的一个例子。原代码如下: [Asm] 纯文本查看 复制代码 /********************************************************************[/i]
[i] created: 2010/12/06[/i]
[i] filename: D:\0day\ExploitMe\exploitme.c[/i]
[i] author: shineast[/i]
[i] purpose: Exploit me driver demo [/i]
[i]*********************************************************************/[/i]
[i]#include <ntddk.h>[/i]
[i]#define DEVICE_NAME L"\\Device\\ExploitMe"[/i]
[i]#define DEVICE_LINK L"\\DosDevices\\DRIECTX1"[/i]
[i]#define FILE_DEVICE_EXPLOIT_ME 0x00008888[/i]
[i]#define IOCTL_EXPLOIT_ME (ULONG)CTL_CODE(FILE_DEVICE_EXPLOIT_ME,0x800,METHOD_NEITHER,FILE_WRITE_ACCESS)[/i]
[i]//创建的设备对象指针[/i]
[i]PDEVICE_OBJECT g_DeviceObject;[/i]
[i]/**********************************************************************[/i]
[i] 驱动派遣例程函数[/i]
[i] 输入:驱动对象的指针,Irp指针[/i]
[i] 输出:NTSTATUS类型的结果[/i]
[i]**********************************************************************/[/i]
[i]NTSTATUS DrvDispatch(IN PDEVICE_OBJECT driverObject,IN PIRP pIrp)[/i]
[i]{ [/i]
[i] PIO_STACK_LOCATION pIrpStack;//当前的pIrp栈[/i]
[i] PVOID Type3InputBuffer;//用户态输入地址[/i]
[i] PVOID UserBuffer;//用户态输出地址 [/i]
[i] ULONG inputBufferLength;//输入缓冲区的大小[/i]
[i] ULONG outputBufferLength;//输出缓冲区的大小 [/i]
[i] ULONG ioControlCode;//DeviceIoControl的控制号[/i]
[i] PIO_STATUS_BLOCK IoStatus;//pIrp的IO状态指针[/i]
[i] NTSTATUS ntStatus=STATUS_SUCCESS;//函数返回值 [/i]
[i] //获取数据[/i]
[i] pIrpStack = IoGetCurrentIrpStackLocation(pIrp);[/i]
[i] Type3InputBuffer = pIrpStack->Parameters.DeviceIoControl.Type3InputBuffer;[/i]
[i] UserBuffer = pIrp->UserBuffer;[/i]
[i] inputBufferLength = pIrpStack->Parameters.DeviceIoControl.InputBufferLength; [/i]
[i] outputBufferLength = pIrpStack->Parameters.DeviceIoControl.OutputBufferLength; [/i]
[i] ioControlCode = pIrpStack->Parameters.DeviceIoControl.IoControlCode;[/i]
[i] IoStatus=&pIrp->IoStatus;[/i]
[i] IoStatus->Status = STATUS_SUCCESS;// Assume success[/i]
[i] IoStatus->Information = 0;// Assume nothing returned[/i]
[i] //根据 ioControlCode 完成对应的任务[/i]
[i] switch(ioControlCode)[/i]
[i] {[/i]
[i] case IOCTL_EXPLOIT_ME: [/i]
[i] if ( inputBufferLength >= 4 && outputBufferLength >= 4 )[/i]
[i] {[/i]
[i] *(ULONG *)UserBuffer = *(ULONG *)Type3InputBuffer;[/i]
[i] IoStatus->Information = sizeof(ULONG);[/i]
[i] }[/i]
[i] break;[/i]
[i] } [/i]
[i] //返回[/i]
[i] IoStatus->Status = ntStatus; [/i]
[i] IoCompleteRequest(pIrp,IO_NO_INCREMENT);[/i]
[i] return ntStatus;[/i]
[i]}[/i]
[i]/**********************************************************************[/i]
[i] 驱动卸载函数[/i]
[i] 输入:驱动对象的指针[/i]
[i] 输出:无[/i]
[i]**********************************************************************/[/i]
[i]VOID DriverUnload( IN PDRIVER_OBJECT driverObject )[/i]
[i]{ [/i]
[i] UNICODE_STRING symLinkName; [/i]
[i] KdPrint(("DriverUnload: 88!\n")); [/i]
[i] RtlInitUnicodeString(&symLinkName,DEVICE_LINK);[/i]
[i] IoDeleteSymbolicLink(&symLinkName);[/i]
[i] IoDeleteDevice( g_DeviceObject ); [/i]
[i]} [/i]
[i]/*********************************************************************[/i]
[i] 驱动入口函数(相当于main函数)[/i]
[i] 输入:驱动对象的指针,服务程序对应的注册表路径[/i]
[i] 输出:NTSTATUS类型的结果[/i]
[i]**********************************************************************/[/i]
[i]NTSTATUS DriverEntry( IN PDRIVER_OBJECT driverObject, IN PUNICODE_STRING registryPath )[/i]
[i]{ [/i]
[i] NTSTATUS ntStatus;[/i]
[i] UNICODE_STRING devName;[/i]
[i] UNICODE_STRING symLinkName;[/i]
[i] int i=0; [/i]
[i] //打印一句调试信息[/i]
[i] KdPrint(("DriverEntry: Exploit me driver demo!\n"));[/i]
[i] //创建设备 [/i]
[i] RtlInitUnicodeString(&devName,DEVICE_NAME);[/i]
[i] ntStatus = IoCreateDevice( driverObject,[/i]
[i] 0,[/i]
[i] &devName,[/i]
[i] FILE_DEVICE_UNKNOWN,[/i]
[i] 0, TRUE,[/i]
[i] &g_DeviceObject );[/i]
[i] if (!NT_SUCCESS(ntStatus))[/i]
[i] {[/i]
[i] return ntStatus; [/i]
[i] }[/i]
[i] //创建符号链接 [/i]
[i] RtlInitUnicodeString(&symLinkName,DEVICE_LINK);[/i]
[i] ntStatus = IoCreateSymbolicLink( &symLinkName,&devName );[/i]
[i] if (!NT_SUCCESS(ntStatus)) [/i]
[i] {[/i]
[i] IoDeleteDevice( g_DeviceObject );[/i]
[i] return ntStatus;[/i]
[i] }[/i]
[i] //设置该驱动对象的卸载函数[/i]
[i] driverObject->DriverUnload = DriverUnload; [/i]
[i] //设置该驱动对象的派遣例程函数[/i]
[i] for (i = 0; i < IRP_MJ_MAXIMUM_FUNCTION; i++)[/i]
[i] {[/i]
[i] driverObject->MajorFunction = DrvDispatch;[/i]
[i] }[/i]
[i] //返回成功结果[/i]
[i] return STATUS_SUCCESS;[/i]
[i]}
除去注释信息,实际上也没有多少代码。这个驱动存在任意地址写任意数据漏洞。我们看到驱动首先创建了ExploitMe这个设备,然后把所有的信息都交给DrvDispatch函数处理。在DrvDispatch内部
[Asm] 纯文本查看 复制代码 Type3InputBuffer = pIrpStack->Parameters.DeviceIoControl.Type3InputBuffer;
获取用户态输入缓冲区的地址,用户态程序传给驱动的数据就保存在这个地址。
[Asm] 纯文本查看 复制代码 outputBufferLength = pIrpStack->Parameters.DeviceIoControl.OutputBufferLength;
获取用户态输出缓冲区地址,驱动向这个地址填充数据返回给用户态程序,完成交互。ioControlCode = pIrpStack->Parameters.DeviceIoControl.IoControlCode;IoControlCode是用户态传给驱动去执行某一子分支的代码,所以接下来我们看到
[Asm] 纯文本查看 复制代码 switch(ioControlCode)对IoControlCode
进行识别,然后执行相应的分支,因此,用户态程序传过来的IoControlCode和驱动定义的IoControlCode必须对应,否则驱动无法识别。那么,驱动定义的IoControlCode都有哪些呢?在这个例子中,我们只看到了 IOCTL_EXPLOIT_ME。关于它的十六进制数值计算,请看
[Asm] 纯文本查看 复制代码 #define IOCTL_EXPLOIT_ME (ULONG)CTL_CODE(FILE_DEVICE_EXPLOIT_ME,0x800,METHOD_NEITHER,FILE_WRITE_ACCESS)
并且用我写的一个小工具计算
下面看驱动如何处理8888a003这个控制代码
[Asm] 纯文本查看 复制代码 if ( inputBufferLength >= 4 && outputBufferLength >= 4 ) { *(ULONG *)UserBuffer = *(ULONG *)Type3InputBuffer; IoStatus->Information = sizeof(ULONG); } break;
先判断输入输出缓冲区长度,如果长度都大于4,那么就把输入缓冲区的前四字节内容赋值给输出缓冲区,由于驱动对输入输出缓冲区没有任何检查,且输入输出缓冲区是是我们用户层程序传给驱动的,那么我们可以传递任何值。假设我们输入缓冲区前四个字节是shellcode的地址,输出缓冲区是指向函数A的地址。当我们给驱动发送8888a003这个控制代码发送数据时,原本保存着函数A地址,现在被shellcode地址覆盖了,此时我们再调用函数A,我们的shellcode就被激活。一个任意地址写任意数据漏洞发生了。
0x03 实践ExploitMe漏洞利用 关于这个驱动的利用书上已经讲的很清楚了,需要说明的是,在我的机器上,这个Exploit并没有执行成功(如果你幸运的话,或许可以)为了证明我们的shellcode确实执行了。我在Exploit开头定义了一个全局变量
[Asm] 纯文本查看 复制代码 //验证shellcode执行成功的标志int flag=0;在Ring0Shellcode把flag修改为 88,最后在程序末尾打印flag
printf("flag=%d\n",flag); //验证shellcode是否调用成功 if(flag==88) { printf("Ring0ShellCode执行成功\n"); }如果Ring0Shellcode执行成功,应该能看到 Ring0ShellCode执行成功 输出。
好,下面我们试一下,首先把exploitme.sys加载到系统(请用虚拟机)
然后把Exploit编译出来,放到测试机器上,运行,在我的机器上,结果
0x04 参考 0day安全:软件漏洞分析技术 暗战亮剑-软件漏洞发掘与安全 内核漏洞的利用与防范 IO_STACK_LOCATION与IRP的一点笔记:http://www.programlife.net/io_stack_location-irp.html
原文:http://www.netfairy.net/?post=171
|