前言

Hello, 欢迎来到windows kernel explot第五篇. 在这一部分我们会讲述从windows 7到windows的各主流版本的利用技巧(GDI 对象滥用). 一共有两篇, 这是上篇.


[+] 从windows 7到windows 10 1607(RS1)的利用
[+] windows 10 1703(RS2)和windows 1709(rs3)的利用.

这篇文章的起源来源于我在当时做第三篇博客的时候, 卡在了分析的点, 然后当时开始陷入自我怀疑, 分析和利用究竟哪一个重要一点. 于是, 我把第三篇博客的那个洞就先放了下, 在github上面更新了一个项目. 保证我在windows的主流平台上都能掌握至少一种方法去利用.

学习的过程采用的主要思路是阅读peper参考别人的代码实现, 调试来验证观点. 所以你能看到项目的代码是十分丑陋的, 所以请不要fork... 会有一点点愧疚 :)

项目目前更新到了RS3, 所以接下来准备的工作是:

[+] 完成RS4和RS5的学习(有相关的的思路, 还未来得及验证)
[+] 完成注释和丑陋代码的修缮

希望能够对您有一点小小的帮助 :)

缓解措施的绕过

如果你有阅读过我的系列第二篇的话, 你应该能够知道write-what-where在内核当中的妙用, rootkits老师在他的个人博客里面有给出一个相当详细的介绍. 所以在这里我就不再赘述.

我们来回顾一下第二篇的内容:

[+] 利用漏洞构造write-what-where
[+] 使用write-what-where替换掉nt!haldispatchTable+8
[+] 替换的时候不直接替换成Shellcode. 替换使程序执行ROP流, 绕过SMEP
[+] 执行shellcode.
    ==> 找到SYSTEM TOKEN VALUE
    ==> 找到user process TOKEN addr
    ==> mov [user process TOKEN addr], VALUE
[+] 提权验证.

可以看到, 我们的思路是在内核当中执行我们的shellcode, 实现提权. 然后想在内核当中执行shellcode, 就需要绕过各种缓解措施. 那么, 如果我们能够在用户层次实现shellcode的功能, 是不是就不需要绕过那么多的缓解措施呢. 答案是肯定的(说来这是我刚刚开始做内核就有的一个猜想...)

我们一开始的假设是我们有任意的写能力(write-what-where), 在项目当中我使用了HEVD模拟. 所以我们解决了:

mov [user process TOKEN addr], SYSTEM TOKEN VALUE

这条指令, 现在的关键点还差下面得两个语句.

[+] 找到`SYSTEM TOKEN` VALUE
[+] 找到`USER PROCESS TOKEN`

找到SYSTEM TOKEN VALUE

我们知道我们要找到一个东西的, 首先需要找到其地址. 所以让我们先来找找SYSTEM TOEKN的地址. 我参考了这里给出的解释, 但是不慌, 让我们先一步一步的来, 先不管三七二十一, 最后再来验证他.

第一步: 找到Ntoskrnl.exe基地址

先来看微软给的一个API函数.

这个函数可以帮我们检索内核当中的驱动的地址. 于是我们依葫芦画瓢. 创建下面的代码.

需要注意的是, 上面的代码的测试环境我是在windows 10 1803版本做的, 针对windows 1803以下的版本是同样成立的(当然包括本小节的windows 7- window8.1). 你知道的, 我是一个懒得截图的人 :)

接着做点小小的修改. 会得到下面的结果:

我们可以看到我们找到了ntoskrnl.exe的基地址. 那么ntoskrnl.exe是啥呢.

[+] Ntoskrnl.exe is a kernel image file that is a fundamental system component.

我们可以看到Ntoskrnl.exe包含是一个内核程序, 期间包含一些有趣的信息.

比如 :)

找到PsInitialSystemProcess


我们可以看到PsInitialSystemProcess存放一个指针, 其指向EPROCESS LIST的第一个项(也就是我们的SYSTEM EPROCESS). 我们可以利用我们在第二篇当中获取nt!HalDispatchTable的思路来获取它.

代码如下:

调试器的验证结果

现在问题就来了, 我们成功的找到了存放SYSTEM EPROCESS的地址放在那里, 但是我们却没有办法去读取他(xxx区域属于内核区域). 我们对内核只有写的权限, 那么我们怎么通过写的权限去获取到读的权限呢. 于是我们的bitmap闪亮登场 :)

BITMAP 的基本利用

我们来讲一下bitmap的利用思路之前, 先回顾我们在上一篇的内容当中, 已经成功的get到了如何泄露bitmap地址的能力(你看我安排的多么机智). 所以让我们借助上一篇的代码泄露一个bitmap观察一一下它的数据.

上面那张图看着有点蒙? 没事的, 让我们先来看看再内核当中bitmap对应的结构体.

typedef struct{
ULONG64 dhsurf; // 8bytes
ULONG64 hsurf; // 8bytes
ULONG64 dhpdev; // 8bytes
ULONG64 hdev; // 8bytes
SIZEL sizlBitmap; // 8bytes
    // cx and cy
ULONG64 cjBits; // 8bytes
ULONG64 pvBits; // 8bytes
ULONG64 pvScan0; // 8bytes
ULONG32 lDelta; // 4bytes
ULONG32 iUniq; // 4bytes
ULONG32 iBitmapFormat; // 4bytes
USHORT iType; // 2bytes
USHORT fjBitmap; // 2bytes
} SURFOBJ64

你可以对照着我给的截图当中的彩色部分, 是我们的关键数据. 蓝色的第一个对应pvBits第二个对应pvScan0. 那么, pvScan0有什么用呢.

pvScan0的作用在bitmap的利用当中尤其重要, 所以我用笨蛋的方法来说明它.

[+] pVscan0指向我们的pixel data. 在CreateBitmap参数当中通过第五个参数传入.
[+] pvScan0如果是fffff901`41ffdb88. 那么pixel data(上面的代码样例是"aaaa")就放在fffff901`41ffdb88.
验证

而微软的另外两个API在本例当中也相当重要, 我们来看一下.

他们的作用是.

[+] SetBitMapBits(GetBitmaps)向pvScan0指向的地址写(读)cb byte大小的数据.

所以如果我们假设能够篡改某个(术语 worker bitmap)bitmappvScan0的值为任意的值的话, 我们就能获取向任意地址的权限.

如果你能懂上面那一句话就太好了, 不懂的话让我们通过一个实验来一步一步理解它.

第一步

第一步让我们先来看一份代码, 代码已经更新到我的github上, 你可以在这里找到它同步实验.

看不懂没有关系的, 没有什么比调试器更能帮我们理解代码了. 先来看在运行了这份代码之后, 发生了写什么神奇的事.

第二步.

在这里我们通过上一篇当中的bitmap地址泄露找到了managerpvScan0的地址.和workerpvScan0的地址. 聪明的你一定能根据前面的结构体明白0x60指的是pvScan0的地址 :)

第三步.

第三步我做了两件事, 第一个单步运行到write_what_where函数里面. 你可以里面发现什么都没有. 于是我借助于调试器模拟了一次write_what_where.

[+] 替换manager的pvSca0的内容为worker的pvScan0的地址.

接着我们就可以进行任意读写了. 运行之后就得到了上面的提权. 是不是感觉有点飘. 让我们来分析一下(我比较建议您单步进入WriteOOB函数和ReadOOB观察数据变化, 我比较懒...)

第四步: 任意读

在上面的替换之后(第三步). 我们可以得到我们的manager.pvScan0指向worker.pvScan0. 由上面的截图我们知道.

[+] manager.pvScan0 ==> fffff901`407491e0
[+] worker.pvScan0  ==> fffff901`40667cf0

实现了这个之后让我来看看实现任意读呢, 让我们来看看我们的源码:

我们假设我们要将0x4000的内容读取出来, 那么readOOB会进行下面的操作.

[+] SetBitmapBits的时候, 向manager.Pvscan0存放的地址(A)出写入0x4000
[+] 地址(A)即为worker.pvScan0的地址.
[+] 现在worker.pvScan0存放的地址为0x4000
[+] 调用GitBitmapBits能获取0x4000处的内容

第五步: 任意写

写的操作和读的内容是差不多的, 所以我原封不动的COPY了一份上面的内容, 稍微做了点修改.

我们假设我们要将0x4000的内容写入值1, 那么readOOB会进行下面的操作.

[+] SetBitmapBits的时候, 向manager.Pvscan0存放的地址(A)出写入0x4000
[+] 地址(A)即为worker.pvScan0的地址.
[+] 现在worker.pvScan0存放的地址为0x4000
[+] 调用SetbitmapBits能对0x4000处的内容进行写操作.

韩国的有位师傅对流程做了一个流程图. very beautiful!!!

第六步: 替换Token

第六步我们发现其实和我们系列一的内容是极其相似的. 只是利用在用户层(我们已经有了任意读写的能力)用c++实现了汇编的功能. 在这里就不再赘述. 你可以阅读我的系列第一篇获取相关的信息.

总结

在这一篇当中我们讲述了在windows 7. 8 . 8.1 1503 1511下利用bitmap实现任意读写的主体思路, 而在接下来的下半部分的文章当中. 我们会讲述在windows 10后期的不同版本当中GDI的滥用. 也会介绍为什么我们在本篇的方法会什么会在windows 10后期的版本为什么会失效的原因. 敬请期待 :)

相关链接

[+] sakura师父的博客: http://eternalsakura13.com/
[+] 小刀师傅的博客: https://xiaodaozhi.com/
[+] SUFOBJECT64: http://gflow.co.kr/window-kernel-exploit-gdi-bitmap-abuse/
[+] 我参考的所有资料的PDF:  https://redogwu.github.io/
[+] 我的个人博客: https://redogwu.github.io/
[+] 我的github上面的那个项目: https://github.com/redogwu/windows_kernel_exploit
[+] 做实验所需要的代码: https://github.com/redogwu/windows_kernel_exploit/tree/master/windows_8/blog_test_win8.1

后记

博客截图好累啊 :)

最后, wjllz是人间大笨蛋.

点击收藏 | 0 关注 | 1
  • 动动手指,沙发就是你的了!
登录 后跟帖