pwn入門:sploitfun——典型的基於堆疊的緩衝區溢位詳解
本文是基於sploitfun系列教程的詳細解析,sploitfun對於純新手而言,其中有些東西還是不夠詳細,新手不能很好的接觸到其中原理,故作此文進行補充
虛擬機器環境:Ubuntu 14.04(x86)
編譯程式碼第一行,表示關閉ASLR(地址空間佈局隨機化)。kernel.randomize_va_space堆疊地址隨機初始化,很好理解,就是在每次將程式載入到記憶體時,程序地址空間的堆疊起始地址都不一樣,動態變化,導致猜測或找出地址來執行shellcode 變得非常困難。
編譯程式碼第二行表示,gcc編譯時,關閉DEP和棧保護。
當-f-stack-protector啟用時(CANNARY棧保護),當其檢測到緩衝區溢位時(例如,緩衝區溢位攻擊)時會立即終止正在執行的程式,並提示其檢測到緩衝區存在的溢位的問題。這是gcc編譯器專門為防止緩衝區溢位而採取的保護措施,具體方法是gcc首先在緩衝區被寫入之前在buf的結束地址之後返回地址之前放入隨機的gs驗證碼,並在緩衝區寫入操作結束時檢驗該值。通常緩衝區溢位會從低地址到高地址覆寫記憶體,所以如果要覆寫返回地址,則需要覆寫該gs驗證碼。這樣就可以通過比較寫入前和寫入後gs驗證碼的資料,判斷是否產生溢位。
NX即No-execute(不可執行)的意思,NX(DEP)的基本原理是將資料所在記憶體頁標識為不可執行,當程式溢位成功轉入shellcode時,程式會嘗試在資料頁面上執行指令,此時CPU就會丟擲異常,而不是去執行惡意指令。
工作原理如下圖:
Gcc預設開啟NX選項,如果需要關閉NX選項可以給gcc編譯器新增-z execstack引數
編譯程式碼第三至第五行。更改檔案許可權
chgrp命令,改變檔案或目錄所屬的組。
chown命令,chown將指定檔案的擁有者改為指定的使用者或組。使用者可以是使用者名稱或使用者ID。組可以是組名或組ID。檔案是以空格分開的要改變許可權的檔案列表,支援萬用字元。
Chown +s命令,為了方便普通使用者執行一些特權命令,SUID/SGID程式允許普通使用者以root身份暫時執行該程式,並在執行結束後再恢復身份。chmod +s 就是給某個程式或者教本以suid許可權
上述漏洞程式碼的第【2】行,可能造成緩衝區溢位錯誤。這個bug可能導致任意程式碼執行,因為源緩衝區內容是使用者輸入的!
我們通過覆蓋返回地址,可以實現任意程式碼執行。
先反彙編main函式,disassemble main或者disass main
下面是收集到的棧溢位背景知識:
函式狀態主要涉及三個暫存器--esp,ebp,eip。esp 用來儲存函式呼叫棧的棧頂地址,在壓棧和退棧時發生變化。ebp 用來儲存當前函式狀態的基地址,在函式執行時不變,可以用來索引確定函式引數或區域性變數的位置。eip 用來儲存即將執行的程式指令的地址,cpu 依照 eip 的儲存內容讀取指令並執行,eip 隨之指向相鄰的下一條指令,如此反覆,程式就得以連續執行指令。
下面讓我們來看看發生函式呼叫時,棧頂函式狀態以及上述暫存器的變化。變化的核心任務是將呼叫函式(caller)的狀態儲存起來,同時建立被呼叫函式(callee)的狀態。
首先將被呼叫函式(callee)的引數按照逆序依次壓入棧內。如果被呼叫函式(callee)不需要引數,則沒有這一步驟。這些引數仍會儲存在呼叫函式(caller)的函式狀態內,之後壓入棧內的資料都會作為被呼叫函式(callee)的函式狀態來儲存。
將被呼叫函式的引數壓入棧內
然後將呼叫函式(caller)進行呼叫之後的下一條指令地址作為返回地址壓入棧內。這樣呼叫函式(caller)的 eip(指令)資訊得以儲存。
將被呼叫函式的返回地址壓入棧內
再將當前的ebp 暫存器的值(也就是呼叫函式的基地址)壓入棧內,並將 ebp 暫存器的值更新為當前棧頂的地址。這樣呼叫函式(caller)的 ebp(基地址)資訊得以儲存。同時,ebp 被更新為被呼叫函式(callee)的基地址。
將呼叫函式的基地址(ebp)壓入棧內,
並將當前棧頂地址傳到 ebp 暫存器內
再之後是將被呼叫函式(callee)的區域性變數等資料壓入棧內。
將被呼叫函式的區域性變數壓入棧內
在壓棧的過程中,esp 暫存器的值不斷減小(對應於棧從記憶體高地址向低地址生長)。壓入棧內的資料包括呼叫引數、返回地址、呼叫函式的基地址,以及區域性變數,其中呼叫引數以外的資料共同構成了被呼叫函式(callee)的狀態。在發生呼叫時,程式還會將被呼叫函式(callee)的指令地址存到 eip 暫存器內,這樣程式就可以依次執行被呼叫函式的指令了。
看過了函式呼叫發生時的情況,就不難理解函式呼叫結束時的變化。變化的核心任務是丟棄被呼叫函式(callee)的狀態,並將棧頂恢復為呼叫函式(caller)的狀態。
首先被呼叫函式的區域性變數會從棧內直接彈出,棧頂會指向被呼叫函式(callee)的基地址。
將被呼叫函式的區域性變數彈出棧外
然後將基地址記憶體儲的呼叫函式(caller)的基地址從棧內彈出,並存到 ebp 暫存器內。這樣呼叫函式(caller)的 ebp(基地址)資訊得以恢復。此時棧頂會指向返回地址。
將呼叫函式(caller)的基地址(ebp)彈出棧外,並存到 ebp 暫存器內
再將返回地址從棧內彈出,並存到 eip 暫存器內。這樣呼叫函式(caller)的 eip(指令)資訊得以恢復。
將被呼叫函式的返回地址彈出棧外,並存到 eip 暫存器內
至此呼叫函式(caller)的函式狀態就全部恢復了,之後就是繼續執行呼叫函式的指令了。
如上述介紹,彙編程式碼含義如圖
前七句,為開始的初始化,第八到第十句為strcpy準備引數,第十一句呼叫strcpy函式,第十二到十四句為printf準備引數,第十五句呼叫printf函式,後面就是清理棧和return的收尾3環節
此時棧的分佈大致如圖所示:
測試步驟1:是否可以覆蓋返回地址?
以Python命令執行,輸入300個A,結果如圖,p檢視暫存器,/x以十六進位制,看到指令暫存器已經被AAAA覆蓋,確定覆蓋返回地址是可能的。
接下來,我們要確定Return Address相對於buf ends的偏移量,首先caller’s EBP有0x4個偏移量,但是由於有一個棧平衡操作,所以buf ends和caller’s EBP之間還可能存在對其空間,但是我們不知道具體空間,可以自己填充來一點點嘗試,如下圖
這樣我們獲得了返回地址距目標緩衝區buf的偏移量0x10c, 0x10c=0x100+0x8+0x4,0x100 是 buf 大小, 0x8 是對其空間, 0x4 是 ebp
這裡繼續補充一點 shellcode 的背景知識。
shellcode --修改返回地址,讓其指向溢位資料中的一段指令。
我們要完成的任務包括:在溢位資料內包含一段攻擊指令,用攻擊指令的起始地址覆蓋掉返回地址。攻擊指令一般都是用來開啟 shell ,從而可以獲得當前程序的控制權,所以這類指令片段也被成為 “shellcode” 。 shellcode 可以用匯編語言來寫再轉成對應的機器碼,也可以上網搜尋直接複製貼上,這裡就不再贅述。下面我們先寫出溢位資料的組成,再確定對應的各部分填充進去。
payload : padding1 + address of shellcode + padding2 + shellcode
shellcode 所用溢位資料的構造
padding1 處的資料可以隨意填充(注意如果利用字串程式輸入溢位資料不要包含 “x00” ,否則向程式傳入溢位資料時會造成截斷),長度應該剛好覆蓋函式的基地址。 address of shellcode 是後面 shellcode 起始處的地址,用來覆蓋返回地址。 padding2 處的資料也可以隨意填充,長度可以任意。 shellcode 應該為十六進位制的機器碼格式。
根據上面的構造,我們要解決兩個問題。
1. 返回地址之前的填充資料( padding1 )應該多長?
我們可以用除錯工具(例如 gdb )檢視彙編程式碼來確定這個距離,也可以在執行程式時用不斷增加輸入長度的方法來試探(如果返回地址被無效地址例如 “AAAA” 覆蓋,程式會終止並報錯)。
2. shellcode 起始地址應該是多少?
我們可以在除錯工具裡檢視返回地址的位置(可以檢視 ebp 的內容然後再加 4 ( 32 位機),參見前面關於函式狀態的解釋),可是在除錯工具裡的這個地址和正常執行時並不一致,這是執行時環境變數等因素有所不同造成的。所以這種情況下我們只能得到大致但不確切的 shellcode 起始地址,解決辦法是在 padding2 裡填充若干長度的 “x90” 。這個機器碼對應的指令是 NOP (No Operation) ,也就是告訴 CPU 什麼也不做,然後跳到下一條指令。有了這一段 NOP 的填充,只要返回地址能夠命中這一段中的任意位置,都可以無副作用地跳轉到 shellcode 的起始處,所以這種方法被稱為 NOP Sled (中文含義是 “ 滑雪橇 ” )。這樣我們就可以通過增加 NOP 填充來配合試驗 shellcode 起始地址。
作業系統可以將函式呼叫棧的起始地址設為隨機化(這種技術被稱為記憶體佈局隨機化,即 Address Space Layout Randomization (ASLR) ),這樣程式每次執行時函式返回地址會隨機變化。反之如果作業系統關閉了上述的隨機化(這是技術可以生效的前提),那麼程式每次執行時函式返回地址會是相同的,這樣我們可以通過輸入無效的溢位資料來生成 core 檔案,再通過除錯工具在 core 檔案中找到返回地址的位置,從而確定 shellcode 的起始地址。
解決完上述問題,我們就可以拼接出最終的溢位資料,輸入至程式來執行 shellcode 了。
shellcode 所用溢位資料的最終構造
但這種方法生效的一個前提是在函式呼叫棧上的資料( shellcode )要有可執行的許可權(另一個前提是上面提到的關閉記憶體佈局隨機化)。很多時候作業系統會關閉函式呼叫棧的可執行許可權,這樣 shellcode 的方法就失效了,不過我們還可以嘗試使用記憶體裡已有的指令或函式,畢竟這些部分本來就是可執行的,所以不會受上述執行許可權的限制。這就包括 return2libc 和 ROP 兩種方法。 通過上述介紹我們先來做準備工作。
我們在 esp 上加上一個 N (自己設一個差不多大小的),但是我們的返回地址 =esp+N<NOP 填充長度,因為上述介紹,除錯工具裡的這個地址和正常執行時並不一致,所以我們要填充 NOP>esp+N, 以便我們能順利滑到 shellcode 。
這裡我 N 就隨便取一個 4 。
構造 shellcode 程式碼
完成圖:
這裡給幾處解釋說明
先是 struct.pack(“<I”, num) , < 表示小端序, I 表示無符號整型
詳情見: https://blog.csdn.net/weiwangchao_/article/details/80395941
Shellcode構造方法 ,詳情見: http://blog.nsfocus.net/easy-implement-shellcode-xiangjie/
文中一些背景技術引用:https://zhuanlan.zhihu.com/p/25816426