免入土(五)绕过 IAT Hook / Inline Hook 实现SafeGetProcAddress

一、概述

无论是刚学习免杀对抗的时候,还是再后期制作免杀木马的时候,都难免接触到一些名词,如自实现 API、IAT 绕过等等,说的都是关于 Windows 官方的 API,这些 API 函数最后会呈现在导入表中,容易被杀软检测。

在日常开发免杀木马的时候,想要动态调用模块里的函数,一般都是通过LoadLibrary 加载模块,再使用GetProcAddress 根据名字获取函数地址进行调用,这就十分容易被杀软 hook 检测。
为了对抗安全检测、避免被 Hook,就可以通过以下的流程手动解析已加载的模块获取相应函数的地址:

  1. 在当前进程中找到指定模块(如 ntdll.dll)的基址,遍历 PEB 里的模块链表
  2. 根据模块基址找到导出表(Export Directory),PE 文件结构中有专门的数据目录指向导出表
  3. 找到目标函数名,得到其 ordinal(序号),根据序号取到真实函数地址(RVA + 模块基址)

通过上述流程解析得到的函数地址:

  1. 绕过 IAT Hook / Inline Hook
  2. 无需调用任何额外 API,更隐蔽

本文通过封装了一个 SafeGetProcAddress 函数去获取指定模块的指定函数的地址,最终获取的地址 GetProcAddress 一致。

二、具体实现

2.1 获取模块:PEB 结构与遍历

每个 Windows 进程都有一个 PEB(Process Environment Block),它记录了进程里所有已加载的模块。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
typedef struct _PEB {
BYTE Reserved1[2];
BYTE BeingDebugged;
BYTE Reserved2[1];
PVOID Reserved3[2];
PPEB_LDR_DATA Ldr;
PRTL_USER_PROCESS_PARAMETERS ProcessParameters;
PVOID Reserved4[3];
PVOID AtlThunkSListPtr;
PVOID Reserved5;
ULONG Reserved6;
PVOID Reserved7;
ULONG Reserved8;
ULONG AtlThunkSListPtr32;
PVOID Reserved9[45];
BYTE Reserved10[96];
PPS_POST_PROCESS_INIT_ROUTINE PostProcessInitRoutine;
BYTE Reserved11[128];
PVOID Reserved12[1];
ULONG SessionId;
} PEB, *PPEB;

其中:

  • Ldr 指向 PEB_LDR_DATA,记录模块链表
  • InMemoryOrderModuleList:链表中每个节点是 LDR_DATA_TABLE_ENTRY
1
2
3
4
5
typedef struct _PEB_LDR_DATA {
BYTE Reserved1[8];
PVOID Reserved2[3];
LIST_ENTRY InMemoryOrderModuleList;
} PEB_LDR_DATA, *PPEB_LDR_DATA;

如何获取 PEB:

  • x64:__readgsqword(0x60)
  • x86:__readfsdword(0x30)

遍历链表:

  • 比对 FullDllName(模块全名)
  • 找到目标模块后返回 DllBase(模块基址)

关键代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
HMODULE GetModuleByPEB(const wchar_t* targetName) {
#ifdef _M_X64
PPEB pPEB = (PPEB)__readgsqword(0x60);
#else
PPEB pPEB = (PPEB)__readfsdword(0x30);
#endif
PPEB_LDR_DATA pLdr = pPEB->Ldr;
PLIST_ENTRY moduleList = &pLdr->InMemoryOrderModuleList;
PLIST_ENTRY pStartListEntry = moduleList->Flink;

for (PLIST_ENTRY pListEntry = pStartListEntry; pListEntry != moduleList; pListEntry = pListEntry->Flink) {
PLDR_DATA_TABLE_ENTRY pEntry = CONTAINING_RECORD(pListEntry, LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks);
// 比对模块名
}
return NULL;
}

2.2 解析导出表:根据名字找到函数地址

拿到模块基址后,能够根据 PE 结构找到模块里指定的函数。

  1. 从基址找到 PE 文件的 NT 头:
1
2
PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)hModule;
PIMAGE_NT_HEADERS ntHeaders = (PIMAGE_NT_HEADERS)((BYTE*)hModule + dosHeader->e_lfanew);
  1. 找到导出表:
1
2
DWORD exportDirRVA = ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;
PIMAGE_EXPORT_DIRECTORY exportDir = (PIMAGE_EXPORT_DIRECTORY)((BYTE*)hModule + exportDirRVA);
  1. 遍历导出表:
  • AddressOfNames:所有导出函数名字的 RVA 数组
  • AddressOfNameOrdinals:与名字对应的序号
  • AddressOfFunctions:函数地址的 RVA 数组
  1. 找到名字对应的 ordinal,再找到真实地址:
1
2
3
4
5
6
7
for (DWORD i = 0; i < exportDir->NumberOfNames; ++i) {
const char* funcName = (const char*)hModule + nameRVAs[i];
if (strcmp(funcName, lpProcName) == 0) {
WORD ordinal = nameOrdinals[i];
return (FARPROC)((BYTE*)hModule + functions[ordinal]);
}
}

2.3 封装 Api:SafeGetProcAddress

把上面两个功能组合:

1
2
3
4
5
FARPROC SafeGetProcAddress(const wchar_t* moduleName, LPCSTR apiName) {
HMODULE hMod = GetModuleByPEB(moduleName);
if (!hMod) return NULL;
return ParseExportByName(hMod, apiName);
}

现在就能:

1
auto pNtCreateFile = SafeGetProcAddress(L"ntdll.dll", "NtCreateFile");

无须 LoadLibrary,也不走官方 GetProcAddress。

三、最终实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
#include "safeApi.h"
#include <winternl.h>

#ifdef _MSC_VER
#pragma comment(lib, "ntdll.lib")
#endif
#include <cwchar>


HMODULE GetModuleByPEB(const wchar_t* targetName) {
#ifdef _M_X64
PPEB pPEB = (PPEB)__readgsqword(0x60);
#else
PPEB pPEB = (PPEB)__readfsdword(0x30);
#endif
PPEB_LDR_DATA pLdr = pPEB->Ldr;
PLIST_ENTRY moduleList = &pLdr->InMemoryOrderModuleList;
PLIST_ENTRY pStartListEntry = moduleList->Flink;

for (PLIST_ENTRY pListEntry = pStartListEntry; pListEntry != moduleList; pListEntry = pListEntry->Flink) {
PLDR_DATA_TABLE_ENTRY pEntry = CONTAINING_RECORD(pListEntry, LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks);
wchar_t* dllName = pEntry->FullDllName.Buffer;
const wchar_t* currentFileName = wcsrchr(dllName, L'\\'); // 获取dll文件名
if (currentFileName) {
currentFileName++;
}
else {
currentFileName = dllName;
}
if (currentFileName) {
if (_wcsnicmp(currentFileName, targetName, wcslen(targetName)) == 0) { // 比较dll文件名(不区分大小写)
return (HMODULE)pEntry->DllBase;
}
}
}
return NULL;
}

FARPROC ParseExportByName(HMODULE hModule, LPCSTR lpProcName) {
if (!hModule || !lpProcName)
return NULL;

PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)hModule;
PIMAGE_NT_HEADERS ntHeaders = (PIMAGE_NT_HEADERS)((BYTE*)hModule + dosHeader->e_lfanew);

DWORD exportDirRVA = ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;
if (!exportDirRVA) return NULL;

PIMAGE_EXPORT_DIRECTORY exportDir = (PIMAGE_EXPORT_DIRECTORY)((BYTE*)hModule + exportDirRVA);
DWORD* functions = (DWORD*)((BYTE*)hModule + exportDir->AddressOfFunctions);


if ((ULONG_PTR)lpProcName <= 0xFFFF) {
WORD ordinal = (WORD)(ULONG_PTR)lpProcName;
WORD baseOrdinal = (WORD)exportDir->Base;
if (ordinal < baseOrdinal || ordinal >= baseOrdinal + exportDir->NumberOfFunctions) {
return NULL;
}
return (FARPROC)((BYTE*)hModule + functions[ordinal - baseOrdinal]);
}


DWORD* nameRVAs = (DWORD*)((BYTE*)hModule + exportDir->AddressOfNames);
WORD* nameOrdinals = (WORD*)((BYTE*)hModule + exportDir->AddressOfNameOrdinals);

for (DWORD i = 0; i < exportDir->NumberOfNames; ++i) {
const char* funcName = (const char*)hModule + nameRVAs[i];
if (strcmp(funcName, lpProcName) == 0) {
WORD ordinal = nameOrdinals[i];
return (FARPROC)((BYTE*)hModule + functions[ordinal]);
}
}

return NULL;
}

FARPROC SafeGetProcAddress(const wchar_t* moduleName, LPCSTR apiName) {
HMODULE hMod = GetModuleByPEB(moduleName);
if (!hMod) return NULL;
return ParseExportByName(hMod, apiName);
}

参考链接

  1. https://learn.microsoft.com/zh-cn/windows/win32/api/winternl/ns-winternl-peb
  2. https://learn.microsoft.com/zh-cn/windows/win32/api/winternl/ns-winternl-peb_ldr_data

免入土(五)绕过 IAT Hook / Inline Hook 实现SafeGetProcAddress
http://candyb0x.github.io/2025/07/13/免入土(五)绕过-IAT-Hook-Inline-Hook-实现SafeGetProcAddress/
作者
Candy
发布于
2025年7月13日
更新于
2025年7月13日
许可协议