反调试技术合集

WindowsAPI

NtQueryInformationProcess

__kernel_entry NTSTATUS NtQueryInformationProcess(
  [in]            HANDLE           ProcessHandle,
  [in]            PROCESSINFOCLASS ProcessInformationClass,
  [out]           PVOID            ProcessInformation,
  [in]            ULONG            ProcessInformationLength,
  [out, optional] PULONG           ReturnLength
);

第二个参数可以是ProcessBasicInformation(获取PEB信息)或ProcessDebugPort/ProcessDebugObjectHandle

当调试状态时,调试端口的值为0xFFFFFFFF

还可以获取DebugHandle(正常情况下为0)

DWORD dbgHandle=0;
NtQueryInformationProcess(GetCurrentProcess(),(PROCESSINFOCLASS)0x1E,&dbgHandle,4,0);

实际底层调用了ZwQueryInformationProcess

可以通过Hook相关API绕过

SeDebugPrivilege

一般程序正常启动不具备调试权限,但是调试器具有调试权限,并且被调试程序会继承权限

,,状XSTuetrrocess 1d尘女X-+
HIMODULE hMod = GetModuleHandle(L"ntd1l. d11");
typedef int (*CSRGETPROCESSID) ();
CSRGETPROCESSID CsrGetProcessId = (CSRGETPROCESSID) GetProcAddress(hMod,"CsrGetProcessId");
//获取csrss. exe的PID4
DIWORD pid = CsrGetProcessId();
//打开成功说明管理员+调试权限。
HANDLE hCsr = OpenProcess(PROCESS_QUERY_INFORIMATITON, FALSE, pid) ;
BOOL isDebugged=hCsr;

破解即要么置csrss.exe字符串为0,要么hook API

调试环境检测

注册表检测

程序异常结束后,如果设置了JIT调试器,会展示一个调试程序对话框。

设置JIT调试器需要两个注册表值

HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\AeDebug(32位系统)
HKLM\SOFTWARE\Wow6432Node\Microsoft\WindowsNT\CurrentVersion\AeDebug(64位系统)

就可以通过这两项注册表值检测调试器

//判断当前系统是32还是64位。
BOOL b64 = FALSE;
IsWow64Process(GetCurrentProcess()&b64) ;
HKEY hkey = NLL;
TCHAR *reg = b64?TEXT("SOFTWARE\\Wow6432Node\\Microsoft\\WindowsNT\\CurrentVersion\\AeDebug"):TEXT("SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\AeDebug");
//打开注册表键v
DIWIORD ret = RegCreateKey(HKEY_LOCAL_MACHINE, reg, &hkey);
if (ret != ERROR_SUCCESS) return FALSE;
TCHAR *subkey = TEXT("Debugger") ;
TCHAR value[256] = {};
DIWORD len = 256;
//查询对应项的值。
ret = RegQueryValueEx(hkey subkey, NULL, NULL, ((LPBYTE)value, &len);
RegCloseKey(hkey);
// 其中value就是调试器名称

窗口枚举

窗口名可以通过Spy++查看

BOOL isDebugged=FindWindow(TEXT("OLLYDBG"),NULL);

枚举窗口方法

BOOL CALLBACK EnumWindowProc (HIMND hWnd, LPARAM lParam) {
    TCHAR winTitle[0x100] = 0;
    GetiWindowText(hWnd, winTitle, 0x100) ;
    if ( tcsstr(winTitleTEXT("0llyDbg")))
    {
        *((int*)IParam) = true;
        //找到目标窗口停止遍历+
        return false;+
    }
    //继续遍历下一个窗口。
    return true;
}
bool CheckDebugEnumWindow() {
    int nFind = false;
    EnumWindows(EnumWindowProc((LPARAMD)&nFind) ;
    return nFind;
}

父进程检测

#ifndef UNICODE
#define UNICODE
#endif // UNICODE
#ifndef _UNICODE
#define _UNICODE
#endif // _UNICODE

#include <stdio.h>
#include <tchar.h>
#include <Windows.h>
#include <winternl.h>
#include <Shlwapi.h>

/*
/// GetProcessImageFileName 函数
//windows7: kernel32.dll|.lib
// windows r8+: psapi.dll|.lib
#include <psapi.h>
*/

#pragma comment(lib, "shlwapi.lib")

#define SETVALUEFROMPOINTER(p, v) (*p=v)
#if defined(UNICODE) || defined(_UNICODE)
#define OutPutStr(f, v) wprintf_s(L##f, v)
#else
#define OutPutStr(f, v) printf_s(f, v)
#endif


DWORD GetParentPIDAndName( DWORD ProcessID, LPTSTR lpszBuffer_Parent_Name, PDWORD ErrCodeForBuffer );

int main(int argc, const char* argv[]) {
    DWORD pid;
    TCHAR buf[BUFSIZ] = {0};
    DWORD err_code;

    pid = GetParentPIDAndName(GetCurrentProcessId(), buf, &err_code);

    if ( err_code ) {
        fprintf(stderr, "GetProcessName--> err code: %lu\n", err_code);
    }

    OutPutStr("ParentProcessPID: %lu\n", pid);
    OutPutStr("ParentProcessFullName: %s\n", buf);
    PathStripPath(buf);
    OutPutStr("ParentProcessName: %s\n", buf);

    return 0;
}

typedef
__kernel_entry NTSTATUS
(NTAPI*NQIP)(
    IN HANDLE ProcessHandle,
    IN PROCESSINFOCLASS ProcessInformationClass,
    OUT PVOID ProcessInformation,
    IN ULONG ProcessInformationLength,
    OUT PULONG ReturnLength OPTIONAL
    );

DWORD GetParentPIDAndName( DWORD ProcessID, LPTSTR lpszBuffer_Parent_Name, PDWORD ErrCodeForBuffer ) {
    /// 打开给定进程PID
    HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, ProcessID);
    if ( !ProcessID ) {
        DWORD err_code = GetLastError();
        fprintf_s(stderr, "[OpenProcess]err code: %lu\n", err_code);
        return 0;
    }

    /// 下面是获取函数 NtQueryInformationProcess 的函数指针
    HMODULE hNtdll = GetModuleHandle(_T("ntdll.dll"));
    if ( !hNtdll ) {
        DWORD err_code = GetLastError();
        fprintf_s(stderr, "[GetModuleHandle]err code: %lu\n", err_code);
        CloseHandle(hProcess);
        return 0;
    }

    NQIP _NtQueryInformationProcess = (NQIP)GetProcAddress(hNtdll, "NtQueryInformationProcess");
    if ( !_NtQueryInformationProcess ) {
        DWORD err_code = GetLastError();
        fprintf_s(stderr, "[GetProcAddress]err code: %lu\n", err_code);
        CloseHandle(hProcess);
        return 0;
    }
    //***

    /// 获取打开的进程的进程进程信息
    PROCESS_BASIC_INFORMATION pbi;
    NTSTATUS status = _NtQueryInformationProcess(
                            hProcess,
                            ProcessBasicInformation,
                            (LPVOID)&pbi, sizeof(PROCESS_BASIC_INFORMATION),
                            NULL);

    DWORD dwParentID = 0;
    if ( NT_SUCCESS(status) ) {
        /// 结构体 PROCESS_BASIC_INFORMATION 的 "Reserved3"字段 是父进程的PID
        dwParentID = (LONG_PTR)pbi.Reserved3;

        if ( NULL != lpszBuffer_Parent_Name ) {
            HANDLE hParentProcess = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, dwParentID);
            if ( hParentProcess ) {
                /// 用来接收进程文件名和路径的长度(必须!)
                DWORD bufs;
                /// 获取进程路径
                BOOL ret = QueryFullProcessImageName(hParentProcess, 0, lpszBuffer_Parent_Name, &bufs);
                if ( TRUE == ret )
                    SETVALUEFROMPOINTER(ErrCodeForBuffer, 0);
                else
                    SETVALUEFROMPOINTER(ErrCodeForBuffer, GetLastError());

                // 结果是DOS路径+文件名
            }
            else {
                SETVALUEFROMPOINTER(ErrCodeForBuffer, GetLastError());
            }
            if ( hParentProcess )
                CloseHandle(hParentProcess);
        }
    }
    else {
        DWORD err_code = GetLastError();
        fprintf_s(stderr, "[NtQueryInformationProcess]err code: %lu\n", err_code);
    }

    CloseHandle(hProcess);
    return dwParentID;
}

其中PID可以使用GetProcessId(PROCESS_QUERY_INFORMATION)获取,之后就可以比对父进程是否是explorer.exe了

进程扫描

PROCESSENTRY32 pe32 = { sizeof(pe32) } ;
HANDLE hProcessSnap = CreateToolhelp32Snapshot( TH32CS_ SNAPPROCESS, 0) ;
if (hProcessSnap == INTALID _HANDLE_ VALUE)
{
    return FALSE;
}
Process32First(hProcessSnap&pe32) ;
//这里只比较了0l1yDbg,也可以添加其他的调试分析工具名。
do{
    if ( tcsicmp(pe32. szExeFile, TEXT("0l1yDbg.exe ")) == 0)
    {
        CloseHlandle(hProcessSnap);
        return TRUE;
    }
} while (Process32Next(hProcessSnap &pe32)) ;
CloseHandle(hProcessSnap);

内核对象扫描

通过查找调试对象来实现反调试

boo1 CheckDebug_QueryObject0 {
    typedef struct _OBJECT_TYPE_INFORMATION{
        INICODE_ STRING TypeNames;
        ULONG TotalNumber0fHandles;
        ULCNG Tota1Number0fObjiects;
    }OBJECT_TYPE_INFORMATION, *POBJECT_TYPE_INFORMATION;
    typedef struct _OBJECT_ALL_INFORMATION{
        UZONG NumberOfObjectsTypes;
        OBJBCT_TYPE_INFORMATION ObjectTypeInfo[1];
    }OBJBCT_ALL_INFORMATION, *POBJECT_ALL_INFORMATION;
    //1.获取欲查询信息大小,
    ULONG uSize = 0;
    NtQueryObject (NULL,(OBJECT_INFORMATION_CLASS)0x03,&uSize,sizeof (uSize),&uSize);
    //2.获取对象大信息..
    POBJECT_ALL_INFORMATION pObjectAllInfo = (POBJECT_ALL_INFORMATION) new BYTE[uSize+4];
    NtQueryObject(NULL,(OBJECI_INFORMATION_CLASS) 0x03, pObjectAllInfo,uSize,&uSize);
    //3.循环遍历并处理对象信息..
    POBJECT_TYPE_INFORMATION pObjectTypeInfo = pObjectAllInfo->ObjectTypeInfo;
    for (int i = 0: i < pObjectAllInfo->Number0fObjectsTypes; i++){
        //3. 1查看此对象的类型是否为DebugObject.
        if(!wcscmp(L"DebugObject"pObjectTypeInfo->TypeNames.buffer)){
            delete[] pObjectA11Info;
            return true;
        }
        //3. 2获取对象名占用空间大小(考虑到了结构体对齐问题)
        ULONG uNameLength = pObjectTypeInfo->TypeNames. Length;
        UZONG uDataLength = uNameLength - uNameLength % sizeof(ULONG)+sizeof (ULONG);
        //3. 3指向下一个对象信息,
        pObjectTypeInfo =(POBJECT_TYPB_INFORMATION) pObjectTypeInfo->TypeNames.Buffer;
        pObjectTypeInfo = (POBJECT_TYPE_INFORMATION) ((PBYTE) pObjectTypeInfo+uDataLength);
    }
    de1ete[] pObjectA11Info;
    return false;
}

调试模式检测

当系统处于被调试状态时,可以通过检测该状态来实现反调试

struct _SYSTEM_KERNEL_DEBUGGER_INFORMATION
{
    BOOLEAN DebuggerEnabled;
    BOOLEAN DebuggerNotPresent;
}DebuggerInfo = { 0 };
NtQuerySystemInformation((SYSTEML INFORILATIOV CLASS) 0x23,&DebuggerInfo,sizeof(DebuggerInfo),NULL);
BOOL isDebugged=DebuggerInfo.DebuggerEnabled;

利用调试器漏洞

ZwSetInformationThread传参ThreadHideFromDebugger时,可以使线程脱离调试器,效果就像崩溃

enum THREAD_INFO_CLASS4{
    ThreadHideFromDebugger = 17
}
typedef NTSTATUS(NTAPI *ZW_SET_INFORMATION_THREAD) (
    IN HANDLE ThreadHandle,
    IN THREAD_INFO_CLASS ThreadInformationClass,
    IN PVOID ThreadInformation,
    IN ULONG ThreadInformationLength
);
ZW_SET_INFORMATION_THREAD ZwSetInformationThread;
ZwSetInformationThread =(ZW_SET_INFORMATION_THREAD)GetProcAddress(LoadLibrary(L"ntdll.d11"),"ZwSetInformationThread");
ZwSetInformationThread (GetCurrentThread()ThreadHideFromDebugger, NULL, NULL);

可以通过Hook代码绕过

调试器漏洞

PE文件中数据目录表项大于0x10时系统会忽略,但是OD会认为PE格式错误

断点检测

软件断点

软件断点会将目标位置修改为0xcc(int 3),可以通过代码查询或更强的内存校验进行检测,笔者利用这个特性出过一道CTF题目

这种使用硬件断点绕过即可。

硬件断点

CPU有8个硬件断点寄存器Dr0~Dr7,其中Dr0~Dr3保存断点地址,Dr4~Dr5保留,Dr6~Dr7说明哪个硬件断点触发的相关属性

可以检查Dr0~Dr3的值是否为0确认硬件断点

通过API方式

CONTEXT context;
HANDLE hThread = GetCurrentThread();
context.ContextFlags = CONTEXT_DEBU_REGISTERS;
GetThreadContext(hThread, &context) ;
BOOL isDebugged=context.Dr0 != 0|| context.Dr1 != 0 || context.Dr2 != 0|l context.Dr3 !=0

可以通过Hook GetCurrentThread破解

异常方式

BOOL bDebugging = FALSE;
__asm{
    // install SEH
    push handler+
    push DWORD ptr fs:[0]
    mov DWORD ptr fs:[0] esp
    __emit(0xcc)
    mov bDebugging;
    jmp normal

    handler:
    mov eax, dword ptr ss : [esp + 0xc];// ContextRecord
    mov dword ptr ds : [eax + 0xb8]offset normal
    mov ecx, [eax + 4]; // Dr0
    or ecx,[eax + 8]; // Dr1
    or ecx [eax + 0x0C];// Dr2
    or ecx [eax + 0x10];// Dr3
    je NoDebugger;
    mov eCx,
    [eax + 0xb4];// ebp
    // vs2015 debug 下bDebugging的地址为ebp-c
    mov [ecx-0x0c],1 // bDebugging

    NoDebugger:
    xor eax, eax
    retn

    normal:
    //remove SEH
    pop dword ptr fs : [0]μ
    add esp4
}
return bDebugging;

单步检测

单步步过时,call/rep指令下一条指令会设置为int 3

BOOL bDebugging = FALSE;
__asm{
    xor eax, eax
    xor ecx, ecx
    inc ecx
    lea esi, key
    //此处步过时key处会被下0xCC断点。
    //将key处的首字节给AL
    rep lodsb

    key:
    cmp al, 0xcc
    je debuging
    jmp over

    debuging:
    mov bDebugging, 1
    over :
}
return bDebugging;

当EFLAGS的TF位被设置时,CPU将进入单步执行模式,CPU执行一条指令触发EXCEPTION_SINGLE_STEP异常,并且TF清零。

检测方式1:主动触发TF异常,与SEH结合使用探测调试器。先pushfd修改再popfd

bool bDebugged = false;
__asm
{
    // install SEH
    push handler
    push DIWIORD ptr fs : [0]
    mov DIWORD ptr fs : [0] esp
    pushfd
    or dword ptr ss : [esp] 0x100
    popfd
    nop
    mov bDebugged, 1
    jmp normal

    handler:
    mov bDebugged, 1
    mov eax, dword ptr ss : [esp + 0xc]
    mov ebxnormal
    mov dword ptr ds:[eax+0xb8], ebx
    xor eax, eax
    retn

    normal:
    remove SEH
    pop dword ptr fs : [0]
    add esp4
}
return bDebugged;

检测方式2:检测调试器是否设置TF位。当执行pushfd时,TF会自动清零,所以test结果为0,这时候使用pop ss将异常和终端挂起,知道下一条指令执行完毕

bool bDebugged = false;
__asm
{
    push ss
    pop ss
    pushfd
    test BYTE PIR SS : [ESP + 1], 10
    jne debugged

    jmp over

    debugged:
    mov bDebugged, 1

    over:
    popfd
}
return bDebugged;

进程信息

PEB

PEB位于fs段0x30偏移处

PEB中0x2偏移处为BeingDebugged表示进程是否被调试,0x68为NtGlobalFlag表示进程的堆特性

mov eax,fs:[0x30]
mov al,byte ptr DS:[eax+2]
test al,al ; 1为被调试,0为没有被调试

IsDebuggerPresent就是使用的这个原理,但是首先使用mov eax,fs:[0x18]获取TEB的地址,再mov eax,[eax+0x30]获取PEB地址

可以通过手动置零绕过

对于NtGlobalFlag

mov eax,fs:[0x30]
mov eax,dword ptr ds:[eax+0x68]
test eax,0x70 ; 为0x70时有调试器

手动置0绕过

STARTUPINFO

explorer启动时会设置为0,调试器启动时会设置非0

STARTUPINFO si={};
GetStartupInfoW(&si);
BOOL isDebugged=si.dwX||si.dwY||si.dwXSize||dw.YSize;

实际上这些数据位于PEB中0x10中的_RTL_USER_PROCESS_PARAMETERS结构体,只需要将_RTL_USER_PROCESS_PARAMETERS结构体中0x4c 0x50 0x54 0x58位置的数据设置为0即可

时间反调试

rdtsc命令将TSC(Time Stamp Counter)读取到EDX:EAX中

使用时间API也可以

只需要判断两次执行时间大于某个值就认为有调试器

异常反调试

异常发生时,SEH机制下OS接收异常,使用进程中注册的SEH处理。但是如果被调试,则先传给调试器,利用这个特征可以判断是否调试运行。

Windows典型异常列表

int 3为例,反调试程序可以在SEH中更改执行流程,如果控制权交给调试器,调试器没执行SEH代码,程序流程就会未知

; install SEH
push handler
push DWORD PTR fs:[0]
mov DWORD PTR fs:[0],esp
__emit(0xcc)
mov isDebugged,1
jmp normal

handler:
mov eax,dword ptr ss:[esp+0xc] ; exception record
mov dword ptr ds:[eax+0xb8],offset normal ; 修改EIP
xor eax,eax
retn

normal:
pop dword ptr fs:[0]
add esp,4

可以通过调试器中的忽略异常绕过

当SEH没法处理异常时,系统会调用UnhandledExceptionFilter,即停止执行弹窗。SetUnhandledExceptionFilter可以替换弹窗行为

LONG WINAPI Fun(_In_ struct_ EXCEPTION POINTERS *ExceptionInfo+) {
    //跳过mov bDebug, 1这条指令
    // int 3异常时,eip会被回拨到cc处
    ExceptionInfo-> ContextRecord->Eip += 5;
    return EXCEPTION_CONTINUE_EXECUTION;
}
bool CheckDebug _SetUnhandledExceptionFilter() {
    bool bDebug = false;
    _asm{
        __emit(0xCC) ;
        //正常运行时, Fun函数会跳过这条指令。
        //调试时,调试器会不停收到int 3异常,程序崩溃
        mov bDebug,1
    }
    return bDebug;
}
int main(){
    SetUnhandledExceptionFilter(Fun);
    if (CheckDebug_ SetUnhandledExceptionFilter()){
        printf("发现调试器\n");
    }
    else{
        printf("正常运行\n");
    }
    getchar();
    return 0;
}

忽略异常即可

int 2d时内核模式下触发异常的,用户模式下正常运行时触发异常,OD有如下特点

  • int 2d被忽略
  • int 2d下一条指令的第一个字节被忽略
  • 单步跟踪int 2d时,不会停在下条指令开始的地方,而是运行到断点
; install SEH
push handler
push DWORD PTR fs:[0]
mov DWORD PTR fs:[0],esp
int 0x2d
nop
mov isDebugged,1
jmp normal

handler:
mov eax,dword ptr ss:[esp+0xc] ; exception record
mov dword ptr ds:[eax+0xb8],offset normal ; 修改EIP
xor eax,eax
retn

normal:
pop dword ptr fs:[0]
add esp,4

破解:把int 2d改为int 3这种OD不会忽略的异常

自调试

同一个进程不允许同时被两个进程调试,可以先调试自己,防止被调试器调试。

通过创建共享对象来确定是否第一次运行

int main()
{
    //尝试打开互斥体。确定是否首次运行程序.
    HHANDLE hMutex = OpenMutex(MUTEX_MODIFY_STATE, FALSE, L"Globa1\\MyMutex");
    if (hMutex){
        //打开成功说明第2次运行,执行正常代码.
        printf("正被调试运行!\n");
        getchar();
    }else{
    //打开失败说明第1次运行。创建互斥体,并调试创建自身进程
    Createluter(NULL,FALSE,L"G1oba1\\MyMutex");
    TCHAR szPath[MAX _PATH] = 0;
    GetModuleFileName(NULL,szPath,MAX_PATH);
    //调试方式打开程序.
    STARTUPINFO si = { sizeof (STARTUPINFO) }:.
    PROCESS_INFORMATION pi = 0;
    BOOL bStatus = CreateProcess(szPath NULL, NULL,NULL, FALSE,DEBUG_PROCESS|DEBUG_ONLY_THIS_PROCESS| CREATE_NEW_CONSOLE,NULL,NULL,&si, &pi);
    if(!bStatus) {
        printf("创建调试进程失败!\n");
        return 0;
    }
    //初始化调试事件结构体,
    DEBUG_ EVENT DbgEvent = { 0 };
    DWORD dwState = DBG_EXCEPTION_NOT_HANDLED;
    //等待目标exe产生调试事件.
    BOOL bExit = FALSE;
    while(!bExit) {.
        WaitForDebugEvent (&DbEvent,INFINITE);
        if (DbgEvent.dwDebugEventCode == EXIT_PROCESS_DEBUG_EVENT){
            //被调试进程退出.
            bExit = TRUE;
        }
        ContinueDebugEvent (DbgEvent.dwProcessId, DbgEvent.dwThreadId,dwState)
    }
}

还可以选择正常创建自身后附加到进程DebugActiveProcess

int main(){
    HANDLE hMutex = OpenMutex(MUTEX _MODIFY_ STATE, FALSE, L"G1oba1\\MyMutex"){
    if (hMutex){
        printf("正被调试运行!\n");
        setchar();
    }else{
        //打开失败说明第1次运行,创建互斥体,并调试创建自身进程
        CreatelMutex(NULL,FALSE, L"Global\\MyMutex");
        TCHAR szPath[MAX PATH]={};
        GetMloduleFi1eName(NULL,szPath, MAX_PATH);
        STARTUPINFO si = { sizeof (STARTUPINFO) };
        PROCESS_INFORMATION pi = {};
        //正常创建。后面附加调试
        BOOL bStatus = CreateProcess(szPath,NULL,NULL, NULL, FALSE,CREATE_NEW_CONSOLE,NULL,NULL, &si, &pi);
        if (!bStatus){
            printf("创建进程失败!\n" );
            return 0;
        }
        if (!DebuzActiveProcess(pi.dwProcessId) {
            printf("附加进程失败!\n");
            return 0;
        }
        DEBUG_EVENT DbgEvent = { 0 };
        DWORD dwState = DBG_EXCEPTION_NOT_HANDLED;
        //等待目标exe产生调试事件
        BOOL bExit = FALSE;
        while (!bExit) {
            WaitForDebuzEvent(&DbEventINFINITE);.
            if (DbgEvent.dwDebugEventCode == EXII_PROCESS_DEBUG_EVENT){
                bExit = TRUE;
            }
            ContinueDebuzEvent (DbgEvent.dwProcessId, DbgEvent.dwThreadId, dwState)
        }
        return 0;
    }
    return 0;
}

父子进程模式

依然是同一个进程不允许同时被两个进程调试,所以父进程创建一个子进程并运行可以避免子进程被调试,绕过方法略微麻烦,但是依然可以绕过。

参考

https://www.freebuf.com/articles/others-articles/181085.html

https://www.52pojie.cn/thread-863269-1-1.html