0day安全 | Chapter 23 Fuzz驱动程序

启程

本章就比较有趣了,利用Fuzz找到了反病毒软件——当年我用过的超级巡警——的Bug。

内核Fuzz思路

Fuzzing嘛,我们必须清楚入口在哪。常见的切入点如下:

  • 内核API:Ring3调用,Ring0执行。如SSDT、Shadow SSDT
  • Hook-API:即安全软件hook过的内核API
  • 网络协议:有些协议的处理是在System进程中,那么就可以考虑构造畸形数据包来Fuzz
  • IoControl:这个是被挖漏洞数最多的。作为Ring0/Ring3交互的重要方式,它很可能出现问题

参考DeviceIoControl,我们可以通过构造畸形参数来Fuzz驱动程序:

BOOL WINAPI DeviceIoControl(
  _In_        HANDLE       hDevice, // 设备句柄
  _In_        DWORD        dwIoControlCode, // IO控制号
  _In_opt_    LPVOID       lpInBuffer, // 输入缓冲区指针
  _In_        DWORD        nInBufferSize,
  _Out_opt_   LPVOID       lpOutBuffer, // 输出缓冲区指针
  _In_        DWORD        nOutBufferSize,
  _Out_opt_   LPDWORD      lpBytesReturned,
  _Inout_opt_ LPOVERLAPPED lpOverlapped // 异步调用时指向的OVERLAPPED指针

方法有两种:

  • IoControl Man-in-the-Middle Fuzz

也就是在内核hook掉NtDeviceIoControlFile函数,检查IoControl对象,当发现是我们要Fuzz的对象时,获取其参数,然后按照Fuzz策略修改其参数,再将篡改后的数据传递给原始NtDeviceIoControlFile函数,观察是否出现内核崩溃或蓝屏(这个思路其实与Hook型内核Rootkit是相同的,比如“Linux Rootkit 实验 0001 基于修改sys_call_table的系统调用挂钩”)。

  • IoControl Driver Fuzz

这种方法指的是对DeviceIoControl每个参数都畸形化,然后组合出不同的参数组(完全给出畸形化参数,而不是去将正常参数部分修改为畸形参数)。相比上面的方法,这种方法测试得更为全面。

内核Fuzz工具

IOCTL Fuzzer

它是一个开源的Windows内核驱动漏洞挖掘命令行工具,可以参考Github: Cr4sh/ioctlfuzzer

它能够根据进程名、驱动名、设备名或IoControlCode去做IRP过滤,提取出符合我们设定要求的IRP,将其参数随机修改来Fuzz(也就是MITM Fuzz的思路)。

作者DIY的IoControl Fuzzer

这是一个带有界面的工具,能够按之前提到的两种方法进行Fuzz。关于这个工具的理念和流程可以参考《0day安全》第583页。

从软件设计角度来讲,它有以下关键概念:

  • Fuzz对象:将要被Fuzz的对象或触发Fuzz的条件
  • Fuzz策略:对参数和数据畸形化的方案
  • Fuzz项:Fuzz对象和Fuzz策略组合起来构成一个Fuzz项

针对IoControl Man-in-the-Middle Fuzz,它的策略如下:

针对IoControl Driver Fuzz,它的策略如下:

该工具实践两种思路的具体流程如下(学习一下,对于开发自己的工具有好处):

(在原书附带光盘资料中,它被放在东辉主动防御里,不太好找。这个软件也蛮有年代感了,膜拜)

实战:超级巡警ASTDriver.sys本地提权漏洞

接下来,我们利用上面的Fuzz工具来实战一下。

我没记错,果然界面很炫酷。参考腾讯云鼎实验室掌门人 这个 Killer 不太冷,Killer前辈已经在腾讯了。

该漏洞存在于超级巡警ASTDriver.sys驱动中。

我们先把核心转储打开:

然后打开超级巡警,接着在Fuzzer中选择超级巡警进程,点击左上方的MITM Fuzz。我们只对输入数据头64字节进行畸形化,点击确定:

在超级巡警中点击分析选项卡,进入SSDT,找到其中红色的已hook函数:

触发漏洞:在Fuzzer中右侧中部点击开始按钮,然后在上图中红色hook函数上右键,选择恢复选中hook,这时系统应该蓝屏(被安排的明明白白):

重启,用WinDbg加载MEMORY.dmp,结果如下:

FAULTING_MODULE: 804d8000 nt
DEBUG_FLR_IMAGE_TIMESTAMP:  47d4cc0e
EXCEPTION_CODE: (NTSTATUS) 0xc0000005 - "0x%08lx"
FAULTING_IP: 
ASTDriver+169b
f7b9769b c70000000000    mov     dword ptr [eax],0

ASTDriver+0x169b:
f7b9769b c70000000000    mov     dword ptr [eax],0    ds:0023:33b4677b=????????
Resetting default scope

DEFAULT_BUCKET_ID:  DRIVER_FAULT

BUGCHECK_STR:  0x8E

LAST_CONTROL_TRANSFER:  from 8051e8ad to 8053480e

STACK_TEXT:  
WARNING: Stack unwind information not available. Following frames may be wrong.
f73babb4 804e47f7 855fb840 86411558 806f12d0 ASTDriver+0x1184
...

STACK_COMMAND:  kb

FOLLOWUP_IP: 
ASTDriver+169b
f7b9769b c70000000000    mov     dword ptr [eax],0

SYMBOL_NAME:  ASTDriver+169b

MODULE_NAME: ASTDriver

IMAGE_NAME:  ASTDriver.sys

可以看出,问题在ASTDriver+0x169b处。使用IDA Pro加载驱动程序,定位到崩溃点:

对其反编译, 发现一个很值得注意的地方:

int __stdcall sub_11690(PVOID VirtualAddress, int a2)
{
  unsigned int v3; // [esp+0h] [ebp-1Ch]
  int v4; // [esp+4h] [ebp-18h]
  char *v5; // [esp+14h] [ebp-8h]
  _DWORD *P; // [esp+18h] [ebp-4h]

  *(_DWORD *)VirtualAddress = 0;
  // ...
  return 0;
}

上述函数竟然在没有任何判断的情况下直接将第一个参数代表的地址处写入0。我们需要向上追溯到调用它的函数去查看这个参数的意义。通过xref我们找到该函数:

int __stdcall sub_110D0(int a1, PIRP Irp)
{
  struct _IO_STACK_LOCATION *v2; // ST18_4
  int result; // eax
  int v4; // [esp+Ch] [ebp-18h]
  int v5; // [esp+10h] [ebp-14h]
  struct _IRP *v6; // [esp+18h] [ebp-Ch]
  PVOID v7; // [esp+1Ch] [ebp-8h]
  DWORD v8; // [esp+20h] [ebp-4h]

  v2 = Irp->Tail.Overlay.CurrentStackLocation;
  v8 = v2->Parameters.Read.ByteOffset.LowPart;
  v6 = Irp->AssociatedIrp.MasterIrp;
  if ( v2->Parameters.Create.Options == 16 )
  {
    v5 = (int)v6->MdlAddress;
    v7 = *(PVOID *)&v6->Type;
    switch ( v8 )
    {
      case 0x50000404u:
        v4 = sub_112B0(v7, v5, (PVOID)v6->Flags, v6->AssociatedIrp.IrpCount);
        break;
      case 0x50000408u:
        v4 = sub_11690(v7, v5); // here!
        break;
      case 0x5000040Cu:
        v4 = sub_11810(v7, v5);
        break;
    }
    IofCompleteRequest(Irp, 0);
    result = v4;
  }
  else
  {
    IofCompleteRequest(Irp, 0);
    result = -1073741811;
  }
  return result;
}

结合我们在“0day安全 Chapter 21 探索ring0”对派遣函数的了解,基本可以确定这个函数就是驱动派遣函数,而上面代码中的v8其实就是IoControlCode。当IoControlCode0x50000408u时,sub_11690(v7, v5)将被执行。

第一个参数v7*(PVOID *)&(Irp->AssociatedIrp.MasterIrp)->Type,这里我没有弄懂它到底是什么。作者说是用户输入缓冲区的第一个DWORD;且v5是用户输入缓冲区的第二个DWORD。

这样一来,一切都清楚了。这是一个向任意地址写入0的漏洞。结合“0day安全 Chapter 22 内核漏洞利用技术”的实验,很明显我们可以利用这个驱动程序去实现提权。

总结

k0shl师傅开发了基于IOCTLBF框架编写的驱动漏洞挖掘工具KDRIVER FUZZER,可以学习一下。

原书本章后面还有东方微点和瑞星的两个漏洞,但是时过境迁,我找不到它们的老版本程序了。总的来说,这一章还挺有趣,只是从第21章开始,难度陡然上升。实事求是地讲,我目前还不能消化这些内容。还是先实践一遍,再慢慢练内功。

Per Aspera Ad Astra