首页
论坛
课程
招聘
[原创][翻译]反调试:进程内存
2021-6-22 15:56 6121

[原创][翻译]反调试:进程内存

2021-6-22 15:56
6121

备注
原文地址:https://anti-debug.checkpoint.com/techniques/process-memory.html
原文标题:Anti-Debug: Process Memory
更新日期:2021年6月22日
此文后期:根据自身所学进行内容扩充
因自身技术有限,只能尽自身所能翻译国外技术文章,供大家学习,若有不当或可完善的地方,希望可以指出,用于共同完善这篇文章。


目录

  • 进程内存
  • 1.断点
  • 1.1.软件断点(INT3)
  • 1.2.反步过
  • 1.2.1.直接修改内存
  • 1.2.2. ReadFile()
  • 1.2.3. WriteProcessMemory()
  • 1.2.4. Toolhelp32ReadProcessMemory()
  • 1.3.内存断点
  • 1.4.硬件断点
  • 2.其他内存检查
  • 2.1.NtQueryVirtualMemory()
  • 2.2.检测一个函数补丁
  • 2.3.  ntdll!DbgBreakPoint()补丁
  • 2.4.  ntdll!DbgUiRemoteBreakin()补丁
  • 2.5执行代码校验和
  • 反制措施

进程内存
进程可以检查自己的内存,以检测调试器的存在或干扰调试器。

本节包括进程内存和检查线程上下文、搜索断点以及作为反附加方法的函数修补。

1.断点
我们总是可以检查进程内存并在代码中搜索软件断点,或者检查CPU的调试寄存器以确定是否设置了硬件断点。

1.1.软件断点(INT3)
其思想是将某些函数的机器码识别为0xCC字节,代表INT 3汇编指令。

这种方法会产生许多误报情况,因此应谨慎使用。
C/C++ 代码:

bool CheckForSpecificByte(BYTE cByte, PVOID pMemory, SIZE_T nMemorySize = 0)
{
    PBYTE pBytes = (PBYTE)pMemory; 
    for (SIZE_T i = 0; ; i++)
    {
        // Break on RET (0xC3) if we don't know the function's size
        if (((nMemorySize > 0) && (i >= nMemorySize)) ||
            ((nMemorySize == 0) && (pBytes[i] == 0xC3)))
            break;

        if (pBytes[i] == cByte)
            return true;
    }
    return false;
}

bool IsDebugged()
{
    PVOID functionsToCheck[] = {
        &Function1,
        &Function2,
        &Function3,
    };
    for (auto funcAddr : functionsToCheck)
    {
        if (CheckForSpecificByte(0xCC, funcAddr))
            return true;
    }
    return false;
}


1.2.反步过
调试器允许你步过函数调用。在这种情况下,调试器隐含地在调用后的指令(即被调用函数的返回地址)上设置一个软件断点。

为了检测是否有跨函数的企图,我们可以检查返回地址的第一个字节的内存。如果软件断点(0xCC)位于返回地址处,我们可以用一些其他指令(如NOP)来修补它。这很可能会破坏代码并使进程崩溃。另一方面,我们可以用一些有意义的代码代替NOP来修补返回地址,改变程序的控制流程。


1.2.1.直接修改内存
可以从函数内部检查调用该函数后是否有软件断点。我们可以在返回地址读取一个字节,如果该字节等于0xCC (INT 3),它可以被0x90 (NOP)重写。这个过程可能会崩溃,因为我们在返回地址破坏了指令。但是,如果知道函数调用之后是哪条指令,可以用这条指令的第一个字节重写断点。
C/C++ 代码:

#include <intrin.h>
#pragma intrinsic(_ReturnAddress)

void foo()
{
    // ...
    
    PVOID pRetAddress = _ReturnAddress();
    if (*(PBYTE)pRetAddress == 0xCC) // int 3
    {
        DWORD dwOldProtect;
        if (VirtualProtect(pRetAddress, 1, PAGE_EXECUTE_READWRITE, &dwOldProtect))
        {
            *(PBYTE)pRetAddress = 0x90; // nop
            VirtualProtect(pRetAddress, 1, dwOldProtect, &dwOldProtect);
        }
    }
    
    // ...
}


1.2.2. ReadFile()
该方法使用kernel32!ReadFile()函数来修补返回地址的代码。

其思路是读取当前进程的可执行文件,并将返回地址作为输出缓冲区传递给kernel32!ReadFile()。返回地址的字节将被打上'M'字符(PE图像的第一个字节),进程可能会崩溃。
C/C++ 代码:

#include <intrin.h>
#pragma intrinsic(_ReturnAddress)

void foo()
{
    // ...
    
    PVOID pRetAddress = _ReturnAddress();
    if (*(PBYTE)pRetAddress == 0xCC) // int 3
    {
        DWORD dwOldProtect, dwRead;
        CHAR szFilePath[MAX_PATH];
        HANDLE hFile;

        if (VirtualProtect(pRetAddress, 1, PAGE_EXECUTE_READWRITE, &dwOldProtect))
        {
            if (GetModuleFileNameA(NULL, szFilePath, MAX_PATH))
            {
                hFile = CreateFileA(szFilePath, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
                if (INVALID_HANDLE_VALUE != hFile)
                    ReadFile(hFile, pRetAddress, 1, &dwRead, NULL);
            }
            VirtualProtect(pRetAddress, 1, dwOldProtect, &dwOldProtect);
        }
    }
    
    // ...
}


1.2.3. WriteProcessMemory()
这个方法使用kernel32!WriteProcessMemory()函数来修补返回地址的代码。
C/C++ 代码:

#include <intrin.h>
#pragma intrinsic(_ReturnAddress)

void foo()
{
    // ...
    
    BYTE Patch = 0x90;
    PVOID pRetAddress = _ReturnAddress();
    if (*(PBYTE)pRetAddress == 0xCC)
    {
        DWORD dwOldProtect;
        if (VirtualProtect(pRetAddress, 1, PAGE_EXECUTE_READWRITE, &dwOldProtect))
        {
            WriteProcessMemory(GetCurrentProcess(), pRetAddress, &Patch, 1, NULL);
            VirtualProtect(pRetAddress, 1, dwOldProtect, &dwOldProtect);
        }
    }
    
    // ...
}


1.2.4. Toolhelp32ReadProcessMemory()
函数kernel32!Toolhelp32ReadProcessMemory()允许你读取其他进程的内存。但是,它可以用于检查反步过情况。
C/C++ 代码:

#include <TlHelp32.h>

bool foo()
{
    // ..
    
    PVOID pRetAddress = _ReturnAddress();
    BYTE uByte;
    if (FALSE != Toolhelp32ReadProcessMemory(GetCurrentProcessId(), _ReturnAddress(), &uByte, sizeof(BYTE), NULL))
    {
        if (uByte == 0xCC)
            ExitProcess(0);
    }
    
    // ..
}


1.3.内存断点
内存断点是通过使用保护页实现的(至少在OllyDbg和ImmunityDebugger中是这样)。保护页为内存页访问提供一次性警报。当一个保护页被执行时,异常STATUS_GUARD_PAGE_VIOLATION 被引发。
通过在kernel32!VirtualAlloc()、kernel32!VirtualAllocEx()、kernel32!VirtualProtect()和kernel32!VirtualProtect()函数中设置PAGE_GUARD页面保护修改器,可以创建一个保护页面。
但是,我们可以滥用调试器实现内存断点的方式来检查程序是否在调试器下执行。我们可以分配一个只包含一个字节0xC3(表示RET指令)的可执行缓冲区。然后,我们将这个缓冲区标记为保护页,将处理存在调试器的情况的地址推送到堆栈中,然后跳转到分配的缓冲区。指令RET将被执行,如果调试器(OllyDbg或ImmunityDebugger)存在,我们将获得推送到堆栈的地址。如果程序在没有调试器的情况下执行,我们将得到一个异常处理程序。
C/C++ 代码:

bool IsDebugged()
{
    DWORD dwOldProtect = 0;
    SYSTEM_INFO SysInfo = { 0 };

    GetSystemInfo(&SysInfo);
    PVOID pPage = VirtualAlloc(NULL, SysInfo.dwPageSize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); 
    if (NULL == pPage)
        return false; 

    PBYTE pMem = (PBYTE)pPage;
    *pMem = 0xC3; 

    // Make the page a guard page         
    if (!VirtualProtect(pPage, SysInfo.dwPageSize, PAGE_EXECUTE_READWRITE | PAGE_GUARD, &dwOldProtect))
        return false;

    __try
    {
        __asm
        {
            mov eax, pPage
            push mem_bp_being_debugged
            jmp eax
        }
    }
    __except(EXCEPTION_EXECUTE_HANDLER)
    {
        VirtualFree(pPage, NULL, MEM_RELEASE);
        return false;
    }

mem_bp_being_debugged:
    VirtualFree(pPage, NULL, MEM_RELEASE);
    return true;
}


1.4.硬件断点
调试寄存器DR0、DR1、DR2和DR3可以从线程上下文中检索到。如果它们包含非零值,可能意味着该进程是在调试器下执行的,并且设置了一个硬件断点。
C/C++ 代码:

bool IsDebugged()
{
    CONTEXT ctx;
    ZeroMemory(&ctx, sizeof(CONTEXT)); 
    ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS; 

    if(!GetThreadContext(GetCurrentThread(), &ctx))
        return false;

    return ctx.Dr0 || ctx.Dr1 || ctx.Dr2 || ctx.Dr3;
}


2.其他内存检查
本节包含直接检查或控制正在运行的进程的虚拟内存以检测或阻止调试的技术。


2.1.NtQueryVirtualMemory()
代码所在进程的内存页在所有进程之间共享,直到写入一个页面。之后,操作系统会对这个页面进行复制,并将其映射到进程的虚拟内存中,所以这个页面不再是 "共享 "的。

因此,我们可以查询当前进程的工作集,用代码检查工作集块中的共享和ShareCount字段的页面。如果代码中存在软件断点,这些字段一定不会被设置。
NTDLL declarations:

namespace ntdll
{
//...

#define STATUS_INFO_LENGTH_MISMATCH 0xC0000004

// ...

typedef enum _MEMORY_INFORMATION_CLASS {
    MemoryBasicInformation,
    MemoryWorkingSetList,
} MEMORY_INFORMATION_CLASS;

// ...

typedef union _PSAPI_WORKING_SET_BLOCK {
    ULONG Flags;
    struct {
        ULONG Protection :5;
        ULONG ShareCount :3;
        ULONG Shared     :1;
        ULONG Reserved   :3;
        ULONG VirtualPage:20;
    };
} PSAPI_WORKING_SET_BLOCK, *PPSAPI_WORKING_SET_BLOCK;

typedef struct _MEMORY_WORKING_SET_LIST
{
    ULONG NumberOfPages;
    PSAPI_WORKING_SET_BLOCK WorkingSetList[1];
} MEMORY_WORKING_SET_LIST, *PMEMORY_WORKING_SET_LIST;

// ...
}


C/C++ 代码:

bool IsDebugged()
{
#ifndef _WIN64
    NTSTATUS status;
    PBYTE pMem = nullptr;
    DWORD dwMemSize = 0;

    do
    {
        dwMemSize += 0x1000;
        pMem = (PBYTE)_malloca(dwMemSize);
        if (!pMem)
            return false;

        memset(pMem, 0, dwMemSize);
        status = ntdll::NtQueryVirtualMemory(
            GetCurrentProcess(), 
            NULL, 
            ntdll::MemoryWorkingSetList, 
            pMem, 
            dwMemSize, 
            NULL);
    } while (status == STATUS_INFO_LENGTH_MISMATCH);

    ntdll::PMEMORY_WORKING_SET_LIST pWorkingSet = (ntdll::PMEMORY_WORKING_SET_LIST)pMem;
    for (ULONG i = 0; i < pWorkingSet->NumberOfPages; i++)
    {
        DWORD dwAddr = pWorkingSet->WorkingSetList[i].VirtualPage << 0x0C;
        DWORD dwEIP = 0;
        __asm
        {
            push eax
            call $+5
            pop eax
            mov dwEIP, eax
            pop eax
        }

        if (dwAddr == (dwEIP & 0xFFFFF000))
            return (pWorkingSet->WorkingSetList[i].Shared == 0) || (pWorkingSet->WorkingSetList[i].ShareCount == 0);
    }
#endif // _WIN64
    return false;
}

这项技术归功于:Virus Bulletin

2.2.检测一个函数补丁
检测调试器的一个常用方法是调用kernel32!IsDebuggerPresent()。缓解这种检查很简单,例如,改变EAX寄存器中的结果或修补kernel32!IsDebuggerPresent()函数的代码。

因此,我们可以不检查进程内存的断点,而是验证kernel32!IsDebuggerPresent()是否被修改。我们可以读取这个函数的第一个字节,并与其他进程中相同函数的这些字节进行比较。即使启用了ASLR,Windows库在所有进程中都被加载到相同的基础地址。基准地址只有在重启后才会改变,但对所有进程来说,它们在会话期间会保持不变。
C/C++ 代码:

bool IsDebuggerPresent()
{
    HMODULE hKernel32 = GetModuleHandleA("kernel32.dll");
    if (!hKernel32)
        return false;

    FARPROC pIsDebuggerPresent = GetProcAddress(hKernel32, "IsDebuggerPresent");
    if (!pIsDebuggerPresent)
        return false;

    HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    if (INVALID_HANDLE_VALUE == hSnapshot)
        return false;

    PROCESSENTRY32W ProcessEntry;
    ProcessEntry.dwSize = sizeof(PROCESSENTRY32W);

    if (!Process32FirstW(hSnapshot, &ProcessEntry))
        return false;

    bool bDebuggerPresent = false;
    HANDLE hProcess = NULL;
    DWORD dwFuncBytes = 0;
    const DWORD dwCurrentPID = GetCurrentProcessId();
    do
    {
        __try
        {
            if (dwCurrentPID == ProcessEntry.th32ProcessID)
                continue;

            hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, ProcessEntry.th32ProcessID);
            if (NULL == hProcess)
                continue;

            if (!ReadProcessMemory(hProcess, pIsDebuggerPresent, &dwFuncBytes, sizeof(DWORD), NULL))
                continue;

            if (dwFuncBytes != *(PDWORD)pIsDebuggerPresent)
            {
                bDebuggerPresent = true;
                break;
            }
        }
        __finally
        {
            if (hProcess)
                CloseHandle(hProcess);
        }
    } while (Process32NextW(hSnapshot, &ProcessEntry));

    if (hSnapshot)
        CloseHandle(hSnapshot);
    return bDebuggerPresent;
}


这项技术归功于:Rouse_

2.3.  ntdll!DbgBreakPoint()补丁
函数ntdll!DbgBreakPrint()有以下执行情况:

当调试器附加在一个正在运行的进程上时,它被调用。它允许调试器获得控制权,因为一个异常被引发,它可以拦截。如果我们擦除ntdll!DbgBreakPrint()里面的断点,调试器就不会闯入,线程就会退出。
C/C++ 代码:

void Patch_DbgBreakPoint()
{
    HMODULE hNtdll = GetModuleHandleA("ntdll.dll");
    if (!hNtdll)
        return;

    FARPROC pDbgBreakPoint = GetProcAddress(hNtdll, "DbgBreakPoint");
    if (!pDbgBreakPoint)
        return;

    DWORD dwOldProtect;
    if (!VirtualProtect(pDbgBreakPoint, 1, PAGE_EXECUTE_READWRITE, &dwOldProtect))
        return;

    *(PBYTE)pDbgBreakPoint = (BYTE)0xC3; // ret
}


2.4.  ntdll!DbgUiRemoteBreakin()补丁
当调试器调用kernel32!DebugActiveProcess()时,调试器会相应地调用ntdll!DbgUiRemoteBreakin()。为了防止调试器附着在进程上,我们可以修补ntdll!DbgUiRemoteBreakin()代码,以调用kernel32!TerminateProcess()。
在下面的例子中,我们用以下代码修补了ntdll!DbgUiRemoteBreakin():

6A 00             push 0
68 FF FF FF FF    push -1 ; GetCurrentProcess() result
B8 XX XX XX XX    mov  eax, kernel32!TreminateProcess
FF D0             call eax

结果是,一旦我们试图将调试器连接到该程序,该程序将自行终止。
C/C++ 代码:

#pragma pack(push, 1)
    struct DbgUiRemoteBreakinPatch
    {
        WORD  push_0;
        BYTE  push;
        DWORD CurrentPorcessHandle;
        BYTE  mov_eax;
        DWORD TerminateProcess;
        WORD  call_eax;
    };
#pragma pack(pop)

void Patch_DbgUiRemoteBreakin()
{
    HMODULE hNtdll = GetModuleHandleA("ntdll.dll");
    if (!hNtdll)
        return;

    FARPROC pDbgUiRemoteBreakin = GetProcAddress(hNtdll, "DbgUiRemoteBreakin");
    if (!pDbgUiRemoteBreakin)
        return;

    HMODULE hKernel32 = GetModuleHandleA("kernel32.dll");
    if (!hKernel32)
        return;

    FARPROC pTerminateProcess = GetProcAddress(hKernel32, "TerminateProcess");
    if (!pTerminateProcess)
        return;

    DbgUiRemoteBreakinPatch patch = { 0 };
    patch.push_0 = '\x6A\x00';
    patch.push = '\x68';
    patch.CurrentPorcessHandle = 0xFFFFFFFF;
    patch.mov_eax = '\xB8';
    patch.TerminateProcess = (DWORD)pTerminateProcess;
    patch.call_eax = '\xFF\xD0';

    DWORD dwOldProtect;
    if (!VirtualProtect(pDbgUiRemoteBreakin, sizeof(DbgUiRemoteBreakinPatch), PAGE_READWRITE, &dwOldProtect))
        return;

    ::memcpy_s(pDbgUiRemoteBreakin, sizeof(DbgUiRemoteBreakinPatch),
        &patch, sizeof(DbgUiRemoteBreakinPatch));
    VirtualProtect(pDbgUiRemoteBreakin, sizeof(DbgUiRemoteBreakinPatch), dwOldProtect, &dwOldProtect);
}


该项技术归公于:Rouse_

2.5执行代码校验和
验证代码校验和是检测软件断点、调试器的跨步、函数的内联拦截或数据修改的可靠方法。

下面的例子显示了如何验证一个函数的校验和。
C/C++ 代码:

PVOID g_pFuncAddr;
DWORD g_dwFuncSize;
DWORD g_dwOriginalChecksum;

static void VeryImportantFunction()
{
    // ...
}

static DWORD WINAPI ThreadFuncCRC32(LPVOID lpThreadParameter)
{
    while (true)
    {
        if (CRC32((PBYTE)g_pFuncAddr, g_dwFuncSize) != g_dwOriginalChecksum)
            ExitProcess(0);
        Sleep(10000);
    }
    return 0;
}

size_t DetectFunctionSize(PVOID pFunc)
{
    PBYTE pMem = (PBYTE)pFunc;
    size_t nFuncSize = 0;
    do
    {
        ++nFuncSize;
    } while (*(pMem++) != 0xC3);
    return nFuncSize;
}

int main()
{
    g_pFuncAddr = (PVOID)&VeryImportantFunction;
    g_dwFuncSize = DetectFunctionSize(g_pFuncAddr);
    g_dwOriginalChecksum = CRC32((PBYTE)g_pFuncAddr, g_dwFuncSize);
    
    HANDLE hChecksumThread = CreateThread(NULL, NULL, ThreadFuncCRC32, NULL, NULL, NULL);
    
    // ...
    
    return 0;
}


反制措施
调试期间:

  • 对于反步过的技巧:进入执行步过检查的函数,并一直执行到结束(OllyDbg/x32/x64dbg中的Ctrl+F9)。

  • 反制所有 "内存 "技巧(包括反步过)的最好方法是找到确切的检查,并用NOP来修补它,或者设置允许应用程序进一步执行的返回值。

对于反调试绕过工具的开发:
1.断点扫描:

  • 软件断点与反反调试:没有干扰这些检查的可能性,因为它们不需要使用API,直接访问内存。

  • 内存断点:一般来说,有可能跟踪被调用的函数序列来应用这种检查。

  • 硬件断点:钩住kernel32!GetThreadContext()并修改调试寄存器。

2.其他检查:没有反制措施。



看雪2022 KCTF 秋季赛 防守篇规则,征题截止日期11月12日!(iPhone 14等你拿!)

最后于 2021-6-23 17:56 被梦幻的彼岸编辑 ,原因:
收藏
点赞2
打赏
分享
最新回复 (3)
雪    币: 617
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
0xjoe 活跃值 2021-6-29 22:27
2
0
终于知道怎样不崩溃了,谢谢大大的翻译!
雪    币: 26
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
年年有余. 活跃值 2021-7-15 22:03
3
0
好多干货!已关注!Thanks
雪    币: 75
活跃值: 活跃值 (444)
能力值: ( LV2,RANK:15 )
在线值:
发帖
回帖
粉丝
琳宇 活跃值 2021-8-9 00:48
4
0
2.1的原理,不是很明白,如果作者有空闲时间,希望能给我回复一下,谢谢
游客
登录 | 注册 方可回帖
返回