【Day 12】卑鄙源之 Hook (下) - 侦测 Hook

环境

  • Windows 10 21H1
  • Visual Studio 2019

前情提要

【Day 11】卑鄙源之 Hook (上) - 侦测 Hook 我们提到可以比对档案与记忆体来判断函数是否有被 Hook,也透过几个 Windows API 把目标 Process iexplore.exe 中的目标 Module WININET.DLL 找出来并取得 Handle。

这一篇要来处理档案的部分,同样找到档案中的 WININET.DLL,然後比对档案与记忆体的差异,透过两者一不一样判断有没有被 Hook。

将档案内容 Map 到 Process 中

首先我们要知道 WININET.DLL 的完整路径是 C:\Windows\SysWOW64\wininet.dll,注意这边不是 C:\Windows\System32\wininet.dll,因为之前实作的目标是 32-bit Process。

既然已经知道档案路径,接下来只要

  1. 取得档案的 Handle
  2. 建立 Mapping 物件
  3. 把档案内容载到当前的 Process 中

这三步分别对应到三个函数,CreateFileCreateFileMappingMapViewOfFile,如此就可以读取档案内容。

读取 PE 的结构找到目标 Export Function

RVA

目前已经可以读取档案与记忆体的内容,接下来要找到目标 Function HttpSendRequestW 的 RVA。

RVA 全名为 Relative Virtual Address,也就是与 Image Base 的距离。假设目前 wininet.dll 的 Image Base 是 0x71280000,HttpSendRequestW 的位址是 0x7159B7C0,则 HttpSendRequestW 的 RVA 就是 0x7159B7C0 - 0x71280000 = 0x31B7C0

这边要注意的是 RVA 不等於 File Offset 哦,File Offset = RVA - Virtual Offset + Raw Offset

PE 结构

PE 结构的部分其实已经在 【Day 07】欢迎来到实力至上主义的 Shellcode (上) - Windows x86 Shellcode【Day 08】欢迎来到实力至上主义的 Shellcode (下) - Windows x86 Shellcode 解释过,但是这次是要用 C/C++ 访问 PE 结构,所以这边再快速说明一次。

首先,Image Base 的位址就是 IMAGE_DOS_HEADER 的开头,IMAGE_DOS_HEADER 的结构如下。可以透过其中的 e_lfanew 算出 IMAGE_NT_HEADERSIMAGE_NT_HEADERS = IMAGE_DOS_HEADER + e_lfanew

typedef struct _IMAGE_DOS_HEADER
{
     WORD e_magic;
     WORD e_cblp;
     WORD e_cp;
     WORD e_crlc;
     WORD e_cparhdr;
     WORD e_minalloc;
     WORD e_maxalloc;
     WORD e_ss;
     WORD e_sp;
     WORD e_csum;
     WORD e_ip;
     WORD e_cs;
     WORD e_lfarlc;
     WORD e_ovno;
     WORD e_res[4];
     WORD e_oemid;
     WORD e_oeminfo;
     WORD e_res2[10];
     LONG e_lfanew;
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

IMAGE_NT_HEADERS 的结构如下。在这里我们需要的是 OptionalHeader,因为它可以帮助我们找到 IMAGE_DATA_DIRECTORY 结构,再利用其中的 VirtualAddress 算出 IMAGE_EXPORT_DIRECTORY。

typedef struct _IMAGE_NT_HEADERS {
  DWORD                   Signature;
  IMAGE_FILE_HEADER       FileHeader;
  IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;

找到 IMAGE_EXPORT_DIRECTORY 的用意是为了找到重要的三个 Table,分别是 Function Table、Ordinal Table、Name Table,IMAGE_EXPORT_DIRECTORY 结构如下,三个 Table 的 RVA 分别是 AddressOfFunctions、AddressOfNames、AddressOfNameOrdinals。

Name Table 中存放着所有 Export Function 名称字串的 RVA,找到目标函数名称後,拿对应的 Name Table 的 Index 去找 Ordinal Table。再把 Ordinal Table 对应的值当作 Index 去找 Function Table,得到的值就是目标 Function 的 RVA。

typedef struct _IMAGE_EXPORT_DIRECTORY {
    DWORD   Characteristics;
    DWORD   TimeDateStamp;
    WORD    MajorVersion;
    WORD    MinorVersion;
    DWORD   Name;
    DWORD   Base;
    DWORD   NumberOfFunctions;
    DWORD   NumberOfNames;
    DWORD   AddressOfFunctions;     // RVA from base of image
    DWORD   AddressOfNames;         // RVA from base of image
    DWORD   AddressOfNameOrdinals;  // RVA from base of image
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

实作

实作流程

  1. 把 wininet.dll 档案 Map 到目前的 Process 中
    • 取得档案的 Handle
    • 建立 Mapping 物件
    • 把档案内容载到当前的 Process 中
  2. 读取 wininet.dll 档案的 PE 结构,取得 HttpSendRequestW 的 RVA
    • 取得 IMAGE_DOS_HEADER 结构後,接着一直找其他 Header,直到找出 IMAGE_EXPORT_DIRECTORY
    • 从 IMAGE_EXPORT_DIRECTORY 找出 Function Table、Ordinal Table、Name Table
    • 对 Name Table 回圈找到 HttpSendRequestW,找到後透过 Function Table、Ordinal Table 取得 RVA
  3. 比对档案与 iexplore.exe 的 HttpSendRequestW 是否相同
    • 用 ReadProcessMemory 读取 iexplore.exe 的 HttpSendRequestW 函数的前 5 Bytes
    • 用 memcmp 比对档案和记忆体的 HttpSendRequestW 一不一样

POC

完整的程序专案可以参考我的 GitHub zeze-zeze/2021iThome

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

int main(int argc, char* argv[]) {
    // 开启目标 Process (iexplore.exe 的 pid)
    HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, 17704);
    if (!hProcess) {
        printf("OpenProcess failed: error %d\n", GetLastError());
        return 1;
    }

    // 用 EnumProcessModules 取得所有的 Module Handle
    HMODULE hMods[1024], hModule = NULL;
    DWORD cbNeeded;
    if (EnumProcessModules(hProcess, hMods, sizeof(hMods), &cbNeeded)) {
        for (int i = 0; i < (int)(cbNeeded / sizeof(HMODULE)); i++) {
            TCHAR szModPathName[MAX_PATH] = { 0 };

            // 用 GetModuleFileNameEx 取得目前的 Module Name
            if (GetModuleFileNameEx(hProcess, hMods[i], szModPathName, sizeof(szModPathName) / sizeof(TCHAR))) {
                // 判断是不是目标 (WININET),是的话就记录下来
                std::wstring sMod = szModPathName;
                if (sMod.find(L"WININET") != std::string::npos) {
                    hModule = hMods[i];
                }
            }
            else {
                printf("GetModuleFileNameEx failed: error %d\n", GetLastError());
                return NULL;
            }
        }
    }
    else {
        printf("EnumProcessModulesEx failed: error %d\n", GetLastError());
        return 1;
    }
    if (hModule == NULL) {
        printf("Cannot find target module\n");
        return 1;
    }

    // 用 GetModuleInformation 取得 Module 资讯
    MODULEINFO lpmodinfo;
    if (!GetModuleInformation(hProcess, hModule, &lpmodinfo, sizeof(MODULEINFO))) {
        printf("GetModuleInformation failed: error %d\n", GetLastError());
        return 1;
    }
    /* 以上是卑鄙源之 Hook (上) 的内容 */

    /* 以下是卑鄙源之 Hook (下) 的内容 */
    // 1. 把 wininet.dll 档案 Map 到目前的 Process 中
    // 取得档案的 Handle
    HANDLE hFile = CreateFile(L"C:\\Windows\\SysWOW64\\wininet.dll", GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_READONLY, NULL);
    if (hFile == INVALID_HANDLE_VALUE) {
        printf("CreateFile failed: error %d\n", GetLastError());
        return NULL;
    }

    // 建立 Mapping 物件
    HANDLE file_map = CreateFileMapping(hFile, NULL, PAGE_READONLY | SEC_IMAGE, 0, 0, L"KernelMap");
    if (!file_map) {
        printf("CreateFileMapping failed: error %d\n", GetLastError());
        return NULL;
    }

    // 把档案内容载到当前的 Process 中
    LPVOID file_image = MapViewOfFile(file_map, FILE_MAP_READ, 0, 0, 0);
    if (file_image == 0) {
        printf("MapViewOfFile failed: error %d\n", GetLastError());
        return NULL;
    }

    // 2. 读取 wininet.dll 档案的 PE 结构,取得 HttpSendRequestW 的 RVA
    DWORD RVA = 0;

    // 取得 IMAGE_DOS_HEADER 结构後,接着一直找其他 Header,直到找出 IMAGE_EXPORT_DIRECTORY
    PIMAGE_DOS_HEADER pDos_hdr = (PIMAGE_DOS_HEADER)file_image;
    PIMAGE_NT_HEADERS pNt_hdr = (PIMAGE_NT_HEADERS)((char*)file_image + pDos_hdr->e_lfanew);
    IMAGE_OPTIONAL_HEADER opt_hdr = pNt_hdr->OptionalHeader;
    IMAGE_DATA_DIRECTORY exp_entry = opt_hdr.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT];
    PIMAGE_EXPORT_DIRECTORY pExp_dir = (PIMAGE_EXPORT_DIRECTORY)((char*)file_image + exp_entry.VirtualAddress);

    // 从 IMAGE_EXPORT_DIRECTORY 找出 Function Table、Ordinal Table、Name Table
    DWORD* func_table = (DWORD*)((char*)file_image + pExp_dir->AddressOfFunctions);
    WORD* ord_table = (WORD*)((char*)file_image + pExp_dir->AddressOfNameOrdinals);
    DWORD* name_table = (DWORD*)((char*)file_image + pExp_dir->AddressOfNames);

    // 对 Name Table 回圈找到 HttpSendRequestW,找到後透过 Function Table、Ordinal Table 取得 RVA
    for (int i = 0; i < (int)pExp_dir->NumberOfNames; i++) {
        if (strcmp("HttpSendRequestW", (const char*)file_image + (DWORD)name_table[i]) == 0) {
            RVA = (DWORD)func_table[ord_table[i]];
        }
    }
    if (!RVA) {
        printf("Failed to find target function\n");
    }

    // 3. 比对档案与 iexplore.exe 的 HttpSendRequestW 是否相同
    // 用 ReadProcessMemory 读取 iexplore.exe 的 HttpSendRequestW 函数的前 5 Bytes
    TCHAR* lpBuffer = new TCHAR[6]{ 0 };
    if (!ReadProcessMemory(hProcess, (LPCVOID)((DWORD)lpmodinfo.lpBaseOfDll + RVA), lpBuffer, 5, NULL)) {
        printf("ReadProcessMemory failed: error %d\n", GetLastError());
        return -1;
    }

    // 用 memcmp 比对档案和记忆体的 HttpSendRequestW 一不一样
    if (memcmp((LPVOID)((DWORD)file_image + RVA), (LPVOID)((DWORD)lpBuffer), 5) == 0) {
        printf("Not Hook\n");
        return 0;
    }
    else {
        printf("Hook\n");
        return 1;
    }

    return 0;
}

实际测试

记得把 pid 换成自己环境测试的 iexplore.exe 的 pid。

拿之前做的 Hook IE 的 POC 来测试,在 Hook 之前,程序应该会输出 Not Hook;在执行 Hook 之後,应该会输出 Hook

道高一尺,魔高一丈

可能有些人马上就想到了绕过方法,因为这篇给的 POC 只检查了函数的前 5 Bytes,所以只要把 Hook 设在中间,这篇给的 POC 不就没用了吗。没错,这招就是 Mid Function Hook,拿这招去绕 GitHub 专案 HookHunter 与 PCHunter 也能成功,因为大部分情况都只会检查前几个 Bytes。

但是 Mid Function Hook 会提高红队恶意程序的开发成本与降低程序稳定性,而且篮队也可能选择牺牲效能检查整个函数。所以 Hook 与侦测 Hook 就感觉演变成像是一种猫捉老鼠的游戏。


<<:  DAY15 - 第四个小范例 : Line股价机器人

>>:  [Day12] TS:什麽!型别还有递回(recursion)的概念?用组合技实作 SnakeToCamelCase

【Day 10】- 你的爬虫是哪一类的? (网路爬虫的类型)

前情提要 前一篇文章带大家看了 BeautifulSoup 库的使用,用他来做资料清洗,使我们真正想...

[Day 23] 从GEIT制定管理规范

这个章节很多都在CISSP有,很多邦友写过,可以参考邦友的文章 https://ithelp.ith...

DAY4: Visual Code 的第一个Node.js与 Node一开始系统无法执行的解决办法

上一篇介绍了安装步骤与执行环境,接下来今天要撰写人生第一个Node.js。因为接下来要介绍的或是要展...

TypeScript 能手养成之旅 Day 12 泛用型别(Generics Types)

前言 今天要来介绍 泛用型别,在我们前面介绍的 型别化名 ,而 泛用型别 就是将 型别化名 参数化,...

用一半的时间做两倍的事

听说,这个书名引发了一些争议。老板角色的人看这本书都认为RD团队读完後就是吃了大补丸,以後做专案只需...