1.15 自实现GetProcAddress
在正常情况下,要想使用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 // 偏移为30,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
从如上输出结果可以看出,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")
// 得到32位模式下kernel32.dll地址
DWORD GetModuleKernel32()
{
DWORD *PEB = NULL, *Ldr = NULL, *Flink = NULL, *p = NULL;
DWORD *BaseAddress = NULL, *FullDllName = NULL;
__asm
{
mov eax, fs:[0x30] // FS保存着TEB
mov PEB, eax // +30定位到PEB
}
// 得到LDR
Ldr = *((DWORD **)((unsigned char *)PEB + 0x0c));
// 在LDR基础上找到第二条链表
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;
// printf("镜像基址 = %08x \r\n 模块路径 = %S \r\n", BaseAddress, (unsigned char *)FullDllName);
// 第二个模块是kernel32.dll
if (count == 1)
{
// printf("address =%x \n", BaseAddress);
return reinterpret_cast<DWORD>(BaseAddress);
}
p = *((DWORD **)p);
count = count + 1;
}
// 未找到Kernel32模块
return 0;
}
// 获取64位模式下的kernel32.dll基址
ULONGLONG GetModuleKernel64()
{
ULONGLONG dwKernel32Addr = 0;
// 获取TEB的地址
_TEB* pTeb = NtCurrentTeb();
// 获取PEB的地址
PULONGLONG pPeb = (PULONGLONG)*(PULONGLONG)((ULONGLONG)pTeb + 0x60);
// 获取PEB_LDR_DATA结构的地址
PULONGLONG pLdr = (PULONGLONG)*(PULONGLONG)((ULONGLONG)pPeb + 0x18);
// 模块初始化链表的头指针InInitializationOrderModuleList
PULONGLONG pInLoadOrderModuleList = (PULONGLONG)((ULONGLONG)pLdr + 0x10);
// 获取链表中第一个模块信息,exe模块
PULONGLONG pModuleExe = (PULONGLONG)*pInLoadOrderModuleList;
//printf("EXE Base = > %X \n", pModuleExe[6]);
// 获取链表中第二个模块信息,ntdll模块
PULONGLONG pModuleNtdll = (PULONGLONG)*pModuleExe;
//printf("Ntdll Base = > %X \n", pModuleNtdll[6]);
// 获取链表中第三个模块信息,Kernel32模块
PULONGLONG pModuleKernel32 = (PULONGLONG)*pModuleNtdll;
//printf("Kernel32 Base = > %X \n", pModuleKernel32[6]);
// 获取kernel32基址
dwKernel32Addr = pModuleKernel32[6];
return dwKernel32Addr;
}
int main(int argc, char *argv[])
{
// 输出32位kernel32
DWORD kernel32BaseAddress = GetModuleKernel32();
std::cout << "kernel32 = " << std::hex << kernel32BaseAddress << std::endl;
// 输出64位kernel32
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()
{
// 获取32位基址
ULONGLONG dwBase = GetModuleKernel32();
// 获取64位基址
// ULONGLONG dwBase = GetModuleKernel64();
// 获取DOS头
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)dwBase;
// 获取32位NT头
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(dwBase + pDos->e_lfanew);
// 获取64位NT头
// PIMAGE_NT_HEADERS64 pNt = (PIMAGE_NT_HEADERS64)(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;
}
// 获取kernel32基址/获取GetProcAddress的基址
fnGetProcAddress pfnGetProcAddress = (fnGetProcAddress)MyGetProcAddress();
std::cout << pfnGetProcAddress << std::endl;
// 获取Kernel32核心API地址
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[])
{
// 获取kernel32基址 / 获取GetProcAddress的基址
fnGetProcAddress pfnGetProcAddress = (fnGetProcAddress)MyGetProcAddress();
ULONGLONG dwBase = GetModuleKernel32();
printf("fnGetProcAddress = %x \n", pfnGetProcAddress);
printf("GetKernel32Addr = %x \n", dwBase);
// 获取Kernel32核心API地址
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);
// 有了核心API之后,即可获取到User32.dll的基地址
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指针内,最后直接调用该指针即可输出如下图所示的效果图;

1.15 自实现GetProcAddress的更多相关文章
- 获取操作系统OS等相关信息
问题一:Windows SDK 8.1版本中的VersionHelper.h文件当中没有IsWindows10ORGreater,所以当你用IsWindows8Point1ORGreater判断出版本 ...
- 动态载入DLL所需要的三个函数详解(LoadLibrary,GetProcAddress,FreeLibrary)
动态载入 DLL 动态载入方式是指在编译之前并不知道将会调用哪些 DLL 函数, 完全是在运行过程中根据需要决定应调用哪些函数. 方法是:用 LoadLibrary 函数加载动态链接库到内存,用 Ge ...
- __declspec的15种用法
__cdecl和__stdcall都是函数调用规范(还有一个__fastcall),规定了参数出入栈的顺序和方法,如果只用VC编程的话可以不用关心,但是要在C++和Pascal等其他语言通信的时候就要 ...
- 【转载】动态载入DLL所需要的三个函数详解(LoadLibrary,GetProcAddress,FreeLibrary)
原文地址:https://www.cnblogs.com/westsoft/p/5936092.html 动态载入 DLL 动态载入方式是指在编译之前并不知道将会调用哪些 DLL 函数, 完全是在运行 ...
- win系统动态载入DLL所需要的三个函数详解(LoadLibrary,GetProcAddress,FreeLibrary)
动态载入 DLL 动态载入方式是指在编译之前并不知道将会调用哪些 DLL 函数, 完全是在运行过程中根据需要决定应调用哪些函数. 方法是:用 LoadLibrary 函数加载动态链接库到内存,用 Ge ...
- .NET平台开源项目速览(15)文档数据库RavenDB-介绍与初体验
不知不觉,“.NET平台开源项目速览“系列文章已经15篇了,每一篇都非常受欢迎,可能技术水平不高,但足够入门了.虽然工作很忙,但还是会抽空把自己知道的,已经平时遇到的好的开源项目分享出来.今天就给大家 ...
- 15个关于Chrome的开发必备小技巧[译]
谷歌Chrome,是当前最流行且被众多web开发人员使用的浏览器.最快六周就更新发布一次以及伴随着它不断强大的开发组件,使得Chrome成为你必备的开发工具.例如,在线编辑CSS,console以及d ...
- 15个C++项目列表
实验楼上有很多C++的实战项目,从简单到进阶,学习每个项目都可以掌握相应的知识点. 如果你还是C++新手的话,那么这个C++的项目列表你可以拿去练手实战开发,毕竟学编程动手实践是少不了的! 如果你不知 ...
- Hacker Rank: Two Strings - thinking in C# 15+ ways
March 18, 2016 Problem statement: https://www.hackerrank.com/challenges/two-strings/submissions/code ...
- 在 Ubuntu 15.04 中使用 ubuntu-make、Eclipse 4.4、Java 8 以及 WTP
Ubuntu 今天发布新版本了 其实昨天(2015-04-23)我就看到了 Ubuntu 发布新版本的新闻,下班后回家的第一件事就是访问 Ubuntu 的官网,很可惜,没有提供下载.今天(2015-0 ...
随机推荐
- 【Haxe】(一)VSCode 搭建 Haxe 开发环境
前言 咱换工作啦! 新工作这边需要用到的开发语言是 Haxe,最近大概会写几篇笔记.Haxe 的介绍就不写了,打算记录点有用的学习内容,先从搭建开发环境开始吧! 当前适用版本: VSCode:Curr ...
- Anaconda 使用的一些体验与困惑
Channels 使用 需要注意的是做生信分析的童鞋使用 conda 环境时一定要特别注意 conda channels 的设置,滥用 channels 很有可能会导致你的软件升降级(甚至环境)错乱. ...
- 判断两个矩形是否相交(Rect Intersection)
0x00 Preface 最近在开发一个2D组态图形组件的过程中,里面的数学模块,涉及到两个矩形是否相交的判断. 这个问题很多年前就写过,算是个小的算法吧. 网络上搜索一下,有很多思路,有一些思路要基 ...
- JAVA SE基础《一》----JAVA入门
初识Java 1.Java背景知识 java是美国sun公司(Stanford University Network)在1995年推出的一门计算机高级编程语言. Java早期称为Oak(橡树),后期改 ...
- Avalonia开发Markdown编辑器
Avalonia开发Markdown编辑器 今天熟悉Avalonia UI,做一个Markdown的文本编辑器. 代码我上传了Github,地址: https://github.com/raokun/ ...
- C++ 数独游戏
C++ 数独游戏 直接上代码: 1 // 数独 sudoku 2 3 #include <iostream> 4 5 using namespace std; 6 7 int P[9][9 ...
- .Net下验证MongoDB 的 Linq 模式联合查询是否可用
MongoDB.Driver 类库提供了 Linq 查询的支持.然而,在使用 Linq 进行联合查询时,是否能够正确转换为 MongoDB 底层的查询语句还有待验证.今天,我将进行实验来验证一下. 输 ...
- 与 AI 同行,利用 ChatGLM 构建知识图谱
大家好,我是东方财富的一名算法工程师,这里分享一些利用大模型赋能知识图谱建设的一些实践. 为什么知识图谱需要大模型 在金融场景中,天然会有大量结构化的数据需要投入大量的人力去生产和维护,而这样的数据又 ...
- 【JavaScript】你真的熟悉bind吗
引言 内容速递 看了本文您能了解到的知识! 在本篇文章中,将带你了解什么是bind,bind的用途.如何手写bind以及工作中实际使用bind的场景. 在JavaScript中,bind()方法是用来 ...
- 【调制解调】PM 调相
说明 学习数字信号处理算法时整理的学习笔记.同系列文章目录可见 <DSP 学习之路>目录,代码已上传到 Github - ModulationAndDemodulation.本篇介绍 PM ...