Quantcast
Channel: CodeSection,代码区,网络安全 - CodeSec
Viewing all articles
Browse latest Browse all 12749

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

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

2017-06-02 10:44:46

阅读:1547次
点赞(0)
收藏
来源: 安全客





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

作者:k0shl





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

作者:k0shl

预估稿费:700RMB

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


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

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


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

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

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

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

利用环境是windows 7 x86 sp1


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

原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,可以看到改动较大的函数。


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

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


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

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


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

补丁后:


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

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

2、CTL_CODE和Dispatch Routine

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

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

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


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

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


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

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


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

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

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

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


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

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


【漏洞分析】CVE-2017-6178:从补丁对比到Exploit
可以看到,在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__stdcallIopXxxControlFile(HANDLEHandle,HANDLEa2,inta3,inta4,inta5,inta6,inta7,SIZE_TNumberOfBytes,PVOIDAddress,SIZE_TLength,chara11) { …… result=(PDEVICE_OBJECT)ObReferenceObjectByHandle( Handle, 0, IoFileObjectType, AccessMode[0], &Object,//通过handle值得到KTHREADOBJECT &HandleInformation); v14=Object;//传给V14 …… if(*((_DWORD*)v14+11)&0x800) v18=(PDEVICE_OBJECT)IoGetAttachedDevice(*((_DWORD*)v14+1));//这里v18得到deviceobject else v18=IoGetRelatedDeviceObject((PFILE_OBJECT)v14); Handlea=v18;//将deviceobject交给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__fastcallIofCallDriver(PDEVICE_OBJECTDeviceObject,PIRPIrp) { …… 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,来看一下这个结构。

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

而关于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__stdcallNtDeviceIoControlFile(HANDLEFileHandle,HANDLEEvent,PIO_APC_ROUTINEApcRoutine,PVOIDApcContext,PIO_STATUS_BLOCKIoStatusBlock,ULONGIoControlCode,PVOIDInputBuffer,ULONGInputBufferLength,PVOIDOutputBuffer,ULONGOutputBufferLength) { return(NTSTATUS)IopXxxControlFile( FileHandle, Event, (int)ApcRoutine, (int)ApcContext, (int)IoStatusBlock, IoControlCode, (int)InputBuffer, InputBufferLength, OutputBuffer, OutputBufferLength, 1); } NTSTATUS__stdcallNtFsControlFile(HANDLEFileHandle,HANDLEEvent,PIO_APC_ROUTINEApcRoutine,PVOIDApcContext,PIO_STATUS_BLOCKIoStatusBlock,ULONGFsControlCode,PVOIDInputBuffer,ULONGInputBufferLength,PVOIDOutputBuffer,ULONGOutputBufferLength) { 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__stdcallDeviceIoControl(HANDLEhDevice,DWORDdwIoControlCode,LPVOIDlpInBuffer,DWORDnInBufferSize,LPVOIDlpOutBuffer,DWORDnOutBufferSize,LPDWORDlpBytesReturned,LPOVERLAPPEDlpOverlapped) { HANDLEv8;//eax@2 NTSTATUSv9;//eax@3 NTSTATUSv11;//eax@13 struct_IO_STATUS_BLOCKIoStatusBlock;//[sp+10h][bp-20h]@13 CPPEH_RECORDms_exc;//[sp+18h][bp-18h]@6 if(!lpOverlapped) { if((dwIoControlCode&0xFFFF0000)!=589824)//将CTLCODE和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的定义。

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

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

随后我们引发了BSOD。

kd>r eax=8700e8a8ebx=868dd638ecx=0000000dedx=8700e838esi=00000000edi=868dd628 eip=83e54587esp=a1340ae0ebp=a1340ae8iopl=0nvupeingnznapocy cs=0008ss=0010ds=0023es=0023fs=0030gs=0000efl=00010283 nt!IofCallDriver+0x57: 83e545878b4608moveax,dwordptr[esi+8]ds:0023:00000008=????????

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


0x02 CVE-2017-6178漏洞分析

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

kd>kb #ChildEBPRetAddrArgstoChild 00a1340ae891cdf8a6857e9948868dd57000000000nt!IofCallDriver+0x57 WARNING:Stackunwindinformationnotavailable.Followingframesmaybewrong. 01a1340afc83e54593000000008700e8388700e838USBPcap+0x18a6 02a1340b148404899f857e99488700e8388700e8a8nt!IofCallDriver+0x63 03a1340b348404bb71868dd570857e994800000001nt!IopSynchronousServiceTail+0x1f8 04a1340bd0840746cc868dd5708700e83800000000nt!IopXxxControlFile+0x6aa 05a1340c0483e5b1ea0000001c0000000000000000nt!NtFsControlFile+0x2a 06a1340c0476df70b40000001c0000000000000000nt!KiFastCallEntry+0x12a 07002cfb0876df5a14751b74140000001c00000000ntdll!KiFastSystemCallRet 08002cfb0c751b74140000001c0000000000000000ntdll!ZwFsControlFile+0xc

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

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

这里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: moveax,[esi+8] pushedx movzxecx,cl pushesi calldwordptr[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末尾的时候,需要返回外层调用。


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

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

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

kd>u83e54593l4 nt!IofCallDriver+0x63: 83e545935epopesi 83e5459459popecx 83e545955dpopebp 83e54596c3ret

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

kd>u13e103cl3 013e103c61popad 013e103d83c424addesp,24h 013e1040c3ret

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


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

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



【漏洞分析】CVE-2017-6178:从补丁对比到Exploit
【漏洞分析】CVE-2017-6178:从补丁对比到Exploit
本文由 安全客 原创发布,如需转载请注明来源及本文地址。
本文地址:http://bobao.360.cn/learning/detail/3935.html

Viewing all articles
Browse latest Browse all 12749

Latest Images

Trending Articles





Latest Images