與靜態反逆向技術相比,動態的更加難以反制。一直阻擋 Debugger 的行動與各種花樣百出的技術是分析時的噩夢。它的目標是隱藏和保護主要的代碼,通常被反逆向技術偵測到後,干擾行為也會開始干擾反編譯器。
這邊來講講一些常見的動態反逆向技術
。
此次的程式與原始碼都放在 :
https://github.com/Dinlon5566/IT_Reverse_Engineering/tree/main/Dx29
很簡單直白的反逆向方法,主要就是檢查某段代碼的執行時間來判斷。若在其中有出現反編譯行為,就會使指令執行的時間比正常執行時間長多了。
首先這是最簡單的演示。透過 clock( )
來取得自 CRT
初始化以來所消耗的時間。
那程式的話就用 loop 來代替一般的程式。
#include <iostream>
#include <Windows.h>
#include <time.h>
int main()
{
clock_t start, finish;
printf("Clock start\n");
start = clock();
for (int i = 0; i < INT_MAX; i++) {}
printf("Clock Stop\n");
finish = clock();
printf("%lf\n",double (finish - start)/CLOCKS_PER_SEC);
MessageBox(0,L"",L"",0);
}
先嘗試一般執行看看 :
花了大概三秒多。
此時利用 x64dbg 來執行,在 loop 段稍微暫停一下後再繼續 :
可以看到比一般執行長多了。
但是這樣的計時太不精準,於是通常會使用下面這個方法。
與 clock( )
相比,RDTSC
直接用了 CPU 對於每個 Clock Cycle
進行記數,這樣可以計算出從計時點A 到計時點B 的每一個 tick
。可以利用組語的 RDTSC
指令來取得 TSC ( Time Stamp Counter )
中的時間。
這邊解釋一下 RDTSC
指令 : 時間計數器在64位元的 MSR
中,這個指令會把高 32 位元存在 EDX
,低 32 位元放在 EAX
。
除了使用組合語言指令,也有 C 上的
__rdtsc
可以使用。
https://learn.microsoft.com/zh-tw/cpp/intrinsics/rdtsc?view=msvc-170
這邊展示用 __rdtsc( )
與組合語言的用法,這兩個 return 的東西是一樣的。
UINT64 rdtsc() {
UINT64 res;
res = __rdtsc();
return res;
}
UINT64 AsmRdtsc() {
UINT64 res;
UINT32 highByte,lowByte;
__asm {
RDTSC
mov highByte,edx
mov lowByte, eax
}
res = highByte;
res <<= 0x20;
res |= lowByte;
return res;
}
這邊做一個直接取用 __rdtsc()
來計時的偵測器。
int main() {
UINT64 T1 = __rdtsc();
for (int i = 0; i < 50; i++) {}
UINT64 T2 = __rdtsc();
UINT64 delta = T2 - T1;
printf("Time = %lld\n",delta);
if (delta > 0xFFFF) {
printf("OUT!\n");
system("pause");
return 0;
}
printf("SAVE~ : )\n");
system("pause");
return 0;
}
來看三種情況 :
BP
)可以看到在不同情況下時間會在不同的區間,可以依據區間來判斷該程式是否被逆向分析中。
而這類破解的方法有 :
kernel
驅動程式直接廢掉 RDTSC
。雖然要查出並破解這份範例程式的難度不難,但實際上這個技術會穿插在程式的各處,導致判斷起來非常困難。
一般的程序若是出現了異常的時候,會透過 SEH
的機制使 OS
接收到例外
中斷,然後調用終止處理常式
處裡。但是程序若運行在 Deubgger 下,例外將會傳給 Debugger 的 例外狀況處理常式
( 類似用 try 包住整份程式 );透過這個特徵可以用來判斷程序是正常運行還是在反編譯環境。
SEH ( 結構例外狀況處裡 ) 說明手冊
https://learn.microsoft.com/zh-tw/cpp/cpp/structured-exception-handling-c-cpp?view=msvc-170
利用GetExceptionCode
取的的例外狀況代碼
https://learn.microsoft.com/zh-tw/windows/win32/debug/getexceptioncode
而 EXECEPTION_BREAKPOINT
( 中斷點例外 ) 是在我們進行 Debugger 中很常使用的功能,當我們按下反編譯器旁邊的紅點,或是在 x64bdg 上停在某一個點的時候都會把到這樣的例外。
這邊演示若在程式碼中插入 0xCC
這個之前在 API Hook 中使用的 INT3 斷點
,反編譯器將會停止在該位置。
printf("STOP\n");
__asm {
int 0x03
}
printf("Continue\n");
在 x32dbg 按下 F9,就算沒有設定 BP 也會停在 0x40104E
位置。而正常開啟的話則會直接退出不顯示"Continue"。
那,要甚麼透過這個方法來判斷是否有 Debugger 跑過呢 ? 其實很簡單,在異常前後設置可以接收例外的指令,當例外觸發後的例外接收器接有接收到的例外則是正常狀況;若是例外接收器沒有接到例外就可以判斷是運行在 Debugger ( 畢竟正常狀況下例外不會自己解決 )。
在 C++ 中可以利用 try
、except
、finally
關鍵字來接收例外,而組合語言上可以利用這幾行來設置
PUSH Handler // 設定的處裡位置
PUSH DWORD PTR FS:[0]
MOV DWORD PTR FS:[0],ESP
並且需要在程序中止前刪除掉 SEH
POP DWORD PTR FS:[0]
ADD ESP, 4
那來試試看應用在 C++ 上面吧 ,重點看在 __asm
中的處裡方式吧
#include <iostream>
#include "windows.h"
void debugerFound() {
MessageBox(0, L"Debuger Found", L"Excption_SEH", 0);
}
int main()
{
printf("Start");
__asm {
// 設置 SEH
push handler
push DWORD PTR fs : [0]
mov DWORD PTR fs:[0], esp
// 觸發 INT3,應該要直接跳到 handler 來處理異常
int 0x03
// 若是異常被處裡就會跑到這裡
call debugerFound
mov eax,0xFFFFFFFF // 直接讓你非法存取
jmp eax
handler:
// 處裡異常
mov eax, dword ptr ss : [esp + 0xc]
mov ebx, normal_code
mov dword ptr ds : [eax + 0xb8] , ebx
xor eax, eax
retn
normal_code :
// 拔除 SEH
pop dword ptr fs : [0]
add esp, 4
}
MessageBox(0, L"Save", L"Excption_SEH", 0);
}
利用 ollyDbg ( 因為 x64dbg 會自動把例外接收後又把例外放回去 ) 來執行這份程式,會發現中斷點被觸發後被跳到了非法存取 ( EIP
= 0xFFFFFFFF
)。
今天就到這邊,明天帶給大家關於陷阱標誌的反逆向技術。