buffer overflow
基本的匯編語言
MOV EAX, EBX : 把 EBX 中存盤的內容傳給 EAX
ADD EAX, EBX : 把 EAX 和 EBX相加,最終存到第一個變數 EAX 中
PUSH EAX : 入堆疊操作,ESP = ESP -4, 然后把EAX放進ESP中
POP EAX : 出堆疊操作,MOV EAX, [ESP]; ESP=ESP+4
CALL func : PUSH EIP; JMP func
RET : return操作,將EIP出堆疊

記憶體布局

A typical memory representation of a C program consists of the following sections.
- Text segment (i.e. instructions)
- Initialized data segment
- Uninitialized data segment (bss)
- Heap
- Stack
text segment
文本段,也稱為代碼段或簡稱為文本,是目標檔案或記憶體中程式的一部分,其中包含可執行指令,
作為記憶體區域,文本段可以放在堆或堆疊的下面,以防止堆和堆疊溢位覆寫它,
通常,文本段是可共享的,因此對于頻繁執行的程式,例如文本編輯器、C 編譯器、shell 等,記憶體中只需要一個副本,此外,文本段通常是只讀的,以防止程式意外修改其指令,
initialized data segment
初始化資料段,通常簡稱為Data Segment,資料段是程式虛擬地址空間的一部分,其中包含由程式員初始化的全域變數和靜態變數,
請注意,資料段不是只讀的,因為變數的值可以在運行時更改,該段又可以分為初始化只讀區和初始化讀寫區,
比如C語言中char s[] = “hello world”定義的全域字串和main(即global)外的int debug=1這樣的C陳述句,都會存放在初始化的讀寫區中,而像const char* string = “hello world”這樣的全域C陳述句使得字串文字“hello world”存盤在初始化的只讀區,字符指標變數string存盤在初始化的讀寫區,
例如:static int i = 10 將存盤在資料段中,global int i = 10 也將存盤在資料段中
Uninitialized data segment
未初始化的資料段通常稱為bss段,以古老的匯編運算子命名,代表“由符號開始的塊”,在程式開始執行之前,該段中的資料由內核初始化為算術 0 未初始化的資料從資料段的末尾開始,包含所有初始化為零或在源代碼中沒有顯式初始化的全域變數和靜態變數,
例如,宣告為 static int i 的變數;將包含在 BSS 段中,
例如,宣告為 int j 的全域變數;將包含在 BSS 段中,
heap
堆疊區域傳統上與堆區域相鄰,并且向相反的方向增長;當堆疊指標遇到堆指標時,可用記憶體耗盡,(使用現代大地址空間和虛擬記憶體技術,它們幾乎可以放置在任何地方,但它們通常仍然以相反
的方向增長,)堆疊區域包含程式堆疊,一種 LIFO 結構,通常位于記憶體的較高部分,在標準 PC x86 計算機體系結構上,它向地址零增長;在其他一些架構上,它會朝相反的方向增長,“堆疊指標”暫存器跟蹤堆疊的頂部;每次將值push到堆疊時都會對其進行調整,為一個函式呼叫推送的一組值稱為“堆疊框架”;堆疊幀至少包含一個回傳地址,
堆疊,其中存盤自動變數,以及每次呼叫函式時保存的資訊,每次呼叫函式時,回傳的地址和呼叫者環境的某些資訊,例如一些機器暫存器,都保存在堆疊中,新呼叫的函式然后在堆疊上為其自動變數分配空間,這就是 C 中遞回函式的作業原理,每次遞回函式呼叫自身時,都會使用一個新的堆疊幀,因此一組變數不會干擾來自該函式另一個實體的變數,
stack
堆是通常發生動態記憶體分配的段,
堆區域從 BSS 段的末尾開始,并從那里增長到更大的地址,Heap區由malloc、realloc和free管理,可以使用brk和sbrk系統呼叫來調整其大小(注意使用brk/sbrk和單個“堆區”不需要履行合約malloc/realloc/free;它們也可以使用 mmap 來實作,以將虛擬記憶體的潛在非連續區域保留到行程的虛擬地址空間中),Heap 區域由行程中的所有共享庫和動態加載的模塊共享,
記憶體空間
用戶空間
用戶空間行程由作業系統中的用戶執行,而不是作業系統本身的一部分,它也可能由初始化系統(例如 systemd)執行,但它不是內核的一部分, 用戶空間 是非內核應用程式在其中運行的記憶體區域,用戶空間行程實際上在記憶體的用戶空間部分中運行,用戶空間行程以 用戶模式運行,這是執行行程指令的非特權執行模式,當用戶模式行程想要使用內核提供的服務(例如磁盤 I/O、網路訪問)時,他們必須切換到內核模式,切換到內核模式涉及觸發內核執行的系統呼叫,下面將更詳細地描述此機制,
用戶運行行程的用戶模式執行可確保用戶空間行程無法訪問或修改內核管理的記憶體,也不會干擾其他行程的執行,這是確保用戶運行的行程不會破壞或干擾作業系統的重要安全控制,
用戶空間是用戶行程運行的系統記憶體部分,這與內核空間形成對比,內核空間是內核執行和提供服務的記憶體部分,
存盤器的內容由專用RAM(隨機存取存盤器)VLSI(超大規模集成電路)半導體芯片組成,可以以極高的速度訪問(即讀取和寫入)但只能暫時保留(即,同時在使用中,或者至多在電源保持打開狀態時),這與存盤器(例如,磁盤驅動器)形成對比,存盤器的訪問速度慢得多,但其內容在電源關閉后仍會保留,并且通常具有大得多的容量,
行程是程式的 執行(即運行)實體,用戶行程是內核以外的所有程式的實體(即實用程式和應用程式),當一個程式要運行時,它被從存盤空間復制到用戶空間,這樣它就可以被 CPU(中央處理器)高速訪問,
內核是構成計算機作業系統中央核心的程式,它不是行程,而是行程的控制器,它對系統上發生的一切都有完全的控制,這包括管理用戶空間內的各個用戶行程并防止它們相互干擾,
類Unix作業系統將系統記憶體劃分為用戶空間和內核空間,對于維護系統的穩定性和安全性具有重要作用,
下圖顯示了用戶空間行程如何依賴內核來訪問硬體,以及它們如何通過系統呼叫(或系統呼叫)介面訪問它,然而,內核本身不僅僅是一個用于低級操作的系統呼叫 API,除了促進與用戶運行行程的介面外,內核還包含行程調度程式、網路堆疊、虛擬檔案系統和用于硬體支持的設備驅動程式,僅舉幾例,

內核空間
內核空間是為內核保留的系統記憶體區域,它是內核運行和執行內核模式指令的地方,內核模式是內核的 CPU 執行模式,它以特權、root 訪問模式運行,當用戶空間應用程式需要內核提供的服務時,它會通知內核執行系統呼叫,并在系統呼叫執行期間切換到內核模式,
Linux中的系統記憶體可以分為兩個不同的區域:內核空間和用戶空間,內核空間是內核(即作業系統的核心)執行(即運行)并提供其服務的地方,記憶體由RAM(隨機存取存盤器)單元組成,其內容可以以極高的速度訪問(即讀取和寫入),但只能暫時保留(即在使用時或至多在電源保持打開時) ). 它的目的是保存當前正在使用的程式和資料,從而充當 CPU(中央處理器)和速度慢得多的存盤器(通常由一個或多個硬碟驅動器 (HDD) 組成)之間的高速中介,用戶空間是一組記憶體位置,用戶行程(即內核以外的所有內容)在其中運行,行程是程式的執行實體,內核的作用之一是管理這個空間內的各個用戶行程,并防止它們相互干擾,用戶行程只能通過使用系統呼叫來訪問內核空間,系統呼叫是類 Unix 作業系統中活動行程對內核執行的服務的請求,例如輸入/輸出(I/O) 或行程創建,活動行程是當前在 CPU 中進行的行程,與正在等待 CPU 中的下一個回合的行程形成對比,I/O 是將資料傳入或傳出 CPU 以及外圍設備(例如磁盤驅動器、鍵盤、滑鼠和列印機)或從其傳出的任何程式、操作或設備,
用戶空間和內核空間的邊界
用戶空間行程使用特殊的 CPU 指令來呼叫大多數現代 CPU 架構上的系統呼叫,用戶空間行程在想要執行系統呼叫時執行 CPU 指令,這會將行程的執行從用戶模式切換到內核模式,系統呼叫在內核模式下執行,然后回傳到用戶空間行程執行,
用戶和內核空間的典型實作是在用戶行程和內核之間共享虛擬地址空間,
在這種情況下,內核空間位于地址空間的頂部,而用戶空間位于底部,為了防止用戶行程訪問內核空間,內核創建映射以防止從用戶模式訪問內核空間,


stack frame
Stack是應用程式記憶體中的一個段,用于存盤區域變數,函式的函式呼叫,每當我們的程式中有一個函式呼叫時,區域變數和其他函式呼叫或子程式的記憶體就會存盤在堆疊幀中,每個函式在應用程式記憶體的堆疊段中獲得自己的堆疊幀,
特征 :
- 堆疊中為函式呼叫分配的記憶體僅在函式執行時存在,一旦函式完成,我們就無法訪問該函式的變數,
- 一旦呼叫函式完成其執行,其堆疊幀將被洗掉,被呼叫函式的執行執行緒將從它離開的位置恢復,
- 堆疊用于存盤函式呼叫,因此當我們在程式中使用大量遞回呼叫時,堆疊記憶體會被函式呼叫或子例程耗盡,這可能會導致堆疊溢位,因為堆疊記憶體是有限的,
- 每個堆疊幀都維護堆疊指標 (SP) 和幀指標 (FP),堆疊指標和幀指標總是指向堆疊頂,它還維護一個指向下一條要執行的指令的程式計數器(PC),
- 每當進行函式呼叫時,都會在堆疊段中創建堆疊幀,呼叫函式提供的引數會在被呼叫函式的堆疊幀中獲得一些記憶體,并將它們壓入被呼叫函式的堆疊幀中,當它們的執行完成時,它們會從堆疊幀中彈出,并且執行執行緒在被呼叫函式中繼續,
堆疊幀結構:
堆疊指標始終指向堆疊頂,幀指標存放子程式整個堆疊幀的地址,子程式或函式的每個堆疊幀包含如下內容,

EBP:基指標(或幀指標),指向堆疊底部(高地址)
- 這是一個固定位置,通過EBP +- offset偏移來查找引數和變數
ESP:堆疊指標,指向堆疊頂部(低位地址)
- 當發生入堆疊或者出堆疊時進行移位


-
push ebp:esp = esp - 4+mov [esp] ebp每個函式都以ebp入堆疊開始執行 -
mov ebp, esp:
-
sub esp, 16:
-
mov DWORD PTR [ebp-8],2
-
push DWORD PTR [ebp-8]
-

-
push ebp:存下當前位置的ebp位置
-
mov ebp esp:然后進行新的函式的運行,運行結束方便找回return的位置 -
sub esp,2048:開辟了512*4=2048位元組的陣列空間
-
mov edx, DWORD PTR [ebp+8]
-
leave:add函式結束,要回傳主函式就需要剛剛舊的ebp的位置,把這個位置還給esp,ebp就回到了原來的主函式下面的位置,然后將之前的ebp(old)退堆疊
-
ret:回傳剛才的地址,并將存盤這一地址的空間釋放,同時也將之前存放變數值的空間釋放
-
mov DWORD PTR [ebp-4],eax:把eax中存盤的結果放到three對應的位置
-
mov eax DWORD PTR [ebp-4]:這一步是將回傳結果存進去eax
呼叫約定
當高級語言函式被編譯成機器碼時, 有一個問題就必須解決:因為CPU沒有辦法知道一個函式呼叫需要多少個、什么樣的引數. 即計算機不知道怎么給這個函式傳遞引數, 傳遞引數的作業必須由函式呼叫者和函式本身來協調. 為此, 計算機提供了一種被稱為堆疊的資料結構來支持引數傳遞.
函式呼叫時, 呼叫者依次把引數壓堆疊, 然后呼叫函式, 函式被呼叫以后, 在堆疊中取得資料, 并進行計算. 函式計算結束以后, 或者呼叫者、或者函式本身修改堆疊, 使堆疊恢復原裝. 在引數傳遞中, 有兩個很重要的問題必須得到明確說明:
- 當引數個數多于一個時, 按照什么順序把引數壓入堆疊
- 函式呼叫后, 由誰來把堆疊恢復原裝
- 函式的回傳值放在什么地方
在高級語言中, 通過函式呼叫規范(Calling Conventions)來說明這兩個問題. 常見的呼叫規范有:
- stdcall
- cdecl
- fastcall
- thiscall
- naked call
stdcall
stdcall 很多時候被稱為pascal呼叫規范, 因為pascal是早期很常見的一種教學用計算機程式設計語言, 其語法嚴謹, 使用的函式呼叫約定是stdcall. 在Microsoft C++系列的C/C++編譯器中, 常常用PASCAL宏來宣告這個呼叫約定, 類似的宏還有WINAPI和CALLBACK.
stdcall呼叫規范宣告的語法為:
int __stdcall function(int a, int b)
stdcall的呼叫約定意味著:
- 引數
從右向左壓入堆疊 - 函式自身修改堆疊
- 函式的裝飾名(decoration name/mangling name)為函式名自動加前導的下劃線, 后面緊跟一個@符號, 其后緊跟著引數的尺寸
引數b首先被壓堆疊, 然后是引數a, 函式呼叫function(1, 2)呼叫處翻譯成匯編語言將變成:
push 2; 第二個引數入堆疊
push 1; 第一個引數入堆疊
call function; 呼叫函式, 注意此時自動把cs:eip入堆疊而對于函式自身, 則可以翻譯為:
push ebp; 保存ebp暫存器, 該暫存器將用來保存堆疊的堆疊頂指標, 可以在函式退出時恢復
mov ebp, esp; 保存堆疊頂指標
mov eax, [ebp + 8H]; 堆疊中ebp指向位置之前依次保存有ebp, cs:eip, a, b, ebp +8指向a
add eax, [ebp + 0CH]; 堆疊中ebp + 12處保存了b mov esp, ebp; 恢復esp
pop ebp;
ret 8;
cdecl
cdecl呼叫約定又稱為C呼叫約定, 是C語言預設的呼叫約定, 它的定義語法是:
int function(int a, int b) //不加修飾默認就是C呼叫約定
int __cdecl function(int a, int b) //明確指定C呼叫約定
cdecl呼叫約定的引數壓堆疊順序是和stdcall是一樣的, 引數首先由有向左壓入堆疊. 所不同的是, 函式本身不清理堆疊, 呼叫者負責清理堆疊. 由于這種變化, C呼叫約定允許函式的引數的個數是不固定的, 這也是C語言的一大特色.

溢位攻擊
程式記憶體堆疊:

函式呼叫鏈的堆疊布局:

漏洞1
int main(int argc, char **argv){
char str[400];
FILE *badfile;
badfile = fopen("badfile","r");
fread(str,sizeof(char),300,badfile);
foo(str); //使用str作為引數呼叫foo函式
return 1;
}
漏洞2


在linux系統下運行以下命令:
關閉地址隨機化(對策)
[root@CentOS_7 repos]# sysctl -w kernel.randomize_va_space=0
kernel.randomize_va_space = 0
編譯stack.c:
[root@CentOS_7 repos]# gcc -o stack -z execstack -fno-stack-protector stack.c
[root@CentOS_7 repos]# chown root stack
[root@CentOS_7 repos]# chmod 4755 stack
現在首要任務就是查找緩沖區底部和回傳地址之間的偏移距離以及查找放置外殼代碼的地址

任務一:緩沖區基址和回傳地址之間的距離
[root@CentOS_7 repos]# gcc -z execstack -fno-stack-protector -g -o stack_dbg stack.c
[root@CentOS_7 repos]# touch badfile
[root@CentOS_7 repos]# gdb stack_dbg

任務二:惡意代碼地址
惡意代碼被寫入badfile中,badfile作為引數傳遞給易受攻擊的函式,


為了增加跳轉到惡意代碼的正確地址的機會,我們可以用NOP指令填充惡意檔案,并將惡意代碼放在緩沖區的末尾,

badfile的結構:



本文來自博客園,作者:ivanlee717,轉載請注明原文鏈接:https://www.cnblogs.com/ivanlee717/p/16897583.html
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/535208.html
標籤:其他
上一篇:Tomcat的概述、部署、及優化
