首页
社区
课程
招聘
[原创][翻译]反调试:对象句柄
2021-6-9 15:43 6081

[原创][翻译]反调试:对象句柄

2021-6-9 15:43
6081

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



目录

  • 对象句柄

  • 1. OpenProcess()

  • 2. CreateFile()

  • 3. CloseHandle()

  • 4. LoadLibrary()

  • 5. NtQueryObject()

  • 反制措施


对象句柄
下面的一组技术代表了使用内核对象句柄来检测调试器存在的检查。一些接受内核对象句柄作为参数的WinAPI函数在调试时可能会有不同的行为,或者会因为调试器的实现而产生副作用。此外,在调试开始时,操作系统会创建特定的内核对象。
1. OpenProcess()
一些调试器可以通过在csrss.exe进程上使用kernel32!OpenProcess()函数来检测。只有当该进程的用户是管理员组的成员并且有调试权限时,该调用才会成功。
C/C++ 代码:

typedef DWORD (WINAPI *TCsrGetProcessId)(VOID);

bool Check()
{   
    HMODULE hNtdll = LoadLibraryA("ntdll.dll");
    if (!hNtdll)
        return false;
    
    TCsrGetProcessId pfnCsrGetProcessId = (TCsrGetProcessId)GetProcAddress(hNtdll, "CsrGetProcessId");
    if (!pfnCsrGetProcessId)
        return false;

    HANDLE hCsr = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pfnCsrGetProcessId());
    if (hCsr != NULL)
    {
        CloseHandle(hCsr);
        return true;
    }        
    else
        return false;
}



2. CreateFile()
当CREATE_PROCESS_DEBUG_EVENT事件发生时,被调试文件的句柄被保存在CREATE_PROCESS_DEBUG_INFO结构中。因此,调试器可以从这个文件中读取调试信息。如果这个句柄没有被调试器关闭,这个文件就不会被打开独占访问。有些调试器会忘记关闭这个句柄。

这个技巧使用kernel32!CreateFileW()(或者kernel32!CreateFileA())来独占打开当前进程的文件。如果调用失败,我们可以认为当前进程是在有调试器的情况下运行的。
C/C++ 代码:

bool Check()
{
    CHAR szFileName[MAX_PATH];
    if (0 == GetModuleFileNameA(NULL, szFileName, sizeof(szFileName)))
        return false;
    
    return INVALID_HANDLE_VALUE == CreateFileA(szFileName, GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, 0);
}


3. CloseHandle()
如果一个进程在调试器下运行,并且一个无效的句柄被传递给ntdll!NtClose()或kernel32!CloseHandle()函数,那么将引发EXCEPTION_INVALID_HANDLE(0xC0000008)异常。这个异常可以被一个异常处理程序缓存起来。如果控制被传递给异常处理程序,表明有一个调试器存在。
C/C++ 代码:

bool Check()
{
    __try
    {
        CloseHandle((HANDLE)0xDEADBEEF);
        return false;
    }
    __except (EXCEPTION_INVALID_HANDLE == GetExceptionCode()
                ? EXCEPTION_EXECUTE_HANDLER 
                : EXCEPTION_CONTINUE_SEARCH)
    {
        return true;
    }
}



4. LoadLibrary()
当使用kernel32!LoadLibraryW()(或kernel32!LoadLibraryA())函数将一个文件加载到进程内存时,LOAD_DLL_DEBUG_EVENT事件发生。被加载文件的句柄将被保存在LOAD_DLL_DEBUG_INFO结构中。因此,调试器可以从这个文件中读取调试信息。如果这个句柄没有被调试器关闭,这个文件就不会被打开独占访问。有些调试器会忘记关闭这个句柄。

为了检查调试器的存在,我们可以用kernel32!LoadLibraryA()加载任何文件,并尝试用kernel32!CreateFileA()独家打开它。如果kernel32!CreateFileA()调用失败,说明调试器是存在的。
C/C++ 代码:

bool Check()
{
    CHAR szBuffer[] = { "C:\\Windows\\System32\\calc.exe" };
    LoadLibraryA(szBuffer);
    return INVALID_HANDLE_VALUE == CreateFileA(szBuffer, GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL);
}



5. NtQueryObject()
当一个调试会话开始时,一个叫做 “debug object”的内核对象被创建,并有一个句柄与之关联。使用ntdll!NtQueryObject()函数,可以查询现有对象的列表,并检查与任何存在的调试对象相关的句柄数量。

然而这个技术不能确定当前进程是否正在被调试。它只能显示自系统启动以来,调试器是否在系统上运行。
C/C++ 代码:

typedef struct _OBJECT_TYPE_INFORMATION
{
    UNICODE_STRING TypeName;
    ULONG TotalNumberOfHandles;
    ULONG TotalNumberOfObjects;
} OBJECT_TYPE_INFORMATION, *POBJECT_TYPE_INFORMATION;

typedef struct _OBJECT_ALL_INFORMATION
{
    ULONG NumberOfObjects;
    OBJECT_TYPE_INFORMATION ObjectTypeInformation[1];
} OBJECT_ALL_INFORMATION, *POBJECT_ALL_INFORMATION;

typedef NTSTATUS (WINAPI *TNtQueryObject)(
    HANDLE                   Handle,
    OBJECT_INFORMATION_CLASS ObjectInformationClass,
    PVOID                    ObjectInformation,
    ULONG                    ObjectInformationLength,
    PULONG                   ReturnLength
);

enum { ObjectAllTypesInformation = 3 };

#define STATUS_INFO_LENGTH_MISMATCH 0xC0000004

bool Check()
{
    bool bDebugged = false;
    NTSTATUS status;
    LPVOID pMem = nullptr;
    ULONG dwMemSize;
    POBJECT_ALL_INFORMATION pObjectAllInfo;
    PBYTE pObjInfoLocation;
    HMODULE hNtdll;
    TNtQueryObject pfnNtQueryObject;
    
    hNtdll = LoadLibraryA("ntdll.dll");
    if (!hNtdll)
        return false;
        
    pfnNtQueryObject = (TNtQueryObject)GetProcAddress(hNtdll, "NtQueryObject");
    if (!pfnNtQueryObject)
        return false;

    status = pfnNtQueryObject(
        NULL,
        (OBJECT_INFORMATION_CLASS)ObjectAllTypesInformation,
        &dwMemSize, sizeof(dwMemSize), &dwMemSize);
    if (STATUS_INFO_LENGTH_MISMATCH != status)
        goto NtQueryObject_Cleanup;

    pMem = VirtualAlloc(NULL, dwMemSize, MEM_COMMIT, PAGE_READWRITE);
    if (!pMem)
        goto NtQueryObject_Cleanup;

    status = pfnNtQueryObject(
        (HANDLE)-1,
        (OBJECT_INFORMATION_CLASS)ObjectAllTypesInformation,
        pMem, dwMemSize, &dwMemSize);
    if (!SUCCEEDED(status))
        goto NtQueryObject_Cleanup;

    pObjectAllInfo = (POBJECT_ALL_INFORMATION)pMem;
    pObjInfoLocation = (PBYTE)pObjectAllInfo->ObjectTypeInformation;
    for(UINT i = 0; i < pObjectAllInfo->NumberOfObjects; i++)
    {

        POBJECT_TYPE_INFORMATION pObjectTypeInfo =
            (POBJECT_TYPE_INFORMATION)pObjInfoLocation;

        if (wcscmp(L"DebugObject", pObjectTypeInfo->TypeName.Buffer) == 0)
        {
            if (pObjectTypeInfo->TotalNumberOfObjects > 0)
                bDebugged = true;
            break;
        }

        // Get the address of the current entries
        // string so we can find the end
        pObjInfoLocation = (PBYTE)pObjectTypeInfo->TypeName.Buffer;

        // Add the size
        pObjInfoLocation += pObjectTypeInfo->TypeName.Length;

        // Skip the trailing null and alignment bytes
        ULONG tmp = ((ULONG)pObjInfoLocation) & -4;

        // Not pretty but it works
        pObjInfoLocation = ((PBYTE)tmp) + sizeof(DWORD);
    }

NtQueryObject_Cleanup:
    if (pMem)
        VirtualFree(pMem, 0, MEM_RELEASE);

    return bDebugged;
}



反制措施
反制这些检查的最简单的方法是只需手动跟踪程序直到检查,然后跳过它(例如用NOP补丁或改变指令指针或在检查后改变零标志寄存器)。

如果你写一个反调试方案,你需要拦截列出的函数,在分析其输入后改变返回值:

  • ntdll!OpenProcess:如果第三个参数是csrss.exe的句柄,则返回NULL。

  • ntdll!NtClose:你可以使用ntdll!NtQueryObject()检查是否可以检索到关于输入句柄的任何信息,如果句柄无效,则不显示异常。

  • ntdll!NtQueryObject: 如果ObjectAllTypesInformation类被查询,从结果中过滤调试对象。


以下技术应该在没有拦截的情况下处理:

  • ntdll!NtCreateFile: 太通用了,无法反制。然而,如果你为一个特定的调试器写一个插件,你可以确保被调试文件的句柄被关闭。

  • kernel32!LoadLibraryW/A:没有反制措施。



[CTF入门培训]顶尖高校博士及硕士团队亲授《30小时教你玩转CTF》,视频+靶场+题目!助力进入CTF世界

收藏
点赞4
打赏
分享
最新回复 (1)
雪    币: 1501
活跃值: (3260)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
小希希 2021-6-17 11:04
2
0
感谢分享
游客
登录 | 注册 方可回帖
返回