恶意样本分析手册——反调试篇(下)

使用Windows API函数检测调试器是否存在是最简单的反调试技术。Windows操作系统中提供了一些这样的API,应用程序可以通过调用这些API来探测自己是否正在被调试。这些API有些是专门用来探测调试器的存在的。而另外一些API是处于其他目的而设计的,但也可以被改造用来探测调试器的存在。

反调试技术总结

通常,防止恶意代码使用API进行反调试的最简单的方法是在恶意代码运行期间修改恶意代码,使其不能调用探测调试器的API函数,或者修改这些API函数的返回值,确保恶意代码执行合适的路径。与这些方法相比,较复杂的方法是挂钩这些函数,比如使用rootkit技术。

IsDebuggerPresent

探测调试器是否存在的最简单的API函数是IsDebuggerPresent。它会查询进程环境块(PEB)中的IdDebugged标志,如果进程没有运行在调试器环境中,函数返回0;如果调试附加了进程,函数返回一个非零值。

PEB结构体如下所示:

typedef struct _PEB {
  BYTE  Reserved1[2];
  BYTE  BeingDebugged;
  BYTE  Reserved2[1];
  PVOID Reserved3[2];
  PPEB_LDR_DATA Ldr;
  PRTL_USER_PROCESS_PARAMETERS  ProcessParameters;
  BYTE  Reserved4[104];
  PVOID Reserved5[52];
  PPS_POST_PROCESS_INIT_ROUTINE PostProcessInitRoutine;
  BYTE  Reserved6[128];
  PVOID     Reserved7[1];
  ULONG SessionId;
} PEB, *PPEB;

FindWindow

使用此函数查找目标窗口,如果找到,可以禁用窗口,也可以直接退出程序

函数声明:

HWND FindWindow
(
LPCSTR lpClassName,
LPCSTR lpWindowName
);

参数表:

lpClassName:指向一个以NULL字符结尾的、用来指定类名的字符串或一个可以确定类名字符串的原子。如果这个参数是一个原子,那么它必须是一个在调用此函数前已经通过GlobalAddAtom函数创建好的全局原子。这个原子(一个16bit的值),必须被放置在lpClassName的低位字节中,lpClassName的高位字节置零。

如果该参数为null时,将会寻找任何与lpWindowName参数匹配的窗口

lpWindowName:指向一个以NULL字符结尾的、用来指定窗口名(即窗口标题)的字符串。如果此参数为NULL,则匹配所有窗口名。

void CAntiDebugDlg::OnBnClickedBtnFindwindow()
{
    HWND Hwnd = NULL;
    Hwnd = ::FindWindow(L"OllyDbg", NULL);
    if (Hwnd == NULL) {
        MessageBoxW(L"Not Being Debugged!");
    }
    else {
        MessageBoxW(L"Being Debugged!");
    }
}

枚举窗口

使用EnumWindow函数枚举窗口,并且为每一窗口调用一次回调函数,在回调函数中可以调用GetWindowText获取窗口的标题。与目标窗口名进行比对,如果比对成功,则说明发现调试器。

函数声明:

WINUSERAPI
BOOL
WINAPI
EnumWindows(
    _In_ WNDENUMPROC lpEnumFunc,
_In_ LPARAM lParam
);

参数表:

lpEnumFunc:回调函数指针。

lParam:指定一个传递给回调函数的应用程序定义值。

回调函数原型:

BOOL CALLBACK EnumWindowsProc(HWND hwnd,LPARAM lParam);

参数表:

Hwnd:顶层窗口的句柄。

Lparam:应用程序定义的一个值(即EnumWindows中的lParam)。

Int GetWindowText(HWND hWnd,LPTSTR lpString,Int nMaxCount);

参数表:

hWnd:带文本的窗口或控件的句柄。

IpString:指向接收文本的缓冲区指针

nMaxCount:指定要保存在缓冲区内的字符的最大个数,其中包含NULL字符。如果文本超过界限,它就被截断。

BOOL CALLBACK EnumWindowsProc(HWND hwnd, LPARAM lParam)
{
    WCHAR wzChar[100] = { 0 };
    CStringW strData = L"OllyDbg";
    if (IsWindowVisible(hwnd)) {
        GetWindowText(hwnd, wzChar, 100);
        if (wcsstr(wzChar, strData)) {
            MessageBoxW(NULL,L"Being Debugged!",NULL,0);
            g_bDebugged = TRUE;
            return FALSE;
        }
    }
    return TRUE;
}

void CAntiDebugDlg::OnBnClickedBtnEnumwindow()
{
    EnumWindows(EnumWindowsProc, NULL);
    if (g_bDebugged == FALSE) {
        MessageBoxW(L"Not Being Debugged!");
    }
}

枚举进程

枚举进程列表,查看是否有调试器进程

函数声明:

HANDLE WINAPI CreateToolhelp32Snapshot(
DWORD dwFlags, 
 DWORD th32ProcessID 
);

通过获取进程信息为指定的进程、进程使用的堆[HEAP]、模块[MODULE]、线程建立一个快照。

参数表:

dwFlags:用来指定“快照”中需要返回的对象,可以是TH32CS_SNAPPROCESS等

th32ProcessID: 一个进程ID号,用来指定要获取哪一个进程的快照,当获取系统进程列表或获取 当前进程快照时可以设为0

BOOL
WINAPI
Process32FirstW(
    HANDLE hSnapshot,
    LPPROCESSENTRY32W lppe
    );

process32First是一个进程获取函数,当我们利用函数CreateToolhelp32Snapshot()获得当前运行进程的快照后,我们可以利用process32First函数来获得第一个进程的句柄。

BOOL
WINAPI
Process32NextW(
    HANDLE hSnapshot,
    LPPROCESSENTRY32W lppe
    );

Process32Next是一个进程获取函数,当我们利用函数CreateToolhelp32Snapshot()获得当前运行进程的快照后,我们可以利用Process32Next函数来获得下一个进程的句柄。

void CAntiDebugDlg::OnBnClickedBtnEnumprocess()
{
    // TODO: 在此添加控件通知处理程序代码
    HANDLE hwnd = NULL;
    PROCESSENTRY32W pe32 = { 0 };
    pe32.dwSize = sizeof(pe32);//如果没有这句,得出的路径不对
    WCHAR str[] = L"OLLYDBG";
    CStringW strTemp;
    BOOL bOK = FALSE;
    hwnd = ::CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL);
    if (hwnd != INVALID_HANDLE_VALUE) {
        bool bMore = Process32FirstW(hwnd, &pe32);
        do {
            strTemp = pe32.szExeFile;
            //统一转换为大写进行比较
            strTemp.MakeUpper();
            if (wcsstr(strTemp, str)) {
                MessageBoxW(L"Being Debugged!");
                bOK = TRUE;
                break;
            }
            else if (wcsstr(pe32.szExeFile, L"WINDBG")) {
                MessageBoxW(L"Being Debugged!");
                bOK = TRUE;
                break;
            }
        } while (Process32NextW(hwnd, &pe32));
    }
    if (bOK == FALSE) {
        MessageBoxW(L"Not Being Debugged!");
    }
    CloseHandle(hwnd);
}

查看父进程是不是Explorer

当我们双击运行应用程序的时候,父进程都是Explorer,如果是通过调试器启动的,父进程就不是Explorer。

通过GetCurrentProcessId()获得当前进程的ID

通过桌面窗口类和名称获得Explorer进程的ID

使用Process32First/Next()函数枚举进程列表,通过PROCESSENTRY32.th32ParentProcessID 获得的当前进程的父进程ID与Explorer的ID进程比对。如果不一样的很可能被调试器附加

函数声明:

DWORD GetWindowThreadProcessId(
HWND hWnd,
LPDWORD lpdwProcessId
);

找出某个窗口的创建者(线程或进程),返回创建者的标志符。

参数说明:

hWnd:(向函数提供的)被查找窗口的句柄.

lpdwProcessId:进程号的存放地址(变量地址)

使用方法:

void CAntiDebugDlg::OnBnClickedBtnExplorer()
{
    // TODO: 在此添加控件通知处理程序代码
    HANDLE hwnd = NULL;
    HANDLE hexplorer = NULL;
    PROCESSENTRY32 pe32 = { 0 };
    pe32.dwSize = sizeof(pe32);
    CStringW str = L"explorer";
    DWORD ExplorerId = 0;
    DWORD SelfId = 0;
    DWORD SelfParentId = 0;
    SelfId = GetCurrentProcessId();
    hexplorer = ::FindWindowW(L"Progman",NULL);
    GetWindowThreadProcessId((HWND)hexplorer, &ExplorerId);
    hwnd = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL);
    if (hwnd != INVALID_HANDLE_VALUE)
    {
        Process32FirstW(hwnd, &pe32);
        do 
        {
            if (SelfId == pe32.th32ProcessID)
            {
                SelfParentId = pe32.th32ParentProcessID;
            }
        } while (Process32NextW(hwnd,&pe32));
    }
    if (ExplorerId == SelfParentId) {
        MessageBoxW(L"Not Being Debugged!");
    }
    else
    {
        MessageBoxW(L"Being Debugged!");
    }
    CloseHandle(hwnd);
}

检测系统时钟

当进程被调试时,调试器事件处理代码、步过指令等将占用 CPU 循环。如果相邻指令之间所花费的时间如果大大超出常规,就意味着进程很可能是在被调试。有如下两种时钟检测来探测调试器存在的方法:

  • 记录执行一段操作前后的时间戳,然后比较这两个时间戳,如果存在滞后,则可以认为存在调试器
  • 记录触发一个异常前后的时间戳。如果不调试进程,可以很快处理完异常,因为调试器处理异常的速度非常慢。默认情况下,调试器处理异常时需要认为干预,这导致大量延迟。虽然很多调试器允许我们忽略异常,将异常直接返回给程序,但这样操作仍然存在不小的延迟

首先我们可以使用rdtsc指令,它返回至系统重新启动以来的时钟数,并且将其作为一个64位的值存入edx:eax中。恶意代码运行两次rdstc指令,然后比较两次读取之间的差值来判断是否存在调试器。

另外也可以使用函数QueryPerformanceCounter和GetTickCount。同rdstc指令一样,这两个API函数也可以被用来执行一个反调试的时钟检测。为了获取比较的时间差,调用两次函数查询这个计数器,如果两次调用时间话费时间过于长,则可以认为存在调试器。

GetTickCount返回从操作系统启动所经过的毫秒数。调用两次此函数,如果返回值的差值相差反常,就说明在调试状态。

QueryPerformanceCounter和GetTickCount的原理类似,这里以GetTickCount为例说明使用方法:

void CAntiDebugDlg::OnBnClickedBtnGettickcount()
{
    // TODO: 在此添加控件通知处理程序代码
    DWORD dwTime1 = 0;
    DWORD dwTime2 = 0;
    dwTime1 = GetTickCount();
    GetCurrentProcessId();
    GetCurrentProcessId();
    GetCurrentProcessId();
    dwTime2 = GetTickCount();
    if (dwTime2 - dwTime1 > 100)
    {
        MessageBoxW(L"Being Debugged!");
    }
    else {
        MessageBoxW(L"Not Being Debugged!");
    }
}

查看StartupInfo结构

在windows操作系统中,Explorer创建进程的时候会把STARTUPINFO结构中的某些值设为0,非Explorer创建进程的时候会忽略这个结构中的值,所以可以通过查看这个结构中的值是不是为0来判断是否在调试状态。

函数说明:

WINBASEAPI
VOID
WINAPI
GetStartupInfoW(
    _Out_ LPSTARTUPINFOW lpStartupInfo
    );
    取得进程在启动时被指定的 STARTUPINFO 结构
typedef struct _STARTUPINFOW {
    DWORD   cb;
    LPWSTR  lpReserved;
    LPWSTR  lpDesktop;
    LPWSTR  lpTitle;
    DWORD   dwX;
    DWORD   dwY;
    DWORD   dwXSize;
    DWORD   dwYSize;
    DWORD   dwXCountChars;
    DWORD   dwYCountChars;
    DWORD   dwFillAttribute;
    DWORD   dwFlags;
    WORD    wShowWindow;
    WORD    cbReserved2;
    LPBYTE  lpReserved2;
    HANDLE  hStdInput;
    HANDLE  hStdOutput;
    HANDLE  hStdError;
} STARTUPINFOW, *LPSTARTUPINFOW;
使用方法:
void CAntiDebugDlg::OnBnClickedBtnStartupinfor()
{
    // TODO: 在此添加控件通知处理程序代码
    STARTUPINFO info = { 0 };
    GetStartupInfo(&info);
    if (info.dwX != 0 || info.dwY != 0 || info.dwXCountChars != 0 || info.dwYCountChars != 0
        || info.dwFillAttribute != 0 || info.dwXSize != 0 || info.dwYSize != 0)
    {
        MessageBoxW(L"Being Debugged!");
    }
    else {
        MessageBoxW(L"Not Being Debugged!");
    }
}

BeingDebugged,NTGlobalFlag

IsDebuggerPresent() API 检测进程环境块(PEB)中的 BeingDebugged 标志检查这个标志以确定进程是否正在被用户模式的调试器调试。通过fs:[30]可以获得PEB地址。然后通过不同的偏移访问不同的值

通常程序没有被调试时, PEB 另一个成员 NtGlobalFlag(偏移 0x68)值为 0,如果进程被调试通常值为 0x70(代表下述标志被设置):

FLG_HEAP_ENABLE_TAIL_CHECK(0X10)
FLG_HEAP_ENABLE_FREE_CHECK(0X20)
FLG_HEAP_VALIDATE_PARAMETERS(0X40)

CheckRemoteDebuggerPresent

这个函数同IsDebuggerPresent函数几乎一致,它用来检测本机器中的一个进程是否运行在调试器中。同时,它也检查PEB结构中的IsDebugged属性。他不仅可以探测进程自身是否被调试,同时可以探测系统其他进程是否被调试。这个函数将一个进程句柄作为参数,检查这个句柄对应的进程是否被调试器附加,同时,CheckRemoteDebuggerPresent也可以通过传递自身进程句柄探测自己是否被调试。

函数声明:

BOOL CheckRemoteDebuggerPresent(
HANDLE hProcess,
PBOOL pbDebuggerPresent
)

此函数用来确定是否有调试器附加到进程。

参数表:

hProcess:进程句柄

pbDebuggerPresent:指向一个BOOL的变量,如果进程被调试,此变量被赋值为TRUE。

使用方法:

typedef BOOL(WINAPI *CHECK_REMOTE_DEBUGGER_PRESENT)(HANDLE, PBOOL);
void CAntiDebugDlg::OnBnClickedBtnCheckremote()
{
    // TODO: 在此添加控件通知处理程序代码
    HANDLE      hProcess;
    HINSTANCE   hModule;
    BOOL        bDebuggerPresent = FALSE;
    CHECK_REMOTE_DEBUGGER_PRESENT CheckRemoteDebuggerPresent;
    hModule = GetModuleHandleA("Kernel32");
    CheckRemoteDebuggerPresent =
        (CHECK_REMOTE_DEBUGGER_PRESENT)GetProcAddress(hModule, "CheckRemoteDebuggerPresent");
    hProcess = GetCurrentProcess();
    CheckRemoteDebuggerPresent(hProcess, &bDebuggerPresent);
    if (bDebuggerPresent == TRUE)
    {
        MessageBoxW(L"Being Debugged!");
    }
    else
    {
        MessageBoxW(L"Not Being Debugged!");
    }
}

NtQueryInformationProcess

ntdll!NtQueryInformationProcess()有 5 个参数。为了检测调试器的存在,需要将 ProcessInformationclass 参数设为 ProcessDebugPort(7)。NtQueryInformationProcess()检索内核结构 EPROCESS 的 DebugPort 成员,这个成员是系统用来与调试器通信的端口句柄。非 0 的 DebugPort 成员意味着进程正在被用户模式的调试器调试。如果是这样的话, ProcessInformation 将被置为 0xFFFFFFFF ,否则 ProcessInformation将被置为 0。

函数声明:

NTSTATUS WINAPI NtQueryInformationProcess(
  _In_      HANDLE           ProcessHandle,
  _In_      PROCESSINFOCLASS ProcessInformationClass,
  _Out_     PVOID            ProcessInformation,
  _In_      ULONG            ProcessInformationLength,
  _Out_opt_ PULONG           ReturnLength
);

参数表:

ProcessHandle:进程句柄

ProcessInformationClass:信息类型

ProcessInformation: 缓冲指针

ProcessInformationLength: 以字节为单位的缓冲大小

ReturnLength:写入缓冲的字节数

使用方法:

typedef NTSTATUS(_stdcall *ZW_QUERY_INFORMATION_PROCESS)(
    HANDLE ProcessHandle,
    PROCESSINFOCLASS ProcessInformationClass, //该参数也需要上面声明的数据结构
    PVOID ProcessInformation,
    ULONG ProcessInformationLength,
    PULONG ReturnLength
    ); //定义函数指针
void CAntiDebugDlg::OnBnClickedBtnQueryinforpro()
{
    // TODO: 在此添加控件通知处理程序代码
    HANDLE      hProcess;
    HINSTANCE   hModule;
    DWORD       dwResult;
    ZW_QUERY_INFORMATION_PROCESS ZwQueryInformationProcess;
    hModule = GetModuleHandleW(L"ntdll.dll");
    ZwQueryInformationProcess = (ZW_QUERY_INFORMATION_PROCESS)GetProcAddress(hModule,"ZwQueryInformationProcess");
    hProcess = GetCurrentProcess();
    ZwQueryInformationProcess(
        hProcess,
        ProcessDebugPort,
        &dwResult,
        4,
        NULL);
    if (dwResult != 0)
    {
        MessageBoxW(L"Being Debugged!");
    }
    else
    {
        MessageBoxW(L"Not Being Debugged!");
    }
}

SetUnhandledExceptionFilter

调试器中步过 INT3 和 INT1 指令的时候,由于调试器通常会处理这些调试中断,所以设置的异常处理例程默认情况下不会被调用, Debugger Interrupts 就利用了这个事实。这样我们可以在异常处理例程中设置标志,通过 INT 指令后如果这些标志没有被设置则意味着进程正在被调试。

函数说明:

LPTOP_LEVEL_EXCEPTION_FILTER WINAPI SetUnhandledExceptionFilter 
( _In_LPTOP_LEVEL_EXCEPTION_FILTER lpTopLevelExceptionFilter
) ;

设置异常捕获函数.,当异常没有处理的时候,系统就会调用SetUnhandledExceptionFilter所设置异常处理函数.

参数表:

lpTopLevelExceptionFilter :函数指针。当异常发生时,且程序不处于调试模式(在vs或者别的调试器里运行)则首先调用该函数。

使用方法:

static DWORD lpOldHandler;
static DWORD NewEip;
typedef LPTOP_LEVEL_EXCEPTION_FILTER(_stdcall  *pSetUnhandledExceptionFilter)(
    LPTOP_LEVEL_EXCEPTION_FILTER lpTopLevelExceptionFilter
    );
pSetUnhandledExceptionFilter lpSetUnhandledExceptionFilter;

LONG WINAPI TopUnhandledExceptionFilter(
    struct _EXCEPTION_POINTERS *ExceptionInfo
)
{
    _asm pushad
    
    lpSetUnhandledExceptionFilter((LPTOP_LEVEL_EXCEPTION_FILTER)lpOldHandler);
    ExceptionInfo->ContextRecord->Eip = NewEip;//转移到安全位置
    _asm popad
    return EXCEPTION_CONTINUE_EXECUTION;
}
void CAntiDebugDlg::OnBnClickedBtnSetunhandleexcfilter()
{
    // TODO: 在此添加控件通知处理程序代码
    bool isDebugged = 0;
    // TODO: Add your control notification handler code here
    lpSetUnhandledExceptionFilter = (pSetUnhandledExceptionFilter)GetProcAddress(LoadLibrary((L"kernel32.dll")),
        "SetUnhandledExceptionFilter");
    //当异常没有处理的时候,系统就会调用此函数所设置的异常处理函数,此函数返回以前设置的回调函数
    lpOldHandler = (DWORD)lpSetUnhandledExceptionFilter(TopUnhandledExceptionFilter);
    _asm {  //获取这个安全地址
        call me;    
me :
        pop NewEip;  
        mov NewEip, offset safe; 
        int 3;  //触发异常,如果被调试,不会走自己定义的异常处理函数
    }
//  MessageBoxW(L"Being Debugged!");
    isDebugged = 1;
    _asm {
safe:
    }
    if (1 == isDebugged) {
        MessageBoxW(L"Being Debugged!");
    }
    else {
        MessageBoxW(L"Not Being Debugged!");
    }
}

SeDebugPrivilege进程权限

默认情况下进程没有 SeDebugPrivilege 权限,调试时,会从调试器继承这个权限,可以通过打开 CSRSS.EXE 进程间接地使用SeDebugPrivilege来判断进程是否被调试。

使用方法:

void CAntiDebugDlg::OnBnClickedBtnSedebugpre()
{
    // TODO: 在此添加控件通知处理程序代码
    HANDLE hProcessSnap;
    HANDLE hProcess;
    PROCESSENTRY32 tp32 = { 0 };  //结构体
    tp32.dwSize = sizeof(tp32);
    CString str = L"csrss.exe";
    hProcessSnap = ::CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL);
    if (INVALID_HANDLE_VALUE != hProcessSnap)
    {
        Process32First(hProcessSnap, &tp32);
        do {
            if (0 == lstrcmpi(str, tp32.szExeFile))
            {
                hProcess = OpenProcess(PROCESS_QUERY_INFORMATION, NULL, tp32.th32ProcessID);
                if (NULL != hProcess)
                {
                    MessageBoxW(L"Being Debugged!");
                }
                else
                {
                    MessageBoxW(L"Not Being Debugged!");
                }
                CloseHandle(hProcess);
            }
        } while (Process32Next(hProcessSnap, &tp32));
    }
    CloseHandle(hProcessSnap);
}

GuardPages

这个检查是针对 OllyDbg 的,因为它和 OllyDbg 的内存访问/写入断点特性相关。除了硬件断点和软件断点外, OllyDbg 允许设置一个内存访问/写入断点,这种类型的断点是通过页面保护来实现的。简单地说,页面保护提供了当应用程序的某块内存被访问时获得通知这样一个途径。

页面保护是通过 PAGE_GUARD 页面保护修改符来设置的,如果访问的内存地址是受保护页面的一部分,将会产生一个 STATUS_GUARD_PAGE_VIOLATION(0x80000001)异常。如果进程被 OllyDbg 调试并且受保护的页面被访问,将不会抛出异常,访问将会被当作内存断点

使用方法:

static bool isDebugged = 1;
LONG WINAPI TopUnhandledExceptionFilter2(
    struct _EXCEPTION_POINTERS *ExceptionInfo
)
{
    _asm pushad
    
    lpSetUnhandledExceptionFilter((LPTOP_LEVEL_EXCEPTION_FILTER)lpOldHandler);
    ExceptionInfo->ContextRecord->Eip = NewEip;
    isDebugged = 0;
    _asm popad
    return EXCEPTION_CONTINUE_EXECUTION;
}

void CAntiDebugDlg::OnBnClickedBtnGuidpages()
{
    // TODO: 在此添加控件通知处理程序代码
    ULONG dwOldType;
    DWORD dwPageSize;
    LPVOID lpvBase;               // 获取内存的基地址
    SYSTEM_INFO sSysInfo;         // 系统信息
    GetSystemInfo(&sSysInfo);     // 获取系统信息
    dwPageSize = sSysInfo.dwPageSize;       //系统内存页大小

    lpSetUnhandledExceptionFilter = (pSetUnhandledExceptionFilter)GetProcAddress(LoadLibrary(L"kernel32.dll"),
        "SetUnhandledExceptionFilter");
    lpOldHandler = (DWORD)lpSetUnhandledExceptionFilter(TopUnhandledExceptionFilter2);

    // 分配内存
    lpvBase = VirtualAlloc(NULL, dwPageSize, MEM_COMMIT, PAGE_READWRITE);
    if (lpvBase == NULL)    
        MessageBoxW(L"VirtualAlloc Error");
    _asm {
        mov   NewEip, offset safe //方式二,更简单
        mov   eax, lpvBase
        push  eax
        mov   byte ptr[eax], 0C3H //写一个 RETN 到保留内存,以便下面的调用
    }
    if (0 == ::VirtualProtect(lpvBase, dwPageSize, PAGE_EXECUTE_READ | PAGE_GUARD, &dwOldType)) {
        MessageBoxW(L"VirtualProtect Error");
    }
    _asm {
        pop   ecx
        call  ecx   //调用时压栈
        safe :
        pop   ecx    //堆栈平衡,弹出调用时的压栈
    }
    if (1 == isDebugged) {
        MessageBoxW(L"Being Debugged!");
    }
    else {
        MessageBoxW(L"Not Being Debugged!");
    }
    VirtualFree(lpvBase, dwPageSize, MEM_DECOMMIT);
}

软件断点

调试器设置断点的基本机制是用软件中断指令INT3.临时替换运行程序中的一条指令,然后当程序运行这条指令时,调用调试异常处理例程,INT 3指令的机器码是0xCC,因此无论何时,使用调试器设置一个断点,它都会插入一个0xCC来修改代码,恶意代码常用的一种反调试技术是在它的代码中查找机器码0xCC来扫描调试器对它代码的INT 3修改。所以可以通过在受保护的代码段和(或) API 函数中扫描字节 0xCC 来识别软件断点,可以搜索在一般地方下的断点,或者是在函数中下的断点

使用方法:

普通断点:

BOOL DetectBreakPoints()
{
    BOOL bDebugged = FALSE;
    _asm
    {
        jmp CodeEnd;
    CodeStart:
        mov eax, ecx;
        nop;
        push eax;
        push ecx;
        pop ecx;
        pop eax;
    CodeEnd:
        cld;
        mov edi, offset CodeStart;
        mov edx, offset CodeStart;
        mov ecx, offset CodeEnd;
        sub ecx, edx;
        mov al, 0CCH;
        repne scasb;
        jnz NotDebugged;
        mov bDebugged, 1;
    NotDebugged:
    }
    return bDebugged;
}
void CAntiDebugDlg::OnBnClickedBtnBreakpoint()
{
    // TODO: 在此添加控件通知处理程序代码
    if (DetectBreakPoints())
    {
        MessageBoxW(L"Being Debugged!");
    }
    else
    {
        MessageBoxW(L"Not Being Debugged");
    }
}
函数断点:
BOOL DetectFuncBreakpoints()
{
    BOOL bFoundOD;
    bFoundOD = FALSE;
    DWORD dwAddr;
    dwAddr = (DWORD)::GetProcAddress(LoadLibrary(L"user32.dll"), "MessageBoxA");
    __asm
    {
        cld; 检测代码开始
        mov     edi, dwAddr
        mov     ecx, 100; 100bytes
        mov     al, 0CCH
        repne   scasb
        jnz     ODNotFound
        mov bFoundOD, 1
        ODNotFound:
    }
    return bFoundOD;
}
void CAntiDebugDlg::OnBnClickedBtnFuncbreakpoint()
{
    // TODO: 在此添加控件通知处理程序代码
    if (DetectFuncBreakpoints())
    {
        MessageBoxW(L"Being Debugged!");
    }
    else
    {
        MessageBoxW(L"Not Being Debugged!");
    }
}

硬件断点

硬件断点是通过设置名为 Dr0 到 Dr7 的调试寄存器来实现的。 Dr0-Dr3 包含至多 4 个断点的地址, Dr6 是个标志,它指示哪个断点被触发了, Dr7 包含了控制 4 个硬件断点诸如启用/禁用或者中断于读/写的标志。

由于调试寄存器无法在 Ring3 下访问,硬件断点的检测需要执行一小段代码。可以利用含有调试寄存器值的 CONTEXT 结构,该结构可以通过传递给异常处理例程的 ContextRecord 参数来访问。

使用方法:

static bool isDebuggedHBP = 0;
LONG WINAPI TopUnhandledExceptionFilterHBP(
    struct _EXCEPTION_POINTERS *ExceptionInfo
)
{
    _asm pushad
    //AfxMessageBox("回调函数被调用");
    ExceptionInfo->ContextRecord->Eip = NewEip;
    if (0 != ExceptionInfo->ContextRecord->Dr0 || 0 != ExceptionInfo->ContextRecord->Dr1 ||
        0 != ExceptionInfo->ContextRecord->Dr2 || 0 != ExceptionInfo->ContextRecord->Dr3)
        isDebuggedHBP = 1;  //检测有无硬件断点
    ExceptionInfo->ContextRecord->Dr0 = 0; //禁用硬件断点,置0
    ExceptionInfo->ContextRecord->Dr1 = 0;
    ExceptionInfo->ContextRecord->Dr2 = 0;
    ExceptionInfo->ContextRecord->Dr3 = 0;
    ExceptionInfo->ContextRecord->Dr6 = 0;
    ExceptionInfo->ContextRecord->Dr7 = 0;
    ExceptionInfo->ContextRecord->Eip = NewEip; //转移到安全位置
    _asm popad
    return EXCEPTION_CONTINUE_EXECUTION;
}
void CAntiDebugDlg::OnBnClickedBtnHdbreakpoint()
{
    // TODO: 在此添加控件通知处理程序代码
    lpSetUnhandledExceptionFilter = (pSetUnhandledExceptionFilter)GetProcAddress(LoadLibrary(L"kernel32.dll"),
        "SetUnhandledExceptionFilter");
    lpOldHandler = (DWORD)lpSetUnhandledExceptionFilter(TopUnhandledExceptionFilterHBP);
    _asm {
        mov   NewEip, offset safe //方式二,更简单
        int   3
        mov   isDebuggedHBP, 1 //调试时可能也不会触发异常去检测硬件断点
        safe:
    }
    if (1 == isDebuggedHBP) {
        MessageBoxW(L"Being Debugged!");
    }
    else {
        MessageBoxW(L"Not Being Debugged!");
    }
}

封锁键盘,鼠标输入

WINUSERAPI
BOOL
WINAPI
BlockInput(
    BOOL fBlockIt);

BlockInput函数阻塞键盘及鼠标事件到达应用程序。该参数指明函数的目的。如果参数为TRUE,则鼠标和键盘事件将被阻塞。如果参数为FALSE, 则鼠标和键盘事件不被阻塞。

可以在代码中的关键位置调用此函数。

使用方法:

void CAntiDebugDlg::OnBnClickedBtnBlockinput()
{
    // TODO: 在此添加控件通知处理程序代码
    DWORD dwNoUse;
    DWORD dwNoUse2;
    ::BlockInput(TRUE);
    dwNoUse = 2;
    dwNoUse2 = 3;
    dwNoUse = dwNoUse2;
    ::BlockInput(FALSE);
}

禁用窗口

与BlockInput函数的功能类似,用来禁用窗口

函数说明:

BOOL EnableWindow(HWND hWnd,BOOL bEnable)

hWnd:被允许/禁止的窗口句柄

bEnable: 定义窗口是被允许,还是被禁止。若该参数为TRUE,则窗口被允许。若该参数为FALSE,则窗口被禁止。

Windows API函数。该函数允许/禁止指定的窗口或控件接受鼠标和键盘的输入,当输入被禁止时,窗口不响应鼠标和按键的输入,输入允许时,窗口接受所有的输入。

使用方法:

void CAntiDebugDlg::OnBnClickedBtnEnbalewindow()
{
    // TODO: 在此添加控件通知处理程序代码
    CWnd *wnd;
    wnd = GetForegroundWindow();
    wnd->EnableWindow(FALSE);
    DWORD dwNoUse;
    DWORD dwNoUse2;
    dwNoUse = 2;
    dwNoUse2 = 3;
    dwNoUse = dwNoUse2;
    wnd->EnableWindow(TRUE);
}

ThreadHideFromDebugger

NtSetInformationThread()用来设置一个线程的相关信息。把 ThreadInformationClass参数设为 ThreadHideFromDebugger(11H)可以禁止线程产生调试事件。

函数声明:

NTSTATUS NTAPI NtSetInformationThread(
IN HANDLE ThreadHandle,
IN THREAD_INFORMATION_CLASS ThreadInformaitonClass,
IN PVOID ThreadInformation,
IN ULONG ThreadInformationLength
);

ThreadHideFromDebugger 内部设置内核结构 ETHREAD 的 HideThreadFromDebugger 成员。一旦这个成员设置以后,主要用来向调试器发送事件的内核函数_DbgkpSendApiMessage()将不再被调用

使用方法:

void CAntiDebugDlg::OnBnClickedBtnSetinforthread()
{
    HANDLE hwnd;
    HMODULE hModule;
    hwnd = GetCurrentThread();
    hModule = LoadLibrary(L"ntdll.dll");
    pfnZwSetInformationThread ZwSetInformationThread;
    ZwSetInformationThread = (pfnZwSetInformationThread)GetProcAddress(hModule, "ZwSetInformationThread");
    ZwSetInformationThread(hwnd, ThreadHideFromDebugger, NULL, NULL);
}

OutputDebugString

OutputDebugString 函数用于向调试器发送一个格式化的字符串, Ollydbg 会在底端显示相应的信息。 OllyDbg 存在格式化字符串溢出漏洞,非常严重,轻则崩溃,重则执行任意代码。这个漏洞是由于 Ollydbg 对传递给 kernel32!OutputDebugString()的字符串参数过滤不严导致的,它只对参数进行那个长度检查,只接受 255 个字节,但没对参数进行检查,所以导致缓冲区溢出。

使用方法

//能够让OD崩溃
void CAntiDebugDlg::OnBnClickedBtnOutputdebugstring()
{
    // TODO: 在此添加控件通知处理程序代码
    ::OutputDebugString(L"%s%s%s");
}

使用TLS回调

TLS回调被用来在程序入口点执行之前运行代码,因此这些代码可以在调试器中秘密执行。TLS是windows的一个存储类,其中数据对象不是一个自动的堆栈变量,而是代码中运行的每个线程的一个本地变量。大致而言,TLS允许每个线程维护一个TLS声明的专有变量。在应用程序实现TLS的情况下,可执行程序的PE头部会包含一个.tls段。TLS提供了初始化和终止TLS数据对象的回调函数。windows系统在执行程序正常的入口点之前运行这些回调函数。

可以使用PEview查看应用程序的.tls段,可以发现TLS回调函数。通常情况下,正常程序不适用.tls段,如果看到了程序的.tls段,就可以怀疑它使用了反调试技术。

使用IDA Pro可以很容易的分析TLS回调函数。一旦IDA Pro完成对应用程序的分析,可以通过Ctrl+E看到二进制的入口点,该组合键的作用是显示应用程序的所有入口点,其中包含TLS回调。

上篇:https://blog.nsfocus.net/anti-test-articles1/

如果您需要了解更多内容,可以
加入QQ群:570982169
直接询问:010-68438880

 

Spread the word. Share this post!

Meet The Author

Leave Comment