MemoryModule阅读与PE文件解析(一)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_18218335/article/details/78338884

参考链接

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 的导入表。
       请看下回分解

猜你喜欢

转载自blog.csdn.net/qq_18218335/article/details/78338884
今日推荐