深入彙編理解緩衝區溢位攻擊
1.基本知識
子彙編程式裡,呼叫函式使用CALL偽指令,原始的傳遞引數的方法可以是使用暫存器和全域性標記(和高階語言,如C中的全域性變數,在.data段定義的標記)。但是由於這樣子函式不能模組化,而且如果程式功能稍大的話,程式碼將非常難於理解和維護,所以後來統一使用棧來管理函式呼叫,包括函式的引數傳遞,返回地址,區域性變數。這樣函式就可以模組化,並且可以寫在另一個檔案中。不過,在Linux核心中,系統呼叫都是使用暫存器傳遞引數的。
2.寫一個簡單的C程式stack.c,如下
------------------------stack.c--------------------------
#include<stdio.h>
void test_func(int,char,char);
int main(){
test_func(1,'B','C');
return 0;
}
void test_func(int a,char b,char c){
int func_a=a;
char func_b=b;
char func_c=c;
}
------------------------------------------------------------
3.編譯這個檔案到組合語言
$gcc -S statk.c
編譯後生成彙編檔案stack.s,我使用的是gcc4.4.1(Ubuntu-9.10(Karmic Koala)),內容如下
--------------------------stack.s----------------------------
.file "stack.c"
.text
.globl main #全域性標記宣告,gcc使用main標記作為程式入口,而gas使用_start標記。
.type main, @function
main:
pushl %ebp
movl %esp, %ebp #規範化的函式呼叫開頭:首先儲存棧頂指標
andl $-16, %esp #Intel推薦16位元組對齊,為了更快的預取
subl $16, %esp #棧向下增長16位元組,留出新參的記憶體,棧很特別,它從高記憶體向低記憶體增長
movl $66, 8(%esp) #將3個引數入棧,入棧是反序的,引數1即在棧最上端(低地址)
movl $65, 4(%esp)
movl $1, (%esp) #從這裡可以知道,引數的增長方式是從低地址往高地址增長,否則
#這個int引數1就要超過棧頂指標了,從另一方面理解,Intel是小端
#對齊的,所有資料的地址都是低位元組地址,所以其餘位元組都放在高記憶體
call test_func #呼叫函式,call偽指令會把返回地址入棧
movl $0, %eax #從這裡算起要修改3行,否則使用gas,ld編譯連結後的程式最後找不到出口,會發生段錯誤,但是並不影響我們。
leave
ret
.size main, .-main
.globl test_func
.type test_func, @function
test_func:
pushl %ebp
movl %esp, %ebp
subl $24, %esp #在前面棧已經16位元組對齊,所以這裡不需要了
movl 12(%ebp), %edx #將引數'B'放入EDX暫存器,這裡為什麼會取得'A'將在後面用圖
#來表示
movl 16(%ebp), %eax #將引數'C'放入EAX暫存器
movb %dl, -20(%ebp) #'B'是字元,所以只取一個位元組,暫存
movb %al, -24(%ebp) #同上
movl 8(%ebp), %eax #取引數1,放入EAX暫存器
movl %eax, -8(%ebp) #給func_a賦值
movzbl -20(%ebp), %eax #擴充套件傳送,表示將一個位元組傳送到暫存器EAX,EAX高24位補0
movb %al, -1(%ebp) #賦值給func_b
movzbl -24(%ebp), %eax
movb %al, -2(%ebp) #賦值給func_c
leave
ret
.size test_func, .-test_func
.ident "GCC: (Ubuntu 4.4.1-4ubuntu8) 4.4.1"
.section .note.GNU-stack,"",@progbits
-------------------------------------------------------------------------
3.修改程式
為了便於除錯,我們將stack.s檔案修改為gas格式,也就是把main換成_start即可,可以使用vi的替換功能方便的完成,並且還要修改__main的退出程式碼,見註釋。修改以後的檔案如下:
----------------------------------stack.s------------------------------
.file "stack.c"
.text
.globl _start
.type _start, @function
_start:
pushl %ebp
movl %esp, %ebp
andl $-16, %esp
subl $16, %esp
movl $67, 8(%esp)
movl $65, 4(%esp)
movl $1, (%esp)
call test_func
pushl $0 #這兩行是修改後的
call exit
.size _start, .-_start
.globl test_func
.type test_func, @function
test_func:
pushl %ebp
movl %esp, %ebp
subl $24, %esp
movl 12(%ebp), %edx
movl 16(%ebp), %eax
movb %dl, -20(%ebp)
movb %al, -24(%ebp)
movl 8(%ebp), %eax
movl %eax, -8(%ebp)
movzbl -20(%ebp), %eax
movb %al, -1(%ebp)
movzbl -24(%ebp), %eax
movb %al, -2(%ebp)
leave
ret
.size test_func, .-test_func
.ident "GCC: (Ubuntu 4.4.1-4ubuntu8) 4.4.1"
.section .note.GNU-stack,"",@progbits
-------------------------------------------------------------------------
使用如下命令生成可以除錯的程式
$as -o stack.o stack.s -gstabs
$ld -dynamic-linker /lib/ld-linux.so.2 -o stack stack.o -lc
解釋:as即gas彙編器,-gstabs引數生成可供gdb除錯的資訊;
ld是聯結器,由於使用了C庫,使用-l引數指定庫libc,由於需要動態連線庫,所以還要指定動態連線程式,使用引數-dynamic-linker來指定。
4.除錯程式
$gdb stack
gdb的使用方法不再詳細介紹,下面檢視一些關鍵的結果。使用一個圖來表示棧,如下(由於不能上傳圖片,見相簿:Ubuntu
5.緩衝區溢位
上面的程式,只是用來詳細介紹函式呼叫過程中,堆疊的變化,緩衝區溢位攻擊的原理就是使用超出緩衝區的字元來填充緩衝區,這樣就有可能覆蓋棧中存放的函式返回地址,覆蓋以後,函式返回時就不能執行原來的程式,而是惡意攻擊者安排的程式碼,為什麼會覆蓋?關鍵是函式區域性變數的增長方式是從低地址向高地址增長,而這時候,call偽指令已經把函式的返回地址入棧了,所以它位於區域性變數的高階,當局部變數錯誤的增長時,就可能覆蓋返回地址。下面舉例
-----------------------------overflow.c--------------------------------
#include<stdio.h>
void func();
int main(){
func();
return 0;
}
void func(){
char input[4];
gets(input);
}
-------------------------------------------------------------------------
main函式中呼叫了一個有名的會發生緩衝區溢位的函式gets,所以不用我們自己去編寫一個函式,使用如下命令
$gcc -S overflow.c //生成彙編檔案overflow.s,修改彙編檔案中main為_start以及程式出口,修改後如下
---------------------------------overflow.s-----------------------------
.file "overflow.c"
.text
.globl _start
.type _start, @function
_start:
pushl %ebp
movl %esp, %ebp
andl $-16, %esp
call func
movl $0, %eax
movl %ebp, %esp
popl %ebp
pushl $0
call exit
.size _start, .-_start
.globl func
.type func, @function
func:
pushl %ebp
movl %esp, %ebp
subl $40, %esp
leal -12(%ebp), %eax #leal表示將源運算元地址傳送到目的,由於gets函式的引數是地址
movl %eax, (%esp) #將引數(一個地址)放入棧中,前面說過,所有呼叫函式的引數都必須倒序入棧。
call gets #呼叫函式gets
leave
ret
.size func, .-func
.ident "GCC: (Ubuntu 4.4.1-4ubuntu8) 4.4.1"
.section .note.GNU-stack,"",@progbits
-----------------------------------------------------------------------
$as -o overflow.o overflow.s -gstabs //編譯彙編檔案到目標檔案
$ld -dynamic-linker /lib/ld-linux.so.2 -o overflow overflow.o -lc
執行完上述命令以後,就可以使用gdb來除錯程式overflow了,在連結檔案的時候,會提示gets很危險,不應該使用它。
6.實驗結果
下面使用gdb來除錯這個程式,gdb的具體使用方法就不介紹了,下面是一些截圖,能夠說明棧是怎樣被覆蓋的(由於不能上傳圖片,見相簿Ubuntu)