这是UAF系列的第二篇. 三篇的主要内容如下.
首先要说很多感谢, NCC group真的做了很杰出的工作, 受益颇多. 然后是keenjoy98老师, 他在blackhat上提供的PDF给我思路的理解提供了很大的帮助. 还有拖了这么多天. sakura师父没有把我打死. 还有就是小刀师傅这段时间不厌其烦的解惑.
然后是一点小小(大大)的抱歉.
其次是一点小小的说明.
然后是一点啼笑皆非的事. 我实习的时候想去xxx, 然后师傅和我说. 你要是把ddctf的两道kernel pwn的题做出来, 我不认为你去不了. 所以ddctf的pwn题本来是我这个月末的目标来着. 结果在做堆头修复的时候. 查资料才发现这就是第二题... emmmmm. 不过我由于我参考了过多的资料, 所以其实不算做出来.
下面是文章主要涉及的知识点:
我自己浪费的时间比较久的是:
所以我会把这三个部分我犯得错误贴出来. 希望能够帮你避免你能够重复犯错.
代码的实现我实现的NCC group的方法. 由于英文比较差, 出现了点理解误差, 所以我的布局和NCC gruop的有一点点小的不同. keenjoy98老师的方法我觉得我应该大概理解了, 但是我可能想的麻烦了, 所以就不再赘述.
希望这篇文章对你能够有点小小的帮助.
Let's Go
故事的开头是这样的. 有一天你想实现一下内核提权. 于是你写了如下的shellcode.
这部分的shellcode你可以从第一篇当中的到解释从而类推. 或者你可以在这里得到. 代码也有详细的注释. 所以 这一部分. 我主要讲一下如何编译64的汇编. x64不支持_asm
内联汇编. 所以我目前知道的有三种选择.
我个人更喜欢第三种. 因为好看. 我的环境是vs 2015. 设置编译选项的动态图如下.
需要注意的是这两个命令. 原封不动的ctrl+c
和ctrl+v
即可
好了. shellcode的编译已经写完了. 我们知道shellcode只能在内核当中执行. 如何在内核当中执行它呢. 在内核当中我们观察到一个有趣的代码段.
函数nt!NtQueryIntervalProfile+0x22调用了nt!KeQueryIntervalProfile, 接着我们观察一下nt!KeQueryIntervalProfile, 发现如下代码段.
我们发现这个地方调用了一个函数指针(一个指针用来存储函数的地址), 我们存储在nt!HalDispatchTable+0x8处 , 那么它指向哪一个函数呢呢. 运行下面的指令
hal是一个函数指针数组. dqs列出其中的值. 我们看到函数hal!HaliQuerySystemInformation
存储在偏移0x8处. 如果. 我是说我们如果能有一个对任意地址写的机会. 我们就有能力修改偏移0x8处的值. 何不试试把它改成shellcode的地址. 那么在KeQueryIntervalProfile
中的代码可以替换成call shellcode. 于是我们就可以执行shellcode. 记下我们接下来要实现的目标
那么我们去找一个漏洞吧, 才不要(逃), 作为一个win内核选手我们得记住我们是拥有windbg的男人. windbg具有的功能
所以我们可以采用windbg来模拟任意地址读写. 整个过程的步骤如下.
最后我们采用在代码最后加上system("cmd") 创建cmd, 用来观察提权是否成功.main函数代码如下.
需要注意的shi__debugbreak
, 相当于int 3
, 方便我们使用windbg, 这是我exp开发过程中用的最多的命令. 动态图如下:
wait, 咋和说好的不一样呢. 蓝屏了.
万恶之源来源于在微软在win8下实现的一个缓解措施. SMEP. 解释如下.
上面的逻辑我们用伪代码表示如下.
我们看到那个地方有一个条件判断. 是否开启SMEP. 所以绕过这个判断. 让我们加入这一个语句绕过smep.
ok, 再次运行. 得到提权. 演示如下.
!
好了. 提权成功, 那我们这篇文章到这里就结束了(我就皮一下...).
好吧, 还没有. 我们用的调试器. 那么我们得用代码模拟调试器呀. 如何模拟调试器呢. 三个小目标.
我们先讲前两个.
我们前面的蓝屏是一键很难受的事, 所以我们得绕过SMEP.
SMEP是微软在win8先加的缓解措施. 其目的是kernel不可执行user space的代码. 所以假设我们的shellcode放在0x410000处(user mode), 我们控制rip执行shellocde的时候, 就会产生kernel执行user space代码的情况. 于是BSOD发生. 漏洞利用失败.
wait. 某某不可执行, 于是我们想到了我们的老本行, DEP. 数据段不可执行. 那么我们可不可以利用DEP的绕过方式: ROP. 答案是肯定的. 于是我们来看下面这一段代码.
等等, cr4是啥. cr4是决定SMEP的关键寄存器. SMEP将基于cr4寄存器来判断. 先来看一张图.
我们通过smep标志位(第20位, 从0计数)来判断是否要启用SMEP. 我们来查看一下我们的cr4寄存器的运行在我的环境下触发漏洞前后的对比.
我们可以看到关键bit位的更改, 假设我们把haldispatchtable+0x8处改为nt!KiConfigureDynamicProcessor+0x40
的时候, rax也刚刚好为0x406f8
, 而刚好返回地址也为shellcode的地址, 那么简直完美. 幸运的是, 假设我们把漏洞利用函数改为此.
动态调试你会发现其刚刚好. 不幸的是, 我们查看这个地方的汇编代码, 长这样:
问题出在ecx. 这个地方返回地址我们可控的是32位, 而我们的exp是64位, 也就是shellcode地址是64位的. pwn2town上介绍了一种我完全看不懂的方法(由于这个原因, 我尝试过其他的SMEP bypass). 所以我换了另外一个思路.
在我的源码最后, 我加了这么几句, 来恢复堆栈平衡和修复cr4寄存器(不能瞎改内核的东西, 借完之后借的还回去)
如果你惊讶于30等值是如何检测出来的, 你可以利用你的windbg, 动态调试来修复就可以了. 而0fffff8020074af75h
是由于ROP的时候返回地址被破坏了, 我一开始采用虚拟机把它记作一个常量. 后来用获取基地址的计数把它替换掉了, 具体的你可以查看我的源码.
由于windbg的存在, 我们可以假设我们已经拥有了write-what-where的功能. So, 如果是要完成将第二项改为shellcode的地址. 那我们第一件要做的事, 势必是去找到他. 调试器中我们很机智的用dqs就找到了, 但是在代码当中如何来实现呢. 源代码当中我是这样实现的
首先, 我们假设我们经过GetKernelImageBase函数获取到了"ntoskrnl.exe"加载在内存当中的基地址, 并把它赋值给了pKernelBase变量(后面我们会让这个假设成为真实). 上面的代码获取nt!haldispatchTable在内核当中的地址的思路是:
好了, 让我们来获取pKernelBase.
windows加了地址随机化. 所以每次开机重新加载的时候. ntoskrnl.exe在内核当中的基地址都不一样. 这一部分, 其实我的个人建议是, 直接保存一个虚拟机镜像, 这样KASLR就已经被绕过了. 直接拷出每个函数在这个镜像当中的地址, 然后直接使用, 把后面的做完了再来绕过KASLR. anyway, 让我们来看一下如何找到内核当中的ntoskrnl的镜像.
我的学习是在r00k1ts大大的这篇文章找到了答案, 获取基地址的思路如下.
哇, 走到这里, 万事OK, 现在我们所欠缺的, 只是如何构造一个write_what_where而已. 让我们来看看我们的漏洞的代码.
很好, 逆向这一部分的工作, 你可以查看我上传的IDB文件观察细节, ncc gourp里面也给了详细的解释. 这里我先给出另外一个函数.
微软给出这个函数的解释如下:
三个参数与漏洞函数的三个参数关系如下.
So, 前面讲了这么多和我们的漏洞有什么关系呢, 针对一个滚动条控件窗口, 首先由一个tagWND
窗口来装载(第一个参数pwndWnd), 期间有一个psbInfo结构体. 如下:
psbInfo存储滚动条的相关信息, 定义如下:
接着, 你可以利用这几个结构体去查看上面的代码, 这里我直接给出结论.
我们看一下过程.
我们得经过上面的这个程序才能实现完整的漏洞触发. 你可以进行逆向看下必须满足什么条件. 这里我给出结论.
于是, 相关的源代码当中, 体现这两个细节的是.
So, 我们来实现控制回调函数.
回调在我看来, 是内核漏洞发生的本源. 因为如果从内核层次回到user mode, 再从user mode回到内核层次, 在用户层次的时候我们拥有着极大的自由. 这样的我们能够做太多事了.
SO: 如何利用回调.
我们假设, 在xxxDrawScrollBar里面会触发某个函数回调, 代码会去执行回调函数A, 如果我们能够HOOK回调函数A. 使其指向我们自己写的回调函数, 我们就能在此期间做一些坏坏的事. 关键的问题是, 这个回调函数A是谁呢?
现在的我看来, 这是一个很简单的问题, 但是当时的我, 花了足够多的时间去解决和思考这个问题.
一开始的时候, 我选用的方法是: 静态阅读xxxDrawScrollBar
的代码, 看下他当中有哪些回调函数, 确定哪些函数会被调用. 于是我祭出了我的IDA, 就一步一步的点啊之类的. 在经历了漫长的调试分析之后, 我失败了. 因为到后面的时候我的思绪乱了.
于是夜里三点, 躺在寝室的床上, 我开始思考人生, 真的要这样下去么, 一辈子就看着代码点点点度日子... 突然灵光一闪烁, 我意识到这样下去破日子不能这样子过下去. 于是我开始思考我掌握的和回调相关的知识. 定位到了关键性的几个信息.
首先看一条命令.
此处指向回调函数指针数组, 类似于这样:
接着查看回调函数必然经过这里:
该函数的原型如下:
其中, ApiNumber勾起了我的兴趣
期间, 我在一个win32k的paper上看到如上定义, 也就是说, 我只要能够通过确定rcx的值, 来确定我要hook的回调函数是谁.
首先, 在这两个地方下断点.
此指令用于查看寄存器的值
在地点A和地点B之间会经过nt!KeUserModeCallback
处, 我们查看rcx, 即可确定会调用哪些回调函数. 就是这么简单.
最后我选取了NCC group推荐的回调函数, 在确定了需要HOOK函数之后, 代码如下.
OK, 由此我们get到了需要HOOK的函数地址, 所以后面我们只要进行简单的相应的赋值语句就好了.
Hook完毕, 让我们进行下一步. 在我们自己定义的fakeHookFunc之中, 我们能干些啥.
这一步, 我决定先给出相关的代码实现:
首先想说的Hookflag和hookCount, 我们在hook了函数之后, 这个回调函数很有可能被系统的其他部分使用. 但是我们想控制的只是由xxxDrawScrollBar
触发的时候, 所以我们得确定一下哪一次才是由xxxDrawScrollBar
触发的. 我设置这两个变量就是为了做这件事.
这一部分我们保证了我们从进入触发流程之后再计数, 之后我们在调用xxxDrawScrollBar
下断点, 看一下从xxxDrawScrollBar
之后进去HOOK是第几次. 是不是有点小小的绕, 让我们来看一下动态的过程.
之后是两处注释, phmValidateHandle函数用于获取hwndVulA的内核地址, 是为了方便我自己调试用的. 至于如何获取的, 你可以查看这里(小刀师傅的博客). 接着.
偏移为b0的地方为其psbInfo. 于是我用了下面的语句来查看信息.
如果rax+b0 地址为 0400: 100, 那么这条命令会打印出100处的内容. 在我整个exp开发的过程中, 我频繁的使用这条语句来进行堆风水布局的验证.
接着是DestoryWindow, 这个函数会销毁窗口的相关内容, 但是其句柄因为不会被销毁, 因为其引用计数不能为0. 但是已经够了, 这样之后, 我们的psbinfo处于free状态, 且指针不为0. 于是我们可以通过堆喷射(堆喷射请参考上一篇)来重新填充内容.
如何来通过堆喷来伪造填充我们的psbInfo呢, 先看一下正常状况下的pbInfo. 我dump下来的数据如下:
接着我调用了下面一个for循环, 实现了堆喷. 覆盖数据如下所示:
为了证明不是我瞎编的, 图如下.
上面那一小节我们证明了我们的看到了我们的win32k!tagSBINFO
大小为0x30(加上对齐和_HEAP_ENTRY, 先别在意这两个.), 接着我们来查看一个结构体:
调用SetPropA第一次的时候, 首先会在分配一个堆, 存储一个tagPropLIST结构体. 第二次调用setPropA的时候, 会继续分配一个tagPROP
结构体(0x10). 也就刚刚是0x28, 再加上其的-heap-enrty
. 刚刚好合适.
接着, 由于刚好是2个, 根据前面的结论. 这个数值会在后面的异或过程中变为0xe. 我们如何来利用0xe呢. 恐怕我们就得说一下tagPropList了.
首先来查看SetPropA函数:
这个函数对于此次漏洞利用的信息有:
这一部分过了之后, 那么我们如何使用这个特性呢. 我们得对前面的结构体加一点点注释.
在漏洞函数执行回win32k!xxxEnableWndSBArrows()
函数之后, 通过前面的讨论, 内核结构遭到篡改. 内核会误以为一共有0xe个tagProp, 所以我们可以在后面继续调用setProp覆盖后面的数据. 也就是有了一个越界读写的能力. ==> 能写0xe个tagProp
听起来不错, 我们有了破坏内核结构的能力. wait, 如果你仔细的查看tagProp和setPropA的对应关系. 你会发现写原语残缺. 截图如下.
高亮的部分就是我们可以控制的内容. 非高亮部分是无法控制的. 我们在win32k!的利用当中, 常见的思路是去破坏tagWND结构体的某一个值. 然后实现任意地址读写. 但是, 假设我们后面接的是一个tagWND
结构体, 那么我们进行写操作的时候我们必定会对其中的某些重要值照成破坏. 照成利用失败.
于是NCC gruop安排了一个新的布局(这一部分的布局我自己改了一下). 如下.
下面我们来解释为什么要这样布局. 首先看一个函数.
接着查看一下tagWND的一个结构体成员.
当调用NtUserDefSetText
函数的时候, 内核当中, 关联的tagWND
结构体的strName会有相应的改变. buffer存储一个指针, 指向o4lstr
指向的字符串. 而这一步的关键点在于. 这些字符是分配在一个堆中. 堆含有一个堆头. 如下所示:
你可以去查看写原语残缺的时候dump的内存. 你会发现heap entry的内容是可控的. 里面包含当前堆块的大小等信息. 所以, 现在假设一种状况:
基于此, 新的问题就产生了
这是我们后面主要需要讨论的. 所以我们从简单的说起.
我在heap cookie上花了大量的时间, 因为当时找的资料并不多, 大多数都是堆内部管理的资料. 我想找一个泄露cookie的资料, 死活没有找到. 所以最后在通过阅读源码+阅读堆内部管理的理论知识, 解决了这个问题.
首先, 我们假设要伪造的_HEAP_ENTRY
所关联的堆大小是0x1b0(后面解释为什么为这个值), 堆是以0x10的为一个单位. 前面我们可以看到前面的_HEAP_ENTRY
结构体偏移0x8处即为size, 那么我们直接把这个值改为0x1b(记住以0x10为单位). 那么是不是就ok了呢.
如果这样做的话, 我们就会被安排的明明白白. windows呢, 很久以前就知道有人想弄它的堆. 所以他就实现了一个Cookie. 来保护它的堆. 保护的过程如下.
windows在每次开机的时候, 都会有一个随机的cookie值生成. heapChunk 释放状态的时候.
所以我们的heapChunk不能乱搞, 我们只单单改大小是过不了堆的检测的. 我们如何构建一个能通过检测的堆. 首先, 假设我们已经获取正确的cookie(此时假设为). 我们dump一下还没有被覆盖的heap
算下small tagIndex
OK, 之后:
这里你可能有一个小小的疑惑, 为什么我要dump解密之后再加密. _HEAP_ENTRY
在未与cookie异或之前, 不管你怎么电脑开机, 每次除了smalltagIndex之外, 应该都是一样的(这个地方可能有点问题, 但是这是我调试得出的结论.). 所以你直接dump改变大小, 重新赋值size. 再和cookie进行异或就可以使用了了. 当然, 你也可以选择具体深究_HEAP_ENTRY
结构体的每一个成员, 算出他们每一个的值.
这一部分我自己的开发过程中. 根本没有管这个cookie. 反正电脑是虚拟机. 那么保存镜像. 每次都是一样的. 那么我只要用调试器获取一个cookie. 然后就可以用了.
我们来讲一下如何用代码来泄露此cookie(这一部分我其实不是独立开发, 用的别人代码调试理解)
这一部分我是如何理解的呢(这个地方我是通过调试器理解+原理, 所以可能有误, 因为实在没有找到现成的详细解释的资料, 所以实在抱歉). 我注意到上面有几个常量. 刚好可以和_HEAP
对应起来.
我dump了几个_HEAP
的数据, 发现他们的0x10处都为0xffeeffee
, 所以依据此可以判断此内存块存放_HEAP
结构.
接着通过下面的这张图:
Desktop heap会有一份堆映射到user space, 也就是我们可以用virtualAlloc可以查询到的, 每一个Desktop heap在kernel中的地址和映射到内核中的地址是固定的, 如果满足user space address + offset = kernel space address
. 说明到了Desktop heap对应的_HEAP
结构, 接着偏移0x80的地方存放的是我们的cookie
值.
我们前面讲过, tagWND里会有一个strName, 与NtUserSetText函数关联, 期间strName是一个nt!_LARGE_UNICODE_STRING
结构体.
我们知道我们差的是write_what_where
:
这就是我们整体的利用思路, 举个例子, 我们不是要写nt!halDispatchTagble+0x8的值为shellcode么.
ok, 现在我们的剩下的唯一问题就是我们如何把布局变成我们想要的布局.
很多时候名字是一个有意思的事, 比如fengshui布局. 光从一个名字我们能得到什么.
对于这个漏洞利用来说, 什么样的环境是我们需要的呢. 前面我们说过.
所以我们期望的布局图示如下.
我在风水布局上面花了相当长的一段时间. 因为对两个地方理解有误导致.
如果听不懂就对了. 我们来搞懂他.(这一部分建议看看我的源码, 虽然很丑)
首先. 由前面我们知道.
通过上面的代码片段我们分配了0x1e0大小的桌面堆块, NtUserDefSetText
函数是我进行堆喷射的接口. 通过它我们能够的到任意大小的heap.
于是, 为了实现上面的堆分配. 我一开始分配了0x300个0x1e0
Desktop heap.
之后为了防止堆块合并, 我进行了隔一个进行free.
free之后, 我通过两次填充, 布局如下:
很好, 我们释放此0x1b0的数据, 接着先填充0x180, 在填充0x30的数据. 在释放0x180之后, 我们申请tagWND
, 如下:
接着隔一释放我们另外的0x1e0的数据块, 一堆循环重复之后, 我们实现了我们想要的布局.
很抱歉, 这一部分实在讲的不够好. 一个是我实在不会做gif图, 那种彩色图实在是不会做, 如果后面我学会了, 这一部分会重新更新. 另外一部分, 我总感觉绕了很多的弯路, 幸运的是, 他是可靠的.
我依据的原则如下:
在实现了布局之后, 我们的漏洞利用就结束了. 只要构造一个假的tagWND
, 改变其strname.buffer
值, 就能够实现我们的任意地址读写.
在我学习heap cookie的过程中, 我查阅资料的时候发现, 这是ddctf的第二题... 于是, 我看到出题的keenjoy98师傅说.
那一瞬间觉得整个人都凉了, 因为我的代码, 何止需要打磨, 简直需要回炉重造. 一开始还是有代码组织的, 后来自从heap cookie
开始, 每一天想的都是如何实现功能, 根本没有想组织的心情. 所以那是一份相当不堪入目的代码.
另外一个问题是死锁, 如果你观察我的代码, 能看到很多的Sleep函数, 原因来源于, 其实exp的开发很久以前就完工了. 但是有个很奇怪的事, 当我运行在调试某些地方写入__debugbreak
的时候, 我在最后运行的时候, 我可以运行cmd
, 但是去掉这些__debugbreak()
, 在调试器当中我打印出Token已经被替换了. 但是就是没有cmd产生. 于是我dump了一下此时卡住时候的堆栈. 发现是由于windows的消息卡住了. 于是我花费了三四天的时间在研究如何绕过死锁. 最后实在没有找到方法(因为操作系统实在是太菜了). 有一天, 我想, 既然我那么多个__debugbreak()
可以抛出cmd
, 那么我是不是能够通过模仿__debugbreak
的行为来绕过死锁呢. 我一开始选取了for循环, 但是在vs 万恶的优化下, 自动帮我去掉了, 所以我最后选取了Sleep(5000)
函数, 成功的帮我绕过了死锁.
首先, 这一部分只算是我自己的想法. 所以不算是教科书般的定义... 所以请把他当作是一种讨论, 不要当作教条.
win32k是一个很大很大的东西, 也就是说, 就算给了你源代码写了详细的注释, 可能你都得花一辈子的时间去理解阅读, 估计是比等名侦探柯南完结更久的时间. 所以, 尽量少去静态逆向win32k的代码. 很多时候, 动态调试能帮你省去很多时间. 我自己做的过程中, 必须需要逆向的代码, 体现在漏洞触发的时候, 我需要理解代码如何才能抵达漏洞触发的那个点的位置. 基于这种情况下. 一般的有用的资料是.
说到底, 我只是想写提权而已. 每一年都有无数个win32k漏洞被爆出来, 每次的漏洞的函数都不一样, 存在很大的可能性, 在你一年之后, 回想一年前的代码你已经忘记的干干净净了. 所以, 纠结于这个函数到底干嘛, 这个结构体到底在干嘛, 我觉得并不一定是合适. 相反, 与我而言. 更重要的是.
拥有能力我觉得是比使用能力更重要的事. 因为如果你有能力, 剩下的过程不过是循环往复, 调试改正. 这样.
可能看了上面的东西你有点小小的难受, SMEP, heap cookie... 这都啥啥啥.... 但是一个好消息. 如果你阅读完全文之后, 你会发现. 其实大多数是依赖于操作系统. 和你此次的利用哪一个漏洞代码其实无关. 也就是说, 这一部分的东西, 你只要学习一次就好. 我觉得这是一个很好的消息. 意味着,如果你是一个懒惰的人, 大可以翻翻有没有开源的库, 别人已经编写好的代码直接用就好了. 类似于这样.
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!
最后于 2018-10-17 23:59
被wjzid编辑
,原因: 更新一个重要的链接(调试的时候使用最多)