目录:
安全编码实践之一:GS编译选项和缓存溢出
安全编码实践之二:NXCOMPAT选项和数据执行保护DEP
安全编码实践之三:C/C++静态代码分析工具Prefast
安全编码实践之四:C/C++中禁用危险API
安全编码实践之五:地址空间格局随机化ASLR
安全编码实践一:GS编译选项和缓存溢出
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编译选项。在安全领域中,本来就没有一个工具或技术可以包打天下,解决所有的安全问题。
安全编码实践二:NXCOMPAT选项和数据执行保护DEP
1.概述
在安全编码实践一中我们谈到GS编译选项和缓存溢出。缓存溢出的直接后果就是可能导致恶意代码的远程执行。GS选项存在自身的局限,例如,有若干方法可以绕过GS选项的保护。
在这篇文章里,我们会介绍另外一个非常重要的安全特性:数据执行保护,即DEP-Data
Execution Prevention,以及与之对应的NXCOMPAT选项。
在栈溢出程序例子中,我们看到,恶意代码是被存放在栈(stack)上的。类似的,如果是一个堆(heap)溢出的安全漏洞,恶意代码是被存放在堆上。无论堆还是栈,都是数据页面。在数据页面上,通常情况下,是不应该执行代码的。
DEP,就是禁止应用程序和服务在非可执行的内存区(non-executable
memory)上执行指令。
2.DEP和硬件支持
2.1软件DEP和硬件DEP
在探讨DEP的原理前,我们先区分两个容易引起混淆的概念:软件DEP(Software
DEP)和硬件DEP(Hardware-enforced DEP)。
软件DEP,并不是真正意义上的DEP。简单的说,它的原理是检查异常处理是否安全(SEH-Safe
Exception Handling)。它是完全通过软件支持的一种安全特性。在以后的安全编码实践中我们会专门讨论SEH。
硬件DEP,则是需要CPU提供支持的,同软件DEP相比,硬件DEP提供的保护更为全面。以后我们提到的DEP,都是指硬件DEP。
2.2 NX位
在80x86体系结构中,操作系统的内存管理是通过页面(page)存储方式来实现的。虚拟内存空间的管理,如代码,数据堆栈,都是通过页面方式来映射到真正的物理内存上。原理参见下图【1】。
图1:页面内存映射
在AMD64位CPU上,在页面表(page table)中的页面信息加了一个特殊的位,NX位(No
eXecute)。
- 如果NX位为0,这个页面上可以执行指令。
- 如果NX位为1,这个页面上不允许执行指令。如果试图执行指令的话,就会产生异常。
Intel在它的CPU上也提供了类似的支持,称为XD 位( eXecute
Disable),其原理和AMD的NX是完全一样的。
操作系统对DEP的支持,就是将系统或应用程序的数据页面,标注上NX位。这样,一旦由于堆栈缓存溢出的安全漏洞,导致恶意代码试图在数据页面上运行,CPU就会产生异常,导致程序终止,而不是去执行恶意指令。
Windows对DEP的支持是从Windows XP SP2,和Windows
Server 2003开始的。用户可以通过Control Panel à System àAdvanced
àPerformance来查看系统的DEP设置。
图2:DEP设置
3.使用NXCOMPAT选项
NXCOMPAT是一个链接(LINK)选项,从Visual Studio
2005起开始支持。在具体介绍NXCOMPAT选项前,我们先看下面一段代码:
// DEP.cpp: Demo of how DEP protects executing code from data page
//
#include "stdafx.h"
/* win32_exec - EXITFUNC=process CMD=calc.exe Size=164 Encoder=Pex http://metasploit.com */
unsigned char scode[] =
"/x2b/xc9/x83/xe9/xdd/xe8/xff/xff/xff/xff/xc0/x5e/x81/x76/x0e/xd2"
"/xbd/xf7/x35/x83/xee/xfc/xe2/xf4/x2e/x55/xb3/x35/xd2/xbd/x7c/x70"
"/xee/x36/x8b/x30/xaa/xbc/x18/xbe/x9d/xa5/x7c/x6a/xf2/xbc/x1c/x7c"
"/x59/x89/x7c/x34/x3c/x8c/x37/xac/x7e/x39/x37/x41/xd5/x7c/x3d/x38"
"/xd3/x7f/x1c/xc1/xe9/xe9/xd3/x31/xa7/x58/x7c/x6a/xf6/xbc/x1c/x53"
"/x59/xb1/xbc/xbe/x8d/xa1/xf6/xde/x59/xa1/x7c/x34/x39/x34/xab/x11"
"/xd6/x7e/xc6/xf5/xb6/x36/xb7/x05/x57/x7d/x8f/x39/x59/xfd/xfb/xbe"
"/xa2/xa1/x5a/xbe/xba/xb5/x1c/x3c/x59/x3d/x47/x35/xd2/xbd/x7c/x5d"
"/xee/xe2/xc6/xc3/xb2/xeb/x7e/xcd/x51/x7d/x8c/x65/xba/xc3/x2f/xd7"
"/xa1/xd5/x6f/xcb/x58/xb3/xa0/xca/x35/xde/x96/x59/xb1/x93/x92/x4d"
"/xb7/xbd/xf7/x35";
int _tmain(int argc, _TCHAR* argv[])
{
void (*pRunCalc)();
pRunCalc = (void (*) ()) (void *) scode;
pRunCalc();
return 0;
} |
字符串sCode所包含的是一段shellcode,也就是一段汇编代码。这段shellcode的功能是运行calc.exe,即计算器程序。pRunCalc作为函数指针指向这段代码。整个程序的目的就是试图在数据页面上执行代码。
在Visual Studio 2005环境,用Win32 Console
Application类型,编译这个程序,生成DEP.exe。
在一台Windows Vista 32位OS,有DEP支持的 机器上,运行DEP.exe。sCode指向的shellcode被执行,弹出了计算器窗口。
为什么DEP没有起保护作用呢?这是因为出于应用程序兼容性的考虑,在Windows
Vista 32位的缺省配置下,DEP只负责保护Windows的系统程序和服务,而不包括其它应用程序。
如果在连接选项上加上/NXCOMPAT,重新链接生成DEP.exe。执行DEP.exe则会出现
图3:DEP.exe异常中止
关闭这个对话框后,在任务条上会弹出如下提示:
图4:DEP触发提示
这就表明是由于DEP保护机制的触发,而导致DEP.exe程序被终止。
如果用WinDBG调试的话,会发现,当DEP.exe试图运行shellcode的第一条指令的时候,出现了系统异常。
0:000:x86> g
(1764.b38): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
*** WARNING: Unable to verify checksum for DEP.exe
DEP!scode:
00000000`00417000 2bc9 sub ecx,ecx
0:000:x86> db 00417000 << 00417000就是sCode的地址
00000000`00417000 2b c9 83 e9 dd e8 ff ff-ff ff c0 5e 81 76 0e d2 +..........^.v..
00000000`00417010 bd f7 35 83 ee fc e2 f4-2e 55 b3 35 d2 bd 7c 70 ..5......U.5..|p
00000000`00417020 ee 36 8b 30 aa bc 18 be-9d a5 7c 6a f2 bc 1c 7c .6.0......|j...|
00000000`00417030 59 89 7c 34 3c 8c 37 ac-7e 39 37 41 d5 7c 3d 38 Y.|4<.7.~97A.|=8
00000000`00417040 d3 7f 1c c1 e9 e9 d3 31-a7 58 7c 6a f6 bc 1c 53 .......1.X|j...S
00000000`00417050 59 b1 bc be 8d a1 f6 de-59 a1 7c 34 39 34 ab 11 Y.......Y.|494..
00000000`00417060 d6 7e c6 f5 b6 36 b7 05-57 7d 8f 39 59 fd fb be .~...6..W}.9Y...
00000000`00417070 a2 a1 5a be ba b5 1c 3c-59 3d 47 35 d2 bd 7c 5d ..Z....<Y=G5..|] |
我们看出,一旦使用的/NXCOMPAT选项,生成的程序在运行时候就会受到DEP机制的保护。
4.NXCOMPAT选项的内部实现
NXCOMPAT的实现,是通过在Windows PE文件头信息(PE Header)中设置IMAGE_DLLCHARACTERISTICS_NX_COMPAT这个标志位。参见MSDN具体的定义信息如下【2】
typedef struct _IMAGE_OPTIONAL_HEADER {
WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
… …
WORD DllCharacteristics;
… …
} IMAGE_OPTIONAL_HEADER,
*PIMAGE_OPTIONAL_HEADER;
DllCharacteristics
The DLL characteristics of the image. The following values are defined.
IMAGE_DLLCHARACTERISTICS_NX_COMPAT
0x0100 The image is compatible with data execution prevention (DEP). |
当IMAGE_DLLCHARACTERISTICS_NX_COMPAT被设置时,就意味该程序选择在存在DEP支持的情况下,被DEP机制保护。
5.NXCOMPAT选项的局限
首先,并不是所有的CPU都提供了硬件DEP的支持。
其次,NXCOMPAT选项,或者是IMAGE_DLLCHARACTERISTICS_NX_COMPAT的设置,只对Windows
Vista系统有效。在以前的系统上,如Windows XPSP2,这个设置会被忽略。也就是说,应用程序并不能自己决定是否被DEP保护。
就性能而言,由于是CPU提供的硬件支持并触发异常,并不会有什么影响。
更大的考虑是兼容性。特别的,如果你的程序使用了ATL 7.1或更早的版本,由于ATL会在数据页面上产生执行代码(不要问我以前ATL为什么会这样设计。J),使用DEP保护运行时就会出现系统异常。
如果你的程序要使用第三方的插件DLL的话,而又事先无法确定第三方的DLL是否可以支持DEP的话,使用NXCOMPAT也可能会有问题。这是因为DEP的保护对象是进程一级。一个典型的例子是IE。IE需要支持第三方的IE
Plugin插件。
使用Task Manager,选择Data Execution Protection列,可以看到在Windows
Vista下,iexplorer.exe并没有被DEP保护。
图5:IE的DEP设置
同GS选项类似,也存在其它攻击方式可以绕过NXCOMPAT+DEP的保护机制,例如return-to-libc攻击。【3】
6.总结
DEP,也就是数据执行保护,可以有效降低堆或栈上的缓存溢出漏洞的危害性。采用NXCOMPAT选项后,应用程序的运行被DEP
机制保护。在考虑兼容性的前提下,建议开发人员采用NXCOMPAT链接选项。
安全编码实践三:C/C++静态代码分析工具Prefast
1.概述
在前面的安全编码实践的文章里,我们讨论了GS编译选项和数据执行保护DEP功能。
结论是GS和DEP可以有效的缓解缓存溢出类型的安全漏洞的危害。关于这个结论,有两个大家需要值得注意的地方。
第一:GS和DEP是缓解(mitigation)措施。也就是说,代码本身仍然存在着安全漏洞,只是由于GS和DEP降低了其危害程度。
第二:GS和DEP存在其自身的局限性。例如,GS不是对每一个函数都适用,而DEP则需要一定的硬件支持。
那么,一个很自然的问题就是,有什么工具可以帮助我们在开发过程中,及早发现并修补代码中存在的安全漏洞?
答案之一是静态代码分析工具。本文会着重介绍微软提供的C/C++的静态代码分析工具:Prefast。对于托管代码(managed
code),微软提供的静态代码分析工具是FxCop。关于FxCop,我们会在以后的安全编码实践的文章中专门介绍。
2.Prefast介绍
2.1历史
Prefast是微软研究院提出的静态代码分析工具。主要目的是通过分析代码的数据和控制信息来检测程序中的缺陷。需要强调的是,Prefast检测的缺项不仅仅是安全缺陷,但是安全缺陷类型是其检测的最为重要的部分。Prefast推出后在微软内部得到了广泛的使用,并经历了若干格版本的升级。现在,微软将这个内部工具商业化,以提供给外部的开发人员使用。
2.2 如何获得Prefast
目前有两个办法可以获得Prefast工具。
- Prefast包括在Visual Studio 2005 /2008的团队版本(team edition)中。
- Prefast包括在Windows驱动程序开发包(Microsoft Windows Driver
Kits)的开发环境中。
需要指出的是,Visual Studio的团队版本的价格要高于Visual
Studio个人版本,而Windows驱动程序开发包是免费下载的,那它们提供的Prefast版本有什么区别?在Visual
Studio的团队版本中,Prefast是直接和代码的开发过程集成的,使用非常方便。并且可以直接根据Prefast的输出结果创建相应的开发任务。而在Windows驱动程序开发包中,Prefast是作为一个单独的工具提供,没有像Visual
Studio团队版本中一样与开发环境集成。
下载Windows驱动程序开发包可以通过http://connect.microsoft.com/,
注册Microsoft Connect后选择Windows Logo Kit (WLK), Windows
Driver Kit (WDK) and Windows Driver Framework (WDF)即可。具体的步骤这里就不详细叙述了。
安装好WDK后Prefast就已经直接在其开发环境下使用了。
2.3使用Prefast
在Visual Studio的团队版本中,使用Prefast,打开Project
Properties --> Configuration Properties --> Code
Analysis -->Enable Code Analysis For C/C++ on build。选择
Yes(/analyze)即可。具体可参见图1。
如果直接使用CL.exe命令行编译器,采用/analyze编译选项即可。例如:cl
test.cpp -W4 /EHsc /analyze
图1:VSTS中使用Prefast(Code
Analysis)
使用在WDK中的Prefast,有以下几个重要命令。
- 运行Prefast:prefast build -cZ
- 查看Prefast输出结果:
- 命令行:prefast list
- GUI:prefast view
有关在WDK下使用Prefast的详细步骤,可以参见微软的PREfast
Step-by-Step文档【1】。
3.Prefast输出的警告(warning)信息
上面的介绍可以看出Prefast的使用操作并不难。成功应用Prefast的关键是要理解其输出的各类警告信息,并作出正确的评估和代码修改。文章前面我们提到过,Prefast检测的不仅仅是安全缺陷,但是安全缺陷类型是其检测的最为重要的部分。下面,我们就介绍一些和安全漏洞紧密相关的警告信息。以下给出的代码例子均来自于微软的文档Code
Analysis for C/C++ Warnings【2】。
警告 C6001: using uninitialized memory
<variable>,使用未初始化的变量。
例子:
#include "stdafx.h"
int f( bool b )
{
int i;
if ( b )
{
i = 0;
}
return i; // 当b为假时,变量i未初始化
} |
大家也许会问,使用未初始化的变量会导致安全漏洞吗?这里我想特别强调的一点是,我们前面经常提到的安全漏洞是缓存溢出导致的,但是大家千万不要认为缓存溢出是导致安全漏洞的唯一原因。有各种各样的代码错误可以导致严重的安全漏洞,使用未初始化的变量也是其中的一种。关于未初始化的变量如何导致安全漏洞的详细信息超出了本文的范畴,有兴趣的读者可以参见微软SWI组的博客文章:MS08-014
: The Case of the Uninitialized Stack Variable Vulnerability【3】
修补:初始化变量。
警告C6029: possible buffer overrun in
call to <function>: use of unchecked value,使用未验证的参数可能导致缓存溢出。
例子:
#include "windows.h"
void f(char *buff, DWORD cbLen, DWORD cbRead, HANDLE hFile)
{
if (!ReadFile (hFile, &cbLen, sizeof (cbLen), &cbRead, NULL))
{
// code ...
if (!ReadFile (hFile, buff, cbLen, &cbRead, NULL)) // warning 6029
{
// code ...
}
}
} |
在上面这个例子中,第一次的ReadFile得到的cbLen值,直接作为最大读取的长度传递给第二个ReadFile。如果cbLen过大的话,就会导致写入buff过多的数据。
修补:验证参数。在第二个ReadFile之前验证cbLen。
if (cbLen <= sizeof (buff)) // check
length
警告C6057: buffer overrun due to number
of characters/number of bytes mismatch in call to <function>,字符(characters)数目和字节(bytes)数目的不匹配导致缓存溢出。
对于ANSI字符串类型,字符数目和字节数目是一致的。但是如果字符串的类型是UNICODE,字节数目就是字符数目的两倍。如果在应该传递字符数目的地方传递了字节数目,就可能导致缓存溢出。
例子:
#include<tchar.h>
#include<windows.h>
void f( HINSTANCE hInst, UINT uID )
{
TCHAR buff[128];
if ( LoadString ( hInst, uID, buff, sizeof buff ) ) // warning C6057
{
// code...
}
} |
LoadString期望的参数是字符串缓存的字符数目。
修补:正确计算数组中元素的个数。
LoadString ( hInst, uID, buff, (sizeof
buff)/(sizeof buff[0]) )
警告C6201: buffer overrun for <variable>,
which is possibly stack allocated: index <name>
is out of valid index range <min> to <max>,数组索引的越界可能导致缓存溢出。
例子:
void f()
{
int buff[25];
for (int i=0; i <= 25; i++) // i exceeds array bound
{
buff[i]=0; // initialize i
// code ...
}
} |
修补:确保数组索引不越界。
警告C6202: buffer overrun for <variable>,
which is possibly stack allocated, in call to <function>:
length <size> exceeds buffer size <max>,使用缓存区时,给出的长度超出缓存区长度的最大值。
例子:
#include <memory.h>
void f( )
{
int intArray[5];
char charArray[5];
memset ((void *)charArray, 0, sizeof intArray);
// code ...
} |
修补:正确的缓存长度。这里sizeof intArray应该是sizeof
charArray。
警告C6204: possible buffer overrun in
call to <function>: use of unchecked parameter
<variable>,使用缓存区时,直接使用未经检查的参数可能导致缓存溢出。
例子:
#include<string.h>
void f(char *pCh)
{
char buff[10];
strcpy(buff, pCh);
} |
pCh的值没有验证就直接使用了。
修补:验证传入的参数。加入检查:if (strlen(pCh) >=
sizeof buff) return;
限於篇幅,我们这里无法一一列举所有和安全缺项紧密相关的重要警告信息,例如C6327,C6383,C6386等等。有关Prefast输出警告信息的详细列表,可以参见Code
Analysis for C/C++ Warnings【2】。
4.静态代码分析工具的局限
静态代码分析工具是SDL(安全软件开发周期)提出的编码实践中非常重要的一环。但是,现实中不存在任何一种工具可以一下子解决所有的安全问题。静态C/C++代码分析工具是不可能发现代码中的所有安全漏洞的。就像GS和DEP一样,它也存在自身的局限性。
由于程序控制流和数据流的复杂程度,静态代码分析工具不可避免的存在:
- 漏报:我们上面举出的代码例子都是非常简单的。随着代码复杂度的增大,尤其是跨函数之间逻辑和数据的交互关系,往往给静态代码分析带来极大的困难。
- 误报:分析信息的不完备,会导致误报警告信息。
5.总结
Prefast是微软提供的针对C/C++程序的静态代码分析工具,可以有效的检测代码中存在的安全缺陷。强烈建议在开发过程中,采用Prefast或其他类型的静态代码分析工具,以降低程序中引入(安全)缺陷的可能。
安全编码实践四:C/C++中禁用危险API
1.概述
在前面的安全编码实践的文章里,我们讨论了GS编译选项,数据执行保护DEP功能,以及静态代码分析工具Prefast。这里,我们讨论在C/C++代码中禁用危险的API,其主要目的是为了减少代码中引入安全漏洞的可能性。
2.那些是危险的API
2.1历史
在微软产品的安全漏洞中,有很大一部分是由于不正确的使用C动态库(C Runtime
Library) 的函数,特别是有关字符串处理的函数导致的。表一给出了微软若干由于不当使用C动态库函数而导致的安全漏洞【1,p242】。
微软安全公告 |
涉及产品 |
涉及的函数 |
MS02-039 |
Microsoft SQL Server 2000 |
sprint |
MS05-010 |
Microsoft License Server |
lstrcpy |
MS04-011 |
Microsoft Windows (DCPromo) |
wvsprintf |
MS04-011 |
Microsoft Windows (MSGina) |
lstrcpy |
MS04-031 |
Microsoft Windows (NetDDE) |
wcscat |
MS03-045 |
Microsoft Windows (USER) |
wcscpy |
表1:不当使用C动态库函数而导致的安全漏洞
不当使用C动态库函数容易引入安全漏洞,这一点并不奇怪。C动态库函数的设计大约是30年前的事情了。当时,安全方面的考虑并不是设计上需要太多注意的地方。
2.2 危险API的列表
有关完整的危险API的禁用列表,大家可以参见http://msdn.microsoft.com/en-us/library/bb288454.aspx.
在这里我们列出其中的一部分,以便大家对那些API被禁用有所体会。
禁用的API |
替代的StrSafe函数 |
替代的Safe CRT函数 |
有关字符串拷贝的API |
strcpy, wcscpy, _tcscpy, _mbscpy,
StrCpy, StrCpyA, StrCpyW, lstrcpy, lstrcpyA, lstrcpyW,
strcpyA, strcpyW, _tccpy, _mbccpy |
StringCchCopy, StringCbCopy,
StringCchCopyEx, StringCbCopyEx
|
strcpy_s |
有关字符串合并的API |
strcat, wcscat, _tcscat, _mbscat,
StrCat, StrCatA, StrCatW, lstrcat, lstrcatA, lstrcatW,
StrCatBuffW, StrCatBuff, StrCatBuffA, StrCatChainW,
strcatA, strcatW, _tccat, _mbccat |
StringCchCat, StringCbCat,
StringCchCatEx, StringCbCatEx
|
strcat_s |
有关sprintf的API |
wnsprintf, wnsprintfA, wnsprintfW,
sprintfW, sprintfA, wsprintf, wsprintfW, wsprintfA,
sprintf, swprintf, _stprintf |
StringCchPrintf, StringCbPrintf,
StringCchPrintfEx, StringCbPrintfEx
|
_snprintf_s
_snwprintf_s
|
表2:禁用API的列表(部分)
其它被禁用的API还有scanf, strtok, gets, itoa等等。
”n”系列的字符串处理函数,例如strncpy等,也在被禁用之列。
3.如何替代被禁用的危险API
从上面的介绍可以看出绝大多数C动态库中的字符串处理函数都被禁用。那么,如何在代码中替代这些危险的API呢?在表2里,我们看到有两种替代方案:
后面我们会讨论这两种方案的不同之处。这里我们先说它们的共同点:提供更安全的字符串处理功能。特别在以下几个方面:
- 目标缓存区的大小被显式指明。
- 动态校验。
- 返回代码。
以StringCchCopy举例。它的定义如下:
HRESULT StringCchCopy(
LPTSTR pszDest,
size_t cchDest,
LPCTSTR pszSrc
); |
cchDest指明目标缓存区pszDest最多能容纳字符的数目,其值必须在1和STRSAFE_MAX_CCH之间。StringCchCopy总是确保pszDest被拷贝的字符串是以NULL结尾。并且提供以下的返回代码:
S_OK,STRSAFE_E_INVALID_PARAMETER,和STRSAFE_E_INSUFFICIENT_BUFFER。这样,采用StringCchCopy来替代被禁用的strcpy的话,就可以有效降低由于误用字符串拷贝而导致缓存溢出的可能。
3.1使用StrSafe
使用StrSafe非常简单。在C/C++代码中加入以下的头文件即可。
#include "strsafe.h"
StrSafe.h包含在Windows Platform SDK中。用户可以通过在微软的网站直接下载。
下面给出一个使用StrSafe的代码示例【2】。
不安全的代码:
void UnsafeFunc(LPTSTR szPath,DWORD cchPath) {
TCHAR szCWD[MAX_PATH];
GetCurrentDirectory(ARRAYSIZE(szCWD), szCWD);
strncpy(szPath, szCWD, cchPath);
strncat(szPath, TEXT("//"), cchPath);
strncat(szPath, TEXT("desktop.ini"),cchPath);
} |
在以上代码里存在着几个问题:首先,没有错误代码的校验。更严重的是,在strncat中,cchPath是目标缓存区可以存放字符的最大数目,而正确传递的参数应该是目标缓存区剩余的字符数目。
使用StrSafe后的代码是
bool SaferFunc(LPTSTR szPath,DWORD cchPath) {
TCHAR szCWD[MAX_PATH];
if (GetCurrentDirectory(ARRAYSIZE(szCWD), szCWD) &&
SUCCEEDED(StringCchCopy(szPath, cchPath, szCWD)) &&
SUCCEEDED(StringCchCat(szPath, cchPath, TEXT("//"))) &&
SUCCEEDED(StringCchCat(szPath, cchPath, TEXT("desktop.ini")))) {
return true;
}
return false;
} |
3.2使用Safe CRT
SafeCRT自Visual Studio 2005起开始支持。当代码中使用了禁用的危险的CRT函数,Visual
Studio 2005编译时会报告相应警告信息,以提醒开发人员考虑将其替代为Safe CRT中更为安全的函数。
下面给出一个使用Safe CRT的代码示例【3】。
不安全的代码:
void UnsafeFunc (const wchar_t * src)
{
// Original
wchar_t dest[20];
wcscpy(dest, src); // 编译警告
wcscat(dest, L"..."); // 编译警告
} |
以上这段代码里存在着明显缓存溢出的问题。
使用Safe CRT后的代码是
errno_t SaferFunc(const wchar_t * src)
{
wchar_t dest[20];
errno_t err = wcscpy_s(dest, _countof(dest), src);
if (!err)
return err;
return wcscat_s(dest, _countof(dest), L"...");
} |
3.3 StrSafe 和Safe CRT的对比
我们看到,StrSafe和Safe CRT存在功能重叠的地方。那么什么时候使用StrSafe,什么时候使用SafeCRT呢?
下面的表格【1,p246】里列出了两者之间的差异。采用何种方式应该根据具体情况而定。有时候也许只能采取其中一种方式:例如如果你的开发系统是Visual
Studio 2003的话,就只能使用StrSafe。或者你的代码中有许多itoa的话,就考虑使用Safe
CRT,因为StrSafe中没有提供简单的替代方式。有时候也许两者都可以。这种情况下,我个人是更喜欢采用StrSafe这种方式,因为它不依赖具体的动态库支持。如果是编写Win32上的程序的话,StrSafe的HRESULT的返回代码,也和Win32
API的代码类似,这样代码的整体风格可能会更加一致。
|
StrSafe |
Safe CRT |
发布方式 |
Web |
Microsoft Visual Studio 2005 |
头文件 |
一个 (StrSafe.h) |
多个 (不同的 C runtime 头文件) |
是否提供链接库的版本 |
是 |
是 |
是否提供内嵌(Inline)版本 |
是 |
否 |
是否是业界标准 |
否 |
正在评估过程 |
Kernel Mode支持 |
是 |
否 |
返回类型 |
HRESULT (user mode)
NTSTATUS (kernel mode) |
随函数变化,errno_t |
是否需要修改代码 |
是 |
是 |
主要针对 |
缓存溢出 |
缓存溢出,和其它安全方面的考虑 |
表3:StrSafe和Safe CRT对比
4.争论
在开发过程中,代码中全面禁用危险的API的编码实践,存在着一定的争议性。其中最具有代表性的观点可以参见Danny
Kalev的Visual C++ 8.0 Hijacks the C++ Standard一文【4】。争论主要集中在以下几点。
4.1对于程序性能的影响
以StrSafe举例,由于增加了更多的动态校验,其速度较C动态库的函数相比,是有所下降的。在【2】一文中,给出了对StrSafe速度方面的测试数据如下:
测试例子:1千万次字符串合并调用。结果:
C动态库:7.3秒
StrSafe:8.3秒
我们看到,如果开发的系统不是完全以字符串处理为工作核心的话,使用StrSafe对系统性能的影响是可以控制的。
4.2开发人员可以决定在合适的地方使用危险API
首先,同意如果代码中正确使用危险API的话,也是可以避免安全漏洞的引入。但是,在具体的开发实践中,存在着以下问题:
- 开发人员的素质和培训
- 有时候即使执行严格的代码复查,仍然可能由于使用危险的API而引入安全漏洞。
第二点尤其关键。大家看到这里可能会有疑问,使用危险的API有这么容易出问题吗?即便代码复查(code
review)也没能看出来?【5】中给出了一个微软安全漏洞的具体实例。
微软 05-047 Plug-n-Play RPC:即插即用中的漏洞,允许远程执行代码和特权提升。经过身份验证的攻击者可以通过创建特制的网络消息并将该消息发送到受影响的系统来尝试利用此漏洞。导致这个严重的安全漏洞的代码如下:
#define MAX_CM_PATH 360
GetInstanceList(
IN LPCWSTR pszDevice, IN OUT LPWSTR *pBuffer, IN OUT PULONG pulLength)
{
WCHAR RegStr[MAX_CM_PATH], szInstance[MAX_DEVICE_ID_LEN];
...
// Validate that passed in pszDevice is an actual registry entry
// If lookup for the key fails, reject call and cleanup.
// ghEnumKey points to HKLM/System/CurrentControlSet/Enum
if (RegOpenKeyEx(ghEnumKey, pszDevice, 0,
KEY_ENUMERATE_SUB_KEYS, &hKey) != ERROR_SUCCESS) {
Status = CR_REGISTRY_ERROR;
goto Clean0;
}
...
ulLen = MAX_DEVICE_ID_LEN; // size in chars
...
// Query szInstance from registry
RegStatus = RegEnumKeyEx(hKey, ulIndex, szInstance, &ulLen, ...);
if (RegStatus == ERROR_SUCCESS) {
// Build lookup string given a valid registry root key and valid instance ID
wsprintf(RegStr, TEXT("%s//%s"), pszDevice, szInstance);} |
复查这段代码时,我们看到,虽然使用了危险的API:wsprintf,但应该是不会发生缓存溢出的问题。这是因为根据MSDN,
图1:注册表字符数目的限制
于是:
- pszDevice 应该少于255 characters
- pszDevice 是一个 有效的 key 在HKLM/System/CurrentControlSet/Enum
- szInstance 是一个有效的subkey在pszDevice下
- RegStr is 360 characters
- 攻击者并不能控制注册表内容
但实际上,wspringf还是导致了缓存溢出的安全漏洞。到底是怎么回事?我们来看一下攻击代码:
errno_t SaferFunc(const wchar_t * src)
int main()
{
PWCHAR pszFilter = (PWCHAR)malloc(sizeof(WCHAR)*1000);
PWCHAR Buffer = (PWCHAR)malloc(86);
wsprintf(pszFilter,L"ISAPNP//ReadDataPort//////////////////////////////////////////////////////////////0");
CM_Get_Device_ID_List((PCWSTR)pszFilter,Buffer,86,1);
return 0;
} |
攻击代码之所以有效,是因为:
- pszFilter(除去末位的0) 作为pszDevice 传递给GetInstanceList
- RegOpenKeyEx 接收了这个长字符串,忽略那些“/”,返回 ERROR_SUCCESS。
通过这个例子我们看出,在开发复杂的系统中,即便是有经验的开发人员,加上严格的代码复查过程,还是有可能由于使用危险的API而导致安全漏洞的引入。这也是微软在Windows
Vista的开发过程中全面禁用危险API的原因。
4.3程序可移植性的影响
这一点的考虑是非常值得重视的。不管是StrSafe,还是Safe CRT,都不是工业界标准。因此,如果开发的系统需要移植到其它平台的话,采用Safe
CRT是肯定不合适的。StrSafe的Inline方式,因为不依赖特定库,对可移植性的影响相对较小。
5.总结
在C/C++程序中禁用危险的API,可以有效降低在代码中引入安全漏洞的可能。在考虑了性能和可移植性的因素下,强烈建议在开发过程中,使用StrSafe或Safe
CRT中对应的安全函数来替代被禁用的危险的API调用。
安全编码实践之五地址空间格局随机化ASLR
1. 概述
在前面安全编码实践中我们介绍过GS编译选项和缓存溢出,以及数据保护DEP。首先,缓存溢出的直接后果就是可能导致恶意代码的远程执行,于是编译器提供了GS保护。但是,GS选项有自身的局限,存在若干方法可以绕过GS选项的保护。于是进一步,操作系统提供了数据执行保护,即DEP,以及与之对应的NXCOMPAT编译选项。
那么是不是现在我们就可以高枕无忧了?在安全领域中,系统的攻防是一个不断发展进化的过程。DEP提出后,就出现了针对DEP的Ret2libc攻击手段。这一点我们曾在介绍DEP的安全编码实践文章的最后简单提及过。
ASLR(Address Space Layout Randomization),地址空间格局的随机化,就是用来防范Ret2libc攻击手段的另一个重要的安全特性。那么,什么是Ret2libc攻击,ASLR的原理是什么,开发人员如何使用这个安全特性,就是我们这篇文章要探讨的内容。
2. DEP和Ret2libc攻击
2.1 DEP对堆栈溢出的保护
在栈溢出介绍中提及到Windows体系结构下函数堆栈布局(地址从高向低)如下:
调用参数 |
返回地址 |
EBP上层函数堆栈基址 |
异常处理代码入口地址
(如果函数设置异常处理) |
局部变量 |
表1:Windows系统的函数堆栈结构
如果发生堆栈溢出,恶意代码通过覆盖在堆栈(stack)上的局部变量,从而修改函数的返回地址,而导致恶意代码执行。下面是这类攻击方式的堆栈结构的一个典型例子。
调用参数 |
覆盖方向—> |
恶意代码 |
返回地址 |
恶意代码的入口地址 |
EBP上层函数堆栈基址 |
溢出的变量覆盖区域,往往包括必要的填充字节 |
异常处理代码入口地址
(如果函数设置异常处理) |
局部变量 |
表2:堆栈溢出时的堆栈结构
当DEP保护机制被使用后,由于恶意代码是存放在系统的数据页面(堆栈页面上),那么函数返回时,指令寄存器EIP将跳转到恶意代码的入口地址。此时该页面是非可执行的(non-executable),于是DEP就会触发系统异常而导致程序中止。
2.2 Ret2libc攻击
在上述的DEP保护机制中,可以看到关键是在函数返回时EIP跳转到了非可执行页面时被DEP检测到。那么Ret2libc的攻击原理是,攻击者设定的函数的返回地址并不直接指向恶意代码,而是指向一个已存在的系统函数的入口地址。由于系统函数所在的页面权限是可执行的,这样就不会触发DEP异常。
那么,攻击者应该将EIP控制指向那个特殊的系统入口函数?一个例子是在Unix
系统下,libc是一个共享的C动态执行库,里面有许多非常有用的函数,例如system函数。它的定义如下:
int system(const char *string);
函数system()可通过运行环境来执行其它程序,例如启动Shell等等。那么,攻击者就可以通过构造以下的堆栈结构【1】:
调用参数 |
覆盖方向—> |
/bin/sh |
返回地址 |
虚假的返回地址 |
EBP上层函数堆栈基址 |
system函数的入口地址 |
异常处理代码入口地址
(如果函数设置异常处理) |
溢出的变量覆盖区域,往往包括必要的填充字节 |
局部变量 |
表3:Ret2libc攻击的堆栈结构
这样,当发生堆栈溢出的函数返回时,EIP跳转到system函数。因为system函数本身就是可执行的,这时不会产生DEP异常。攻击者通过构造system函数的调用参数来可以启动其它程序。在攻击过程中,函数返回到libc库(return
to libc)是关键,这也就是Ret2libc名字的来由。
细心的读者也许已经发现,在表3中,没有任何恶意代码被插入。攻击者虽然可以通过system或者其它系统函数来执行很多敏感的操作,但在多数情况下,还是更希望可以执行自身定制的恶意代码。如何可以做到这一点?于是在最初的Ret2libc的攻击方式的基础上,又发展出特别针对Windows系统攻击的手段。它的原理是通过VirtualProtect函数来修改恶意代码所在内存页面的执行权限,然后再将控制转移到恶意代码。
VirtualProtect是Windows系统kernel32.dll提供的函数,其功能是修改调用进程所在虚拟地址空间(virtual
address)的内存区域的保护权限。它的定义如下:
BOOL WINAPI VirtualProtect(
__in LPVOID lpAddress,
__in SIZE_T dwSize,
__in DWORD flNewProtect,
__out PDWORD lpflOldProtect
); |
攻击者构造以下的堆栈结构【2】来调用VirtualProtect:
调用参数 |
覆盖方向—> |
恶意代码 |
lpflOldProtect值 |
设定可执行权限参数 |
恶意代码页面的大小 |
恶意代码所在内存页面的基址 |
恶意代码的入口地址 |
返回地址 |
VirtualProtect函数的入口地址 |
EBP上层函数堆栈基址 |
溢出的变量覆盖区域,往往包括必要的填充字节 |
异常处理代码入口地址
(如果函数设置异常处理) |
局部变量 |
表4:使用VirtualProtect攻击的堆栈结构
首先,当发生堆栈溢出的函数返回时,EIP跳转到VirtualProtect函数。注意到这里攻击者特别构造将恶意代码的入口地址作为VirtualProtect函数退出时的返回地址。由于在VirtualProtect的执行过程中,恶意代码所在的页面被修改为可执行权限,这样当VirtualProtect返回时,EIP再跳转到恶意代码时就不会触发任何DEP异常。
除了使用VirtualProtect函数,攻击者还可以使用其它函数,例如NtSetInformationProcess等等。
3. ASLR和/dynamicbase链接选项
在上面对Ret2libc攻击方式的介绍中,我们看到最为关键的一点是攻击者事先预知了特定函数,如system或VirtualProtect的入口地址。在Windows
XP或Windows 2000上,这些函数的入口地址是固定的,即攻击者事先可以确定的。
在Windows Vista中引入了ASLR安全特性。它的原理就是在当一个应用程序或动态链接库,如kernel32.dll,被加载时,如果其选择了被ASLR保护,那么系统就会将其加载的基址随机设定。这样,攻击者就无法事先预知动态库,如kernel32.dll的基址,也就无法事先确定特定函数,如VirtualProtect,的入口地址了。
ASLR是系统一级的特性。系统动态库,如kernel32.dll,加载地址,是在系统每次启动的时候被随机设定的。
下面是一个简化的ASLR演示程序【3】。
// aslr.cpp : Demo the dynamic base of DLLs due to ASLR
//
#include "stdafx.h"
#include <windows.h>
#include <stdio.h>
void foo( void )
{
printf( "Address of function foo = %p/n", foo );
}
int _tmain(int argc, _TCHAR* argv[])
{
HMODULE hMod = LoadLibrary( L"Kernel32.dll" );
// Note—this is for release builds
HMODULE hModMsVc = LoadLibrary( L"MSVCR90.dll" );
void* pvAddress = GetProcAddress(hMod, "LoadLibraryW");
printf( "Kernel32 loaded at %p/n", hMod );
printf( "Address of LoadLibrary = %p/n", pvAddress );
pvAddress = GetProcAddress( hModMsVc, "system" );
printf( "MSVCR90.dll loaded at %p/n", hModMsVc );
printf( "Address of system function = %p/n", pvAddress );
foo();
if( hMod ) FreeLibrary( hMod );
if( hModMsVc ) FreeLibrary( hModMsVc );
return 0;
} |
这段程序的目的是输出kerner32.dll和msvcr90.dll的基址,loadlibrary和system函数的入口地址,以及应用程序本身一个函数foo()的入口地址。
使用ASLR非常简单。从Visual Studio 2005 SP1开始,增加了/dynamicbase链接选项。/dynamicbase选项可以通过Project
Property -> Configuration Properties -> Linker
-> Advanced -> Randomized Base Address,或直接修改linker的命令行编译选项即可。见下图。
图1:/dynamicbase链接选项配置
在Visual Studio 2008环境,用Win32 Console
Application类型,编译链接演示程序。注意,如果使用Visual Studio 2005 SP1的话,需要将msvcr90.dll更改为msvcr80.dll。
如果程序没有使用ASLR功能的话,在Windows Vista下运行。输出的结果是:
Kernel32 loaded at 763F0000
Address of LoadLibrary = 7641361F
MSVCR90.dll loaded at 671F0000
Address of system function = 6721C88B
Address of function foo = 00401800
重启系统
Kernel32 loaded at 76320000
Address of LoadLibrary = 7634361F
MSVCR90.dll loaded at 6A340000
Address of system function = 6A36C88B
Address of function foo = 00401800 |
我们看到,即使程序本身没有使用ASLR,Kernel32.dll和MSVCR90.dll的加载地址也发生了变化。这是因为这两个库都已经选择了被ASLR保护。但是应用程序自身foo()函数的地址是固定的。
如果程序使用ASLR功能的话,在Windows Vista下运行。输出的结果是:
Kernel32 loaded at 763F0000
Address of LoadLibrary = 7641361F
MSVCR90.dll loaded at 671F0000
Address of system function = 6721C88B
Address of function foo = 003B1800
重启系统
Kernel32 loaded at 76320000
Address of LoadLibrary = 7634361F
MSVCR90.dll loaded at 697A0000
Address of system function = 697CC88B
Address of function foo = 00871800 |
应用程序自身函数foo()的加载地址也随着系统重启发生了变化。即一旦使用了/dynamicbase选项,生成的程序在运行时候就会受到ASLR机制的保护。
4. ASLR的局限
首先,ASLR安全特性只在Windows Vista和其后的Windows版本(如Windows
Server 2008)中实现。
其次,ASLR是需要和DEP配合使用的。如果CPU不提供对于DEP的硬件支持,或者应用程序没有选择被DEP保护的话,恶意代码一旦可以执行,就可以通过程序进程表结构来获得特定DLL的加载基址。
就性能和兼容性而言,ASLR的实现上都做了考虑,没有太多的影响。一个范例是Microsoft
Office 2007。Office 2007的程序全面使用ASLR功能,并没有发现对其性能和兼容性带来太大的影响【4】。
5. 总结
ASLR安全特性在Windows Vista和其后的Windows版本(如Windows
Server 2008)中实现。它可以防范基于Ret2libc方式的针对DEP的攻击。ASLR和DEP配合使用,能有效阻止攻击者在堆栈上运行恶意代码。建议开发人员使用/dynamicbase链接选项让开发的应用程序或动态链接库使用ASLR功能。 |