識別和避免反彙編中遇到的花指令
工作中需要花費大量時間和精力去分析花指令成為困擾逆向工程師以及安全響應人員的一件事。花指令就是一串沒有任何實際意義的指令。除了浪費時間之外,我發現有的人也會被他們發現的花指令感到震驚和興奮。這是因為他們在一個他們本來不能執行代碼的內存中發現了可執行代碼,也就相當於發現了一個漏洞利用程序或高級惡意軟體樣本。
在這篇文章中,我將通過介紹花指令產生的原因以及其我將討論如何識別花指令。 我們將重點關注x86反彙編,在其他的平台上也是類似的情況。
問題的產生
人們在分析彙編代碼的過程中,由於反彙編器將花指令反彙編成有效的指令,所以很多人默認就假設所有彙編代碼都是實際的代碼指令,這就是很多人所犯的第一個錯誤。由於X86平台的指令是密集編碼的,所以很多指令都是一個位元組編碼的。乍一看,幾乎所有數據的反彙編代碼都將產生可能有效的x86代碼。
例如,我生成了一個16kb的隨機數據文件,並對其進行了反彙編。
這產生了6,455個有效的32位x86指令,只有239個位元組的無法識別的數據。這意味著反彙編程序無法解析成有效指令的數據。可以看到超過98%的隨機數據可以被反彙編成有效的指令。
為了解釋我的意思是無法識別的數據,讓我們看看這個隨機數據的第一部分的反彙編,我們將遇到許多指令和一個位元組的無法識別的數據。這個片段中的第一列數據是數據偏移量。第二列顯示組成指令的位元組,第三列顯示這些位元組的反彙編表示。對於除高亮行(偏移量0x16)之外的所有行,反彙編顯示一個有效的指令。偏移量0x16表示可能看起來像一條指令的內容,但符號「db」只是彙編表示法來聲明一個位元組。反彙編器表示在這個位置是位元組0xF6,它不能將其識別為指令的一部分。x86指令集是編碼密集,以至於每個位元組值都可能是下一個指令的潛在開始。在這種情況下,0xF6可能是一個指令的有效開始,取決於它後面的位元組,但這種情況下的其與後面的位元組0x4E並不構成有效的操作數。在隨機數據的16kb中,274個無法識別的位元組只有27個不同的值。在這27個值中,唯一一個與常用英文字元範圍重疊的是字母「b」(0x62)。
在這篇文章中,我們將堅持使用32位反彙編,因為它的流暢度更高,但在16位和64位英特爾彙編中也會出現同樣的問題。 當先前提供的隨機數據被拆分為16位代碼時,它產生了96%的有效指令,而拆分為64位則產生了95%的有效指令。
你可能會想如何用大面積的零來表示著這段內存空間中沒有代碼存在,這該多麼美妙。高級反彙編可能會智能地識別大量的零代碼,並跳過反彙編,但它們還是將大量的零反彙編為有效的x86指令。 這裡有幾個反彙編零位元組來說明這一點:
英文文本的問題更為嚴重。我生成了一個包含隨機文本(lorem ipsum)的60kb文件,並對其進行了反彙編,生成了23,170條指令,並且沒有無法識別的數據。所以,100%的隨機文本被反彙編成有效的指令。
下面的片段顯示了標準Lorem ipsum反彙編的前三個單詞(「Lorem ipsum dolor」):
所以,我們在看反彙編代碼時,不僅要看對應序列能否被反彙編成功,還是要看其是否是有效的指令。
解決方法
儘管編寫更好的啟發式的腳本用來過濾掉花指令是可行的,但是目前我們最好的武器依舊是是人類的大腦。對於一個經驗豐富的逆向工程師來說,在本文中到目前為止所看到的代碼片段有很多東西,他們是學習判斷你所看到的代碼是否是花指令的關鍵。
特權指令
x86處理器有四個用於保護的「環」,就像「魔戒」一樣,但是不那麼有趣。其中兩個環(Ring1和Ring2)通常根本不使用。
內核模式執行Ring0和Ring3用戶模式則是常用的兩種特權級別。某些指令只能在Ring0中運行。這些特殊的特權指令也碰巧是單位元組操作碼,並且經常發生在反彙編花指令中。讓我們再來看看這個「Lorem ipsum」反彙編出來的指令,但這次我會突出特權指令的說明:如果你知道你正在查看的代碼不是作為操作系統引導載入程序、內核或設備驅動程序運行的,那麼當查看到這些特權指令時我們應該意識到這些反彙編不是真正有效的代碼。
突出顯示的指令都是IN和OUT指令的變體,主要作用是從硬體埠讀取和寫入數據。這些可能的指令必須在設備驅動程序中使用,如果在Ring3用戶模式下執行,將會產生一個異常。即使你試圖反彙編內核代碼,這些指令發生的頻率也會比你反彙編操作系統的任意文件的要高得多。以下是你將經常在反彙編的花指令中看到的一些常見Ring0指令的列表:
- IN (INS, INSB, INSW, INSD)
- OUT (OUTS, OUTSB, OUTSW, OUTSD)
- IRET
- IRETD
- ARPL
- ICEBP / INT 1
- CLI
- STI
- HLT
罕見的指令
在用戶模式代碼中有許多Ring3指令是有效的,但是在編譯代碼中非常少見。 我們可以將這些指令分為三類:便利指令,不太可能的數學指令和遠指針指令。我們來看看這些類別。
便利指令
- ENTER
- LEAVE
- LOOP (LOOPE/LOOPZ, LOOPNE/LOOPNZ)
- PUSHA
- POPA
彙編語言程序員可以使用ENTER和LEAVE指令來獲得函數序言和結語,這兩個指令可以手動的使用PUSH,MOV和SUB來完成。現代編譯器傾向於避免使用ENTER和LEAVE,因此大多數程序員也不會使用它們。因為他們一起佔據了操作碼範圍的近1%,所以它們在花指令代碼中的出現是非常普遍的。
LOOP指令及其條件同類的LOOPZ和LOOPNZ提供了一種用彙編語言編寫循環的非常直觀且有用的方法。 編譯器通常選擇不使用這些,而是使用JMP和條件跳轉指令來製作自己的循環。
PUSHA和POPA指令提供了將所有寄存器的狀態保存到堆棧的機制。這為彙編語言程序員提供了一個可能方便的類宏指令。由於它也存儲和恢復堆棧指針本身,這使得一個懶惰的編碼器在函數啟動時盲目地存儲寄存器信息,並在函數結束時恢復它們的潛在用途變得複雜了。你不會在編譯代碼中找到這些指令,但是它們也佔據了可用操作碼範圍的近1%,所以它們經常出現在花指令中。
不太可能的數學指令
- 浮點指令
- F*
- WAIT/FWAIT
浮點指令通常以字母「F」開始。雖然有些程序使用浮點數學,但大部分程序都不使用浮點數學。浮點指令佔用操作碼範圍的很大一部分,所以它們在花指令中的是普遍存在的。這就是你對你正在嘗試進行逆向工程的代碼設計的知識將派上用場的地方。如果您使用3D圖形對程序進行反向工程,則需要查找潛在的大量浮點指令。對於我們在FLARE團隊所做的工作,惡意軟體分析,浮點數學是非常罕見的。值得注意的例外是shellcode通常使用一些浮點指令作為獲取指向自身的指針。
- SAHF
- LAHF
SAHF和LAHF指令將AH寄存器的內容複製到標誌寄存器EFLAGS中。這是一種編程行為,不會從高級語言轉換下來,因此編譯器通常不會輸出這些指令。如果不是手動編寫彙編代碼,那麼這些指令將會很少出現。由於這些是單操作碼範圍內的單位元組指令,所以它們經常在反彙編花指令中出現。
- ASCII調整指令
- AAA
- AAS
- AAM
- AAD
「AA」系列指令涉及以二進位編碼的十進位形式處理數據。這是一個比較早的編碼方式,基本很難再現代計算中遇到。但是,您將經常在反彙編花指令中遇到這些指令,因為它們是單位元組指令。
- SBB
SBB指令與SUB相似,只是它將進位標誌添加到源操作數。這可以在合法的代碼中看到,特別是當試圖對大於機器字長的數字進行算術運算時(32位代碼中的64位數學)。不幸的是,SBB指令不少於九個操作碼,占可能範圍的3.5%。它們不是單位元組指令,但是有很多形式和很多操作碼,它們經常在反彙編的的花指令中出現。
- XLAT
在你的彙編代碼中XLAT是一個有趣的指令。它沒有直接的翻譯到一個單一的高級語言結構,因此,編譯器不像我們在FLARE團隊那樣喜歡它。由於它是一個單位元組指令,所以你會發現它比使用彙編語言找到程序員更頻繁。
- CLC
- STC
- CLD
- STD
這些指令清除並設置進位和目標標誌。 它們可能在流操作附近的編譯器生成的代碼中找到(通常會在REP前綴的地方)。儘管如此,由於它們都是單位元組指令,它們在花指令中出現的可能性非常高。
遠指針指令
- LDS
- LSS
- LES
- LFS
- LGS
在英特爾架構中,遠指針的使用並不存在於16位時代。但是,設置遠指針的指令仍然佔用兩個單位元組操作碼和兩個位元組操作碼範圍中的三個值。因此,你會看到這些指令經常出現在反彙編花指令中。
指令前綴
x86中的指令可以有前綴。前綴位元組通常會修改後續指令的行為。前綴的常用用法是改變操作數的大小。例如,如果您正在以32位模式執行指令,並且希望使用16位寄存器或操作數執行計算,則可以在計算指令中添加一個前綴,以通知CPU它是一個16位指令而不是一個32位的。有許多這樣的前綴,不幸的是,其中許多都落在ASCII表的字母範圍內。這意味著,當反彙編ASCII文本(如我們的lorem ipsum),指令前綴將是非常普遍的。這些指令前綴會出現在正常指令中,但沒有在花指令中出現的頻繁。
如果您正在拆解32位代碼,並且您看到大量使用的16位寄存器(AX,BX,CX,DX,SP,BP等,而不是EAX,EBX,ECX,EDX,ESP和EBP ),你可能正在看花指令。反彙編器還將代表在指令助記符之前添加了某些符號的其他前綴。如果你在代碼中經常看到任何這些關鍵字,那很可能是花指令代碼:
- LOCK
- BOUND
- WAIT
段選擇器
- FS
- GS
- SS
- ES
在16位模式下,使用段寄存器(CS,DS,FS,GS,SS,ES)完成定址存儲器。程序的代碼通常是基於CS「代碼段」寄存器來引用的,程序處理的數據是從DS「數據段」寄存器引用的
ES,FS和GS是額外的數據段寄存器,合法地用於32位代碼,但這是一個更高級的話題。有段選擇符前綴位元組,可以在指令之前添加段,強制指定它參考內存基於特定的段,而不是它設計使用的默認段。
由於這些都佔用了那個寶貴的單位元組操作碼空間,所以它們會在反彙編的花指令中頻繁出現。從我反彙編的隨機數據中得到的下列指令顯示了一個在沒有定址內存的指令上使用的GS寄存器的段選擇符前綴:反彙編垃圾也會比普通代碼更頻繁地使用這些段寄存器,而且編譯器不會輸出。 讓我們來看看反彙編花指令的另一行:
該指令從堆棧彈出SS「堆棧」寄存器。這是一個完全有效的指示; 然而,這是反彙編的32位代碼和段寄存器通常不會像在16位模式中那樣改變。在同樣的反彙編中,只有上面幾行代碼是另一個奇怪的代碼行:
32位體系結構支持更多段寄存器的定址,而不是段寄存器的名稱。這條指令是將一些數據移動到第7段寄存器中,我的反彙編器命名為「segr7」。
結論
往小了說,反彙編花指令代碼可能會浪費你的時間和精力。在最糟糕的情況下,它可以讓你正在分析的數據是不正確的。在這篇文章中,我們學會了通過識別和理解不可能的代碼來發現反彙編的花指令。我們打破了不尋常的代碼分類為最常見的情況,並討論他們的意義,為什麼他們會頻繁發生,為什麼他們不應該出現。通過學習這些指標,你可以輕鬆地發現反彙編中的花指令代碼,節省你的時間。
本文由看雪翻譯小組 skeep 編譯,來源fireeye@Nick Harbour 轉載請註明來自看雪社區
推薦閱讀:
※【遊戲框架系列】嵌入Bochs
※如何評價微軟在WinHEC 2016大會上演示的基於ARM的完整版Windows 10?
※x86寄存器使用的疑惑?
※x86 結構會被 ARM 結構淘汰嗎?