一、概述
无论是刚学习免杀对抗的时候,还是再后期制作免杀木马的时候,都难免接触到一些名词,如自实现 API、IAT 绕过等等,说的都是关于 Windows 官方的 API,这些 API 函数最后会呈现在导入表中,容易被杀软检测。
在日常开发免杀木马的时候,想要动态调用模块里的函数,一般都是通过LoadLibrary
加载模块,再使用GetProcAddress
根据名字获取函数地址进行调用,这就十分容易被杀软 hook 检测。
为了对抗安全检测、避免被 Hook,就可以通过以下的流程手动解析已加载的模块获取相应函数的地址:
- 在当前进程中找到指定模块(如 ntdll.dll)的基址,遍历 PEB 里的模块链表
- 根据模块基址找到导出表(Export Directory),PE 文件结构中有专门的数据目录指向导出表
- 找到目标函数名,得到其 ordinal(序号),根据序号取到真实函数地址(RVA + 模块基址)
通过上述流程解析得到的函数地址:
- 绕过 IAT Hook / Inline Hook
- 无需调用任何额外 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 结构找到模块里指定的函数。
- 从基址找到 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 2
| DWORD exportDirRVA = ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress; PIMAGE_EXPORT_DIRECTORY exportDir = (PIMAGE_EXPORT_DIRECTORY)((BYTE*)hModule + exportDirRVA);
|
- 遍历导出表:
AddressOfNames
:所有导出函数名字的 RVA 数组
AddressOfNameOrdinals
:与名字对应的序号
AddressOfFunctions
:函数地址的 RVA 数组
- 找到名字对应的 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'\\'); if (currentFileName) { currentFileName++; } else { currentFileName = dllName; } if (currentFileName) { if (_wcsnicmp(currentFileName, targetName, wcslen(targetName)) == 0) { 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); }
|
参考链接
- https://learn.microsoft.com/zh-cn/windows/win32/api/winternl/ns-winternl-peb
- https://learn.microsoft.com/zh-cn/windows/win32/api/winternl/ns-winternl-peb_ldr_data