最近学习反调试技术,总结了网络上的一些反调试技术,文章中的代码均通过调试,在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)
{
/********************************************************
1. 定义字符串
********************************************************/
TCHAR MsgBoxA[MAX_PATH]="MessageBoxA";
/********************************************************
2. 获取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:__imp__LoadLibraryA@4 ; 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:__imp__GetProcAddress@8 ; 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)
{
/********************************************************
1. 加密字符串
********************************************************/
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"的加密形式。
/********************************************************
2. 解密字符串
********************************************************/
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"
/********************************************************
3. 获取MessageBoxA的函数地址
********************************************************/
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
/********************************************************
3. 获取MessageBoxA的函数地址
********************************************************/
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结构也稍有不同。
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!