前言

由于传播、利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,文章作者不为此承担任何责任。(本文仅用于交流学习),本文仅作技术研究。

本文涉及到的案例可在附件获取。

反调试概念

反调试技术主要用于防止调试器观察和控制程序的执行,在商业软件和恶意软件中,常常会使用反调试来防止分析。

有些作者可能会在在程序中设计进行检测,例如:如果发现指令有CC,就知道有人在调试,就可以防止你调试(善意的可能会单纯关闭软件,或者让CC失效,恶意的会格盘,安装木马类似类似,这种手段叫做暗桩),这也就是反调试。

PEB静态反调试

注意:PEB实际上本质也是使用Windows API来检测,进行反调试,这里单独划分是因为这些函数主要是检测PEB信息。

特点:对于所有的用户层 PEB 静态反调试,可以在程序正式的运行之前先挂起用户程序寻找相关函数来进行分析。

前置知识

PEB简介

在Windows操作系统中,PEB是指Process Envirment Block,是一个数据结构。存储着有关进程的信息,每个进程都有一个对应的PEB结构。

对于PEB结构体可以详见

https://www.vergiliusproject.com/kernels/x86/Windows%20XP/SP3/_PEB

这里给出小部分的结构体信息

struct _PEB
{
    UCHAR InheritedAddressSpace;                                            //0x0
    UCHAR ReadImageFileExecOptions;                                         //0x1
    UCHAR BeingDebugged;                                                    //0x2
    UCHAR SpareBool;                                                        //0x3
    VOID* Mutant;                                                           //0x4
    VOID* ImageBaseAddress;                                                 //0x8
    struct _PEB_LDR_DATA* Ldr;                                              //0xc
    struct _RTL_USER_PROCESS_PARAMETERS* ProcessParameters;                 //0x10
    VOID* SubSystemData;                                                    //0x14
    VOID* ProcessHeap;                                                      //0x18
    struct _RTL_CRITICAL_SECTION* FastPebLock;                              //0x1c
    VOID* FastPebLockRoutine;                                               //0x20
    VOID* FastPebUnlockRoutine;                                             //0x24
    ULONG EnvironmentUpdateCount;                                           //0x28
    VOID* KernelCallbackTable;                                              //0x2c
    ULONG SystemReserved[1];                                                //0x30
    ULONG AtlThunkSListPtr32;                                               //0x34
    struct _PEB_FREE_BLOCK* FreeList;                                       //0x38
    ULONG TlsExpansionCounter;                                              //0x3c
    VOID* TlsBitmap;                                                        //0x40
    ULONG TlsBitmapBits[2];                                                 //0x44
    VOID* ReadOnlySharedMemoryBase;                                         //0x4c
    VOID* ReadOnlySharedMemoryHeap;                                         //0x50
    VOID** ReadOnlyStaticServerData;                                        //0x54
    VOID* AnsiCodePageData;                                                 //0x58
    VOID* OemCodePageData;                                                  //0x5c
    VOID* UnicodeCaseTableData;                                             //0x60
    ULONG NumberOfProcessors;                                               //0x64
    ULONG NtGlobalFlag;                                                     //0x68
    ....
    ....
};

TEB简介

Thread Environment Block (TEB) 是Windows NT系列的操作系统(包括Windows XP, Windows 7, 等等)用来存储关于线程的重要信息的数据结构。

TEB包含了许多关于线程的信息,比如当前线程的线程标识符(ThreadId),线程的栈信息(Stack Base, Stack Limit),线程的错误号(ERROR_CODE)等。这些信息用于帮助操作系统或者线程自身在运行过程中进行自我管理和调度。

注意:每个线程都有一个TEB结构,由操作系统分配并维护。

TEB结构体详见

https://www.vergiliusproject.com/kernels/x86/Windows%20XP/SP3/_TEB

这里给出部分代码:

struct _TEB
{
    struct _NT_TIB NtTib;                                                   //0x0
    VOID* EnvironmentPointer;                                               //0x1c
    struct _CLIENT_ID ClientId;                                             //0x20
    VOID* ActiveRpcHandle;                                                  //0x28
    VOID* ThreadLocalStoragePointer;                                        //0x2c
    struct _PEB* ProcessEnvironmentBlock;                                   //0x30
    ...
    ...
};

NT_TIB结构体

https://www.vergiliusproject.com/kernels/x86/Windows%20XP/SP3/_NT_TIB
struct _NT_TIB
{
    struct _EXCEPTION_REGISTRATION_RECORD* ExceptionList;                   //0x0
    VOID* StackBase;                                                        //0x4
    VOID* StackLimit;                                                       //0x8
    VOID* SubSystemTib;                                                     //0xc
    union
    {
        VOID* FiberData;                                                    //0x10
        ULONG Version;                                                      //0x10
    };
    VOID* ArbitraryUserPointer;                                             //0x14
    struct _NT_TIB* Self;                                                   //0x18
};

PEB获取

  • 直接获取

    MOV EAX,DWORD PTR FS:[0x30]       ;FS:[0x30]是PEB的地址
    
  • 间接获取

    MOV EAX,DWORD PTR FS:[0x18]       ;FS:[0x18]存储的是与TEB关联的PEB指针的地址
    MOV EAX,DWORD PTR DS:[EAX+0x30]    ;DS:[EAX+0x30]存储的是PEB的地址
    

BeingDebugged标志

概述

在Windows API中,IsDebuggerPresent该函数检测进程环境块(PEB)中的PEB.BeingDebugged值,通过布尔返回值检查当前进程是否被调试器调试。

函数原型:

BOOL IsDebuggerPresent();

返回值:

  • 如果当前进程在调试器的上下文中运行,则返回值为非零。
  • 如果当前进程未在调试器的上下文中运行,则返回值为零。

案例查看

我们在有IsDebuggerPresent的程序中拖入OD查看(三种可能出现的情况)

FS = 0x7FFDF000

我们发现以下内容:

  • IsDebuggerPresent()被汇编成三条指令

    MOV EAX,DWORD PTR FS:[0x18]       ;FS:[0x18]存储的是与TEB关联的PEB指针的地址
    MOV EAX,DWORD PTR DS:[EAX+0x30]    ;DS:[EAX+0x30]存储的是PEB的地址
    MOVZX EAX,BYTE PTR DS:[EAX+2]     ;DS:[EAX+2]存储的是BeingDebugged的值

    还有一种寻址方式,具体看是直接寻址还是间接寻址。

    MOV EAX,DWORD PTR FS:[0x30]   
    MOVZX EAX,BYTE PTR DS:[EAX+2]
  • FS的值是不固定的

    分析:

    • FS寄存器的值是用来指向当前线程的Thread Environment Block (TEB)的,这个值是由操作系统在线程创建时动态分配的。也就是说,每一个线程的FS寄存器的值通常不同,它们指向了各自线程的TEB。

    • 因此0x7FFDD000,0x7FFDE000和0x7FFDF000,它们都是在某一时刻对应线程的TEB的地址。这些地址的变化可能是由于操作系统使用了不同的线程,或者同一个线程在不同时刻由于某些原因(可能是线程上下文切换,或者是操作系统的内存管理机制)导致TEB的地址发生了改变。

  • 这三条汇编指令分析后的结果如下。

    PS:有可能你们的值跟我的也不一样。

    FS = 0x7FFDF000 //TEB的起始地址
    [0x7FFDF018] = 0x7FFDF000 
    [0x7FFDF030] = 0X7FFD7000 
    [0X7FFD7002] = BeingDebugged的值,0x1
    

总结与绕过

在此声明,BeingDebugged反调试技术,关键的是这三条指令,IsDebuggerPresent()函数只是将其封装起来,方便使用。

MOV EAX,DWORD PTR FS:[0x18]     ;FS:[0x18]存储的是与TEB关联的PEB指针的地址
MOV EAX,DWORD PTR DS:[EAX+0x30]  ;DS:[EAX+0x30]存储的是PEB的地址
MOVZX EAX,BYTE PTR DS:[EAX+2]       ;DS:[EAX+2]存储的是BeingDebugged的值

绕过方式

  • 修改IsDebuggerPresent函数返回值即可绕过,或者定位和修改PEB.BeingDebugged的值。
  • OD安装插件Hideod
  • 或者直接分析其汇编语句,修改跳转语句。因为这只是个检测,会有对应的语句来退出程序。

Flags ForceFlags标志

概述

若进程处于调试状态,则ProcessHeap会持有特定值PEB.ProcessHeap

Flags和ForceFlags标志是HEAP的两个标志

只要进程被调试,进程在堆上分配的内存,在分配的堆的头信息里,Flags ForceFlags这个标志位会被修改,因此可以通过判断这两个标志位的方式来反调试。

程序正常运行时:

  • HEAP.Flags=0x2
  • HEAP.ForceFlags=0x0

HEAP概述

PEB.ProcessHeap(PEB结构体中偏移0x18的位置)成员既可以从PEB结构体中直接获得,也可以通过GetProcessHeap() API获得。

struct _PEB
{
    UCHAR InheritedAddressSpace;                                            //0x0
    UCHAR ReadImageFileExecOptions;                                         //0x1
    UCHAR BeingDebugged;                                                    //0x2
    union
    {
        UCHAR BitField;                                                     //0x3
        struct
        {
            UCHAR ImageUsesLargePages:1;                                    //0x3
            UCHAR IsProtectedProcess:1;                                     //0x3
            UCHAR IsImageDynamicallyRelocated:1;                            //0x3
            UCHAR SkipPatchingUser32Forwarders:1;                           //0x3
            UCHAR IsPackagedProcess:1;                                      //0x3
            UCHAR IsAppContainer:1;                                         //0x3
            UCHAR IsProtectedProcessLight:1;                                //0x3
            UCHAR IsLongPathAwareProcess:1;                                 //0x3
        };
    };
    VOID* Mutant;                                                           //0x4
    VOID* ImageBaseAddress;                                                 //0x8
    struct _PEB_LDR_DATA* Ldr;                                              //0xc
    struct _RTL_USER_PROCESS_PARAMETERS* ProcessParameters;                 //0x10
    VOID* SubSystemData;                                                    //0x14
    VOID* ProcessHeap;                                                      //0x18
    ....
    ....
};

HEAP结构简介

结构体代码

https://www.vergiliusproject.com/kernels/x86/Windows%20XP/SP3/_HEAP
struct _HEAP
{
    struct _HEAP_ENTRY Entry;                                               //0x0
    ULONG Signature;                                                        //0x8
    ULONG Flags;                                                            //0xc
    ULONG ForceFlags;                                                       //0x10
    ULONG VirtualMemoryThreshold;                                           //0x14
    ULONG SegmentReserve;                                                   //0x18
    ULONG SegmentCommit;                                                    //0x1c
    .....
    .....    
};

案例查看

GetProcessHeap() API代码的汇编形式基本类似于IsDebuggerPresent(),按照TEB→PEB→PEB.ProcessHeap顺序访问的ProcessHeap

MOV EAX,DWORD PTR FS:[0x18]     ;FS:[0x18]存储的是与TEB关联的PEB指针的地址
MOV EAX,DWORD PTR DS:[EAX+0x30] ;DS:[EAX+0x30]存储的是PEB的地址
MOV EAX, DWORD PTR DS:[EAX+0x18]     ;HEAP指针

MOV EAX,DWORD PTR DS:[EAX+0x0C] ;   Flags标志
MOV EAX,DWORD PTR DS:[EAX+0x10] ;   ForceFlags标志

案例查看:

导入OD,注意不要运行。一般情况下,当导入后,EBX的值就是PEB的地址。

  • PEB地址

  • HEAP地址

  • Flags标志与ForceFlags标志

安装插件后的内容:

绕过方式

  • 在调试时使用自行修改值
  • 使用OD插件,如Hideod

NtGlobalFlag标志

概述

在32位机器上, NtGlobalFlag字段位于PEB(进程环境块)0x68的偏移处,,64位机器则是在偏移0xBC位置。

通常,当进程未被调试时,NtGlobalFlag字段包含值0x0。调试进程时,该字段通常包含值0x70,表示设置了以下标志:

FLG_HEAP_ENABLE_TAIL_CHECK (0x10)
FLG_HEAP_ENABLE_FREE_CHECK (0x20)
FLG_HEAP_VALIDATE_PARAMETERS (0x40)

案例查看

MOV EAX,DWORD PTR FS:[0x18]     ;FS:[0x18]存储的是与TEB关联的PEB指针的地址
MOV EAX,DWORD PTR DS:[EAX+0x30]  ;DS:[EAX+0x30]存储的是PEB的地址
MOV EAX,DWORD PTR DS:[EAX+0x68] ;   NtGlobalFlag标志

安装插件后:

绕过方式

  • 在调试时使用自行修改值
  • 使用OD插件,如Hideod

注:将运行中的进程附加到调试器时,NtGlobalFlag值不变

总结

根据PEB信息来进行反调试,是最基本的反调试方式。

针对这几个标志,我们在熟悉后,可以使用OD插件来帮助我们绕过。

针对调试程序反调试

有些程序,会针对OD进行检测,检测在这个程序运行的时候是否存在调程序进程。

这里面我们以OD为例子

前置配置

忽略异常


安全设置

检测进程名

概述

一些程序会在运行时,时刻检测Windows正在运行的程序(即进程),来查看是否存在Ollydbg.exe,若存在则将其关闭(这是最小风险的,更有甚者会格盘,释放病毒等)。

一般这种程序都会将可以函数给隐藏,即我们在OD名称位于程序窗口中,找不到可疑函数。此时我们可以尝试通过观测GetProcAddressWindows API来捕获是否间接加载隐藏函数。

原理分析

隐藏函数捕捉

GetProcAddress函数分析

从指定的动态链接库 (DLL) 检索导出函数 (也称为过程) 或变量的地址。

FARPROC GetProcAddress(
  [in] HMODULE hModule,
  [in] LPCSTR  lpProcName
);

参数:

  • hModule:包含函数或变量的 DLL 模块的句柄。

  • lpProcName:函数或变量名称,或函数的序号值。 如果此参数是序号值,则它必须在低序位字中;高序位字必须为零。

返回值:

  • 如果函数成功,则返回值是导出的函数或变量的地址。
  • 如果函数失败,则返回值为 NULL。

程序放至OD,注意不能运行,然后设置GetProcAddress函数断点。

此时,我们可以运行程序,因为可能存在很多程序通过GetProcAddress函数调用,所以我们要时刻观察堆栈区给出的信息。

我们一直F9,观察到有三个可疑函数

  • EnumProcesses

    EnumProcesses 是一个 Windows API,它被用来枚举当前系统中所有运行的进程的ID。

    函数原型如下:

    BOOL EnumProcesses(
      DWORD *lpidProcess,
      DWORD cb,
      DWORD *cbNeeded
    );
    

    这个函数需要三个参数:

    • lpidProcess:指向一个数组,用于接收进程标识符列表。
    • cb:指定 lpidProcess 数组的大小,以字节为单位。通常可以设置为 sizeof(DWORD)*数组长度。
    • cbNeeded:返回实际需要的数组大小。

    EnumProcesses 函数将会将所有当前运行的进程的标识符填充到 lpidProcess 数组中,并在 cbNeeded 中返回实际需要的数组大小。如果数组大小不足,函数将返回0,并在 GetLastError 函数中设置错误代码。

然后我们Ctrl + F9,使得GetProcAdress执行到函数尾部,获取该函数的地址,地址存放于EAX寄存器。

EnumProcesses的地址是0x76BC3A76

设置EnumProcesses函数断点,可以在添加注释,因为我们使用过地址设置断点,OD不会自动给我们添加注释

  • EnumProcessModules

    EnumProcessModules API函数用于在运行中的进程中列出所有加载的模块。每个进程可能会加载多个模块,包括DLL或其他可执行文件。这个函数提供了了解进程内在模块组成的能力,这在调试或程序安全分析时非常有用。

    函数原型是:

    BOOL EnumProcessModules(
      HANDLE hProcess,
      HMODULE *lphModule,
      DWORD cb,
      LPDWORD lpcbNeeded
    );
    

    函数参数简述:

    • hProcess:一个打开的进程对象的句柄,这个句柄必须具有查询进程的全过程信息权限。
    • lphModule:指向一个数组,该数组接收模块的句柄列表。
    • cb:指定lphModule数组的大小,以字节为单位。
    • lpcbNeeded:指向一个变量,该变量接收返回的进程模块的字节数量。

    同样操作获取EnumProcessModules函数地址0x76BC1EF4

    设置EnumProcessModules函数断点,注释

  • GetModuleBaseNameA

    GetModuleBaseNameA 是一个 Windows API 函数,用于获取指定模块的基本名字,即不包含路径名的模块名字。

    函数原型如下:

    DWORD GetModuleBaseNameA(
      HANDLE  hProcess,
      HMODULE hModule,
      LPSTR   lpBaseName,
      DWORD   nSize
    );
    

    这个函数需要四个参数:

    • hProcess:含有要查询的模块的进程的句柄。这个句柄必须具有 PROCESS_QUERY_INFORMATION 和 PROCESS_VM_READ 的权限。
    • hModule:要查询的模块的句柄。如果这个参数为 NULL,那么函数将会返回进程自身的名字。
    • lpBaseName:一个字符串缓冲区,函数将会把查询到的名字复制到这个缓冲区。
    • nSize:lpBaseName 缓冲区的大小,以字符为单位。

    同样操作获取GetModuleBaseNameA的地址0x76BC2132

    设置GetModuleBaseNameA函数断点,注释

函数断点追踪

我们继续F9,发现程序在EnumProcesses中断,其中0x0012EDE4就是lpidProcess的地址,EnumProcesses会将获取到的进程PID覆盖到这个地址之后的内存地址区域。

Ctrl + F9,运行到EnumProcesses返回语句,查看0x0012EDE4,发现已经覆盖,我们通过任务管理器查看PID,可以看到当前的进程PID都以写入。EAX的值为0x1表示函数成功执行。

注意:任务管理器的PID值是十进制,这里面存储的是十六进制。例如Ollydbg.exe的PID是1668,其十六进制是684.在此这只内存访问断点,因为这个程序针对的是OD。

继续F9运行程序,发现在OpenProcess函数中断

OpenProcess函数用于打开一个现有的进程对象并获取进程的句柄,这个句柄可用于在该进程的上下文中访问它的物理内存和方法。

HANDLE OpenProcess(
  DWORD dwDesiredAccess,  // 访问权限标记
  BOOL  bInheritHandle,   // 是否允许句柄被子进程继承
  DWORD dwProcessId       // 进程标识符PID
);

函数参数解释:

  • dwDesiredAccess:需要的访问权限。例如,如果你想读取进程的内存,必须使用PROCESS_VM_READ标记。完整的访问权限列表在MSDN文档中有描述。
  • bInheritHandle:子进程是否继承该句柄。
  • dwProcessId:目标进程的PID(进程ID)。

如果函数调用成功,它将返回一个指向目标进程的开放句柄。如果函数失败,它将返回 NULL。可以使用 GetLastError 函数来获取具体的错误信息。

注意:使用 OpenProcess 获取到的进程句柄在使用完之后需要通过 CloseHandle 函数进行关闭,以防资源泄漏。

这里面获取Ollydbg.exe的句柄

F9继续运行,到EnumProcessModules函数,然后Ctrl + F9执行到返回语句。

在0x12FDA8处存储这Ollydbg.exe的基址0x00400000

函数基址,也称为函数的起始地址,是函数在内存中的开始位置。操作系统在加载可执行文件时,会把它的各个组成部分(如代码段,数据段等)分配到内存中的某个位置。函数基址即是指该函数代码在内存中的起始位置。

继续F9执行,执行到GetModuleBaseNameA函数,会获取Ollydbg.exe的名字,即Ollydbg.exe

接下来函数就会比较OD的名字,比较成功后,会通过TerminateProcess函数关闭OD

这是整个流程

00401D25  |> /68 A9624600   /PUSH 042-反调.004662A9                    ; /Arg2 = 004662A9 ASCII "Unknown"
00401D2A  |. |8D95 F8EEFFFF |LEA EDX,DWORD PTR SS:[EBP-1108]         ; |将0x0012CE0复制给EDX,注意这是个关键地址,这是后边获取到的进程名存储的地址
00401D30  |. |52            |PUSH EDX                                ; |EDX(0x0012ECE0)入栈
00401D31  |. |E8 AE9D0500   |CALL 042-反调.0045BAE4                    ; \将[0x0012ECE0]重置,方便后边对比
00401D36  |. |83C4 08       |ADD ESP,8                               ;  堆栈平衡
00401D39  |. |8B4D E4       |MOV ECX,DWORD PTR SS:[EBP-1C]           ;  将已经对比的进程数复制给ECX
00401D3C  |. |FFB48D FCEFFF>|PUSH DWORD PTR SS:[EBP+ECX*4-1004]      ; /ProcessID(这里面是EnumProcesses获取得到的PID号)入栈
00401D43  |. |6A 00         |PUSH 0                                  ; |Inheritable = FALSE
00401D45  |. |68 10040000   |PUSH 410                                ; |Access = VM_READ|QUERY_INFORMATION
00401D4A  |. |E8 A9300600   |CALL <JMP.&KERNEL32.OpenProcess>        ; \OpenProc 获取对应进程的handle 句柄
00401D4F  |. |8945 DC       |MOV DWORD PTR SS:[EBP-24],EAX           ;  将获取的进程句柄给复制到SS:[EBP-24],即0x12FDC4
00401D52  |. |837D DC 00    |CMP DWORD PTR SS:[EBP-24],0             ;  比较是否否为0,即是否成功获取
00401D56  |. |74 2C         |JE SHORT 042-反调.00401D84
00401D58  |. |8D45 E8       |LEA EAX,DWORD PTR SS:[EBP-18]           ;  将0012FDD0赋值给EAX
00401D5B  |. |50            |PUSH EAX                                ;  EnumProcessModule lpcbNeeded参数入栈,接受的字节字节数
00401D5C  |. |6A 04         |PUSH 4                                  ;  lphModule数组的大小
00401D5E  |. |8D55 C0       |LEA EDX,DWORD PTR SS:[EBP-40]           ;  将0x12FDAB数组地址(将要存储的进程的基址)复制给EDX
00401D61  |. |52            |PUSH EDX
00401D62  |. |FF75 DC       |PUSH DWORD PTR SS:[EBP-24]              ;  将要打开的对象句柄,可以查看进程信息
00401D65  |. |FF55 B4       |CALL DWORD PTR SS:[EBP-4C]              ;  EnumProcessModules,获取对应基址
00401D68  |. |85C0          |TEST EAX,EAX
00401D6A  |. |74 18         |JE SHORT 042-反调.00401D84
00401D6C  |. |68 04010000   |PUSH 104                                ;  GetModuleBaseNameA 缓冲区大小
00401D71  |. |8D8D F8EEFFFF |LEA ECX,DWORD PTR SS:[EBP-1108]
00401D77  |. |51            |PUSH ECX                                ;  缓冲区地址
00401D78  |. |FF75 C0       |PUSH DWORD PTR SS:[EBP-40]              ;  要查询的模块的句柄
00401D7B  |. |FF75 DC       |PUSH DWORD PTR SS:[EBP-24]              ;  还有要查询的模块进程的句柄
00401D7E  |. |FF55 B0       |CALL DWORD PTR SS:[EBP-50]              ;  GetModuleBaseNameA,获取对应的进程名
00401D81  |. |8945 C4       |MOV DWORD PTR SS:[EBP-3C],EAX           ;  EAX是进程名的长度
00401D84  |> |FF75 DC       |PUSH DWORD PTR SS:[EBP-24]              ; /hObject
00401D87  |. |E8 E02E0600   |CALL <JMP.&KERNEL32.CloseHandle>        ; \CloseHandle
00401D8C  |. |8D85 F4EDFFFF |LEA EAX,DWORD PTR SS:[EBP-120C]         ;  这是这个程序设定的要对比的字符串 即 OLLYDBG.EXE
00401D92  |. |50            |PUSH EAX                                ;  字符串入栈
00401D93  |. |8D95 F8EEFFFF |LEA EDX,DWORD PTR SS:[EBP-1108]         ;  这是GetModuleBaseNameA的获取的进程名
00401D99  |. |52            |PUSH EDX                                ; /入栈
00401D9A  |. |E8 21BD0500   |CALL 042-反调.0045DAC0                    ; \将获取到的进程名大写,方便对比
00401D9F  |. |59            |POP ECX
00401DA0  |. |50            |PUSH EAX
00401DA1  |. |E8 F29C0500   |CALL 042-反调.0045BA98                    ;  将获取的进程的大写于OLLYDBG.EXE 进行对比
00401DA6  |. |83C4 08       |ADD ESP,8                               ;  堆栈平衡
00401DA9  |. |85C0          |TEST EAX,EAX                            ;  如果完全匹配后,EAX的值应该是O,检查是否成功匹配
00401DAB  |. |75 74         |JNZ SHORT 042-反调.00401E21
00401DAD  |. |C745 E0 01000>|MOV DWORD PTR SS:[EBP-20],1
00401DB4  |. |8B4D E4       |MOV ECX,DWORD PTR SS:[EBP-1C]
00401DB7  |. |FFB48D FCEFFF>|PUSH DWORD PTR SS:[EBP+ECX*4-1004]      ; /ProcessId
00401DBE  |. |6A 00         |PUSH 0                                  ; |Inheritable = FALSE
00401DC0  |. |6A 01         |PUSH 1                                  ; |Access = TERMINATE
00401DC2  |. |E8 31300600   |CALL <JMP.&KERNEL32.OpenProcess>        ; \OpenProcess
00401DC7  |. |8945 DC       |MOV DWORD PTR SS:[EBP-24],EAX
00401DCA  |. |837D DC 00    |CMP DWORD PTR SS:[EBP-24],0
00401DCE  |. |74 3F         |JE SHORT 042-反调.00401E0F
00401DD0  |. |6A 00         |PUSH 0                                  ; /ExitCode = 0
00401DD2  |. |FF75 DC       |PUSH DWORD PTR SS:[EBP-24]              ; |hProcess
00401DD5  |. |E8 78300600   |CALL <JMP.&KERNEL32.TerminateProcess>   ; \TerminateProcess
00401DDA  |. |85C0          |TEST EAX,EAX
00401DDC  |. |74 17         |JE SHORT 042-反调.00401DF5
00401DDE  |. |FF75 DC       |PUSH DWORD PTR SS:[EBP-24]              ; /hObject
00401DE1  |. |E8 862E0600   |CALL <JMP.&KERNEL32.CloseHandle>        ; \CloseHandle
00401DE6  |. |FF75 D0       |PUSH DWORD PTR SS:[EBP-30]              ; /hLibModule
00401DE9  |. |E8 CC2E0600   |CALL <JMP.&KERNEL32.FreeLibrary>        ; \FreeLibrary
00401DEE  |. |33C0          |XOR EAX,EAX
00401DF0  |. |E9 AF020000   |JMP 042-反调.004020A4
00401DF5  |> |FF75 DC       |PUSH DWORD PTR SS:[EBP-24]              ; /hObject
00401DF8  |. |E8 6F2E0600   |CALL <JMP.&KERNEL32.CloseHandle>        ; \CloseHandle
00401DFD  |. |FF75 D0       |PUSH DWORD PTR SS:[EBP-30]              ; /hLibModule
00401E00  |. |E8 B52E0600   |CALL <JMP.&KERNEL32.FreeLibrary>        ; \FreeLibrary
00401E05  |. |B8 5A020000   |MOV EAX,25A
00401E0A  |. |E9 95020000   |JMP 042-反调.004020A4
00401E0F  |> |FF75 D0       |PUSH DWORD PTR SS:[EBP-30]              ; /hLibModule
00401E12  |. |E8 A32E0600   |CALL <JMP.&KERNEL32.FreeLibrary>        ; \FreeLibrary
00401E17  |. |B8 5C020000   |MOV EAX,25C
00401E1C  |. |E9 83020000   |JMP 042-反调.004020A4
00401E21  |> |FF45 E4       |INC DWORD PTR SS:[EBP-1C]
00401E24  |. |8B55 E4       |MOV EDX,DWORD PTR SS:[EBP-1C]           ;  将已经对比的进程数复制给EDX
00401E27  |. |3B55 F0       |CMP EDX,DWORD PTR SS:[EBP-10]           ;  EDX与总共要对比的进程数量对比,查看是否完全对比
00401E2A  |.^\0F82 F5FEFFFF \JB 042-反调.00401D25                      ;  检查是否所有程序都对比完,如没有则跳转到 00401D25继续对比

详细比较函数

0045BA98  /$  8B4C24 04     MOV ECX,DWORD PTR SS:[ESP+4]    ;  0x12ECE0是GetModuleBaseNameA获取到的名字
0045BA9C  |.  8B5424 08     MOV EDX,DWORD PTR SS:[ESP+8]    ;  0x12EBDC,是设定要匹配的名字
0045BAA0  |.  53            PUSH EBX
0045BAA1  |.  33C0          XOR EAX,EAX
0045BAA3  |.  33DB          XOR EBX,EBX
0045BAA5  |>  8A01          /MOV AL,BYTE PTR DS:[ECX]                
0045BAA7  |.  8A1A          |MOV BL,BYTE PTR DS:[EDX]               
0045BAA9  |.  2BC3          |SUB EAX,EBX
0045BAAB  |.  75 34         |JNZ SHORT 042-反调.0045BAE1
0045BAAD  |.  84DB          |TEST BL,BL
0045BAAF  |.  74 30         |JE SHORT 042-反调.0045BAE1
0045BAB1  |.  8A41 01       |MOV AL,BYTE PTR DS:[ECX+1]
0045BAB4  |.  8A5A 01       |MOV BL,BYTE PTR DS:[EDX+1]
0045BAB7  |.  2BC3          |SUB EAX,EBX
0045BAB9  |.  75 26         |JNZ SHORT 042-反调.0045BAE1
0045BABB  |.  84DB          |TEST BL,BL
0045BABD  |.  74 22         |JE SHORT 042-反调.0045BAE1
0045BABF  |.  8A41 02       |MOV AL,BYTE PTR DS:[ECX+2]
0045BAC2  |.  8A5A 02       |MOV BL,BYTE PTR DS:[EDX+2]
0045BAC5  |.  2BC3          |SUB EAX,EBX
0045BAC7  |.  75 18         |JNZ SHORT 042-反调.0045BAE1
0045BAC9  |.  84DB          |TEST BL,BL
0045BACB  |.  74 14         |JE SHORT 042-反调.0045BAE1
0045BACD  |.  8A41 03       |MOV AL,BYTE PTR DS:[ECX+3]
0045BAD0  |.  8A5A 03       |MOV BL,BYTE PTR DS:[EDX+3]
0045BAD3  |.  2BC3          |SUB EAX,EBX
0045BAD5  |.  75 0A         |JNZ SHORT 042-反调.0045BAE1
0045BAD7  |.  83C1 04       |ADD ECX,4
0045BADA  |.  83C2 04       |ADD EDX,4
0045BADD  |.  84DB          |TEST BL,BL
0045BADF  |.^ 75 C4         \JNZ SHORT 042-反调.0045BAA5
0045BAE1  |>  5B            POP EBX                                  ;  009724D4
0045BAE2  \.  C3            RETN

总结与绕过

  1. 首先通过GetProcAddress函数来获取隐藏函数
  2. 再EnumProcesses来枚举当前系统中所有运行的进程的PID
  3. 通过OpenProcess函数来获取PID对应的句柄,这里获取的应该是系统进程的句柄
  4. 再通过EnumProcessesModules来获取系统进程的的模块的基址,即每个进程的句柄。
  5. 再通过GetModulesBaseNameA来获取对应句柄和基址的进程名
  6. 甘比句柄
  7. 对比获取到的进程名和程序设置的进程名:
    • 如果相等, 则通过TerminateProcess推出OD
    • 如果不等,则返回循环第三步,继续对比

绕过方式,因为只匹配进程名,所以我们可以在运行调试器时,更改调试器名字来绕过

检测类名和标题名

接下来我们继续来分析,针对调试程序的反调试。

检测类名和标题名的过程与检车进程名的流程是类似的,只不过使用的Windows API不同

概述

如果你的程序发现或者检测到调试器运行的迹象,比如一个具有特定类名或窗口标题名的窗口存在,程序可以立即停止运行或者采取其他反调试措施。

这是一个简单的例子,它检查是否存在特定类型的调试器( OllyDbg)

原理分析

因为其流程是类似的,这里就不会再一步步调试,概述其流程。

首先还是通过GetProcAddress来查看隐藏函数。

在这个程序出现一个有趣的现象,我们发现函数列表中GetProcAddress函数也不存在。

但是我们是可以在OD的Command框,直接输入BP GetProcAddress,来设置断点。

通过GetProcAddress获取到以下可以函数。

其中LstrcmpA、OpenProcess、TerminateProcess这里不再概述。

  • CreateToolHelp32Snpshot

    获取指定进程以及这些进程使用的堆、模块和线程的快照。

    HANDLE CreateToolhelp32Snapshot(
     [in] DWORD dwFlags,
     [in] DWORD th32ProcessID
    );
    

    参数列表:

    • dwFlags:指定你要创建的快照包括什么。比如包含所有进程的快照、包含特定进程的所有模块的快照、包含进程中所有线程的快照等。

      一些常用的值包括

      • TH32CS_SNAPPROCESS(获取所有进程的快照)、
      • TH32CS_SNAPMODULE(获取特定进程的所有模块的快照)等。
    • 如果在dwFlags中指定了TH32CS_SNAPMODULE,th32ProcessID用来指定你想要获取其模块情况的那个进程的ID。如果在dwFlags中指定了TH32CS_SNAPALL,这个值就没有任何用处。

    返回值:

    • 如果函数成功,它将返回指定快照的打开句柄。
    • 如果函数失败,它将返回 INVALID_HANDLE_VALUE。

    通过 Process32First 获取快照中的第一个进程,然后可以使用 Process32Next 函数来获取剩下的进程。注意在处理完所有的进程后,需要用 CloseHandle 函数关闭 snapshot 句柄来防止资源泄漏。

  • Process32First

    检索有关系统快照中遇到的第一个进程的信息。

    BOOL Process32First(
     [in]      HANDLE           hSnapshot,
     [in, out] LPPROCESSENTRY32 lppe
    );
    

    参数列表:

    • hSnapshot:快照的句柄,该句柄是从上次调用 [CreateToolhelp32Snapshot函数返回的。
    • lppe:这是一个指向 PROCESSENTRY32 结构的指针,此结构在函数返回时包含了进程的信息。

    返回值:

    • 如果进程列表的第一个条目已复制到缓冲区,则返回 TRUE ,否则返回 FALSE 。
  • Process32Next

    检索有关系统快照中记录的下一个进程的信息。

    BOOL Process32Next(
     [in]  HANDLE           hSnapshot,
     [out] LPPROCESSENTRY32 lppe
    );
    

    参数列表:

    • hSnapshot:快照的句柄,该句柄是从上次调用 [CreateToolhelp32Snapshot函数返回的。
    • lppe:这是一个指向 PROCESSENTRY32 结构的指针,此结构在函数返回时包含了进程的信息。

    返回值:

    • 如果进程列表的下一个条目已复制到缓冲区,则返回 TRUE ;否则返回 FALSE 。
  • FindWindowA

    检索其类名和窗口名称与指定字符串匹配的顶级窗口的句柄。 此函数不搜索子窗口。 此函数不执行区分大小写的搜索。

    HWND FindWindowA(
     [in, optional] LPCSTR lpClassName,
     [in, optional] LPCSTR lpWindowName
    );

    参数解释:

    • lpClassName:窗口类名。这个参数可以为 NULL,NULL 的意思是会选择所有类名。
    • lpWindowName:窗口的标题,也就是出现在窗口标题栏的那个字符串。

    返回值:

    • 如果函数执行成功,返回值是具有指定类名或窗口名的窗口的句柄。
    • 如果函数执行失败或未找到匹配的窗口,返回值为 NULL。可以调用 GetLastError 获取错误信息。

整个进程如图所示

0040114B   .  FF15 10314000 CALL DWORD PTR DS:[403110]               ;  CreateToolHelp32Snpshot函数,创建进程快照
00401151   .  A3 5C324000   MOV DWORD PTR DS:[40325C],EAX            ;  将快照句柄复制到 内存0x40325C
00401156   .  68 34314000   PUSH buggers3.00403134
0040115B   .  50            PUSH EAX
0040115C   .  FF15 18314000 CALL DWORD PTR DS:[403118]               ;  ProcessFirst,获取快照的第一个程序信息,这是为了FindWindow未生效做准备
00401162   .  6A 00         PUSH 0
00401164   .  68 AE304000   PUSH buggers3.004030AE                   ;  类名为 OllyDbg
00401169   .  FF15 28314000 CALL DWORD PTR DS:[403128]               ;  使用FindWindow函数来查找是否存在类名为OllyDbg的程序
0040116F   .  83F8 00       CMP EAX,0                                ;  检查EAX是否为0,即是否存在此OllyDbg进程
00401172   .  0BC0          OR EAX,EAX
00401174   .  75 04         JNZ SHORT buggers3.0040117A              ;  如果存在 则跳转至0x40117A,向下执行,最终退出程序
00401176   .  7C 27         JL SHORT buggers3.0040119F
00401178   .  EB 25         JMP SHORT buggers3.0040119F              ;  不存在则跳转至0x0040119F
0040117A   >  50            PUSH EAX
0040117B   .  56            PUSH ESI
0040117C   .  57            PUSH EDI
0040117D   .  BF 01000000   MOV EDI,1
00401182   .  BE 2C314000   MOV ESI,buggers3.0040312C
00401187   >  FF36          PUSH DWORD PTR DS:[ESI]
00401189   .  FF15 0C314000 CALL DWORD PTR DS:[40310C]               ;  kernel32.FreeLibrary
0040118F   .  83C6 04       ADD ESI,4
00401192   .  4F            DEC EDI
00401193   .^ 75 F2         JNZ SHORT buggers3.00401187
00401195   >  5F            POP EDI                                  ;  ntdll.7C930228
00401196   .  5E            POP ESI
00401197   .  58            POP EAX
00401198   .  6A 00         PUSH 0                                   ; /ExitCode = 0
0040119A   .  E8 57000000   CALL <JMP.&kernel32.ExitProcess>         ; \ExitProcess

当确实找到对应类名程序,则推出程序

若没有找到,则继续对比,此程序是考虑到有可能程序的类名被改,进一步对比其进程名。

0040119F   >  68 B6304000   PUSH buggers3.004030B6                   ;  ASCII "OLLYDBG.EXE"
004011A4   . |68 58314000   PUSH buggers3.00403158                   ;  这里面是 ProcessFirst或ProcessNext获取到的程序名
004011A9   . |FF15 24314000 CALL DWORD PTR DS:[403124]               ;  将其程序名与OLLYDBG.EXE比较
004011AF   . |0BC0          OR EAX,EAX                               ;  查看对比结果
004011B1   . |75 2F         JNZ SHORT buggers3.004011E2              ;  不相等则跳转到0x4011E2,继续获取快照里面的进程信息
004011B3   . |FF35 3C314000 PUSH DWORD PTR DS:[40313C]
004011B9   . |6A 01         PUSH 1
004011BB   . |68 FF0F1F00   PUSH 1F0FFF
004011C0   . |FF15 14314000 CALL DWORD PTR DS:[403114]               ;  kernel32.OpenProcess
004011C6   . |A3 64324000   MOV DWORD PTR DS:[403264],EAX
004011CB   . |6A 00         PUSH 0
004011CD   . |FF35 64324000 PUSH DWORD PTR DS:[403264]
004011D3   . |FF15 20314000 CALL DWORD PTR DS:[403120]               ;  kernel32.TerminateProcess
004011D9   . |6A 00         PUSH 0                                   ; /ExitCode = 0
004011DB   . |E8 16000000   CALL <JMP.&kernel32.ExitProcess>         ; \ExitProcess
004011E0   . |EB 11         JMP SHORT buggers3.004011F3
004011E2   > |68 34314000   PUSH buggers3.00403134
004011E7   . |FF35 5C324000 PUSH DWORD PTR DS:[40325C]
004011ED   . |FF15 1C314000 CALL DWORD PTR DS:[40311C]               ;  kernel32.Process32Next
004011F3   > |EB 7B         JMP SHORT buggers3.00401270              ;  获取过信息后,跳转到0x401270
004011F5     |CC            INT3
004011F6   .-|FF25 00204000 JMP DWORD PTR DS:[<&kernel32.ExitProcess>;  kernel32.ExitProcess
004011FC     |CC            INT3
004011FD     |CC            INT3
004011FE     |CC            INT3
004011FF     |CC            INT3
00401200  /$ |55            PUSH EBP
00401201  |. |8BEC          MOV EBP,ESP
00401203  |. |53            PUSH EBX
00401204  |. |8B45 08       MOV EAX,DWORD PTR SS:[EBP+8]
00401207  |. |8D50 03       LEA EDX,DWORD PTR DS:[EAX+3]
0040120A  |> |8B18          /MOV EBX,DWORD PTR DS:[EAX]
0040120C  |. |83C0 04       |ADD EAX,4
0040120F  |. |8D8B FFFEFEFE |LEA ECX,DWORD PTR DS:[EBX+FEFEFEFF]
00401215  |. |F7D3          |NOT EBX
00401217  |. |23CB          |AND ECX,EBX
00401219  |. |81E1 80808080 |AND ECX,80808080
0040121F  |.^|74 E9         \JE SHORT buggers3.0040120A
00401221  |. |F7C1 80800000 TEST ECX,8080
00401227  |. |75 06         JNZ SHORT buggers3.0040122F
00401229  |. |C1E9 10       SHR ECX,10
0040122C  |. |83C0 02       ADD EAX,2
0040122F  |> |D0E1          SHL CL,1
00401231  |. |1BC2          SBB EAX,EDX
00401233  |. |5B            POP EBX
00401234  |. |C9            LEAVE
00401235  \. |C2 0400       RETN 4
00401238     |CC            INT3
00401239     |CC            INT3
0040123A     |CC            INT3
0040123B     |CC            INT3
0040123C     |CC            INT3
0040123D     |CC            INT3
0040123E     |CC            INT3
0040123F     |CC            INT3
00401240  /$ |55            PUSH EBP
00401241  |. |8BEC          MOV EBP,ESP
00401243  |. |56            PUSH ESI
00401244  |. |8B4D 08       MOV ECX,DWORD PTR SS:[EBP+8]
00401247  |. |8B55 0C       MOV EDX,DWORD PTR SS:[EBP+C]
0040124A  |. |33F6          XOR ESI,ESI
0040124C  |> |8A040E        /MOV AL,BYTE PTR DS:[ESI+ECX]
0040124F  |. |3A0416        |CMP AL,BYTE PTR DS:[ESI+EDX]
00401252  |. |75 10         |JNZ SHORT buggers3.00401264
00401254  |. |83C6 01       |ADD ESI,1
00401257  |. |84C0          |TEST AL,AL
00401259  |.^|75 F1         \JNZ SHORT buggers3.0040124C
0040125B  |. |8D440E FF     LEA EAX,DWORD PTR DS:[ESI+ECX-1]
0040125F  |. |2B45 08       SUB EAX,DWORD PTR SS:[EBP+8]
00401262  |. |EB 02         JMP SHORT buggers3.00401266
00401264  |> |33C0          XOR EAX,EAX
00401266  |> |5E            POP ESI
00401267  |. |C9            LEAVE
00401268  \. |C2 0800       RETN 8
0040126B     |00            DB 00
0040126C     |00            DB 00
0040126D     |00            DB 00
0040126E     |00            DB 00
0040126F     |00            DB 00
00401270   > |68 58314000   PUSH buggers3.00403158                   ;  ASCII "[System Process]"
00401275   . |68 A0314000   PUSH buggers3.004031A0                   ;  ASCII "buggers3.exe"
0040127A   . |FF15 24314000 CALL DWORD PTR DS:[403124]               ;  将获取到的进程名与被调试进程 buggers3.eze对比
00401280   . |85C0          TEST EAX,EAX                             ;  当遍历到buggers3.exe,即相当于对比完,跳出没有调试
00401282   .^\0F85 17FFFFFF JNZ buggers3.0040119F
00401288   .  68 CC314000   PUSH buggers3.004031CC                   ;  ASCII "MessageBoxA"
0040128D   .  FF35 2C314000 PUSH DWORD PTR DS:[40312C]               ;  user32.77D10000
00401293   .  FF15 08314000 CALL DWORD PTR DS:[403108]               ;  kernel32.GetProcAddress

总结与绕过

  • 首先通过CreateToolHelp32Snpshot来对进程快照
  • 然后通过FindWindow,来匹配,查看是否存在要匹配的调试器
  • Process32First和Process32Next是做了又一次匹配。

绕过,FindWindow会检测类名和标题名,我们可以实用工具,来更改类名和标题名。

这里给出一个案例re-pair

可以看懂,程序名字已经被更改。

在使用其他程序来进行验证