目录
概览
这篇文章接上一篇文章教程:《从内存中加载DLL》,来分析下该项目的代码,项目目录如下

先介绍一下,封装了加载函数及相关头文件的是MemoryModule.c和MemoryModule.h中,而对于怎么使用这些封装好的函数来从内存中加载dll,作者写了一个例子,在example文件夹中

其中DllLoader通过使用MemoryModule中的函数来完成从内存中加载Dll的一个Demo,至于加载的Dll,就位于SampleDll中,也就是SampleDLL.cpp,只是导出了一个很简单的求和函数:
// example/SampleDLL/SampleDLL.cpp #include "SampleDLL.h" extern "C" { SAMPLEDLL_API int addNumbers(int a, int b) { return a + b; } }
所以我们主要来看DllLoader的代码,学习下MemoryModule是怎么使用的,同时对比下正常的使用dll的方式和使用MemoryModule这个项目有什么区别
主项目分析
首先是一些导入和定义:
#include <assert.h> #include <windows.h> #include <tchar.h> #include <stdio.h> #include <malloc.h> #include "../../MemoryModule.h" typedef int (*addNumberProc)(int, int); #define DLL_FILE TEXT("..\\SampleDLL\\SampleDLL.dll")
能说的不多,有一点稍微说下:
typedef int (*addNumberProc)(int, int);
这一行定义了一个addNumberProc类型,这是一个接受两个整型参数,返回值为整型的函数指针类型
接着我们先看一个正常的 从磁盘上加载dll的方式:
void LoadFromFile(void) { addNumberProc addNumber; HRSRC resourceInfo; DWORD resourceSize; LPVOID resourceData; TCHAR buffer[100]; HINSTANCE handle = LoadLibrary(DLL_FILE); if (handle == NULL) return; addNumber = (addNumberProc)GetProcAddress(handle, "addNumbers"); _tprintf(_T("From file: %d\n"), addNumber(1, 2)); resourceInfo = FindResource(handle, MAKEINTRESOURCE(VS_VERSION_INFO), RT_VERSION); _tprintf(_T("FindResource returned 0x%p\n"), resourceInfo); resourceSize = SizeofResource(handle, resourceInfo); resourceData = LoadResource(handle, resourceInfo); _tprintf(_T("Resource data: %ld bytes at 0x%p\n"), resourceSize, resourceData); LoadString(handle, 1, buffer, sizeof(buffer)); _tprintf(_T("String1: %s\n"), buffer); LoadString(handle, 20, buffer, sizeof(buffer)); _tprintf(_T("String2: %s\n"), buffer); FreeLibrary(handle); }
如果我们使用正常的方式加载dll,来使用他的导出函数,常规的操作如下:
- LoadLibrary 装载dll
- GetProcAddress 获得函数地址(这里就可以直接使用了)
- FindResource 找到指定资源块
- SizeofResource 和 LoadResource 加载资源
- LoadString 加载字符串
最后用FreeLibrary将加载进来的dll释放,完成整个的dll从装载到使用再到卸载的全过程. 由于是使用的windowsAPI,所以像是Dll的装载时的展开等都是由系统自动完成的,但用这种方法加载的Dll向系统注册,比较容易暴露我们的行踪,所以我们可以利用下面的方式,手动完成系统完成的所有操作,手动将Dll加载到项目中, 这种手动解析Dll并加载手法就叫做反射
整个DllLoader.c中的主函数只有短短几行:
int main() { LoadFromFile(); printf("\n\n"); LoadFromMemory(); printf("\n\n"); TestCustomAllocAndFree(); return 0; }
LoadFromFile刚刚说过了, 是正常的加载dll的方式, 我们接着看LoadFromMemory函数,如果我们想利用反射的方式从内存中加载Dll,就需要自己实现自己的LoadFromMemory函数
void LoadFromMemory(void) { void *data; size_t size; HMEMORYMODULE handle; addNumberProc addNumber; HMEMORYRSRC resourceInfo; DWORD resourceSize; LPVOID resourceData; TCHAR buffer[100]; data = ReadLibrary(&size); if (data == NULL) { return; } handle = MemoryLoadLibrary(data, size); if (handle == NULL) { _tprintf(_T("Can't load library from memory.\n")); goto exit; } addNumber = (addNumberProc)MemoryGetProcAddress(handle, "addNumbers"); _tprintf(_T("From memory: %d\n"), addNumber(1, 2)); resourceInfo = MemoryFindResource(handle, MAKEINTRESOURCE(VS_VERSION_INFO), RT_VERSION); _tprintf(_T("MemoryFindResource returned 0x%p\n"), resourceInfo); resourceSize = MemorySizeofResource(handle, resourceInfo); resourceData = MemoryLoadResource(handle, resourceInfo); _tprintf(_T("Memory resource data: %ld bytes at 0x%p\n"), resourceSize, resourceData); MemoryLoadString(handle, 1, buffer, sizeof(buffer)); _tprintf(_T("String1: %s\n"), buffer); MemoryLoadString(handle, 20, buffer, sizeof(buffer)); _tprintf(_T("String2: %s\n"), buffer); MemoryFreeLibrary(handle); exit: free(data); }
先是data = ReadLibrary(&size);
我们看该函数的定义:
void* ReadLibrary(size_t* pSize) { size_t read; void* result; FILE* fp; fp = _tfopen(DLL_FILE, _T("rb")); //打开Dll文件 if (fp == NULL) { _tprintf(_T("Can't open DLL file \"%s\"."), DLL_FILE); return NULL; } fseek(fp, 0, SEEK_END); //设置文件指针到文件末尾 *pSize = static_cast<size_t>(ftell(fp)); //获取指针位置(相当于是获取了文件大小) if (*pSize == 0) { fclose(fp); return NULL; } result = (unsigned char *)malloc(*pSize); //开辟文件大小的空间 if (result == NULL) { return NULL; } fseek(fp, 0, SEEK_SET); //重新设置文件指针到文件开头 read = fread(result, 1, *pSize, fp); //将文件内容逐字节的读入到刚刚开辟的内存中 fclose(fp); //关闭文件指针 if (read != *pSize) //如果没有完全读入到内存 { free(result); return NULL; } return result; //返回内存的地址 }
其中*pSize = static_cast<size_t>(ftell(fp));
是C++新引入的一种写法, static_cast是一个关键字,用于显示类型转换, 表示该转换为良性转换,一般不会导致意外发生,风险很低。用法为:
xxx_cast<newType>(data)
同类的关键字说明如下:
关键字 | 说明 |
---|---|
static_cast | 用于良性转换,一般不会导致意外发生,风险很低。 |
const_cast | 用于 const 与非 const、volatile 与非 volatile 之间的转换。 |
reinterpret_cast | 高度危险的转换,这种转换仅仅是对二进制位的重新解释,不会借助已有的转换规则对数据进行调整,但是可以实现最灵活的 C++ 类型转换。 |
dynamic_cast | 借助 RTTI,用于类型安全的向下转型(Downcasting)。 |
ReadLibrary函数只是简单的完成了将文件复制到一块内存中的操作,没什么可说的,接着逐步来到了重点MemoryLoadLibrary函数,该函数位于MemoryModule.c中,函数定义如下:
HMEMORYMODULE MemoryLoadLibrary(const void *data, size_t size) { return MemoryLoadLibraryEx(data, size, MemoryDefaultAlloc, MemoryDefaultFree, MemoryDefaultLoadLibrary, MemoryDefaultGetProcAddress, MemoryDefaultFreeLibrary, NULL); }
而MemoryLoadLibraryEx则是一个关键的函数, 比较长,我们一段一段看, 首先, 是参数:
HMEMORYMODULE MemoryLoadLibraryEx(const void *data, size_t size, CustomAllocFunc allocMemory, CustomFreeFunc freeMemory, CustomLoadLibraryFunc loadLibrary, CustomGetProcAddressFunc getProcAddress, CustomFreeLibraryFunc freeLibrary, void *userdata)
其中,data和size是调用者提供的, 刚刚分析过, 分别代表被复制进内存中的Dll的地址起始指针 和 该块地址的大小(文件的大小), 而后传递的是自定类型的函数指针,定义分别如下:
//MemoryModule.h typedef LPVOID (*CustomAllocFunc)(LPVOID, SIZE_T, DWORD, DWORD, void*); typedef BOOL (*CustomFreeFunc)(LPVOID, SIZE_T, DWORD, void*); typedef HCUSTOMMODULE (*CustomLoadLibraryFunc)(LPCSTR, void *); typedef FARPROC (*CustomGetProcAddressFunc)(HCUSTOMMODULE, LPCSTR, void *); typedef void (*CustomFreeLibraryFunc)(HCUSTOMMODULE, void *);
而作为参数传递的,则是定义在MemoryModule.h中的几个函数:

其具体定义在MemoryModule.c中,注释也写得很清楚,默认的函数实现是直接调用系统API的:

把这几个函数作为参数传递进MemoryLoadLibrary,主要是为了修补,先接着看:
#ifdef _WIN64 //如果是Win64,则定义blockedMemory POINTER_LIST *blockedMemory = NULL; #endif if (!CheckSize(size, sizeof(IMAGE_DOS_HEADER))) { // 确保文件大小大于IMAGE_DOS_HEADER得长度 return NULL; } dos_header = (PIMAGE_DOS_HEADER)data; //dos_header 指向data首地址,也就是PIMAGE_DOS_HEADER结构得首地址 if (dos_header->e_magic != IMAGE_DOS_SIGNATURE) { //根据dos_header得e_magic字段判断文件类型,应该为0x5A4D, 也就是"MZ" SetLastError(ERROR_BAD_EXE_FORMAT); return NULL; } if (!CheckSize(size, dos_header->e_lfanew + sizeof(IMAGE_NT_HEADERS))) { //确保文件大小大于DOS头+PE头的大小(dos_header->e_lfanew存有PE头相对于基址的偏移地址 return NULL; } old_header = (PIMAGE_NT_HEADERS)&((const unsigned char *)(data))[dos_header->e_lfanew]; //取PE头地址放入old_header变量 if (old_header->Signature != IMAGE_NT_SIGNATURE) { // 判断PE头的Signature,应为0x00004550,也就是"PE00" SetLastError(ERROR_BAD_EXE_FORMAT); return NULL; } if (old_header->FileHeader.Machine != HOST_MACHINE) { //判断PE头中的FileHeader项的Machine字节是否符合当前的机器位数(该项指出dll可以运行的位数 32位/64位) SetLastError(ERROR_BAD_EXE_FORMAT); return NULL; } if (old_header->OptionalHeader.SectionAlignment & 1) { 判断OptionalHeader的SectionAlignment项是否是2的倍数(该项标示PE文件内区块的对齐大小,一般为0x200或0x1000) // Only support section alignments that are a multiple of 2 SetLastError(ERROR_BAD_EXE_FORMAT); return NULL; }
上面的代码中有两点需要说的:
old_header = (PIMAGE_NT_HEADERS)&((const unsigned char *)(data))[dos_header->e_lfanew]
这里使用了数组的方式来取值,由于[]的优先级比&高,所以这里是先把data转换成const unsigned char *的指针,起始也就是一个const unsigned char 的数组,这个dos_header->e_lfanew中存的就是PE头相对于dos头的偏移,所以直接[dos_header->e_lfanew], 相当于是从开始往后取了偏移个字节数的地址的值,想取地址还需要加一步&,此时指针就指向了PE头,转换成相应的类型即可- 关于HOST_MACHINE ,该宏定义被写在了开头:
#ifdef _WIN64 #define HOST_MACHINE IMAGE_FILE_MACHINE_AMD64 #else #define HOST_MACHINE IMAGE_FILE_MACHINE_I386 #endif
接着就是
section = IMAGE_FIRST_SECTION(old_header);
先看IMAGE_FIRST_SECTION这个函数,其实这是一个宏定义,该宏被定义再winnt.h中,如下:
#define IMAGE_FIRST_SECTION( ntheader ) ((PIMAGE_SECTION_HEADER) \
((ULONG_PTR)(ntheader) + \
FIELD_OFFSET( IMAGE_NT_HEADERS, OptionalHeader ) + \
((ntheader))->FileHeader.SizeOfOptionalHeader \
))
FIELD_OFFSET也是个宏,用于将第二个参数相对于第一个参数的偏移求出,所以这个IMAGE_FIRST_SECTION得到的地址就是Dll的基地址+OptionalHeader到基地址的偏移+OptionalHeader的长度,即取到了OptionalHeader后紧跟着的一块区域,如下图的PE文件结构,其实也就是区块的地址,符合宏的名字映像第一个区块地址

这里面有个比较有意思的东西在于FIELD_OFFSET的定义:
#define FIELD_OFFSET(type, field) ((LONG)(LONG_PTR)&(((type *)0)->field))
这里用到了(type *)0
这种写法,这里是好像是在0地址处存储了一个type类型的结构体,然后取到了指定成员的地址,由于起始地址是0,所以自然取到的成员地址就等于该成员在结构体内的偏移,这种写法我也是第一次见,这里记录下
ok,言归正传,接着说:
optionalSectionSize = old_header->OptionalHeader.SectionAlignment; for (i=0; i<old_header->FileHeader.NumberOfSections; i++, section++) { //NumberOfSections存储了所有区块的个数 size_t endOfSection; if (section->SizeOfRawData == 0) { // Section without data in the DLL endOfSection = section->VirtualAddress + optionalSectionSize; } else { endOfSection = section->VirtualAddress + section->SizeOfRawData; } if (endOfSection > lastSectionEnd) { lastSectionEnd = endOfSection; } }
optionalSectionSize取到了SectionAlignment,这里存放的是区块被装入内存时的大小,需要该值以用于后续的展开,然后遍历所有区块,找到区块大小等于0的区块,该区块标志着区块的结束,最终endOfSection指向的是最后一个区块的结尾的地址(这里的地址是RVA)。
GetNativeSystemInfo(&sysInfo);
用于获取系统信息,一般是用于判断系统位数,以执行不同的代码
alignedImageSize = AlignValueUp(old_header->OptionalHeader.SizeOfImage, sysInfo.dwPageSize); //dwPageSize指定页面的大小和页面保护和委托的颗粒,也就是被 VirtualAlloc 函数使用的页大小 if (alignedImageSize != AlignValueUp(lastSectionEnd, sysInfo.dwPageSize)) { //判断文件大小是否等于最后一个区块前所有部分的大小(二者都取整后的) SetLastError(ERROR_BAD_EXE_FORMAT); return NULL; }
AlignValueUp是自定义的一个宏:
static inline size_t AlignValueUp(size_t value, size_t alignment) {
return (value + alignment - 1) & ~(alignment - 1);
}
这里取到了value的向上取整(加了一个页大小,然后末尾清零了)的值
至此位置,前期的一些准备工作已经完成了,接下来就要步入真正的内容了。
敬请期待下一篇~
番外:
看项目过程中遇到的一些知识
#pragma warning用法
#if _MSC_VER // Disable warning about data -> function pointer conversion #pragma warning(disable:4055) // C4244: conversion from 'uintptr_t' to 'DWORD', possible loss of data. #pragma warning(error: 4244) // C4267: conversion from 'size_t' to 'int', possible loss of data. #pragma warning(error: 4267) #define inline __inline #endif #ifdef _WIN64 #define HOST_MACHINE IMAGE_FILE_MACHINE_AMD64 #else #define HOST_MACHINE IMAGE_FILE_MACHINE_I386 #endif
_MSC_VER是一个MSVC的内置宏,定义了编译器的版本,所以这段的意思就是,如果是用微软的C编译器编译该段程序,则设定#pragma warning等
这里涉及的#pragma warning的用法如下:
#pragma warning(disable:4055)//不显示4055号警告信息
#pragma warning(error:4244)//把4244号警告信息作为一个错误。
然后定义了inline,至于这里每一个错误号分别对应什么错误,注释已经给的很清楚了
extern "C"用法
#ifdef __cplusplus extern "C" { #endif ... ... #ifdef __cplusplus } #endif
的方式来确保编译器不会对其中的函数名进行重命名,每一个函数的用法都有详细的注释
文章评论