黑客與宕機
宕機問題有一種比較少見的pattern,就是看起來完全不相關的機器同時出現宕機。處理這個pattern的問題,我們需要找到,在這些機器上能同時觸發問題的條件。
通常,這些機器要麼幾乎在同一時間點出現問題,要麼從某一個時間點開始,相繼出現問題。對於前一種情況,比較常見的情形是,物理機故障導致運行在其上的所以虛擬機宕機,或者一個遠程管理軟體同時殺死了所有被管理的系統里的關鍵進程;對於後一種情況,可能的一個原因是,用戶在所有實例上部署了同一個有問題的模塊(軟體、驅動)。
而實例被大範圍的hack,則是另一個常見的原因。在WannaCry病毒肆虐的時候,經常出現一些公司,或者一些部門的機器全部藍屏的情形。今天跟大家分享一例這樣的問題。
壞掉的內核棧
使用crash工具sys命令,我們可以看到系統的一些基本信息,和宕機的直接原因。對於這個問題來說,宕機的直接原因是"Kernel panic - not syncing: stack-protector: Kernel stack is corrupted in: ffffffffa02987eb"。關於這條信息,我們必須逐字解讀。"Kernel panic - not syncing:"這部分內容在內核函數panic里輸出,凡是調用到panic函數,必然會有這一部分輸出,所以這一部分內容和問題沒有直接關係。而"stack-protector: Kernel stack is corrupted in:"這部分內容,在內核函數__stack_chk_fail,這個函數是一個堆棧檢查函數,它會檢查堆棧,同時在發現問題的時候調用panic函數產生core dump報告問題。而它報告的問題是堆棧損壞。關於這個函數,後續我們會跟進一步分析。
而ffffffffa02987eb這個地址,是函數__builtin_return_address(0)的返回值。當這個函數的參數是0的時候,這個函數的輸出值是調用它的函數的返回地址。這句話現在有點繞,但是後續分析完調用棧,會很清楚。
函數調用棧
分析宕機問題的核心,就是分析panic的調用棧。下邊的調用棧,乍看起來是system_call_fastpath調用了__stack_chk_fail,然後__stack_chk_fail調用了panic,報告了堆棧損壞的問題。但是稍微和類似的堆棧做一點比較的話,就會發現,事實並非這麼簡單。
下邊是一個類似的,以system_call_fastpath函數開始的調用棧,不知道大家有沒有看出來這個調用棧和上邊調用棧的不同。實際上,以system_call_fastpath函數開始的調用棧,表示這是一次系統調用(system call)的內核調用棧。下圖中的調用棧,表示用戶模式的進程,有一次epoll的系統調用,然後這個調用進入了內核模式。而上圖中的調用棧顯然是有問題的,因為我們就算查遍所有的文檔,也不會找到一個系統調用,會對應於內核__stack_chk_fail函數。
提示:這邊引出另外一個,在分析core dump的時候需要注意的問題,就是用bt列印出來的調用棧有的時候是錯誤的。
Raw stack
所謂的調用棧,其實不是一種數據結構。用bt列印出來的調用棧,其實是從真正的數據結構,線程(內核)堆棧中,根據一定的演算法重構出來的。而這個重構過程,其實是函數調用過程的一個逆向工程。相信大家都知道堆棧的特性,先進後出。關於函數調用,以及堆棧的使用,可以參考一下下邊這張圖。大家可以看到,每個函數調用,都會在堆棧上分配到一定的空間。而CPU執行每個函數調用指令(call),都會順便把這條call指令的下一條指令壓棧。這些「下一條指令」,就是所謂的函數返回地址。
這個時候,我們再回頭看Panic的直接原因那一部分,函數__builtin_return_address(0)的返回值。這個返回值,其實就是調用__stack_chk_fail的call指令的下一條指令,這條指令屬於調用者函數。這條指令地址被記錄為ffffffffa02987eb。我們用sym命令查看這個地址臨近的函數名,顯然這個地址不屬於函數system_call_fastpath,也不屬於內核任何函數。這也再次驗證了,panic調用棧是錯誤的這個結論。
關於raw stack,我們可以用bt -r命令來查看。因為raw stack往往有幾個頁面,這裡只截圖和__stack_chk_fail相關的這一部分內容。
這部分內容,有三個重點數據需要注意,panic調用__crash_kexec函數的返回值,這個值是panic函數的一條指令的地址;__stack_chk_fail調用panic函數的返回值,同樣的,它是__stack_chk_fail函數的一條指令的地址;ffffffffa02987eb這個指令地址,屬於另外一個未知函數,這個函數調用了__stack_chk_fail。
Syscall number & Syscall table
因為帶有system_call_fastpath函數的調用棧,對應著一次系統調用,而panic的調用棧是壞的,所以這個時候我們自然而然會疑問,到底這個調用棧對應的是什麼系統調用。在linux操作系統實現中,系統調用被實現為異常。而操作系統通過這次異常,把系統調用相關的參數,通過寄存器傳遞到內核。在我們使用bt命令列印出調用棧的時候,我們同時會輸出,發生在這個調用棧上的異常上下文,也就是保存下來的,異常發生的時候,寄存器的值。對於系統調用(異常),關鍵的寄存器是RAX,它保存的是系統調用號。我們先找一個正常的調用棧驗證一下這個結論。0xe8是十進位的232。
使用crash工具,sys -c命令可以查看內核系統調用表。我們可以看到,232對應的系統調用號,就是epoll。
這個時候我們再回頭看「函數調用棧」這節的插圖,我們會發現異常上下文中RAX是0。正常情況下這個系統調用號對應read函數,如下圖。
如下圖,有問題的系統調用表顯然是被修改過的。修改系統調用表(system call table)這種事情,常見的有兩種代碼會做(這個相當辯證),一種是殺毒軟體,而另外一種是hack程序。當然還有另外一種情況,就是某個蹩腳的內核驅動,無意識的改寫了系統調用表。
另外我們可以看到,被改寫過的函數的地址,顯然和最初被__stack_chk_fail函數報出來的地址,是非常鄰近的。這也可以證明,系統調用確實是走進了錯誤的read函數,最終踩到了__stack_chk_fail函數。
Raw data
基於上邊的數據,來下結論,總歸還是有點經驗主義。更何況,我們甚至不能區分,問題是由殺毒軟體導致的,還是木馬導致的。這個時候我們花費了比較多的時間,嘗試從core dump里挖掘出ffffffffa02987eb這個地址更多的信息。有一些最基本的嘗試,比如嘗試找出這個地址對應的內核模塊等,但是都無功而返。這個地址既不屬於任何內核模塊,也不被已知的內核函數所引用。這個時候,我們做了一件事情,就是把這個地址前後連續的,所有已經落實(到物理頁面)的頁面,用rd命令列印出來,然後看看有沒有什麼奇怪的字元串可以用來作為特徵串定位問題。
就這樣,我們在鄰近地址發現了下邊這些字元串。很明顯這些字元串應該是函數名。我們可以看到hack_open和hack_read這兩個函數,對應被hacked的0和2號系統調用。還有函數像disable_write_protection等等。這些函數名,顯然說明這是一段「不平凡」的代碼。公網搜索這些函數會發現很多rootkit的示例。
後記
宕機問題的core dump分析,需要我們非常的耐心。我個人的一條經驗是:every bit matters,就是不要放過任何一個bit的信息。core dump因為機制本身的原因,和生成過程中一些隨機的因素,必然會有數據不一致的情況,所以很多時候,一個小的結論,需要從不同的角度去驗證。
了解更多請微博關注阿里雲客戶滿意中心
推薦閱讀:
※史上最全編程語言列表,你掌握了哪些?
※全球化2.0時代,澳門為何會擁抱阿里雲?
※從開源時代到萬物互聯,開發者如何弄潮?
※Kyligence Cloud支持阿里雲,加速雲上大數據分析
※阿里雲說要「萬物智能」,夢想何時才能照進現實?