]在一個例子中,洗牌進程中斷工作進程運行的步驟如下:1>洗牌進程通過調(diào)用發(fā)送信號函數(shù)send_signal_to_stop向工作進程發(fā)送信號后(line2)循環(huán)等待工作進程被中斷(line3) ;2>工作進程在接收到信號后會立即進入信號處理函數(shù):首先保存需要通信的信息內(nèi)容(linel-linM),然后循環(huán)等待(line5) ;3>洗牌進程在確認工作進程已經(jīng)中斷執(zhí)行并且保存相應(yīng)信息后,開始進行狀態(tài)迀移(line4)。圖3的(b)部分示例性地給出了 step4,即工作進程被中斷時內(nèi)存布局。通過圖3的工作進程(b)部分,可以觀察到ucontext結(jié)構(gòu)體已經(jīng)存放在棧上,其中(RIP寄存器,也就是PC寄存器的值,本文中有時也簡稱為PC)記錄了被中斷指令的地址是Addr4。
[0056]Step5:完成工作進程新舊版本代碼的迀移。眾所周知,內(nèi)存中存在大量的代碼指針(code pointer),對所有的代碼指針都進行同步是非常困難的。發(fā)明人將內(nèi)存中的代碼指針存儲的數(shù)據(jù)分為兩類:函數(shù)入口地址(GOT表和虛函數(shù)表等)和函數(shù)內(nèi)地址(棧上返回地址等)。對于第一種代碼指針,可以在函數(shù)的起始位置插入跳轉(zhuǎn)指令指向隨機后的代碼位置,這樣新舊版本切換時就不需要同步此類代碼指針。對于棧上返回地址,其同步方法是:將工作進程的棧共享給洗牌進程,然后由洗牌進程完成返回地址的同步。由于工作進程始終在運行,它的棧會隨時發(fā)生變化,也就意味著會時刻向棧上壓返回地址。為了解決這個問題,利用Linux的信號機制讓洗牌進程給工作進程發(fā)信號,中斷工作進程的運行。為了不終止工作進程的運行,在工作進程中注冊信號處理函數(shù),工作進程在接收到信號后會進入信號處理函數(shù),然后在信號處理函數(shù)中循環(huán)等待。通過這種方法可以保證棧不再發(fā)生變化,洗牌進程有機會同步棧上的返回地址。信號機制解決了棧返回地址同步的同時,還能夠解決了 PC寄存器的同步問題(將舊版本的PC切換到新版本代碼處)。當進程接收到信號后,操作系統(tǒng)會向該進程的棧上存儲被中斷指令處的所有寄存器狀態(tài)。如果在信號處理函數(shù)中修改這些寄存器狀態(tài),那么在信號處理函數(shù)返回后OS會將最新寄存器狀態(tài)存回寄存器中。利用這一機制,可以通過修改PC寄存器在棧上的值達到同步PC的目的。由于工作進程的棧是共享給洗牌進程的,因此洗牌進程可以通過修改棧上的PC寄存器的值完成新舊版本PC的切換。
[0057]—個實施例中,工作進程新舊版本代碼的迀移包括三步操作:1>同步棧上所有的返回地址:洗牌進程通過IPC得到工作進程棧頂位置,然后對棧上的所有返回地址進行同步(通過查找之前記錄的map信息,將當前返回地址修正為隨機后的地址);2>同步工作進程的PC:通過IPC讀取ucontext獲得RIP寄存器(PC寄存器)的值。然后查找map信息得到隨機后指令的地址,然后將新地址寫回;3>在函數(shù)入口處插入跳轉(zhuǎn)指令,使其指向隨機后函數(shù)的位置。由于信號處理函數(shù)也會被隨機并且工作進程此時的PC在該函數(shù)第5行循環(huán)執(zhí)行。為了確保工作進程不會崩潰,在信號處理函數(shù)入口處插入跳轉(zhuǎn)指令時會確保被修改的字節(jié)數(shù)小于第5行循環(huán)代碼與函數(shù)入口的距離(保證不會破壞line5以后的代碼)。圖3的(c)部分示例性地給出了 step 5,即工作進程完成新舊版本代碼的迀移后的內(nèi)存布局。參考圖3的(c)部分顯示棧上的返回地址此時已經(jīng)從地址Addrf同步到地址AddrS,PC也從地址Addr4同步到地址Addr6,原始函數(shù)的起始位置也已經(jīng)插入跳轉(zhuǎn)指令指向隨機化處理后的新版本代碼位置。
[0058]Step6:喚醒工作進程。在一個實施例中,洗牌進程通過調(diào)用圖4中的wake_up函數(shù)喚醒工作進程。具體方法是將flag置O (Iinel),然后循環(huán)等待工作進程切換到新版本代碼(line2)。當flag變?yōu)镺后,工作進程會跳出信號處理函數(shù)中的循環(huán),最后返回去執(zhí)行sigreturn系統(tǒng)調(diào)用(返回中斷指令處重新執(zhí)行)。由于洗牌進程在Step5進行了狀態(tài)迀移處理(信號處理函數(shù)的返回地址已經(jīng)同步到新地址),因此信號處理函數(shù)signal_handler會返回到新版本的sigreturn相關(guān)指令中。在生成新版本代碼的sigreturn相關(guān)指令時在其開頭處插入兩條指令將flag置位I用來通知洗牌進程,工作進程已經(jīng)運行到新版本代碼中(如圖4所示)。洗牌進程在確認工作進程已經(jīng)開始執(zhí)行新版本代碼后,開始抹掉之前舊版本的代碼。抹掉的方式是通過插入非法指令進行抹除。圖3的(d)部分示例性地給出了step 6,即工作進程被喚醒后的內(nèi)存布局。從圖3的(d)部分中可以觀察到在信號處理函數(shù)結(jié)束運行后,工作進程開始執(zhí)行新版本代碼。此時工作進程的舊代碼也已經(jīng)同步進行抹除。
[0059]Step7:洗牌進程隔一段時間后,開始再次重復(fù)step3、step4,step5和step6。新版本代碼還是存儲在代碼緩存中。與第一次隨機不同的是對于返回地址和PC的同步:由于每次生成隨機代碼都是通過之前反匯編記錄的中間表示生成的,所以map信息里只記錄了最初版本(隨機之前代碼)指令地址與隨機后地址的映射關(guān)系。再次隨機時洗牌進程里就會有新舊版本的map信息。同步返回地址和PC時,需要將新舊版本map信息進行對比,然后得到舊版本指令對應(yīng)新版本的地址。需要注意的是在洗牌進程發(fā)送信號中斷工作進程運行時,工作進程會進入代碼緩存中舊版本的信號處理函數(shù)代碼,而不再是原始代碼。
[0060]進一步地,在一個優(yōu)選實施例中,提出了一種共享工作進程的所有代碼段和棧的方法,該方法可用于所述St印I中。
[0061]應(yīng)用程序在加載階段,加載器會把主程序和其依賴的庫加載到內(nèi)存空間中并分配棧和堆空間,加載和分配的方法是使用“mmap”系統(tǒng)調(diào)用并以MAP_PRIVATE作為其中一個參數(shù)。Linux操作系統(tǒng)提供給用戶的“_ap”系統(tǒng)調(diào)用同時還支持共享內(nèi)存的實現(xiàn),但是需要使用“MAP_SHARED”參數(shù)。本實施例提出了一個非常簡單的解決方法,即修改加載器,將其中的“mmap”都改為使用MAP_SHARED。
[0062]為了獲得更好的移植性,本實施例不修改操作系統(tǒng)中的任何庫。但是這同樣會帶來一個實現(xiàn)上的問題:在執(zhí)行被預(yù)裝載進工作進程的共享庫文件時,所有的代碼和棧已經(jīng)被加載分配到內(nèi)存中但是不能進行共享內(nèi)存。如果要將代碼和棧共享給洗牌進程,就需要無效當前內(nèi)存中需要共享的區(qū)域然后重新調(diào)用mmap系統(tǒng)調(diào)用并使用MAP_SHARED作為參數(shù)。但是這樣會影響到正在進行共享工作代碼的正常執(zhí)行。為了解決這個問題,本實施例中使用一個臨時共享庫文件(記為ShareTmpSO)來輔助共享共享庫文件(即SharedSO)的代碼。臨時共享庫文件由共享庫文件通過dlopen加載進來,臨時共享庫文件會通過讀取“/proc/self/maps”文件獲取當前進程的內(nèi)存布局,然后保存內(nèi)存中除了臨時共享庫文件以外的所有代碼段和棧的數(shù)據(jù),在無效這些代碼段和棧空間后,再次調(diào)用“_ap”系統(tǒng)調(diào)用進行共享,最后將數(shù)據(jù)復(fù)原。臨時共享庫文件中的代碼不需要調(diào)用外部庫函數(shù)并且會通過內(nèi)嵌匯編切換到自己全局數(shù)據(jù)區(qū)的棧進行執(zhí)行(因此不會因為無效棧區(qū)域?qū)е屡R時共享庫文件的代碼不能正常運行)。當共享代碼執(zhí)行完后,共享庫文件會自動卸載臨時共享庫文件,此時共享工作進程的所有代碼段和棧的過程結(jié)束。
[0063]進一步地,在另一個優(yōu)選實施例中,還提供了一種隱藏新生成的代碼的方法,它可以用于前文中的step5中。在step5中,需要在函數(shù)的入口處插入跳轉(zhuǎn)指令指向隨機化處理后的新版本代碼位置,并且新代碼在運行時會向棧上壓最新代碼的返回地址。這些信息都可能會泄露新版本代碼的位置,使攻擊者能夠根據(jù)代碼指針(GOT表,虛函數(shù)表和棧上返回地址等)讀取新版本代碼。
[0064]本優(yōu)選實施例在不改變原有功能的前提下,利用x86的gs段寄存器的支持來隱藏新生成的代碼。由于目前主程序和庫里都沒有使用gs段寄存器,因此我們對該寄存器的使用不影響原有程序的運行。gs段寄存器里存儲的是GDT表(全局描述符表)的一個索引,具體指向的地址保存在GDT表中,攻擊者是無法讀取到GDT表中的內(nèi)容。由于目前主程序和庫代碼都沒有使用gs段寄存器,因此內(nèi)存中不存在使用gs段寄存器進行訪存的對齊指令。對于非對齊的指令,可以在做代碼隨機時通過插入一些填充(即padding)來有效消除掉。有了 gs段寄存器的支持,就可以把所有需要隱藏的地址存入地址表(Address Table)中,然后用gs段寄存器指向地址表的基址。地址表存放的位置為圖2中所示的地址表區(qū)域。
[0065]圖5示出了本實施例中采用地址表來隱藏新版本代碼的示意圖。在函數(shù)funcA的入口處,插入的不再是相對跳轉(zhuǎn)的指令,而是jmpq% gs:1ndex指令。gs段寄存器指向的是地址表的首地址,然后通過索引index進行檢索。為了隱藏棧上的返回地址,本實施例的方案中,在新生成的代碼中還將call和ret指令進行了修改,使棧上存放的是地址表的索引index(如圖5代碼緩存中的代碼所示