多字节存储的顺序

多字节数据存储的顺序与CPU有关,微处理中存放数据的顺序有正序(大端Big-Endian)和逆 序(大端Little-Endian),也就是大端与小端。一般英特尔处理器是小端,其它的一般是大 端。例如:将12345678H写入以1000H开始的内存中,如下图所示:

Unicode编码是16位的也就是两个字节,范围在0~65535,它包含了ASCII码。

Kernel32.dll:控制着系统的内存管理、数据的输入输出操作和中断处理,当windows启动时,Kernel32.dll就驻留在内存中特定的写保护区域,使别的程序无法占用这个内存区域。

user32.dll:windows用户界面相关应用程序接口,用于包括windows处理,基本用户界面等特性,如创建窗口和发送消息。

句柄:是整个windows编程的基础,一个句柄是指使用的一个唯一的整数值。windows使用了大量的句柄来标志很多对象。

PE的意思就是Portable Executable【可移置的执行体】它是windows环境自身所带的执行体文件格式。它的一些特性继承自Unix的文件格式,意味着是跨平台的,任何win32平台的PE装载器都能识别和使用该文件格式。

PE文件的内容被分割为不同的区块【Section,又称为区段,节等】,块中包含代码和数据,每个区块都有自己在内存中的属性,即可读/写,只读等。

.text是在编译或汇编结束时产生的一种块,它的内容全是指令代码。
.rdata是运行期只读数据
.data是初始化的数据块
.idata包含其它外业DLL的函数及数据信息,即输入表
.rsrc包含模块的全部资源:如图标,菜单,位图等。

PE非常好的一个地方就是在硬盘上的数据结构与在内存中的结构是一致的,当系统装载一个可执行文件到内存中,主要就是将一个PE文件的某一部分映射到地址空间中,这样PE文析的数据结构在硬盘和内存中就是一样的了。

入口点【Entry Point】PE文件执行时的入口点,也就是说程序在执行时的第一行代码地址应该是这个值。

文件偏移地址【File Offset】当PE文件储存在磁盘上的时候,各数据的地址称作文件的偏移地址。文件偏移地址从PE文件的第一个字节开始计数,起始值为0

虚拟地址【Virtual Address,VA】又称为内存偏移地址【Memory Offset】由于windows程序运行在保护模式下,所以应用程序访问存储器所使用的逻辑地址称为虚拟地址(因此它不是真正的特地理地址,真正的地物理地址被保护机制保护起来了。)

基地址【ImageBase】文件执行时将被映射到指定内存地址中,这个初始地址称为基地址。这个值由PE文件本身设定的。默认设置,用VC++建立的EXE文件基地址是00400000H,DLL文件基地址是10000000H,但这个值可以在编译器中设定的。这个基地址就是程序的一个句柄。

PE格式的定义主要地方位于winnt.h文件中,这个头文件中几乎能找到关于PE文件的所有定义。

应用程序文件的内容被分割为不同的区块,块中包含代码和数据,各个区块按页边界来对齐,区块没有大小限制,是一个连续的结构。PE文件结构的操作系统中执行时,PE装载器将从IMAGE_DOS_HEADER结构中的e_lfanew字段里找到PE Header起始偏移量,加上基地址就得到PE文件头的指针。

DOS头结构说明

PE Header是PE相关结构NT映像头【IMAGE_NT_HEADER】的简称,里面包含着许多PE装载器用到的重要字段。

typedef struct _IMAGE_OPTIONAL_HEADER {   
 //    
// Standard fields.      
//
+18h    WORD    Magic;         // 标志字, ROM 映像(0107h),普通可执行文件(010Bh)+1Ah    BYTE      MajorLinkerVersion;     // 链接程序的主版本号
+1Bh    BYTE      MinorLinkerVersion;     // 链接程序的次版本号
+1Ch    DWORD   SizeOfCode;     // 所有含代码的节的总大小
+20h    DWORD   SizeOfInitializedData;    // 所有含已初始化数据的节的总大小
+24h    DWORD   SizeOfUninitializedData; // 所有含未初始化数据的节的大小
+28h    DWORD   AddressOfEntryPoint;    // 程序执行入口RVA
+2Ch    DWORD   BaseOfCode;      // 代码的区块的起始RVA
+30h    DWORD   BaseOfData;      // 数据的区块的起始RVA    
//    
// NT additional fields.    以下是属于NT结构增加的领域。    
//
+34h    DWORD   ImageBase;      // 程序的首选装载地址
+38h    DWORD   SectionAlignment;      // 内存中的区块的对齐大小
+3Ch    DWORD   FileAlignment;      // 文件中的区块的对齐大小
+40h    WORD    MajorOperatingSystemVersion;  // 要求操作系统最低版本号的主版本号+42h    WORD    MinorOperatingSystemVersion;  // 要求操作系统最低版本号的副版本号+44h    WORD    MajorImageVersion;       // 可运行于操作系统的主版本号
+46h    WORD    MinorImageVersion;       // 可运行于操作系统的次版本号
+48h    WORD    MajorSubsystemVersion;  // 要求最低子系统版本的主版本号
+4Ah    WORD    MinorSubsystemVersion;  // 要求最低子系统版本的次版本号
+4Ch    DWORD   Win32VersionValue;       // 莫须有字段,不被病毒利用的话一般为0
+50h    DWORD   SizeOfImage;       // 映像装入内存后的总尺寸
+54h    DWORD   SizeOfHeaders;       // 所有头 + 区块表的尺寸大小
+58h    DWORD   CheckSum;       // 映像的校检和
+5Ch    WORD    Subsystem;       
// 可执行文件期望的子系统
+5Eh    WORD    DllCharacteristics;       // DllMain()函数何时被调用,默认为 0
+60h    DWORD   SizeOfStackReserve;       // 初始化时的栈大小
+64h    DWORD   SizeOfStackCommit;       // 初始化时实际提交的栈大小
+68h    DWORD   SizeOfHeapReserve;        // 初始化时保留的堆大小
+6Ch    DWORD   SizeOfHeapCommit;        // 初始化时实际提交的堆大小
+70h    DWORD   LoaderFlags;        // 与调试有关,默认为 0 
+74h    DWORD   NumberOfRvaAndSizes;  // 下边数据目录的项数,这个字段自Windows NT 发布以来        // 一直是16
+78h    IMAGE_DATA_DIRECTORY    DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];       // 数据目录表
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

●  AddressOfEntryPoint字段

指出文件被执行时的入口地址,这是一个RVA地址。如果在一个可执行文件上附加了一段代码并想让这段代码首先被执行,那么只需要将这个入口地址指向附加的代码就可以了。

●  ImageBase字段
指出文件的优先装入地址。也就是说当文件被执行时,如果可能的话,Windows优先将文件装入到由ImageBase字段指定的地址中,只有指定的地址已经被模块使用时,文件才被装入到地址中。链接器产生可执行文件的时候对应这个地址来生成机器码,所以当文件被装入这个地址时不需要进行重定位操作,装入的速度最快,如果文件被装载到地址的话,将不得不进行重定位操作,这样就要慢一点。 对于EXE文件来说,由于每个文件总是使用独立的虚拟地址空间,优先装入地址不可能被模块占据,所以EXE总是能够按照这个地址装入,这也意味着EXE文件不再需要重定位信息。对于DLL文件来说,由于多个DLL文件全部使用宿主EXE文件的地址空间,不能保证优先装入地址没有被**的DLL使用,所以DLL文件中必须包含重定位信息以防万一。因此,在前面介绍的 IMAGE_FILE_HEADER 结构的 Characteristics 字段中,DLL 文件对应的 IMAGE_FILE_RELOCS_STRIPPED 位总是为0,而EXE文件的这个标志位总是为1。

在链接的时候,可以通过对link.exe指定/base:address选项来自定义优先装入地址,如果不指定这个选项的话,一般EXE文件的默认优先装入地址被定为00400000h,而DLL文件的默认优先装入地址被定为10000000h。

●  SectionAlignment 字段和 FileAlignment字段

SectionAlignment字段指定了节被装入内存后的对齐单位。也就是说,每个节被装入的地址必定是本字段指定数值的整数倍。而FileAlignment字段指定了节存储在磁盘文件中时的对齐单位。

●  Subsystem字段

指定使用界面的子系统,它的取值如表17.3所示。这个字段决定了系统如何为程序建立初始的界面,链接时的/subsystem:**选项指定的就是这个字段的值,在前面章节的编程中我们早已知道:如果将子系统指定为Windows CUI,那么系统会自动为程序建立一个控制台窗口,而指定为Windows GUI的话,窗口必须由程序自己建立。

●  DataDirectory字段

这个字段可以说是最重要的字段之一,它由16个相同的IMAGE_DATA_DIRECTORY结构组成,虽然PE文件中的数据是按照装入内存后的页属性归类而被放在不同的节中的,但是这些处于各个节中的数据按照用途可以被分为导出表、导入表、资源、重定位表等数据块,这16个IMAGE_DATA_DIRECTORY结构就是用来定义多种不同用途的数据块的(如表17.4所示)。IMAGE_DATA_DIRECTORY结构的定义很简单,它仅仅指出了某种数据块的位置和长度。

IMAGE_DATA_DIRECTORY STRUCT

 VirtualAddress DWORD ? ;数据的起始RVA

 isize DWORD ? ;数据块的长度

IMAGE_DATA_DIRECTORY ENDS

数据目录列表的含义

在PE文件中寻找特定的数据时就是从这些IMAGE_DATA_DIRECTORY结构开始的,比如要存取资源,那么必须从第3个IMAGE_DATA_DIRECTORY结构(索引为2)中得到资源数据块的大小和位置;同理,如果要查看PE文件导入了哪些DLL文件的哪些API函数,那就必须首先从第2个IMAGE_DATA_DIRECTORY结构得到导入表的位置和大小。

找到PE头以后就从0开始计算偏移了。如下图所示的SectionAlignment 字段和 FileAlignment字段

typedef struct _IMAGE_SECTION_HEADER 
{
    BYTE Name[IMAGE_SIZEOF_SHORT_NAME];   // 节表名称,如“.text” 
    //IMAGE_SIZEOF_SHORT_NAME=8
    union
    {
        DWORD PhysicalAddress; // 在文件中的物理地址
DWORD VirtualSize;     // 真实长度,这两个值是一个联合结构,可以使用其中的任何一个,一般是取后一个
} Misc;
    DWORD VirtualAddress;          // 节区的 RVA 地址
DWORD SizeOfRawData;           // 在文件中对齐后的尺寸
DWORD PointerToRawData;        // 在文件中的偏移量
DWORD PointerToRelocations;    // 在OBJ文件中使用,重定位的偏移
DWORD PointerToLinenumbers;    // 行号表的偏移(供调试使用地)
WORD NumberOfRelocations;      // 在OBJ文件中使用,重定位项数目
WORD NumberOfLinenumbers;      // 行号表中行号的数目
DWORD Characteristics;         // 节属性如可读,可写,可执行等
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

当处理PE文件的时候,任何的RVA必须经过到文件偏移的换算,才能用来定位并访问文件中的数据,但换算却无法用一个简单的公式来完成,事实上,唯一可用的方法就是最笨的穷举法。

步骤一:循环扫描区块表得出每个区块在内存中的起始RVA【根据IMAGE_SECTION_HEADER中得VirtualAddress字段】根据区块的大小【根据IMAGE_SECTION_HEADER中的SizeOfRawData字段】算出区块的结束RVA【两者相加即可】,最后判断目标RVA是否落在该区块。

步骤二:通过步骤一定位了目标RVA处于具体的某个区块中后,那么用目标RVA减去该区块的起始RVA,这样就能得到目标RVA相对于起始地址的偏移量RVA2

步骤三:在区块表中获取该区块在文件中所处的偏移地址【根据IMAGE_SECTION_HEADER中的PointerToRawData字段】将这个偏移值加上步骤二得到的RVA2值,就得到了真正的文件偏移地址。

PE文件中的数据载入内存后根据不同页面属性被划分成很多区块,并有区块表的数据来描述这些区块。注:一个区块中的数据仅仅只是由于属性相同而放在一起,并不一定是同一种用途的内容。可能不同用途的数据有可能放入同一个区块中,如将属性都是可读的放在一起。PE文件头中IMAGE_OPTIONAL_HEADER32结构的数据目录表来指出它们的位置,可以由数据目录表来定位的数据包括输入表,输出表,资源,重定位表和TLS等15种数据。

typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics;
DWORD OriginalFirstThunk; //导入名称表(INT)的地址RVA
} DUMMYUNIONNAME;
DWORD TimeDateStamp; //时间戳多数情况可忽略.如果是0xFFFFFFFF为绑定导入
DWORD ForwarderChain; //链表的前一个结构
DWORD Name; //导入DLL文件名的地址RVA
DWORD FirstThunk; //导入地址表(IAT)的地址RVA
} IMAGE_IMPORT_DESCRIPTOR;
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;

//成员OriginalFirstThunk与FirstThunk都指向此结构:
typedef struct _IMAGE_THUNK_DATA32 {
union {
DWORD ForwarderString; // PBYTE
DWORD Function; // PDWORD
DWORD Ordinal; // 序号
DWORD AddressOfData; // PIMAGE_IMPORT_BY_NAME
} u1;
} IMAGE_THUNK_DATA32;
typedef IMAGE_THUNK_DATA32 * PIMAGE_THUNK_DATA32;

//如果结构IMAGE_THUNK_DATA32成员最高有效位(MSB)为1时低31位为导出序号.否则指向此结构.
typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint; //导出序号(有些编译器不会填充此值)
CHAR Name[1]; //该值长度不准确,以0结尾的字符串.导出函数名.
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;

每个被PE文件链接进来的DLL文件都分别对应一个IID数组结构。

简单示例:找到数据目录表Import Table的RVA是【0x2A000】——–在区块表中找到RVA【0x2A000】对应的物理地址就是28000H

用UE打开28000H处这个地址就是输入表IID的地方。有两个IID数组,也就是有两个动态链接库。

00 02 A1 5C =2A15CH ; 2A15CH -2A000H=15CH;28000H+15CH = 2815CH,如下图志示:IMAGE_THUNK_DATA值的最高位为1时,表示函数以序号方式输入;IMAGE_THUNK_DATA值的最高位为0时,表示函数以字符串类型的函数名方式输入;此例为0是以函数名方式。

DC A2 02 00 =2A2DCH; 2A2DCH-2A000H=2DCH;28000H+2DCH = 282DCH,如下图所示:

输出表工作示意图

Windows装载器的工作步骤

输出表示例:用UE打开dll库文件是它在物理硬盘中的位置,从0开始存储的。RVA是2060H这是加载到内存中的虚拟地址;

Export Table 的2060H -2000H = 60H;60H + 600H = 660H;

2060H所在区段

通过UE定位到660H处就是输出表的位置。name: 209CH,base:1,NumberOfFunctions:2,NumberOfName:2,AddressOfFunctions:2088,AddressOfNames:2090,AddressOfNames:2098,对照IMAGE_EXPORT_DIRECTORY_STRU结构都可以找出。

可以使用rundll32.exe命令将dll库加载到内存中。

直接寻址,就是直接将机器码倒过来变成了地址。是由于大端小端的存在。其中如:05 FF是指令的机器码

如::10001038 E8CFFFFFFF call 1000100C 这是怎么来的呢?机器码E8CFFFFFFF中没有地址的迹象;那是如何将地址翻译成机器码的呢?CFFFFFFF事实上就是一个偏移地址,由于咱使用的是小端所以要倒过来就 FFFF FFCF也就是等于-31h,那么1000103Dh – 31h ==1000100Ch

基址重定位结构表

PE中的资源详解,常用的修改资源工具有Resource Hacker, Exescope,注:如果提示文件已损坏需将修改的文件保存到根目录即原文件同级位置。

次源的树状结构示意图

IMAGE_RESOURCE_DIRECTORY STRUCT结构

Name字段完全是个百变精灵,该字段定义的是目录项的名称或ID,当结构用于第一层目录时,定义的是资源类型;当结构定义于第二层目录时,定义的是资源的名称;当结构定义于第三层目录时定义的是代码页编号;注:当最高位为0的时候,表示字段的值为ID使用;而最高位为1的时候,字段的低位作为指针使用,但这个指针不是直接指向字符串而是指向一个IMAGE_RESOURCE_DIR_STRING_U结构。

IMAGE_RESOURCE_DIR_STRING_U STRUCT
Length dword   ?; 字符串的长度
NameString  dword  ?;  UNICODE字符串,由于字符串不定长,由Length制定长度
IMAGE_RESOURCE_DIR_STRING_U ENDS

以QQ为例来解析资源:打开QQ程序可以看到数据目录表中的Resource Table 为 F000

以16进制打开QQ文件的F000位置,

提供最优质的资源集合

立即查看 了解详情