三、靜態分析高級技術
***********************************************************************************************************************************************************************
知識補充——匯編基礎:
************
*基礎知識:*
************
本節內容需要一定的反匯編基礎,需要學習一定的匯編知識
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1、計算機系統被分為六個抽象層次:
硬件層(熟悉《計算機科學導論》的相關內容)
微指令層(即固件層,惡意代碼分析時同常不予關注)
機器碼層(由高級語言編寫的計算機程序編譯而來)
低級語言層(主要是匯編語言,惡意代碼分析主要關注該層次)
高級語言層(如c/c++等)
解釋性語言層(位於最高層,jave,c#等,獨立於操作系統)
2、掌握逆向工程
運用反匯編器"disassembler、IDA Pro"將惡意代碼的二進制文件翻譯為匯編語言。尤其對x86和x64等語言的學習需要有一定了解,可以參考《匯編語言的藝術-Randall Hyde》(清華大學出版社)
3、x86體系結構
(1)三種硬件組成:
中央處理器(CPU):執行代碼
內存(RAM):存儲所有的數據和代碼
輸入/輸出系統(I/O):對外部設備提供接口
(2)一個程序內存的節
通常由低地址向高地址依次為:
棧->堆->代碼->數據
數據:程序初始值,全局值和靜態值
代碼:交付CPU執行,決定了程序是做什么的
堆:程序執行期間所需的動態內存空間
棧:程序函數的局部變量及參數存放位置,同時控制程序執行流
*注意:這個順序也不是一定的,四個主要的內存節可以分布在內存的任意位置
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
具備編譯器使用約定的知識基礎
********************************************************************
************
*主干知識:*
************
x86匯編語言
1、指令:
(1)助記符:表示要執行的指令
mov:移動數據
(2)目標操作數:說明指令要使用的信息,如:寄存器或數據
ecx:指向寄存器
(3)源操作數:
2、操作碼和字節序:
操作碼:cpu所執行的指令
字節序:一個大數據項中,最高位還是最低位被排在第一位(即排在最低的地址上)
注意一:x86采用小端字節序,如:0x42在x86架構上的表示為0x42000000而不是0x00000042
注意二:一些惡意代碼在網絡通信時必須改變字節序,因為網絡數據使用大端字節序,而x86程序使用小端字節序。(如127.0.0.0網絡通信中表示為0x7F000001,而在本地內存的小端字節序下,表示為0x0100007F)
3、操作數:說明指令要使用的數據
分為三種類型:
立即數操作數:是一個固定值,如:0x42
寄存器操作數:指向寄存器,如:ecx
內存地址操作數:指向感興趣的值所在的內存地址,一般由方括號內包含值、寄存器或方程式組成,如:[eax]
4、寄存器:是可以被CPU使用的少數數據存儲器
所有的通用寄存器都是32位,可以再匯編代碼中以32位或16位引用,如:EDX指向完整的32位寄存器,而DX指向EDX寄存器的低16位
x86下有四類常用寄存器:
(1)通用寄存器:CPU在執行期間使用
EAX(AX,AH,AL)
EBX(BX,BH,BL)
ECX(CX,CH,CL)
EDX(DX,DH,DL)
EBP(BP)
ESP(SP)
ESI(SI)
一些x86指令只能使用特定的寄存器,如乘法和除法指令只能使用EAX和EDX
EAX通常儲存了一個函數調用的返回值,如果看到一個函數調用后馬上用到了EAX寄存器,可能就是在操作返回值
(2)段寄存器:用於定位內存節
CS
SS
DS
ES
FS
GS
(3)狀態標志:用於做出決定
EFIAGS
這是一個標志寄存器,x86中是32位的,每一位只一個標志,執行期間,每一位要么是置位(值為1),要么是清除(值為0),並由這些值來控制CPU的運算,或者給出某些CPU運算的值,對惡意代碼分析,重要的標志如下:
ZF:當一個運算的結果等於0時,ZF被置位,否則被清除
CF:當一個運算的結果相對於目標操作數太大或太小時,CF被置位,否則被清除
SF:當一個運算的結果為負數,SF被置位;若結果為正數,SF被清除。對算術運算,當運算結果的最高位值為1時,SF也會被置位
TF:用於調試,當它被置位時,x86處理器每次只執行一條指令
(4)指令指針:
EIP
EIP寄存器又稱為(指令指針或程序計數器,保存程序將要執行的下一條指令在內存中的地址,唯一作用就是告訴處理器接下來要做什么
5、簡單指令(采用intel匯編語法)
(1)mov指令:用於將數據從一個位置移動到另一個位置,對內存數據進行讀寫操作
格式:mov destination(目標地址),source(源數據/地址)
mov eax,ebx 將EBX中的內容復制至EAX寄存器
mov eax,0x42 將立即數0x42復制至EAX寄存器
mov eax,[0x4037C4] 將內存地址0x4037C4的4個字節復制到EAX寄存器
mov eax,[ebx] 將EBX寄存器指向的內存地址處4個字節復制到EAX寄存器
mov eax,[ebx+esi*4] 將ebx+esi*4等式結果指向的內存地址處4個字節復制到EAX
(2)lea指令:用來將一個內存地址付給目的操作數
格式:lea destination,source
lea eax,[ebx+8] 將EBX+8的值給EAX
*與mov指令對比:mov是尋址方式復制,lea是直接賦值。如:lea eax,[ebx+8]是將EBX+8的值給EAX,而mov eax,[ebx+8]是加載內存中地址為EBX+8處的數據。從功能上來說lea eax,[ebx+8]和mov eax,ebx+8是等效的,但是這種尋址形式的mov指令時無效的
(3)inc指令:目標操作數+1
格式:inc destination
inc eax 將EAX寄存器的值遞增1
(4)dec指令:目標操作數-1
格式:dec destination
dec eax 將eax寄存器的值遞減1
(5)move指令:(DST)<-(SRC) 將原操作數(字節或字)傳送到目的地址。是數據的傳送,即拷貝的功能(數據類型不變)。
格式:move destination,source
move cx,5 把5放入ecx寄存器中
指令支持的尋址方式:目的操作數和源操作數不能同時用存儲器尋址方式,這個限制適用於所有指令。指令的執行對標志位的影響:不影響標志位。
指令的特殊要求:目的操作數DST和源操作數SRC不允許同時為段寄存器;目的操作數DST不能是CS,也不能用立即數方式。
(6)mul指令:乘法運算指令
格式:mul source
mul ecx 將ecx的值與eax相乘
如果SRC是字節操作數,則把AL中的無符號數與SRC相乘得到16位結果送AX中,即:AX←(AL)*(SRC)。如果SRC是字操作數,則把AX中的無符號數與SRC相乘得到32位結果送DX和AX中,DX存高16位,AX存低16位,即:AX←(AL)*(SRC)。受影響的標志位有:CF和OF(AF、PF、SF和ZF無定義)。如果乘積結果的高半部分等於零,則CF=OF=0,否則CF=OF=1
mul source指令總是將eax乘上source,乘法的結果以64位的形式分開儲存在兩個寄存器:EDX和EAX中,其中EDX存儲了高的32位,EAX存儲低的32位。
(7)div指令:除法運算指令
格式:div source
div ecx 將eax的值除以ecx的值
與mul運算方向相反,將edx和eax合起來儲存的64位值除以source,除法的商存儲到eax,余數存儲在edx中
(7)add指令:加法運算指令
格式:add destination,value
add eax,ebx 將ebx的值加上eax並將結果保存至eax
(8)sub指令:減法運算指令
格式 sub destination,value
sub eax,0x10 eax寄存器的值減去0x10
(9)or,and,xor指令:分別對應“或”“與”“非”,用法與add和sub相同
xor eax,eax 將eax寄存器清零
or eax,0x7575 對eax進行與0x7575的or操作
*有一種快速將寄存器置零的用法,用該指令實現:xor eax,eax,這種用法只需要2個字節。
(10)shr/shl指令:用於對寄存器做移位操作:右移/左移
格式:shr destination,count
mov eax,0xA 將eax寄存器左移兩位,這個指令將導致eax=0x28,
shl eax 因為1010(0xA的二進制表示)左移兩位之后為101000(即0x28)
*移位運算可以作為乘法的優化算法:“一般的,左移n位,相當於乘以2的n次方,右移n位相當於除以2的n次方”
(11)ror/rol指令:循環移位指令,但移出的那一位會被填到另一端空出來的位上(即:右循環ror會將最低位循環移到最高位,左循環rol會將最低位循環移到最高位
格式:與shr/shl相同
mov bl,0xA 將bl寄存器循環移位兩位,這兩條指令將導致bl=10000010,因為1010向右循環移動2位為10000010
ror bl,2
(12)nop指令:無運算指令,直接執行下一條指令
*注:nop實際上是xchg eax,eax的偽名字,這條指令的opcode(操作碼)是0x90。
*在緩沖區溢出攻擊中,當攻擊者無法完美地控制利用代碼,就經常使用nop滑板,它起到了填充代碼的作用,以降低shellcode可能在中間部分開始執行所造成的風險。
6、棧
采用壓和彈得操作來刻畫的數據結構,是一種后入先出的結構,由高地址到低地址依次使用,例如依次壓入數字1、2、3,則第一個被彈出的為3,最后一個被彈出的為1。
x86架構中用ESP和EBP兩個寄存器來支持棧:
ESP是棧指針,包含了指向棧頂的內存地址,數據被壓入或彈出棧的時候,該寄存器的值會改變。
EBP是棧基址寄存器,在一個函數中會保持不變,,程序會把它當成定位器,用來確定局部變量和參數的位置。
棧操作指令:
push
pop
call
leave
enter
ret
popa
popad
條件指令:
test
cmp
分支指令:
jmp
jcc(條件跳轉語句總稱)
*************************************
重復指令:
x可替換為b,w,d,分別表示字節,字,雙子
movsx
cmpsx
stosx
scasx
rep
repe,repz
repne,repnz
*************************************
*函數調用*
*棧的布局*
*條件指令*
*分支指令*-*跳轉指令*
*重復指令*
*************************************
7、c語言主函數和偏移
p77頁實例
8、關於x86的其他指令,可參考x86完整架構手冊
www.intel.com/products/processor/manuals/index.htm
主要內容:
第1卷:基本架構
第2A卷:指令集參考A-M;第2B卷:指令集參考N-Z(這兩卷很重要)
第3A卷:系統編程指南(第一部分);第3B卷:系統編程指南(第二部分)
優化參考手冊
匯編知識基礎補充(完)
***********************************************************************************************************************************************************************
靜態高級分析實操部分:
(一)IDA Pro的使用
參考書籍:《The IDA Pro Book:The Unofficial Guide to the World's Most Popular Disassembler,2nd Edition》(No Starch出版社)
(二)匯編中的c代碼結構
1、全局變量通過內存地址引用,而局部變量通過棧地址引用
2、對if語句的判別
(1)、如果在cmp比較指令后直接接有jnz(條件跳轉)等跳轉指令,表示,如果比較結果為不相等(為假)時則跳轉,否則繼續執行,如:
cmp eax,[ebp+var_8]
jnz short loc_40102B
表示如果eax儲蓄器的值與eax+var_8不相等,則跳轉至40102B處
(2)、if語句必定有一個條件跳轉指令,但注意不是所有的跳轉指令都對應if語句。
(3)、注意兩個跳轉指令的區別:
jz為條件跳轉指令:結果為零或相等時跳轉
jnz為條件跳轉指令,結果不為零或不相等時跳轉
jmp代碼跳轉指令,當跳轉執行完畢或都滿足條件時,跳過條件語句,直接執行后面的代碼
3、反匯編算術指令由編譯器決定
4、對循環的識別
(1)對for循環的識別
for循環總是包含4個組件——初始化,比較,執行指令,遞增或遞減
補充匯編條件指令:(跳轉指令前一般都會有test、cmp等比較指令)
jge指令:當大於或等於時進行跳轉;
jae指令:當高於等於,或者進位標志轉移清零時跳轉;
(2)對while循環的識別
與for循環的結構很相似,區別在於少一個遞增或遞減的組件。
有一個條件跳轉和一個無條件跳轉
5、理解函數的調用約定
(1)cdecl調用約定
參數從右向左依次壓入棧,當函數完成調用時,由調用者自行清理棧,並將返回值保存在EAX中。
***********************************實例:
實際c代碼:
int test(int x,int y,int z);
int a,b,c,ret;
ret=test(a,b,c);
—————————————————反匯編代碼:
1、push c
2、push b
3、push a
4、call test
5、add esp,12
6、mov ret,eax
注:第5行的操作為調用者清理棧
***********************************
(2)stdcall調用約定
該約定是Windows API的標准約定,除了在函數完成時不需要調用者清理棧外,與cdecl約定的反匯編代碼一樣。
*調用windows api的代碼都不需要清理棧,清理棧的工作由實現api函數代碼的dll程序承擔,所以在該約定下,上述實例的代碼中會少掉第五行的內容
(3)fastcall調用約定
*約定細節會隨編譯器的不同而有所不同,但工作方式大體相同:
前一些參數被傳到寄存器中,(在微軟fastcall約定中還會有兩個備用寄存器:EDX和ECX),如果需要的話,剩下的參數再以從左向右的次序加載到棧上
*該約定的優點:使用該約定更高效,因為代碼不涉及很多的棧操作
***********************************
【壓棧與移動】
不同的編譯器在選擇壓棧還是移動時調用的約定不同,如visual studio多半會選擇壓棧(push),而gun多半會選擇移動(mov)。
***********************************
6、分析switch語句
【注】:switch有兩種樣式編譯:if樣式和使用跳轉表
(1)if樣式:
該樣式下的switch反匯編與if反匯編代碼十分類似,但:
switch的反匯編代碼往往有連續的好幾組同等級別的cmp和jcc指令。而if的反匯編雖然有可能存在連續,但不會是同等級別的一組指令。
總體格式是:先依次比較,滿足條件后再執行case分支操作。
(2)跳轉表
case后的值先減一,然后把減一后的值乘以字節數跳轉到跳轉表所在的基地址位置,再在該基地址處予以跳轉,到代碼執行處。
【比較】:if樣式通常是分多次判斷跳轉,而跳轉表通常是先減一,然后依次判斷,兩次跳轉
7、反匯編數組【 !】
數組是通過一個基地址作為起始點訪問的,往往由一個ecx寄存器所謂索引使用,ecx乘以數據類型的字節數來指明元素的大小,結果值與數組的基地址相加,來訪問正確的數組元素
8、識別結構體【 !】
結構體通過一個起始指針的基地址來訪問
9、分析鏈表遍歷
需要識別出包含指針的對象,並且這些指針指向同一類型的對象。還要識別其遞歸結構
(三)分析惡意windows程序
1、Windows API
windows API采用匈牙利表達法,即通過前綴表示數據類型。
(1)類型:
WORD(w):表示16位的無符號數值
DWORD(dw):表示雙字節、32位的無符號數值
Handles(H):對象索引,句柄中存儲的信息並沒有文檔化,且只被api操作
Long Pointer(L):指向另一類型的指針,如:LPByte指向字節的指針、LPCSTR指向字符串的指針,字符串通常由LP作為前綴
Callback:表示一個將會被windows api調用的函數
(2)句柄
是在操作系統中被打開或創建的項,比如窗口、進程、模塊、菜單、文件等
句柄可以引用對象或內存位置,很像指針,但與指針不同的是:不能進行數學運算,且並不總是表示對象地址
(3)文件系統函數
CreatFile:用來創建和打開文件,可以打開文件、管道、流、I/O設備
ReadFile和WriteFile:用來對文件進行讀寫操作,都作為流進行
CreatFileMapping和MapViewOfFile:前者負責從磁盤上加載一個文件到內存中;后者返回一個指向映射的基地址指針,被用來訪問內存中的文件。程序可以使用后者返回的指針,在文件中任意位置進行讀寫
(4)特殊文件
有些特殊文件的訪問方式和普通文件不一樣,不能通過盤符與文件夾進行訪問,但這些特殊文件可以提供對系統硬件和內部數據更強的訪問能力
【共享文件】
是以\\serverName\share或
\\?\serverName\share開頭命名的特殊文件
用來訪問共享目錄中的目錄或文件,\\?\前綴告訴操作系統禁用所有的字符串解析,並允許訪問長文件名
【通過名字空間訪問的文件】
名字空間可以被認為是固定數目的文件夾,滅個文件夾中保存不同類型的對象。
底層的名字空間是NT名字空間,以前綴\開始,這個名字空間可以訪問所有設備一級所有在自其中存在的其他名字空間
以前綴\\.\開始的Win32設備名字空間,可以直接訪問物理設備,並像文件一樣讀寫操作,從而繞過文件系統。惡意代碼可以通過這個方式讀寫數據而無需創建文件,以避開安全程序檢測
【備用數據流(ADS)】
這個特性允許附加數據被添加到一個已存在的NTFS文件中,相當於添加一個文件到另外一文件中。額外數據在列一個目錄時不會被顯示出來,並且當顯示文件內容是也不顯示,只有訪問流時才可見
惡意代碼可以用其來隱藏數據
2、Windows注冊表
注冊表用來保存操作系統與程序的配置信息,比如設置和選項,惡意代碼功能也可由注冊表分析出來
(1)注冊表根鍵
HKEY_LOCAL_MACHINE(HKLM):保存對本地機器全局設置
HKEY_CURRENT_USER(HKCU):保存當前用戶特定的設置
HKEY_CLASSES_ROOT:保存定義的類型信息
HKEY_CURRENT_CONFIG:保存關於當前硬件配置的設置,特別是與當前和標准配置之間不同的部分
HKEY_USERS:定義默認用戶、新用戶和當前用戶
(2)IDA PRO一些對注冊表的注釋
samDesired:指示了安全訪問請求的類型
ulOptions:是一個表示調用選項的無符號長整數
hkey:被訪問的根鍵的句柄
(3)注冊表腳本
注冊表版本號
[要修改的子鍵](有中括號)
"要修改的項目名"="鍵值"(有引號)
【例】:
**************************************
Windows Registry Editor Version 5.00
[HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Run]
"MaliciousValue"="C:\Windows\evil.exe"
**************************************
3、網絡API
(1)伯克利兼容套接字
在windows中有Winsock庫實現,在ws2_32.dll中,其中以下幾個是常用的:
socket:創建套接字
bind:將套接字綁定到特定端口,在accept之前調用
listen:預示一個套接字將進入監聽,等待入站連接
accept:向一個遠程套接字打開一個連接,並接受連接
connect:向一個遠程套接字打開一個連接,遠程套接字必須在等待連接
recv:從遠程套接字接收數據
send:發送數據到遠程套接字
【注】:WSAStartup函數必須在其他網絡函數之前調用,用來為網絡庫分配資源。在調試代碼查找網絡連接入口是,在WSAStartup函數中設置斷點,一般網絡入口應該跟在后面不遠處
(2)服務端和客戶端
惡意代碼既可以是服務端也可能是客戶端
在連接一個遠程套接字的客戶端中,會依次有socket調用,connect調用,如果需要的話,會跟着send和recv調用
一個監聽入站連接的服務端,順序則依次是socket函數創建套接字、bind函數將這個套接字附加到一個端口、listen調用將這個套接字設置為監聽狀態、然后accept調用掛起,等待遠程套接字的連接,如果需要還可能有send和recv調用
(3)WinINet API
該函數保存在wininet.dll中,是一個高一級的API,實現應用層協議,如http等,惡意代碼可以使用其連接服務器,獲得指令
InternetOpen:初始化一個到互聯網的連接
InternetOpenUrl:訪問一個URL
InternetReadFile和ReadFile:允許程序從一個來自互聯網的下載文件中讀取數據
4、跟蹤惡意代碼的運行
(1)DLL(動態鏈接庫)
【惡意代碼使用DLL的方式】
保存惡意代碼:通過被其他程序調用公共DLL來將自己加載到其他進程上
通過使用Windows DLL:可以達到和操作系統交互的目的
通過使用第三方DLL:和其他程序交互,或擴充惡意代碼本身功能
【基本DLL結構】
和exe文件一樣使用PE格式,只有一個標志,有很多到處函數,導入函數較少。
主函數是DLLMain,被指定為文件的入口點
(2)進程
訪問某內存地址的惡意程序,只會影響包含惡意代碼的那個進程在這個位置上保存的東西,系統中其他使用這個地址的程序則不會受到影響
惡意代碼通常會使用CreatProcess函數來創建新進程
【注】:
通過CreatProcess創建的遠程shell,要找到這台遠程主機,需要判斷其使用的套接字在哪里被初始化。要發現哪個程序將被運行,需要通過被執行的命令行地址來查找字符串
(3)線程
通過CreatThread函數來創建線程,函數調用者指定一個起始地址,被稱為start函數。當分析調用CreatThread的代碼時,除了分析這個start函數外,還需要分析調用CreatThread的剩下的代碼
【惡意代碼使用CreatThread的方式】:
使用CreatThread加載一個新的惡意庫文件到進程中,通過調用CreatThread時將起始地址設置為LoadLibrary的地址;
可以為輸入和輸出創建兩個線程,一個用來在套接字或管道上監聽,並輸出到一個進程的標准輸入里;一個用來從標准輸出讀取數據,並發送到套接字或管道上。發送所有信息到單一套接字或管道,來和運行的程序進行無縫通信;
(4)使用互斥量的進程間協作
互斥量用於控制共享資源的訪問。一次只允許一個進程訪問某共享資源
互斥量通常使用硬編碼的名字,將它們作為基於主機的感染跡象是很好的選擇,如果一個互斥量被兩個不使用其他通信方式通信的進程使用時,它的名字必須相互一致
線程通過一個對WaitForSingleObject的調用獲取對互斥量的訪問。完成互斥量的使用后使用ReleaseMutex函數
可以通過CreateMutex函數創建,進程可以通過OpenMutex調用來獲取另一個進程中互斥量的句柄
(5)服務
作為服務安裝也是惡意代碼執行附加代碼的另一種方式
通過一些API來安裝和操作:
OpenSCManager:返回一個服務控制管理器的句柄,被用來進行所有后續與服務相關的函數調用。和服務交互的代碼會調用
CreateService:添加一個新服務到服務控制管理器,並允許調用者指定服務是否在引導時自動啟動或手動啟動
StartService:啟動服務,並且在服務被設置成手動啟動時使用
(6)組件對象模型(COM)
這是一個標准接口,使得不同軟件組件在不知道其他組件代碼的接口規范時,相互之間可以進行調用
【注】:
每個使用COM的線程,必須在調用任何其他COM庫函數之前,至少調用一次OleInitialize或CoInitializeEx函數,可以搜索這些調用來分析是否使用了COM功能,然后找到一些正在被使用對象的標識符繼續分析
【COM服務器惡意代碼】:
這類惡意代碼很容易檢測,有如下幾個函數必須由COM服務器軟件導出:DllCanUnloadNow、DllGetClassObject、DllInstall、DllRegisterServer、DllUnregisterServer
5、內核中的惡意代碼簡介
當反匯編中出現SYSENTER、SYSCALL或者INT 0x2E時,指明一個調用被使用進入到內核
6、原生API
原生API是用來和windows進行交互的底層API,惡意代碼通過調用原生API函數可以繞過普通的windows API
主要用到的是IDA Pro從反匯編代碼分析惡意代碼的行為與調用,從而進一步得出惡意代碼危害行為報告,要求熟練掌握IDA Pro的使用及Windows API的基本知識(掌握其調用所需參數及功能即可,不需深入了解其實現過程)。
還要注意的是,在分析過程中,由於對系統庫函數及API的不熟悉,導致分析進入歧途,如會去分析庫函數的實現過程,從而使分析進入死胡同。