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

参考链接

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文件日期时间戳,指这个文件生成的时间
PointerToSymbolTableCoff 调试符号表的偏移地址
NumberOfSymbolsCoff 符号表中符号的个数,这个d域在realease 中为0
SizeOfOptionalHeader可选头结构大小
Characteristics表示了文件的一些特征。

      比如对于一个可执行文件而言,分离调试文件是如何操作的。调试器通常使用的方法是将调试信息从PE文件中分离,并保存到一个调试文件(.DBG)中。调试器需要了解是否要在一个单独的文件中寻找调试信息,以及这个文件是否已经将调试信息分离了。要使调试器不在文件中查找的话,就需要用到IMAGE_FILE_DEBUG_STRIPPED这个特征,它表示文件的调试信息是否已经被分离了。调试器可以通过快速查看PE文件的头部的方法来决定文件中是否存在着调试信息。

标志含义
IMAGE_FILE_RELOCS_STRIPPED0x0001文件中不存在重定位信息
IMAGE_FILE_EXECUTABLE_IMAGE0x0002文件是可执行的
IMAGE_FILE_LINE_NUMS_STRIPPED0x0004不存在行信息
IMAGE_FILE_LOCAL_SYMS_STRIPPED0x0008不存在符号信息
IMAGE_FILE_AGGRESIVE_WS_TRIM0x0010
IMAGE_FILE_LARGE_ADDRESS_AWARE0x0020可访问大于2GB 的地址
IMAGE_FILE_BYTES_REVERSED_LO0x0080小尾方式
IMAGE_FILE_32BIT_MACHINE0x0100只在32位平台运行
IMAGE_FILE_DEBUG_STRIPPED0x0200不包含调试信息
IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP0x0400不能从可移动盘运行
IMAGE_FILE_NET_RUN_FROM_SWAP0x0800不能从网络运行
IMAGE_FILE_SYSTEM0x1000系统文件不能直接运行
IMAGE_FILE_DLL0x2000DLL文件
IMAGE_FILE_UP_SYSTEM_ONLY0x4000不能在多处理器上运行
IMAGE_FILE_BYTES_REVERSED_HI0x8000大尾方式

      由此我们可以理解,此程序只处理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开关改变这个值
SectionAlignmentWindows NT虚拟内存管理器规定,段对齐不能少于页尺寸(当前的x86平台是4096字节),并且必须是成倍的页尺寸。4096字节是x86链接器的默认值,但是它可以通过-ALIGN: linker开关来设置
FileAlignment2.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_TLSTLS目录
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 的导入表。
       请看下回分解

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
要查看DLL的源码,你可以使用C#反编译工具,比如JetBrains dotPeek。首先,你需要下载并安装这个工具。然后,打开dotPeek并点击菜单栏的【File->Open】按钮选择要反编译的DLL文件。一旦你打开了DLL文件,你就可以在dotPeek中查看并浏览源码了。 另外,如果你想了解更多关于内存加载并运行DLL函数的内容,你可以查看MemoryModule的开源源码。这个工具可以在Windows系统中加载并运行DLL函数。你可以在VC2015中打开编译的源码文件。 总之,通过使用反编译工具和开源源码,你可以查看和修改已编译的DLL文件的源代码,从而获取你想要的结果。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [关于 C# dll文件的反编译获取源码](https://blog.csdn.net/qq_36694133/article/details/116519118)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 33.333333333333336%"] - *2* [MemoryModule开源源码,windows内存加载并运行DLL函数,VC++2015打开编译的](https://download.csdn.net/download/airen3339/88229044)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 33.333333333333336%"] - *3* [几种工具反编译被编译好的DLL文件](https://blog.csdn.net/weixin_34015860/article/details/93233036)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 33.333333333333336%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值