1.15 自实现GetProcAddress

2024-03-10 20:50
文章标签 实现 1.15 getprocaddress

本文主要是介绍1.15 自实现GetProcAddress,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

在正常情况下,要想使用GetProcAddress函数,需要首先调用LoadLibraryA函数获取到kernel32.dll动态链接库的内存地址,接着在调用GetProcAddress函数时传入模块基址以及模块中函数名即可动态获取到特定函数的内存地址,但在有时这个函数会被保护起来,导致我们无法直接调用该函数获取到特定函数的内存地址,此时就需要自己编写实现LoadLibrary以及GetProcAddress函数,该功能的实现需要依赖于PEB线程环境块,通过线程环境块可遍历出kernel32.dll模块的入口地址,接着就可以在该模块中寻找GetProcAddress函数入口地址,当找到该入口地址后即可直接调用实现动态定位功能。

首先通过PEB/TEB找到自身进程的所有载入模块数据,获取TEB也就是线程环境块。在编程的时候TEB始终保存在寄存器 FS 中。

0:000> !teb
TEB at 00680000ExceptionList:        008ff904StackBase:            00900000StackLimit:           008fc000RpcHandle:            00000000Tls Storage:          0068002cPEB Address:          0067d0000: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

从如上输出结果可以看出,LDRPEB 结构体偏移的 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 774c0b080: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 001400120: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保存着TEBmov PEB, eax            // +30定位到PEB}// 得到LDRLdr = *((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.dllif (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);// 模块初始化链表的头指针InInitializationOrderModuleListPULONGLONG 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位kernel32DWORD kernel32BaseAddress = GetModuleKernel32();std::cout << "kernel32 = " << std::hex << kernel32BaseAddress << std::endl;// 输出64位kernel32ULONGLONG 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的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/795467

相关文章

Linux下删除乱码文件和目录的实现方式

《Linux下删除乱码文件和目录的实现方式》:本文主要介绍Linux下删除乱码文件和目录的实现方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录linux下删除乱码文件和目录方法1方法2总结Linux下删除乱码文件和目录方法1使用ls -i命令找到文件或目录

SpringBoot+EasyExcel实现自定义复杂样式导入导出

《SpringBoot+EasyExcel实现自定义复杂样式导入导出》这篇文章主要为大家详细介绍了SpringBoot如何结果EasyExcel实现自定义复杂样式导入导出功能,文中的示例代码讲解详细,... 目录安装处理自定义导出复杂场景1、列不固定,动态列2、动态下拉3、自定义锁定行/列,添加密码4、合并

mybatis执行insert返回id实现详解

《mybatis执行insert返回id实现详解》MyBatis插入操作默认返回受影响行数,需通过useGeneratedKeys+keyProperty或selectKey获取主键ID,确保主键为自... 目录 两种方式获取自增 ID:1. ​​useGeneratedKeys+keyProperty(推

Spring Boot集成Druid实现数据源管理与监控的详细步骤

《SpringBoot集成Druid实现数据源管理与监控的详细步骤》本文介绍如何在SpringBoot项目中集成Druid数据库连接池,包括环境搭建、Maven依赖配置、SpringBoot配置文件... 目录1. 引言1.1 环境准备1.2 Druid介绍2. 配置Druid连接池3. 查看Druid监控

Linux在线解压jar包的实现方式

《Linux在线解压jar包的实现方式》:本文主要介绍Linux在线解压jar包的实现方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录linux在线解压jar包解压 jar包的步骤总结Linux在线解压jar包在 Centos 中解压 jar 包可以使用 u

c++ 类成员变量默认初始值的实现

《c++类成员变量默认初始值的实现》本文主要介绍了c++类成员变量默认初始值,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧... 目录C++类成员变量初始化c++类的变量的初始化在C++中,如果使用类成员变量时未给定其初始值,那么它将被

Qt使用QSqlDatabase连接MySQL实现增删改查功能

《Qt使用QSqlDatabase连接MySQL实现增删改查功能》这篇文章主要为大家详细介绍了Qt如何使用QSqlDatabase连接MySQL实现增删改查功能,文中的示例代码讲解详细,感兴趣的小伙伴... 目录一、创建数据表二、连接mysql数据库三、封装成一个完整的轻量级 ORM 风格类3.1 表结构

基于Python实现一个图片拆分工具

《基于Python实现一个图片拆分工具》这篇文章主要为大家详细介绍了如何基于Python实现一个图片拆分工具,可以根据需要的行数和列数进行拆分,感兴趣的小伙伴可以跟随小编一起学习一下... 简单介绍先自己选择输入的图片,默认是输出到项目文件夹中,可以自己选择其他的文件夹,选择需要拆分的行数和列数,可以通过

Python中将嵌套列表扁平化的多种实现方法

《Python中将嵌套列表扁平化的多种实现方法》在Python编程中,我们常常会遇到需要将嵌套列表(即列表中包含列表)转换为一个一维的扁平列表的需求,本文将给大家介绍了多种实现这一目标的方法,需要的朋... 目录python中将嵌套列表扁平化的方法技术背景实现步骤1. 使用嵌套列表推导式2. 使用itert

Python使用pip工具实现包自动更新的多种方法

《Python使用pip工具实现包自动更新的多种方法》本文深入探讨了使用Python的pip工具实现包自动更新的各种方法和技术,我们将从基础概念开始,逐步介绍手动更新方法、自动化脚本编写、结合CI/C... 目录1. 背景介绍1.1 目的和范围1.2 预期读者1.3 文档结构概述1.4 术语表1.4.1 核