dll内存加载的实现
学过C/C++的同学应该都接触过库的加载函数LoadLibrary。这个API函数可以实现动态添加dll的效果。但是有个问题,就是用这个函数加载库之后,可以在模块中找到库的相关信息,这在正常情况下是没啥的。但是在非正常的情况下,这就有较大的缺陷。比如在写外挂的时候,需要往游戏进程中注入一个dll,如果用LoadLibrary函数来加载,加载之后在进程模块中就会看到这个dll的信息,这样只要那些搞游戏开发的有意识的扫下进程模块,你的这个库就立马暴露了。所以在外挂中,隐藏库是很有必要的,而内存加载就可以达到隐藏的目的。这里会涉及到PE文件的相关知识,由于篇幅问题,PE格式就不细说,这里只做了解,PE的格式如下图所示
这图看着挺复杂,其实就是PE头+区块表+区块数据,具体的大家就自己去查资料吧。
要加载库到内存中,首先我们得先读取库的数据,读取函数如下
//读取dll数据到内存
//szLibName 要读取的库文件
//pBuffer 用来获取数据的缓存(这个值位NULL则只是获取文件大小,文件大小由lSize返回)
//lSize 读取的数据大小(文件大小)
//成功返回0,失败返回-1
int ReadLibrary(wchar_t *szLibName, void *pBuffer, uint64_t &lSize)
{
int nResult = 0;
LARGE_INTEGER li = { 0 };
HANDLE hFile = INVALID_HANDLE_VALUE;
hFile = CreateFileW(szLibName, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (INVALID_HANDLE_VALUE == hFile){
return ERR_OPEN_FILE;
}
li.LowPart = GetFileSize(hFile, (LPDWORD)&li.HighPart);
//pBuffer为NULL则只是为了获取文件大小
if (NULL == pBuffer){
lSize = li.QuadPart;
CloseHandle(hFile);
return ERR_SUCCESS;
}
if (ReadFile(hFile, pBuffer, lSize, (LPDWORD)&lSize, NULL)) {
nResult = ERR_SUCCESS;
}
else{
nResult = ERR_READ_FILE;
}
CloseHandle(hFile);
return nResult;
}
这个函数比较简单,只是打开文件,获取文件大小,然后读取文件,关闭文件等。做的一些处理就是当传入的缓存为NULL时,表示要获取文件大小,这时先不进行文件的读取,而只是将文件大小返回,以便外部开辟相应大小的缓存来装载数据。当数据读取出来之后,我们先要判断这个文件是否是PE格式,代码如下
//判断当前文件是否是PE格式
int IsPEFile(void *pBuffer)
{
uint32_t uResult = 0;
PIMAGE_DOS_HEADER pDosHeader = NULL;
PIMAGE_FILE_HEADER pFileHeader = NULL;
PIMAGE_NT_HEADERS pNTHeader = NULL;
pDosHeader = (PIMAGE_DOS_HEADER)pBuffer;
pNTHeader = (PIMAGE_NT_HEADERS)((char*)pBuffer + pDosHeader->e_lfanew);
//PE格式的文件是以"MZ"开头,在pDosHeader->e_lfanew处的值是"PE\0\0"
if (pDosHeader->e_magic == IMAGE_DOS_SIGNATURE && pNTHeader->Signature == IMAGE_NT_SIGNATURE){
uResult = ERR_PE_FORMAT;
}
else{
uResult = ERR_OTHERE_FORMAT;
}
return uResult;
}
要想理解上面的代码,我们先来看Dos头的结构,如下
typedef struct _IMAGE_DOS_HEADER { // DOS .EXE header
WORD e_magic; // Magic number
WORD e_cblp; // Bytes on last page of file
WORD e_cp; // Pages in file
WORD e_crlc; // Relocations
WORD e_cparhdr; // Size of header in paragraphs
WORD e_minalloc; // Minimum extra paragraphs needed
WORD e_maxalloc; // Maximum extra paragraphs needed
WORD e_ss; // Initial (relative) SS value
WORD e_sp; // Initial SP value
WORD e_csum; // Checksum
WORD e_ip; // Initial IP value
WORD e_cs; // Initial (relative) CS value
WORD e_lfarlc; // File address of relocation table
WORD e_ovno; // Overlay number
WORD e_res; // Reserved words
WORD e_oemid; // OEM identifier (for e_oeminfo)
WORD e_oeminfo; // OEM information; e_oemid specific
WORD e_res2; // Reserved words
LONG e_lfanew; // File address of new exe header
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
通过上面的PE文集格式的结构可知,在文件开始处,也就是Dos头有个"MZ"的标志,这个被定义为IMAGE_DOS_SIGNATURE,其值为0x5A4D,其实就是字符'M'和'Z'的ascii码值。在Dos头中,最后一个成员变量e_lfanew是NT头的RVA,即PE文件数据的首地址加上这个值就是NT头的首地址,NT头的格式如下
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
在NT头中有个Signature,这个成员变量是NT头的标志,被定义为IMAGE_NT_SIGNATURE,其值为0x00004550,其实就是"PE\0\0"的ascii码值。这里只是简单的判断了下这两个标志的值是否为定义的值,如果是,则认为这个文件的数据是PE格式,否则不是。
判断了文件格式之后,接下来就是加载文件数据到内存了。加载到内存并不是在内存中开辟个空间,然后直接把数据写入就行了,在PE格式中,有加载后的内存偏移和文件偏移的区别,文件偏移就是数据保存在文件中使用的偏移,内存偏移就是dll被加载到内存后的偏移,这些偏移在PE结构中都可以读取出来。因此在加载之前,我们先要计算dll加载到内存后要占用多少空间,然后在开辟相应大小的空间来加载dll。计算空间的代码如下
//计算加载到内存后的数据大小
//pImage 读取到的库文件数据
//返回加载到内存后索要占用的内存大小
uint64_t GetImageSize(void *pImage)
{
int i = 0;
uint32_t uSizeOfOptional = 0;
uint32_t uSectionNumber = 0;
uint32_t uImagePage = 0;
uint64_t lImageSize = 0;
PIMAGE_DOS_HEADER pDosHeader = NULL;
PIMAGE_FILE_HEADER pFileHeader = NULL;
PIMAGE_NT_HEADERS pNTHeader = NULL;
PIMAGE_SECTION_HEADER pSectionHeader = NULL;
SYSTEM_INFOsysInfo = { 0 };
//判断是不是PE格式的文件
if (ERR_PE_FORMAT != IsPEFile(pImage)){
return ERR_OTHERE_FORMAT;
}
//先获取DOS头
pDosHeader = (PIMAGE_DOS_HEADER)pImage;
//获取NT头指针
pNTHeader = (PIMAGE_NT_HEADERS)((char*)pImage + pDosHeader->e_lfanew);
//获取第一个section的指针
pSectionHeader = IMAGE_FIRST_SECTION(pNTHeader);
//计算映像大小(映像大小 = 最后一个section的RVA + SizeOfRawData)
for (i = 0; i < pNTHeader->FileHeader.NumberOfSections; i ++) {
if (pSectionHeader.VirtualAddress + pSectionHeader.SizeOfRawData > lImageSize){
lImageSize = pSectionHeader.VirtualAddress + pSectionHeader.SizeOfRawData;
}
}
//获取当前系统的基本信息
GetSystemInfo(&sysInfo);
//计算映像要占用的内存页数
uImagePage = lImageSize / sysInfo.dwPageSize;
//如果当前映像大小不能被内存页大小整除,则表示最后一页数据没有占满
//即lImageSize / sysInfo.dwPageSize得出的结果还有余数,所以uImagePage要加1
if (0 != lImageSize % sysInfo.dwPageSize){
uImagePage ++;
}
lImageSize = uImagePage * sysInfo.dwPageSize;
return lImageSize;
}
这段代码用到了NT头中的FileHeader这个成员变量,这是个IMAGE_FILE_HEADER结构,其结构如下
typedef struct _IMAGE_FILE_HEADER {
WORD Machine;
WORD NumberOfSections; //区块的个数
DWORD TimeDateStamp;
DWORD PointerToSymbolTable;
DWORD NumberOfSymbols;
WORD SizeOfOptionalHeader;
WORD Characteristics;
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
在这个结构中NumberOfSections成员变量存储的是区块的个数,我们可以用这个值来控制查找各个区块的RVA和大小。
上面给出的PE结构图可以看出,在NT头之后,跟着就是区块表,区块表后跟着的就是区块数据,最后面的那些COFF啥的一般都为空,可以不用管。所以我们要计算的加载后的大小就是NT头+区块表+区块数据。
在上面的代码中,先用IMAGE_FIRST_SECTION来得到区块表的指针。既然PE文件后面都是区块数据,那么最后个区块的RVA+该区块的大小,不就是dll加载到内存后的大小了么(这里可能有点绕,大家仔细揣摩)。那怎么找到最后一个区块数据的RVA和它的大小呢?
这里用到的方法很简单,逐一比较各个区块RVA+该区块大小的值,得到的最大值即可认为是最后一个区块数据的RVA+区块数据长度(因为当前区块的RVA>=前一个区块的RVA+区块大小),而最后一个区块数据的RVA+长度就是dll加载到内存中的大小。
我们得到大小之后,要把这个大小转成内存页的倍数,所以要先找出内存页的大小,然后用当前得到的值除以内存叶大小,则可得到要占用多少内存页,但是还有不被内存页整除的情况,所以用这个大小对内存页求模,如果不为0表示不能被内存页整除,即所需的内存页数+1.
得到大小之后,就是分配内存,加载数据到内存中,代码如下
//分配内存,加载数据到内存中
//pImage 分配的内存
//pBuffer 读取到的dll文件数据
//lSize 需要分配的内存大小
int LoadLibraryImage(void *&pImage, void *pBuffer, uint64_t lSize)
{
uint32_t uResult = 0;
uint32_t uPEHeaderLen = 0;
uint32_t uSectionTableSize = 0;
PIMAGE_DATA_DIRECTORY pDataDir = NULL;
PIMAGE_DOS_HEADER pDosHeader = NULL;
PIMAGE_FILE_HEADER pFileHeader = NULL;
PIMAGE_NT_HEADERS pNTHeader = NULL;
PIMAGE_OPTIONAL_HEADER pOptionalHeader = NULL;
PIMAGE_SECTION_HEADER pSectionHeader = NULL;
pDosHeader = (PIMAGE_DOS_HEADER)pBuffer;
pNTHeader = (PIMAGE_NT_HEADERS)((char*)pDosHeader + pDosHeader->e_lfanew);
pSectionHeader = IMAGE_FIRST_SECTION(pNTHeader);
uPEHeaderLen = (char*)pSectionHeader - (char*)pDosHeader;
pImage = VirtualAlloc(NULL, lSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if (NULL == pImage){
return ERR_MALLOC;
}
ZeroMemory(pImage, lSize);
//先写入PE头
MoveMemory(pImage, pBuffer, uPEHeaderLen);
//写入区块表
uSectionTableSize = pNTHeader->FileHeader.NumberOfSections * sizeof(IMAGE_SECTION_HEADER);
MoveMemory((char*)pImage + uPEHeaderLen, (char*)pBuffer + uPEHeaderLen, uSectionTableSize);
//写入区块数据
for (int i = 0; i < pNTHeader->FileHeader.NumberOfSections; i ++) {
MoveMemory((char*)pImage + pSectionHeader.VirtualAddress, (char*)pBuffer + pSectionHeader.PointerToRawData, pSectionHeader.SizeOfRawData);
}
return uResult;
}
上面这段代码实现的是分配内存,然后往内存中写入数据。由于PE头和区块表没有文件偏移和内存偏移的问题,所以可以把它们直接写入内存即可。但是区块数据有特定的RVA,所以我们就根据其RVA来加载。这样把所有的数据都加载到内存之后,加载的环节就结束了,接下来就是做加载后的修补工作了。
加载数据到内存之后,我们要对输入表进行修复,代码如下
//修复输入表
//pImage PE文件加载到内存中的地址
int ImportRepair(void *pImage)
{
char *szDllName = NULL;
uint32_t uImportAddr = 0;
uint32_t uImportSize = 0;
uint32_t uExportAddr = 0;
uint32_t uExportSize = 0;
uint32_t *pOrgThunk = NULL;
uint32_t *pFirstThunk = NULL;
HMODULEhModule = NULL;
PIMAGE_IMPORT_DESCRIPTOR pImportDesc = NULL;
PIMAGE_DATA_DIRECTORY pImport = NULL;
PIMAGE_DOS_HEADER pDosHeader = NULL;
PIMAGE_NT_HEADERS pNTHeader = NULL;
PIMAGE_IMPORT_BY_NAME pImportName = NULL;
pDosHeader = (PIMAGE_DOS_HEADER)pImage;
pNTHeader = (PIMAGE_NT_HEADERS)((char*)pDosHeader + pDosHeader->e_lfanew);
pImport = &pNTHeader->OptionalHeader.DataDirectory;
if (NULL == pImport || 0 == pImport->VirtualAddress){
return ERR_INVALID_IMPORT;
}
pImportDesc = (PIMAGE_IMPORT_DESCRIPTOR)((char*)pDosHeader + pImport->VirtualAddress);
uImportSize = pImport->Size;
//重写输入表
for (;;) {
//获取dll名
szDllName = (char*)pDosHeader + pImportDesc->Name;
hModule = LoadLibraryA(szDllName);
if (NULL == hModule){
return ERR_LOAD_LIB;
}
pOrgThunk = (uint32_t *)((char*)pDosHeader + pImportDesc->OriginalFirstThunk);
pFirstThunk = (uint32_t*)((char*)pDosHeader + pImportDesc->FirstThunk);
while (*pOrgThunk){
//最高位为1,表示函数以序号方式输入,低31位是函数序号
if (*pOrgThunk & 0x80000000){
*pFirstThunk = (uint32_t)GetProcAddress(hModule, (char*)(*pOrgThunk & 0xffff));
}
else{
pImportName = (PIMAGE_IMPORT_BY_NAME)((char*)pDosHeader + *pOrgThunk);
*pFirstThunk = (uint32_t)GetProcAddress(hModule, pImportName->Name);
}
//无法获取函数地址则退出
if (0 == *pFirstThunk){
FreeLibrary(hModule);
return ERR_PROC_ADDR;
}
pOrgThunk++;
pFirstThunk++;
}
pImportDesc++;
if (0 == *(uint32_t*)pImportDesc){
break;
}
}
return ERR_SUCCESS;
}
要修复输入表,就要找到输入表的指针。这个得通过IMAGE_OPTIONAL_HEADER32结构来获取。先来看IMAGE_OPTIONAL_HEADER32结构
typedef struct _IMAGE_OPTIONAL_HEADER {
//
// Standard fields.
//
WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
DWORD BaseOfData;
//
// NT additional fields.
//
DWORD ImageBase;
DWORD SectionAlignment;
DWORD FileAlignment;
WORD MajorOperatingSystemVersion;
WORD MinorOperatingSystemVersion;
WORD MajorImageVersion;
WORD MinorImageVersion;
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
DWORD Win32VersionValue;
DWORD SizeOfImage;
DWORD SizeOfHeaders;
DWORD CheckSum;
WORD Subsystem;
WORD DllCharacteristics;
DWORD SizeOfStackReserve;
DWORD SizeOfStackCommit;
DWORD SizeOfHeapReserve;
DWORD SizeOfHeapCommit;
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes;
IMAGE_DATA_DIRECTORY DataDirectory;
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
这个结构的最后一个成员DataDirectory是个数组,这个数组中存储的是输入表、输出表、重定位、资源等结构的RVA。接下来介绍下输入表的结构,如下
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics; // 0 for terminating null import descriptor
DWORD OriginalFirstThunk; // RVA to original unbound IAT (PIMAGE_THUNK_DATA)
} DUMMYUNIONNAME;
DWORD TimeDateStamp; // 0 if not bound,
// -1 if bound, and real date\time stamp
// in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND)
// O.W. date/time stamp of DLL bound to (Old BIND)
DWORD ForwarderChain; // -1 if no forwarders
DWORD Name;
DWORD FirstThunk; // RVA to IAT (if bound this IAT has actual addresses)
} IMAGE_IMPORT_DESCRIPTOR;
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;
输入表的结构比较简单,只有5个成员(开头是个联合),我们只需要关注其中的3个成员即可。其中Name是该dll中依赖的其他dll的名称的RVA,而OriginalFirstThunk和FirstThunk比较重要,都是IMAGE_THUNK_DATA 结构,如下所示
typedef struct _IMAGE_THUNK_DATA32 {
union {
DWORD ForwarderString; // PBYTE
DWORD Function; // PDWORD
DWORD Ordinal;
DWORD AddressOfData; // PIMAGE_IMPORT_BY_NAME
} u1;
} IMAGE_THUNK_DATA32;
这个结构是个联合,有着多种意义。当该结构的最高位为1时,表示函数以序号的方式输入,则其低31位为函数的序号,当其最高位为0时,表示函数以字符串类型的方式输入,其低31未为指向IMAGE_IMPORT_BY_NAME的RVA。而这里的OriginalFirstThunk和FirstThunk,其中前者是不可改写的,后者是在加载的时候需要往里填入输入函数地址的。即我哦们这里的输入表修复其实就是往这里面填入输入函数地址。填入的方式为先加载库,根据OriginalFirstThunk来得到函数名或者序号,然后用GetProcAddress来获取函数地址,将得到的地址写入FirstThunk,直到加载完所有的输入函数地址即可。输入表修复了之后,我们接下来要修正重定位了。
先说为啥要做重定位。链接器生成PE文件时有个默认的装载基地址,一般情况下,PE文件都会被加载到这个默认的基地址处,这样就不需要重定位,但是当PE文件被加载到其他地址处时,链接器所登记的地址就不正确了,这样就需要通过重定位来修正。修正重定位的代码如下
//基址重定位
//pImage PE文件加载到内存中的地址
int BaseRelocation(void *pImage)
{
uint32_t uSizeOfBlock = 0;
uint16_t *pTypeOffset = NULL;
uint32_t *pRelocAddr = NULL;
PIMAGE_DOS_HEADER pDosHeader = NULL;
PIMAGE_NT_HEADERS pNTHeader = NULL;
PIMAGE_BASE_RELOCATION pBaseRelocation = NULL;
pDosHeader = (PIMAGE_DOS_HEADER)pImage;
pNTHeader = (PIMAGE_NT_HEADERS)((char*)pDosHeader + pDosHeader->e_lfanew);
pBaseRelocation = (PIMAGE_BASE_RELOCATION)((char*)pDosHeader + pNTHeader->OptionalHeader.DataDirectory.VirtualAddress);
while (pBaseRelocation->VirtualAddress != 0){
uSizeOfBlock = pBaseRelocation->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION);
pTypeOffset = (uint16_t*)((uint32_t)pBaseRelocation + sizeof(IMAGE_BASE_RELOCATION));
for (int i = 0; i < uSizeOfBlock / 2; i ++) {
//类型是IMAGE_REL_BASED_HIGHLOW(高4位表示类型)
if (IMAGE_REL_BASED_HIGHLOW == ((*pTypeOffset & 0xf000) >> 12)){
pRelocAddr = (uint32_t*)((char*)pDosHeader + pBaseRelocation->VirtualAddress + (*pTypeOffset & 0xfff));
*pRelocAddr = (uint32_t)((uint32_t)pDosHeader - pNTHeader->OptionalHeader.ImageBase + *pRelocAddr);
}
pTypeOffset++;
}
pBaseRelocation = (PIMAGE_BASE_RELOCATION)((uint32_t)pBaseRelocation + pBaseRelocation->SizeOfBlock);
}
return ERR_SUCCESS;
}
这里先来了解下重定位的结构
typedef struct _IMAGE_BASE_RELOCATION {
DWORD VirtualAddress;
DWORD SizeOfBlock;
//WORD TypeOffset;
} IMAGE_BASE_RELOCATION;
typedef IMAGE_BASE_RELOCATION UNALIGNED * PIMAGE_BASE_RELOCATION;
这个结构很简单,但是要注意的是,这结构很面跟着一个数组,这个数组每个元素占2字节,其中高4位表示重定位类型,低12位表示重定位地址。我们要做的就是取到重定位数据的指针(和获取输入表指针方式一样),然后计算重定位的项数,最后修改重定位地址,重定位地址的修改方式为库加载到内存的地址+当前重定位地址-默认的基址。
重定位修正之后,我们只需要得到库的入口地址,然后调用即可。到了这里,库加载的流程算了结束了,加载的代码如下
HMODULE MemLoadLibrary(wchar_t *szLibName)
{
uint32_t nResult = 0;
uint64_t lSize = 0;
char *pLibBuf = NULL;
char *pImage = NULL;
DLLMAIN pfnDllMain = NULL;
nResult = ReadLibrary(szLibName, NULL, lSize);
if (ERR_SUCCESS != nResult){
return (HMODULE)ERR_LOAD_LIB;
}
pLibBuf = (char*)malloc(lSize);
if (NULL == pLibBuf) {
return (HMODULE)ERR_LOAD_LIB;
}
memset(pLibBuf, 0, lSize);
nResult = ReadLibrary(szLibName, pLibBuf, lSize);
lSize = GetImageSize(pLibBuf);
if (LoadLibraryImage((void*&)pImage, pLibBuf, lSize)) {
free(pLibBuf);
if (NULL != pImage){
VirtualFree(pImage, 0, MEM_RELEASE);
pImage = NULL;
}
return (HMODULE)ERR_LOAD_LIB;
}
ImportRepair(pImage);
BaseRelocation(pImage);
//获取DllMain函数的地址
pfnDllMain = (DLLMAIN)GetDllMain(pImage);
//调用DllMain函数
pfnDllMain((HMODULE)pImage, DLL_PROCESS_ATTACH, NULL);
return (HMODULE)pImage;
}
库加载了之后,我们还可以提供一个获取函数地址的函数接口,这样当库加载了之后,我们可以通过这个接口来获取函数地址。代码如下
void* MemGetProcAddress(HMODULE hModule, char *szFuncName)
{
uint32_t uFuncAddr = 0;
uint16_t uFuncAddrIndex = 0;
char *szExportFuncName = NULL;
uint32_t *pAddrOfFunctions = NULL;
uint32_t *pAddrOfName = NULL;
uint32_t *pAddrOfNameOrdinals = NULL;
PIMAGE_DOS_HEADER pDosHeader = NULL;
PIMAGE_NT_HEADERS pNTHeader = NULL;
PIMAGE_EXPORT_DIRECTORY pExportDir = NULL;
pDosHeader = (PIMAGE_DOS_HEADER)hModule;
pNTHeader = (PIMAGE_NT_HEADERS)((char*)hModule + pDosHeader->e_lfanew);
pExportDir = (PIMAGE_EXPORT_DIRECTORY)((char*)hModule + pNTHeader->OptionalHeader.DataDirectory.VirtualAddress);
pAddrOfFunctions = (uint32_t*)((char *)hModule + pExportDir->AddressOfFunctions);
pAddrOfName = (uint32_t*)((char *)hModule + pExportDir->AddressOfNames);
pAddrOfNameOrdinals = (uint32_t *)((char *)hModule + pExportDir->AddressOfNameOrdinals);
if (NULL == pExportDir){
return NULL;
}
for (int i = 0; i < pExportDir->NumberOfNames; i ++) {
if (pAddrOfName != 0){
szExportFuncName = (char*)((char*)hModule + pAddrOfName);
if (!strcmp(szExportFuncName, szFuncName)) {
uFuncAddrIndex = pAddrOfNameOrdinals;
uFuncAddr = pAddrOfFunctions;
break;
}
}
}
return ((char*)hModule + uFuncAddr);
}
这个函数的流程是找到输出表,然后在输出表中找到要导出的函数名,根据函数名来取出函数地址,然后将函数地址返回。最后则是释放加载的库,代码如下
void MemFreeLibrary(HMODULE hModule)
{
PIMAGE_DOS_HEADER pDosHeader = NULL;
PIMAGE_NT_HEADERS pNTHeader = NULL;
DLLMAIN pfnDllMain = NULL;
pDosHeader = (PIMAGE_DOS_HEADER)hModule;
pNTHeader = (PIMAGE_NT_HEADERS)((char*)hModule + pDosHeader->e_lfanew);
//获取DllMain函数的地址
pfnDllMain = (DLLMAIN)GetDllMain((char*)hModule);
if (NULL != pfnDllMain){
pfnDllMain(hModule, DLL_PROCESS_DETACH, NULL);
}
VirtualFree((LPVOID)hModule, 0, MEM_RELEASE);
}
到了这里,关于内存加载的流程和代码也介绍完了,想要把过程弄明白,需要有一定的PE格式的知识,建议大家边看先熟悉PE的知识,再来看这个,效果会更好
附件里式写好的代码,感兴趣的可以拿去玩玩
确切的说学完C/C++还不知道LoadLibrary这个函数,要学完'VC++或者Windows系统编程才知道
我是来水经验的…… 看懂你的文章还得学几年?告诉我!:'( 我是来水经验的…… 非常感谢 支持,看起来还是可以的 不觉明历 我是来水经验的……