在【Day 11】卑鄙源之 Hook (上) - 侦测 Hook 我们提到可以比对档案与记忆体来判断函数是否有被 Hook,也透过几个 Windows API 把目标 Process iexplore.exe 中的目标 Module WININET.DLL 找出来并取得 Handle。
这一篇要来处理档案的部分,同样找到档案中的 WININET.DLL,然後比对档案与记忆体的差异,透过两者一不一样判断有没有被 Hook。
首先我们要知道 WININET.DLL 的完整路径是 C:\Windows\SysWOW64\wininet.dll
,注意这边不是 C:\Windows\System32\wininet.dll
,因为之前实作的目标是 32-bit Process。
既然已经知道档案路径,接下来只要
这三步分别对应到三个函数,CreateFile、CreateFileMapping、MapViewOfFile,如此就可以读取档案内容。
目前已经可以读取档案与记忆体的内容,接下来要找到目标 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 结构的部分其实已经在 【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_HEADERS,IMAGE_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;
完整的程序专案可以参考我的 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
前情提要 前一篇文章带大家看了 BeautifulSoup 库的使用,用他来做资料清洗,使我们真正想...
这个章节很多都在CISSP有,很多邦友写过,可以参考邦友的文章 https://ithelp.ith...
上一篇介绍了安装步骤与执行环境,接下来今天要撰写人生第一个Node.js。因为接下来要介绍的或是要展...
前言 今天要来介绍 泛用型别,在我们前面介绍的 型别化名 ,而 泛用型别 就是将 型别化名 参数化,...
听说,这个书名引发了一些争议。老板角色的人看这本书都认为RD团队读完後就是吃了大补丸,以後做专案只需...