最近學習反調試技術,總結了網絡上的一些反調試技術,文章中的代碼均通過調試,在OllyDbg中測試通過,同時謝謝看雪的《加密與解密》第三版
測試軟體:
IDA 最新5.5,使用5.4
OllyDbg 最新2.0,結合v1.10(漢化第二版)
一. 抗靜态分析技術
靜态分析是指從反彙編出來的程式清單上分析程式流程,反靜态分析主要是指擾亂彙編代碼可讀性。
1. 花指令
在原程式中添加一些彙編指令,添加後不影響原程式的正常功能,但是能使反彙編工具反彙編錯誤。
相關知識點
花指令是一堆彙編指令,對于程式而言,花指令是無用的彙編代碼,花指令的有無不影響程式的運作。彙編語言是機器指令的符号化,是底層的程式設計語言,在彙編時,每一條彙編語句都會根據CPU特定的指令符号表将彙編指令翻譯成二進制代碼。
根據反彙編工具的反彙編算法,構造代碼和資料,在指令流中插入很多“資料垃圾”,幹擾反彙編軟體的判斷,進而使得它錯誤地确定指令的起始位置,杜絕了先把程式代碼列出來再慢慢分析的做法。
在反彙編中,利用IDA Pro等靜态分析軟體可以将二進制程式反彙編成彙編代碼。花指令是利用了反彙編時單純根據機器指令字來決定反彙編結果來擾亂程式的靜态分析。編寫花指令的基本原則是保持堆棧的平衡。如push ebp把基址指針寄存器壓入堆棧,pop ebp是把基址指針起存期彈出堆棧,是以push ebp和pop ebp一定要成對的使用,這樣堆棧是平衡的,指令的運作不會影響程式的運作。
簡單示例
(1)未加花程式 能正确反彙編
原程式:
void Function()
{
Input++;
Output++;
Input+=Output;
printf("函數結果:%d,%d",Input,Output);
}
反彙編代碼:
IDA:
.text:00401096 rep stosd
.text:00401098 mov eax, Input
.text:0040109D add eax, 1
.text:004010A0 mov Input, eax
.text:004010A5 mov ecx, Output
.text:004010AB add ecx, 1
.text:004010AE mov Output, ecx
.text:004010B4 mov edx, Input
.text:004010BA add edx, Output
.text:004010C0 mov Input, edx
.text:004010C6 mov eax, Output
.text:004010CB push eax
.text:004010CC mov ecx, Input
.text:004010D2 push ecx
.text:004010D3 push offset s_PSDD ; "函數結果:%d,%d"
.text:004010D8 call printf
OD中:
(2)簡單加花後,在IDA中反彙編錯誤,在w32dasm中反彙編錯誤,但是在OD中反彙編正确
加花後程式:
void Function()
{
_asm jz label
_asm jnz label
_asm __emit 0e8h //e8是call指令
label:
Input++;
Output++;
Input+=Output;
printf("函數結果:%d,%d",Input,Output);
}
在OD中:
在IDA中:
.text:00401096 rep stosd
.text:00401098 jz short near ptr loc_40109C+1
.text:00401098
.text:0040109A jnz short near ptr loc_40109C+1
.text:0040109A
.text:0040109C
.text:0040109C loc_40109C: ; CODE XREF: .text:00401098 j
.text:0040109C ; .text:0040109A j
.text:0040109C call near ptr 4275D142h
.text:004010A1 add [ebx-3F5CFE40h], al
.text:004010A7 xor eax, 0D8B0042h
.text:004010AC mov esp, 83004235h
.text:004010B1 rol dword ptr [ecx], 89h
.text:004010B4 or eax, offset Output
.text:004010B9 mov edx, Input
.text:004010BF add edx, Output
.text:004010C5 mov Input, edx
.text:004010CB mov eax, Output
.text:004010D0 push eax
.text:004010D1 mov ecx, Input
.text:004010D7 push ecx
.text:004010D8 push offset s_PSDD ; "函數結果:%d,%d"
.text:004010DD call printf
(3)改進,加花後,IDA與OD均反彙編錯誤
改進後程式:
void Function()
{
_asm xor eax,eax
_asm test eax,eax
_asm jz label1
_asm jnz label0
label0:
_asm __emit 0e8h
label1:
Input++;
Output++;
Input+=Output;
printf("函數結果:%d,%d",Input,Output);
}
OD中:
IDA中:
.text:0040109C jz short near ptr loc_4010A0+1
.text:0040109C
.text:0040109E jnz short $+2
.text:004010A0
.text:004010A0 loc_4010A0: ; CODE XREF: .text:0040109C j
.text:004010A0 call near ptr 4275D146h
.text:004010A5 add [ebx-3F5CFE40h], al
.text:004010AB xor eax, 0D8B0042h
.text:004010B0 mov esp, 83004235h
.text:004010B5 rol dword ptr [ecx], 89h
.text:004010B8 or eax, offset Output
.text:004010BD mov edx, Input
.text:004010C3 add edx, Output
.text:004010C9 mov Input, edx
.text:004010CF mov eax, Output
.text:004010D4 push eax
.text:004010D5 mov ecx, Input
.text:004010DB push ecx
.text:004010DC push offset s_PSDD ; "函數結果:%d,%d"
.text:004010E1 call printf
應用
(1) 簡單加花
實作簡單,但是原理比較簡單,高手很容易就能去除,一般以消耗攻擊者的耐性來達到目的。
(2)複雜加花
類似于加殼
1. 記錄程式的原入口點,
2. 找到PE檔案的空白區域,在空白區域内寫入花指令(或者添加新節)
3. 把入口點位址改為新入口位址
4. 花指令執行完後跳轉到原入口點位址
在程式執行時,程式将從新的入口位址執行,即花指令先被執行,然後再執行程式原來的入口位址功能。
增加了靜态分析的難度,提高了代碼的資訊隐藏效果,該方法一般應用于病毒的免殺中。
2. 隐藏API
逆向分析從業人員往往就是通過API在極短時間内擷取了大量資訊,進而使他們成功定位目标程式的關鍵代碼段。是以隐藏對API 的調用可以有效地提高程式的抗分析能力。
例如一個簡單的程式:
int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
MessageBox(NULL,"test","box",0);
return 0;
}
反彙編代碼如下:
.text:00401028 mov esi, esp
.text:0040102A push 0 ; uType
.text:0040102C push offset Caption ; "Test"
.text:00401031 push offset Text ; "Reverse Me"
.text:00401036 push 0 ; hWnd
.text:00401038 call ds:MessageBoxA(x,x,x,x)
目的是讓反彙編的代碼看不到 ds: MessageBoxA(x,x,x,x) 這樣的提示。
1.最基本、最簡單的方法:
int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
TCHAR MsgBoxA[MAX_PATH]="MessageBoxA";
HMODULE hMod=LoadLibrary("user32.dll");
MYFUNC func=(MYFUNC)GetProcAddress(hMod,MsgBoxA);//擷取MessageBoxA的函數位址。
func(0,"Reverse Me","Test",0); //調用MessageBoxA函數。
FreeLibrary(hMod);
return 0;
}
IDA中(V5.4)
.text:0040102E mov eax, dword ptr ds:aMessageboxa ; "MessageBoxA"
.text:00401033 mov dword ptr [ebp+ProcName], eax
.text:00401039 mov ecx, dword ptr ds:aMessageboxa+4
.text:0040103F mov [ebp+var_100], ecx
.text:00401045 mov edx, dword ptr ds:aMessageboxa+8
.text:0040104B mov [ebp+var_FC], edx
.text:00401051 mov ecx, 3Eh
.text:00401056 xor eax, eax
.text:00401058 lea edi, [ebp+var_F8]
.text:0040105E rep stosd
.text:00401060 mov esi, esp
.text:00401062 push offset LibFileName ; "user32.dll"
.text:00401067 call ds:[email protected] ; LoadLibraryA(x)
.text:0040106D cmp esi, esp
.text:0040106F call __chkesp
.text:00401074 mov [ebp+hLibModule], eax
.text:0040107A mov esi, esp
.text:0040107C lea eax, [ebp+ProcName]
.text:00401082 push eax ; lpProcName
.text:00401083 mov ecx, [ebp+hLibModule]
.text:00401089 push ecx ; hModule
.text:0040108A call ds:[email protected] ; GetProcAddress(x,x)
.text:00401090 cmp esi, esp
.text:00401092 call __chkesp
.text:00401097 mov [ebp+var_10C], eax
.text:0040109D mov esi, esp
.text:0040109F push 0
.text:004010A1 push offset aTest ; "Test"
.text:004010A6 push offset aReverseMe ; "Reverse Me"
.text:004010AB push 0
.text:004010AD call [ebp+var_10C]
OD中:
3. 進行簡單加密處理
隐藏字元串"MessageBoxA","Reverse Me","Test"
int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
char MsgBoxA[]={0x5c,0x74,0x62,0x62,0x70,0x76,0x74,0x53,0x7e,0x69,0x50,0x00};
//字元串"MessageBoxA"的加密形式。
char lpText[]={0x43,0x74,0x67,0x74,0x63,0x62,0x74,0x31,0x5C,0x74,0x00};
//字元串"Reverse Me"的加密形式。
char lpCaption[]={0x45,0x74,0x62,0x65,0x00};
//字元串"Test"的加密形式。
for(int i=0;i<strlen(MsgBoxA);i++)
MsgBoxA[i]^=0x11; //解密字元串"MessageBoxA"
for(i=0;i<strlen(lpText);i++)
lpText[i]^=0x11; //解密字元串"Reverse Me"
for(i=0;i<strlen(lpCaption);i++)
lpCaption[i]^=0x11; //解密字元串"Test"
HMODULE hMod=LoadLibrary("user32.dll");
if(hMod)
{
MYFUNC func=(MYFUNC)GetProcAddress(hMod,MsgBoxA); //擷取MessageBoxA()的函數位址。
func(0,lpText,lpCaption,0); //調用MessageBoxA函數。
FreeLibrary(hMod);
}
return 0;
}
IDA中:
.text:0040146E mov edx, [ebp+arg_8]
.text:00401471 push edx
.text:00401472 mov eax, [ebp+arg_4]
.text:00401475 push eax
.text:00401476 push offset s_SecondChanceA ; "Second Chance Assertion Failed: File %s"...
.text:0040147B lea ecx, [ebp+OutputString]
.text:00401481 push ecx
.text:00401482 call dword_4235D0
OD中:
3.将GetProcAddress也隐藏
類似上面的方法
4.簡單的SMC
Decrypt((char * )404445,(char * )404543);
__asm inc eax
__asm dec eax
HMODULE hMod=LoadLibrary("user32.dll");
if(hMod)
{
MYFUNC func=(MYFUNC)GetProcAddress(hMod,MsgBoxA); //擷取MessageBoxA()的函數位址。
func(0,lpText,lpCaption,0); //調用MessageBoxA函數。
FreeLibrary(hMod);
}
__asm inc eax
__asm dec eax
見下
應用
廣泛
凡是出現可能會洩露資訊的API調用中,均可用該技術進行隐藏。
4. SMC技術
基本概念
SMC技術(self modifying code 程式自修改技術),是一種将可執行檔案中的代碼或資料進行加密,防止被逆向工程工具對程式進行靜态分析的方法。
程式隻有在運作時才對代碼或資料進行解密,進而正常運作程式和通路資料。
二.抗動态調試技術
動态調試,也就是使用調試器等工具對軟體的運作進行跟蹤,進而對關鍵代碼進行逆向工程,或者幹脆破壞軟體中的保護措施,對軟體進行盜版,抗動态調試即阻礙這個逆向過程。
反調試目的:主要針對OD等調試跟蹤程式,檢測記憶體或者程序中是否有調試軟體運作,進而讓這些程式失效或者直接失去響應。檢測自身是否運作在調試器下。
1.調用Win32API檢測程式是否處于調試狀态
IsDebuggerPresentFlag()
// IsDebuggerPresentFlag
// 傳回值為true時檢測到調試器,否則無調試器
/*
bool IsDebuggerPresentFlag()
{
HINSTANCE hInst = LoadLibrary("kernel32.dll");
if(hInst!=NULL)
{
FARPROC pIsDebuggerPresent=GetProcAddress(hInst,"IsDebuggerPresent");
if(pIsDebuggerPresent!=NULL)
return pIsDebuggerPresent();
}
return FALSE;
}
也可以自實作,
bool IsDebuggerPresentFlag()
{
__asm
{
mov eax, fs:[0x30] //在位于TEB偏移30h處獲得PEB位址
movzx eax, byte ptr [eax+2] //獲得PEB偏移2h處BeingDebugged的值
test eax, eax
jne rt_label
jmp rf_label
}
rt_label:
return true;
rf_label:
return false;
}
測試結果:該方法太古老,在OD中不能檢測
NtGlobalFlags()
// NtGlobalFlags,可檢測出檢測OllyDebug
// 傳回值為true時檢測到調試器,否則無調試器
bool NtGlobalFlags()
{
__asm
{
mov eax, fs:[30h]
mov eax, [eax+68h]
and eax, 0x70
test eax, eax
jne rt_label
jmp rf_label
}
rt_label:
return true;
rf_label:
return false;
}
在OD中能檢測,但是用OD的插件能夠去掉(HideOD插件選上後不能檢測,選上auto to run hideod 以及hideNtDebugBit)
HeapFlags()
// HeapFlags
// 傳回值為true時檢測到調試器,否則無調試器
bool HeapFlags()
{
__asm
{
mov eax, fs:[30h]
mov eax, [eax+18h] ;PEB.ProcessHeap
mov eax, [eax+0ch] ;PEB.ProcessHeap.ForceFlags
cmp eax, 2
jne rt_label
jmp rf_label
}
rt_label:
return true;
rf_label:
return false;
}
在OD中,能檢測,但是用OD的插件能夠去掉(HideOD插件選上後不能檢測,選上auto to run hideod以及hideNtDebugBit)
RemoteDebuggerPresent()
// RemoteDebuggerPresent
// 傳回值為true時檢測到調試器,否則無調試器
typedef BOOL (WINAPI *CHECK_REMOTE_DENUGGER_PRESENT)(HANDLE,PBOOL);
BOOL RemoteDebuggerPresent()
{
BOOL bDebuggerPresent=FALSE;
CHECK_REMOTE_DENUGGER_PRESENT CheckRemoteDebuggerPresent;
HMODULE hModule = GetModuleHandle("kernel32.dll");
if (hModule==INVALID_HANDLE_VALUE)
{
return FALSE;
}
CheckRemoteDebuggerPresent =(CHECK_REMOTE_DENUGGER_PRESENT)GetProcAddress(hModule, "CheckRemoteDebuggerPresent");
if( CheckRemoteDebuggerPresent(GetCurrentProcess(),&bDebuggerPresent))
{
return bDebuggerPresent;
}
return FALSE;
}
在OD中,能檢測,但是用OD的插件能夠去掉(hideOd插件選上後不能測試,選上CheckRemoteDebuggerPresent)
優缺點
你可以在你要反調試的程式中每個一段時間調用一次這個函數來檢測程序是否被調試了,但是調用這個api實在是不安全,因為很可能這個函數已經被 hook了,你的調用結果将完全被hook這個函數的人控制了,盡管可以用彙編代碼自己實作,但是調試着也可以修改标志變量,讓你擷取到的東西無效。
實作簡單,仍然被很多軟體采用,對于初級調試人員能夠起到作用,但是進階調試人員很容易就能看出來,網絡上存在大量的繞過此技術的方法,一些調試軟體的插件基本上都能繞過,在這類函數上花太多心思也效果不大。
2.父程序檢測
基本原理
每個程式被正常啟動後,都有一個父程序,這個父程序通常是Explorer.exe(資料總管啟動)、cmd.exe(指令行啟動)或者Services.exe(系統服務)中的一種,還有一種情況是使用調試器啟動程序後,該程序的父程序就是調試器。當某程序的父程序不是可信的程序時,一般可以認為該程序被調試了。
具體流程
1、 通過TEB或者使用GetCurrentProcessId來檢索目前程序的PID。
2、 用Process32First和Process32Next得到所有程序的清單,通過PROCESSENTRY32.th32ParentProcessID獲得目前程序的父程序PID
3、 如果父程序的PID不是信任的程式,那麼目标程序可能被調試了。
void GetFileNameFromPath(char* szSource)
{
char *szTemp=strrchr(szSource,'\\');
if(szTemp!=NULL)
{
szTemp++;
DWORD l=DWORD(strlen(szTemp))+1;
CopyMemory(szSource,szTemp,l);
}
}
bool ParentProcess()
{
HANDLE hSnapshot=NULL;
DWORD PID_child;
DWORD PID_parent=0;
DWORD PID_explorer=0;
DWORD PID_cmd=0;
DWORD PID_Services=0;
HANDLE hh_parent = NULL;
PROCESSENTRY32 pe32 = {0};
pe32.dwSize = sizeof(PROCESSENTRY32);//0x128;
PID_child=GetCurrentProcessId();//getpid(); //獲得目前程序的PID
hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS,0);
if (Process32First(hSnapshot, &pe32))
{
while (Process32Next(hSnapshot, &pe32)) //對所有目前程序進行判斷
{
GetFileNameFromPath(pe32.szExeFile);
// CharUpperBuff(pe32.szExeFile,(DWORD)strlen(pe32.szExeFile));
if(strcmpi(pe32.szExeFile,"EXPLORER.EXE")==0)
{
if(PID_explorer==0)
PID_explorer=pe32.th32ProcessID; //找到explorer程序的PID
}
if(pe32.th32ProcessID==PID_child) //如果此程序的PID等于目前程序的PID
{
PID_parent=pe32.th32ParentProcessID;
}
}
}
if(PID_parent==PID_explorer)
{
return true;
}
else
{
return false;
}
}
在OD中能夠檢測
3. 進階保護技術
3.1虛拟機加密
基于虛拟機的代碼保護技術,将基于x86彙編系統的可執行代碼裝換為位元組碼指令系統的代碼,即可以将檔案原始的指令編譯成虛拟處理器使用的僞指令。對于破解者來說,僞指令相當于一種全新的指令系統,他們将無法得知這種指令系統的運作機制。簡言之,作用就是把代碼弄的很亂,讓人根本看不明白。
一個虛拟機引擎由編譯器、解釋器和VPU Context(虛拟CPU環境)組成,再配上一個或多個指令系統。編譯器用來将一條條X86指令解釋成自己的指令系統。解釋器附加在被加殼的軟體中,用來解釋這些自定義的指令。通常有一套或多套自己定義的指令系統,用來虛拟執行這些bytecode。
如今已經将虛拟機應用到商業中的保護殼現有三款:Vmprotect,themida和 execrypt。
Themida是一款綜合型強殼,包含了資源加密、代碼變形和虛拟機技術。
Vmprotect是一款專門對代碼加虛拟機加密保護的加殼軟體,其保護強度是3款當中最強的,可以修改應用程式的源代碼。VMProtect 将原檔案的部分代碼轉換為在虛拟機中運作的位元組碼。可以将虛拟機想像成帶有不同于 Intel 8086 處理器指令系統的虛拟處理器;例如,虛拟機沒有比較兩個操作數的指令,也沒有條件跳轉和無條件跳轉等。Vmprotect最早由2005年問世起,至今尚無人公開宣稱能還原其原彙編代碼,可見其強度,是以Vmprotect已逐漸被人們用來保護其産品,使破解者對于這個看起來毛茸茸的刺猬毫無辦法。
3.2基于TLS的反調試方法
基于TLS的反調試方法,即在實際的入口點代碼執行之前執行代碼,這是通過使用Thread Local Storage (TLS)回調函數來實作的。通過這些回調函數執行反調試,這樣逆向分析人員将無法跟蹤這些例程。
TLS全稱為Thread Local Storage,是Windows為解決一個程序中多個線程同時通路全局變量而提供的機制。TLS可以簡單地由作業系統代為完成整個互斥過程,也可以由使用者自己編寫控制信号量的函數。當程序中的線程通路預先制定的記憶體空間時,作業系統會調用系統預設的或使用者自定義的信号量函數,保證資料的完整性與正确性。
當使用者選擇使用自己編寫的信号量函數時,在應用程式初始化階段,系統将要調用一個由使用者編寫的初始化函數以完成信号量的初始化以及其他的一些初始化工作。此調用必須在程式真正開始執行到入口點之前就完成,以保證程式執行的正确性。
TLS回調函數具有如下的函數原型:
void NTAPI TlsCallBackFunction(PVOID Handle, DWORD Reason, PVOID Reserve);
充分利用TLS回調函數在程式入口點之前就能獲得程式控制權的特性,在TLS回調函數中進行反調試操作比傳統的反調試技術有更好的效果。
F-Secure的Blacklight就加入了這種方法,它建立的的TLS回調函數使用了在主程序對象完全被建立之前讓主程序分叉的方法,這樣就能夠達到迷惑調試器的效果。
3.3異常處理SEH
異常處理指人為産生異常,改變程式流程或在異常處理時進行解碼,加大跟蹤難度。常見的異常有記憶體存取錯誤異常,中斷異常(INT3/單步/其它中斷等),非法指令,非法EIP等。
例如如下一個簡單的例子:
int APIENTRY WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow)
{
SetUnhandledExceptionFilter(UnhandledExceptionFilter1);
int a=2,b=0,c=0;
c=a/b;
return 0;
}
LONG WINAPI UnhandledExceptionFilter1(struct _EXCEPTION_POINTERS* ExceptionInfo)
{
MessageBox(0,"SEHtest","haha",0);
return 0;
}
把代碼放到SetUnhandledExceptionFilter設定的函數裡面。通過人為觸發一個unhandled exception來執行。由于設定的UnhandledExceptionFilter函數隻有在調試器沒有加載的時候才會被系統調用,這裡就巧妙地使用了系統的這個功能來保護代碼。
UnhandledExceptionFilter在沒有debugger attach的時候才會被調用,UnhandledExceptionFilter是否調用取決于系統核心的判斷。使用者态的調試器要想改變這個行為,要破費一番腦筋了。測試中,在沒有調試器時,能正常執行,用OD進行調試時,斷在00401065處:
Armadillo中就采用了seh技術改變程式的流程,在ASProtect 1.31版中,共有29-31個SEH異常結構,對不同的程式加密方式可以不同,是以設定的SEH結構也稍有不同