大概是在一年半之前我在本身CSDN博客上写了详解反虚拟机技术和详解反调试技术,没想到看的人还不少,有人甚至给我发私信发邮件,在百度和谷歌搜索“反调试”和“反虚拟机”,第一条结果就是个人文章。我决定在看雪也分享一下。固然我只是作了个整理收集的工做,没有一条技术和一行代码是我原创的,参考连接会附在最后。html
反调试技术,恶意代码用它识别是否被调试,或者让调试器失效。恶意代码编写者意识到分析人员常用调试器来观察恶意代码的操做,所以他们使用反调试技术尽量地延长恶意代码的分析时间。为了阻止调试器的分析,当恶意代码意识到本身被调试时,它们可能改变正常的执行路径或者修改自身程序让本身崩溃,从而增长调试时间和复杂度。不少种反调试技术能够达到反调试效果。这里介绍当前经常使用的几种反调试技术,同时也会介绍一些逃避反调试的技巧。ios
恶意代码会使用多种技术探测调试器调试它的痕迹,其中包括使用Windows API、手动检测调试器人工痕迹的内存结构和查询调试器遗留在系统中的痕迹等。调试器探测是恶意代码最经常使用的反调试技术。git
使用Windows API函数检测调试器是否存在是最简单的反调试技术。Windows操做系统中提供了这样一些API,应用程序能够经过调用这些API,来检测本身是否正在被调试。这些API中有些是专门用来检测调试器的存在的,而另一些API是出于其余目的而设计的,但也能够被改造用来探测调试器的存在。其中很小部分API函数没有在微软官方文档显示。一般,防止恶意代码使用API进行反调试的最简单的办法是在恶意代码运行期间修改恶意代码,使其不能调用探测调试器的API函数,或者修改这些API函数的返回值,确保恶意代码执行合适的路径。与这些方法相比,较复杂的作法是挂钩这些函数,如使用rootkit技术。程序员
IsDebuggerPresent查询进程环境块(PEB)中的IsDebugged标志。若是进程没有运行在调试器环境中,函数返回0;若是调试附加了进程,函数返回一个非零值。github
1
2
3
4
|
BOOL
CheckDebug()
{
return
IsDebuggerPresent();
}
|
CheckRemoteDebuggerPresent同IsDebuggerPresent几乎一致。它不只能够探测系统其余进程是否被调试,经过传递自身进程句柄还能够探测自身是否被调试。windows
1
2
3
4
5
6
|
BOOL
CheckDebug()
{
BOOL
ret;
CheckRemoteDebuggerPresent(GetCurrentProcess(), &ret);
return
ret;
}
|
这个函数是Ntdll.dll中一个API,它用来提取一个给定进程的信息。它的第一个参数是进程句柄,第二个参数告诉咱们它须要提取进程信息的类型。为第二个参数指定特定值并调用该函数,相关信息就会设置到第三个参数。第二个参数是一个枚举类型,其中与反调试有关的成员有ProcessDebugPort(0x7)、ProcessDebugObjectHandle(0x1E)和ProcessDebugFlags(0x1F)。例如将该参数置为ProcessDebugPort,若是进程正在被调试,则返回调试端口,不然返回0。数组
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
BOOL
CheckDebug()
{
int
debugPort = 0;
HMODULE
hModule = LoadLibrary(
"Ntdll.dll"
);
NtQueryInformationProcessPtr NtQueryInformationProcess = (NtQueryInformationProcessPtr)GetProcAddress(hModule,
"NtQueryInformationProcess"
);
NtQueryInformationProcess(GetCurrentProcess(), 0x7, &debugPort,
sizeof
(debugPort), NULL);
return
debugPort != 0;
}
BOOL
CheckDebug()
{
HANDLE
hdebugObject = NULL;
HMODULE
hModule = LoadLibrary(
"Ntdll.dll"
);
NtQueryInformationProcessPtr NtQueryInformationProcess = (NtQueryInformationProcessPtr)GetProcAddress(hModule,
"NtQueryInformationProcess"
);
NtQueryInformationProcess(GetCurrentProcess(), 0x1E, &hdebugObject,
sizeof
(hdebugObject), NULL);
return
hdebugObject != NULL;
}
BOOL
CheckDebug()
{
BOOL
bdebugFlag = TRUE;
HMODULE
hModule = LoadLibrary(
"Ntdll.dll"
);
NtQueryInformationProcessPtr NtQueryInformationProcess = (NtQueryInformationProcessPtr)GetProcAddress(hModule,
"NtQueryInformationProcess"
);
NtQueryInformationProcess(GetCurrentProcess(), 0x1E, &bdebugFlag,
sizeof
(bdebugFlag), NULL);
return
bdebugFlag != TRUE;
}
|
编写应用程序时,常常须要涉及到错误处理问题。许多函数调用只用TRUE和FALSE来代表函数的运行结果。一旦出现错误,MSDN中每每会指出请用GetLastError()函数来得到错误缘由。恶意代码可使用异常来破坏或者探测调试器。调试器捕获异常后,并不会当即将处理权返回被调试进程处理,大多数利用异常的反调试技术每每据此来检测调试器。多数调试器默认的设置是捕获异常后不将异常传递给应用程序。若是调试器不能将异常结果正确返回到被调试进程,那么这种异常失效能够被进程内部的异常处理机制探测。
对于OutputDebugString函数,它的做用是在调试器中显示一个字符串,同时它也能够用来探测调试器的存在。使用SetLastError函数,将当前的错误码设置为一个任意值。若是进程没有被调试器附加,调用OutputDebugString函数会失败,错误码会从新设置,所以GetLastError获取的错误码应该不是咱们设置的任意值。但若是进程被调试器附加,调用OutputDebugString函数会成功,这时GetLastError获取的错误码应该没改变。安全
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
BOOL
CheckDebug()
{
DWORD
errorValue = 12345;
SetLastError(errorValue);
OutputDebugString(
"Test for debugger!"
);
if
(GetLastError() == errorValue)
{
return
TRUE;
}
else
{
return
FALSE;
}
}
|
对于DeleteFiber函数,若是给它传递一个无效的参数的话会抛出ERROR_INVALID_PARAMETER异常。若是进程正在被调试的话,异常会被调试器捕获。因此,一样能够经过验证LastError值来检测调试器的存在。如代码所示,0x57就是指ERROR_INVALID_PARAMETER。数据结构
1
2
3
4
5
6
|
BOOL
CheckDebug()
{
char
fib[1024] = {0};
DeleteFiber(fib);
return
(GetLastError() != 0x57);
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
BOOL
CheckDebug()
{
DWORD
ret = CloseHandle((
HANDLE
)0x1234);
if
(ret != 0 || GetLastError() != ERROR_INVALID_HANDLE)
{
return
TRUE;
}
else
{
return
FALSE;
}
}
BOOL
CheckDebug()
{
DWORD
ret = CloseWindow((
HWND
)0x1234);
if
(ret != 0 || GetLastError() != ERROR_INVALID_WINDOW_HANDLE)
{
return
TRUE;
}
else
{
return
FALSE;
}
}
|
ZwSetInformationThread拥有两个参数,第一个参数用来接收当前线程的句柄,第二个参数表示线程信息类型,若其值设置为ThreadHideFromDebugger(0x11),使用语句ZwSetInformationThread(GetCurrentThread(), ThreadHideFromDebugger, NULL, 0);调用该函数后,调试进程就会被分离出来。该函数不会对正常运行的程序产生任何影响,但若运行的是调试器程序,由于该函数隐藏了当前线程,调试器没法再收到该线程的调试事件,最终中止调试。还有一个函数DebugActiveProcessStop用来分离调试器和被调试进程,从而中止调试。两个API容易混淆,须要牢记它们的区别。框架
虽然使用Windows API是探测调试器存在的最简单办法,但手动检查数据结构是恶意代码编写者最常使用的办法。这是由于不少时候经过Windows API实现的反调试技术无效,例如这些API函数被rootkit挂钩,并返回错误信息。所以,恶意代码编写者常常手动执行与这些API功能相同的操做。在手动检测中,PEB结构中的一些标志暴露了调试器存在的信息。这里,咱们关注检测调试器存在经常使用的一些标志。
Windows操做系统维护着每一个正在运行的进程的PEB结构,它包含与这个进程相关的全部用户态参数。这些参数包括进程环境数据,环境数据包括环境变量、加载的模块列表、内存地址,以及调试器状态。
进程运行时,位置fs:[30h]指向PEB的基地址。为了实现反调试技术,恶意代码经过这个位置检查BeingDebugged标志,这个标志标识进程是否正在被调试。
1
2
3
4
5
6
7
8
9
10
11
|
BOOL
CheckDebug()
{
int
result = 0;
__asm
{
mov eax, fs:[30h]
mov al,
BYTE
PTR [eax + 2]
mov result, al
}
return
result != 0;
}
|
这种检查有多种形式,最终,条件跳转决定代码的路径。避免这种问题最简单的方法是在执行跳转指令前,手动修改零标志,强制执行跳转(或者不跳转)。
能够或者手动修改BeingDebugged属性值为0。在OllyDbg中安装命令行插件,为了启动该插件,用OllyDbg加载恶意代码,选择Plugins->Command Line->Command Line选项,在命令行窗口输入下面的命令。
如图所示,这条命令会将BeingDebugged属性转储到转储面板窗口。右键单击BeingDebugged属性,选择Binary->Fill With 00's,这时属性被设置为0。
OllyDbg的一些插件能够帮助咱们修改BeingDebugged标志。其中最流行的有HideDebugger、Hidedebug和PhantOm。以PhantOm为例,一样将dll文件拷贝到OllyDbg的安装目录下就会自动安装。选择Plugins->PhantOm->Options选项,勾选hide from PEB便可。
Reserved数组中一个未公开的位置叫做ProcessHeap,它被设置为加载器为进程分配的第一个堆的位置。ProcessHeap位于PEB结构的0x18处。第一个堆头部有一个属性字段,它告诉内核这个堆是否在调试器中建立。这些属性叫做ForceFlags和Flags。在Windows XP系统中,ForceFlags属性位于堆头部偏移量0x10处;在Windows 7系统中,对于32位的应用程序来讲ForceFlags属性位于堆头部偏移量0x44处。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
BOOL
CheckDebug()
{
int
result = 0;
DWORD
dwVersion = GetVersion();
DWORD
dwWindowsMajorVersion = (
DWORD
)(LOBYTE(LOWORD(dwVersion)));
//for xp
if
(dwWindowsMajorVersion == 5)
{
__asm
{
mov eax, fs:[30h]
mov eax, [eax + 18h]
mov eax, [eax + 10h]
mov result, eax
}
}
else
{
__asm
{
mov eax, fs:[30h]
mov eax, [eax + 18h]
mov eax, [eax + 44h]
mov result, eax
}
}
return
result != 0;
}
|
一样,恶意代码也能够检查Windows XP系统中偏移量0x0C处,或者Windows 7系统中偏移量0x40处的Flags属性。这个属性总与ForceFlags属性大体相同,但一般状况下Flags与值2进行比较。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
BOOL
CheckDebug()
{
int
result = 0;
DWORD
dwVersion = GetVersion();
DWORD
dwWindowsMajorVersion = (
DWORD
)(LOBYTE(LOWORD(dwVersion)));
//for xp
if
(dwWindowsMajorVersion == 5)
{
__asm
{
mov eax, fs:[30h]
mov eax, [eax + 18h]
mov eax, [eax + 0ch]
mov result, eax
}
}
else
{
__asm
{
mov eax, fs:[30h]
mov eax, [eax + 18h]
mov eax, [eax + 40h]
mov result, eax
}
}
return
result != 2;
}
|
避免这种问题方法和前面的差很少。若是用OllyDbg的命令行插件修改,输入的命令为dump ds:[fs:[30]+0x18]+0x10。若是用PhantOm插件,它会禁用调试堆建立功能而不须要手动设置。
因为调试器中启动进程与正常模式下启动进程有些不一样,因此它们建立内存堆的方式也不一样。系统使用PEB结构偏移量0x68处的一个未公开位置,来决定如何建立堆结构。若是这个位置的值为0x70,咱们就知道进程正运行在调试器中。
1
2
3
4
5
6
7
8
9
10
11
12
|
BOOL
CheckDebug()
{
int
result = 0;
__asm
{
mov eax, fs:[30h]
mov eax, [eax + 68h]
and eax, 0x70
mov result, eax
}
return
result != 0;
}
|
操做系统建立堆时,值0x70是下列标志的一个组合。若是进程从调试器启动,那么进程的这些标志将被设置。
(FLG_HEAP_ENABLE_TAIL_CHECK|FLG_HEAP_ENABLE_FREE_CHECK|FLG_HEAP_VALIDATE_PARAMETERS)
避免这种问题方法和前面的差很少。若是用OllyDbg的命令行插件修改,输入的命令为dump fs:[30]+0x68。若是用PhantOm插件,它会逃避使用NTGlobalFlag的反调试技术而不须要手动设置。
一般,咱们使用调试工具来分析恶意代码,但这些工具会在系统中驻留一些痕迹。恶意代码经过搜索这种系统痕迹,来肯定你是否试图分析它。
下面是调试器在注册表中的一个经常使用位置。
SOFTWARE\Microsoft\Windows NT\CurrentVersion\AeDebug(32位系统)
SOFTWARE\Wow6432Node\Microsoft\WindowsNT\CurrentVersion\AeDebug(64位系统)
该注册表项指定当应用程序发生错误时,触发哪个调试器。默认状况下,它被设置为Dr.Watson。若是该这册表的键值被修改成OllyDbg,则恶意代码就可能肯定它正在被调试。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
BOOL
CheckDebug()
{
BOOL
is_64;
IsWow64Process(GetCurrentProcess(), &is_64);
HKEY
hkey = NULL;
char
key[] =
"Debugger"
;
char
reg_dir_32bit[] =
"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\AeDebug"
;
char
reg_dir_64bit[] =
"SOFTWARE\\Wow6432Node\\Microsoft\\WindowsNT\\CurrentVersion\\AeDebug"
;
DWORD
ret = 0;
if
(is_64)
{
ret = RegCreateKeyA(HKEY_LOCAL_MACHINE, reg_dir_64bit, &hkey);
}
else
{
ret = RegCreateKeyA(HKEY_LOCAL_MACHINE, reg_dir_32bit, &hkey);
}
if
(ret != ERROR_SUCCESS)
{
return
FALSE;
}
char
tmp[256];
DWORD
len = 256;
DWORD
type;
ret = RegQueryValueExA(hkey, key, NULL, &type, (
LPBYTE
)tmp, &len);
if
(
strstr
(tmp,
"OllyIce"
)!=NULL ||
strstr
(tmp,
"OllyDBG"
)!=NULL ||
strstr
(tmp,
"WinDbg"
)!=NULL ||
strstr
(tmp,
"x64dbg"
)!=NULL ||
strstr
(tmp,
"Immunity"
)!=NULL)
{
return
TRUE;
}
else
{
return
FALSE;
}
}
|
FindWindow函数检索处理顶级窗口的类名和窗口名称匹配指定的字符串。
1
2
3
4
5
6
7
8
9
10
11
|
BOOL
CheckDebug()
{
if
(FindWindowA(
"OLLYDBG"
, NULL)!=NULL || FindWindowA(
"WinDbgFrameClass"
, NULL)!=NULL || FindWindowA(
"QWidget"
, NULL)!=NULL)
{
return
TRUE;
}
else
{
return
FALSE;
}
}
|
EnumWindows函数枚举全部屏幕上的顶层窗口,并将窗口句柄传送给应用程序定义的回调函数。
1
2
3
4
5
6
7
8
9
10
|
BOOL
CALLBACK EnumWndProc(
HWND
hwnd,
LPARAM
lParam)
{
char
cur_window[1024];
GetWindowTextA(hwnd, cur_window, 1023);
if
(
strstr
(cur_window,
"WinDbg"
)!=NULL ||
strstr
(cur_window,
"x64_dbg"
)!=NULL ||
strstr
(cur_window,
"OllyICE"
)!=NULL ||
strstr
(cur_window,
"OllyDBG"
)!=NULL ||
strstr
(cur_window,
"Immunity"
)!=NULL)
{
*((
BOOL
*)lParam) = TRUE;
}
return
TRUE;
}
|
1
2
3
4
5
6
|
BOOL
CheckDebug()
{
BOOL
ret = FALSE;
EnumWindows(EnumWndProc, (
LPARAM
)&ret);
return
ret;
}
|
GetForegroundWindow获取一个前台窗口的句柄。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
BOOL
CheckDebug()
{
char
fore_window[1024];
GetWindowTextA(GetForegroundWindow(), fore_window, 1023);
if
(
strstr
(fore_window,
"WinDbg"
)!=NULL ||
strstr
(fore_window,
"x64_dbg"
)!=NULL ||
strstr
(fore_window,
"OllyICE"
)!=NULL ||
strstr
(fore_window,
"OllyDBG"
)!=NULL ||
strstr
(fore_window,
"Immunity"
)!=NULL)
{
return
TRUE;
}
else
{
return
FALSE;
}
}
|
为了防范这种技术,在OllyDbg的PhantOm插件中勾选hide OllyDbg windows。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
BOOL
CheckDebug()
{
DWORD
ID;
DWORD
ret = 0;
PROCESSENTRY32 pe32;
pe32.dwSize =
sizeof
(pe32);
HANDLE
hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if
(hProcessSnap == INVALID_HANDLE_VALUE)
{
return
FALSE;
}
BOOL
bMore = Process32First(hProcessSnap, &pe32);
while
(bMore)
{
if
(stricmp(pe32.szExeFile,
"OllyDBG.EXE"
)==0 || stricmp(pe32.szExeFile,
"OllyICE.exe"
)==0 || stricmp(pe32.szExeFile,
"x64_dbg.exe"
)==0 || stricmp(pe32.szExeFile,
"windbg.exe"
)==0 || stricmp(pe32.szExeFile,
"ImmunityDebugger.exe"
)==0)
{
return
TRUE;
}
bMore = Process32Next(hProcessSnap, &pe32);
}
CloseHandle(hProcessSnap);
return
FALSE;
}
|
在逆向工程中,为了帮助恶意代码分析人员进行分析,可使用调试器设置一个断点,或是单步执行一个进程。然而,在调试器中执行这些操做时,它们会修改进程中的代码。所以,恶意代码常使用几种反调试技术探测软件/硬件断点、完整性校验、时钟检测等几种类型的调试器行为。直接运行恶意代码与在调试器中运行恶意代码也会在一些细节上不一样,如父进程信息、STARTUPINFO信息、SeDebugPrivilege权限等。
调试器设置断点的基本机制是用软件中断指令INT 3临时替换运行程序中的一条指令,而后当程序运行到这条指令时,调用调试异常处理例程。INT 3指令的机器码是0xCC,所以不管什么时候,使用调试器设置一个断点,它都会插入一个0xCC来修改代码。恶意代码经常使用的一种反调试技术是在它的代码中查找机器码0xCC,来扫描调试器对它代码的INT 3修改。repne scasb指令用于在一段数据缓冲区中搜索一个字节。EDI需指向缓冲区地址,AL则包含要找的字节,ECX设为缓冲区的长度。当ECX=0或找到该字节时,比较中止。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
BOOL
CheckDebug()
{
PIMAGE_DOS_HEADER pDosHeader;
PIMAGE_NT_HEADERS32 pNtHeaders;
PIMAGE_SECTION_HEADER pSectionHeader;
DWORD
dwBaseImage = (
DWORD
)GetModuleHandle(NULL);
pDosHeader = (PIMAGE_DOS_HEADER)dwBaseImage;
pNtHeaders = (PIMAGE_NT_HEADERS32)((
DWORD
)pDosHeader + pDosHeader->e_lfanew);
pSectionHeader = (PIMAGE_SECTION_HEADER)((
DWORD
)pNtHeaders +
sizeof
(pNtHeaders->Signature) +
sizeof
(IMAGE_FILE_HEADER) +
(
WORD
)pNtHeaders->FileHeader.SizeOfOptionalHeader);
DWORD
dwAddr = pSectionHeader->VirtualAddress + dwBaseImage;
DWORD
dwCodeSize = pSectionHeader->SizeOfRawData;
BOOL
Found = FALSE;
__asm
{
cld
mov edi,dwAddr
mov ecx,dwCodeSize
mov al,0CCH
repne scasb
jnz NotFound
mov Found,1
NotFound:
}
return
Found;
}
|
在OllyDbg的寄存器窗口按下右键,点击View debug registers能够看到DR0、DR一、DR二、DR三、DR6和DR7这几个寄存器。DR0、Dr一、Dr二、Dr3用于设置硬件断点,因为只有4个硬件断点寄存器,因此同时最多只能设置4个硬件断点。DR四、DR5由系统保留。 DR六、DR7用于记录Dr0-Dr3中断点的相关属性。若是没有硬件断点,那么DR0、DR一、DR二、DR3这4个寄存器的值都为0。
1
2
3
4
5
6
7
8
9
10
11
12
|
BOOL
CheckDebug()
{
CONTEXT context;
HANDLE
hThread = GetCurrentThread();
context.ContextFlags = CONTEXT_DEBUG_REGISTERS;
GetThreadContext(hThread, &context);
if
(context.Dr0 != 0 || context.Dr1 != 0 || context.Dr2 != 0 || context.Dr3!=0)
{
return
TRUE;
}
return
FALSE;
}
|
恶意代码能够计算代码段的校验并实现与扫描中断相同的目的。与扫描0xCC不一样,这种检查仅执行恶意代码中机器码CRC或者MD5校验和检查。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
BOOL
CheckDebug()
{
PIMAGE_DOS_HEADER pDosHeader;
PIMAGE_NT_HEADERS32 pNtHeaders;
PIMAGE_SECTION_HEADER pSectionHeader;
DWORD
dwBaseImage = (
DWORD
)GetModuleHandle(NULL);
pDosHeader = (PIMAGE_DOS_HEADER)dwBaseImage;
pNtHeaders = (PIMAGE_NT_HEADERS32)((
DWORD
)pDosHeader + pDosHeader->e_lfanew);
pSectionHeader = (PIMAGE_SECTION_HEADER)((
DWORD
)pNtHeaders +
sizeof
(pNtHeaders->Signature) +
sizeof
(IMAGE_FILE_HEADER) +
(
WORD
)pNtHeaders->FileHeader.SizeOfOptionalHeader);
DWORD
dwAddr = pSectionHeader->VirtualAddress + dwBaseImage;
DWORD
dwCodeSize = pSectionHeader->SizeOfRawData;
DWORD
checksum = 0;
__asm
{
cld
mov esi, dwAddr
mov ecx, dwCodeSize
xor eax, eax
checksum_loop :
movzx ebx, byte ptr[esi]
add eax, ebx
rol eax, 1
inc esi
loop checksum_loop
mov checksum, eax
}
if
(checksum != 0x46ea24)
{
return
FALSE;
}
else
{
return
TRUE;
}
}
|
被调试时,进程的运行速度大大下降,例如,单步调试大幅下降恶意代码的运行速度,因此时钟检测是恶意代码探测调试器存在的最经常使用方式之一。有以下两种用时钟检测来探测调试器存在的方法。
记录一段操做先后的时间戳,而后比较这两个时间戳,若是存在滞后,则能够认为存在调试器。
记录触发一个异常先后的时间戳。若是不调试进程,能够很快处理完异常,由于调试器处理异常的速度很是慢。默认状况下,调试器处理异常时须要人为干预,这致使大量延迟。虽然不少调试器容许咱们忽略异常,将异常直接返回程序,但这样操做仍然存在不小的延迟。
较经常使用的时钟检测方法是利用rdtsc指令(操做码0x0F31),它返回至系统从新启动以来的时钟数,而且将其做为一个64位的值存入EDX:EAX中。恶意代码运行两次rdtsc指令,而后比较两次读取之间的差值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
BOOL
CheckDebug()
{
DWORD
time1, time2;
__asm
{
rdtsc
mov time1, eax
rdtsc
mov time2, eax
}
if
(time2 - time1 < 0xff)
{
return
FALSE;
}
else
{
return
TRUE;
}
}
|
同rdtsc指令同样,这两个Windows API函数也被用来执行一个反调试的时钟检测。使用这种方法的前提是处理器有高分辨率能力的计数器-寄存器,它能存储处理器活跃的时钟数。为了获取比较的时间差,调用两次QueryPerformanceCounter函数查询这个计数器。若两次调用之间花费的时间过于长,则能够认为正在使用调试器。GetTickCount函数返回最近系统重启时间与当前时间的相差毫秒数(因为时钟计数器的大小缘由,计数器每49.7天就被重置一次)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
BOOL
CheckDebug()
{
DWORD
time1 = GetTickCount();
__asm
{
mov ecx,10
mov edx,6
mov ecx,10
}
DWORD
time2 = GetTickCount();
if
(time2-time1 > 0x1A)
{
return
TRUE;
}
else
{
return
FALSE;
}
}
|
通常双击运行的进程的父进程都是explorer.exe,可是若是进程被调试父进程则是调试器进程。也就是说若是父进程不是explorer.exe则能够认为程序正在被调试。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
BOOL
CheckDebug()
{
LONG
status;
DWORD
dwParentPID = 0;
HANDLE
hProcess;
PROCESS_BASIC_INFORMATION pbi;
int
pid = getpid();
hProcess = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, pid);
if
(!hProcess)
return
-1;
PNTQUERYINFORMATIONPROCESS NtQueryInformationProcess = (PNTQUERYINFORMATIONPROCESS)GetProcAddress(GetModuleHandleA(
"ntdll"
),
"NtQueryInformationProcess"
);
status = NtQueryInformationProcess(hProcess,SystemBasicInformation,(
PVOID
)&pbi,
sizeof
(PROCESS_BASIC_INFORMATION),NULL);
PROCESSENTRY32 pe32;
pe32.dwSize =
sizeof
(pe32);
HANDLE
hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if
(hProcessSnap == INVALID_HANDLE_VALUE)
{
return
FALSE;
}
BOOL
bMore = Process32First(hProcessSnap, &pe32);
while
(bMore)
{
if
(pbi.InheritedFromUniqueProcessId == pe32.th32ProcessID)
{
if
(stricmp(pe32.szExeFile,
"explorer.exe"
)==0)
{
CloseHandle(hProcessSnap);
return
FALSE;
}
else
{
CloseHandle(hProcessSnap);
return
TRUE;
}
}
bMore = Process32Next(hProcessSnap, &pe32);
}
CloseHandle(hProcessSnap);
}
|
explorer.exe建立进程的时候会把STARTUPINFO结构中的值设为0,而非explorer.exe建立进程的时候会忽略这个结构中的值,也就是结构中的值不为0。因此能够利用STARTUPINFO来判断程序是否在被调试。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
BOOL
CheckDebug()
{
STARTUPINFO si;
GetStartupInfo(&si);
if
(si.dwX!=0 || si.dwY!=0 || si.dwFillAttribute!=0 || si.dwXSize!=0 || si.dwYSize!=0 || si.dwXCountChars!=0 || si.dwYCountChars!=0)
{
return
TRUE;
}
else
{
return
FALSE;
}
}
|
默认状况下进程是没有SeDebugPrivilege权限的,可是当进程经过调试器启动时,因为调试器自己启动了SeDebugPrivilege权限,当调试进程被加载时SeDebugPrivilege也就被继承了。因此咱们能够检测进程的SeDebugPrivilege权限来间接判断是否存在调试器,而对SeDebugPrivilege权限的判断能够用可否打开csrss.exe进程来判断。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
BOOL
CheckDebug()
{
DWORD
ID;
DWORD
ret = 0;
PROCESSENTRY32 pe32;
pe32.dwSize =
sizeof
(pe32);
HANDLE
hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if
(hProcessSnap == INVALID_HANDLE_VALUE)
{
return
FALSE;
}
BOOL
bMore = Process32First(hProcessSnap, &pe32);
while
(bMore)
{
if
(
strcmp
(pe32.szExeFile,
"csrss.exe"
)==0)
{
ID = pe32.th32ProcessID;
break
;
}
bMore = Process32Next(hProcessSnap, &pe32);
}
CloseHandle(hProcessSnap);
if
(OpenProcess(PROCESS_QUERY_INFORMATION, NULL, ID) != NULL)
{
return
TRUE;
}
else
{
return
FALSE;
}
}
|
恶意代码能够用一些技术来干扰调试器的正常运行。例如线程本地存储(TLS)回调、插入中断、异常等。这些技术当且仅当程序处于调试器控制之下时才试图扰乱程序的运行。
Thread Local Storage(TLS),即线程本地存储,是Windows为解决一个进程中多个线程同时访问全局变量而提供的机制。TLS能够简单地由操做系统代为完成整个互斥过程,也能够由用户本身编写控制信号量的函数。当进程中的线程访问预先制定的内存空间时,操做系统会调用系统默认的或用户自定义的信号量函数,保证数据的完整性与正确性。下面是一个简单的TLS回调的例子,TLS_CALLBACK1函数在main函数执行前调用IsDebuggerPresent函数检查它是否正在被调试。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
|
#include "stdafx.h"
#include <stdio.h>
#include <windows.h>
void
NTAPI __stdcall TLS_CALLBACK1(
PVOID
DllHandle,
DWORD
dwReason,
PVOID
Reserved);
#ifdef _M_IX86
#pragma comment (linker, "/INCLUDE:__tls_used")
#pragma comment (linker, "/INCLUDE:__tls_callback")
#else
#pragma comment (linker, "/INCLUDE:_tls_used")
#pragma comment (linker, "/INCLUDE:_tls_callback")
#endif
EXTERN_C
#ifdef _M_X64
#pragma const_seg (".CRT$XLB")
const
#else
#pragma data_seg (".CRT$XLB")
#endif
PIMAGE_TLS_CALLBACK _tls_callback[] = { TLS_CALLBACK1,0};
#pragma data_seg ()
#pragma const_seg ()
#include <iostream>
void
NTAPI __stdcall TLS_CALLBACK1(
PVOID
DllHandle,
DWORD
Reason,
PVOID
Reserved)
{
if
(IsDebuggerPresent())
{
printf
(
"TLS_CALLBACK: Debugger Detected!\n"
);
}
else
{
printf
(
"TLS_CALLBACK: No Debugger Present!\n"
);
}
}
int
main(
int
argc,
char
* argv[])
{
printf
(
"233\n"
);
return
0;
}
|
要在程序中使用TLS,必须为TLS数据单独建一个数据段,用相关数据填充此段,并通知连接器为TLS数据在PE文件头中添加数据。_tls_callback[]数组中保存了全部的TLS回调函数指针。数组必须以NULL指针结束,且数组中的每个回调函数在程序初始化时都会被调用,程序员可按须要添加。但程序员不该当假设操做系统已何种顺序调用回调函数。如此则要求在TLS回调函数中进行反调试操做须要必定的独立性。
正常运行这个程序会打印下面的内容。
TLS_CALLBACK: No Debugger Present!
233
若是把在OllyDbg中运行,在OllyDbg暂停以前会打印下面的内容。
TLS_CALLBACK: Debugger Detected!
使用PEview查看.tls段,能够发现TLS回调函数。一般状况下,正常程序不使用.tls段,若是在可执行程序中看到.tls段,应该当即怀疑它使用了反调试技术。
在OllyDbg中选择Options->Debugging Options->Events,而后设置System break-point做为第一个暂停的位置,这样就可让OllyDbg在TLS回调执行前暂停。
在IDA Pro中按Ctrl+E快捷键看到二进制的入口点,该组合键的做用是显示应用程序全部的入口点,其中包括TLS回调。双击函数名能够浏览回调函数。
因为TLS回调已广为人知,所以同过去相比,恶意代码使用它的次数已经明显减小。为数很少的合法程序使用TLS回调,因此可执行程序中的.tls段特别突出。
由于调试器使用INT 3来设置软件断点,因此一种反调试技术就是在合法代码段中插入0xCC(INT 3)欺骗调试器,使其认为这些0xCC机器码是本身设置的断点。
1
2
3
4
5
6
7
8
9
10
11
12
|
BOOL
CheckDebug()
{
__try
{
__asm
int
3
}
__except(1)
{
return
FALSE;
}
return
TRUE;
}
|
除了使用_try和_except之外还能够直接使用汇编代码安装SEH。在下面的代码中若是进程没有处于调试中,则正常终止;若是进程处于调试中,则跳转到非法地址0xFFFFFFFF处,没法继续调试。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
|
#include "stdio.h"
#include "windows.h"
#include "tchar.h"
void
AD_BreakPoint()
{
printf
(
"SEH : BreakPoint\n"
);
__asm {
// install SEH
push handler
push
DWORD
ptr fs:[0]
mov
DWORD
ptr fs:[0], esp
// generating exception
int
3
// 1) debugging
// go to terminating code
mov eax, 0xFFFFFFFF
jmp eax
// process terminating!!!
// 2) not debugging
// go to normal code
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:
// remove SEH
pop dword ptr fs:[0]
add esp, 4
}
printf
(
" => Not debugging...\n\n"
);
}
int
_tmain(
int
argc,
TCHAR
* argv[])
{
AD_BreakPoint();
return
0;
}
|
双字节操做码0xCD03也能够产生INT 3中断,这是恶意代码干扰WinDbg调试器的有效方法。在调试器外,0xCD03指令产生一个STATUS_BREAKPOINT异常。然而在WinDbg调试器内,因为断点一般是单字节机器码0xCC,所以WinDbg会捕获这个断点而后将EIP加1字节。这可能致使程序在被正常运行的WinDbg调试时,执行不一样的指令集(OllyDbg能够避免双字节INT 3的攻击)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
BOOL
CheckDebug()
{
__try
{
__asm
{
__emit 0xCD
__emit 0x03
}
}
__except(1)
{
return
FALSE;
}
return
TRUE;
}
|
INT 2D原为内核模式中用来触发断点异常的指令,也能够在用户模式下触发异常。但程序调试运行时不会触发异常,只是忽略。INT 2D指令在ollydbg中有两个有趣的特性。在调试模式中执行INT 2D指令,下一条指令的第一个字节将被忽略。使用StepInto(F7)或者StepOver(F8)命令跟踪INT 2D指令,程序不会停在下一条指令开始的地方,而是一直运行,就像RUN(F9)同样。在下面的代码中,程序调试运行时,执行INT 2D以后不会运行SEH,而是跳过NOP,把bDebugging标志设置为1,跳转到normal_code;程序正常运行时,执行INT 2D以后触发SEH,在异常处理器中设置EIP并把bDebugging标志设置为0。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
BOOL
CheckDebug()
{
BOOL
bDebugging = FALSE;
__asm {
// install SEH
push handler
push
DWORD
ptr fs:[0]
mov
DWORD
ptr fs:[0], esp
int
0x2d
nop
mov bDebugging, 1
jmp normal_code
handler:
mov eax, dword ptr ss:[esp+0xc]
mov dword ptr ds:[eax+0xb8], offset normal_code
mov bDebugging, 0
xor eax, eax
retn
normal_code:
// remove SEH
pop dword ptr fs:[0]
add esp, 4
}
printf
(
"Trap Flag (INT 2D)\n"
);
if
( bDebugging )
return
1;
else
return
0;
}
|
片内仿真器(ICE)断点指令ICEBP(操做码0xF1)是Intel未公开的指令之一。因为使用ICE难以在任意位置设置断点,所以ICEBP指令被设计用来下降使用ICE设置断点的难度。运行ICEBP指令将会产生一个单步异常,若是经过单步调试跟踪程序,调试器会认为这是单步调试产生的异常,从而不执行先前设置的异常处理例程。利用这一点,恶意代码使用异常处理例程做为它的正常执行流程。为了防止这种反调试技术,执行ICEBP指令时不要使用单步。
1
2
3
4
5
6
7
8
9
10
11
12
|
BOOL
CheckDebug()
{
__try
{
__asm __emit 0xF1
}
__except(1)
{
return
FALSE;
}
return
TRUE;
}
|
EFLAGS寄存器的第八个比特位是陷阱标志位。若是设置了,就会产生一个单步异常。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
BOOL
CheckDebug()
{
__try
{
__asm
{
pushfd
or word ptr[esp], 0x100
popfd
nop
}
}
__except(1)
{
return
FALSE;
}
return
TRUE;
}
|
前面已经讨论了各类使用异常机制的反调试手段。
RaiseException函数产生的若干不一样类型的异常能够被调试器捕获。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
BOOL
TestExceptionCode(
DWORD
dwCode)
{
__try
{
RaiseException(dwCode, 0, 0, 0);
}
__except(1)
{
return
FALSE;
}
return
TRUE;
}
BOOL
CheckDebug()
{
return
TestExceptionCode(DBG_RIPEXCEPTION);
}
|
进程中发生异常时若SEH未处理或注册的SEH不存在,会调用UnhandledExceptionFilter,它会运行系统最后的异常处理器。UnhandledExceptionFilter内部调用了前面提到过的NtQueryInformationProcess以判断是否正在调试进程。若进程正常运行,则运行最后的异常处理器;若进程处于调试,则将异常派送给调试器。SetUnhandledExceptionFilter函数能够修改系统最后的异常处理器。下面的代码先触发异常,而后在新注册的最后的异常处理器内部判断进程正常运行仍是调试运行。进程正常运行时pExcept->ContextRecord->Eip+=4;将发生异常的代码地址加4使得其可以继续运行;进程调试运行时产生无效的内存访问异常,从而没法继续调试。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
#include "stdio.h"
#include "windows.h"
#include "tchar.h"
LPVOID
g_pOrgFilter = 0;
LONG
WINAPI ExceptionFilter(PEXCEPTION_POINTERS pExcept)
{
SetUnhandledExceptionFilter((LPTOP_LEVEL_EXCEPTION_FILTER)g_pOrgFilter);
// 8900 MOV DWORD PTR DS:[EAX], EAX
// FFE0 JMP EAX
pExcept->ContextRecord->Eip += 4;
return
EXCEPTION_CONTINUE_EXECUTION;
}
void
AD_SetUnhandledExceptionFilter()
{
printf
(
"SEH : SetUnhandledExceptionFilter()\n"
);
g_pOrgFilter = (
LPVOID
)SetUnhandledExceptionFilter(
(LPTOP_LEVEL_EXCEPTION_FILTER)ExceptionFilter);
__asm {
xor eax, eax;
mov dword ptr [eax], eax
jmp eax
}
printf
(
" => Not debugging...\n\n"
);
}
int
_tmain(
int
argc,
TCHAR
* argv[])
{
AD_SetUnhandledExceptionFilter();
return
0;
}
|
在OllyDbg中,选择Options->Debugging Options->Exceptions来设置把异常传递给应用程序。
与全部软件同样,调试器也存在漏洞,有时恶意代码编写者为了防止被调试,会攻击这些漏洞。这里咱们展现几种OllyDbg调试器处理PE格式文件时的常见漏洞。
OllyDbg很是严格地遵循了微软对PE文件头部的规定。在PE文件的头部,一般存在一个叫做IMAGE_OPTIONAL_HEADER的结构。
须要特别注意这个结构中的最后几个元素。NumberOfRvaAndSizes属性标识后面DataDirectory数组中的元素个数。DataDirectory数组表示在这个可执行文件中的什么地方可找到其余导入可执行模块的位置,它位于可选头部结构的末尾,是一个比IMAGE_DATA_DIRECTORY略大一些的数组。数组中每一个结构目录都指明了目录的相对虚拟地址和大小。DataDirectory数组的大小被设置为IMAGE_NUMBEROF_DIRECTORY_ENTRIES,它等于0x10。由于DataDirectory数组不足以容纳超过0x10个目录项,因此当NumberOfRvaAndSizes大于0x10时,Windows加载器将会忽略NumberOfRvaAndSizes。OllyDbg遵循了这个标准,而且不管NumberOfRvaAndSizes是什么值,OllyDbg都使用它。所以,设置NumberOfRvaAndSizes为一个超过0x10的值,会致使在程序退出前,OllyDbg对用户弹出一个窗口。如图所示,使用LordPE打开可执行文件,修改RVA数及大小并保存,再用OllyDbg打开,会提示错误Bad or unknown format of 32-bit executable file。
另外一种PE头的欺骗与节头部有关。文件内容中包含的节包括代码节、数据节、资源节,以及一些其余信息节。每一个节都拥有一个IMAGE_SECTION_HEADER结构的头部。
VirtualSize和SizeOfRawData是其中两个比较重要的属性。根据微软对PE的规定,VirtualSize应该包含载入到内存的节大小,SizeOfRawData应该包含节在硬盘中的大小。Windows加载器使用VirtualSize和SizeOfRawData中的最小值将节数据映射到内存。若是SizeOfRawData大于VirtualSize,则仅将VirtualSize大小的数据复制入内存,忽略其他数据。由于OllyDbg仅使用SizeOfRawData,因此设置SizeOfRawData为一个相似0x77777777的大数值时,会致使OllyDbg崩溃。如图所示,使用LordPE打开可执行文件,点击区段,在区段表上右击,点击编辑区段,修改物理大小并保存,再用OllyDbg打开,会提示一样的错误。
对抗这种反调试技术的最简单方法是用相似的编辑器手动修改PE头部。OllyDbg2.0和WinDbg不存在这种漏洞。
恶意代码常尝试利用OllyDbg1.1的格式化字符串漏洞,为OutputDebugString函数提供一个%s字符串的参数,让OllyDbg崩溃。所以,须要注意程序中可疑的OutputDebugString调用,例如OutputDebugString("%s%s%s%s%s%s%s%s%s")。若是执行了这个调用,OllyDbg将会崩溃。
最后让咱们总结一下提到的内容。腾讯2016游戏安全技术竞赛有一道题,大概意思就是给一个exe,要求编写一个Tencent2016D.dll,并导出多个接口函数CheckDebugX。X为1-100之间的数字,好比CheckDebug1,CheckDebug8,...,CheckDebug98。函数功能是检测本身是否处于被调试状态,是返回TRUE,不然返回FALSE。函数的原型都是typedef BOOL (WINAPI* Type_CheckDebug)();。编译好dll以后,放在Tencent2016D.exe的同目录,运行Tencent2016D.exe,点击检测按钮,正常运行时,函数接口输出为0,调试运行或者被附加运行时,接口输出1。咱们把提到的知识综合一下完成这道题目。
解题的参考代码和题目相关信息:https://github.com/houjingyi233/test-debug/
参考资料:
1.《恶意代码分析实战》第16章反调试技术(本文的主体框架)
2.《逆向工程核心原理》第51章静态反调试技术&第52章动态反调试技术
4.天枢战队官方博客(本文大部分代码的来源)