<程序员>11期文章
申明。文章仅代表个人观点,与所在公司无任何联系。
1. 概述
函数堆栈缓存溢出,是操作系统和应用程序安全漏洞最常见,最严重的类型之一。它往往导致可以允许攻击者可以远程执行恶意代码。
例如,以下这段代码[2,p147]就展示了Windows系统RPC调用中的函数堆栈缓存溢出类型的安全漏洞。它就是导致冲击波病毒(Blaster)爆发的根源。
HRESULT GetMachineName(WCHAR *pwszPath)
{
WCHAR wszMachineName[ N + 1 ];
...
LPWSTR pwszServeName = wszMachineName;
while (*pwszPath != L'//')
*pwszServerName++ = *pwszPath++;
...
}
在微软的安全开发周期模型中,专门在安全编码实践中推荐:对于微软的最新C/C++编译器,使用GS选项编译选项,加入检测函数堆栈缓存溢出错误额外代码。
那么,GS编译选项的内部原理是什么?它是如何检测函数的堆栈缓存溢出?如何使用?本文将会深入探讨这些问题。
2. Windows 系统的堆栈结构
为了说明/GS 编译选项的内部原理,我们需要先从Windows 系统的堆栈结构谈起。
用下面这段程序举例:
int test( int iLen, char *pBuf);
int _tmain(int argc, _TCHAR* argv[])
{
char * pBuf = "AAAAAAAAAAAAAAAAAAAA";
test( 10, pBuf );
return 0;
}
int test( int iLen, char *pBuf)
{
char p[10];
strncpy( p, pBuf, 20 );
return 0;
}
细心的读者也许已经看出,在执行 strncpy 会有堆栈溢出。我们后面会具体分析这一点。我们先看一下具体的汇编指令是如何操作函数调用时的堆栈的。
在 main 函数调用 test 函数时,有以下指令:
0040100b 8b45fc mov eax,[ebp-0x4]
0040100e 50 push eax ;参数pBuf
0040100f 6a0a push 0xa ;参数10
00401011 e80a000000 call GSTest1!test (00401020) ;调用test
00401016 83c408 add esp,0x8
可以看出,在函数调用时(C函数调用类型),首先
· 将参数自右向左压栈
· 执行call 指令,将函数的返回地址(00401016)压栈,并转移IP(指令寄存器)为调用函数的入口地址。
以下是堆栈上的具体数据。
0:000> dd esp
Esp值 返回地址 参数
0013fed4 00401016 0000000a 004050ec 004050ec
test函数的入口指令:
GSTest1!test:
00401020 55 push ebp ;保存上层函数堆栈基址
00401021 8bec mov ebp,esp ;设置当前函数堆栈基址
00401023 83ec0c sub esp,0xc ;分配局部变量空间
test函数的出口指令:
00401035 83c40c add esp,0xc ;释放堆栈局部变量
00401038 33c0 xor eax,eax ;清空相应寄存器
0040103a 8be5 mov esp,ebp ;恢复堆栈指针
0040103c 5d pop ebp ;恢复上层函数堆栈基址
0040103d c3 ret ;返回
在test函数退出时的ret指令,就会将堆栈中保存的返回地址设置到IP(指令寄存器)。这样,程序就会从函数调用出继续执行。
由此,我们可以总结一下Windows 系统的堆栈结构。其中包括以下数据:
调用参数
返回地址
EBP上层函数堆栈基址
异常处理代码入口地址
(如果函数设置异常处理)
局部变量
表1:Windows系统的堆栈结构
3. 缓存溢出分析
了解了堆栈的基本结构,我们就可进一步阐述攻击者是如何利用缓存溢出来控制代码走向,以达到运行恶意代码的目的。
由于函数的堆栈空间是自上向下分配的。那么,一个缓存溢出的程序缺陷,将导致溢出部分的数据在堆栈上自下向上覆盖。如果溢出部分的数据量足够大的话,就可能覆盖堆栈上的返回地址。那么,当函数返回时,控制就不是返回到事先设定的上层函数,而是一个可以由攻击者指定的地址。
继续接着上面的实例分析。以下是执行strncpy的汇编指令
00401026 6a14 push 0x14 ;参数20
00401028 8b450c mov eax,[ebp+0xc] ;参数pBuf
0040102b 50 push eax
0040102c 8d4df4 lea ecx,[ebp-0xc] ;参数p
0040102f 51 push ecx
00401030 e80b000000 call GSTest1!strncpy (00401040)
在执行strncpy前,参数p的值为0013fec4
这时候的堆栈结构是
0:000> dd esp
0013feb8 0013fec4 004050ec 00000014 00403bca
0013fec8 0013fed0 00407004 0013fee4 00401016
0013fed8 0000000a 004050ec 004050ec 0013ffc0
因为strncpy会从0013fec4地址处开始覆盖20个字节的数据,超出了p数组的长度(10字节)。那么,溢出的数据就会沿着堆栈自低向高覆盖。
执行strncpy后的堆栈:
0:000> dd esp
0013feb8 0013fec4 004050ec 00000014 41414141
0013fec8 41414141 41414141 41414141 41414141
0013fed8 0000000a 004050ec 004050ec 0013ffc0
这时堆栈上保存的返回地址0x00401016已经被覆盖为溢出的数据0x41414141(0x41是A的ASCII代码)。
于是,函数退出时,就会直接跳至0x41414141处运行。
0:000> r
eax=00000000 ebx=7ffdf000 ecx=00000000 edx=41414141 esi=00000a28
edi=00000000 eip=41414141 esp=0013fed8 ebp=41414141 iopl=0
换句话说,攻击者可以通过控制用以覆盖函数的返回地址的溢出数据的值,来控制程序的运行了。如果这段溢出数据是可以远程发送的,例如是发送到网络某个端口的数据包,那么攻击者就可以远程运行恶意代码。
4. GS编译选项分析
4.1堆栈的变化
GS编译选项的原理就是在堆栈上插入一个安全cookie,以测试堆栈上的返回地址是否被修改过。安全cookie为4个字节,在堆栈上的位置如下。
调用参数
返回地址
EBP上层函数堆栈基址
安全cookie
异常处理代码入口地址
(如果函数设置异常处理)
局部变量
表2:GS编译选项的堆栈结构
那么,如果是堆栈的局部变量发生缓存溢出的错误而导致返回地址被覆盖的话,由于安全cookie所在的位置,它也一定会被覆盖。
4.2函数的入口和出口代码
GS编译选项,对函数的入口和出口代码都添加了针对安全cookie操作的指令。
test函数的入口指令:
GSTest1!test:
00401020 55 push ebp ;保存上层函数堆栈基址
00401021 8bec mov ebp,esp ;设置当前函数堆栈基址
00401023 83ec10 sub esp,0x10
00401026 a130704000 mov eax,[GSTest1!__security_cookie (00407030)]
0040102b 8945fc mov [ebp-0x4],eax
首先,堆栈的空间分配从0x0c变化为0x10,是因为需要多分配4字节的安全cookie。增加的另外两条指令是为了将GSTest1!__security_cookie的值放入堆栈的安全cookie的指定位置。
这时候的堆栈结构如下:
0:000> dd esp
0013fec0 0013fee0 004013e8 0013fed0 6a915791
0013fed0 0013fee4 00401016 0000000a 004050ec
0x6a915791就是安全cookie,它存放在返回地址0x00401016前。
test函数的出口指令则变为:
0040103d 83c40c add esp,0xc
00401040 33c0 xor eax,eax
00401042 8b4dfc mov ecx,[ebp-0x4]
00401045 e85b010000 call GSTest1!__security_check_cookie (004011a5)
0040104a 8be5 mov esp,ebp
0040104c 5d pop ebp
0040104d c3 ret
也增加了两条指令。首先将堆栈上的安全cookie的值放入ecx,然后调用__security_check_cookie函数来检查其值是否被修改过。
如果一旦发现安全cookie的值被改动,那么就会转入异常处理,终止程序运行。这样,即使存在缓存溢出的错误,GS选项也能阻止恶意代码通过覆盖函数的返回地址这种攻击方式。
4.3安全cookie检查和错误处理
安全cookie的检查通过__security_check_cookie函数。它的逻辑非常简单:
GSTest1!__security_check_cookie:
004011a5 3b0d30704000 cmp ecx,[GSTest1!__security_cookie (00407030)]
004011ab 7501 jnz GSTest1!__security_check_cookie+0x9 (004011ae)
004011ad c3 ret
004011ae e9c1ffffff jmp GSTest1!report_failure
如果堆栈上的安全cookie的值和GSTest1!__security_cookie的值一致的话,那么函数正常退出。否则,就会执行错误处理程序:跳往GSTest1!report_failure。之后,会运行__security_error_handler。如果应用程序没有特别设定__security_error_handler,那么缺省的错误处理就会弹出以下提示框并终止程序。
图1:GS编译选项的缓存溢出错误提示框
4.4如何使用GS编译选项
以Visual Studio 2003举例。选择:项目配置,C/C++,Code Generation,Buffer Security Check,就可以控制是否使用GS编译选项来编译程序。
图2:使用Visual Studio 2003的GS编译选项
4.5对性能的影响
从上面的分析看出,GS编译选项会增加4个字节的堆栈的分配空间,以及在函数的入口和出口添加针对安全cookie的若干指令。那么,它对程序的性能影响如何?
需要指出的是,GS编译选项不是对每一个函数对设置安全cookies。Visual Studio的编译程序首先会确定函数是否属于 “潜在危险”的函数,例如在函数的堆栈上分配了字符串数组,就是一个特征。只有那些被确认为“潜在危险”函数,GS编译选项才会使用安全cookie。
根据Visual Studio 编译组的数据,在绝大多数情况下,对性能的影响不超过2%【1】。
所以在微软的安全开发周期模型中,强烈推荐使用GS编译选项。相对于轻微的性能损失,GS编译选项对程序的安全性的提高是非常巨大的。事实上,微软最新的Windows Vista操作系统的开发中,就应用了GS编译选项。
4.6局限
GS编译选项针对的是函数的栈缓存溢出(stack buffer overrun),覆盖返回地址的攻击方式。对于程序的其它类型的安全漏洞,它并不能提供有效保护。例如:
堆缓存溢出(heap buffer overrun)
攻击异常处理程序
等等
所以,决不是使用了GS编译选项就可以高枕无忧了。需要强调的是,GS编译选项并不是消除了程序的缓存溢出安全漏洞,而是试图在特定情况下,降低安全漏洞的危害程度。例如,即使GS编译选项可以防止恶意代码被远程执行,但是程序也会异常终止。如果该程序是一个重要的服务器进程,这就会是一个典型的DOS(Deny of Service)攻击。
通过合理的开发方式,以确保代码中没有安全错误,才是最为重要的。
总结
GS编译选项可以有效降低缓存溢出安全漏洞的危害程度。尽管它对于程序的其它类型的安全漏洞,并不能提供有效保护,作者仍然强烈推荐,在软件开发中,使用GS编译选项。在安全领域中,本来就没有一个工具或技术可以包打天下,解决所有的安全问题。
参考文献
1. The Compiler Security Checks In Depth,Brandon Bray, Microsoft Corporation
2. The Security Development Lifecycle, Michael Howard, Steve Lipner