前俩天的学习记录Windows上面的反调试学习,主要是参考《恶意代码实战分析》和《加密与解密》里面的,给每个小技术都写了程序示例,自己编译反调试了一遍。对于加解密一书是还有很多不理解的地方的,目前只能记录到这了,后面继续慢慢学吧,欢迎一起交流讨论,谢谢师傅。
IsDebuggerPresent
函数通过获取进程环境块(PEB)中的BeingDebugged
标志来检测进程是否处于调试状态。
其实现代码:
1 2 3 4 |
|
BeingDebugged是PEB中的一个标志。每个运行中的进程都有一个PEB结构,其0x02偏移处就是BeingDebugged标志,如果程序处于调试状态,该标志的值会被设置为非零值。相关Windows API就是通过访问该值来进行反调试操作。
如何访问PEB?PEB的地址储存在另一个名为线程环境块(TEB)中。
Windows在调入进程、创建线程时,操作系统会为每个线程分配TEB,而且FS段寄存器总是被设置成使得FS:[0]
指向当前线程的TEB数据。而TEB结构中的0x30偏移处正是PEB的地址。
Windows一般通过TEB间接获取PEB的地址:
mov eax,fs:[18h] //获取当前线程的TEB地址 mov eax,[eax+30h] //在TEB偏移30h处获得PEB地址
TIB+18h处为Self。它是TIB的自身指针,指向TEB的首地址。因此也可以省略它直接使用fs:[30h]
得到自己进程的PEB。
如何过掉IsDebuggerPresent?
示例程序:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
第一种方法是IDA中在修改恶意代码(例如jz跳转将其改为jnz,或者修改cmp)
第二种使用xdbg,在数据窗口使用“Ctrl+G”搜索fs:[30]+2;
这里的01就是BeingDebugged标志,将其用“Ctrl+E”修改成00,运行程序,程序输出:
CheckRemoteDebuggerPresent
不仅可以探测进程自身是否被调试,也可以探测系统其他进程是否被调试。函数接收两个参数进程句柄和一个指向布尔值的指针。如果指定的进程正在被调试,则函数会把指向布尔值的指针设为 TRUE,否则设为FALSE。
函数原型:
1 2 3 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 |
|
在《恶意代码分析实战》一书中提到该函数也检测了BeingDebugged标志,而在《加密与解密》中说该函数并没有使用BeingDebugged标志,按照过IsDebuggerPresent的方式再试一次,发现CheckRemoteDebuggerPresent的确没有用到BeingDebugged标志。
将BeingDebugged标志位设为00后,程序依旧处于反调试状态。
那么CheckRemoteDebuggerPresent
是通过什么来进行反调试的?
在xdbg中Ctrl+G搜索CheckRemoteDebuggerPresent
,可以找到该函数的汇编代码
75D131B0 | 8BFF | mov edi,edi | 75D131B2 | 55 | push ebp | 75D131B3 | 8BEC | mov ebp,esp | 75D131B5 | 51 | push ecx | ecx:EntryPoint 75D131B6 | 837D 08 00 | cmp dword ptr ss:[ebp+8],0 | 75D131BA | 56 | push esi | esi:EntryPoint 75D131BB | 74 36 | je kernelbase.75D131F3 | 75D131BD | 8B75 0C | mov esi,dword ptr ss:[ebp+C] | esi:EntryPoint 75D131C0 | 85F6 | test esi,esi | esi:EntryPoint 75D131C2 | 74 2F | je kernelbase.75D131F3 | 75D131C4 | 6A 00 | push 0 | 75D131C6 | 6A 04 | push 4 | 75D131C8 | 8D45 FC | lea eax,dword ptr ss:[ebp-4] | [ss:[ebp-04]]:BaseThreadInitThunk 75D131CB | 50 | push eax | 75D131CC | 6A 07 | push 7 | 75D131CE | FF75 08 | push dword ptr ss:[ebp+8] | 75D131D1 | FF15 F0A2D375 | call dword ptr ds:[其中唯一关键处就是在17行调用了
NtQueryInformationProcess
函数。该函数原型:
1
2
3
4
5
6
7
__kernel_entry NTSTATUS NtQueryInformationProcess(
[in]
HANDLE
ProcessHandle,
[in] PROCESSINFOCLASS ProcessInformationClass,
[out]
PVOID
ProcessInformation,
[in]
ULONG
ProcessInformationLength,
[out, optional]
PULONG
ReturnLength
);
该函数会根据不同的
ProcessInformationClass
查询有关一个进程对象的信息,在文档中列举了一些情况:
Value Meaning ProcessInformationClass 0 搜索指向 PEB 结构的指示器,该结构可用于确定指定进程是否正在调试,以及系统用于标识指定进程的唯一值。使用CheckRemoteDebuggerPresent和GetProcessId 函数获取此信息。 ProcessDebugPort 7 检索DWORD_PTR值,该值是进程的调试器的端口号。非零值表示进程正在环3调试器的控制下运行。使用CheckRemoteDebuggerPresent或IsDebuggerPresent函数。 ProcessWow64Information 26 确定进程是否在 WOW64 环境中运行(WOW64 是允许基于 Win32 的应用程序在 64 位 Windows 上运行的 x86 模拟器)。使用IsWow64Process2函数获取此信息。 ProcessImageFileName 27 搜索包含进程图像文件名称的 **UNICODE_STRING值。**使用QueryFullProcessImageName或GetProcessImageFileName函数获取此信息。 ProcessBreakOnTermination 29 搜索ULONG值,该值指示进程是否被视为关键进程。注意 从 Windows XP SP3 开始可以使用该值。从 Windows 8.1 开始,应改用IsProcessCritical 。 **ProcessTelemetryIdInformation **64 检索包含有关进程的元数据的**PROCESS_TELEMETRY_ID_INFORMATION_TYPE值。** ProcessSubsystemInformation 75 检索指示进程子系统类型的**SUBSYSTEM_INFORMATION_TYPE值。**ProcessInformation参数指向的蜡烛图应该足够大以容纳单个SUBSYSTEM_INFORMATION_TYPE枚举。 回到上述汇编,正是查询了7号信息
ProcessDebugPort
:所以说,
CheckRemoteDebuggerPresent
实际上是调用了NtQueryInformationProcess
函数,查询了某个进程的ProcessDebugPort
,这个值是系统用来与调试器通信的端口句柄。如何过掉CheckRemoteDebuggerPresent?
在IDA中和
IsDebuggerPresent
都可以采取修改恶意代码的形式,使用xdbg的话,将传给NtQueryInformationProcess
的参数7修改掉就可以了。在本例中修改了参数7可以过掉(可能只是个例,因为修改成0,也就是获取进程的基本信息,应该不一定会成功)。还是应该要修改后面的eax寄存器中的值。将01修改成00。
OutputDebugString
OutPutDebugString
函数的作用是在调试器中显示一个字符串。函数原型:
1
2
3
void
OutputDebugStringW(
[in, optional]
LPCWSTR
lpOutputString
);
如何用它来检测调试状态?
可以配合
SetLastError
和GetLastError
函数,这俩个函数前者将当前的错误码设置成一个任意值,后者是获取当前的错误码。如果进程没有被调试器附加,那么调用OutPutDebugString
函数就会失败,错误码会被重新设置,因此再使用GetLastError
函数获取的错误码应该就不是我们设置的值。若进程被调试器附加并调用了OutPutDebugString
函数,那么该函数会调用成功,GetLastError
函数获取的也就是我们设置的值。程序示例:
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
#include
#include
void
CheckDebugger()
{
DWORD
errorValue = 12345;
SetLastError(errorValue);
OutputDebugString(
"Test for Debugger"
);
// 检查GetLastError返回的值是否改变
if
(GetLastError() == errorValue) {
printf
(
"[-] 检测到调试器\n"
);
ExitProcess(1);
// 结束进程
}
else
{
printf
(
"[+] 未检测到调试器\n"
);
}
}
int
main()
{
CheckDebugger();
// 这里可以添加其他代码
return
0;
}
上述程序是由《恶意代码实战分析》的代码清单扩展来的,但实际发现,没有附加调试器的情况下,也会报**“[-] 检测到调试器”**。原因应该是因为:程序没有调用
OutPutDebugString
函数也会改变错误码。ZwSetInformationThread(ThreadHideFromDebugger)
一种调试器攻击。
ZwSetInformationThread
函数,用于设置线程的优先级。示例如下:
1
2
3
4
5
6
NTSYSAPI NTSTATUS ZwSetInformationThread(
[in]
HANDLE
ThreadHandle,
[in] THREADINFOCLASS ThreadInformationClass,
[in]
PVOID
ThreadInformation,
[in]
ULONG
ThreadInformationLength
);
这个函数可以设置一个与线程相关的信息。查看
ThreadInformationClass
列表:
可以看到
ThreadHideFromDebugger
,关于它的定义:
这个信息类只能被设置。它禁用了线程的调试事件生成。这个信息类不需要数据,因此ThreadInformation
可以是一个空指针。ThreadInformationLength
应该是零。通过为线程设置
ThreadHideFromDebugger
,可以禁止某个线程产生调试事件。测试示例:
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
47
48
49
50
51
52
#include
// #include
// 包含 NTSTATUS
#include
// 确保 NTSTATUS 已定义。如果没有,手动定义它。
#ifndef NTSTATUS
typedef
LONG
NTSTATUS;
#endif
// 定义 ZwSetInformationThread 函数类型
typedef
NTSTATUS (WINAPI *ZW_SET_INFORMATION_THREAD)(
HANDLE
,
DWORD
,
PVOID
,
ULONG
);
#define ThreadHideFromDebugger 0x11 // 17 in decimal
// 函数来禁用调试事件
VOID
DisableDebugEvent(
VOID
)
{
HMODULE
hModule;
ZW_SET_INFORMATION_THREAD ZwSetInformationThread;
// 获取 ntdll.dll 模块的句柄
hModule = GetModuleHandleA(
"Ntdll.dll"
);
if
(hModule == NULL) {
printf
(
"无法获取 ntdll.dll 的句柄.\n"
);
return
;
}
// 获取 ZwSetInformationThread 函数的地址
ZwSetInformationThread = (ZW_SET_INFORMATION_THREAD)GetProcAddress(hModule,
"ZwSetInformationThread"
);
if
(ZwSetInformationThread == NULL) {
printf
(
"无法获取 ZwSetInformationThread 函数的地址.\n"
);
return
;
}
// 调用函数尝试隐藏当前线程
NTSTATUS status = ZwSetInformationThread(GetCurrentThread(), ThreadHideFromDebugger, NULL, 0);
if
(status != 0) {
printf
(
"调用 ZwSetInformationThread 失败,状态码: 0x%X\n"
, status);
}
else
{
printf
(
"当前线程已尝试隐藏自调试器.\n"
);
}
}
int
main()
{
printf
(
"测试程序开始...\n"
);
// 尝试禁用调试事件
DisableDebugEvent();
printf
(
"....."
);
return
0;
}
我用单步测试的,在打印“当前线程已尝试隐藏自调试器.”之后延迟一会程序就会退出调试状态。
**关于怎么干掉这个反调试。(待学)**[原创]调试陷阱ThreadHideFromDebugger的另一种对抗方法-软件逆向-看雪-安全社区|安全招聘|kanxue.com
手动检测数据结构
在一些情况下,程序中可能没有使用Windows API进行反调试,所以需要我们手动检查数据结构,关注一些会暴露调试器的数据结构(PEB)。
检测BeingDebugged属性
BeingDebugged
如上文所说,位于PEB中的0x2偏移处;过掉这种调试的方法就是在执行跳转是,手动修改零标志(使其强制跳转或不跳转);或者修改跳转指令;手动设置BeingDebugged属性值为0;
检测ProcessHeap属性
在PEB结构的Reserved4数组中有一个未公开的位置叫做
ProcessHeap
,它被设置为加载器为进程分配的第一个堆的位置。ProcessHeap位于PEB结构的0x18偏移处。第一个堆头部有一个属性字段,它告诉内核这个堆是否在调试器中创建。这些属性叫做ForceFlags
和Flags
。在正常情况下,系统在为进程创建第一个堆时,会将它的Flags和ForceFlags分别设为2和0,而在调试状态下,这俩个标志通常会被设为50000062h(取决于NtGlobalFlag)和40000060h。
同时,
Flags
位于ProcessHeap的0x0c偏移处,ForceFlags
位于ProcessHeap的0x10偏移处。因此可以写出这样一段检测代码:
mov eax,fs:[0x30] ;获取PEB mov eax,[eax+0x18] ;获取ProcessHeap cmp dword ptr [eax+0x0C],2 ;获取Flags jne __debugger_detected cmp dword ptr [eax+0x10],0 ;获取ForceFlags jne __debugger_detected对付这种反调试的方法之一就是手动修改ProcessHeap标志。
检测NtGlobalFlag
NtGlobalFlag
位于PEB结构的0x68偏移处。因为在调试器中启动进程和正常模式下启动进程时它们创建内存堆的方式不同。如果进程是由调试器创建的,那么该标志的值会被设置成0x70。过掉反调试的方法都差不多。
识别调试器行为
在逆向工程中,进行代码分析时,可以用调试器设置断点,或者单步执行一个进程。当调试器在执行这些操作时,它们会修改进程中的代码。因此,恶意代码常使用探测INT扫描、完整性校验,以及时钟检测等几种类型的调试器行为。
断点检测
软件断点
原理:调试器在设置断点时的一般采用的是软件断点(INT 3),打下INT 3后调试器会临时替换运行程序中的一条指令,当程序运行到这里时,调用调试异常处理例程。
INT 3 的机器码是0xCC,所以若是在关键位置检测到该指令,就可以判断进程处于调试状态。。
常用的反调试方法,扫描0xCC。
call $+5 pop edi sub edi, 5 mov ecx, 400h mov eax, 0CCh repne scasb jz DebuggerDetected这段先执行了一个函数调用,随后用pop指令将eip寄存器的值存入edi,然后将edi设置为代码的开始。接下来扫描这段代码的0xCC字节,如果发现0xCC则证明存在调试器。
对抗这种反调试技术的方法就是使用硬件断点。
示例程序:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include
#include
int
main() {
// 获取MessageBoxA函数地址
FARPROC addr = GetProcAddress(LoadLibraryW(L
"user32.dll"
),
"MessageBoxA"
);
// 读取函数入口处的第一个字节
BYTE
byteAtAddr = *(
BYTE
*)addr;
// 显示消息框作为正常功能的一部分
MessageBoxA(NULL,
"context"
,
"title"
, MB_OK);
// 检查是否为0xCC(INT 3指令)
if
(byteAtAddr == 0xCC) {
std::cout <<
"检测到调试"
<< std::endl;
}
else
{
std::cout <<
"无调试"
<< std::endl;
}
return
0;
}
用xdbg调试,Ctrl+G搜索
MessageBoxA
。在函数处下断点,之后一直单步就可以发现程序输出了检测到调试。在调用完函数后,调试器扫描了esp-10处开始的字节,查找0xCC。
对抗:
在xdbg右键断点设置硬件断点,
再次调试发现,变成了无调试。成功过掉。(
硬件断点
先了解一下什么是硬件断点,硬件断点和DRx寄存器有关,下图是Intel CPU体系架构里对DRx寄存器的介绍。
- DR0~DR3就好,这四个位置一般用于设置硬件断点。
- DR4和DR5保留,并未公开。
- DR6是调试寄存器组状态寄存器。
- DR7是调试寄存器组控制寄存器。
硬件断点的原理是使用DR0~DR3设定地址,并使用DR7设定状态,因此最多设置4个断点。
怎么实现硬件断点反调试?
先获取硬件断点信息,利用函数
GetThreadContext
,它检索指定线程的上下文。函数原型:
1
2
3
4
BOOL
GetThreadContext(
[in]
HANDLE
hThread,
[in, out] LPCONTEXT lpContext
);
第一个参数是要检索其上下文线程的句柄,第二个参数指向CONTEXT结构。该结构是一个在Windows API中定义的结构体,它用于存储线程的上下文信息,包括寄存器和其他重要的状态信息。
因此我们需要获取CONTEXT结构体中的DRx寄存器的信息,使用
CONTEXT_DEBUG_REFGISTERS
标志。示例程序:
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
#include
#include
int
main()
{
CONTEXT TestContext;
ZeroMemory(&TestContext,
sizeof
(CONTEXT));
// 将结构体清零
TestContext.ContextFlags = CONTEXT_DEBUG_REGISTERS;
if
(GetThreadContext(GetCurrentThread(), &TestContext)) {
if
(TestContext.Dr0 != 0 || TestContext.Dr1 != 0 ||
TestContext.Dr2 != 0 || TestContext.Dr3 != 0) {
MessageBoxA(NULL,
"硬件断点检测成功, 程序正在被调试!"
,
"硬件断点检测"
, MB_OK);
}
else
{
MessageBoxA(NULL,
"没有检测到硬件断点。"
,
"硬件断点检测"
, MB_OK);
}
}
else
{
DWORD
dwError = GetLastError();
std::cerr <<
"GetThreadContext failed with error: "
<< dwError << std::endl;
MessageBoxA(NULL,
"无法获取线程上下文信息。"
,
"错误"
, MB_OK);
}
system
(
"pause"
);
// 使用cin.get()或std::getchar()可能是一个更好的选择
return
0;
}
进行调试
随便下个硬件断点,检测成功。
采用异常来进行硬件断点反调试
执行代码校验和检查
恶意代码可以计算代码段的校验并实现与扫描中断相同的目的。与扫描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
37
38
#include
#include
BOOL
CheckIntegrity() {
PIMAGE_DOS_HEADER pDosHeader;
PIMAGE_NT_HEADERS pNtHeaders;
PIMAGE_SECTION_HEADER pSectionHeader;
DWORD
dwBaseImage = (
DWORD
)GetModuleHandle(NULL);
pDosHeader = (PIMAGE_DOS_HEADER)dwBaseImage;
pNtHeaders = (PIMAGE_NT_HEADERS)((
DWORD
)pDosHeader + pDosHeader->e_lfanew);
pSectionHeader = IMAGE_FIRST_SECTION(pNtHeaders);
DWORD
dwAddr = pSectionHeader->VirtualAddress + dwBaseImage;
DWORD
dwCodeSize = pSectionHeader->SizeOfRawData;
DWORD
checksum = 0;
BYTE
* code =
reinterpret_cast
<
BYTE
*>(dwAddr);
for
(
DWORD
i = 0; i < dwCodeSize; ++i) {
checksum += code[i];
checksum = (checksum >> 31) | (checksum << 1);
// ROL checksum, 1
}
if
(checksum != 0x46ea24) {
return
FALSE;
}
return
TRUE;
}
int
main() {
if
(CheckIntegrity()) {
std::cout <<
"Integrity check passed."
<< std::endl;
}
else
{
std::cout <<
"Integrity check failed!"
<< std::endl;
}
return
0;
}
在这个程序中,
checksum
变量计算为代码段中所有字节的累加和,并对每个字节累加后进行一次ROL。然后将这个checksum
和一个预设的校验和值比较。关于绕过,可以修改检查函数或者校验和值。
时钟检测
程序调试时,进程的运行速度大大降低(单步调试)。
有如下俩种用时钟检测来探测调试器存在的方法:
- 记录执行一段操作前后的时间戳,然后比较这俩个时间戳,如果存在滞后,则可以认为存在调试器。
- 记录一个异常前后的时间戳。如果不调试进程,可以很快处理完异常,因为调试器处理异常的速度非常慢。因此默认情况下,调试器处理异常需要人为干预,这导致大量延迟。
rdstc指令
rdtsc指令(操作码0x0F31)用于获取CPU自开机运行起的时钟周期数,并且将其作为一个64位的值存入edx和eax寄存器中。执行俩次rdstc指令,然后比较这俩次取值之间的差值
汇编:
rdtsc mov ecx, eax mov ebx, edx ;计算俩个rdtsc的偏移量 rdtsc cmp edx, ebx ja __debugger_found sub eax, ecx cmp eax, 0x200 ja __debugger_found俩次调用rdtsc,先检查了高位edx相不相同,相同的话再检查低位的差值是否大于0x20。
QueryPerformanceCounter或GetTickCount
QueryPerformanceCounter
函数检索性能计数器的当前值,这是一个高分辨率 (<1us) 时间戳,可用于时间间隔度量。
GetTickCount
检索自系统启动以来经过的毫秒数。这俩个函数都可以用于时钟检测。
下面是一个GetTickCount的示例:
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
#include
#include
BOOL
IsDebuggerPresentWithTiming(){
DWORD
startTick,endTick,elapsed;
// 获取初始的 tick 计数
startTick = GetTickCount();
// 此处可以添加代码
OutputDebugString(
"Debugger check...\n"
);
// 获取结束的 tick 计数
endTick = GetTickCount();
// 计算经过的 tick 数
elapsed = endTick - startTick;
// 如果时间差超出预设值,则可能存在调试器。
if
(elapsed > 200){
return
TRUE;
}
return
FALSE;
}
int
main(){
if
(IsDebuggerPresentWithTiming()){
std::cout <<
"可能检测到调试器!"
<
}
else
{
std::cout <<
"没有检测到调试器!"
<
}
return
0;
}
父进程检测
从理论上讲,一个程序被正常启动时,其父进程应该是Exploer.exe(资源管理器启动)、**cmd.exe(命令行启动)或者Services.exe(系统服务)**中的一个。如果某一个进程的父进程并非上述3个进程之一,一遍可以认为它被调试了(或者被内存补丁之类的Loader程序加载了)。
实现这种检测的方法:
- 通过TEB(TEB.ClientId)或者GetCurrentProcessId来检索当前进程的PID。
- 通过Process32First、Process32Next得到所有进程的列表,判断explorer.exe的PID(通过PROCESSENTERY.szExeFile)和通过PROCESSENTRy.th32ParentProcessID获得的当前进程的父进程ID是否相同。
- 如果父进程的PID不是上述三种的其中之一,那么目标进程很可能被调试了。
干扰调试器的功能
TLS回调函数
Thread Local Storage(TLS),即线程本地存储,是Windows为解决一个进程中多个线程同时访问全局变量而提供的机制。
TLS回调函数就是在程序加载到调试器后,TLS回调会先于程序入口执行之前运行代码,这样就可以提前进行反调试操作或者修改代码。
使用异常×
(待学)。。。
插入中断
插入INT 3
调试器使用 INT 3设置软件断点,所以一种反调试技术就是在合法代码中插入0xCC来欺骗调试器,使其认为这些0xCC机器码是自己设置的段带你。
一些调试器用跟踪自身设置的断点的方法来避免这种反调试技术。
插入INT 2D断点×
插入ICE断点×
嗯。待学(INT 2D。ICE是什么。)