fuzzySecurity自16年起更新了数篇Windows内核exp的教程,本文是内核篇的第十篇,也是目前为止的最后一篇。点击查看原文。
欢迎回到另一个Windows内核exp开发系列教程!今天我们看些不同寻常的东西。不久前@zeroSteiner 在rzpnk.sys中挖掘出了两个bug(CVE-2017-9770 & CVE-2017-9769),该驱动由Razer Synapse所用。此后不久我决定看看这两个bug并且。。。我还发现了可以本地提权的另外一个逻辑bug(CVE-2017-14398)!
本文我们将简要的阐述 CVE-2017-9769,此后我们会针对我发现的bug CVE-2017-14398写一个exp。开始之前我想大声疾呼 @aionescu,他总是说我根本不知道我在做什么,但这一次我可以挺直腰板了!我也曾一度在玩Binary Ninja,截图就是从那里获取的,如果有人对此感兴趣的话。
Resources:
Razer Rzpnk.Sys IOCTL 0x226048 OOB Read (CVE-2017-9770) (@zeroSteiner) - here
Razer Rzpnk.Sys IOCTL 0x22a050 ZwOpenProcess (CVE-2017-9769) (@zeroSteiner) - here
MSI ntiolib.sys/winio.sys local privilege escalation (@rwfpl) - here
在开始之前我想先快速的展示一下,这些漏洞函数在调用图示上有多么的接近。对于分发函数来说,它们字面上是相邻的。
这些调用分支源于上面同一个决策点,我们可以看到它从IOCTL中减掉了0x10,当该值是0的时候,就跳转到ZwOpenProcess调用处,如果值为0x14的话,就跳转到ZwMapViewOfSection调用处。
同时注意到该驱动会检查输入和输出缓冲区的尺寸,如果提供的输入参数不够或输出缓冲区不够大的话,分支会落入失败的情景。
我们不会在该函数上浪费太多时间,该漏洞很容易被证明。我们知道函数需要两个QWORD作为输入参数,从Spencer的exp中可以看到他pack了一个pid和null作为QWORD。我们可以用下面的POC快速复制。
运行POC,得到下面的输出。
如果我们查看返回的两个QWORD值的话就会发现,第一个是我们传入的PID值,而第二个是一个句柄。当我们在PowerShell进程中查看返回的句柄时,可以看到如下的内容。
游戏也就通关了,我们有一个对System pid完全访问权限的句柄,这意味着我们可以从该进程空间中任意读写。Spencer的exp中的方法起始就是:
是时候上点好东西了!我强烈推荐你先看看 @rwfpl的文章来了解一下该bug类型的背景知识,ntiolib/winio exp在这里 here。
记住在第一个截图中函数期望一个0x30尺寸的输入(6个QWORD),同时输出也是0x30大小。我们可以快速创建一个POC来访问到漏洞函数。
在做调试之前,我们先运行POc并看看驱动返回了什么。
很好,附加了一串输入参数后我们可以看到Int64返回了0,低序DWORD返回了NTSTATUS code。这种情况下, ZwMapViewOfSection返回了 STATUS_INVALID_HANDLE ,它表示函数期望一个区段句柄作为第一个参数,而我们却填充了一些垃圾值。另一方面的影响在于我们可以通过对比NTSTATUS码0x0(STATUS_SUCCESS)来鉴别调用是否成功。
图中可以看到我们已经知道了一些静态参数的值,为了消除疑惑我们在调用ZwMapViewOfSection调用处下个断点,检查寄存器和堆栈。ZwMapViewOfSection使用stdcall标准,因此,参数会被存储在RCX,RDX,R8,R9以及栈上。
把这些全部放在我们的输入参数中,得到下面的内容。
这里我们大部分能控制的都很直接。进程句柄的话我们仅仅需要传递一个到Powershell有完整权限的句柄,提交尺寸/视图尺寸则很简单,就是我们映射到进程的大小。但还是有个问题,我们要如何得到一个区段句柄,驱动并没有任何的函数允许我们去调用ZwCreateSection或ZwOpenSection。
Leaking Physical Memory
在这一点上我甚是迷惑,由于我无法创建一个区段句柄,exp也就到此为止了。幸运的是@aionescu 给了我一些灵感。使用NtQuerySystemInformation并携带SystemHandleInformation类别参数我们可以泄露出系统上通过进程打开的所有句柄。这些句柄是每-进程用户空间句柄,然而,System进程(PID=4)是一个特殊的例子,它允许我们转换用户空间句柄成内核句柄!
我为什么要关心这些?实际上System有一个到"\Device\PhysicalMemory"的句柄,如果我们可以泄露出该句柄的话,就可以利用ZwMapViewOfSection来直接映射物理内存到我们的PowerShel进程了!
我写了一个powershell函数来实现这个功能。它使用静态的句柄常量来判断进程打开的句柄类型。我近期更新了这个函数,它现在可以在Win 7到Win 10 RS2上工作。 Get-Handles是我GitHub上的 PSKernel-Primitives repo的一部分,如果你想要利用PowerShell做内核pwn的话,可以使用它。
获取内核句柄所需要做的就是把一个静态值与0x204(64位:0xffffffff80000000,32位:0x80000000)做加法运算。我们可以动态的做这件事。
现在我们可以把所有东西整合一下,写一个新POC。为了测试起见,我们尝试去映射1mb的物理内存到PowerShell。
运行POC,得到下列输出。
注意到我们通过阅读NTSTATUS码来丈量是否成功,Int64值此前是0,而这一次它是一个地址,表示我们本地进程中区段的映射位置。在Process Hacker中可以看到确实分配了一个严格大小的1024kb的内存块。
使用Process Hacker,我们实际上可以转储这块内存到磁盘上。如果我们那样做的话,就可以看到下面一些有趣的内容。Bootkit?
我们已证实了这个漏洞,但我们要如何去获取一个SYSTEM shell呢?
Hunting EPROCESS
最直接的办法就是通过写一个经典的token窃取exp。困难在于要在我们映射的内存中查找EPROCESS结构体。WinDBG中显示EPROCESS结构体被分配在一个标签为'Proc'的池块上。
EPROCESS指针减去'Proc'池块首,我们可以立即算出头部尺寸。相反的,如果我们有了一个任意'Proc'池地址的话,我们也可以计算出EPROCESS结构的任意属性的位置。
如果你想查找这些依赖于架构/版本的偏移字段而不想利用KD的话,你可以看看 Terminus Project。这是 @rwfpl 所分享的很帮的资源,它会在很多情景下节省你大量的时间。
所以我们就把问题简化到寻找'Proc'池块上,现在还没通关。为了找到这些块我们可以扫描整个内存映射区段来查找这个特殊的'Proc'池标签。
因为优化的原因,我们注意到池块都以0x10对齐。本质上,这意味着我们只需要以16字节作为单位进行读取。这可能没什么但却节省了宝贵的时间。即使如此,这个搜索也是非常之缓慢,毕竟'Proc'池块标签在900mb之后就开始存在了。我们可以通过扫描0x30000000(0x30000000/(1024*1024)=768mb)起始的映射内存来进一步的优化搜索过程,对不同OS版本来说,这个数可能会变化。
为了验证我们的理论,我们可以用下面的循环(在1.2gb映射区段)来查找'Proc'池块并转储一些EPROCESS数据来验证。
一部分结果如下:
如你所见,该实现体并不完美,某些映像的名称被截断了,且有着少量的检测完全没有显示EPROCESS结构体。幸运的是,我发现大部分进程都可以保证被正确的检测到(包括PowerShell/lsass)。
遗留问题就是去修改上面的循环,使得它记录下lsass进程的token以及PowerShell进程的token位置。一旦两个元素都找到了我们就可以简单的替换PowerShell的token来提权到SYSTEM!这可能需要一些实验,在8,8.1和10上进行exp将是一个较为直接的练习。唯一需要考虑的就是EPROCESS结构体的变化以及开发一种策略来处理System进程的多个区段句柄。最终的exp如下。
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!
最后于 2018-3-20 19:59
被玉涵编辑
,原因: