溢出型漏洞分析

溢出型漏洞

前言:本身剛開始看這一塊,加上本身的C語言基礎並很差,不少地方都是參考的其餘文章,因此可能會有不少錯誤的地方python

1.基於C語言的緩衝區溢出漏洞分析

1.1 緩衝區溢出漏洞介紹

1.1.1 緩衝區溢出漏洞

  • 緩衝區溢出漏洞是指在設計計算機系統軟件或者應用軟件時,採用了一些年代久遠如彙編,C,C++等編程語言,而這些語言在設計指出並無考慮在內存管理上的安全性,十分依賴程序員,因此當採用了不安全的函數(例如C的puts())來接受輸入的數據時,由於沒有考慮到數據的長度的合法性,可能會形成數據超過原本的應有長度,從而覆蓋掉後面的數據,以後程序讀取後面的數據時便會發生各類錯誤,引起風險

1.1.2 漏洞產生背景

  • 以C語言爲例,在C語言設計之初,由於計算機的硬件資源十分有限,所以自動化的內存管理(如Java,Python的垃圾回收機制)和內存檢查(如數組的邊界檢查)的實現是不現實的,須要程序員手動進行內存的管理,這在當時並無什麼不妥,並且當時C語言設計人員也是着重考慮功能的加強,也沒有考慮安全性問題,因此就有了各類安全性漏洞的產生,其中比較多的就是緩衝區溢出漏洞

1.2 C語言中內存的劃分

1.2.1 內存劃分

內存劃分示意圖程序員

(1)代碼區(text segment)算法

  • 存放CPU執行的機器指令(machine instructions),代碼區指令根據程序設計流程依次執行,能夠經過跳轉指令來實現其餘函數代碼的執行。
  • 一般,代碼區是可共享的(即另外的執行程序能夠調用它),由於對於頻繁被執行的程序,只須要在內存中有一份代碼便可。代碼區一般是隻讀的,使其只讀的緣由是防止程序意外地修改了它的指令。另外,代碼區還規劃了局部變量的相關信息

(2)全局初始化數據區/靜態數據區(Data Segment)編程

  • 數據段一般是指用來存放程序中已初始化的全局變量的一塊內存區域。數據段屬於靜態內存分配。
  • 數據段中的靜態數據區存放的是程序中已初始化的全局變量靜態變量常量
    • char* s = "ABC"存儲在常量區,所以只讀不可改;char[] s = "ABC",存儲在棧,所以是可改的
    • const修飾的全局變量也爲常量

(3)未初始化數據區 (Block Started by Symbol,BSS)數組

  • 一般是指用來存放程序中未初始化的全局變量的一塊內存區域,屬於靜態內存分配,即程序一開始就將其清零或者被賦爲NULL。通常在初始化時BSS段部分將會清零

(4)堆區安全

  • 堆是用於存放進程運行中被動態分配的內存段,它的大小並不固定,可動態擴張或縮減
  • 當進程調用malloc 等函數分配內存時,新分配的內存就被動態添加到堆上(堆被擴張);當利用free 等函數釋放內存時,被釋放的內存從堆中被剔除(堆被縮減)
  • 在將應用程序加載到內存空間執行時,操做系統負責代碼段、數據段和BSS段的加載,並將在內存中爲這些段分配空間。棧段亦由操做系統分配和管理,而不須要程序員顯示地管理;堆段由程序員本身管理,即顯式地申請和釋放空間

(5)棧區數據結構

  • 該區存放函數的參數值局部變量的值等,以及在進行任務切換時存放當前任務的上下文內容。其操做方式相似於數據結構中的棧。每當一個函數被調用,該函數返回地址和一些關於調用的信息,好比某些寄存器的內容,被存儲到棧區。從這個意義上講,咱們能夠把堆棧當作一個寄存、交換臨時數據的內存區
  • 棧的分配由系統進行,因此效率要比靠依據函數庫算法來分配的堆效率要高
  • 棧向低字節方向增加

1.2.2 進行內存劃分的意義

(1)意義編程語言

  • 一個進程在運行過程當中,代碼是根據流程依次執行的,只須要訪問一次,固然跳轉和遞歸有可能使代碼執行屢次,而數據通常都須要訪問屢次,所以單獨開闢空間以方便訪問和節約空間。編輯器

  • 臨時數據及須要再次使用的代碼在運行時放入棧區中,生命週期短。函數

  • 局數據和靜態數據有可能在整個程序執行過程當中都須要訪問,所以單獨存儲管理。

  • 堆區由用戶自由分配,以便管理

C語言內存分配參考

內存管理基礎+經常使用內存管理函數使用


2 溢出漏洞

2.1 棧溢出漏洞

2.1.1 棧溢出漏洞

  • 棧溢出指的是程序向棧中某個變量中寫入的字節數超過了這個變量自己所申請的字節數,於是致使與其相鄰的棧中的變量的值被覆蓋
  • 棧溢出漏洞輕則可使程序崩潰,重則可使攻擊者控制程序執行流程
  • 發生棧溢出的基本前提是
    • 程序必須向棧上寫入數據
    • 寫入的數據大小沒有被良好地控制

2.1.2 棧幀 stack frame

(1)棧幀包括

  • 函數的返回地址和參數
  • 臨時變量: 包括函數的非靜態局部變量以及編譯器自動生成的其餘臨時變量‘
  • 函數調用的上下文-寄存器

(2)重要的寄存器

  • CPU的寄存器保存的是指向其所須要的信息的內存地址
    • EBP:基址寄存器,指向棧底
      • ebp用來存儲當前函數狀態的基地址,在函數運行時不變,能夠用來索引肯定函數參數或局部變量的位置
    • ESP:棧頂寄存器,指向棧頂
      • esp 用來存儲函數調用棧的棧頂地址,在壓棧和退棧時發生變化
    • EIP:程序計數器,指向的地址的值保存着下一條要進行的指令
      • cpu 依照eip的存儲內容讀取指令並執行eip隨之指向相鄰的下一條指令,如此反覆,程序就得以連續執行指令

(3)操做棧的經常使用指令

  • push:壓棧,

    • PUSH指令會對ESP/RSP/SP寄存器的值進行減法運算,使之減去4字節(32位)或8字節(64位),而後將操做數寫到上述寄存器裏的指針所指向的內存中
  • pop:彈棧

    • POP指令是PUSH指令的逆操做:它先從棧指針指向的內存中讀取數據,用以備用(一般是寫到其餘寄存器裏),而後再將棧指針的數值加上4字節或8字節

(4)函數調用過程

  • main函數調用fun,稱main函數爲caller,被調用函數fun稱爲callee

    • 在壓棧的過程當中,esp 寄存器的值不斷減少(對應於棧從內存高地址向低地址生長)。壓入棧內的數據包括調用參數返回地址調用函數的基地址,以及局部變量
      • 其中調用參數之外的數據共同構成了被調用函數(callee)的狀態。
    • 在發生調用時,程序還會將被調用函數(callee)的指令地址存到eip寄存器內,這樣程序就能夠依次執行被調用函數的指令了
    1. 首先將callee函數的參數逆序壓入棧,(若是被調用函數calle不須要參數,則沒有這一步驟
    2. 將被調用的函數callee壓入棧後,將調用函數caller進行調用以後的下一條指令地址做爲返回地址壓入棧內(即壓入calle結束後須要執行的指令,以便告訴CPU這個函數調用完成以後該幹什麼,本例即返回到main函數的return處),這樣調用函數(caller)eip(指令)信息得以保存
    3. 將當前ebp寄存器中的值(也就是調用函數的基地址)壓入棧內,並ebp寄存器的值更新爲當前棧頂的地址(即caller的esp地址)
      • 這樣這樣調用函數callerebp(基地址)信息得以保存。同時,ebp 被更新爲被調用函數callee的基地址(將當前棧頂地址傳到ebp寄存器內)
    4. esp的值減去一個字節數目值,實現esp向低字節移動
    5. 以後將被調用函數callee的局部變量等數據壓入棧內
    6. 開始執行eip的指向的內存地址中的指令
  • 當被調用函數callee完成以後,須要丟棄被調用函數callee的狀態,並將棧頂恢復爲調用函數caller的狀態

    1. 首先被調用函數的局部變量會從棧內直接彈出,棧頂會指向被調用函數callee的基地址

    2. 而後將基地址內存存儲的調用函數caller的基地址從棧內彈出,並存儲到ebp寄存器內

      • 這樣調用函數callerebp(基地址)信息得以恢復。此時棧頂會指向返回地址(即esp寄存器的值更新爲被調用函數callee執行時的ebp的值)
    3. 再將返回地址從棧內彈出,並存到eip寄存器內。這樣調用函數callereip(指令)信息得以恢復。

    4. 至此caller的函數狀態就所有恢復了,以後就是繼續執行調用函數的指令

函數調用過程參考

2.1.3 利用棧溢出漏洞

(1)利用棧溢出覆蓋函數的局部變量數據值

  • C語言中的gets()從標準輸入設備讀字符串函數,其能夠無限讀取,不會判斷上限,因此會形成溢出,因此能夠利用這個漏洞來實現程序的數據以及流程的改變

  • 如下是一個鍵盤輸入與內置的局部變量的值的判斷,而後決定是否執行特定程序的程序

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    
    void fun() {
        char password[6] = "ABCDE";
        char str[6];
        gets(str);
        str[5] = '\0';
        if (strcmp(str, password) == 0){ //比較stryupassword的值是否相同
            printf("開始執行python!\n");
        	system("python"); //開始啓動python程序
        }
        else{
            printf("NO!\n");
        }
    }
    
    int main() {
        fun();
        return 0;
    }
    • 首先在fun()處設置一個斷點,表示下一步要進入fun()函數內,此時:

      • EIP = 0x006E18B0

      • ESP = 0x003EF850

      • EBP = 0x003EF920

      • fun函數開頭的彙編指令:

        void fun() {
        006E18B0  push        ebp  
        006E18B1  mov         ebp,esp  
        006E18B3  sub         esp,0E0h
        1. 可看出EIP寄存器地址存儲的指令就是將ebp寄存器內容(相應的地址)利用push指令壓入棧
        2. mov指令將esp寄存器的內容複製到ebp
        3. sub指令將esp寄存器的內容額外減去0x0e0h,即esp低地址移動0x0e0h個字節
    • 當咱們的fun函數執行到gets(str)時,查看變量的地址以及內存的值

      變量的內存位置
      attack	    0x006e1840   {StackOverflow.exe!attack(...)} void (...)
      fun	        0x006e18b0   {StackOverflow.exe!fun(...)}	 void (...)
      str	        0x00cffacc   "燙燙燙...	                   char[0x00000006]
      password	0x00cffadc   "ABCDE"	                     char[0x00000006]
      內存視圖
      0x00CFFACC [cc cc cc cc cc cc]cc cc  ????????
      0x00CFFAD4  cc cc cc cc cc cc cc cc  ????????
      0x00CFFADC [41 42 43 44 45 00]cc cc  ABCDE.??
      0x00CFFAE4  cc cc cc cc bc fb cf 00  ???????.
      • 0x00CFFADC開始即可以看出連續的6個字節對應的就是"ABCDE",即password的值,最後的是'\0'爲結束符
      • 0x00CFFACC對應的爲str的起始位置
    • 因爲puts函數不會限制輸入數據的長度,因此咱們能夠經過輸入特定字符在覆蓋掉password

      • 這裏因爲str是從0x00CFFACC開始的咱們要輸如21個字節(從str開始到45),咱們連續輸入21個A
    • 輸入完畢後,運行到if語句時,再次查看內存

      內存視圖
      0x00CFFACC [41 41 41 41 41 00]41 41  AAAAA.AA
      0x00CFFAD4  41 41 41 41 41 41 41 41  AAAAAAAA
      0x00CFFADC [41 41 41 41 41 00]cc cc  AAAAA.??
      0x00CFFAE4  cc cc cc cc bc fb cf 00  ???????.
      • 此時password的值已經被覆蓋爲爲"AAAAA",與str的值相同,故能夠經過strcmp校驗
    • 結果

    • 結束後,此時EIP內容爲main函數以後的指令地址,繼續執行main函數,程序完成

  • 注:實際上可能因編譯器不一樣,環境不一樣,其str與password內存位置差距也不一樣,須要自行判斷

(2)利用棧溢出漏洞覆蓋參數值

  • 再來看一個,經過寫入字符數據來覆蓋整形key,使得該key值與口令相等

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    
    
    void fun(int key) {
        char buffer[5];
        puts(buffer);
        if (key == 0x41424344) { //對應的char爲a,b,c,d
            printf("開始執行python!\n");
            system("python");
        }
        else {
            printf("NO!\n");
        }
    }
    
    int main()
    {
        fun(0x45464748);
        return 0;
    }
    • 和以前相同,觀察&password以及&key的值

      內存視圖
      0x00D3F790 [a8 f7 d3 00]06 c0 6c 00   ???..?l.
      0x00D3F798  f0 f7 d3 00 0d 1a 6c 00     ???...l.
      0x00D3F7A0 [48 47 46 45]52 13 6c 00   HGFER.l.
          
      變量內存位置
      buffer	    0x00D3F790
      &key	    0x00D3F7A0
    • key位於參數位置,因此經過寫入20個字節數據覆蓋掉key這個參數的值就能夠了,讓key的新值爲0x41424344(即輸入字符的最後四個字符爲DBCA)就能夠了

      • 由於整形數值爲大段存儲因此須要倒過來
  • 輸入16個字符+DCBA,結果:

    • 注意由於順便修改了EBP和返回地址,因此函數執行完以後便會img,若是不想崩潰的話,須要將該字段的值保持和原來相同,即將第9-16個字符設爲相應的ascll碼對應的字符(該處不少沒有對應的字符,因此不可能和原來相同,若是是經過讀取文本的方式來輸入key,能夠用16進制編輯器來編輯相應的文本數據內容)

(3)利用棧溢出漏洞修改返回地址,實現函數的跳轉

  • 因爲EBP後的四個字節爲返回地址,即函數執行完以後的下一條執行的指令地址,能夠經過修改該字段來實現執行特定的函數

  • 代碼以下

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    
    void attack() {
        printf("Attacked!\n");
        system("python");
        exit(0);
    }
    
    void fun() {
        char password[6] = "ABCDE";
        char str[6];
        FILE* fp;
        if (!(fp = fopen("H:\\SaftyTest\\StackOverflow\\password.txt", "r"))) {
            exit(0);
        }
        fscanf(fp, "%s", str);
    
        str[5] = '\0';
        if (strcmp(str, password) == 0)
            printf("OK!\n");
        else
            printf("NO!\n");
    }
    
    int main(){
        fun();
        return 0;
    }
    • 當執行到打開文本函數時,內存信息以下:

      內存視圖
      0x00F7F6C8 [00 10 d4 00 e4 f6]f7 00   ..?.???.
      0x00F7F6D0 [41 42 43 44 45 00]a2 00   ABCDE.?.
      0x00F7F6D8 [2c f7 f7 00|88 17 a2 00]  ,??.?.?.
          
      變量視圖
      attack	  0x00a21880   {StackOverflow.exe!attack(...)} 	void (...)
      fun	      0x00a218e0   {StackOverflow.exe!fun(...)}	    void (...)
      str       0x00f7f6c8   ""	                            char[0x00000006]
      password  0x00f7f6d0   "ABCDE"	                        char[0x00000006]
    • 此時attack函數地址爲0x00a21880,因此須要讀入數據使得EBP後的返回地址RET覆蓋爲0x00a21880

    • 修改password.txt的文本爲以下內容,並保存:

      41414141414141414141414141414141414141418018A200
      • 共24字節,最後四位爲0x8018a200

      • 注:該內容爲16進制內容,實際打開文本看到的內容可能爲:

        AAAAAAAAAAAAAAAAAAAA€?
      • 這裏並無考慮兩個字符數組比較的問題,若是想要顯示OK,只需前str與password的相同位後的值改成‘/0’便可(對應文本的16進制內容爲00)

    • 繼續執行程序,內存信息以下:

      0x00F7F6C8  41 41 41 41 41 00 41 41   AAAAA.AA
      0x00F7F6D0 [41 41 41 41]41 41 41 41   AAAAAAAA
      0x00F7F6D8 [41 41 41 41|80 18 a2 00]  AAAA€.?.
    • 程序結果:

  • 因爲本環境下C語言程序中採用大端存儲,即數據的低字節在內存中的字節地址更高,因此最後將幾個數據字節順序倒置

  • 本例子運行完python後直接退出,若是不直接退出的話會發生如(2)同樣的結果,緣由也相同

待續。。。

相關文章
相關標籤/搜索