本文由作者首發於合天智匯:http://www.heetian.com/info/840
之前兩篇文章,針對惡意代碼爲了確保自身只有一個範例在執行進行了正向開發和逆向分析。逆向入門分析實戰(一)、逆向分析入門實戰(二)
這種現象在惡意代碼中非常常見,現在對上次的內容進行一個簡要的回顧和擴充套件:
使用ida pro對惡意代碼進行反彙編時會發現如下特徵:
1、可以找到使用了windows的api函數 CreateMutex,該函數其中一個參數爲互斥變數名。
2、在呼叫CreateMutex函數之後,通常會呼叫GetLastError函數,返回值與ERROR_ALREADY_EXISTS相同,即會使用cmp指令對返回值eax和ERROR_ALREADY_EXISTS對應的常數(16進位制的B7,10進位制的183)進行對比。
當然,本篇文章不是爲了繼續講這個,現在我們應該更加深入的分析其他惡意代碼常見的手法,這次分析惡意代碼常見的獲取計算機基本資訊的函數,同樣是先通過正向開發,然後進行逆向分析。
通常,惡意代碼比較關注的計算機基本資訊包括計算機名,計算機使用者名稱,計算機的版本。下面 下麪我們就以獲取這三個基本資訊爲例,進行正向開發和逆向分析。
一 、正向開發,獲取計算機基本資訊
首先,我們需要掌握幾個知識點:
1、GetComputerName函數,該函數有兩個參數,第一個參數是一個緩衝區,用來接收計算機名。第二個參數指定該緩衝區的大小。對於經常使用Python進行程式設計的人來說,可能覺得有點奇怪,因爲以Python語言的風格可能會是這樣的:
computerName=GetComputerName()
函數無需傳遞參數,返回值即爲計算機名。但是對於windows api很多函數來說,都會是這種風格,用某一個參數用來接收返回值,習慣就好。
2、GetUserName與GetComputerName函數用法十分類似。
3、GetVersionEx是用來獲取計算機版本的函數,該函數只有一個參數,我乍一看覺得這個函數還挺簡單,肯定和上面兩個函數一樣直接把返回值即計算機的版本返回到這個參數裡了,當我仔細去看MSDN文件時發現,呵,參數居然是lpVersionInfo,這是什麼破東西?經過仔細調研發現,這是一個指向OSVERSIONINFO結構體的指針,好吧,當初學C語言的時候就覺得指針這玩意賊煩,現在又來了。那就好好再學習一下指針吧!這個指針指向OSVERSIONINFO結構體,而這個結構體就是用來承載返回值系統版本的。也就是我們先建立一個OSVERSIONINFO結構體,之後把結構體的指針作爲參數傳入GetVersionEx函數即可,然後再從OSVERSIONINFO結構體中讀取相應的系統版本。
接下來看程式碼:
這段程式碼中,在主函數中先宣告子函數,然後呼叫子函數,之後使用getchar函數,用來獲取一個鍵盤輸入。爲什麼加這個getchar函數?主要是因爲如果不加這個,有的時候,當你直接雙擊這個程式時,可能命令列一閃而過讓你看不清輸出的內容。
子函數中,首先是獲取計算機名,建立了一個szComputerName字元陣列用來接收計算機名,陣列大小爲MAXBYTE,爲一個常數,通常爲256,當然不同操作系統版本對應的大小可能不太一樣,在後面逆向的時候我們可以檢視。
之後,同理獲取計算機使用者名稱。最後獲取系統版本,其中if語句裡的條件可能看不太懂,這個主要是OSVERSIONINFO結構體的成員變數需要查閱MSDN即可。dwMajorVersion就對應不同的系統版本。當對應的值爲6並且dwMinorVersion爲1則是windows7或者windowsserver 2008 R2。如果想瞭解更多關於系統版本的內容可以檢視MSDN:
https://docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-osversioninfoa
之後編譯,執行:
使用windows7時的效果:
該主機計算機名確實爲PC:
版本爲windows 7:
而使用windows 10時的效果:
二、逆向分析:
將程式拖入ida pro:
首先呼叫puts將字串輸出,之後呼叫getSystemInfo函數,這就是我們前面編寫的子函數。雙擊進入發現有很多行彙編程式碼,如果我們現階段就要把每一行都弄得很明白,那要花很長時間,學習很多知識,所以和上次一樣,我們要分清主次,把所呼叫的函數分析清楚即可,等後續自己「功力深厚」一些,再努力把每一行彙編程式碼都分析清楚。我們選中call,會發現所有的call指令都變黃了,這樣便於我們分析,爲了便於描述,將每一行彙編的地址調出來,點選options->General->Line prefixes,將其勾選:
1、逆向分析獲取計算機名和使用者名稱
此部分內容對應的彙編程式碼如下:
檢視0040175E處的call命令,此處呼叫GetComputerNameA函數,之前提到過後面多出一個A指的是當前使用的爲ASCII環境。
在這個函數前面有兩個備註分別是lpBuffer和nSize這兩個其實就是GetComputerNameA的參數,我們之前學習的時候,一般是使用push將參數入棧,此處怎麼不是push?其實是一樣的,仔細檢視彙編指令push即可知道,其實push指令將參數入棧後,棧頂指針esp便會指向該參數,而此處使用的是mov將參數的值賦值給esp所指的地址空間,本質上是一樣的。至於爲何將nSize賦值給esp+4所指的空間,這是因爲棧的增長方向以及參數所佔的記憶體大小有關,此處簡要介紹一下,因爲標準呼叫約定中需要從右往左入棧,需要先將nSize參數入棧,再將lpBuffer參數入棧。根據棧的特點,先入棧的參數地址高,後入棧的參數地址低。即棧是一種由高地址向低地址擴充套件的數據結構。有興趣的可以進一步查閱push指令和函數呼叫約定相關的資料。
執行完0040175E處的call命令返回值便會儲存在lpBuffer對應的緩衝區內,此處即ebp+Buffer所指的空間內。之後呼叫printf函數,進行格式化輸出,其中同樣涉及到esp+4,原因也是因爲需要將GetComputerNameA獲取到的計算機名先入棧,再將「computer name is %s \r\n」入棧。如果你熟悉格式化字串漏洞的話,你會有一種很熟悉的感覺,當然如果你已經掌握了這裏的知識,可以深入去瞭解格式化字串漏洞的相關內容。
獲取使用者名稱的逆向分析過程與獲取計算機名的過程類似,此處不過多介紹了。
2、逆向分析獲取操作系統版本
這段內容對應的彙編程式碼如下:
首先檢視00401812處的程式碼,94h對應10進製爲148,這個數值便是sizeof(OSVERSIONINFO)的返回值,即OSVERSIONINFO結構體的大小。之後使用lea指令,它是LoadEffective Address的縮寫,即載入有效地址,將該結構體所在的地址賦值給eax,之後將eax中儲存的地址空間存入esp所指的空間中,整個過程即可完成結構體指針入棧工作。
然後呼叫GetVersionEXA函數,返回值將會放置在結構體中。之後將該結構體中的dwMajorVersion成員變數與6對比,dwMinorVersion與1對比,之後使用jnz命令來決定是否跳轉,之前的文章將提及過,該命令是jump not zero的縮寫,當不等於0時跳轉。如果此處不是很熟悉,建議查閱之前的文章,掌握cmp和jnz以及zf標誌暫存器的相關知識。再之後便使用puts輸出字串。
總結:
獲取這些基本的資訊其實很簡單,只需呼叫幾個windows API即可,對惡意代碼進行逆向分析也很容易定位到是否在獲取這些基本資訊。而如果我們通過對這些windowsAPI呼叫的過程進行深入分析,便可以進一步掌握C語言、彙編語言和數據結構等相關的原理,比如指針,lea指令、棧的工作原理,或者進一步學習格式化字串漏洞。
參考書籍:
《C++駭客程式設計揭祕與防範》冀雲著,第1版,2012.6--北京,人民郵電出版社
《C++反彙編與逆向分析技術揭祕》錢松林,趙海旭著--北京:機械工業出版社,2011年9月。
《惡意代碼分析實戰》 (美)Michael Sikorski / Andrew Honig 著,諸葛建偉,姜輝,張光凱譯 -- 北京:電子工業出版社,2014年4月,原書名: Practical Malware Analysis: The Hands-On Guide to DissectingMalicious Software。
《彙編語言》王爽 著--2版,北京:清華大學出版社,2008年4月。
《逆向工程核心原理》,李承遠著--北京,人民郵電出版社,2014年第1版。