【漏洞分析】CVE-2017-6178:从补丁对比到Exploit

阅读量306990

|

发布时间 : 2017-06-02 10:44:46

https://p0.ssl.qhimg.com/t01659611ec96094345.jpg

作者:k0shl

预估稿费:700RMB

投稿方式:发送邮件至linwei#360.cn,或登陆网页版在线投稿


0x00 关于USBPcap和CVE-2017-6178

前段时间在EDB闲逛,看到了一个驱动的内核漏洞CVE-2017-6178,像我这样在学习Kernel PWN的新手自然是不会错过的:),经过调试分析之后感觉学到了一些东西,于是来和大家一起分享一下。

USBPcap是一个USB数据包捕获的工具,可以配合Wireshark抓取USB设备的数据包,在安装完USBPcap之后会同时安装一个驱动,这个驱动里存在一个指针未初始化的漏洞,并可以通过这个漏洞完成对系统的提权,也就是CVE-2017-6178。

http://p6.qhimg.com/t01312878bf07d31f02.png

事实上,EDB上已经有一个Exploit的代码,但是经过我的分析调试,发现这个代码是存在问题的,也就是说,会触发BSOD,但并不会完成提权,这个提权代码在一直执行到替换Token位置都是没有问题的,但在进行Token替换前后堆栈被破坏,需要在提权结束返回时进行一个小Patch,才能最后完成提权。

那么,当我们没有PoC或者Exp的时候,该如何来复现漏洞并完成攻击呢?首先我们需要复现,以及完成PoC的编写,这个过程往往需要Fuzz或者补丁对比等等的方法来完成,随后就是我们的Kernel PWN了。

今天我就来和大家一起分享一下从补丁对比到Exploit的过程,首先我来讲解一下从补丁对比到PoC复现的过程,随后我们来分析一下CVE-2017-6178这个指针未初始化漏洞形成的原因,随后我们一起来进行Kernel PWN并获得system权限,最后,有一点关于这个补丁绕过的小脑洞(尽管应该是不能绕过的,但多思考总是好的:)),我是努力中的新手,文中有失误的地方,欢迎各位师傅批评指正,感谢阅读!

利用环境是Windows 7 x86 sp1

http://p2.qhimg.com/t01ea1992592aad3d76.jpg

原Exp的EDB地址:https://www.exploit-db.com/exploits/41542/ 

USBPcap官网地址:http://desowin.org/usbpcap/ 


0x01 从补丁对比到PoC

1、补丁对比

首先,我们需要通过最新版补丁的对比来分析一下这个漏洞可能存在的点,官网最新版的USBPcap是在2017年4月更新的1.2.0版本,下载下来后进行安装,安装结束后,可以看到USBPcap的驱动程序USBPcap.sys,我们获取一个老版本的USBPcap 1.0.0版本,进行bindiff,可以看到改动较大的函数。

http://p1.qhimg.com/t01670f801d76043f58.png

可以看到,下面绿色部分是包含有变化的函数,通过对每个函数分析,发现其中多数函数存在一个共通点,就是增加了一条判断。

http://p1.qhimg.com/t013bb6e3abe1aee861.png

这个判断是将edi+8中存放的值和ecx作比较,而ecx经过xor运算已经置0,也就是将edi+8存放的值和0作比较,我们知道CVE-2017-6178是一个未初始化指针引发的漏洞,那么这个很有可能就是对未初始化的情况做判断,来看一下补丁前的情况:

http://p8.qhimg.com/t017f1f9f90abed765e.png

补丁后:

http://p4.qhimg.com/t019667d13a692781df.png

因此,增加了这个判断的函数,我们考虑是可能利用的攻击面,接下来,我们来构造能触发这个漏洞的PoC。

2、CTL_CODE和Dispatch Routine

对第三方驱动的攻击和对Windows kernel的攻击有所不同,对驱动的攻击需要了解一些比较关键的过程,一个就是和驱动的交互过程,和IRP数据结构,其实这些内容网上有更多非常详细的内容,我对驱动也不是特别了解,但在对这个漏洞的调试和逆向的过程中也学到了不少东西,这里我分享一下和此漏洞有关的信息,其他和驱动相关的知识可以到网上搜索。

在驱动攻防中,很重要的就是和驱动交互的过程,其中要调用CreateFile来获取和设备交互的句柄,随后通过DeviceIOControl来完成和驱动的通信交互,同样这里复现这个PoC也有两种方法,一种比较方便是直接Fuzz,这里我分享一个比较好的驱动Fuzz开源工具DIBF-Fuzz:https://github.com/iSECPartners/DIBF 

想和驱动交互,需要知道驱动设备的名称,以及对应的CTL_CODE,获取驱动设备的名称有很多种方法,比如直接逆向分析软件,比如注册表,比如直接运行驱动对应程序:

http://p5.qhimg.com/t0133ca80c067cae0f7.png

当然,也有一些工具可以辅助我们,比如Device Tree和WinOBJ等等。

http://p3.qhimg.com/t014107444ba361c2d9.png

这里直接打开程序可以看到有两个设备名称\.USBPcap1和\.USBPcap2,事实上这个两个驱动最后都会和USBPcap.sys交互,接下来我们需要获取CTL_CODE,CTL_CODE是DeviceIOControl函数中非常关键的参数,它并不单纯的是一个十六进制数,它的结构是这样的:

http://p6.qhimg.com/t01ec4f29290f32cba6.png

关于CTL_CODE各个比特位内容的含义网上有很多说明,这里我不进行赘述,其中比较关键的是Function,它决定了进入IRP分发的派遣函数中负责驱动具体功能函数后要执行的具体函数,当然,像DIBF这种Fuzzer会直接爆破CTL_CODE,比如从0x220000开始逐步加1,如果命中,则会执行具体函数,否则会返回ERROR NTSTATUS。

在这个漏洞中,我们可以在我们认为可能存在漏洞函数下断点,然后去暴力跑CTL_CODE直到命中为止,但我们也可以逆向DeivceIOControl来看看其中的秘密,这样需要来简单分析一下DeviceIOControl函数到底做了什么。

关于DeviceIOControl的逆向分析比较长,这里我不详细介绍逆向过程只说明其中的关键点,首先我们来看一下DeviceIOControl的函数调用关系。DeviceIOControl刚开始是在用户态,随后会通过KiFastSyscall进入内核态。

http://p1.qhimg.com/t015821dc67a395a20b.png

在到达IofCallDriver之后,我们看到在IofCallDriver中会引用一个地址,这个地址保存着一个Dispatch Routine,其中存放着IRP关于这个驱动的派遣函数地址。

http://p7.qhimg.com/t01d734dba2d1d6a62f.png

可以看到,在Dispatch Routine中存放着我们比较关心可能存在漏洞的函数地址0x91cdf85a,这个函数是一个IRP关于USBPcap驱动的派遣函数,并非是我们USBPcap.sys的具体驱动功能函数,也就是说,我们CTL_CODE中的Function部分的值并不重要,我们只需要想办法能令call [eax+ecx*4+38]的值指向0x91cdf85a就可以了。

仔细分析IofCallDriver+0x5f的上下文,发现eax存放的是Dispatch Routine指针,也就是说ecx决定了是否指向0x91cdea28,所以我们需要知道ecx寄存器存放的是什么。

经过我的分析,发现调整CTL_CODE,ecx的值总是0xe,经过计算之后,总是指向0x91cdea28,这个地址是USBPcap.sys中实现具体驱动功能的主函数,于是我向外层逆向,发现了ecx到底从哪里来的。这里我省略了逆向过程,来正向看一下ecx的整个赋值过程。

首先函数在IoXxxControlFile实现IRP结构的封装和分发。

PDEVICE_OBJECT __stdcall IopXxxControlFile(HANDLE Handle, HANDLE a2, int a3, int a4, int a5, int a6, int a7, SIZE_T NumberOfBytes, PVOID Address, SIZE_T Length, char a11)
{
……
  result = (PDEVICE_OBJECT)ObReferenceObjectByHandle(
                             Handle,
                             0,
                             IoFileObjectType,
                             AccessMode[0],
                             &Object,           // 通过handle值得到KTHREAD OBJECT
                             &HandleInformation);
  v14 = Object;                                 // 传给V14
  ……
      if ( *((_DWORD *)v14 + 11) & 0x800 )
      v18 = (PDEVICE_OBJECT)IoGetAttachedDevice(*((_DWORD *)v14 + 1));// 这里v18得到device object
    else
      v18 = IoGetRelatedDeviceObject((PFILE_OBJECT)v14);
    Handlea = v18;                              // 将device object交给handlea
    v26 = IoAllocateIrp(Handlea->StackSize, v42[0] == 0);// 分配IRP结构
    v26->Tail.Overlay.OriginalFileObject = (PFILE_OBJECT)v14;
    v26->Tail.Overlay.Thread = (PETHREAD)v38;
    v26->Tail.Overlay.AuxiliaryBuffer = 0;
    v26->RequestorMode = AccessMode[0];
    v26->PendingReturned = 0;
    v26->Cancel = 0;
    v26->CancelRoutine = 0;
    v26->UserEvent = Event;
    v26->UserIosb = (PIO_STATUS_BLOCK)a5;
    v26->Overlay.AllocationSize.LowPart = a3;
    v26->Overlay.AllocationSize.HighPart = a4;
    v28 = v26->Tail.Overlay.PacketType - 36;    // key!
    *(_DWORD *)v28 = (a11 != 0) + 13;//这里给后面ecx赋值
……

可以看到,这里会对Tail.Overlay.PacketType-36赋值,这个地方很关键,随后向内层跟踪到IofCallDriver函数中。

  NTSTATUS __fastcall IofCallDriver(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{ 
……
       if ( --Irp->CurrentLocation <= 0 )
      KeBugCheckEx(0x35u, (ULONG_PTR)Irp, 0, 0, 0);
    v4 = Irp->Tail.Overlay.PacketType - 36;//获取Irp结构
    Irp->Tail.Overlay.PacketType = v4;
    v5 = *(_BYTE *)v4;//将其中存放的值交给v5
    *(_DWORD *)(v4 + 20) = v2;
    if ( v5 != 22 || (v6 = *(_BYTE *)(v4 + 1), v6 != 2) && v6 != 3 )
      result = v2->DriverObject->MajorFunction[v5](v2, Irp);//在MajorFunction表中找到偏移并调用函数
}

这里v5的值就是v4中存放的值,也就是Tail.Overlay.PacketType-36地址位置存放的值,这是一个IRP中Tail结构里的Stack_Location,来看一下这个结构。

http://p3.qhimg.com/t01aa44ab0fa37c4972.png

而关于Stack Location的描述可以在MSDN中找到相关的信息,其中有一条就是Stack_Location中包含了IRP_MJ_XXXX,他们最终会指向MajorFunction中关于此驱动对应IRP派遣函数,这也就是ecx寄存器到底是什么。我们关注一下刚才我发的关于IoXxxControlFile函数中的,关于Stack_Location的赋值过程*(_DWORD *)v28 = (a11 != 0) + 13;,这个赋值过程的关键是a11,而13的值就是0xD,根据这行代码的逻辑,当a11不为0时,值为1,那么ecx的值最后就为0xe,但若a11的值为0,那么值为0,则ecx的值就为0xD,观察一下之前我发的Dispatch Routine,USBPcap.sys主函数偏移差4字节位置存放的就是我们要跟踪的关键函数0x91cdf85a。

那么a11就是决定我们能否到达可能存在漏洞函数的关键,a11是IoXxxControlFile的传入参数,来看一下之前函数流程,有两个外层调用。

NTSTATUS __stdcall NtDeviceIoControlFile(HANDLE FileHandle, HANDLE Event, PIO_APC_ROUTINE ApcRoutine, PVOID ApcContext, PIO_STATUS_BLOCK IoStatusBlock, ULONG IoControlCode, PVOID InputBuffer, ULONG InputBufferLength, PVOID OutputBuffer, ULONG OutputBufferLength)
{
  return (NTSTATUS)IopXxxControlFile(
                     FileHandle,
                     Event,
                     (int)ApcRoutine,
                     (int)ApcContext,
                     (int)IoStatusBlock,
                     IoControlCode,
                     (int)InputBuffer,
                     InputBufferLength,
                     OutputBuffer,
                     OutputBufferLength,
                     1);
}
NTSTATUS __stdcall NtFsControlFile(HANDLE FileHandle, HANDLE Event, PIO_APC_ROUTINE ApcRoutine, PVOID ApcContext, PIO_STATUS_BLOCK IoStatusBlock, ULONG FsControlCode, PVOID InputBuffer, ULONG InputBufferLength, PVOID OutputBuffer, ULONG OutputBufferLength)
{
  return (NTSTATUS)IopXxxControlFile(
                     FileHandle,
                     Event,
                     (int)ApcRoutine,
                     (int)ApcContext,
                     (int)IoStatusBlock,
                     FsControlCode,
                     (int)InputBuffer,
                     InputBufferLength,
                     OutputBuffer,
                     OutputBufferLength,
                     0);
}

当调用NtDeviceIoControlFile的时候,a11的值为1,那么最后ecx索引值为1,结果是0xe,而当如果调用NtFsControlFile的时候,a11值为0,我们有可能到达漏洞函数,这样我们来看一下如何能令程序进入NtFsControlFile函数。

BOOL __stdcall DeviceIoControl(HANDLE hDevice, DWORD dwIoControlCode, LPVOID lpInBuffer, DWORD nInBufferSize, LPVOID lpOutBuffer, DWORD nOutBufferSize, LPDWORD lpBytesReturned, LPOVERLAPPED lpOverlapped)
{
  HANDLE v8; // eax@2
  NTSTATUS v9; // eax@3
  NTSTATUS v11; // eax@13
  struct _IO_STATUS_BLOCK IoStatusBlock; // [sp+10h] [bp-20h]@13
  CPPEH_RECORD ms_exc; // [sp+18h] [bp-18h]@6
  if ( !lpOverlapped )
  {
    if ( (dwIoControlCode & 0xFFFF0000) != 589824 )//将CTL CODE和0x90000做比较,如果相同,则执行NtFsControlFile,不同则执行NtDeviceIoControlFile
      v11 = NtDeviceIoControlFile(
              hDevice,
              0,
              0,
              0,
              &IoStatusBlock,
              dwIoControlCode,
              lpInBuffer,
              nInBufferSize,
              lpOutBuffer,
              nOutBufferSize);
    else
      v11 = NtFsControlFile(
              hDevice,
              0,
              0,
              0,
              &IoStatusBlock,
              dwIoControlCode,
              lpInBuffer,
              nInBufferSize,
              lpOutBuffer,
              nOutBufferSize);

回溯到DeviceIoControl之后,我发现这里将dwIoControlCode和0XFFFF0000做了与运算,也就是保留高4位,然后和0x90000作比较,如果相等,则会进入NtFsControlFile,原来还是CTL_CODE决定了进入漏洞函数,但不是Function部分,而是DeviceType部分,来看一下我们常用对第三方驱动DeviceType的CTL_CODE值0x220000的定义,以及我们这次用到的0x90000的定义。

#define FILE_DEVICE_FILE_SYSTEM         0x00000009
#define FILE_DEVICE_UNKNOWN             0x00000022

这样,我们利用对驱动通信过程的回溯分析,找到了能命中可能存在漏洞的方法,首先利用CreateFile创建和\.USBPcap1句柄,随后利用DeviceIOControl和设备通信,传递的CTL_CODE为0x90000。

文末我上传了关于这个漏洞的PoC,exp的话由于网络安全法刚刚颁布,关于exp的技术分享还不明晰暂时不公开,等确认是合法的技术分享后再公开。

随后我们引发了BSOD。

kd> r
eax=8700e8a8 ebx=868dd638 ecx=0000000d edx=8700e838 esi=00000000 edi=868dd628
eip=83e54587 esp=a1340ae0 ebp=a1340ae8 iopl=0         nv up ei ng nz na po cy
cs=0008  ss=0010  ds=0023  es=0023  fs=0030  gs=0000             efl=00010283
nt!IofCallDriver+0x57:
83e54587 8b4608          mov     eax,dword ptr [esi+8] ds:0023:00000008=????????

BOOM!接下来我们进行漏洞分析。


0x02 CVE-2017-6178漏洞分析

其实这个漏洞形成原因非常简单,是一个简单的DeviceObject未初始化引发的漏洞,通过kb可以回溯堆栈调用。

kd> kb
 # ChildEBP RetAddr  Args to Child              
00 a1340ae8 91cdf8a6 857e9948 868dd570 00000000 nt!IofCallDriver+0x57
WARNING: Stack unwind information not available. Following frames may be wrong.
01 a1340afc 83e54593 00000000 8700e838 8700e838 USBPcap+0x18a6
02 a1340b14 8404899f 857e9948 8700e838 8700e8a8 nt!IofCallDriver+0x63
03 a1340b34 8404bb71 868dd570 857e9948 00000001 nt!IopSynchronousServiceTail+0x1f8
04 a1340bd0 840746cc 868dd570 8700e838 00000000 nt!IopXxxControlFile+0x6aa
05 a1340c04 83e5b1ea 0000001c 00000000 00000000 nt!NtFsControlFile+0x2a
06 a1340c04 76df70b4 0000001c 00000000 00000000 nt!KiFastCallEntry+0x12a
07 002cfb08 76df5a14 751b7414 0000001c 00000000 ntdll!KiFastSystemCallRet
08 002cfb0c 751b7414 0000001c 00000000 00000000 ntdll!ZwFsControlFile+0xc

在USBPcap.sys中调用了IofCallDriver而引发了读取了0x0这个无效地址的值,引发了BSOD,这里esi寄存器的值为0x0,来看一下这个值由何而来。

NTSTATUS __fastcall IofCallDriver(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
  v2 = DeviceObject;
  if ( pIofCallDriver )
  {
  }
  else
  {
    ……
      result = v2->DriverObject->MajorFunction[v5](v2, Irp);//漏洞位置,esi是v2的值,v2的值是DeviceObject,也就是说这是一个DeviceObject未初始化的原因
  }
  return result;
}

这里v2的值也就是esi寄存器的值是DeviceObject,而这个值是0x0,证明未给DeviceObject赋初值,来往外层回溯一下USBPcap.sys对应函数中。

  if ( v3 >= 0 )
  {
    v5 = *(struct _DEVICE_OBJECT **)(v2 + 8);
    ++*((_BYTE *)Tag + 35);
    *((_DWORD *)Tag + 24) += 36;
    v6 = (struct _IO_REMOVE_LOCK *)(v2 + 16);
    v7 = IofCallDriver(v5, (PIRP)Tag);
    IoReleaseRemoveLockEx(v6, Tag, 0x18u);
    result = v7;

在USBPcap.sys函数中,v5会获取v2+8的值,这里是一个DEVICE OBJECT结构体,然后直接调用IofCallDriver,并将v5传入,这里没有对v5的值是否赋初值进行检查,而直接调用了IofCallDriver引发了漏洞,来看一下补丁后这里的结果。

    if ( v4 >= 0 )
  {
     v6 = *(struct _DEVICE_OBJECT **)(v2 + 8);
    if ( v6 )
    {
      ++*((_BYTE *)Tag + 35);
      *((_DWORD *)Tag + 24) += 36;
      v7 = IofCallDriver(v6, (PIRP)Tag);
    }
  }

补丁后,对DEVICE_OBJECT的值进行了判断,若不为0,也就是赋初值了,才会正常调用IofCallDriver函数。完成了PoC,我们来最后完成对这个漏洞的利用。


0x03 PWN!!

其实关于这个漏洞利用非常简单,这个和我之前写过的MS16-034的利用过程很像,地址在:

http://whereisk0shl.top/ssctf_pwn450_windows_kernel_exploitation_writeup.html 

在Win7下没有对零页地址的限制,可以直接在零页分配地址,来构造一个fake device object来进行赋值,同时来看一下漏洞利用前后的。

loc_437587:
mov     eax, [esi+8]
push    edx
movzx   ecx, cl
push    esi
call    dword ptr [eax+ecx*4+38h]

这里esi的值由于未初始化是0x0,在零页申请地址后,我们可以在0x8中构造一个fake address,这里直接是0x0就行,那么eax的值就是0x0,到最后call调用的就是0x0+ecx*4+0x38中存放的值,ecx在这里是定值0xd,那么最后相当于call [6c],我们申请完零页地址后,在6c部署shellcode,就能在Ring0态执行shellcode了。

到这里其实都没有问题,利用也很简单,但事实上在我们使用shellcode之后存在一个堆栈平衡的问题,导致常用的shellcode无法使用,需要进行一个小patch,来看一下这到底是是怎么回事。

首先,我们执行到shellcode末尾的时候,需要返回外层调用。

https://p0.ssl.qhimg.com/t018c0ebef664fee72f.png

可以看到,之前的返回地址时83e54593,这个地址是IofCallDriver的地址,后面的返回地址时USBPcap.sys中的地址91cdf8a6,只有返回到USBPcap.sys中之后,才能正常通过USBPcap.sys中的ret结束和驱动通信。

但是这里,如果从shellcode返回到IofCallDriver后,来看一下上下文。

kd> u 83e54593 l4
nt!IofCallDriver+0x63:
83e54593 5e              pop     esi
83e54594 59              pop     ecx
83e54595 5d              pop     ebp
83e54596 c3              ret

这里是3个pop,然后就ret了,这样到达不了上面我们分析的图中的USBPcap.sys的地址,而是返回到0x0这个地址中,随后会执行报错,因此这里我们需要打一个小补丁,其中一种方法就是在shellcode中,不让shellcode返回到IofCallDriver,而是直接返回到USBPcap.sys中,因此我们在shellcode要返回的时候,调整esp,直接将其指向USBPcap.sys的ret address即可。

kd> u 13e103c l3
013e103c 61              popad
013e103d 83c424          add     esp,24h
013e1040 c3              ret

通过add esp,24h,就可以保持堆栈平衡了。这样,我们利用EPROCESS的shellcode对token进行替换,完成Kernel PWN。

https://p1.ssl.qhimg.com/t0187dfee823315b9db.png


0x04 关于补丁以及不成熟的脑洞

到此我们完成了对CVE-2017-6178从补丁对比,到漏洞分析,其中比较麻烦的过程就是对DeviceIOControl的逆向过程,但也挺有意思了,了解到IRP这个驱动通信极重要结构体的一些功能,当然还存在很多的不足,需要在以后慢慢学习,驱动利用也是内核利用的一部分,也是很重要很有趣的一部分。

当然,经过刚才的分析,我发现在补丁中只是对device object对象是否为0进行了判断,而不是对device object的合法性进行判断,也就是说,当device object为一个任意不为0的值的时候,也能够绕过判断,那就导致如果指向的是一个我们可控的位置,就仍然存在漏洞,但我花了一段时间研究如何修改device object的值,发现好像device object只能在内核层被赋值,有想过hook的想法,但终究没有实现,身边也没有有相关经验的小伙伴一起研究。

所以就算一个不成熟的脑洞,如果有师傅觉得可以做到,欢迎一起交流,最后感谢大家阅读!谢谢大家!

最后放一下我的CVE-2017-6178 POC地址,exp会在之后确认仅用于技术交流不触犯网络安全法后公开:

https://github.com/k0keoyo/try_exploit/tree/master/_cve_2017_6178_poc 

本文由k0shl原创发布

转载,请参考转载声明,注明出处: https://www.anquanke.com/post/id/86203

安全客 - 有思想的安全新媒体

分享到:微信
+12赞
收藏
k0shl
分享到:微信

发表评论

内容需知
合作单位
  • 安全客
  • 安全客
Copyright © 北京奇虎科技有限公司 三六零数字安全科技集团有限公司 安全客 All Rights Reserved 京ICP备08010314号-66