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 |
虚假的返回地址 |
||
返回地址 |
system函数的入口地址 |
|
EBP上层函数堆栈基址 |
溢出的变量覆盖区域,往往包括必要的填充字节 |
|
异常处理代码入口地址 (如果函数设置异常处理) |
||
局部变量 |
表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
#include
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的命令行编译选项即可。
在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功能。
6. 参考文献
【1】 Bypassing non-executable-stack during exploitation using return-to-libc,http://www.infosecwriters.com/text_resources/pdf/return-to-libc.pdf,c0ntex
【2】 A Brief History of Exploitation Techniques & Mitigations on Windows, http://hick.org/~mmiller/presentations/misc/exploitation_techniques_and_mitigations_on_windows.pdf, Matt Miller
【3】 Writing Secure Code for Windows Vista, Michael Howard, David LeBlanc
【4】 Use of ASLR, NX, etc,http://blogs.msdn.com/david_leblanc/archive/2008/03/14/use-of-aslr-nx-etc.aspx,David LeBlanc
from:http://blog.csdn.net/chengyun_chu/archive/2009/10/09/4644227.aspx