反调试技术

文章目录

  • 前言
  • 系统API实现方式
    • IsDebuggerPresent (+0x2)
    • NtGlobalFlag(+0x68)
    • Heap flags(0x18)
    • CheckRemoteDebuggerPresent
    • NtQueryInformationProcess
    • ZwSetInformationThread
  • 示例
    • 示例1
      • 比较明文字符串和输入字符串
      • NtGlobalFlag
      • 时间差检测
      • ProcessMonitor
      • 检测进程名
      • 检测 VMware
      • SEH
      • 获取flag
    • 示例2

前言

思来想去还是先写反反调试,壳的话打算在PE文件内容里写
反调试顾名思义就是用尽一切手段防止运行时对程序的非法篡改和窥视,加大代码的复杂度和分析等

以下只是遇到的一些反调试,其中有一些知识都没有学习到,不过慢慢来也慢慢补坑

系统API实现方式

IsDebuggerPresent (+0x2)

微软给出的解释是此函数允许应用程序确定自己是否正在被调试,并依此改变行为。例如通过OutputDebugString函数提供更多调试信息

IsDebuggerPresent 这个 API 的实现方式是从 PEB 读取 BeingDebugged 字段来判断进程是否被调试状态

实现代码:

mov eax,dword ptr fs:[0x30]
movzx eax,byte ptr ds:[rax+0x2]
ret

想要 bypass 这种检查就非常容易,修改 PEB 结构中的 BeingDebugged 字段值为 0 就OK了。

NtGlobalFlag(+0x68)

在 32 位机器上, NtGlobalFlag字段位于PEB(进程环境块)0x68的偏移处, 64 位机器则是在偏移0xBC位置. 该字段的默认值为 0. 当调试器正在运行时, 该字段会被设置为一个特定的值. 尽管该值并不能十分可信地表明某个调试器真的有在运行, 但该字段常出于该目的而被使用.

该字段包含有一系列的标志位. 由调试器创建的进程会设置以下标志位:

调试程序时,PEB.NtGlobalFlag的值会被设置为0x70,所以,检测该成员的值即可判断进程是否处于被调试状态。

bypass 这个检查也很容易,因为标志位都在被调试进程的地址空间里,直接改掉就行了。

Heap flags(0x18)

这里引用ctfwiki给的解释:

Heap flags包含有两个与NtGlobalFlag一起初始化的标志: Flags和ForceFlags. 这两个字段的值不仅会受调试器的影响, 还会由 windows 版本而不同, 字段的位置也取决于 windows 的版本.

Flags 字段:
在 32 位 Windows NT, Windows 2000 和 Windows XP 中, Flags位于堆的0x0C偏移处. 在 32 位 Windows Vista 及更新的系统中, 它位于0x40偏移处.
在 64 位 Windows XP 中, Flags字段位于堆的0x14偏移处, 而在 64 位 Windows Vista 及更新的系统中, 它则是位于0x70偏移处.

ForceFlags 字段:
在 32 位 Windows NT, Windows 2000 和 Windows XP 中, ForceFlags位于堆的0x10偏移处. 在 32 位 Windows Vista 及更新的系统中, 它位于0x44偏移处.
在 64 位 Windows XP 中, ForceFlags字段位于堆的0x18偏移处, 而在 64 位 Windows Vista 及更新的系统中, 它则是位于0x74偏移处.。

CheckRemoteDebuggerPresent

kernel32的CheckRemoteDebuggerPresent()函数用于检测指定进程是否正在被调试. Remote在单词里是指同一个机器中的不同进程.
两个参数:一个是进程的 HANDLE,一个是 PBOOL。

BOOL WINAPI CheckRemoteDebuggerPresent(_In_    HANDLE hProcess,_Inout_ PBOOL  pbDebuggerPresent
);

如果调试器存在 (通常是检测自己是否正在被调试), 该函数会将pbDebuggerPresent指向的值设为0xffffffff.

NtQueryInformationProcess

NtQueryInformationProcess 是一个查询信息的接口,输入参数包括查询的信息类型、进程HANDLE、结果指针等。
kernel32的CheckRemoteDebuggerPresent()函数内部通过调用NtQueryInformationProcess()来检测调试, 而NtQueryInformationProcess内部则是查询EPROCESS结构体的DebugPort字段, 当进程正在被调试时, 返回值为0xffffffff.

内核接口,还不会hook先放置一下。

ZwSetInformationThread

ZwSetInformationThread 等同于 NtSetInformationThread,通过为线程设置 ThreadHideFromDebugger,可以禁止线程产生调试事件,代码如下:

#include <Windows.h>
#include <stdio.h>typedef DWORD(WINAPI* ZW_SET_INFORMATION_THREAD) (HANDLE, DWORD, PVOID, ULONG);
#define ThreadHideFromDebugger 0x11
VOID DisableDebugEvent(VOID)
{HINSTANCE hModule;ZW_SET_INFORMATION_THREAD ZwSetInformationThread;hModule = GetModuleHandleA("Ntdll");ZwSetInformationThread = (ZW_SET_INFORMATION_THREAD)GetProcAddress(hModule, "ZwSetInformationThread");ZwSetInformationThread(GetCurrentThread(), ThreadHideFromDebugger, 0, 0);
}int main()
{printf("Begin\n");DisableDebugEvent();printf("End\n");return 0;
}

关键代码为ZwSetInformationThread(GetCurrentThread(), ThreadHideFromDebugger, 0, 0),如果处于调试状态,执行完该行代码,程序就会退出。
想要bypass也比较简单,可以看到ZwSetInformationThread()函数的第二个参数为ThreadHideFromDebugger,其值为0x11。当执行到该函数的时候,若发现第 2 个参数值为 0x11,跳过或者将 0x11 修改为其他值即可。

示例

示例1

还是以ctfwiki给的示例来做,直接运行,要求输入密码,输入出错则提示

无壳32位,丢ida看看,查找字符串

显然,字符串表明程序中可能有各种检测,比如检测进程名ollydbg.exe, ImmunityDebugger.exe, idaq.exe和Wireshark.exe,然后也有其他的检测,可以看到字符串password is wrong和You password is correct的字样,同时还有疑是待解密的flag字符串

先从提示处开始入手,也就是主函数,果然存在大量的反调试

int __cdecl main(int argc, const char **argv, const char **envp)
{FILE *v3; // eaxHANDLE v4; // eaxint v11; // [esp+C4h] [ebp-A8h]DWORD v12; // [esp+D4h] [ebp-98h]LPCSTR lpFileName; // [esp+D8h] [ebp-94h]BOOL pbDebuggerPresent; // [esp+DCh] [ebp-90h]int v15; // [esp+E0h] [ebp-8Ch]int v16; // [esp+E4h] [ebp-88h]int i; // [esp+E8h] [ebp-84h]int v18; // [esp+ECh] [ebp-80h]int v19; // [esp+F0h] [ebp-7Ch]char v20[4]; // [esp+F8h] [ebp-74h]int v21; // [esp+108h] [ebp-64h]char v22; // [esp+10Ch] [ebp-60h]char v23; // [esp+10Dh] [ebp-5Fh]CPPEH_RECORD ms_exc; // [esp+154h] [ebp-18h]v22 = 0;memset(&v23, 0, 0x3Fu);v21 = 1;printf("Input password >");v3 = (FILE *)sub_40223D();fgets(&v22, 64, v3);strcpy(v20, "I have a pen.");v21 = strncmp(&v22, v20, 0xDu);               // 直接比较明文和输入字符串if ( !v21 ){puts("Your password is correct.");if ( IsDebuggerPresent() == 1 )             // API: IsDebuggerPresent() 静态反调{puts("But detected debugger!");exit(1);}if ( sub_401120() == 0x70 )                 // API:NtGlobalFlag 静态反调{puts("But detected NtGlobalFlag!");exit(1);}v4 = GetCurrentProcess();CheckRemoteDebuggerPresent(v4, &pbDebuggerPresent);// API:CheckRemoteDebuggerPresent() 静态反调if ( pbDebuggerPresent ){printf("But detected remotedebug.\n");exit(1);}v12 = GetTickCount();                       // 时间差检测 动态反调for ( i = 0; i == 100; ++i )Sleep(1u);v15 = 1000;if ( GetTickCount() - v12 > 0x3E8 ){printf("But detected debug.\n");exit(1);}lpFileName = "\\\\.\\Global\\ProcmonDebugLogger";// 通过检测设备文件来检测ProcessMonitorif ( CreateFileA("\\\\.\\Global\\ProcmonDebugLogger", 0x80000000, 7u, 0, 3u, 0x80u, 0) != (HANDLE)-1 ){printf("But detect %s.\n", &lpFileName);exit(1);}v11 = sub_401130();                         //  API: CreateToolhelp32Snapshot()检测进程if ( v11 == 1 ){printf("But detected Ollydbg.\n");exit(1);}if ( v11 == 2 ){printf("But detected ImmunityDebugger.\n");exit(1);}if ( v11 == 3 ){printf("But detected IDA.\n");exit(1);}if ( v11 == 4 ){printf("But detected WireShark.\n");exit(1);}if ( sub_401240() == 1 )                    // 检测 VMware{printf("But detected VMware.\n");exit(1);}v16 = 1;                                    // 异常 动态反调v19 = 1;v18 = 1 / 0;ms_exc.registration.TryLevel = -2;printf("But detected Debugged.\n");exit(1);}printf("password is wrong.\n");return 0;
}

进行一些反调的简单分析:

比较明文字符串和输入字符串

  v22 = 0;memset(&v23, 0, 0x3Fu);v21 = 1;printf("Input password >");v3 = (FILE *)sub_40223D();fgets(&v22, 64, v3);strcpy(v20, "I have a pen.");v21 = strncmp(&v22, v20, 0xDu);               

先输出Input password >,然后用fgets()获取用户输入的字符串, 将I have a pen,复制到v20的缓冲区中,然后用strncmp比对用户输入与I have a pen.的内容, 并将比较结果返回给v21. 以下会根据v21, 也就是根据输入的password是否正确而进行跳转

NtGlobalFlag

    if ( sub_401120() == 0x70 )                 // API:NtGlobalFlag{puts("But detected NtGlobalFlag!");exit(1);}
//sub_401120
int sub_401120()
{return *(_DWORD *)(__readfsdword(0x30u) + 104) & 0x70;
}

0x68是 PEB 的NtGlobalFlag字段对应偏移值
0x70是FLG_HEAP_ENABLE_TAIL_CHECK (0x10),FLG_HEAP_ENABLE_FREE_CHECK (0x20) 和FLG_HEAP_VALIDATE_PARAMETERS (0x40)这三个标志

由于是静态反调,所以在xdbg中无效

时间差检测

v13 = GetTickCount();
for ( i = 0; i == 100; ++i )    // 睡眠Sleep(1u);
v16 = 1000;
if ( GetTickCount() - v13 > 1000 )  //  检测时间差
{printf("But detected debug.\n");exit(1);
}

GetTickCount会返回启动到现在的毫秒数,循环里光是sleep(1)就进行了 100 次,也就是 100 毫秒。 两次得到的时间作差如果大于 1000 毫秒,时差明显大于所耗的时间, 也就间接检测到了调试。
反动调经常使用的时间检测法,其余的时间检测法还有 时钟检测,其他的时间API函数,如QueryPerformanceCounter、GetTickCount、GetSystemTime、GetLocalTime等
bypass也很简单,只需修改标志位让其跳转即可

ProcessMonitor

lpFileName = "\\\\.\\Global\\ProcmonDebugLogger";
if ( CreateFileA("\\\\.\\Global\\ProcmonDebugLogger", 0x80000000, 7u, 0, 3u, 0x80u, 0) != (HANDLE)-1 )
{printf("But detect %s.\n", &lpFileName);     exit(1);
}

这里通过检测设备文件\\.\Global\ProcmonDebugLogger来检测ProcessMonitor

检测进程名

v11 = sub_401130();    
if ( v11 == 1 )
{printf("But detected Ollydbg.\n");exit(1);
}
if ( v11 == 2 )
{printf("But detected ImmunityDebugger.\n");exit(1);
}
if ( v11 == 3 )
{printf("But detected IDA.\n");exit(1);
}
if ( v11 == 4 )
{printf("But detected WireShark.\n");exit(1);
}//sub_401130()
signed int sub_401130()
{PROCESSENTRY32 pe; // [esp+0h] [ebp-138h]HANDLE hSnapshot; // [esp+130h] [ebp-8h]BOOL i; // [esp+134h] [ebp-4h]pe.dwSize = 296;memset(&pe.cntUsage, 0, 0x124u);hSnapshot = CreateToolhelp32Snapshot(2u, 0);for ( i = Process32First(hSnapshot, &pe); i == 1; i = Process32Next(hSnapshot, &pe) ){if ( !_stricmp(pe.szExeFile, "ollydbg.exe") )return 1;if ( !_stricmp(pe.szExeFile, "ImmunityDebugger.exe") )return 2;if ( !_stricmp(pe.szExeFile, "idaq.exe") )return 3;if ( !_stricmp(pe.szExeFile, "Wireshark.exe") )return 4;}return 0;
}

通过执行sub_401130()函数来检测进程,并根据检测到的不同进程,返回相应的值。

sub_401120()中使用了 API: CreateToolhelp32Snapshot来获取当前的进程信息, 并在 for 循环里依次比对。如果找到指定的进程名, 就直接返回相应的值,然后根据返回值跳转到不同的分支里。

检测 VMware

if ( sub_401240() == 1 )    // 8. 通过vmware的I/O端口进行检测
{printf("But detected VMware.\n");exit(1);
}
//sub_401240()
signed int sub_401240()
{unsigned __int32 v0; // eaxv0 = __indword(0x5658u);return 1;
}

这是 VMware 的一个 "后门"I/O 端口, 0x5658 = “VX”.
如果程序在 VMware 内运行, 程序使用In指令通过0x5658端口读取数据时, EBX寄存器的值就会变为0x564D5868(0x564D5868 == “VMXh”)

SEH

v17 = 1;
v20 = 1;
v12 = 0;
v19 = 1 / 0;    // SEH
ms_exc.registration.TryLevel = -2;
printf("But detected Debugged.\n");
exit(1);

接下来这一段,这里v19 = 1 / 0明显是不合常理的,会产生一个除零异常. 而后面的ms_exc.registration.TryLevel = -2,解除异常,TryLevel=TRYLEVEL_NONE (-2) . 来看汇编代码

这里的idiv [ebp+var_9C]触发异常后就由程序注册的异常处理函数接管,如果没有在异常处理程序入口设下断点的话, 程序就容易跑飞,将异常交给SEH处理下好断点即可。

获取flag

前面分析到有一串类似待解密的flag字符串,怎么都没有看到相关的代码流,实际上由于 IDA 反编译的限制,使得反编译出的伪 C 代码并不正确,来看最后一个Debugged提示的汇编代码,有些代码流没有被实际反编译出来

.text:00401627 loc_401627:                             
.text:00401627                 call    sub_4012E0
.text:0040162C                 movzx   eax, ax
.text:0040162F                 mov     [ebp+var_A8], eax
.text:00401635                 cmp     [ebp+var_A8], 0
.text:0040163C                 jz      short loc_401652  //该函数也没有被反编译出来
.text:0040163E                 push    offset aButDetectedDeb_2 ,"But detected Debugged.\n"
.text:00401643                 call    _printf
.text:00401648                 add     esp, 4
.text:0040164B                 push    1               ; int
.text:0040164D                 call    _exit

以上代码并没有被实际反编译出来,而loc_401652()也是同样没有被反编译出来的代码

.text:00401652 loc_401652:                           
.text:00401652                 mov     [ebp+var_78], 0
.text:00401659                 cmp     [ebp+var_78], 1
.text:0040165D                 jnz     loc_40174D
.text:00401663                 mov     ecx, 7
.text:00401668                 mov     esi, offset aAjJq7hbotHU8ac ; ";aj&@:JQ7HBOt[h?U8aCBk]OaI38"
.text:0040166D                 lea     edi, [ebp+var_CC]
.text:00401673                 rep movsd
.text:00401675                 movsb
.text:00401676                 xor     ecx, ecx
.text:00401678                 mov     [ebp+var_AF], ecx
.text:0040167E                 lea     edx, [ebp+var_CC]
.text:00401684                 mov     [ebp+var_D8], edx
.text:0040168A                 mov     [ebp+Text], 0
.text:00401691                 push    7Fh             ; size_t
.text:00401693                 push    0               ; int
.text:00401695                 lea     eax, [ebp+var_157]
.text:0040169B                 push    eax             ; void *
.text:0040169C                 call    _memset
.text:004016A1                 add     esp, 0Ch
.text:004016A4                 lea     ecx, [ebp+Text]
.text:004016AA                 mov     [ebp+var_D4], ecx
.text:004016B0                 mov     edx, [ebp+var_D8]
.text:004016B6                 push    edx             ; char *
.text:004016B7                 call    _strlen
.text:004016BC                 add     esp, 4
.text:004016BF                 mov     [ebp+var_D0], eax
.text:004016C5                 mov     [ebp+var_15C], 0
.text:004016CF                 jmp     short loc_4016FEloc_4016FE():
.text:004016FE loc_4016FE:                           
.text:004016FE                 mov     eax, [ebp+var_15C]
.text:00401704                 cmp     eax, [ebp+var_D0]
.text:0040170A                 jnb     short loc_401737
.text:0040170C                 mov     ecx, [ebp+var_D0]
.text:00401712                 sub     ecx, [ebp+var_15C]
.text:00401718                 push    ecx
.text:00401719                 mov     edx, [ebp+var_D4]
.text:0040171F                 push    edx
.text:00401720                 mov     eax, [ebp+var_D8]
.text:00401726                 push    eax
.text:00401727                 call    sub_401000
.text:0040172C                 add     esp, 0Ch
.text:0040172F                 test    eax, eax
.text:00401731                 jz      short loc_401735
.text:00401733                 jmp     short loc_401737loc_401737():
.text:00401737 loc_401737:                            
.text:00401737                                         ; 
.text:00401737                 push    0               ; uType
.text:00401739                 push    offset Caption  ; "check!"
.text:0040173E                 lea     ecx, [ebp+Text]
.text:00401744                 push    ecx             ; lpText
.text:00401745                 push    0               ; hWnd
.text:00401747                 call    ds:MessageBoxA

可以看到后续的代码中调用了MessageboxA()函数,其中第三个参数是解密后的flag

再向上查找的话发现在进入flag的解密前会有一个永假条件跳转,在调试的时候雄修改标志位跳过即可,之后让程序解密flag,调用窗体

弹出窗体,得到flag

示例2

改天再补吧

本文链接:https://my.lmcjl.com/post/12577.html

展开阅读全文

4 评论

留下您的评论.