本文來源:長亭技術專欄
作者:Jwizard

0x00 寫在最前面

_開場白:_快報快報!今天是2017 Pwn2Own黑客大賽的第一天,長亭安全研究實驗室在比賽中攻破Linux操作系統和Safari瀏覽器(突破沙箱且拿到系統最高權限),積分14分,在11支隊伍中暫居 Master of Pwn 第一名。作為熱愛技術樂于分享的技術團隊,我們開辦了這個專欄,傳播普及計算機安全的“黑魔法”,也會不時披露長亭安全實驗室的最新研究成果。

安全領域博大精深,很多童鞋都感興趣卻苦于難以入門,不要緊,我們會從最基礎的內容開始,循序漸進地講給大家。技術長路漫漫,我們攜手一起出發吧。

0x10 本期簡介

在計算機安全領域,緩沖區溢出是個古老而經典的話題。眾所周知,計算機程序的運行依賴于函數調用棧。棧溢出是指在棧內寫入超出長度限制的數據,從而破壞程序運行甚至獲得系統控制權的攻擊手段。本文將以32位x86架構下的程序為例講解棧溢出的技術詳情。

為了實現棧溢出,要滿足兩個條件。第一,程序要有向棧內寫入數據的行為;第二,程序并不限制寫入數據的長度。歷史上第一例被廣泛注意的“莫里斯蠕蟲”病毒就是利用C語言標準庫的 gets() 函數并未限制輸入數據長度的漏洞,從而實現了棧溢出。

Fig 1. 波士頓科學博物館保存的存有莫里斯蠕蟲源代碼的磁盤(source: Wikipedia

如果想用棧溢出來執行攻擊指令,就要在溢出數據內包含攻擊指令的內容或地址,并且要將程序控制權交給該指令。攻擊指令可以是自定義的指令片段,也可以利用系統內已有的函數及指令。

0x20 背景知識

在介紹如何實現溢出攻擊之前,讓我們先簡單溫習一下函數調用棧的相關知識。

函數調用棧是指程序運行時內存一段連續的區域,用來保存函數運行時的狀態信息,包括函數參數與局部變量等。稱之為“棧”是因為發生函數調用時,調用函數(caller)的狀態被保存在棧內,被調用函數(callee)的狀態被壓入調用棧的棧頂;在函數調用結束時,棧頂的函數(callee)狀態被彈出,棧頂恢復到調用函數(caller)的狀態。函數調用棧在內存中從高地址向低地址生長,所以棧頂對應的內存地址在壓棧時變小,退棧時變大。

Fig 2. 函數調用發生和結束時調用棧的變化

函數狀態主要涉及三個寄存器--esp,ebp,eip。esp 用來存儲函數調用棧的棧頂地址,在壓棧和退棧時發生變化。ebp 用來存儲當前函數狀態的基地址,在函數運行時不變,可以用來索引確定函數參數或局部變量的位置。eip 用來存儲即將執行的程序指令的地址,cpu 依照 eip 的存儲內容讀取指令并執行,eip 隨之指向相鄰的下一條指令,如此反復,程序就得以連續執行指令。

下面讓我們來看看發生函數調用時,棧頂函數狀態以及上述寄存器的變化。變化的核心任務是將調用函數(caller)的狀態保存起來,同時創建被調用函數(callee)的狀態。

首先將被調用函數(callee)的參數按照逆序依次壓入棧內。如果被調用函數(callee)不需要參數,則沒有這一步驟。這些參數仍會保存在調用函數(caller)的函數狀態內,之后壓入棧內的數據都會作為被調用函數(callee)的函數狀態來保存。

Fig 3. 將被調用函數的參數壓入棧內

然后將調用函數(caller)進行調用之后的下一條指令地址作為返回地址壓入棧內。這樣調用函數(caller)的 eip(指令)信息得以保存。

Fig 4. 將被調用函數的返回地址壓入棧內

再將當前的ebp 寄存器的值(也就是調用函數的基地址)壓入棧內,并將 ebp 寄存器的值更新為當前棧頂的地址。這樣調用函數(caller)的 ebp(基地址)信息得以保存。同時,ebp 被更新為被調用函數(callee)的基地址。

Fig 5. 將調用函數的基地址(ebp)壓入棧內,并將當前棧頂地址傳到 ebp 寄存器內

再之后是將被調用函數(callee)的局部變量等數據壓入棧內。

Fig 6. 將被調用函數的局部變量壓入棧內

在壓棧的過程中,esp 寄存器的值不斷減小(對應于棧從內存高地址向低地址生長)。壓入棧內的數據包括調用參數、返回地址、調用函數的基地址,以及局部變量,其中調用參數以外的數據共同構成了被調用函數(callee)的狀態。在發生調用時,程序還會將被調用函數(callee)的指令地址存到 eip 寄存器內,這樣程序就可以依次執行被調用函數的指令了。

看過了函數調用發生時的情況,就不難理解函數調用結束時的變化。變化的核心任務是丟棄被調用函數(callee)的狀態,并將棧頂恢復為調用函數(caller)的狀態。

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

Fig 7. 將被調用函數的局部變量彈出棧外

然后將基地址內存儲的調用函數(caller)的基地址從棧內彈出,并存到 ebp 寄存器內。這樣調用函數(caller)的 ebp(基地址)信息得以恢復。此時棧頂會指向返回地址。

Fig 8. 將調用函數(caller)的基地址(ebp)彈出棧外,并存到 ebp 寄存器內

再將返回地址從棧內彈出,并存到 eip 寄存器內。這樣調用函數(caller)的 eip(指令)信息得以恢復。

Fig 9. 將被調用函數的返回地址彈出棧外,并存到 eip 寄存器內

至此調用函數(caller)的函數狀態就全部恢復了,之后就是繼續執行調用函數的指令了。

0x30 技術清單

介紹完背景知識,就可以繼續回歸棧溢出攻擊的主題了。當函數正在執行內部指令的過程中我們無法拿到程序的控制權,只有在發生函數調用或者結束函數調用時,程序的控制權會在函數狀態之間發生跳轉,這時才可以通過修改函數狀態來實現攻擊。而控制程序執行指令最關鍵的寄存器就是 eip(還記得 eip 的用途嗎?),所以我們的目標就是讓 eip 載入攻擊指令的地址。

先來看看函數調用結束時,如果要讓 eip 指向攻擊指令,需要哪些準備?首先,在退棧過程中,返回地址會被傳給 eip,所以我們只需要讓溢出數據用攻擊指令的地址來覆蓋返回地址就可以了。其次,我們可以在溢出數據內包含一段攻擊指令,也可以在內存其他位置尋找可用的攻擊指令。

Fig 10. 核心目的是用攻擊指令的地址來覆蓋返回地址

再來看看函數調用發生時,如果要讓 eip 指向攻擊指令,需要哪些準備?這時,eip 會指向原程序中某個指定的函數,我們沒法通過改寫返回地址來控制了,不過我們可以“偷梁換柱”--將原本指定的函數在調用時替換為其他函數。

所以這篇文章會覆蓋到的技術大概可以總結為(括號內英文是所用技術的簡稱):

  • 修改返回地址,讓其指向溢出數據中的一段指令(shellcode

  • 修改返回地址,讓其指向內存中已有的某個函數(return2libc

  • 修改返回地址,讓其指向內存中已有的一段指令(ROP

  • 修改某個被調用函數的地址,讓其指向另一個函數(hijack GOT

本篇文章會覆蓋前兩項技術,后兩項會在下篇繼續介紹。(所以請點擊“關注專欄”持續關注我們吧 ^_^ )

0x40 Shellcode

--修改返回地址,讓其指向溢出數據中的一段指令

根據上面副標題的說明,要完成的任務包括:在溢出數據內包含一段攻擊指令,用攻擊指令的起始地址覆蓋掉返回地址。攻擊指令一般都是用來打開 shell,從而可以獲得當前進程的控制權,所以這類指令片段也被成為“shellcode”。shellcode 可以用匯編語言來寫再轉成對應的機器碼,也可以上網搜索直接復制粘貼,這里就不再贅述。下面我們先寫出溢出數據的組成,再確定對應的各部分填充進去。

payload : padding1 + address of shellcode + padding2 + shellcode

Fig 11. 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 了。

Fig 12. shellcode 所用溢出數據的最終構造

看起來并不復雜對吧?但這種方法生效的一個前提是在函數調用棧上的數據(shellcode)要有可執行的權限(另一個前提是上面提到的關閉內存布局隨機化)。很多時候操作系統會關閉函數調用棧的可執行權限,這樣 shellcode 的方法就失效了,不過我們還可以嘗試使用內存里已有的指令或函數,畢竟這些部分本來就是可執行的,所以不會受上述執行權限的限制。這就包括 return2libc 和 ROP 兩種方法。

0x50 Return2libc

--修改返回地址,讓其指向內存中已有的某個函數

根據上面副標題的說明,要完成的任務包括:在內存中確定某個函數的地址,并用其覆蓋掉返回地址。由于 libc 動態鏈接庫中的函數被廣泛使用,所以有很大概率可以在內存中找到該動態庫。同時由于該庫包含了一些系統級的函數(例如 system() 等),所以通常使用這些系統級函數來獲得當前進程的控制權。鑒于要執行的函數可能需要參數,比如調用 system() 函數打開 shell 的完整形式為 system(“/bin/sh”) ,所以溢出數據也要包括必要的參數。下面就以執行 system(“/bin/sh”) 為例,先寫出溢出數據的組成,再確定對應的各部分填充進去。

payload: padding1 + address of system() + padding2 + address of “/bin/sh”

Fig 13. return2libc 所用溢出數據的構造

padding1 處的數據可以隨意填充(注意不要包含 “\x00” ,否則向程序傳入溢出數據時會造成截斷),長度應該剛好覆蓋函數的基地址。address of system() 是 system() 在內存中的地址,用來覆蓋返回地址。padding2 處的數據長度為4(32位機),對應調用 system() 時的返回地址。因為我們在這里只需要打開 shell 就可以,并不關心從 shell 退出之后的行為,所以 padding2 的內容可以隨意填充。address of “/bin/sh” 是字符串 “/bin/sh” 在內存中的地址,作為傳給 system() 的參數。

根據上面的構造,我們要解決個問題。

1. 返回地址之前的填充數據(padding1)應該多長?

解決方法和 shellcode 中提到的答案一樣。

2. system() 函數地址應該是多少?

要回答這個問題,就要看看程序是如何調用動態鏈接庫中的函數的。當函數被動態鏈接至程序中,程序在運行時首先確定動態鏈接庫在內存的起始地址,再加上函數在動態庫中的相對偏移量,最終得到函數在內存的絕對地址。說到確定動態庫的內存地址,就要回顧一下 shellcode 中提到的內存布局隨機化(ASLR),這項技術也會將動態庫加載的起始地址做隨機化處理。所以,如果操作系統打開了 ASLR,程序每次運行時動態庫的起始地址都會變化,也就無從確定庫內函數的絕對地址。在 ASLR 被關閉的前提下,我們可以通過調試工具在運行程序過程中直接查看 system() 的地址,也可以查看動態庫在內存的起始地址,再在動態庫內查看函數的相對偏移位置,通過計算得到函數的絕對地址。

最后,“/bin/sh” 的地址在哪里?

可以在動態庫里搜索這個字符串,如果存在,就可以按照動態庫起始地址+相對偏移來確定其絕對地址。如果在動態庫里找不到,可以將這個字符串加到環境變量里,再通過 getenv() 等函數來確定地址。

解決完上述問題,我們就可以拼接出溢出數據,輸入至程序來通過 system() 打開 shell 了。

0x60 半途小結

小結一下,本篇文章介紹了棧溢出的原理和兩種執行方法,兩種方法都是通過覆蓋返回地址來執行輸入的指令片段(shellcode)或者動態庫中的函數(return2libc)。需要指出的是,這兩種方法都需要操作系統關閉內存布局隨機化(ASLR),而且 shellcode 還需要程序調用棧有可執行權限。下篇會繼續介紹另外兩種執行方法,其中有可以繞過內存布局隨機化(ASLR)的方法,敬請關注。

0x70 號外

給大家推薦幾個可以練習安全技術的網站:

Pwnhub ( pwnhub | Beta ):長亭出品,題目豐富,積分排名機制,還可以兌換獎品,快來一起玩耍吧!

Pwnable.kr ( http://pwnable.kr ):有不同難度的題目,內容涵蓋多個領域,界面很可愛

Pwnable.twPwnable.tw ):由臺灣CTF愛好者組織的練習平臺,質量較高

Exploit Exercises ( https://exploit-exercises.com ):有比較完善的題目難度分級,還有虛擬機鏡像供下載

最后,放出一張長亭戰隊在PWN2OWN的比賽精彩瞬間,No Pwn No Fun ! 也祝長亭戰隊再創佳績!

References:


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/271/