在正常情况下,要想使用GetProcAddress
函数,需要首先调用LoadLibraryA
函数获取到kernel32.dll
动态链接库的内存地址,接着在调用GetProcAddress
函数时传入模块基址以及模块中函数名即可动态获取到特定函数的内存地址,但在有时这个函数会被保护起来,导致我们无法直接调用该函数获取到特定函数的内存地址,此时就需要自己编写实现LoadLibrary
以及GetProcAddress
函数,该功能的实现需要依赖于PEB
线程环境块,通过线程环境块可遍历出kernel32.dll
模块的入口地址,接着就可以在该模块中寻找GetProcAddress
函数入口地址,当找到该入口地址后即可直接调用实现动态定位功能。
首先通过PEB/TEB
找到自身进程的所有载入模块数据,获取TEB
也就是线程环境块。在编程的时候TEB
始终保存在寄存器 FS
中。
0:000> !teb TEB at 00680000 ExceptionList: 008ff904 StackBase: 00900000 StackLimit: 008fc000 RpcHandle: 00000000 Tls Storage: 0068002c PEB Address: 0067d000
0:000> dt _teb 00680000 ntdll!_TEB +0x000 NtTib : _NT_TIB +0x01c EnvironmentPointer : (null) +0x020 ClientId : _CLIENT_ID +0x028 ActiveRpcHandle : (null) +0x02c ThreadLocalStoragePointer : 0x0068002c Void +0x030 ProcessEnvironmentBlock : 0x0067d000 _PEB
|
从该命令的输出可以看出,PEB
结构体的地址位于 TEB
结构体偏移0x30
的位置,该位置保存的地址是 0x0067d000
。也就是说,PEB
的地址是 0x0067d000
,通过该地址来解析 PEB
并获得 LDR
结构。
0:000> dt nt!_peb 0x0067d000 ntdll!_PEB +0x000 InheritedAddressSpace : 0 '' +0x001 ReadImageFileExecOptions : 0 '' +0x002 BeingDebugged : 0x1 '' +0x003 BitField : 0x4 '' +0x003 ImageUsesLargePages : 0y0 +0x003 IsProtectedProcess : 0y0 +0x003 IsImageDynamicallyRelocated : 0y1 +0x003 SkipPatchingUser32Forwarders : 0y0 +0x003 IsPackagedProcess : 0y0 +0x003 IsAppContainer : 0y0 +0x003 IsProtectedProcessLight : 0y0 +0x003 IsLongPathAwareProcess : 0y0 +0x004 Mutant : 0xffffffff Void +0x008 ImageBaseAddress : 0x00f30000 Void +0x00c Ldr : 0x774c0c40 _PEB_LDR_DATA
|
从如上输出结果可以看出,LDR
在 PEB
结构体偏移的 0x0C
处,该地址保存的地址是 0x774c0c40
通过该地址来解析 LDR
结构体。WinDBG
输出如下内容:
0:000> dt _peb_ldr_data 0x774c0c40 ntdll!_PEB_LDR_DATA +0x000 Length : 0x30 +0x004 Initialized : 0x1 '' +0x008 SsHandle : (null) +0x00c InLoadOrderModuleList : _LIST_ENTRY [ 0x9e3208 - 0x9e5678 ] +0x014 InMemoryOrderModuleList : _LIST_ENTRY [ 0x9e3210 - 0x9e5680 ] +0x01c InInitializationOrderModuleList : _LIST_ENTRY [ 0x9e3110 - 0x9e35f8 ] +0x024 EntryInProgress : (null) +0x028 ShutdownInProgress : 0 '' +0x02c ShutdownThreadId : (null)
0:000> dt _LIST_ENTRY ntdll!_LIST_ENTRY +0x000 Flink : Ptr32 _LIST_ENTRY +0x004 Blink : Ptr32 _LIST_ENTRY
|
现在来手动遍历第一条链表,输入命令0x9e3208
:在链表偏移 0x18
的位置是模块的映射地址,即 ImageBase
;在链表
偏移 0x28
的位置是模块的路径及名称的地址;在链表偏移 0x30
的位置是模块名称的地址。
0:000> dd 0x9e3208 009e3208 009e3100 774c0c4c 009e3108 774c0c54 009e3218 00000000 00000000 00f30000 00f315bb 009e3228 00007000 00180016 009e1fd4 00120010 009e3238 009e1fda 000022cc 0000ffff 774c0b08
0:000> du 009e1fd4 009e1fd4 "C:\main.exe" 0:000> du 009e1fda 009e1fda "main.exe"
|
读者可自行验证,如下所示的确是模块的名称。既然是链表,就来下一条链表的信息,009e3100
保存着下一个链表结构。依次遍历就是了。
0:000> dd 009e3100 009e3100 009e35e8 009e3208 009e35f0 009e3210 009e3110 009e39b8 774c0c5c 773a0000 00000000 009e3120 0019c000 003c003a 009e2fe0 00140012
0:000> du 009e2fe0 009e2fe0 "C:\Windows\SYSTEM32\ntdll.dll"
|
上述地址009e3100
介绍的结构,是微软保留结构,只能从网上找到一个结构定义,然后自行看着解析就好了。
typedef struct _LDR_DATA_TABLE_ENTRY { PVOID Reserved1[2]; LIST_ENTRY InMemoryOrderLinks; PVOID Reserved2[2]; PVOID DllBase; PVOID EntryPoint; PVOID Reserved3; UNICODE_STRING FullDllName; BYTE Reserved4[8]; PVOID Reserved5[3]; union { ULONG CheckSum; PVOID Reserved6; }; ULONG TimeDateStamp; } LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;
|
根据如上流程,想要得到kernel32.dll
模块的入口地址,我们可以进行这几步,首先得到TEB
地址,并在该地址中寻找PEB
线程环境块,并在该环境块内得到LDR
结构,在该结构中获取第二条链表地址,输出该链表中的0x10
以及0x20
即可得到当前模块的基地址,以及完整的模块路径信息,该功能的实现分为32位于64位,如下代码则是实现代码。
#include <iostream> #include <Windows.h>
#pragma comment(linker, "/merge:.data=.text") #pragma comment(linker, "/merge:.rdata=.text") #pragma comment(linker, "/section:.text,RWE")
DWORD GetModuleKernel32() { DWORD *PEB = NULL, *Ldr = NULL, *Flink = NULL, *p = NULL; DWORD *BaseAddress = NULL, *FullDllName = NULL;
__asm { mov eax, fs:[0x30] mov PEB, eax }
Ldr = *((DWORD **)((unsigned char *)PEB + 0x0c));
Flink = *((DWORD **)((unsigned char *)Ldr + 0x14)); p = Flink;
p = *((DWORD **)p);
int count = 0;
while (Flink != p) { BaseAddress = *((DWORD **)((unsigned char *)p + 0x10)); FullDllName = *((DWORD **)((unsigned char *)p + 0x20));
if (BaseAddress == 0) break;
if (count == 1) { return reinterpret_cast<DWORD>(BaseAddress); }
p = *((DWORD **)p); count = count + 1; }
return 0; }
ULONGLONG GetModuleKernel64() { ULONGLONG dwKernel32Addr = 0;
_TEB* pTeb = NtCurrentTeb(); PULONGLONG pPeb = (PULONGLONG)*(PULONGLONG)((ULONGLONG)pTeb + 0x60); PULONGLONG pLdr = (PULONGLONG)*(PULONGLONG)((ULONGLONG)pPeb + 0x18); PULONGLONG pInLoadOrderModuleList = (PULONGLONG)((ULONGLONG)pLdr + 0x10);
PULONGLONG pModuleExe = (PULONGLONG)*pInLoadOrderModuleList;
PULONGLONG pModuleNtdll = (PULONGLONG)*pModuleExe;
PULONGLONG pModuleKernel32 = (PULONGLONG)*pModuleNtdll;
dwKernel32Addr = pModuleKernel32[6]; return dwKernel32Addr; }
int main(int argc, char *argv[]) { DWORD kernel32BaseAddress = GetModuleKernel32(); std::cout << "kernel32 = " << std::hex << kernel32BaseAddress << std::endl;
ULONGLONG kernel64BaseAddress = GetModuleKernel64(); std::cout << "kernel64 = " << std::hex << kernel32BaseAddress << std::endl;
system("pause"); return 0; }
|
如上代码中分别实现了32
位于64
位两种获取内存模块基址GetModuleKernel32
用于获取32位模式,GetModuleKernel64
则用于获取64位内存基址,读者可自行调用两种模式,输出如下图所示;

我们通过调用GetModuleKernel32()
函数读入kernel32.dll
模块入口地址后,则下一步就可以通过循环,遍历该模块的导出表并寻找到GetProcAddress
导出函数地址,找到该导出函数内存地址后,则可以通过kernel32
模块基址加上dwFunAddrOffset
相对偏移,获取到该函数的内存地址,此时通过函数指针就可以将该函数地址读入到内存指针内。
ULONGLONG MyGetProcAddress() { ULONGLONG dwBase = GetModuleKernel32(); PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)dwBase; PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(dwBase + pDos->e_lfanew); PIMAGE_DATA_DIRECTORY pExportDir = pNt->OptionalHeader.DataDirectory; pExportDir = &(pExportDir[IMAGE_DIRECTORY_ENTRY_EXPORT]); DWORD dwOffset = pExportDir->VirtualAddress; PIMAGE_EXPORT_DIRECTORY pExport = (PIMAGE_EXPORT_DIRECTORY)(dwBase + dwOffset); DWORD dwFunCount = pExport->NumberOfFunctions; DWORD dwFunNameCount = pExport->NumberOfNames; DWORD dwModOffset = pExport->Name;
PDWORD pEAT = (PDWORD)(dwBase + pExport->AddressOfFunctions); PDWORD pENT = (PDWORD)(dwBase + pExport->AddressOfNames); PWORD pEIT = (PWORD)(dwBase + pExport->AddressOfNameOrdinals);
for (DWORD dwOrdinal = 0; dwOrdinal < dwFunCount; dwOrdinal++) { if (!pEAT[dwOrdinal]) { continue; }
DWORD dwID = pExport->Base + dwOrdinal; ULONGLONG dwFunAddrOffset = pEAT[dwOrdinal];
for (DWORD dwIndex = 0; dwIndex < dwFunNameCount; dwIndex++) { if (pEIT[dwIndex] == dwOrdinal) { ULONGLONG dwNameOffset = pENT[dwIndex]; char* pFunName = (char*)((ULONGLONG)dwBase + dwNameOffset); if (!strcmp(pFunName, "GetProcAddress")) { return dwBase + dwFunAddrOffset; } } } } return 0; }
typedef ULONGLONG(WINAPI *fnGetProcAddress)(_In_ HMODULE hModule, _In_ LPCSTR lpProcName); typedef HMODULE(WINAPI *fnLoadLibraryA)(_In_ LPCSTR lpLibFileName);
int main(int argc, char *argv[]) {
DWORD kernel32BaseAddress = GetModuleKernel32(); if (kernel32BaseAddress == 0) { return 0; }
fnGetProcAddress pfnGetProcAddress = (fnGetProcAddress)MyGetProcAddress(); std::cout << pfnGetProcAddress << std::endl;
fnLoadLibraryA pfnLoadLibraryA = (fnLoadLibraryA)pfnGetProcAddress((HMODULE)kernel32BaseAddress, "LoadLibraryA"); printf("自定义读入LoadLibrary = %x \n", pfnLoadLibraryA);
system("pause"); return 0; }
|
输出效果如下图所示,我们即可读入fnLoadLibraryA
函数的内存地址;

上述代码的使用也很简单,当我们能够得到GetProcAddress
的内存地址后,就可以使用该内存地址动态定位到任意一个函数地址,我们通过得到LoadLibrary
函数地址,与GetModuleHandleA
函数地址,通过两个函数就可以定位到Windows
系统内任意一个函数,我们以调用MessageBox
弹窗为例,动态输出一个弹窗,该调用方式如下所示。
typedef ULONGLONG(WINAPI *fnGetProcAddress)(_In_ HMODULE hModule, _In_ LPCSTR lpProcName); typedef HMODULE(WINAPI *fnLoadLibraryA)(_In_ LPCSTR lpLibFileName); typedef int(WINAPI *fnMessageBox)(HWND hWnd, LPSTR lpText, LPSTR lpCaption, UINT uType); typedef HMODULE(WINAPI *fnGetModuleHandleA)(_In_opt_ LPCSTR lpModuleName); typedef BOOL(WINAPI *fnVirtualProtect)(_In_ LPVOID lpAddress, _In_ SIZE_T dwSize, _In_ DWORD flNewProtect, _Out_ PDWORD lpflOldProtect); typedef void(WINAPI *fnExitProcess)(_In_ UINT uExitCode);
int main(int argc, char * argv[]) { fnGetProcAddress pfnGetProcAddress = (fnGetProcAddress)MyGetProcAddress(); ULONGLONG dwBase = GetModuleKernel32(); printf("fnGetProcAddress = %x \n", pfnGetProcAddress); printf("GetKernel32Addr = %x \n", dwBase);
fnLoadLibraryA pfnLoadLibraryA = (fnLoadLibraryA)pfnGetProcAddress((HMODULE)dwBase, "LoadLibraryA"); printf("pfnLoadLibraryA = %x \n", pfnLoadLibraryA);
fnGetModuleHandleA pfnGetModuleHandleA = (fnGetModuleHandleA)pfnGetProcAddress((HMODULE)dwBase, "GetModuleHandleA"); printf("pfnGetModuleHandleA = %x \n", pfnGetModuleHandleA);
fnVirtualProtect pfnVirtualProtect = (fnVirtualProtect)pfnGetProcAddress((HMODULE)dwBase, "VirtualProtect"); printf("pfnVirtualProtect = %x \n", pfnVirtualProtect);
pfnLoadLibraryA("User32.dll"); HMODULE hUser32 = (HMODULE)pfnGetModuleHandleA("User32.dll"); fnMessageBox pfnMessageBoxA = (fnMessageBox)pfnGetProcAddress(hUser32, "MessageBoxA"); printf("User32 = > %x \t MessageBox = > %x \n", hUser32, pfnMessageBoxA);
HMODULE hKernel32 = (HMODULE)pfnGetModuleHandleA("kernel32.dll"); fnExitProcess pfnExitProcess = (fnExitProcess)pfnGetProcAddress(hKernel32, "ExitProcess"); printf("Kernel32 = > %x \t ExitProcess = > %x \n", hKernel32, pfnExitProcess);
int nRet = pfnMessageBoxA(NULL, "hello lyshark", "MsgBox", MB_YESNO); if (nRet == IDYES) { printf("你点击了YES \n"); }
system("pause"); pfnExitProcess(0); return 0; }
|
运行上述代码,通过动态调用的方式获取到MessageBox
函数内存地址,并将该内存放入到pfnMessageBoxA
指针内,最后直接调用该指针即可输出如下图所示的效果图;
