参考链接
https://github.com/fancycode/MemoryModule
本文阅读github 上MemoryModule 代码的同时,介绍PE 文件相关的基础知识。
该项目实现“手动加载DLL”即“实现了自己的LoadLibrary函数”,将DLL 加载到内存中,然后进行常规的DLL 操作。
第一步,通过调用LoadLibrary 函数加载DLL 并进行一些常规的DLL 操作
如得到函数地址并运行,还有得到DLL 资源等相关操作。第二步,调用自己实现的LoadLibrary函数
主函数读取并得到文件大小,然后调用MemoryLoadLibrary(data,size)函数,函数内部调用MemoryLoadLibraryEx函数,此函数为一个加载的核心函数。函数中会不断调用CheckSize函数判断文件大小是否正确,其原理就是,文件真实大小不能小于DLL 文件中所有结构的大小的综合,每次得到数据大小时,总会核对文件大小,我们不再一一介绍。
图片来源:http://blog.csdn.net/liuyez123/article/details/51281905
首先观察上面这张图片,了解PE 文件的整体结构,便于理解下面的介绍。
dos_header = (PIMAGE_DOS_HEADER)data;
if (dos_header->e_magic != IMAGE_DOS_SIGNATURE)
// PE 文件刚开始为MS-DOS头,其结构体定义如下:
typedef struct_IMAGE_DOS_HEADER{ // DOS.EXE header
WORD e_magic; //魔术数字0x5A4D 表示‘MZ’
WORD e_cblp; //文件最后页的字节数
WORD e_cp; //文件页数
WORD e_crlc; //重定义元素个数
WORD e_cparhdr; //头部尺寸,以段落为单位
WORD e_minalloc; //所需的最小附加段
WORD e_maxalloc; //所需的最大附加段
WORD e_ss; //初始的 SS值(相对偏移量)
WORD e_sp; //初始的 SP值
WORD e_csum; //校验和
WORD e_ip; //初始的IP值
WORD e_cs; //初始的 CS值(相对偏移量)
WORD e_lfarlc; //重分配表文件地址
WORD e_ovno; //覆盖号
WORD e_res[4]; //保留字
WORD e_oemid; //OEM identifier (for e_oeminfo)
WORD e_oeminfo; //OEM information; e_oemid specific
WORD e_res2[10]; //保留字
LONG e_lfanew; //新 exe头部的文件地址(PE文件头部的定位,偏移量)
} IMAGE_DOS_HEADER,*PIMAGE_DOS_HEADER;
文件头刚开始的两个字节为“MZ”,还有许多其它的域对于MS-DOS操作系统来说都有用,对于Windows NT来说,最后一个域e_lfnew,一个4字节的文件偏移量,PE文件头部就是由它定位的。PE文件头部是紧跟在MS-DOS头部和实模式程序残余之后的。
所谓是模式程序残余,指的是一个在装载时能够被MS-DOS运行的实际程序,对于一个MS-DOS 的可执行映像文件,应用程序从这里执行的。这部分数据包含了一些加密数据,标识编译这个PE 文件的组件。可用来检举某些病毒程序所编译的程序来自哪台机器。
old_header = (PIMAGE_NT_HEADERS)&((constunsignedchar *)(data))[dos_header->e_lfanew];
// 得到NT 头,所谓NT 头,由识别标识,文件头和可选头三部分组成的。
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
if (old_header->Signature != IMAGE_NT_SIGNATURE)
// #define IMAGE_NT_SIGNATURE 0x00004550 // PE00
// 只有标识为IMAGE_NT_SIGNATURE标识的程序我们才进行处理,即普通的PE 文件。
if (old_header->FileHeader.Machine != HOST_MACHINE)
#define HOST_MACHINEIMAGE_FILE_MACHINE_I386
//首先我们来看文件头的结构:
typedef struct_IMAGE_FILE_HEADER{
WORD Machine; // 用来表示这个可执行文件被构建的目标机器种类,由此我们知道该程序支持X86 程序
WORD NumberOfSections;
DWORD TimeDateStamp;
DWORD PointerToSymbolTable;
DWORD NumberOfSymbols;
WORD SizeOfOptionalHeader;
WORD Characteristics;
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
成员 | 含义 |
---|---|
NumberOfSections | 段的数目 |
TimeDateStamp | 文件日期时间戳,指这个文件生成的时间 |
PointerToSymbolTable | Coff 调试符号表的偏移地址 |
NumberOfSymbols | Coff 符号表中符号的个数,这个d域在realease 中为0 |
SizeOfOptionalHeader | 可选头结构大小 |
Characteristics | 表示了文件的一些特征。 |
比如对于一个可执行文件而言,分离调试文件是如何操作的。调试器通常使用的方法是将调试信息从PE文件中分离,并保存到一个调试文件(.DBG)中。调试器需要了解是否要在一个单独的文件中寻找调试信息,以及这个文件是否已经将调试信息分离了。要使调试器不在文件中查找的话,就需要用到IMAGE_FILE_DEBUG_STRIPPED这个特征,它表示文件的调试信息是否已经被分离了。调试器可以通过快速查看PE文件的头部的方法来决定文件中是否存在着调试信息。
标志 | 值 | 含义 |
---|---|---|
IMAGE_FILE_RELOCS_STRIPPED | 0x0001 | 文件中不存在重定位信息 |
IMAGE_FILE_EXECUTABLE_IMAGE | 0x0002 | 文件是可执行的 |
IMAGE_FILE_LINE_NUMS_STRIPPED | 0x0004 | 不存在行信息 |
IMAGE_FILE_LOCAL_SYMS_STRIPPED | 0x0008 | 不存在符号信息 |
IMAGE_FILE_AGGRESIVE_WS_TRIM | 0x0010 | |
IMAGE_FILE_LARGE_ADDRESS_AWARE | 0x0020 | 可访问大于2GB 的地址 |
IMAGE_FILE_BYTES_REVERSED_LO | 0x0080 | 小尾方式 |
IMAGE_FILE_32BIT_MACHINE | 0x0100 | 只在32位平台运行 |
IMAGE_FILE_DEBUG_STRIPPED | 0x0200 | 不包含调试信息 |
IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP | 0x0400 | 不能从可移动盘运行 |
IMAGE_FILE_NET_RUN_FROM_SWAP | 0x0800 | 不能从网络运行 |
IMAGE_FILE_SYSTEM | 0x1000 | 系统文件不能直接运行 |
IMAGE_FILE_DLL | 0x2000 | DLL文件 |
IMAGE_FILE_UP_SYSTEM_ONLY | 0x4000 | 不能在多处理器上运行 |
IMAGE_FILE_BYTES_REVERSED_HI | 0x8000 | 大尾方式 |
由此我们可以理解,此程序只处理Win32 DLL。
if (old_header->OptionalHeader.SectionAlignment& 1)
首先我们来看可选头的结构体定义:
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_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
标准域
所谓标准域,就是和UNIX可执行文件的COFF格式所公共的部分。虽然标准域保留了COFF中定义的名字,但是Windows NT仍然将它们用作了不同的目的。
成员 | 含义 |
---|---|
AddressOfEntryPoint | 这个域表示应用程序入口点的位置 |
BaseOfCode | 已载入映像的代码(“.text”段)的相对偏移量 |
BaseOfData | 已载入映像的未初始化数据(“.bss”段)的相对偏移量 |
Windows NT附加域 | 为Windows NT特定的进程行为提供了装载器的支持 |
ImageBase | 进程映像地址空间中的首选基地址。Windows NT的Microsoft Win32 SDK链接器将这个值默认设为0x00400000,但是你可以使用-BASE:linker开关改变这个值 |
SectionAlignment | Windows NT虚拟内存管理器规定,段对齐不能少于页尺寸(当前的x86平台是4096字节),并且必须是成倍的页尺寸。4096字节是x86链接器的默认值,但是它可以通过-ALIGN: linker开关来设置 |
FileAlignment | 2.39版链接器将映像文件以0x200字节的边界对齐,这个值可以被强制改为512到65535这么多 |
SizeOfImage | 链接器首先决定每个段需要多少字节,并且最后将页面总数向上取整至最接近的 |
SectionAlignment | 边界,然后总数就是每个段个别需求之和了 |
SizeOfHeaders | 这个域表示文件中有多少空间用来保存所有的文件头部,包括MS-DOS头部、PE文件头部、PE可选头部以及PE段头部。文件中所有的段实体就开始于这个位置 |
CheckSum | 用来在装载时验证可执行文件的,它是由链接器设置并检验的。由于创建这些校验和的算法是私有信息,所以在此不进行讨论 |
Subsystem | 每个可能的子系统取值列于WINNT.H的IMAGE_OPTIONAL_HEADER结构之后。 |
DllCharacteristics | 用来表示一个DLL映像是否为进程和线程的初始化及终止包含入口点的标记 |
SizeOfStackReserve、SizeOfStackCommit、SizeOfHeapReserve、SizeOfHeapCommit | 这些域控制要保留的地址空间数量,并且负责栈和默认堆的申请。在默认情况下,栈和堆都拥有1个页面的申请值以及16个页面的保留值。这些值可以使用链接器开关-STACKSIZE:与-HEAPSIZE:来设置 |
LoaderFlags | 告知装载器是否在装载时中止和调试,或者默认地正常运行 |
NumberOfRvaAndSizes | 标识了接下来的DataDirectory数组。请注意它被用来标识这个数组,而不是数组中的各个入口数字,这一点非常重要 |
DataDirectory | 数据目录表示文件中其它可执行信息重要组成部分的位置。它事实上就是一个IMAGE_DATA_DIRECTORY结构的数组,位于可选头部结构的末尾。当前的PE文件格式定义了16种可能的数据目录,这之中的11种现在在使用中 |
数据目录 | 含义 |
---|---|
IMAGE_DIRECTORY_ENTRY_EXPORT | 导出目录 |
IMAGE_DIRECTORY_ENTRY_IMPORT | 导入目录 |
IMAGE_DIRECTORY_ENTRY_RESOURCE | 资源目录 |
IMAGE_DIRECTORY_ENTRY_EXCEPTION | 异常目录 |
IMAGE_DIRECTORY_ENTRY_SECURITY | 安全目录 |
IMAGE_DIRECTORY_ENTRY_BASERELOC | 重定位基本表 |
IMAGE_DIRECTORY_ENTRY_DEBUG | 调试目录 |
IMAGE_DIRECTORY_ENTRY_COPYRIGHT | 描述字串 |
IMAGE_DIRECTORY_ENTRY_GLOBALPTR | 机器值(MIPS GP) |
IMAGE_DIRECTORY_ENTRY_TLS | TLS目录 |
IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG | 载入配置目录 |
typedef struct_IMAGE_DATA_DIRECTORY {
ULONG VirtualAddress;
ULONG Size;
}IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
每个数据目录入口指定了该目录的尺寸和相对虚拟地址。如果你要定义一个特定的目录的话,就需要从可选头部中的数据目录数组中决定相对的地址,然后使用虚拟地址来决定该目录位于哪个段中。一旦你决定了哪个段包含了该目录,该段的段头部就会被用于查找数据目录的精确文件偏移量位置。
这里我们看到,程序只处理文件对齐粒度为2 的倍数的DLL,一般程序的默认对齐粒度为0x200,这里的这一步判断可能是作者遇到过特殊情况吧。
section = IMAGE_FIRST_SECTION(old_header);
#define IMAGE_FIRST_SECTION( ntheader ) ((PIMAGE_SECTION_HEADER) \
((ULONG_PTR)(ntheader) + \
FIELD_OFFSET( IMAGE_NT_HEADERS, OptionalHeader ) + \
((ntheader))->FileHeader.SizeOfOptionalHeader \
))
为了理解这个宏,我们来看这个图,重复一下这个图,省得老跳转了。根据上面的结构体的定义我们可以看到,文件头中有一个成员SizeOfOptionalHeader 包含了可选头大小,而可选头之后就是段头部(图中叫节表)。因此第一个段头部的地址=NT头 + 可选头在NT头中的偏移+可选头的大小。
其中段头部的定义如下
#define IMAGE_SIZEOF_SHORT_NAME 8
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME];
union {
DWORD PhysicalAddress;
DWORD VirtualSize;
} Misc;
DWORD VirtualAddress;
DWORD SizeOfRawData;
DWORD PointerToRawData;
DWORD PointerToRelocations;
DWORD PointerToLinenumbers;
WORD NumberOfRelocations;
WORD NumberOfLinenumbers;
DWORD Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
成员 | 含义 |
---|---|
Name | 每个段都有一个8字符长的名称域,并且第一个字符必须是一个句点 |
VirtualSize | 实际的,被使用的区块的大小。VirtualSize 大于SizeOfRawData 则SizeOfRawData 表示来自于可执行文件初始化数据的大小与VirtualSize 相差的字节用0 填充。这个字段在OBJ文件中设为0. |
VirtualAddress | 该块装载到内存中的RVA,按照内存页对齐的。 |
SizeOfRawData | 该块在磁盘文件中所占的大小,数值等于VirtualSize按照FileAlignment对齐后的大小 |
PointerToRawData | 一个文件中段实体位置的偏移量 |
Characteristics | 段属性值 |
属性值 | 含义 |
---|---|
0x00000020 | 代码段 |
0x00000040 | 已初始化数据段 |
0x00000080 | 未初始化数据段 |
0x04000000 | 该段数据不能被缓存 |
0x08000000 | 该段不能被分页 |
0x10000000 | 共享段 |
0x20000000 | 可执行段 |
0x40000000 | 可读段 |
0x80000000 | 可写段 |
optionalSectionSize =old_header->OptionalHeader.SectionAlignment;
for (i=0; i<old_header->FileHeader.NumberOfSections;i++, section++) {
size_t endOfSection;
if (section->SizeOfRawData == 0) {
// Section without data in the DLL
// 对于没有初始化的数据的section,我们默认使用的一个页面的大小
endOfSection =section->VirtualAddress + optionalSectionSize;
} else {
endOfSection =section->VirtualAddress + section->SizeOfRawData;
}
if (endOfSection > lastSectionEnd) {
lastSectionEnd =endOfSection;
}
}
GetNativeSystemInfo(&sysInfo);
alignedImageSize =AlignValueUp(old_header->OptionalHeader.SizeOfImage, sysInfo.dwPageSize);
if (alignedImageSize != AlignValueUp(lastSectionEnd,sysInfo.dwPageSize)) {
SetLastError(ERROR_BAD_EXE_FORMAT);
return NULL;
}
上面我们看到,ImageSize 的定义是:链接器首先决定每个段需要多少字节,并且最后将页面总数向上取整至最接近的SectionAlignment边界,然后总数就是每个段个别需求之和了。而这里的for循环遍历所有的段,找到最大的lastSectionEnd,即文件最后的段的最后的地址,这里的虚拟地址,代表的是文件被加载进内存之后相对于imagebase 的相对地址,即已经对于内存物理页面对齐之后的相对虚拟地址,加上这个段的相对大小,就是其加载进内存之后最后的虚拟地址,即有效访问的最后的相对虚拟地址,lastSectionEnd 对 页面大小向上取整是应该等于ImageSize对页面大小向上取整的,或者说,应该等于ImageSize ,因为ImageSize已经做过了向上取整的操作。核对了这个大小之后继续操作
然后这里有一个拷贝段的操作CopySections,我们等到后面的一个文章中与另一个点一起介绍。
locationDelta = (ptrdiff_t)(result->headers->OptionalHeader.ImageBase- old_header->OptionalHeader.ImageBase);
if (locationDelta != 0) {
result->isRelocated = PerformBaseRelocation(result, locationDelta);
} else {
result->isRelocated = TRUE;
}
这里首先计算得到当前实际加载的DLL基地址与一个原先假定的DLL 加载基地址只差。然后判断如果两个地址相同,直接跳过,否则进行重定向表的修订操作。
所谓重定向表:
简单来说,因为DLL 中的代码需要引用一些DLL 内部的内存地址,但是生成DLL 文件的时候,无法保证将来DLL 被加载到目标进程的什么内存地址,于是,DLL 中假定了一个加载地址即ImageBase,其所有的对于DLL内部地址空间的引用都是相对于这个ImageBase的,为了将来能够修正这些内存地址并缩小文件大小,重定向表中以页面大小【4KB】为单位将文件分为一个个块来存储重定向信息:
typedef struct _IMAGE_BASE_RELOCATION {
DWORD VirtualAddress;
DWORD SizeOfBlock;
// WORD TypeOffset[1];
} IMAGE_BASE_RELOCATION;
VirtualAddress字段是当前页面起始地址的RVA值,本块中所有重定位项中的12位地址加上这个起始地址后就得到了真正的RVA值。SizeOfBlock字段定义的是当前重定位块的大小,从这个字段的值可以算出块中重定位项的数量,由于SizeOfBlock=4+4+2×n,也就是sizeof IMAGE_BASE_RELOCATION+2×n,所以重定位项的数量n就等于(SizeOfBlock-sizeof IMAGE_BASE_RELOCATION)÷2。
IMAGE_BASE_RELOCATION结构后面跟着的n个字就是重定位项,每个重定位项的16位数据位中的低12位就是需要重定位的数据在页面中的地址,剩下的高4位也没有被浪费,它们被用来描述当前重定位项的种类。
虽然高4位定义了多种重定位项的属性,但实际上在PE文件中只能看到0和3这两种情况。
所有的重定位块最终以一个VirtualAddress字段为0的IMAGE_BASE_RELOCATION结束。
参考:http://www.blogfshare.com/pe-relocate.html
重定项的修正:
PerformBaseRelocation(PMEMORYMODULEmodule,ptrdiff_tdelta)
{
unsigned char *codeBase = module->codeBase;
PIMAGE_BASE_RELOCATION relocation;
PIMAGE_DATA_DIRECTORY directory =GET_HEADER_DICTIONARY(module,IMAGE_DIRECTORY_ENTRY_BASERELOC);
if (directory->Size == 0) {
return (delta == 0);
}
relocation = (PIMAGE_BASE_RELOCATION) (codeBase + directory->VirtualAddress);
for (; relocation->VirtualAddress > 0; ) {
DWORD i;
unsigned char *dest = codeBase + relocation->VirtualAddress;
unsigned short *relInfo = (unsigned short*) OffsetPointer(relocation, IMAGE_SIZEOF_BASE_RELOCATION);
for (i=0; i<((relocation->SizeOfBlock-IMAGE_SIZEOF_BASE_RELOCATION) / 2); i++, relInfo++) {
// the upper 4 bits define the type of relocation
int type = *relInfo>> 12;
// the lower 12 bits define the offset
int offset = *relInfo& 0xfff;
switch (type)
{
case IMAGE_REL_BASED_ABSOLUTE:
// skip relocation
break;
case IMAGE_REL_BASED_HIGHLOW:
// change complete 32 bit address
{
DWORD *patchAddrHL = (DWORD *) (dest + offset);
*patchAddrHL += (DWORD)delta;
}
break;
#ifdef _WIN64
caseIMAGE_REL_BASED_DIR64:
{
ULONGLONG *patchAddr64 =(ULONGLONG *) (dest + offset);
*patchAddr64 += (ULONGLONG)delta;
}
break;
#endif
default:
//printf("Unknown relocation: %d\n", type);
break;
}
}
// advance to next relocation block
relocation = (PIMAGE_BASE_RELOCATION) OffsetPointer(relocation, relocation->SizeOfBlock);
}
return TRUE;
}
修正DLL 中的重定项之后,程序扫描并构建DLL 的导入表。
请看下回分解