一個小小指針,竟把Linux內核攻陷了!
怎樣攻進操作系統內核?
這是一個很有意思也很硬核的問題。
黑客通過應用程序的漏洞(如Java、PHP、Apache、IE、Chrome、Adobe、office等)獲得執行代碼能力后,由于操作系統安全方面的設定,很多情況下都是在沙盒或者低權限進程中運行,許多操作都無法進行。要想做更多高權限的事情,黑客通常會使用工具來提權。
x86
arm
操作系統的安全防線是建立在內核至高權限掌控的基礎之上,無論是殺毒軟件,沙箱,還是防火墻都運行在內核態。要突破安全包圍,必須獲得內核級權限的執行能力,才能和這些安全防護正面PK。
我們常常聽到的Android系統ROOT和iOS系統的越獄就是內核攻擊的典型應用。
獲得內核權限以后,攻擊者可安裝rootkit級木馬病毒,實現文件隱藏、進程隱藏、通信隱藏等高級木馬功能,對系統危害極為嚴重。
內核0day漏洞,在APT攻擊中是核武器級別的存在,地下網絡安全交易市場價值巨大。
進入內核的四種方式
實際上,我們的程序每時每刻都在往返于用戶空間和內核空間,只不過這些進出的大門都被操作系統提前安排好了,進入內核后該去哪里執行什么代碼,是操作系統說了算,由不得我們自己的程序做主。
從用戶態空間進入內核,有四種方式:
中斷:
中斷分為兩種:硬中斷和軟中斷
硬中斷:硬件設備向CPU發起的中斷信號
軟中斷:CPU執行int指令觸發,早期的操作系統中實現系統調用就是通過這種方式,如Windows上的 int 2e,Linux上的 int 80
不管是硬中斷還是軟中斷,CPU遇到以后都會保留當前執行的現場上下文,進入內核去執行中斷處理函數。這些函數記錄在中斷描述符表IDT中,由操作系統初始化系統的時候預先安排。
異常:
異常是CPU在執行指令的過程中出現的問題,如除法指令的除數為0,訪問的內存地址無效等等。
異常和中斷處理邏輯類似,也是通過記錄在IDT中的異常處理函數來執行,同樣由操作系統初始化系統的時候預先安排。
系統調用:
系統調用大家應該就很熟悉了,我們要實現文件系統訪問、網絡I/O、進程線程使用、內存分配釋放等等行為,都需要借助操作系統提供的編程接口來實現,這些接口叫做:系統調用。
前面提到,早期的x86架構下的CPU,沒有專門的系統調用機制,操作系統們都使用軟中斷的形式來進入內核完成系統調用。
后來,因為系統調用是一個很高頻的需求,軟中斷的方式效率有些低下,CPU加入了專門的系統調用機制,這包括一些專用的寄存器和一些專用的系統調用指令。如sysenter(x86)、syscall(x64)、swi(arm)。
通過系統調用進入內核后,該轉向哪里執行也是操作系統提前安排好了,由不得應用程序做主。
開發驅動程序:
最后一種進入內核的方式就是開發驅動程序了,但加載驅動本身就需要極高的權限,所以這一點就不詳述了。
以上就是通過正規途徑,讓我們的程序進入內核態運行的方式,可見,一旦進入內核態,執行流就進入了操作系統提前設置好的代碼,攻擊者沒辦法胡來。
有正規途徑,當然就有不正規途徑,也就是通過形形色色的漏洞攻擊系統內核,從而使我們的程序進入內核態執行,這也是這篇文章的重點。
下面列舉一些常見的攻擊手法。
零地址攻擊
學過C語言的朋友都知道,零地址,也就是NULL,在C語言中代表著空指針。
一些沒有經驗的程序員在寫一些接口函數時,往往容易忘記檢查指針參數是否是NULL,而導致程序的崩潰異常。
以32位操作系統為例,進程的地址空間是:
- 0x00000000~0xFFFFFFFF,
在x86架構上,內存一般以4KB頁面單元進行管理。
你有沒有想過,如果進程的地址空間中,以零地址(也就是NULL)開始的第一個4KB頁面如果被分配了,會出現什么事情?
假設在內核中,有一段代碼忘記對空指針的檢查,就通過這個指針來調用函數。而攻擊者提前將零地址頁面分配準備好數據,就像這樣:
這會發生什么后果?
后果就是,攻擊者的代碼將會在內核態下執行!
然而假設不只是假設,它曾真實發生過,就算強如微軟的程序大佬,也會有忘記檢查空指針的時候。
典型漏洞案例:CVE-2014-4113 Windows
釋放后使用:UAF
除了空指針,懸在C/C++程序員頭頂的還有一把利刃,這就是懸空指針。
懸空指針的意思是忘記對已經釋放的內存/對象指針即時置空,而在后面又去使用這個指針,但此時對應的內存已被回收,引發不可預期的后果。
哎,這個指針可真是害人不淺啊!
你有沒有想過,假如在對象釋放后忘記對指針及時置空,后面又繼續使用這個指針,就在這兩個動作發生之間的那一段時間里,不懷好意的人去把原來釋放的那塊內存空間給“占領”了,布置好惡意的數據代碼,會發生什么后果?
這就是大名鼎鼎的釋放后使用UAF攻擊!
UAF意思是Use After Free。
來看一個簡單的例子:
兩個對象,一個真,一個假,它倆對象占據的內存空間一樣大。
下面這段代碼,在原始對象釋放后,忘記對obj指針置空,隨后分配一個FakeObject,由于堆分配算法的原因,這倆對象一樣大小,很大概率新的對象就會分配到剛剛釋放的那片內存上去。
此時再通過原來已經懸空的指針來調用函數,實際上調用到了新的對象的函數,劫持了執行流程。
這只是一個簡單的示例,真實環境中比這要復雜得多,但原理是一致的。
而這種事情一旦發生在系統內核,那后果就嚴重了,應用程序可以劫持內核空間的執行流程,執行自己的代碼。
典型漏洞案例:CVE-2016-0728 Linux
整數溢出 + 數組越界
在操作系統中,有很多函數地址以表格的形式存儲了起來,如:
- 系統調用表:SSDT/sys_call_table
- 中斷描述符表:IDT
假如有辦法能修改這些表格中的函數地址,改寫成攻擊者的代碼地址,不就能有辦法讓我們的代碼在內核模式下運行了嗎?
道理是這么個道理,但這些表格本身就位于內核空間,普通應用程序別說去改寫了,連讀取都費勁。
那真的沒辦法了嗎?
還是有的!
假如內核中某段代碼在向某個數組中某個元素寫入數據,又恰巧忘記了檢查數組的下標是不是越界,再恰好這個下標可以通過應用程序來控制,那豈不是可以越界寫?一不小心寫到了前面那些函數表格里去了咋辦?
你可能已經意識到了,這不是假設,是真實案例!
典型漏洞案例:CVE-2013-2094 Linux
這是一個Linux內核任意地址寫入漏洞,通過精準控制系統調用的參數,實現改寫IDT中的函數地址為惡意代碼地址,實現在內核態執行惡意代碼!
下面是一些內核攻擊的真實案例:
安全防御
魔高一尺,道高一丈,形形色色的攻擊也推進了操作系統和CPU的安全能力建設:
- Intel? 2011年在第三代Core處理器禁止內核狀態下去執行用戶地址空間指令 SMEP(supervisor mode execute prevention),設置CR4寄存器的bit20位為1開啟
- ARM從armv7開始加入PXN技術,原理同SMEP
- Windows 8.1禁止使用零頁地址內存
- Linux 2.6.26開始使用vm.mmap_min_addr限制地址空間最小值,防止使用零頁內存
- ······
空指針、懸空指針、數組越界、整數溢出···這些一個個看起來不起眼的編程問題,如果發生在操作系統內核之中,那造成的后果便是災難性的!可見,養成一個好的編程習慣有多重要!
連開發操作系統的大神程序員們都會犯錯誤,何況我們呢?
本文轉載自微信公眾號「編程技術宇宙」,可以通過以下二維碼關注。轉載本文請聯系編程技術宇宙公眾號。