阿里妹導讀:造成 系統異常宕機(無響應、異常重啓)的原因有很多種, 最常見的 是操作系統內部缺陷和設備驅動缺陷。 本文作者將和大 家分享內存轉儲分析的底層邏輯和方法論,並通過一個線上真實案例來展示從分析到得出結論的整個過程,希望對同學們處理此類問題和對系統的理解上有所幫助。

文末推薦:《安全問道》, 3 分鐘掌握一個互聯網安全知識。

相信凡是與計算機高頻親密接觸的人,都遇到過系統無響應,或突然重啓的情況。這樣的情況如果發生在客戶端設備,如手機,或者筆記本電腦上,且不是頻繁出現,基本上我們的解法就是鴕鳥算法,即默默重啓設備,然後繼續使用,當作什麼都沒發生過。

但是,如果這樣的問題發生在服務端,比如運行微信、微博後臺程序的虛擬機或者物理機上,那往往會產生相當嚴重的影響。輕則導致業務中斷,重則導致業務長時間無法工作。

大家都知道,驅動這些計算機的是運行在其上的操作系統,如 Windows 或者 Linux 等。系統異常宕機(無響應、異常重啓)的原因有很多種,但總體來看,操作系統內部缺陷,或者設備驅動缺陷是最常見的兩類原因。

從根本上解決這類問題“唯一正確”的方法,是操作系統內存轉儲分析(Memory Dump Analysis)。內存轉儲分析屬於高階的軟件調試能力,需要工程師有豐富且全面的系統級別理論知識和大量的疑案破解似的上手實踐經驗。

內存轉儲分析的方法論

內存轉儲分析是對專業能力要求極高的一個工作,也是非常不容易的一件事情。在以往案例分享後,得到比較有趣的反饋,如“耳邊想起了柯南的配音”,或者“真黑貓警長!做的是 IT 工程師,卻整天搞刑偵工作”。

內存轉儲分析需要用到的基礎能力,包括但不限於反彙編、彙編分析、各種語言的代碼分析,系統層面各種結構的理解,如堆,棧,虛表等,甚至深入到 bit 級別。

試想,一個系統運行了很長一段時間。在這段時間裏,系統積累了大量正常、甚至不正常的狀態。這時如果系統突然出現了一個問題,那這個問題十有八九跟長時間積累下來的狀態有關係。

分析內存轉儲,就是分析發生問題時,系統產生的“快照”。實際上需要工程師以這個快照爲出發點,追溯歷史,找出問題發生源頭。這有點像是從案發現場,推理案發經過一樣。

死鎖分析方法

內存轉儲分析方法,可以從所要解決問題的角度,簡單分成兩類,分別是死鎖分析方法,和異常分析方法。這兩種方法的區別在於,死鎖分析方法以系統全局爲出發點,而異常分析則從具體異常點開始。

死鎖問題表現出來,就是系統不響應問題。死鎖分析方法着眼於全局。這裏的全局,就是整個操作系統,包括所有進程在內的系統全貌。我們從教科書裏學到的知識,一個運行中的程序,包括了代碼段,數據段和堆棧段。用這個方法去看一個系統也同樣適合。系統的全貌,其實就包括正在被執行的代碼(線程),和保存狀態的數據(數據、堆棧)。

死鎖的本質,是系統中部分或者全部線程,進入了互相等待且互相依賴的狀態,使得進程所承載的任務無法被繼續執行了。所以我們分析這類問題的中心思想,就是分析系統中所有的線程的狀態和它們之間的依賴關係,正如如圖 1 所示。

圖 1

線程的狀態相對來說是比較確定的信息。我們可以通過讀取內存轉儲中線程的狀態標誌位,來獲取這類的信息。而依賴關係分析則需要很多的技巧和實踐經驗。最常用分析方法有對象的持有等待關係分析,時序分析等。

異常分析方法

相對死鎖分析,異常分析方法的核心是異常。我們經常遇到的異常有除零操作,非法指令執行,錯誤地址訪問,甚至包括軟件層面自定義的非法操作等。這些異常反應到操作系統層面,就是異常重啓類宕機問題。

異常問題歸根結底是處理器執行了具體的指令而觸發的。換句話說,我們看到的現象,肯定是處理器踩到了異常點。所以分析異常類問題,我們需要從異常點出發,逐步地推導出代碼執行到這一點的完整邏輯。

以經驗來看,懂得做內存轉儲異常分析的工程師不多,而理解以上一點的人更是少之又少。很多工程師分析異常重啓問題,基本上只停留在異常本身,根本沒有推導出問題背後的整個邏輯。

相比死鎖分析方法,異常分析的方法沒有那麼多固定的章法,甚至很多時候,因爲問題邏輯複雜,我們沒有辦法找出根本原因。

總體看來,異常分析的底層邏輯,是不斷地對比預期和非預期的狀況,然後找出背後的原因。比如處理其執行了錯誤指令而觸發的異常,那我們需要從回答正常被執行的指令應該是什麼,爲什麼處理器拿到了這個錯誤指令這兩個問題開始,不斷深入,追根究底。

用死鎖分析方法處理異常問題,用異常分析方法處理死鎖問題

以上兩種內存轉儲分析方法,是基於問題分析的起點和一般性分析手段來分類的。在實際問題處理過程中,我們經常需要從系統全局狀態中,找到進一步處理異常問題的思路,也會用具體細節分析手段,來給全局類問題最後一擊。

黑客與宕機

問題背景

宕機問題有一種比較少見的問題模式,就是看起來完全不相關的機器同時出現宕機。處理這個模式的問題,我們需要找到在這些機器上能同時觸發問題的條件。

通常,這些機器要麼幾乎在同一時間點出現問題,要麼從某一個時間點開始,相繼出現問題。對於前一種情況,比較常見的情形是,物理設備故障導致運行在其上的所有虛擬機宕機,或者一個遠程管理軟件同時殺死了多個系統的關鍵進程;對於後一種情況,可能的一個原因是,用戶在所有實例上部署了同一個有問題的模塊(軟件、驅動)。

而實例被大範圍地攻擊,則是另一個常見的原因。比如在 WannaCry 勒索病毒肆虐的時候,經常出現一些公司,或者一些部門的機器全部藍屏的情形。

在這個案例中,用戶安裝了阿里雲的雲監控產品之後,出現了大範圍雲服務器連續宕機的情況。爲了自證清白,我們耗費了不少體力腦力來深入分析這個問題。通過此案例分享,希望能給讀者以啓發。

壞掉的內核棧

我們處理操作系統宕機類問題的唯一正確方法是內存轉儲。不管是 Linux 或 Windows,在系統宕機之後,都能夠通過自動,或者人工的方式,產生內存轉儲。

分析 Linux 內存轉儲的第一步,我們使用 crash 工具打開內存轉儲,並用 sys 命令觀察系統的基本信息和宕機的直接原因。對於這個問題來說,宕機的直接原因是"Kernel panic - not syncing: stack-protector: Kernel stack is corrupted in: ffffxxxxxxxx87eb",如圖 2 所示。

圖2

關於這條信息,我們必須逐字解讀。 "Kernel panic - not syncing:" 這部分內容在內核函數 panic  裏輸出,凡是調用到 panic  函數,必然會有這一部分輸出,所以這一部分內容和問題沒有直接關係。 而"stack-protector: Kernel stack is corrupted in:" 這部分內容,在內核函數 __stack_chk_fail ,這個函數是一個堆棧檢查函數,它會檢查堆棧,同時在發現問題的時候調用 panic 函數產生內存轉儲報告問題。

而它報告的問題是堆棧損壞。關於這個函數,後續我們會進一步分析。

ffffxxxxxxxx87eb 這個地址,是函數 __builtin_return_address(0) 的返回值。當這個函數的參數是 0 的時候,這個函數的輸出值是調用它的函數的返回地址。這句話現在有點繞,但是後續分析完調用棧,問題就會變得很清楚。

函數調用棧

分析宕機問題的核心,就是分析 panic 的調用棧。圖 3 中的調用棧,乍看起來是 system_call_fastpath 調用了 __stack_chk_fail ,然後 __stack_chk_fail 調用了 panic ,報告了堆棧損壞的問題。但是稍微和類似的堆棧作一點比較的話,就會發現,事實並非這麼簡單。

圖3

圖 4 是一個類似的,以 system_call_fastpath 函數開始的調用棧。不知道大家有沒有看出來這個調用棧和上邊調用棧的不同。實際上,以 system_call_fastpath 函數開始的調用棧,表示這是一次系統調用(system call)的內核調用棧。

圖4

圖 4 的調用棧,表示用戶模式的進程,有一次 epoll 的系統調用,然後這個調用進入了內核模式。而圖 3 中的調用棧顯然是有問題的,因爲我們就算查遍所有的文檔,也不會找到一個系統調用,會對應於內核 __stack_chk_fail 函數。

這裏需要提醒的是,這邊引出另外一個,在分析內存轉儲的時候需要注意的問題,就是用 bt 打印出來的調用棧有的時候是錯誤的。

所謂的調用棧,其實不是一種數據結構。用 bt 打印出來的調用棧,其實是從真正的數據結構,線程內核堆棧中,根據一定的算法重構出來的。而這個重構過程,其實是函數調用過程的一個逆向工程。

相信大家都知道堆棧的特性,即先進後出。關於函數調用,以及堆棧的使用,可以參考圖 5。可以看到,每個函數調用,都會在堆棧上分配到一定的空間。而 CPU 執行每個函數調用指令 call,都會順便把這條 call 指令的下一條指令壓棧。這些“下一條指令”,就是所謂的函數返回地址。

圖5

這個時候,我們再回頭看  Panic 的直接原因那一部分,即函數 __builtin_return_address(0) 的返回值。

這個返回值,其實就是調用 __stack_chk_fail call 指令的下一條指令,這條指令屬於調用者函數。這條指令地址被記錄爲 ffffxxxxxxxx87eb

如圖 6 所示,我們用 sym 命令查看這個地址臨近的函數名,顯然這個地址不屬於函數 system_call_fastpath ,也不屬於內核任何函數。這也再次驗證了, panic 調用棧是錯誤的這個結論。

圖6

關於 raw stack ,如圖7所示,我們可以用 bt -r 命令來查看。因爲 raw stack 往往有幾個頁面,這裏只截圖和 __stack_chk_fail 相關的這一部分內容。

圖7

這部分內容,有三個重點數據需要注意, panic 調用 __crash_kexec 函數的返回值,這個值是 panic 函數的一條指令的地址; __stack_chk_fail 調用 panic 函數的返回值,同樣的,它是 __stack_chk_fail 函數的一條指令的地址; ffffxxxxxxxx87eb 這個指令地址,屬於另外一個未知函數,這個函數調用了 __stack_chk_fail

Syscall number 和 Syscall table

因爲帶有 system_call_fastpath 函數的調用棧,對應着一次系統調用,而 panic 的調用棧是壞的,所以這個時候我們自然而然會疑問,到底這個調用棧對應的是什麼系統調用。

在 linux 操作系統實現中,系統調用被實現爲異常。而操作系統通過這次異常,把系統調用相關的參數,通過寄存器傳遞到內核。在我們使用 bt 命令打印出調用棧的時候,我們同時會輸出,發生在這個調用棧上的異常上下文,也就是保存下來的,異常發生的時候,寄存器的值。

對於系統調用(異常),關鍵的寄存器是 RAX,如圖 8 所示。它保存的是系統調用號。我們先找一個正常的調用棧驗證一下這個結論。0xe8 是十進制的 232。

圖8

使用 crash 工具, sys -c 命令可以查看內核系統調用表。我們可以看到,232 對應的系統調用號,就是 epoll,如圖 9 所示。

圖9

這個時候我們再回頭看“函數調用棧”這節的圖 3,我們會發現異常上下文中 RAX 是 0。正常情況下這個系統調用號對應 read 函數,如圖 10 所示。

圖10

從圖 11 中,我們可以看出,有問題的系統調用表顯然是被修改過的。修改系統調用表(system call table)這種事情,常見的有兩種代碼會做,這個相當辯證。一種是殺毒軟件,而另外一種是病毒或木馬程序。當然還有另外一種情況,就是某個蹩腳的內核驅動,無意識地改寫了系統調用表。

另外我們可以看到,被改寫過的函數的地址,顯然和最初被 __stack_chk_fail 函數報出來的地址,是非常鄰近的。這也可以證明,系統調用確實是走進了錯誤的 read 函數,最終踩到了 __stack_chk_fail 函數。

圖11

Raw data

基於上邊的數據,來完全說服客戶,總歸還是有點經驗主義。更何況,我們甚至不能區分,問題是由殺毒軟件導致的,還是木馬導致的。這個時候我們花費了比較多的時間,嘗試從內存轉儲裏挖掘出 ffffxxxxxxxx87eb 這個地址更多的信息。

有一些最基本的嘗試,比如嘗試找出這個地址對應的內核模塊等等,但是都無功而返。這個地址既不屬於任何內核模塊,也不被已知的內核函數所引用。這個時候,我們做了一件事情,就是把這個地址前後連續的,所有已經落實(到物理頁面)的頁面,用 rd 命令打印出來,然後看看有沒有什麼奇怪的字符串可以用來作爲 signature 定位問題。

就這樣,我們在鄰近地址發現了下邊這些字符串,如圖 12 所示。很明顯這些字符串應該是函數名。我們可以看到 hack_open hack_read 這兩個函數,對應被 hacked 的 0 和 2 號系統調用。還有函數像 disable_write_protection 等等。這些函數名,顯然說明這是一段“不平凡”的代碼。

圖12

後記

宕機問題的內存轉儲分析,需要我們足夠的耐心。我個人的一條經驗是:every bit matters,就是不要放過任何一個 bit 的信息。內存轉儲因爲機制本身的原因,和生成過程中一些隨機的因素,必然會有數據不一致的情況,所以很多時候,一個小的結論,需要從不同的角度去驗證。

《安全問道》系列視頻

3 分鐘掌握一個互聯網安全知識

WannaCr y 事件最“細思恐極“的一個事實是?爲何初創企業也會被 DDos 攻擊?白帽子爲什麼那麼有錢?阿里雲安全專家爲你科普和安全相關的話題、熱點和現象,通俗易懂,三分鐘帶你明白一個安全之道——從企業安全 的疑難雜症,到熱門事件的縱深點評。

點擊“閱讀原文”快去看看 吧~

關注 「阿里技術」

把握前沿技術脈搏

戳我,看《安全問道》。

相關文章