作者:k0shl
预估稿费:700RMB
投稿方式:发送邮件至linwei#360.cn,或登陆网页版在线投稿
0x00 关于USBPcap和CVE-2017-6178
前段时间在EDB闲逛,看到了一个驱动的内核漏洞CVE-2017-6178,像我这样在学习Kernel PWN的新手自然是不会错过的:),经过调试分析之后感觉学到了一些东西,于是来和大家一起分享一下。
USBPcap是一个USB数据包捕获的工具,可以配合Wireshark抓取USB设备的数据包,在安装完USBPcap之后会同时安装一个驱动,这个驱动里存在一个指针未初始化的漏洞,并可以通过这个漏洞完成对系统的提权,也就是CVE-2017-6178。
事实上,EDB上已经有一个Exploit的代码,但是经过我的分析调试,发现这个代码是存在问题的,也就是说,会触发BSOD,但并不会完成提权,这个提权代码在一直执行到替换Token位置都是没有问题的,但在进行Token替换前后堆栈被破坏,需要在提权结束返回时进行一个小Patch,才能最后完成提权。
那么,当我们没有PoC或者Exp的时候,该如何来复现漏洞并完成攻击呢?首先我们需要复现,以及完成PoC的编写,这个过程往往需要Fuzz或者补丁对比等等的方法来完成,随后就是我们的Kernel PWN了。
今天我就来和大家一起分享一下从补丁对比到Exploit的过程,首先我来讲解一下从补丁对比到PoC复现的过程,随后我们来分析一下CVE-2017-6178这个指针未初始化漏洞形成的原因,随后我们一起来进行Kernel PWN并获得system权限,最后,有一点关于这个补丁绕过的小脑洞(尽管应该是不能绕过的,但多思考总是好的:)),我是努力中的新手,文中有失误的地方,欢迎各位师傅批评指正,感谢阅读!
利用环境是Windows 7 x86 sp1
原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,可以看到改动较大的函数。
可以看到,下面绿色部分是包含有变化的函数,通过对每个函数分析,发现其中多数函数存在一个共通点,就是增加了一条判断。
这个判断是将edi+8中存放的值和ecx作比较,而ecx经过xor运算已经置0,也就是将edi+8存放的值和0作比较,我们知道CVE-2017-6178是一个未初始化指针引发的漏洞,那么这个很有可能就是对未初始化的情况做判断,来看一下补丁前的情况:
补丁后:
因此,增加了这个判断的函数,我们考虑是可能利用的攻击面,接下来,我们来构造能触发这个漏洞的PoC。
2、CTL_CODE和Dispatch Routine
对第三方驱动的攻击和对Windows kernel的攻击有所不同,对驱动的攻击需要了解一些比较关键的过程,一个就是和驱动的交互过程,和IRP数据结构,其实这些内容网上有更多非常详细的内容,我对驱动也不是特别了解,但在对这个漏洞的调试和逆向的过程中也学到了不少东西,这里我分享一下和此漏洞有关的信息,其他和驱动相关的知识可以到网上搜索。
在驱动攻防中,很重要的就是和驱动交互的过程,其中要调用CreateFile来获取和设备交互的句柄,随后通过DeviceIOControl来完成和驱动的通信交互,同样这里复现这个PoC也有两种方法,一种比较方便是直接Fuzz,这里我分享一个比较好的驱动Fuzz开源工具DIBF-Fuzz:https://github.com/iSECPartners/DIBF
想和驱动交互,需要知道驱动设备的名称,以及对应的CTL_CODE,获取驱动设备的名称有很多种方法,比如直接逆向分析软件,比如注册表,比如直接运行驱动对应程序:
当然,也有一些工具可以辅助我们,比如Device Tree和WinOBJ等等。
这里直接打开程序可以看到有两个设备名称\.USBPcap1和\.USBPcap2,事实上这个两个驱动最后都会和USBPcap.sys交互,接下来我们需要获取CTL_CODE,CTL_CODE是DeviceIOControl函数中非常关键的参数,它并不单纯的是一个十六进制数,它的结构是这样的:
关于CTL_CODE各个比特位内容的含义网上有很多说明,这里我不进行赘述,其中比较关键的是Function,它决定了进入IRP分发的派遣函数中负责驱动具体功能函数后要执行的具体函数,当然,像DIBF这种Fuzzer会直接爆破CTL_CODE,比如从0x220000开始逐步加1,如果命中,则会执行具体函数,否则会返回ERROR NTSTATUS。
在这个漏洞中,我们可以在我们认为可能存在漏洞函数下断点,然后去暴力跑CTL_CODE直到命中为止,但我们也可以逆向DeivceIOControl来看看其中的秘密,这样需要来简单分析一下DeviceIOControl函数到底做了什么。
关于DeviceIOControl的逆向分析比较长,这里我不详细介绍逆向过程只说明其中的关键点,首先我们来看一下DeviceIOControl的函数调用关系。DeviceIOControl刚开始是在用户态,随后会通过KiFastSyscall进入内核态。
在到达IofCallDriver之后,我们看到在IofCallDriver中会引用一个地址,这个地址保存着一个Dispatch Routine,其中存放着IRP关于这个驱动的派遣函数地址。
可以看到,在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,来看一下这个结构。
而关于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末尾的时候,需要返回外层调用。
可以看到,之前的返回地址时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。
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