曾永瑞 李 喆
(北京天融信網絡安全技術有限公司阿爾法實驗室 北京 100085) (zeng_yongrui@topsec.com.cn)
近年來,隨著攻防技術不斷發(fā)展,二進制漏洞相關的資料也越來越多,但國內的環(huán)境導致了目前大家對Windows系統(tǒng)上的二進制漏洞有更多的關注,多數介紹二進制漏洞的書籍資料都是基于Windows平臺的,而對于Linux二進制漏洞的研究的相關資料只能在網絡上一些個人博客、論壇中找到,缺乏有效的總結和整理, 也沒能很好地引起人們的重視.本文的目的就是對Linux平臺上的二進制漏洞利用作一個淺顯的梳理.
本文完成了以下幾個方面的工作:
1) 介紹了目前Linux系統(tǒng)上常見的二進制漏洞類型以及其基本原理.
2) 整理了這些漏洞經典的利用方法、相關的文獻資料;接著,介紹了Linux系統(tǒng)中針對這些利用方法而出現的安全機制;最后,深入介紹了幾個目前繞過這些安全機制的方法.
3) 總結了文中提到的漏洞特點以及共性, 并針對它們的本質特征提出進一步的防護方案.
緩沖區(qū)溢出攻擊的概念最早可以追溯到20世紀70年代,但直到1996年,AlphaOne[1]才首次公開展示了棧緩沖區(qū)溢出漏洞的利用原理,并提出了“shellcode”這一概念.
若程序在接受用戶輸入時,不對輸入數據進行邊界檢查,直接將其保存到棧上,且寫入目標數據結構的數據大小超過為該結構分配的內存大小時,會發(fā)生棧緩沖區(qū)溢出.這將導致棧上相鄰數據的損壞,通常在錯誤觸發(fā)溢出的情況下,會導致程序崩潰.但需要注意的是,棧包含有函數調用的返回地址,在現在主流的各種緩解措施沒有出現之前,攻擊者只需簡單地將可執(zhí)行代碼注入正在運行的程序緩沖區(qū)中,再將返回地址覆蓋為指向注入代碼的地址,就能輕易獲取未經授權系統(tǒng)的訪問權限.
即便到了今天,存在各種緩解措施的情況下,每年緩沖區(qū)溢出漏洞在各種類型漏洞中所占比例一直都是居高不下,所以棧溢出漏洞依然值得警惕.
相對于棧而言,堆內存的管理機制要復雜得多,在各種系統(tǒng)上的實現也不相同,甚至不同平臺中堆內存管理器也有不同的實現,例如dlmalloc,ptmalloc,tcmalloc,jemalloc等等,甚至有些程序還會使用自己創(chuàng)建的內存池來管理內存.
1.2.1堆溢出
堆溢出的原理和棧溢出類似,當程序向堆塊(chunk)中寫入的數據超過了堆塊本身可使用的字節(jié)數時,溢出的數據將會覆蓋到物理相鄰的下一個堆塊.
1999年,w00w00安全小組的Conover[2]首次詳細介紹了堆溢出的原理和利用方式,從這時起,漏洞利用的目標逐漸擴大到了堆上.
1.2.2釋放重引用漏洞
釋放重引用漏洞UAF(use after free)如其字面意思,當一個內存塊被釋放之后,再次使用指向這塊內存的指針所引發(fā)的漏洞.
許多程序中,內存被分配以用于存儲對象實例.這些對象使用完后,程序會釋放分配的內存,以節(jié)省系統(tǒng)資源.然后,需要將指向該對象的指針設為NULL,如果一個指針沒有指向適當類型的有效對象,則稱這類指針為“懸掛指針”(dangling pointer).通常來說導致“懸掛指針”有如下2個原因:
1) 程序利用了已被釋放的C++對象,從而訪問無效的內存位置;
2) 一個程序返回了指向它的本地變量的指針,因為這個變量只在該函數內部有效,一旦函數結束,這個指針將變?yōu)橐粋€無效的指針.
在某些情況下,程序可能會使用指向已被釋放對象的指針.如果發(fā)生這種情況,程序將進入意外的執(zhí)行流程,這可能導致程序崩潰或甚至更危險的后果,有關UAF漏洞的利用方法,將在2.3節(jié)介紹.
1.2.3雙重釋放漏洞
雙重釋放(double free)[3]漏洞本質上可以算是UAF漏洞的一個子集,即對1塊內存釋放2次,這會產生不確定的后果.
雙重釋放漏洞的利用原理也很好理解:堆塊釋放后,物理相鄰的前、后堆塊若為空閑狀態(tài),會進行合并操作,然后利用Unlink機制將該空閑堆塊從(unsorted bin)中取下.如果用戶精心構造的假堆塊被Unlink,很容易導致一次固定地址寫,然后轉換為任意地址讀寫,從而控制程序的執(zhí)行.
C語言中存在3種基本的整型數據類型——short,int和long,這3種類型又可分為有符號(signed)和無符號(unsigned),每種數據類型都有各自的大小范圍[4].但是對有符號類型和無符號類型的區(qū)分只在編譯器層面,底層匯編指令處理的只是二進制數據.當程序中的數據超過其數據類型的范圍,則會造成整數溢出.這種溢出本身不會造成任意代碼執(zhí)行,但卻可能繞過程序的邊界檢查,從而間接地導致?;蚨训木彌_區(qū)溢出,如圖1所示:
圖1 一個整數溢出漏洞的例子
上述代碼第4行中的strlen()的返回類型是size_t(unsigned int),其返回值存儲在unsigned char(范圍是0~255)數據類型中.因此,任何大于unsigned char的表示范圍的值都會導致整數溢出.當用戶輸入的密碼長度在260~264 B之間時,變量passwd_len實際上為4~8,繞過了第5行的邊界檢查,進而導致了一個棧的緩沖區(qū)溢出漏洞.
格式化字符串函數是一種特別的函數,它可以傳入可變數量的參數,并將第1個參數作為格式化字符串,來解析之后的參數.通俗來說,格式化字符串函數就是將計算機內存中表示的數據轉化為人類可讀的字符串格式.幾乎所有的CC++程序都會利用這類函數來輸出信息,或處理字符串.
關于格式化字符串漏洞的原理,可以先看看下面這條語句:
printf(″Color %s, Number %d, Float %4.2f″).
這條語句中沒有為格式化字符串提供參數,程序依然可以編譯成功并正常運行,但會將棧上格式化字符串地址上面的3個變量分別解析為:
1) 其地址對應的字符串;
2) 其內容對應的整型值;
3) 其內容對應的浮點值.
對于2)和3)來說并沒有太大的問題,但是對于1)來說,如果提供了一個不可訪問地址,程序就會因此而崩潰.通過格式化字符串,用戶可以使用%s,%x從棧輸出數據;也可以用%n向任意地址寫入數據,這就造成了內存泄露的問題.表1中列出了常見的幾種格式化字符串函數.
表1 常見輸出格式化字符串函數
競爭條件(race condition)[5]是指多個線程或進程在讀寫1個共享數據時結果依賴于它們執(zhí)行的相對時間.競爭條件發(fā)生在多個進程或者線程讀寫數據時,其最終的結果依賴于多個進程的指令執(zhí)行順序.由于目前的系統(tǒng)中大量采用并發(fā)編程,經常對資源進行共享,往往會產生競爭條件漏洞.而且由于在并發(fā)時,執(zhí)行流的不確定性很大,競爭相對難察覺,并且在漏洞的復現和調試方面會比較困難.這給修復競爭條件漏洞也帶來了不小的困難.
2016年Linux內核內存子系統(tǒng)在處理寫入時復制COW(copy-on-write)產生的競爭條件漏洞,可以說是近年來最有名的競爭條件漏洞.這個被稱為“臟牛”(DirtyCow)的漏洞在發(fā)現時已經存在于Linux內核9年之久,可見競爭條件漏洞的隱蔽性.
堆和棧中的漏洞曾經乃至現在都是二進制漏洞中所占比例最大的,而整數溢出漏洞最終實現利用,也是通過間接轉化為堆棧溢出漏洞.所以本節(jié)主要介紹在各種漏洞緩解措施未普及之前,棧溢出漏洞和堆相關漏洞的經典利用方法.
最古老的棧溢出漏洞利用,只需簡單地將函數的返回地址覆蓋為需要執(zhí)行的shellcode的地址即可,隨著不可執(zhí)行位NX(no-execute)[6]技術的應用,直接向?;蛘叨焉现苯幼⑷雜hellcode的方式難以繼續(xù)發(fā)揮效果.直到2007年,加州大學圣迭戈分校的Shacham[7]在其論文中提出了面向導向編程ROP(return oriented programming)技術,其主要思想是在棧緩沖區(qū)溢出的基礎上,依靠借用程序中多個以返回指令結尾的代碼塊(gadgets),組成1個ROP鏈,它可以實現任何邏輯功能,從而控制程序的執(zhí)行流程,其原理如圖2所示:
圖2 ROP技術原理
ROP技術是圖靈完備的,換句話說,它為攻擊者提供了一種功能齊全的“編程語言”,可以使用它來執(zhí)行任何所需的操作.此外,gadget可以選擇的位置和利用方式也多種多樣,表2中列出了一些Linux下常見的ROP技術:
表2 常見的ROP技術
關于堆漏洞利用的傳統(tǒng)方法,可以參考Phantasmagoria[8]于2005年提出的5種堆漏洞利用方法,他將這些方法命名為“The House of xxx”,具體如下:
1) The House of Prime;
2) The House of Mind;
3) The House of Force;
4) The House of Lore;
5) The House of Spirit.
這幾種堆溢出利用的首要思路就是通過覆蓋物理相鄰的下一堆塊的元數據來破壞堆的數據結構,進而執(zhí)行惡意代碼.然而隨著ASLR等技術的出現,文中所提到的方法在大多數情況下都無法應用,但是現如今又出現了一系列新的名為“The House of xxx”的利用方法,和文獻[8]中所提到的方法已有較大的不同.
關于UAF漏洞的利用,Watchfire的安全研究人員Afek等人[9]在2007年的Black Hat USA大會上,結合一個Microsoft IIS 6服務器的漏洞,完整地講述了UAF漏洞的原理和利用技巧.
UAF最常用的利用方式就是通過申請與重用對象大小相等的堆塊,使惡意數據被分配到已釋放對象的內存位置,從而覆蓋舊對象的內存空間.
了解編譯器實現對象的方式對于能夠使用適當的數據覆蓋原始對象至關重要.以32位程序的UAF漏洞為例,一個C++對象開頭是一個指向虛函數表的指針,如圖3所示.
若將惡意數據的第1個DWORD大小的內容設置為包含一個特殊地址,該地址將取代對象的虛函數表.內存的其余部分布置為將要執(zhí)行的shellcode.當使用call [eax+offset]執(zhí)行虛函數調用時,再將程序的執(zhí)行流程轉移到shellcode中.整個過程的原理如圖4所示.
圖3 對象虛函數的實現方式
圖4 UAF漏洞利用的最常見方式
堆噴射(heap spray)技術的出現最早可以追溯到2001年,奧地利著名安全小組TESO發(fā)布的一個針對BSD telnet服務器攻擊程序中[10],首次利用了堆噴射技術.當時該技術并沒有確切的名稱,其目的也只是為了提供一個穩(wěn)定的存放shellcode的地方,然后在棧溢出劫持返回地址進行跳轉.
隨著后來ASLR技術的出現,這個古老技術又成為了輔助繞過ASLR的絕佳手段.尤其是對于一些能內嵌執(zhí)行腳本的程序,為攻擊者提供了動態(tài)分配內存的途徑,對于不同的程序來說往往有如下這些堆噴射方法:
1) 利用JavaScript進行堆噴射;
2) 利用ActionScript進行堆噴射;
3) 利用HTML5進行堆噴射;
4) 利用VBScript進行堆噴射;
5) 直接將堆噴射數據構造進文件中,例如圖片、視頻等.
3.1.1地址空間布局隨機化
在第2節(jié)的一些漏洞利用方法中,最后都要將程序流程指向內存中已布置好的shellcode的地址,為了防止這些內存損壞漏洞,地址空間布局隨機化(address space layout randomization, ASLR)[11]技術應運而生.
為了防止攻擊者每次都能可靠地跳轉到內存中特定的地址,ASLR隨機排列進程的關鍵數據區(qū)域的地址.
3.1.2不可執(zhí)行位
計算機將指令當成數據的概念,使得匯編語言、編譯器與其他自動編程工具得以實現,但也造成一些缺陷,緩存溢出就是一個典型例子.NX技術的出現,就是為了彌補這一缺陷,用于區(qū)分內存中指令集與數據.任何標記了NX位的區(qū)塊代表僅供存儲數據使用而不是存儲處理器的指令集,處理器不會將此處的數據作為代碼執(zhí)行,這種技術可防止多數的緩存溢出攻擊.
3.2.1位置無關可執(zhí)行程序
前面提到的ASLR是操作系統(tǒng)的功能選項,作用于可執(zhí)行文件裝入內存運行時,因而只能隨機化stack,heap和libraries的基址;而位置無關可執(zhí)行程序(position independent executables, PIE)[12]是編譯器的功能選項(-fPIE),作用于可執(zhí)行文件的編譯過程,其隨機化了可行性文件裝載內存的基址(代碼段,PLT,GOT,數據段等共同的基址).
PIE需要在源代碼級別遵循一套特定的語義,并且需要編譯器的支持.使用了絕對內存地址的指令,會被替換為相對尋址指令.這些間接處理過程可能導致PIE的運行效率下降,但是因為大多數處理器對位置無關可執(zhí)行程序都有很好的支持,使得這一點點效率的下降基本可以忽略.
3.2.2重定位只讀
要理解重定位只讀(relocation read-only, RELRO)[13],首先需要了解ELF文件的動態(tài)鏈接過程.一個程序在引用了某個模塊中的函數時,因為不知道模塊加載位置,需要將相關代碼地址抽出,放在數據段中的全局偏移表(global offset table, GOT)中.
例如調用func函數,假設該函數定義在glibc共享庫中,若要找到該函數的地址,鏈接器需要生成一段額外的代碼,放入代碼段中一個名為過程鏈接表(procedure link table, PLT)的位置,通過這段代碼獲取func函數地址,并完成對它的調用.其過程如圖5所示.
圖5 ELF文件動態(tài)鏈接過程
由于GOT表是可寫的,假設攻擊者得到了一個任意地址寫入的機會,把GOT表中的函數地址覆蓋為shellcode地址,在程序進行調用這個函數時就會執(zhí)行shellcode.而開啟RELRO后,可以在程序解析符號時,將此過程中使用的重要數據結構標記為只讀,從而防止攻擊者修改這些重要結構.
RELRO又可以分為部分重定位只讀(partial RELRO)和完全重定位只讀(full RELRO)2種模式.這2種模式最大的區(qū)別在于,前者GOT表是可寫的,而后者GOT表是只讀的,但是因為Full RELRO嚴重影響了程序運行的性能,所以一般只在重要的程序中才會開啟.
3.2.3Canary
Canary[14]是一種用于緩解緩沖區(qū)溢出的安全機制,這個名稱源于曾經礦井中用來預警瓦斯氣體的金絲雀(Canary),因為金絲雀瓦斯氣體十分敏感,當瓦斯含量超過一定限度,人類還毫無察覺時,金絲雀卻早已毒發(fā)身亡.
同樣的道理,在棧上返回地址之前添加一個Canary值,函數返回時檢查該值有沒有被改變,便可知道是否發(fā)生了溢出.在GCC中,通過-fstack-protector選項和-fstack-protector-all選項以支持該功能.
圖6 TLS結構
以x64平臺為例,Canary是從fs:0x28偏移位置獲取的,fs寄存器用于存放線程局部存儲( thread local storage, TLS)信息,該結構在Glibc中的實現如圖6所示:
Canary值由glibc產生并保存在tcbhead_t結構中,程序返回時,將棧上的Canary與該結構中保存的值比較,當檢查失敗時,執(zhí)行glibc的__stack_chk_fail函數,并終止進程.
3.2.4FORTIFY_SOURCE
該技術源于2004年由RedHat的工程師提交的一個GCC和glibc補丁[15],其目的是提供一種輕量級的緩沖區(qū)溢出和格式化字符串漏洞防護機制 .它可以通過編譯時定義FORTIFY_SOURCE標志來配置,在目前主流的Linux發(fā)行版甚至安卓平臺中都能夠見到它的身影.
對內存拷貝型函數,在用戶調用過程中,FORTIFY_SOURCE將對圖7中幾種行為進行判定:
圖7 FORTIFY_SOURCE的4種判斷方式
1) 正確,不需要在編譯或運行時檢查.
2) 編譯器不知道實際復制進buf中數據的長度,于是將此類不安全的函數替換為相對安全的__memcpy_chk和__strcpy_chk函數,如果發(fā)生溢出,就會調用內置的chk_fail ()函數,如圖8所示:
圖8 glibc-2.24中__memcpy_chk的實現
3) 編譯器在編譯的過程中會檢測到溢出.
4) 在此類調用的方式下,編譯器無從得知緩沖區(qū)的長度,在編譯時不作檢查,運行時無法檢查,這種調用往往可能導致緩沖區(qū)的溢出.GCC中可以通過設置D_FORTIFY_SOURCE選項為1或2這2種不同的級別,前者認為錯,而后者認為安全.
上述4類是FORTIFY_SOURCE對內存拷貝類函數的判斷標準.
此外,FORTIFY_ SOURCE還有針對格式化字符串的保護,glibc中的printf等格式化字符串函數默認可以使用%n參數,并且能夠任意指定格式化串的參數,例如%4$p,在FORTIFY_SOURCE=2的編譯條件下,glibc啟動了FORTIFY_SOURCE的相關保護.glibc在調用printf時,更改為調用__printf_chk函數,如圖9所示:
圖9 glibc-2.24中___printf_chk的實現
3.3.1內核地址空間布局隨機化
內核地址空間布局隨機化(kernel address space layout randomization, KASLR),顧名思義也就是內核地址的隨機化,這項技術將在開機引導時將內核代碼加載到隨機位置,在Linux內核3.14版本中引入了該特性.
3.3.2內核頁表隔離
內核頁表隔離(kernel page-table isolation, KPTI)是Linux內核中的一種安全技術,它將現在被用戶空間和內核空間共享使用的這張表分成2部分,內核空間和用戶空間各自使用1個,從而解決頁表泄露的問題.
3.3.3管理模式訪問保護管理模式執(zhí)行保護
在很多內核漏洞的利用過程中,通常會將內核指針重定向到用戶空間,這種利用方式被稱為ret2usr.為了抵御這種攻擊,管理模式訪問保護(supervisor mode access prevention, SMAP)和管理模式執(zhí)行保護(supervisor mode execution prevention, SMEP)被提出.兩者的作用分別是禁止內核訪問用戶空間的數據、禁止內核執(zhí)行用戶空間的代碼.Linux內核從3.0開始支持SMEP,從3.7開始支持SMAP.
SROP(sigreturn oriented programming)這種方法由阿姆斯特丹自由大學的Bosman等人[16]提出,該技術利用了Linux系統(tǒng)信號處理機制存在一個設計缺陷.和傳統(tǒng)的ROP攻擊相比顯得更加簡單、可靠、可移植.
了解Linux的信號處理機制是使用SROP技術的前提,這里可以參考圖10,簡要地了解一下Linux信號處理機制:
圖10 Linux信號處理機制
當有中斷或異常產生時,內核向進程發(fā)出一個Signal,此時進程被掛起,系統(tǒng)切換到內核態(tài).內核執(zhí)行do_signal()函數,并且最終調用setup_frame()函數,向用戶棧中push一個保存有全部寄存器的值和Signal信息(定義在名為sigcontext的結構中),另外還會push一個sigruturn()系統(tǒng)調用的地址.此時的用戶棧布局如圖11所示.
圖11 setup_frame()調用后用戶棧
其中User Context和Signal Info這2部分被稱為Signal Frame.之后,跳轉到注冊過的Signal handler中處理相應的Signal.Signal handler返回后,內核執(zhí)行sigreturn系統(tǒng)調用,為該進程恢復之前保存的上下文,其中包括將所有壓入的寄存器,重新pop回對應的寄存器,最后恢復進程的執(zhí)行.
觀察上面信號處理的流程,可以發(fā)現主要的變動都在Signal Frame中.而Signal Frame是保存在用戶的地址空間中,所以用戶是可以讀寫的.
由于內核不會去保存這個Signal對應的Signal Frame,所以當執(zhí)行sigreturn系統(tǒng)調用時,此時的Signal Frame并不一定是之前內核為用戶進程保存的Signal Frame.到這里SROP技術的基本思想一目了然,它通過控制調用堆棧,將偽造的sigcontext結構置于調用棧上,當執(zhí)行完sigreturn,偽造的sigcontext結構內容被恢復到寄存器中,便可控制程序流程.一個最簡單的例子如圖12所示.
2) 將rax設置為execve的系統(tǒng)調用號.
3) 將rip設置為syscalll指令的地址.
sigreturn系統(tǒng)調用將這些值恢復相應寄存器,最后恢復進程執(zhí)行時,將會得到一個shell.
如果需要執(zhí)行一系列的函數,就像構造一個ROP鏈一樣,只需要作2處修改即可:
1) 修改棧指針指向下一個偽造的sigcontext.
2) 將程序指針設置為syscall;ret這個指令序列的地址,如圖13所示.
SROP通常只需要一個gadget即可成功實施此攻擊,使得這種攻擊變得簡單而有效,在文獻[16]中,作者還給出了“syscall; ret”這個gadget在不同內核版本中出現的位置,有興趣的讀者可以去自行查閱.
圖12 偽造的sigcontext結構
圖13 SROP鏈
Blind ROP技術由斯坦福大學的Bittau等人[17]提出,該技術可以在沒有二進制程序和源碼的情況下同時繞過NX,ASLR和Canary的安全保護,但必須符合以下條件:
1) 目標程序存在棧溢出漏洞;
2) 目標程序進程在崩潰之后會重新啟動,且進程加載基址與原來相同,即使用fork重新創(chuàng)建子進程,但未采用execve執(zhí)行磁盤上的程序(fork-only without execve).
BROP的主要思想就是首先通過不斷的枚舉,得到棧上正確的Canary,然后枚舉出可用的Gadgets來構造write系統(tǒng)調用、遠程dump內存,最終實現漏洞利用,具體過程下面將詳細介紹.
1) 暴力枚舉
不斷增大輸入緩沖區(qū)數據的長度,直到發(fā)現目標程序剛好崩潰,得到Canary之前??臻g的長度.
2) Stack Reading
因為Canary的變化只是在每個字節(jié)上,所以文獻[17]中提出了一種名為Stack Reading的方法來快速枚舉出正確的Canary值,原理如圖14所示:
圖14 通過Stack Reading 枚舉出正確的Canary
首先只覆蓋Canary的第1個字節(jié),然后從0x00到0xFF枚舉,當枚舉到一個正確的數值時,服務器進程并不會崩潰,記錄下這個字節(jié),繼續(xù)覆蓋第2個字節(jié),然后不斷重復前面的過程,直到將Canary逐字節(jié)全部枚舉出來.
0x00到0xFF有256種可能,所以在32位系統(tǒng)中,最多需要嘗試4×256=1 024次,64位中最多需要嘗試8×256=2 048次,整個過程只需幾秒.
3) 尋找Stop Gadgets
接下來需要找到構造ROP鏈,從遠程dump漏洞程序的內存.文獻[17]提出了Stop Gadget這一概念,它尋找其他Gadgets取到了至關重要的作用.為了找到這種Gadgets,需要將返回地址覆蓋為某個代碼段的地址,然后通過不斷枚舉尋找(代碼段的地址可以在上一步Stack Reading中獲得,對于沒有PIE的程序也可以從可執(zhí)行文件默認的加載基址開始),當程序的執(zhí)行流跳到這時,程序并不會崩潰,而是進入了無限循環(huán),攻擊者能一直保持連接狀態(tài).
4) 尋找Useful Gadgets
接下來,需要找到其他具有某些功能而不是會造成Crash的Gadget.這類Gadgets稱為“Useful Gadgets”.
同樣還是采用上一步枚舉的方法,將返回地址覆蓋為某個代碼段的地址,返回地址之后填充多個Stop Gadgets,如圖15所示.
當正在枚舉的地址是一個無效的Gadget時,程序將會崩潰,如圖16所示.
但是如果是一個有效的Gadget,程序將進入一個無限循環(huán)的狀態(tài),如圖17所示.
以上是尋找Useful Gadgets的原理,而要從遠程dump內存,可以通過構造write(int sock, void *buf, int len)來實現,接下來的關鍵就是如何找到相關的Gadgets.構造這個調用的4個Gadgets可以在圖18所示的位置找到.
圖15 枚舉有效的Gadgets
圖16 無效的Gadget導致程序崩潰
圖17 有效的Gadget使程序保持運行狀態(tài)
圖18 通過Stack Reading 枚舉出正確的Canary
在這之后,便可以通過dump下的內存,構造漏洞利用程序.
Stack Pivot[18]出現的時間已經很久了,但到目前為止,一直是漏洞利用中比較重要的輔助技術.目的是將棧劫持到一個攻擊者能夠控制的內存上去,在該位置再進行ROP.
假設攻擊者控制了棧上部分區(qū)域,但是中間有一段不可控制的內存,這時攻擊者需要控制棧指針跳轉到可控部分,繼續(xù)執(zhí)行ROP指令,圖19只是使用該技術的最簡單情況,要想劫持棧指針還有很多方法,任何可以修改棧指針的Gadget都可以用于Stack Pivot.
圖19 Stack Pivot
在第4節(jié)中曾提到過,對于partial RELRO的情況,GOT表仍然是可寫的,仍可以進行GOT表覆寫,所以本節(jié)中將重點介紹繞過full RELRO的方法[19],此外這里假設讀者已了解Linux的動態(tài)重定位原理,就不再對其進行詳細介紹了.
當使用full RELRO時,所有的重定位將在加載時完成,GOT表被設置只讀,不會有惰性解析(lazy binding)的過程,并且link_map結構的地址和_dl_runtime_resolve地址也不會初始化.在.dynamic段中的Elf_Dyn結構中,有一個DT_DEBUG條目,它的值是程序加載時,由動態(tài)加載器設置好的,指向堆中保存的一個r_debug類型的數據結構.此外,該結構的r_map域保存著一個指向link_map鏈表的指針,因此可以通過這個結構來恢復link_map的值,如圖20所示.
圖20 從DT_DEBUG條目恢復link_map
得到link_map的位置后,需要偽造一個重定位項(因為.got.plt是只讀的).為此需要覆蓋掉原來l_info結構的DT_JMPREL域的內容,使其指向一個偽造的動態(tài)條目,而這個動態(tài)條目則指向一個重定位項.這個重定位項引用了已經存在的函數符號,而r_offset則指向一塊可寫的內存區(qū)域,如圖21所示.
圖21 覆蓋DT_JMPREL偽造動態(tài)條目
接著,還需要恢復_dl_runtime_resolve函數的指針,因為GOT表中已經沒有_dl_runtime_resolve函數的指針了.解引用l_info域中的第1個link_map結構取得描述第1個共享庫的link_map,而這個共享庫是不被完全RELRO保護的.攻擊者通過l_info[DT_PLTGOT]域來得到對應的動態(tài)條目(右側的.dynamic),接著是.plt.got段(總是在右側),其中的第2個條目里就有_dl_runtime_resolve的地址,如圖22所示:
圖22 _dl_runtime_resolve
最后,將link_map結構作為第1個參數,將一個新的.dynsym偏移作為第2個參數,就可以調用_dl_runtime_resolve函數了.但這里存在一個問題問題——_dl_runtime_resolve不僅會調用目標函數,還會嘗試將目標函數的地址寫到正確的GOT項中,但因為GOT是不可寫的,程序會因此崩潰.為了解決這個問題,將原本指向.rel.dyn段的DT_JMPREL指向攻擊者控制的一塊內存區(qū)域,并在那塊位置偽造一個Elf_Rel結構,且其r_offset域指向一塊可寫的內存區(qū)域,其r_info指向目標符號.所以,當一個庫被解析時,它的地址將會被寫到一個可寫的位置,程序就不會崩潰,而且請求的函數也將會被執(zhí)行.
近幾年,二進制漏洞分析與利用技術已經發(fā)展得相當成熟,很多技術也被黑產所利用,也正因如此,軟件安全領域得到了極大的發(fā)展,對個人終端、服務器的攻擊越來越困難.從上面介紹的利用方法來看,雖然有能繞過各種安全機制的途徑,但往往利用條件都有一些限制.如果程序開啟了全部安全機制(NX,PIE,ASLR,Full RELRO等),那么就需要通過泄露地址等方式才能完成利用.
所以未來的二進制漏洞的發(fā)展趨勢,可能會逐漸向其他方面轉移.例如卡巴斯基實驗室最近發(fā)布報告指出,近幾年來,路由器等網絡設備已成為高級持續(xù)性威脅(APT)常用的攻擊渠道,結合目前的行業(yè)趨勢,筆者預測未來Linux相關二進制攻防博弈主要集中在下面3個領域.
1) 移動終端
移動互聯網時代早已到來,移動終端上每年發(fā)現的漏洞正在呈倍增長.特別是基于Linux的Android系統(tǒng),有很多Linux內核漏洞也會影響到它.而對于iOS系統(tǒng)漏洞,其漏洞數量也是保持持續(xù)上升趨勢.雖然由于iOS的封閉性和相關安全研究者較少,但是隨著這幾年相關安全書籍和文章增加,iOS設備的漏洞也會越來越多.
2) 物聯網設備
物聯網的誕生,勢必將改變人們未來的生活,根據相關估算,到2020年,物聯網設備的數量將會超過200億.此外根據統(tǒng)計,暴露在公網上的物聯網設備中,大部分都是使用Linux操作系統(tǒng),根據目前相關資料以及近年來出現的與物聯網設備相關的蠕蟲病毒信息,針對物聯網設備的攻擊手段及技術已經很成熟.這些攻擊手段與傳統(tǒng)安全攻擊手段類似,但更偏重于系統(tǒng)底層.同時由于物聯網設備的系統(tǒng)特殊性和封閉性,都對這種攻擊提供了很好的保護作用.
相對其他領域而言,物聯網可以算是才剛剛起步,物聯網設備的安全更是沒有引起很多廠商的重視.作為和人們生活息息相關的物品,如果物聯網產品存在安全問題,那就有可能直接影響到個人財產安全,甚至人身安全.例如曾經Black Hat上展示的關于心臟起搏器、胰島素泵(注射胰島素的設備,當注入過量時可導致患者昏迷)的入侵等等.
3) 云計算平臺
云計算平臺的架構可以簡單地分為軟件即服務(SaaS)、平臺即服務(PaaS)、基礎設施即服務(IaaS),它們分別為用戶提供了應用軟件、系統(tǒng)平臺和IT基礎設施資源等.
在SaaS層上,傳統(tǒng)的Web漏洞、軟件漏洞都可能會出現,而此層的漏洞風險更大,也是外部最容易觸及到的.
在PaaS層上,由于大多數服務器采用Linux系統(tǒng),Linux上的Web服務器漏洞、系統(tǒng)提權等漏洞依然需要人們重視.
在IaaS層上,則存在虛擬機漏洞、數據存儲缺陷等安全問題.如果利用虛擬機逃逸漏洞,進而控制云平臺主系統(tǒng),那么造成的后果也是不堪設想的.