黑客与宕机
相信凡是與計算機高頻親密接觸的人,都遇到過系統(tǒng)無響應(yīng),或突然重啟的情況。這樣的情況如果發(fā)生在客戶端設(shè)備,如手機,或者筆記本電腦上,且不是頻繁出現(xiàn),基本上我們的解法就是鴕鳥算法,即默默重啟設(shè)備,然后繼續(xù)使用,當作什么都沒發(fā)生過。
但是,如果這樣的問題發(fā)生在服務(wù)端,比如運行微信、微博后臺程序的虛擬機或者物理機上,那往往會產(chǎn)生相當嚴重的影響。輕則導(dǎo)致業(yè)務(wù)中斷,重則導(dǎo)致業(yè)務(wù)長時間無法工作。
大家都知道,驅(qū)動這些計算機的是運行在其上的操作系統(tǒng),如 Windows 或者 Linux 等。系統(tǒng)異常宕機(無響應(yīng)、異常重啟)的原因有很多種,但總體來看,操作系統(tǒng)內(nèi)部缺陷,或者設(shè)備驅(qū)動缺陷是最常見的兩類原因。
從根本上解決這類問題“唯一正確”的方法,是操作系統(tǒng)內(nèi)存轉(zhuǎn)儲分析(Memory Dump Analysis)。內(nèi)存轉(zhuǎn)儲分析屬于高階的軟件調(diào)試能力,需要工程師有豐富且全面的系統(tǒng)級別理論知識和大量的疑案破解似的上手實踐經(jīng)驗。
內(nèi)存轉(zhuǎn)儲分析的方法論
內(nèi)存轉(zhuǎn)儲分析是對專業(yè)能力要求極高的一個工作,也是非常不容易的一件事情。在以往案例分享后,得到比較有趣的反饋,如“耳邊想起了柯南的配音”,或者“真黑貓警長!做的是 IT 工程師,卻整天搞刑偵工作”。
內(nèi)存轉(zhuǎn)儲分析需要用到的基礎(chǔ)能力,包括但不限于反匯編、匯編分析、各種語言的代碼分析,系統(tǒng)層面各種結(jié)構(gòu)的理解,如堆,棧,虛表等,甚至深入到 bit 級別。
試想,一個系統(tǒng)運行了很長一段時間。在這段時間里,系統(tǒng)積累了大量正常、甚至不正常的狀態(tài)。這時如果系統(tǒng)突然出現(xiàn)了一個問題,那這個問題十有八九跟長時間積累下來的狀態(tài)有關(guān)系。
分析內(nèi)存轉(zhuǎn)儲,就是分析發(fā)生問題時,系統(tǒng)產(chǎn)生的“快照”。實際上需要工程師以這個快照為出發(fā)點,追溯歷史,找出問題發(fā)生源頭。這有點像是從案發(fā)現(xiàn)場,推理案發(fā)經(jīng)過一樣。
死鎖分析方法
內(nèi)存轉(zhuǎn)儲分析方法,可以從所要解決問題的角度,簡單分成兩類,分別是死鎖分析方法,和異常分析方法。這兩種方法的區(qū)別在于,死鎖分析方法以系統(tǒng)全局為出發(fā)點,而異常分析則從具體異常點開始。
死鎖問題表現(xiàn)出來,就是系統(tǒng)不響應(yīng)問題。死鎖分析方法著眼于全局。這里的全局,就是整個操作系統(tǒng),包括所有進程在內(nèi)的系統(tǒng)全貌。我們從教科書里學(xué)到的知識,一個運行中的程序,包括了代碼段,數(shù)據(jù)段和堆棧段。用這個方法去看一個系統(tǒng)也同樣適合。系統(tǒng)的全貌,其實就包括正在被執(zhí)行的代碼(線程),和保存狀態(tài)的數(shù)據(jù)(數(shù)據(jù)、堆棧)。
死鎖的本質(zhì),是系統(tǒng)中部分或者全部線程,進入了互相等待且互相依賴的狀態(tài),使得進程所承載的任務(wù)無法被繼續(xù)執(zhí)行了。所以我們分析這類問題的中心思想,就是分析系統(tǒng)中所有的線程的狀態(tài)和它們之間的依賴關(guān)系,正如如圖 1 所示。
圖 1
線程的狀態(tài)相對來說是比較確定的信息。我們可以通過讀取內(nèi)存轉(zhuǎn)儲中線程的狀態(tài)標志位,來獲取這類的信息。而依賴關(guān)系分析則需要很多的技巧和實踐經(jīng)驗。最常用分析方法有對象的持有等待關(guān)系分析,時序分析等。
異常分析方法
相對死鎖分析,異常分析方法的核心是異常。我們經(jīng)常遇到的異常有除零操作,非法指令執(zhí)行,錯誤地址訪問,甚至包括軟件層面自定義的非法操作等。這些異常反應(yīng)到操作系統(tǒng)層面,就是異常重啟類宕機問題。
異常問題歸根結(jié)底是處理器執(zhí)行了具體的指令而觸發(fā)的。換句話說,我們看到的現(xiàn)象,肯定是處理器踩到了異常點。所以分析異常類問題,我們需要從異常點出發(fā),逐步地推導(dǎo)出代碼執(zhí)行到這一點的完整邏輯。
以經(jīng)驗來看,懂得做內(nèi)存轉(zhuǎn)儲異常分析的工程師不多,而理解以上一點的人更是少之又少。很多工程師分析異常重啟問題,基本上只停留在異常本身,根本沒有推導(dǎo)出問題背后的整個邏輯。
相比死鎖分析方法,異常分析的方法沒有那么多固定的章法,甚至很多時候,因為問題邏輯復(fù)雜,我們沒有辦法找出根本原因。
總體看來,異常分析的底層邏輯,是不斷地對比預(yù)期和非預(yù)期的狀況,然后找出背后的原因。比如處理其執(zhí)行了錯誤指令而觸發(fā)的異常,那我們需要從回答正常被執(zhí)行的指令應(yīng)該是什么,為什么處理器拿到了這個錯誤指令這兩個問題開始,不斷深入,追根究底。
用死鎖分析方法處理異常問題,用異常分析方法處理死鎖問題
以上兩種內(nèi)存轉(zhuǎn)儲分析方法,是基于問題分析的起點和一般性分析手段來分類的。在實際問題處理過程中,我們經(jīng)常需要從系統(tǒng)全局狀態(tài)中,找到進一步處理異常問題的思路,也會用具體細節(jié)分析手段,來給全局類問題最后一擊。
黑客與宕機
問題背景
宕機問題有一種比較少見的問題模式,就是看起來完全不相關(guān)的機器同時出現(xiàn)宕機。處理這個模式的問題,我們需要找到在這些機器上能同時觸發(fā)問題的條件。
通常,這些機器要么幾乎在同一時間點出現(xiàn)問題,要么從某一個時間點開始,相繼出現(xiàn)問題。對于前一種情況,比較常見的情形是,物理設(shè)備故障導(dǎo)致運行在其上的所有虛擬機宕機,或者一個遠程管理軟件同時殺死了多個系統(tǒng)的關(guān)鍵進程;對于后一種情況,可能的一個原因是,用戶在所有實例上部署了同一個有問題的模塊(軟件、驅(qū)動)。
而實例被大范圍地攻擊,則是另一個常見的原因。比如在 WannaCry 勒索病毒肆虐的時候,經(jīng)常出現(xiàn)一些公司,或者一些部門的機器全部藍屏的情形。
在這個案例中,用戶安裝了阿里云的云監(jiān)控產(chǎn)品之后,出現(xiàn)了大范圍云服務(wù)器連續(xù)宕機的情況。為了自證清白,我們耗費了不少體力腦力來深入分析這個問題。通過此案例分享,希望能給讀者以啟發(fā)。
壞掉的內(nèi)核棧
我們處理操作系統(tǒng)宕機類問題的唯一正確方法是內(nèi)存轉(zhuǎn)儲。不管是 Linux 或 Windows,在系統(tǒng)宕機之后,都能夠通過自動,或者人工的方式,產(chǎn)生內(nèi)存轉(zhuǎn)儲。
分析 Linux 內(nèi)存轉(zhuǎn)儲的第一步,我們使用 crash 工具打開內(nèi)存轉(zhuǎn)儲,并用 sys 命令觀察系統(tǒng)的基本信息和宕機的直接原因。對于這個問題來說,宕機的直接原因是"Kernel panic - not syncing: stack-protector: Kernel stack is corrupted in: ffffxxxxxxxx87eb",如圖 2 所示。
圖2
關(guān)于這條信息,我們必須逐字解讀。"Kernel panic - not syncing:" 這部分內(nèi)容在內(nèi)核函數(shù) panic 里輸出,凡是調(diào)用到 panic 函數(shù),必然會有這一部分輸出,所以這一部分內(nèi)容和問題沒有直接關(guān)系。而"stack-protector: Kernel stack is corrupted in:" 這部分內(nèi)容,在內(nèi)核函數(shù) __stack_chk_fail,這個函數(shù)是一個堆棧檢查函數(shù),它會檢查堆棧,同時在發(fā)現(xiàn)問題的時候調(diào)用 panic 函數(shù)產(chǎn)生內(nèi)存轉(zhuǎn)儲報告問題。
而它報告的問題是堆棧損壞。關(guān)于這個函數(shù),后續(xù)我們會進一步分析。
而 ffffxxxxxxxx87eb 這個地址,是函數(shù) __builtin_return_address(0) 的返回值。當這個函數(shù)的參數(shù)是 0 的時候,這個函數(shù)的輸出值是調(diào)用它的函數(shù)的返回地址。這句話現(xiàn)在有點繞,但是后續(xù)分析完調(diào)用棧,問題就會變得很清楚。
函數(shù)調(diào)用棧
分析宕機問題的核心,就是分析 panic 的調(diào)用棧。圖 3 中的調(diào)用棧,乍看起來是 system_call_fastpath 調(diào)用了 __stack_chk_fail,然后 __stack_chk_fail 調(diào)用了 panic,報告了堆棧損壞的問題。但是稍微和類似的堆棧作一點比較的話,就會發(fā)現(xiàn),事實并非這么簡單。
圖3
圖 4 是一個類似的,以 system_call_fastpath 函數(shù)開始的調(diào)用棧。不知道大家有沒有看出來這個調(diào)用棧和上邊調(diào)用棧的不同。實際上,以 system_call_fastpath 函數(shù)開始的調(diào)用棧,表示這是一次系統(tǒng)調(diào)用(system call)的內(nèi)核調(diào)用棧。
圖4
圖 4 的調(diào)用棧,表示用戶模式的進程,有一次 epoll 的系統(tǒng)調(diào)用,然后這個調(diào)用進入了內(nèi)核模式。而圖 3 中的調(diào)用棧顯然是有問題的,因為我們就算查遍所有的文檔,也不會找到一個系統(tǒng)調(diào)用,會對應(yīng)于內(nèi)核 __stack_chk_fail 函數(shù)。
這里需要提醒的是,這邊引出另外一個,在分析內(nèi)存轉(zhuǎn)儲的時候需要注意的問題,就是用 bt 打印出來的調(diào)用棧有的時候是錯誤的。
所謂的調(diào)用棧,其實不是一種數(shù)據(jù)結(jié)構(gòu)。用 bt 打印出來的調(diào)用棧,其實是從真正的數(shù)據(jù)結(jié)構(gòu),線程內(nèi)核堆棧中,根據(jù)一定的算法重構(gòu)出來的。而這個重構(gòu)過程,其實是函數(shù)調(diào)用過程的一個逆向工程。
相信大家都知道堆棧的特性,即先進后出。關(guān)于函數(shù)調(diào)用,以及堆棧的使用,可以參考圖 5。可以看到,每個函數(shù)調(diào)用,都會在堆棧上分配到一定的空間。而 CPU 執(zhí)行每個函數(shù)調(diào)用指令 call,都會順便把這條 call 指令的下一條指令壓棧。這些“下一條指令”,就是所謂的函數(shù)返回地址。
圖5
這個時候,我們再回頭看 Panic 的直接原因那一部分,即函數(shù) __builtin_return_address(0) 的返回值。
這個返回值,其實就是調(diào)用 __stack_chk_fail 的 call 指令的下一條指令,這條指令屬于調(diào)用者函數(shù)。這條指令地址被記錄為 ffffxxxxxxxx87eb。
如圖 6 所示,我們用 sym 命令查看這個地址臨近的函數(shù)名,顯然這個地址不屬于函數(shù) system_call_fastpath,也不屬于內(nèi)核任何函數(shù)。這也再次驗證了,panic 調(diào)用棧是錯誤的這個結(jié)論。
圖6
關(guān)于 raw stack,如圖7所示,我們可以用 bt -r 命令來查看。因為 raw stack 往往有幾個頁面,這里只截圖和 __stack_chk_fail 相關(guān)的這一部分內(nèi)容。
圖7
這部分內(nèi)容,有三個重點數(shù)據(jù)需要注意,panic 調(diào)用 __crash_kexec 函數(shù)的返回值,這個值是 panic 函數(shù)的一條指令的地址;__stack_chk_fail 調(diào)用 panic 函數(shù)的返回值,同樣的,它是 __stack_chk_fail 函數(shù)的一條指令的地址;ffffxxxxxxxx87eb 這個指令地址,屬于另外一個未知函數(shù),這個函數(shù)調(diào)用了 __stack_chk_fail。
Syscall number 和 Syscall table
因為帶有 system_call_fastpath 函數(shù)的調(diào)用棧,對應(yīng)著一次系統(tǒng)調(diào)用,而 panic 的調(diào)用棧是壞的,所以這個時候我們自然而然會疑問,到底這個調(diào)用棧對應(yīng)的是什么系統(tǒng)調(diào)用。
在 linux 操作系統(tǒng)實現(xiàn)中,系統(tǒng)調(diào)用被實現(xiàn)為異常。而操作系統(tǒng)通過這次異常,把系統(tǒng)調(diào)用相關(guān)的參數(shù),通過寄存器傳遞到內(nèi)核。在我們使用 bt 命令打印出調(diào)用棧的時候,我們同時會輸出,發(fā)生在這個調(diào)用棧上的異常上下文,也就是保存下來的,異常發(fā)生的時候,寄存器的值。
對于系統(tǒng)調(diào)用(異常),關(guān)鍵的寄存器是 RAX,如圖 8 所示。它保存的是系統(tǒng)調(diào)用號。我們先找一個正常的調(diào)用棧驗證一下這個結(jié)論。0xe8 是十進制的 232。
圖8
使用 crash 工具,sys -c 命令可以查看內(nèi)核系統(tǒng)調(diào)用表。我們可以看到,232 對應(yīng)的系統(tǒng)調(diào)用號,就是 epoll,如圖 9 所示。
圖9
這個時候我們再回頭看“函數(shù)調(diào)用棧”這節(jié)的圖 3,我們會發(fā)現(xiàn)異常上下文中 RAX 是 0。正常情況下這個系統(tǒng)調(diào)用號對應(yīng) read 函數(shù),如圖 10 所示。
圖10
從圖 11 中,我們可以看出,有問題的系統(tǒng)調(diào)用表顯然是被修改過的。修改系統(tǒng)調(diào)用表(system call table)這種事情,常見的有兩種代碼會做,這個相當辯證。一種是殺毒軟件,而另外一種是病毒或木馬程序。當然還有另外一種情況,就是某個蹩腳的內(nèi)核驅(qū)動,無意識地改寫了系統(tǒng)調(diào)用表。
另外我們可以看到,被改寫過的函數(shù)的地址,顯然和最初被 __stack_chk_fail 函數(shù)報出來的地址,是非常鄰近的。這也可以證明,系統(tǒng)調(diào)用確實是走進了錯誤的 read 函數(shù),最終踩到了 __stack_chk_fail 函數(shù)。
圖11
Raw data
基于上邊的數(shù)據(jù),來完全說服客戶,總歸還是有點經(jīng)驗主義。更何況,我們甚至不能區(qū)分,問題是由殺毒軟件導(dǎo)致的,還是木馬導(dǎo)致的。這個時候我們花費了比較多的時間,嘗試從內(nèi)存轉(zhuǎn)儲里挖掘出 ffffxxxxxxxx87eb 這個地址更多的信息。
有一些最基本的嘗試,比如嘗試找出這個地址對應(yīng)的內(nèi)核模塊等等,但是都無功而返。這個地址既不屬于任何內(nèi)核模塊,也不被已知的內(nèi)核函數(shù)所引用。這個時候,我們做了一件事情,就是把這個地址前后連續(xù)的,所有已經(jīng)落實(到物理頁面)的頁面,用 rd 命令打印出來,然后看看有沒有什么奇怪的字符串可以用來作為 signature 定位問題。
就這樣,我們在鄰近地址發(fā)現(xiàn)了下邊這些字符串,如圖 12 所示。很明顯這些字符串應(yīng)該是函數(shù)名。我們可以看到 hack_open 和 hack_read 這兩個函數(shù),對應(yīng)被 hacked 的 0 和 2 號系統(tǒng)調(diào)用。還有函數(shù)像 disable_write_protection 等等。這些函數(shù)名,顯然說明這是一段“不平凡”的代碼。
圖12
后記
宕機問題的內(nèi)存轉(zhuǎn)儲分析,需要我們足夠的耐心。我個人的一條經(jīng)驗是:every bit matters,就是不要放過任何一個 bit 的信息。內(nèi)存轉(zhuǎn)儲因為機制本身的原因,和生成過程中一些隨機的因素,必然會有數(shù)據(jù)不一致的情況,所以很多時候,一個小的結(jié)論,需要從不同的角度去驗證。
原文鏈接:https://developer.aliyun.com/article/768085?
版權(quán)聲明:本文中所有內(nèi)容均屬于阿里云開發(fā)者社區(qū)所有,任何媒體、網(wǎng)站或個人未經(jīng)阿里云開發(fā)者社區(qū)協(xié)議授權(quán)不得轉(zhuǎn)載、鏈接、轉(zhuǎn)貼或以其他方式復(fù)制發(fā)布/發(fā)表。申請授權(quán)請郵件developerteam@list.alibaba-inc.com,已獲得阿里云開發(fā)者社區(qū)協(xié)議授權(quán)的媒體、網(wǎng)站,在轉(zhuǎn)載使用時必須注明"稿件來源:阿里云開發(fā)者社區(qū),原文作者姓名",違者本社區(qū)將依法追究責任。 如果您發(fā)現(xiàn)本社區(qū)中有涉嫌抄襲的內(nèi)容,歡迎發(fā)送郵件至:developer2020@service.aliyun.com 進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,本社區(qū)將立刻刪除涉嫌侵權(quán)內(nèi)容。總結(jié)
- 上一篇: Flutter瀑布流及通用列表解决方案
- 下一篇: BERT 蒸馏在垃圾舆情识别中的探索