脫殼的藝術php
Mark Vincent Yasonhtml
概述:脫殼是門藝術——脫殼既是一種心理挑戰,同時也是逆向領域最爲激動人心的智力遊戲之一。爲了甄別或解決很是難的反逆向技巧,逆向分析人員有時不得不瞭解操做系統的一些底層知識,聰明和耐心也是成功脫殼的關鍵。這個挑戰既牽涉到殼的建立者,也牽涉到那些決心躲過這些保護的脫殼者。react
本文主要目的是介紹殼經常使用的反逆向技術,同時也探討了能夠用來躲過或禁用這些保護的技術及公開可用的工具。這些信息將使研究人員特別是惡意代碼分析人員在分析加殼的惡意代碼時能識別出這些技術,當這些反逆向技術阻礙其成功分析時能決定下一步的動做。第二個目的,這裏介紹的信息也會被那些計劃在軟件中添加一些保護措施用來減緩逆向分析人員分析其受保護代碼的速度的研究人員用到。固然沒有什麼能使一個熟練的、消息靈通的、堅決的逆向分析人員止步的。算法
關鍵詞:逆向工程、殼、保護、反調試、反逆向數組
1簡介 sass
在逆向工程領域,殼是最有趣的謎題之一。在解謎的過程當中,逆向分析人員會得到許多關於系統底層、逆向技巧等知識。安全
殼(這個術語在本文中既指壓縮殼也包括加密殼)是用來防止程序被分析的。它們被商業軟件合法地用於防止信息披露、篡改及盜版。惋惜惡意軟件也基於一樣的理由在使用殼,只不過動機不良。網絡
因爲大量惡意軟件存在加殼現象,研究人員和惡意代碼分析人員爲了分析代碼,開始學習脫殼的技巧。可是隨着時間的推移,爲防止逆向分析人員分析受保護的程序併成功脫殼,新的反逆向技術也被不斷地添加到殼中。而且戰鬥還在繼續,新的反逆向技術被開發的同時逆向分析人員也在針鋒相對地發掘技巧、研究技術並開發工具來對付它們。數據結構
本文主要關注於介紹殼所使用的反逆向技術,同時也探討了躲過/禁用這些保護措施的工具及技術。可能有些殼經過抓取進程映像(dump)可以輕易被搞定,這時處理反逆向技術彷佛沒有必要,可是有些狀況下加密殼的代碼須要加以跟蹤和分析,例如:多線程
須要躲過部分加密殼代碼以便抓取進程映像、讓輸入表重建工具正確地工做。
深刻分析加密殼代碼以便在一個反病毒產品中整合進脫殼支持。
此外,當反逆向技術被惡意程序直接應用,以防止跟蹤並分析其惡意行爲時,熟悉反逆向技術也是頗有價值的。
本文毫不是一個完整的反逆向技術的清單,由於它只涵蓋了殼中經常使用的、有趣的一些技術。建議讀者參閱最後一節的連接和圖書資料,以瞭解更多其餘逆向及反逆向的技術。
筆者但願您以爲這些材料有用,並能應用其中的技術。脫殼快樂!
2 調試器檢測技術
本節列出了殼用來肯定進程是否被調試或者系統內是否有調試器正在運行的技術。這些調試器檢測技術既有很是簡單(明顯)的檢查,也有涉及到native APIs和內核對象的。
2.1 PEB.BeingDebugged Flag : IsDebuggerPresent()
最基本的調試器檢測技術就是檢測進程環境塊(PEB)1中的BeingDebugged標誌。kernel32!IsDebuggerPresent() API檢查這個標誌以肯定進程是否正在被用戶模式的調試器調試。
下面顯示了IsDebuggerPresent() API的實現代碼。首先訪問線程環境塊(TEB)2獲得PEB的地址,而後檢查PEB偏移0x02位置的BeingDebugged標誌。
mov eax, large fs: 18h
mov eax, [eax+30h]
movzx eax, byte ptr [eax+2]
retn
除了直接調用IsDebuggerPresent(),有些殼會手工檢查PEB中的BeingDebugged標誌以防逆向分析人員在這個API上設置斷點或打補丁。
示例
下面是調用IsDebuggerPresent() API和使用PEB.BeingDebugged標誌肯定調試器是否存在的示例代碼。
;call kernel32!IsDebuggerPresent()
call [IsDebuggerPresent]
test eax,eax
jnz .debugger_found
;check PEB.BeingDebugged directly
Mov eax,dword [fs:0x30] ;EAX = TEB.ProcessEnvironmentBlock
movzx eax,byte [eax+0x02] ;AL = PEB.BeingDebugged
test eax,eax
jnz .debugger_found
因爲這些檢查很明顯,殼通常都會用後面章節將會討論的垃圾代碼或者反—反編譯技術進行混淆。
對策
人工將PEB.BeingDebugged標誌置0可輕易躲過這個檢測。在數據窗口中Ctrl+G(前往表達式)輸入fs:[30],能夠在OllyDbg中查看PEB數據。
另外Ollyscript命令"dbh"能夠補丁這個標誌。
dbh
最後,Olly Advanced3 插件有置BeingDebugged標誌爲0的選項。
2.2 PEB.NtGlobalFlag , Heap.HeapFlags, Heap.ForceFlags
PEB.NtGlobalFlag PEB另外一個成員被稱做NtGlobalFlag(偏移0x68),殼也經過它來檢測程序是否用調試器加載。一般程序沒有被調試時,NtGlobalFlag成員值爲0,若是進程被調試這個成員一般值爲0x70(表明下述標誌被設置):
FLG_HEAP_ENABLE_TAIL_CHECK(0X10)
FLG_HEAP_ENABLE_FREE_CHECK(0X20)
FLG_HEAP_VALIDATE_PARAMETERS(0X40)
這些標誌是在ntdll!LdrpInitializeExecutionOptions()裏設置的。請注意PEB.NtGlobalFlag的默認值能夠經過gflags.exe工具或者在註冊表如下位置建立條目來修改:
HKLM\Software\Microsoft\Windows Nt\CurrentVersion\Image File Execution Options
Heap Flags 因爲NtGlobalFlag標誌的設置,堆也會打開幾個標誌,這個變化能夠在ntdll!RtlCreateHeap()裏觀測到。一般狀況下爲進程建立的第一個堆會將其Flags和ForceFlags4分別設爲0x02(HEAP_GROWABLE)和0 。然而當進程被調試時,這兩個標誌一般被設爲0x50000062(取決於NtGlobalFlag)和0x40000060(等於Flags AND 0x6001007D)。默認狀況下當一個被調試的進程建立堆時下列附加的堆標誌將被設置:
HEAP_TAIL_CHECKING_ENABLED(0X20)
HEAP_FREE_CHECKING_ENABLED(0X40)
示例
下面的示例代碼檢查PEB.NtGlobalFlag是否等於0,爲進程建立的第一個堆是否設置了附加標誌(PEB.ProcessHeap):
;ebx = PEB
Mov ebx,[fs:0x30]
;Check if PEB.NtGlobalFlag != 0
Cmp dword [ebx+0x68],0
jne .debugger_found
;eax = PEB.ProcessHeap
Mov eax,[ebx+0x18]
;Check PEB.ProcessHeap.Flags
Cmp dword [eax+0x0c],2
jne .debugger_found
;Check PEB.ProcessHeap.ForceFlags
Cmp dword [eax+0x10],0
jne .debugger_found
對策
能夠將 PEB.NtGlobalFlag和PEB.HeapProcess標誌補丁爲進程未被調試時的相應值。下面是一個補丁上述標誌的ollyscript示例:
Var peb
var patch_addr
var process_heap
//retrieve PEB via a hardcoded TEB address( first thread: 0x7ffde000)
Mov peb,[7ffde000+30]
//patch PEB.NtGlobalFlag
Lea patch_addr,[peb+68]
mov [patch_addr],0
//patch PEB.ProcessHeap.Flags/ForceFlags
Mov process_heap,[peb+18]
lea patch_addr,[process_heap+0c]
mov [patch_addr],2
lea patch_addr,[process_heap+10]
mov [patch_addr],0
一樣地Olly Advanced插件有設置PEB.NtGlobalFlag和PEB.ProcessHeap的選項。
2.3 DebugPort: CheckRemoteDebuggerPresent()/NtQueryInformationProcess()
Kernel32!CheckRemoteDebuggerPresent()是另外一個能夠用於肯定是否有調試器被附加到進程的API。這個API內部調用了ntdll!NtQueryInformationProcess(),調用時ProcessInformationclass參數爲ProcessDebugPort(7)。而NtQueryInformationProcess()檢索內核結構EPROCESS5的DebugPort成員。非0的DebugPort成員意味着進程正在被用戶模式的調試器調試。若是是這樣的話,ProcessInformation 將被置爲0xFFFFFFFF ,不然ProcessInformation 將被置爲0。
Kernel32!CheckRemoteDebuggerPresent()接受2個參數,第1個參數是進程句柄,第2個參數是一個指向boolean變量的指針,若是進程被調試,該變量將包含TRUE返回值。
BOOL CheckRemoteDebuggerPresent(
HANDLE hProcess,
PBOOL pbDebuggerPresent
)
ntdll!NtQueryInformationProcess()有5個參數。爲了檢測調試器的存在,須要將ProcessInformationclass參數設爲ProcessDebugPort(7):
NTSTATUS NTAPI NtQueryInformationProcess(
HANDLE ProcessHandle,
PROCESSINFOCLASS ProcessInformationClass,
PVOID ProcessInformation,
ULONG ProcessInformationLength,
PULONG ReturnLength
)
示例
下面的例子顯示瞭如何調用CheckRemoteDebuggerPresent()和NtQueryInformationProcess()來檢測當前進程是否被調試:
; using Kernel32!CheckRemoteDebuggerPresent()
lea eax,[.bDebuggerPresent]
push eax ;pbDebuggerPresent
push 0xffffffff ;hProcess
call [CheckRemoteDebuggerPresent]
cmp dword [.bDebuggerPresent],0
jne .debugger_found
; using ntdll!NtQueryInformationProcess(ProcessDebugPort)
lea eax,[.dwReturnLen]
push eax ;ReturnLength
push 4 ;ProcessInformationLength
lea eax,[.dwDebugPort]
push eax ;ProcessInformation
push ProcessDebugPort ;ProcessInformationClass(7)
push 0xffffffff ;ProcessHandle
call [NtQueryInformationProcess]
cmp dword [.dwDebugPort],0
jne .debugger_found
對策
一種方法是在NtQueryInformationProcess()返回的地方設置斷點,當這個斷點被斷下來後,將ProcessInformation 補丁爲0。 下面是自動執行這個方法的ollyscript示例:
var bp_NtQueryInformationProcess
// set a breakpoint handler
eob bp_handler_NtQueryInformationProcess
// set a breakpoint where NtQueryInformationProcess returns
gpa "NtQueryInformationProcess","ntdll.dll"
find $RESULT,#C21400# //retn 14
mov bp_NtQueryInformationProcess,$RESULT
bphws bp_NtQueryInformationProcess,"X"
run
bp_handler_NtQueryInformationProcess:
//ProcessInformationClass == ProcessDebugPort?
cmp [esp+8],7
jne bp_handler_NtQueryInformationProcess_continue
//patch ProcessInformation to 0
mov patch_addr,[esp+c]
mov [patch_addr],0
// clear breakpoint
bphwc bp_NtQueryInformationProcess
bp_handler_NtQueryInformationProcess_continue:
run
Olly Advanced插件有一個patch NtQueryInformationProcess()的選項,這個補丁涉及注入一段代碼來操縱NtQueryInformationProcess()的返回值。
2.4 Debugger Interrupts
在調試器中步過INT3和INT1指令的時候,因爲調試器一般會處理這些調試中斷,因此異常處理例程默認狀況下將不會被調用,Debugger Interrupts就利用了這個事實。這樣殼能夠在異常處理例程中設置標誌,經過INT指令後若是這些標誌沒有被設置則意味着進程正在被調試。另外,kernel32!DebugBreak()內部是調用了INT3來實現的,有些殼也會使用這個API。
示例
這個例子在異常處理例程中設置EAX的值爲0xFFFFFFFF(經過CONTEXT6記錄)以此來判斷異常處理例程是否被調用:
; set exception handler
push .exeception_handler
push dword [fs:0]
mov [fs:0],esp
;reset flag(EAX) invoke int3
xor eax,eax
int3
;restore exception handler
pop dword [fs:0]
add esp,4
; check if the flag had been set
test eax,eax
je .debugger_found
:::
.exeception_handler:
;EAX = ContextRecord
mov eax,[esp+0xc]
;set flag (ContextRecord.EAX)
mov dword [eax+0xb0],0xffffffff
;set ContextRecord.EIP
inc dword [eax+0xb8]
xor eax,eax
retn
對策
因爲調試中斷而致使執行中止時,在OllyDbg中識別出異常處理例程(經過視圖->SEH鏈)並下斷點,而後Shift+F9將調試中斷/異常傳遞給異常處理例程,最終異常處理例程中的斷點會斷下來,這時就能夠跟蹤了。
另外一個方法是容許調試中斷自動地傳遞給異常處理例程。在OllyDbg中能夠經過 選項-> 調試選項 -> 異常 -> 忽略下列異常 選項卡中鉤選"INT3中斷"和"單步中斷"複選框來完成設置。
2.5 Timing Checks
當進程被調試時,調試器事件處理代碼、步過指令等將佔用CPU循環。若是相鄰指令之間所花費的時間若是大大超出常規,就意味着進程極可能是在被調試,而殼正好利用了這一點。
示例
下面是一個簡單的時間檢查的例子。在某一段指令的先後用RDTSC指令(Read Time-Stamp Counter)並計算相應的增量。增量值0x200取決於兩個RDTSC指令之間的代碼執行量。
rdtsc
mov ecx,eax
mov ebx,edx
;...more instructions
nop
push eax
pop eax
nop
;...more instructions
;compute delta between RDTSC instructions
rdtsc
;Check high order bits
cmp edx,ebx
ja .debugger_found
;Check low order bits
sub eax,ecx
cmp eax,0x200
ja .debugger_found
其它的時間檢查手段包括使用kernel32!GetTickCount() API, 或者手工檢查位於0x7FFE0000地址的SharedUserData7數據結構的TickCountLow 及TickCountMultiplier 成員。
使用垃圾代碼或者其它混淆技術進行隱藏之後,這些時間檢查手段尤爲是使用RDTSC將會變得難於識別。
對策
一種方法就是找出時間檢查代碼的確切位置,避免步過這些代碼。逆向分析人員能夠在增量比較代碼以前下斷而後用 運行 代替 步過 直到斷點斷下來。另外也能夠下GetTickCount()斷點以肯定這個API在什麼地方被調用或者用來修改其返回值。
Olly Advanced採用另外一種方法——它安裝了一個內核模式驅動程序作如下工做:
1 設置控制寄存器CR48中的時間戳禁止位(TSD),當這個位被設置後若是RDTSC指令在非Ring0下執行將會觸發一個通用保護異常(GP)。
2 中斷描述表(IDT)被設置以掛鉤GP異常而且RTDSC的執行被過濾。若是是因爲RDTSC指令引起的GP,那麼僅僅將前次調用返回的時間戳加1。
值得注意的是上面討論的驅動可能會致使系統不穩定,應該始終在非生產機器或虛擬機中進行嘗試。
2.6 SeDebugPrivilege
默認狀況下進程是沒有SeDebugPrivilege權限的。然而進程經過OllyDbg和WinDbg之類的調試器載入的時候,SeDebugPrivilege權限被啓用了。這種狀況是因爲調試器自己會調整並啓用SeDebugPrivilege權限,當被調試進程加載時SeDebugPrivilege權限也被繼承了。
一些殼經過打開CSRSS.EXE進程間接地使用SeDebugPrivilege肯定進程是否被調試。若是可以打開CSRSS.EXE意味着進程啓用了SeDebugPrivilege權限,由此能夠推斷進程正在被調試。這個檢查能起做用是由於CSRSS.EXE進程安全描述符只容許SYSTEM訪問,可是一旦進程擁有了SeDebugPrivilege權限,就能夠忽視安全描述符9而訪問其它進程。注意默認狀況下這一權限僅僅授予了Administrators組的成員。
示例
下面是SeDebugPrivilege檢查的例子:
;query for the PID of CSRSS.EXE
call [CsrGetProcessId]
;try to open the CSRSS.EXE process
push eax
push FALSE
push PROCESS_QUERY_INFORMATION
call [OpenProcess]
;if OpenProcess() was successful,
;process is probably being debugged
test eax,eax
jnz .debugger_found
這裏使用了ntdll!CsrGetProcessId() API獲取CSRSS.EXE的PID,可是殼也可能經過手工枚舉進程來獲得CSRSS.EXE的PID。若是OpenProcess()成功則意味着SeDebugPrivilege權限被啓用,這也意味着進程極可能被調試。
對策
一種方法是在ntdll!NtOpenProcess()返回的地方設斷點,一旦斷下來後,若是傳入的是CSRSS.EXE的PID則修改EAX值爲0xC0000022(STATUS_ACCESS_DENIED)。
2.7 Parent Process(檢測父進程)
一般進程的父進程是explorer.exe(雙擊執行的狀況下),父進程不是explorer.exe說明程序是由另外一個不一樣的應用程序打開的,這極可能就是程序被調試了。
下面是實現這種檢查的一種方法:
1 經過TEB(TEB.ClientId)或者使用GetCurrentProcessId()來檢索當前進程的PID
2 用Process32First/Next()獲得全部進程的列表,注意explorer.exe的PID(經過PROCESSENTRY32.szExeFile)和經過PROCESSENTRY32.th32ParentProcessID得到的當前進程的父進程PID
3 若是父進程的PID不是explorer.exe的PID,則目標進程極可能被調試
可是請注意當經過命令行提示符或默認外殼非explorer.exe的狀況下啓動可執行程序時,這個調試器檢查會引發誤報。
對策
Olly Advanced提供的方法是讓Process32Next()老是返回fail,這樣殼的進程枚舉代碼將會失效,因爲進程枚舉失效PID檢查將會被跳過。這些是經過補丁 kernel32!Process32NextW()的入口代碼(將EAX值設爲0而後直接返回)實現的。
77E8D1C2 > 33C0 xor eax, eax
77E8D1C4 C3 retn
77E8D1C5 83EC 0C sub esp, 0C
2.8 DebugObject: NtQueryObject()
除了識別進程是否被調試以外,其餘的調試器檢測技術牽涉到檢查系統當中是否有調試器正在運行。
逆向論壇中討論的一個有趣的方法就是檢查DebugObject10類型內核對象的數量。這種方法之因此有效是由於每當一個應用程序被調試的時候,將會爲調試對話在內核中建立一個DebugObject類型的對象。
DebugObject的數量能夠經過ntdll!NtQueryObject()檢索全部對象類型的信息而得到。NtQueryObject接受5個參數,爲了查詢全部的對象類型,ObjectHandle參數被設爲NULL,ObjectInformationClass參數設爲ObjectAllTypeInformation(3):
NTSTATUS NTAPI NtQueryObject(
HANDLE ObjectHandle,
OBJECT_INFORMATION_CLASS ObjectInformationClass,
PVOID ObjectInformation,
ULONG Length,
PULONG ResultLength
)
這個API返回一個OBJECT_ALL_INFORMATION結構,其中NumberOfObjectsTypes成員爲全部的對象類型在ObjectTypeInformation數組中的計數:
typedef struct _OBJECT_ALL_INFORMATION{
ULONG NumberOfObjectsTypes;
OBJECT_TYPE_INFORMATION ObjectTypeInformation[1];
}
檢測例程將遍歷擁有以下結構的ObjectTypeInformation數組:
typedef struct _OBJECT_TYPE_INFORMATION{
[00] UNICODE_STRING TypeName;
[08] ULONG TotalNumberofHandles;
[0C] ULONG TotalNumberofObjects;
...more fields...
}
TypeName成員與UNICODE字符串"DebugObject"比較,而後檢查TotalNumberofObjects 或 TotalNumberofHandles 是否爲非0值。
對策
與NtQueryInformationProcess()解決方法相似,在NtQueryObject()返回處設斷點,而後補丁 返回的OBJECT_ALL_INFORMATION結構,另外NumberOfObjectsTypes成員能夠置爲0以防止殼遍歷ObjectTypeInformation數組。能夠經過建立一個相似於NtQueryInformationProcess()解決方法的ollyscript腳原本執行這個操做。
相似地,Olly Advanced插件向NtQueryObject() API中注入代碼,若是檢索的是ObjectAllTypeInformation類型則用0清空整個返回的緩衝區。
2.9 Debugger Window
調試器窗口的存在標誌着有調試器正在系統內運行。因爲調試器建立的窗口擁有特定類名(OllyDbg的是OLLYDBG,WinDbg的是WinDbgFrameClass),使用user32!FindWindow()或者user32!FindWindowEx()能很容易地識別這些調試器窗口。
示例
下面的示例代碼使用FindWindow()查找OllyDbg或WinDbg建立的窗口來識別他們是否正在系統中運行。
push NULL
push .szWindowClassOllyDbg
call [FindWindowA]
test eax,eax
jnz .debugger_found
push NULL
push .szWindowClassWinDbg
call [FindWindowA]
test eax,eax
jnz .debugger_found
.szWindowClassOllyDbg db 「OLLYDBG」,0
.szWindowClassWinDbg db 「WinDbgFrameClass」,0
對策
一種方法是在FindWindow()/FindWindowEx()的入口處設斷點,斷下來後,改變lpClassName參數的內容,這樣API將會返回fail,另外一種方法就是直接將返回值設爲NULL。
2.10 Debugger Process
另一種識別系統內是否有調試器正在運行的方法是列出全部的進程,檢查進程名是否與調試器(如 OLLYDBG.EXE,windbg.exe等)的相符。實現很直接,利用Process32First/Next()而後檢查映像名稱是否與調試器相符就好了。
有些殼也會利用kernel32!ReadProcessMemory()讀取進程的內存,而後尋找調試器相關的字符串(如」OLLYDBG」)以防止逆向分析人員修改調試器的可執行文件名。一旦發現調試器的存在,殼要麼顯示一條錯誤信息,要麼默默地退出或者終止調試器進程。
對策
和父進程檢查相似,能夠經過補丁 kernel32!Process32NextW() 使其老是返回fail值來防止殼枚舉進程。
2.11 Device Drivers
檢測內核模式的調試器是否活躍於系統中的典型技術是訪問他們的設備驅動程序。該技術至關簡單,僅涉及調用kernel32!CreateFile()檢測內核模式調試器(如SoftICE)使用的那些衆所周知的設備名稱。
示例
一個簡單的檢查以下:
push NULL
push 0
push OPEN_EXISTING
push NULL
push FILE_SHARE_READ
push GENERIC_READ
push .szDeviceNameNtice
call [CreateFileA]
cmp eax,INVALID_HANDLE_VALUE
jne .debugger_found
.szDeviceNameNtice db "\\.\NTICE",0
某些版本的SoftICE會在設備名稱後附加數字致使這種檢查失敗,逆向論壇中相關的描述是窮舉附加的數字直到發現正確的設備名稱。新版殼也用設備驅動檢測技術檢測諸如Regmon和Filemon之類的系統監視程序的存在。
對策
一種簡單的方法就是在kernel32!CreateFileW()內設置斷點,斷下來後,要麼操縱FileName參數要麼改變其返回值爲INVALID_HANDLE_VALUE(0xFFFFFFFF)。
2.12 OllyDbg:Guard Pages
這個檢查是針對OllyDbg的,由於它和OllyDbg的內存訪問/寫入斷點特性相關。
除了硬件斷點和軟件斷點外,OllyDbg容許設置一個內存訪問/寫入斷點,這種類型的斷點是經過頁面保護11來實現的。簡單地說,頁面保護提供了當應用程序的某塊內存被訪問時得到通知這樣一個途徑。
頁面保護是經過PAGE_GUARD頁面保護修改符來設置的,若是訪問的內存地址是受保護頁面的一部分,將會產生一個STATUS_GUARD_PAGE_VIOLATION(0x80000001)異常。若是進程被OllyDbg調試而且受保護的頁面被訪問,將不會拋出異常,訪問將會被看成內存斷點來處理,而殼正好利用了這一點。
示例
下面的示例代碼中,將會分配一段內存,並將待執行的代碼保存在分配的內存中,而後啓用頁面的PAGE_GUARD屬性。接着初始化標設符EAX爲0,而後經過執行內存中的代碼來引起STATUS_GUARD_PAGE_VIOLATION異常。若是代碼在OllyDbg中被調試,由於異常處理例程不會被調用因此標設符將不會改變。
;set up exception handler
push .exception_handle
push dword [fs:0]
mov [fs:0],esp
;allocate memory
push PAGE_READWRITE
push MEM_COMMIT
push 0x1000
push NULL
call [VirtualAlloc]
test eax,eax
jz .failed
mov [.pAllocatedMem],eax
;store a RETN on the allocated memory
mov byte [eax],0xC3
;then set the PAGE_GUARD attribute of the allocated memory
lea eax,[.dwOldProtect]
push eax
push PAGE_EXECUTE_READ | PAGE_GUARD
push 0x1000
push dword [.pAllocatedMem]
call [VirtualProtect]
;set marker (EAX) as 0
xor eax,eax
;trigger a STATUS_GUARD_PAGE_VIOLATION exception
call [.pAllocatedMem]
;check if marker had not been changed (exception handler not called)
test eax,eax
je .debugger_found
.exception_handler
;EAX = CONTEXT record
mov eax,[esp+0xC]
;set marker (CONTEXT.EAX) to 0xFFFFFFFF
;to signal that the exception handler was called
mov dword [eax+0xb0],0xFFFFFFFF
xor eax,eax
retn
對策
因爲頁面保護引起一個異常,逆向分析人員能夠故意引起一個異常,這樣異常處理例程將會被調用。在示例中,逆向分析人員能夠用INT3指令替換掉RETN指令,一旦INT3指令被執行,Shift+F9強制調試器執行異常處理代碼。這樣當異常處理例程調用後,EAX將被設爲正確的值,而後RETN指令將會被執行。
若是異常處理例程裏檢查異常是否真地是STATUS_GUARD_PAGE_VIOLATION,逆向分析人員能夠在異常處理例程中下斷點而後修改傳入的ExceptionRecord參數,具體來講就是ExceptionCode, 手工將ExceptionCode設爲STATUS_GUARD_PAGE_VIOLATION便可。
3 斷點和補丁檢測技術
本節列舉了殼最經常使用的識別軟件斷點、硬件斷點和補丁的方法。
3.1 Software Breakpoint Detection
軟件斷點是經過修改目標地址代碼爲0xCC(INT3/Breakpoint Interrupt)來設置的斷點。殼經過在受保護的代碼段和(或)API函數中掃描字節0xCC來識別軟件斷點。
示例
檢測可能和下面同樣簡單:
cld
mov edi,Protected_Code_Start
mov ecx,Protected_Code_End - Protected_Code_Start
mov al,0xcc
repne scasb
jz .breakpoint_found
有些殼對比較的字節值做了些運算使得檢測變得不明顯,例如:
if ( byte XOR 0x55 == 0x99 ) then breakpoint found
Where: 0x99 == 0xCC XOR 0x55
對策
若是軟件斷點被發現了逆向分析人員可使用硬件斷點來代替。若是須要在API內部下斷,可是殼又檢測API內部的斷點,逆向分析人員能夠在最終被ANSI版API調用的UNICODE版的API下斷(如:用LoadLibraryExW代替LoadLibraryA),或者用相應的native API來代替。
3.2 Hardware Breakpoint Detection
另外一種斷點稱之爲硬件斷點,硬件斷點是經過設置名爲Dr0到Dr7的調試寄存器12來實現的。Dr0-Dr3包含至多4個斷點的地址,Dr6是個標誌,它指示哪一個斷點被觸發了,Dr7包含了控制4個硬件斷點諸如啓用/禁用或者中斷於讀/寫的標誌。
因爲調試寄存器沒法在Ring3下訪問,硬件斷點的檢測須要執行一小段代碼。殼利用了含有調試寄存器值的CONTEXT結構,CONTEXT結構能夠經過傳遞給異常處理例程的ContextRecord參數來訪問。
示例
這是一段查詢調試寄存器的示例代碼:
; set up exception handler
push .exception_handler
push dword [fs:0]
mov [fs:0],esp
;eax will be 0xFFFFFFFF if hardware breakpoints are identified
xor eax,eax
;throw an exception
mov dword [eax],0
;restore exception handler
pop dword [fs:0]
add esp,4
;test if EAX was updated (breakpoint identified)
test eax,eax
jnz .breakpoint_found
:::
.exception_handler
;EAX = CONTEXT record
mov eax,[esp+0xc]
;check if Debug Registers Context.Dr0-Dr3 is not zero
cmp dword [eax+0x04],0
jne .hardware_bp_found
cmp dword [eax+0x08],0
jne .hardware_bp_found
cmp dword [eax+0x0c],0
jne .hardware_bp_found
cmp dword [eax+0x10],0
jne .hardware_bp_found
jmp .exception_ret
.hardware_bp_found
;set Context.EAX to signal breakpoint found
mov dword [eax+0xb0],0xFFFFFFFF
.exception_ret
;set Context.EIP upon return
add dword [eax+0xb8],6
xor eax,eax
retn
有些殼也利用調試寄存器的值做爲解密密鑰的一部分。這些調試寄存器要麼初始化爲一個特定值要麼爲0。所以,若是這些調試寄存器被修改,解密將會失敗。當解密的代碼是受保護的程序或者脫殼代碼的一部分的時候,將致使無效指令並形成程序一些意想不到的終止。
對策
若是殼沒檢測軟件斷點,逆向分析人員能夠嘗試使用軟件斷點,一樣OllyDbg的內存讀/寫斷點也可使用。當逆向分析人員須要設置API斷點的時候在native或者是UNICODE版的API內部設軟件斷點也是可行的。
3.3 Patching Detection via Code Checksum Calculation
補丁檢測技術能識別殼的代碼是否被修改(代碼被修改則意味着反調試例程已經被禁用了),其次也能識別是否設置了軟件斷點。補丁檢測是經過代碼校驗來實現的,校驗計算包括從簡單到複雜的校驗和/哈希算法。
示例
下面是一個比較簡單的校驗和計算的例子:
mov esi,Protected_Code_Start
mov ecx,Protected_Code_End - Protected_Code_Start
xor eax,eax
.checksum_loop
movzx ebx,byte [esi]
add eax,ebx
rol eax,1
inc esi
loop .checksum_loop
cmp eax,dword [.dwCorrectChecksum]
jne .patch_found
對策
若是代碼校驗例程識別出了軟件斷點,能夠用硬件斷點來代替。若是校驗例程識別出了代碼補丁,逆向分析人員能夠經過在補丁地址設置內存訪問斷點來定位校驗例程所在,一旦發現了校驗例程,能夠修改校驗和爲預期的值或者在比較失敗後修改適當的標誌。
4反分析技術
反分析技術的目標是減緩逆向分析人員對受保護代碼和(或)加殼後的程序分析和理解的速度。咱們將討論諸如加密/壓縮、垃圾代碼、代碼變形、反-反編譯等技術,這些技術的目的是爲了混淆代碼、考驗耐心、浪費逆向分析人員的時間,解決這些問題須要逆向分析人員擁有耐心、聰慧等品質。
4.1 Encryption and Compression
加密和壓縮是最基本的反分析形式。它們初步設防,防止逆向分析人員直接在反編譯器內加載受保護的程序而後沒有任何困難地開始分析。
加密 殼一般都既加密自己代碼也加密受保護的程序。不一樣的殼所採用的加密算法大不相同,有很是簡單的XOR循環,也有執行數次運算的很是複雜的循環。對於某些多態變形殼,爲了防止查殼工具正確地識別殼,每次加殼所採用的加密算法都不一樣,解密代碼也經過變形顯得很不同。
解密例程做爲一個取數、計算、存諸操做的循環很容易辨認。下面是一個對加密過的DWORD值執行數次XOR操做的簡單的解密例程。
0040A07C LODS DWORD PTR DS:[ESI]
0040A07D XOR EAX,EBX
0040A07F SUB EAX,12338CC3
0040A084 ROL EAX,10
0040A087 XOR EAX,799F82D0
0040A08C STOS DWORD PTR ES:[EDI]
0040A08D INC EBX
0040A08E LOOPD SHORT 0040A07C ;decryption loop
這裏是另外一個多態變形殼的解密例程:
00476056 MOV BH,BYTE PTR DS:[EAX]
00476058 INC ESI
00476059 ADD BH,0BD
0047605C XOR BH,CL
0047605E INC ESI
0047605F DEC EDX
00476060 MOV BYTE PTR DS:[EAX],BH
00476062 CLC
00476063 SHL EDI,CL
:::More garbage code
00476079 INC EDX
0047607A DEC EDX
0047607B DEC EAX
0047607C JMP SHORT 0047607E
0047607E DEC ECX
0047607F JNZ 00476056 ;decryption loop
下面是由同一個多態殼生成的另外一段解密例程:
0040C045 MOV CH,BYTE PTR DS:[EDI]
0040C047 ADD EDX,EBX
0040C049 XOR CH,AL
0040C04B XOR CH,0D9
0040C04E CLC
0040C04F MOV BYTE PTR DS:[EDI],CH
0040C051 XCHG AH,AH
0040C053 BTR EDX,EDX
0040C056 MOVSX EBX,CL
::: More garbage code
0040C067 SAR EDX,CL
0040C06C NOP
0040C06D DEC EDI
0040C06E DEC EAX
0040C06F JMP SHORT 0040C071
0040C071 JNZ 0040C045 ;decryption loop
上面兩個示例中高亮的行是主要的解密指令,其他的指令都是用來迷惑逆向分析人員的垃圾代碼。注意寄存器是如何交換的,還有兩個示例之間解密方法是如何改變的。
Compression 壓縮的主要目的是爲了縮小可執行文件代碼和數據的大小,可是因爲原始的包含可讀字符串的可執行文件變成了壓縮數據,所以也有那麼一些混淆的做用。看看幾款殼所使用的壓縮引擎:UPX使用NRV(Not Really Vanished)和LZMA(Lempel-Ziv-Markov chain-Algorithm),FSG使用aPLib,Upack使用LZMA,yoda加密殼使用LZO。這其中有些壓縮引擎能夠自由地使用於非商業應用,可是商業應用須要許可/註冊。
對策
解密和解壓縮循環很容易就能被躲過,逆向分析人員只須要知道解密和解壓縮循環什麼時候結束,而後在循環結束後面的指令上下斷點。記住,有些殼會在解密循環中檢測斷點。
4.2 Garbage Code and Code Permutation
Garbage Code 在脫殼的例程中插入垃圾代碼是另外一種有效地迷惑逆向分析人員的方法。它的目的是在加密例程或者諸如調試器檢測這樣的反逆向例程中掩蓋真正目的的代碼。經過將本文描述過的調試器/斷點/補丁檢測技術隱藏在一大堆無關的、不起做用的、混亂的指令中,垃圾代碼能夠增長這些檢測的效果。此外,有效的垃圾代碼是那些看似合法/有用的代碼。
示例
下面是一段在相關的指令中插入了垃圾代碼的解密例程:
0044A21A JMP SHORT sample.0044A21F
0044A21C XOR DWORD PTR SS:[EBP],6E4858D
0044A223 INT 23
0044A225 MOV ESI,DWORD PTR SS:[ESP]
0044A228 MOV EBX,2C322FF0
0044A22D LEA EAX,DWORD PTR SS:[EBP+6EE5B321]
0044A233 LEA ECX DWORD PTR DS:[ESI+543D583E]
0044A239 ADD EBP,742C0F15
0044A23F ADD DWORD PTR DS:[ESI],3CB3AA25
0044A245 XOR EDI,7DAC77E3
0044A24B CMP EAX,ECX
0044A24D MOV EAX,5ACAC514
0044A252 JMP SHORT sample.0044A257
0044A254 XOR DWORD PTR SS:[EBP],AAE47425
0044A25B PUSH ES
0044A25C ADD EBP,5BAC5C22
0044A262 ADC ECX,3D71198C
0044A268 SUB ESI,-4
0044A26B ADC ECX,3795A210
0044A271 DEC EDI
0044A272 MOV EAX,2F57113F
0044A277 PUSH ECX
0044A278 POP ECX
0044A279 LEA EAX,DWORD PTR SS:[EBP+3402713D]
0044A27F EDC EDI
0044A280 XOR DWORD PTR DS:[ESI],33B568E3
0044A286 LEA EBX,DWORD PTR DS:[EDI+57DEFEE2]
0044A28C DEC EDI
0044A28D SUB EBX,7ECDAE21
0044A293 MOV EDI,185C5C6C
0044A298 MOV EAX,4713E635
0044A29D MOV EAX,4
0044A2A2 ADD ESI,EAX
0044A2A4 MOV ECX,1010272F
0044A2A9 MOV ECX,7A49B614
0044A2AE CMP EAX,ECX
0044A2B0 NOT DWORD PTR DS:[ESI]
示例中相關的解密指令是:
0044A225 MOV ESI,DWORD PTR SS:[ESP]
0044A23F ADD DWORD PTR DS:[ESI],3CB3AA25
0044A268 SUB ESI,-4
0044A280 XOR DWORD PTR DS:[ESI],33B568E3
0044A29D MOV EAX,4
0044A2A2 ADD ESI,EAX
0044A2B0 NOT DWORD PTR DS:[ESI]
Code Permutation 代碼變形是更高級殼使用的另外一種技術。經過代碼變形,簡單的指令變成了複雜的指令序列。這要求殼理解原有的指令並能生成新的執行相同操做的指令序列。
一個簡單的指令置換示例:
mov eax,ebx
test eax,eax
轉換成下列等價的指令:
push ebx
pop eax
or eax,eax
結合垃圾代碼使用,代碼變形是一種有效地減緩逆向分析人員理解受保護代碼速度的技術。
示例
爲了說明,下面是一個經過代碼變形並在置換後的代碼間插入了垃圾代碼的調試器檢測例程:
004018A8 MOV ECX,A104B412
004018AD PUSH 004018C1
004018B2 RETN
004018B3 SHR EDX,5
004018B6 ADD ESI,EDX
004018B8 JMP SHORT 004018BA
004018BA XOR EDX,EDX
004018BC MOV EAX,DWORD PTR DS:[ESI]
004018BE STC
004018BF JB SHORT 004018DE
004018C1 SUB ECX,EBX
004018C3 MOV EDX,9A01AB1F
004018C8 MOV ESI,DWORD PTR FS:[ECX]
004018CB LEA ECX DWORD PTR DS:[EDX+FFFF7FF7]
004018D1 MOV EDX,600
004018D6 TEST ECX,2B73
004018DC JMP SHORT 004018B3
004018DE MOV ESI,EAX
004018E0 MOV EAX,A35ABDE4
004018E5 MOV ECX,FAD1203A
004018EA MOV EBX,51AD5EF2
004018EF DIV EBX
004018F1 ADD BX,44A5
004018F6 ADD ESI,EAX
004018F8 MOVZX EDI,BYTE PTR DS:[ESI]
004018FB OR EDI,EDI
004018FD JNZ SHORT 00401906
其實這是一個很簡單的調試器檢測例程:
00401081 MOV EAX,DWORD PTR FS:[18]
00401087 MOV EAX,DWORD PTR DS:[EAX+30]
0040108A MOVZX EAX,BYTE PTR DS:[EAX+2]
0040108E TEST EAX,EAX
00401090 JNZ SHORT 00401099
對策
垃圾代碼和代碼變形是一種用來考驗耐心和浪費逆向分析人員的時間的方式。所以,重要的是知道這些混淆技術背後隱藏的指令是否值得去理解(是否是僅僅執行解密、殼的初始化等動做)。
避免跟蹤進入這些難懂的指令的方法之一是在殼最經常使用的API下斷點(如:VirtualAlloc,VitualProtect,LoadLibrary,GetProcAddress等)並把這些API看成跟蹤的標誌。若是在這些跟蹤標誌之間出了錯,這時候就對這一段代碼進行詳細的跟蹤。另外,設置內存訪問/寫入斷點也讓逆向分析人員能有針對性地分析那些修改/訪問受保護進程最有趣的部分的代碼,而不是跟蹤大量的代碼最終卻(極可能)發現是一個肯定的例程。
最後,在VMWare中運行OllyDbg並不時地保存調試會話快照,這樣一來逆向分析人員就能夠回到某一個特定的跟蹤狀態。若是出了錯,能夠返回到某一特定的跟蹤狀態繼續跟蹤分析。
4.3 Anti-Disassembly
用來困惑逆向分析人員的另外一種方法就是混亂反編譯輸出。反-反編譯是使經過靜態分析理解二進制代碼的過程大大複雜化的有效方式。若是結合垃圾代碼和代碼變形一塊兒使用將會更具效果。
反-反編譯技術的一個具體的例子是插入一個垃圾字節而後增長一個條件分支使執行跳轉到垃圾字節(譯者注:即咱們常說的花指令)。可是這個分支的條件永遠爲FALSE。這樣垃圾代碼將永遠不會被執行,可是反編譯引擎會開始反編譯垃圾字節的地址,最終致使不正確的反編譯輸出。
示例
這是一個加了一些反-反編譯代碼的簡單PEB.BeingDebugged標誌檢查例子。高亮的行是主要指令,其他的是反-反編譯代碼。它用到了垃圾字節0xff並增長了用來迷惑反編譯引擎的跳到垃圾字節的假的條件跳轉。
;Anti-disassembly sequence #1
push .jmp_real_01
stc
jnc .jmp_fake_01
retn
.jmp_fake_01:
db 0xff
.jmp_real_01:
;--------------------------------
mov eax,dword [fs:0x18]
;Anti-disassembly sequence #2
push .jmp_real_02
clc
jc .jmp_fake_02
retn
.jmp_fake_02:
db 0xff
.jmp_real_02:
;--------------------------------
mov eax,dword [eax+0x30]
movzx eax,byte [eax+0x02]
test eax,eax
jnz .debugger_found
下面是WinDbg中的反彙編輸出:
0040194A 6854194000 PUSH 0X401954
0040194F F9 STC
00401950 7301 JNB image00400000+0x1953(00401953)
00401952 C3 RET
00401953 FF64A118 JMP DWORD PTR [ECX+0X18]
00401957 0000 ADD [EAX],AL
00401959 006864 ADD [EAX+0X64],CH
0040195C 194000 SBB [EAX],EAX
0040195F F8 CLC
00401960 7201 JB image00400000+0x1963 (00401963)
00401962 C3 RET
00401963 FF8B40300FB6 DEC DWORD PTR [EBX+0XB60F3040]
00401969 40 INC EAX
0040196A 0285C0750731 ADD AL,[EBP+0X310775C0]
OllyDbg中的反彙編輸出:
0040194A 6854194000 PUSH 00401954
0040194F F9 STC
00401950 7301 JNB SHORT 00401953
00401952 C3 RETN
00401953 FF64A118 JMP DWORD PTR DS:[ECX+18]
00401957 0000 ADD BYTE PTR DS:[EAX],AL
00401959 006864 ADD BYTE PTR DS:[EAX+0X64],CH
0040195C 194000 SBB DWORD PTR DS:[EAX],EAX
0040195F F8 CLC
00401960 7201 JB SHORT 00401963
00401962 C3 RETN
00401963 FF8B40300FB6 DEC DWORD PTR DS:[EBX+B60F3040]
00401969 40 INC EAX
0040196A 0285C0750731 ADD AL,BYTE PTR SS:[EBP+310775C0]
最後IDAPro中的反彙編輸出:
0040194A push (offset loc_401953+1)
0040194F stc
00401950 jnb short loc_401953
00401952 retn
00401953 ;------------------------------------------------------------------
00401953
00401953 loc-401953: ;CODE XREF: sub_401946+A
00401953 ;DATA XREF: sub_401946+4
00401953 jmp dword ptr [ecx+18h]
00401953 sub_401946 endp
00401953
00401953 ;------------------------------------------------------------------
00401957 db 0
00401958 db 0
00401959 db 0
0040195A db 68h; h
0040195B dd offset unk_401964
0040195F db 0F8h;
00401960 db 72h; r
00401961 db 1
00401962 db 0C3h;+
00401963 db 0FFh
00401964 unk_401964 db 8Bh; i ;DATA XREF: text:0040195B
00401965 db 40h; @
00401966 db 30h; 0
00401967 db 0Fh
00401968 db 0B6h;|
00401969 db 40h; @
0040196A db 2
0040196B db 85h;
0040196C db 0C0h;+
0040196D db 75h; u
注意全部這三個反編譯引擎/調試器是如何落入反-反編譯陷阱的,分析這樣的反彙編代碼對於逆向分析人員來講是很不容易的。還有其它的幾種干擾反編譯引擎的手段,這只是一個例子。另外這些反-反編譯代碼能夠編碼成一個宏,這樣彙編源碼就清晰多了。
建議讀者參考Eldad Eliam13的一本精彩的逆向書籍,裏面包含了反-反編譯的詳細信息和其它一些逆向話題。
5 調試器攻擊技術
本節羅列了殼用來主動攻擊調試器的技術,若是進程正在被調試那麼執行會忽然中止、斷點將被禁用。和前面描述的技術相似,結合反-反編譯技術隱藏起來使用效果會更佳。
5.1 Misdirection and Stopping Execution via Exceptions
線性地跟蹤可以讓逆向分析人員容易理解並掌握代碼的真正目的。所以殼使用一些技術使得跟蹤代碼再也不是線性的且更加費時。
一個廣泛使用的技巧是在脫殼的過程當中拋出一些異常,經過拋出一些可捕獲的異常,逆向分析人員必需熟悉異常發生的時候EIP指向何處,當異常處理例程執行完以後EIP又指向何處。
另外異常是殼用來反覆中止脫殼代碼執行的手段之一,由於當進程被調試時拋出異常,調試器會暫停脫殼代碼的執行。
殼一般使用結構化異常處理(SEH)14做爲異常處理的機制,然而新殼也開始使用向量化異常15。
示例
下面示例代碼拋出溢出異常(經過INTO)產生錯誤,經過數輪循環後由ROL指令來修改溢出標誌。可是因爲溢出異常是一個陷阱異常,EIP將指向JMP指令。若是逆向分析人員使用OllyDbg而且沒有將異常傳遞給進程(經過Shift+F7/F8/F9)而是繼續步進,進程將會進入一個死循環。
;set up exception handler
push .exception_handler
push dword [fs:0]
mov [fs:0],esp
;throw an exception
mov ecx,1
.loop:
rol ecx,1
into
jmp .loop
;restore exception handler
pop dword [fs:0]
add esp,4
:::
.exception_handler
;EAX = CONTEXT record
mov eax,[esp+0xc]
;set Context.EIP upon return
add dword [eax+0xb8],2
xor eax,eax
retn
殼一般會拋出違規訪問(0xC0000005)、斷點(0x80000003)和單步(0x80000004)異常。
對策
當殼使用可捕獲的異常僅僅是爲了執行不一樣的代碼時,能夠經過選項-> 調試選項 -> 異常選項卡配置OllyDbg使得異常處理例程自動被調用。下面是異常處理配置對話框的屏幕截圖。逆向分析人員也能夠添加那些不能經過複選框選擇的自定義的異常。
當殼在異常處理例程內部執行重要操做時,逆向分析人員能夠在異常處理例程中下斷,其地址能夠在OllyDbg中經過視圖->SEH鏈看到。而後Shift+F7/F8/F9將控制移交給異常處理例程。
5.2 Blocking Input
爲了防止逆向分析人員控制調試器,當脫殼主例程運行的時候,殼能夠經過調用user32!BlockInput() API 來阻斷鍵盤和鼠標的輸入。經過垃圾代碼和反-反編譯技術進行隱藏使用這種方法,若是逆向分析人員沒有識別出來的話是頗有效的。一旦生效系統看上去沒有反應,只剩下逆向分析人員在那裏莫名其妙。
典型的場景多是逆向分析人員在GetProcAddress()內下斷,而後運行脫殼代碼直到被斷下。可是跳過一段垃圾代碼以後殼調用BlockInput()。當GetProcAddress()斷點斷下來後,逆向分析人員會忽然困惑地發現沒法控制調試器了,不知究竟發生了什麼。
示例
BlockInput()須要一個boolean型的參數fBlockIt。若是這個參數是true,鍵盤和鼠標事件被阻斷;若是是false,鍵盤和鼠標事件被解除阻斷:
; Block input
push TRUE
call [BlockInput]
;...Unpacking code...
;Unblock input
push FALSE
call [BlockInput]
對策
幸虧最簡單的方法就是補丁 BlockInput()使它直接返回。這是補丁user32!BlockInput()入口的ollyscript腳本:
gpa "BlockInput","user32.dll"
mov [$RESULT],#C20400# //retn 4
Olly Advanced插件一樣有補BlockInput()的選項。另外,能夠同時按CTRL+ALT+DELETE鍵手工解除阻斷。
5.3 ThreadHideFromDebugger
這項技術用到了經常被用來設置線程優先級的API ntdll!NtSetInformationThread(),不過這個API也可以用來防止調試事件被髮往調試器。
NtSetInformationThread()的參數列表以下。要實現這一功能,ThreadHideFromDebugger(0x11)被看成ThreadInformationClass參數傳遞,ThreadHandle一般設爲當前線程的句柄(0xFFFFFFFE):
NTSTATUS NTAPI NtSetInformationThread(
HANDLE ThreadHandle,
THREAD_INFORMATION_CLASS ThreadInformaitonClass,
PVOID ThreadInformation,
ULONG ThreadInformationLength
);
ThreadHideFromDebugger內部設置內核結構ETHREAD16的HideThreadFromDebugger成員。一旦這個成員設置之後,主要用來向調試器發送事件的內核函數_DbgkpSendApiMessage()將再也不被調用。
示例
調用NtSetInformationThread()的一個典型示例:
push 0 ;InformationLength
push NULL ;ThreadInformation
push ThreadHideFromDebugger ;0x11
push 0xfffffffe ;GetCurrentThread()
call [NtSetInformationThread]
對策
能夠在ntdll!NtSetInformationThread()裏下斷,斷下來後,逆向分析人員能夠操縱EIP防止API調用到達內核,這些均可以經過ollyscript來自動完成。另外,Olly Advanced插件也有補這個API的選項。補過以後一旦ThreadInformaitonClass參數爲HideThreadFromDebugger,API將再也不深刻內核僅僅執行一個簡單的返回。
5.4 Disabling Breakpoints
另一種攻擊調試器的方法就是禁用斷點。殼經過CONTEXT結構修改調試寄存器來禁用硬件斷點。
示例
在這個示例中,經過傳入異常處理例程的CONTEXT記錄,調試寄存器被清空了。
;set up exception handler
push .exception_handler
push dword [fs:0]
mov [fs:0],esp
;throw an exception
xor eax,eax
mov dword [eax],0
;restore exception handler
pop dword [fs:0]
add esp,4
:::
.exception_handler
;EAX = CONTEXT record
mov eax,[esp+0xc]
;Clear Debug Registers: Context.Dr0-Dr3,Dr6,Dr7
mov dword [eax+0x04],0
mov dword [eax+0x08],0
mov dword [eax+0x0C],0
mov dword [eax+0x10],0
mov dword [eax+0x14],0
mov dword [eax+0x18],0
;set Context.EIP upon return
add dword [eax+0xb8],6
xor eax,eax
retn
對於軟件斷點,殼能夠直接搜索INT3(0xCC)並用任意/隨機的操做碼加以替換。這樣作之後,軟件斷點失效而且原始的指令將會被破壞。
對策
顯然當硬件斷點被檢測之後能夠用軟件斷點來代替,反之亦然。若是二者都被檢測,能夠試試OllyDbg的內存訪問/寫入斷點功能。
5.5 Unhandled Exception Filter
MSDN文檔聲明當一個異常到達Unhandled Exception Filter(kernel32!UnhandledExceptionFilter)而且程序沒有被調試時,Unhandled Exception Filter將會調用在kernel32!SetUnhandledExceptionFilter()API做爲參數指定的高層exception Filter。殼利用了這一點,經過設置exception Filter而後拋出異常,若是程序被調試那麼這個異常將會被調試器接收,不然,控制被移交到exception Filter運行得以繼續。
示例
下面的示例中經過SetUnhandledExceptionFilter()設置了一個高層的exception Filter,而後拋出一個違規訪問異常。若是進程被調試,調試器將收到兩次異常通知,不然exception Filter將修改CONTEXT.EIP並繼續執行。
;set the exception filter
push .exception_filter
call [SetUnhandledExceptionFilter]
mov [.original_filter],eax
;throw an exception
xor eax,eax
mov dword [eax],0
;restore exception filter
push dword [.original_filter]
call [SetUnhandledExceptionFilter]
:::
.exception_filter:
;EAX = ExceptionInfo.ContextRecord
mov eax,[esp+4]
mov eax,[eax+4]
;set return EIP upon return
add dword [eax+0xb8],6
;return EXCEPTION_CONTINUE_EXECUTION
mov eax,0xffffffff
retn
有些殼並不調用SetUnhandledExceptionFilter()而是直接經過kernel32!_BasepCurrentTopLevelFilter手工設置exception Filter,以防逆向分析人員在那個API上下斷。
對策
有意思的是kernel32!UnhandledExceptionFilter()內部實現代碼是使用ntdll!NtQueryInformationProcess(ProcessDebugPort)來肯定進程是否被調試,從而決定是否調用已註冊的exception Filter。所以,處理方法和DebugPort調試器檢測技術相同。
5.6 OllyDbg:OutputDebugString() Format String Bug
這個調試器攻擊手段只對OllyDbg有效。已知OllyDbg面對能致使崩潰或執行任意代碼的格式化字符串漏洞是脆弱的,這個漏洞是因爲向kernel32!OutputDebugString()傳遞了不當的字符串參數引發的。這個漏洞在當前OllyDbg(1.10)依然存在而且仍然沒有打補丁。
示例
下面這個簡單的示例將致使OllyDbg拋出違規訪問異常或不可預期的終止。
push .szFormatString
call [OutputDebugStringA]
:::
.szFormatString db "%s%s",0
對策
能夠經過補丁 kernel32!OutputDebugStringA()入口使之直接返回來加以解決。
6. 高級及其它技術
本節羅列了不屬於前面任一分類的一些高級和其它的反逆向技術。
6.1 Process Injection
進程注入已經成爲某些殼的一個特色。脫殼代碼打開一個選定的宿主進程(自身、explorer.exe、iexplorer.exe等)而後將脫殼後的程序注入到這個宿主進程。
下面是一個支持進程注入的殼的屏幕截圖。
惡意代碼利用殼的這個特色使它們能躲過一些防火牆,這些防火牆經過檢查進程是否在獲准進行外部網絡鏈接的應用程序列表中而決定是否放行。
殼所採用的執行進程注入的一種方法以下:
1. 向kernel32!CreateProcess()傳遞CREATE_SUSPENDED進程建立標誌,將宿主進程做爲一個掛起的子進程打開。這時一個初始化了的線程被建立並掛起,因爲loader例程(ntdll!LrdInitializeThunk)尚未被調用,DLL尚未被載入。這個線程的上下文中包含PEB地址、宿主進程入口點信息的寄存器值被設置。
2. 使用kernel32!GetThreadContext()獲取子進程初始化線程的上下文。
3. 經過CONTEXT.EBX獲取子進程的PEB地址。
4. 讀PEB.ImageBase(PEB+0x8)獲取子進程的映像基址。
5. 將BaseAddress參數指向檢索到的映像基址,調用ntdll!NtUnmapViewOfSection()來unmap子進程中的原始宿主映像。
6. 脫殼代碼使用kernel32!VirtualAllocEx()在子進程中分配一段內存,dwSize參數等於脫殼後程序的映像大小。
7. 使用kernel32!WriteProcessMemory()將脫殼後的程序的PE頭和每一個節寫入子進程。
8. 將子進程的PEB.ImageBase更新以匹配脫殼後的程序映像基址。
9. 經過kernel32!SetThreadContext()更新子進程初始化線程的上下文,將其中的CONTEXT.EAX設置爲脫殼後程序的入口點。
10. 經過kernel32!ResumeThread()恢復子進程的執行。
爲了從入口點開始調試打開的子進程,逆向分析人員能夠在WriteProcessMemory()中設置斷點,當包含入口點的節被寫入子進程的時候,將入口點代碼補丁爲」跳往自身」指令(0xEB0xFE)。當子進程的主線程被恢復,子進程將在入口點進入一個死循環。這時逆向分析人員就能夠附加一個調試器到子進程,恢復被修改的指令,繼續正常的調試。
6.2 Debugger Blocker
Armadillo殼引入了稱之爲Debugger Blocker的功能,它能夠阻止逆向分析人員將調試器附加到一個受保護的進程。這個保護是經過調用Windows提供的調試函數來實現的。
具體來講就是脫殼代碼扮演一個調試器的角色(父進程),經過它打開、調試/控制包含脫殼後程序的子進程。
因爲受保護的進程已經被調試,經過kernel32!DebugActiveProcess()來附加調試器將會失敗,緣由是相應的native API ntdll!NtDebugActiveProcess()將返回STATUS_PORT_ALREADY_SET。 NtDebugActiveProcess()的失敗的根本緣由在於內核結構EPROCESS的DebugPort成員已經被設置過了。
爲了附加調試器到受保護的進程,好幾個逆向論壇發佈的解決方法是在父進程的上下文裏調用dernel32!DebugActiveProcessStop()。能夠經過附加調試器到父進程,在kernel32!WaitForDebugEvent()內部下斷,斷下來後,注入一段調用DebugActiveProcessStop(childProcessID)的代碼並執行,一旦調用成功,這時就能夠附加調試器到受保護的進程了。
6.3 TLS Callbacks
另外一個被殼使用的技術就是在實際的入口點代碼執行以前執行代碼,這是經過使用Thread Local Storage (TLS)回調函數來實現的。殼經過這些回調函數執行調試器檢測及解密例程,這樣逆向分析人員將沒法跟蹤這些例程。
TLS回調可使用諸如pedump之類的PE文件分析工具來識別。若是可執行文件中存在TLS條目,數據條目將會顯示出來。
Data directory
EXPORT rva:00000000 size:00000000
IMPORT rva:00061000 size:000000E0
:::
TLS rva:000610E0 size:00000018
:::
IAT rva:00000000 size:00000000
DELAY_IMPORT rva:00000000 size:00000000
COM_DESCRPTR rva:00000000 size:00000000
unused rva:00000000 size:00000000
接着顯示TLS條目的實際內容。AddressOfCallBacks成員指向一個以null結尾的回調函數數組。
TLS directory:
StartAddressOfRawData: 00000000
EndAddressOfRawData: 00000000
AddressOfIndex: 004610F8
AddressOfCallBacks: 004610FC
SizeOfZeroFill: 00000000
Characteristics: 00000000
在這個例子中,RVA 0x4610fc指向回調函數指針(0x490f43和0x44654e):
默認狀況下OllyDbg載入這個例子將會暫停在入口點。因爲TLS回調函數是在實際的入口點執行以前被調用的,OllyDbg應該配置一下使其在TLS回調被調用以前中斷在實際的loader。
能夠經過選擇選項->調試選項->事件->第一次中斷於->系統斷點來設置中斷於ntdll.dll內的實際loader代碼。
這樣設置之後,OllyDbg將會中斷在位於執行TLS回調的ntdll!LdrpRunInitializeRoutines()以前的ntdll!_LdrpInitializeProcess(),這時就能夠在回調例程中下斷並跟蹤了。
關於PE文件格式的更多信息及包括pedump的二進制/源碼能夠在以下的連接得到:
An In-Depth Look into the Win32 Portable Executable File Format by Matt Pietrek
http://msdn.microsoft.com/msdnmag/issues/02/02/PE/default.aspx
An In-Depth Look into the Win32 Portable Executable File Format,Part 2 by Matt Pietrek
http://msdn.microsoft.com/msdnmag/issues/02/03/PE2/
最新版本的微軟PE文件格式能夠經過以下連接得到:
Microsoft Portable Executable and Common Object File Format Specification
http://www.microsoft.com/whdc/system/platform/firmware/PECOFF.mspx
6.4 Stolen Bytes
代碼抽取基本上就是殼移走受保護程序的一部分(一般是入口點的少許指令),這部分指令被複制並在分配的內存中執行。這在某種程度上保護了程序,由於若是從內存中dump受保護進程,被抽取的指令將不會被恢復。
這是一個可執行文件的原始入口點代碼:
004011CB MOV EAX,DWORD PTR FS:[0]
004011D1 PUSH EBP
004011D2 MOV EBP,ESP
004011D4 PUSH -1
004011D6 PUSH 0047401C
004011DB PUSH 0040109A
004011E0 PUSH EAX
004011E1 MOV DWORD PTR FS:[0],ESP
004011E8 SUB ESP,10
004011EB PUSH EBX
004011EC PUSH ESI
004011ED PUSH EDI
下面是被Enigma加密殼偷取了前兩個指令的同一段代碼:
004011CB POP EBX
004011CC CMP EBX,EBX
004011CE DEC ESP
004011CF POP ES
004011D0 JECXZ SHORT 00401169
004011D2 MOV EBP,ESP
004011D4 PUSH -1
004011D6 PUSH 0047401C
004011DB PUSH 0040109A
004011E0 PUSH EAX
004011E1 MOV DWORD PTR FS:[0],ESP
004011E8 SUB ESP,10
004011EB PUSH EBX
004011EC PUSH ESI
004011ED PUSH EDI
這是被ASProtect殼偷取了數條指令的相同例子。它增長了一條jump指令,指向內存中一段執行被偷代碼的過程,被偷的指令和垃圾代碼攙雜在一塊兒,想要恢復被偷的代碼困難重重。
004011CB JMP 00B70361
004011D0 JNO SHORT 00401198
004011D3 INC EBX
004011D4 ADC AL,0B3
004011D6 JL SHORT 00401196
004011D8 INT1
004011D9 LAHF
004011DA PUSHFD
004011DB MOV EBX,1D0F0294
004011E0 PUSH ES
004011E1 MOV EBX,A732F973
004011E6 ADC BYTE PTR DS:[EDX-E],CH
004011E9 MOV ECX,EBP
004011EB DAS
004011EC DAA
004011ED AND DWORD PTR DS:[EBX+58BA76D7],ECX
6.5 API Redirection
API重定向是用來防止逆向分析人員輕易重建受保護程序輸入表的一種方法。原始的輸入表被銷燬,對API的調用被重定向到位於內存中的例程,而後由這些例程負責調用實際的API。
在這個例子中代碼調用了kernel32!CopyFileA() API:
00404F05 LEA EDI,DWORD PTR SS:[EBP-20C]
00404FOB PUSH EDI
00404FOC PUSH DWORD PTR SS:[EBP-210]
00404F12 CALL <JMP.&KERNEL32.CopyFileA>
被調用的代碼是一個JMP指令,跳轉到輸入表中的函數地址。
004056B8 JMP DWORD PTR DS:[<&KERNEL32.CopyFileA>]
然而當ASProtect殼重定向KERNEL32!CopyFileA() API時,這段代碼被修改成一個call指令,調用殼本身分配的內存中的過程。
004056B8 CALL 00D90000
下圖說明了被偷的指令是如何被安置的。前7條KERNEL32!CopyFileA()代碼中的指令被複制過來,另外0x7C83005E Call指令指向的代碼也被複制過來。經過一個RETN指令,將控制移交回kernel32.dll領空KERNEL32!CopyFileA()中間的0x7C830063地址處:
有些殼則更進一步將整個DLL映像載入到一段分配的內存中,而後重定向API調用到這些DLL映像的拷貝。 這個技術使得在實際的API中下斷點變難了。
6.6 Multi-Threaded Packers
對於多線程殼,另外一個線程經常用於執行一些諸如解密受保護程序這樣必需的操做。多線程殼複雜度增長了,因爲跟蹤代碼變得複雜,理解代碼的難度也大大增長了。
PECrypt是一款多線程殼殼,它用第2個線程來解密數據,而後這些數據被主線程使用,這些線程之間經過事件對象進行同步。
PECrypt殼操做並同步線程:
6.7 Virtual Machines
使用虛擬機的概念很簡單:逆向分析人員最終會想出如何躲過/解決反調試和反逆向技術,當受保護的程序最終須要在內存中解密並執行時,面對靜態分析就顯得脆弱不堪了。
隨着虛擬機的出現,受保護部分的代碼被轉換成了p-code,p-code在執行時能夠轉換成機器碼。原始的機器指令被替換,理解代碼所做所爲的複雜度成指數上升。
下面是這個概念的簡單圖示:
像Oreans technologies的CodeVirtualizer和StraForce這些最新的殼都應用了虛擬機的概念來保護程序。
對付虛擬機須要分析p-code是若是組織並被虛擬機轉換的,儘管這一切並不簡單。得到足夠的信息以後,就能夠開發一款反編譯引擎來分析P-code並將它們轉換成機器碼或者是可理解的指令。
一個開發p-code反編譯引擎的例子和虛擬機實現的詳細信息能夠經過以下連接得到:
Defeating HyperUnpackMe2 With an IDA Processor Module, Rolf Rolles III
http://www.openrce.org/articles/full_view/28
7. 工具
本節列舉了逆向分析人員和惡意代碼分析人員能夠用來分析、脫殼的公開可用的工具。
免責聲明:這些都是第三方工具,筆者對這些工具可能致使的系統不穩定和可能影響系統的其餘問題不負任何責任。建議老是在測試或惡意代碼分析環境中運行這些工具。
7.1 OllyDbg
http://www.ollydbg.de/
逆向分析人員和惡意代碼分析人員使用的一款強大Ring3調試器。它的插件功能容許其餘的逆向分析人員建立更多的插件,使得逆向和脫殼變得愈來愈簡單。
7.2 Ollyscript
http://www.openrce.org/downloads/details/106/OllyScript
一個OllyDbg的插件,容許經過使用相似於彙編語言的腳本實現自動設置/處理斷點、補丁代碼/數據等。在執行重複性的工做或者是自動脫殼是尤爲有用。
7.3 Olly Advanced
http://www.openrce.org/downloads/details/241/Olly_Advanced
針對逆向分析人員若是說殼有盔甲的話,那麼這個OllyDbg的插件就是逆向分析人員調試器的盔甲。它有不少選項用來躲過反調試技術,隱藏OllyDbg使其不被殼檢測到。
7.4 OllyDump
http://www.openrce.org/downloads/details/108/OllyDump
成功脫殼後,這個OllyDbg插件能夠用來dump進程而且重建輸入表。
7.5 ImpRec
http://www.woodmann.com/crackz/Unpackers/Imprec16.zip
最後,這是另外一款dump進程和重建輸入表的工具。它是一款獨立的工具,它提供了最出色的輸入表重建能力。
8 參考
書籍:逆向工程,軟件保護
Reversing: Secrets of Reverse Engineering. E.Eilam.Wiley, 2005
Crackproof Your Software , P.Cerven.No Starch Press, 2002
書籍:Windows和處理器底層
Microsoft Windows Internal, 4th Edition . M. Russinovich, D. Solomon, Microsoft Press,
IA-32 Intel Architecture Software Developer’s Manual. Volume 1-3, Intel Corporation, 2006
連接:Windows底層
ReactOS Project
http://www.reactos.org/en/index.html
Source Search: http://www.reactos.org/generated/doxygen/
Wine Project
http://www.winehq.org/
Source Search: http://source.winehq.org/source/
The Undocumented Functions
http://undocumented.ntinternals.net
MSDN
http://msdn2.microsoft.com/en-us/default.aspx
連接:逆向工程,軟件保護,脫殼
OpenRCE
http://www.openrce.org
OpenRCE Anti Reverse Engineering Techniques Database
http://www.openrce.org/reference_library/anti_reversing
RCE Forums
http://www.woodmann.com/forum/index.php
EXETOOLS Forums
http://forum.exetools.com