打算复习下以前的笔记,选择性的整理迁移后关闭旧博客。要深入学习逆向,了解PE格式是最基础的,于是有了本篇
典型PE文件格式 PE32(portable executable)是32位Windows下可执行文件的格式,可执行文件要被加载器加载解析才能运行,就像之前学的图像格式,PE也有格式规范,装载器按照预定义的方式加载解析它,总的来看它长这样: 当然本篇学习的是PE格式头部分,这里先放上《黑客免杀攻防》里附的典型PE文件格式,即数据在文件中的排列方式,会在随后依次介绍它们各自的含义和它们之间的逻辑:
说明:这是典型格式,并不是每一个PE文件都这样,只要符合规范即可,表中大致按照各个结构体为一个整体作色,可以看到各个部分的大小,偏移,意义(详细的请看下面的介绍),特别需要关注加粗部分。 注:下文的代码注释会以本表作为实例来说明以便于理解,实际并不能保证是这么多。
基本概念
名词
含义
映像
泛指映射在内存中的可执行文件
ImageBase
PE文件在内存中优先装载的地址
RVA
relative virtual address,相对虚拟地址,它是相对imagebase的偏移位置
对齐粒度
对齐大小
小端序
Intel的CPU是小端序的,小端序即低位存低地址,高位存高地址,如数字0x1a335e(越往后位数越低)的存储是0x5e331a(越往后地址越高)
变量大小
BYTE->8位WORD->16位,LONG->32位,DWORD->32位(仅限于本文)
DOS 这是每个PE开始部分,为兼容而设计,在现代操作系统几乎没用了,主要看最后一个字段,它指示PE头的位置。 DOS头:
IMAGE_DOS_HEADER结构体:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 typedef struct _IMAGE_DOS_HEADER { WORD e_magic; WORD e_cblp; WORD e_cp; WORD e_crlc; WORD e_cparhdr; WORD e_minalloc; WORD e_maxalloc; WORD e_ss; WORD e_sp; WORD e_csum; WORD e_ip; WORD e_cs; WORD e_lfarlc; WORD e_ovno; WORD e_res[4 ]; WORD e_oemid; WORD e_oeminfo; WORD e_res2[10 ]; LONG e_lfanew; } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
(注:本文的所有结构体来自VS2015的winnt.h头)
DOS存根(stub)为可选,在dos下会运行并结束程序,大小也任意
尽管它几乎没用但是它有显著的标志可以用来识别文件格式,例如魔数MZ ,与存根显示”The program cant run ….”
PE头 这才是最重要的部分,主要学习此部分
文件头 文件头包含了PE标识和下面接着的部分,注意观察 IMAGE_NT_HEADERS32结构体:
1 2 3 4 5 typedef struct _IMAGE_NT_HEADERS { DWORD Signature; IMAGE_FILE_HEADER FileHeader; IMAGE_OPTIONAL_HEADER32 OptionalHeader; } IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
IMAGE_FILE_HEADER结构体:
1 2 3 4 5 6 7 8 9 typedef struct _IMAGE_FILE_HEADER { WORD Machine; WORD NumberOfSections; DWORD TimeDateStamp; DWORD PointerToSymbolTable; DWORD NumberOfSymbols; WORD SizeOfOptionalHeader; WORD Characteristics; } IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
PE属性可为下面的值(看长相即可知这类宏能进行位运算):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #define IMAGE_FILE_RELOCS_STRIPPED 0x0001 #define IMAGE_FILE_EXECUTABLE_IMAGE 0x0002 #define IMAGE_FILE_LINE_NUMS_STRIPPED 0x0004 #define IMAGE_FILE_LOCAL_SYMS_STRIPPED 0x0008 #define IMAGE_FILE_AGGRESIVE_WS_TRIM 0x0010 #define IMAGE_FILE_LARGE_ADDRESS_AWARE 0x0020 #define IMAGE_FILE_BYTES_REVERSED_LO 0x0080 #define IMAGE_FILE_32BIT_MACHINE 0x0100 #define IMAGE_FILE_DEBUG_STRIPPED 0x0200 #define IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP 0x0400 #define IMAGE_FILE_NET_RUN_FROM_SWAP 0x0800 #define IMAGE_FILE_SYSTEM 0x1000 #define IMAGE_FILE_DLL 0x2000 #define IMAGE_FILE_UP_SYSTEM_ONLY 0x4000 #define IMAGE_FILE_BYTES_REVERSED_HI 0x8000
扩展头: IMAGE_OPTIONAL_HEADER32结构体格式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 typedef struct _IMAGE_OPTIONAL_HEADER { WORD Magic; BYTE MajorLinkerVersion; BYTE MinorLinkerVersion; DWORD SizeOfCode; DWORD SizeOfInitializedData; DWORD SizeOfUninitializedData; DWORD AddressOfEntryPoint; DWORD BaseOfCode; DWORD BaseOfData; 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;
目录表: 存储了15个表的大小与起始地址,这里面有一些十分重要的表,例如IAT与EAT,将在后面介绍,先看看长相:
可以看到最后一项全为0,表示保留,这是C语法,结构体数组以空结构体作为结束
IMAGE_DATA_DIRECTORY结构体:
1 2 3 4 typedef struct _IMAGE_DATA_DIRECTORY { DWORD VirtualAddress; DWORD Size; } IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
从上一节中看到,它是一个数组,每一个元素就是这么个结构体,他们代表的含义是:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #define IMAGE_DIRECTORY_ENTRY_EXPORT 0 #define IMAGE_DIRECTORY_ENTRY_IMPORT 1 #define IMAGE_DIRECTORY_ENTRY_RESOURCE 2 #define IMAGE_DIRECTORY_ENTRY_EXCEPTION 3 #define IMAGE_DIRECTORY_ENTRY_SECURITY 4 #define IMAGE_DIRECTORY_ENTRY_BASERELOC 5 #define IMAGE_DIRECTORY_ENTRY_DEBUG 6 #define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE 7 #define IMAGE_DIRECTORY_ENTRY_GLOBALPTR 8 #define IMAGE_DIRECTORY_ENTRY_TLS 9 #define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 10 #define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT 11 #define IMAGE_DIRECTORY_ENTRY_IAT 12 #define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT 13 #define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14
区段表: 区段表像上面的目录表,是一种结构体的数组,结构体如下: IMAGE_SECTION_HEADER结构体:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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;
区段属性值:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 #define IMAGE_SCN_TYPE_NO_PAD 0x00000008 #define IMAGE_SCN_CNT_CODE 0x00000020 #define IMAGE_SCN_CNT_INITIALIZED_DATA 0x00000040 #define IMAGE_SCN_CNT_UNINITIALIZED_DATA 0x00000080 #define IMAGE_SCN_LNK_OTHER 0x00000100 #define IMAGE_SCN_LNK_INFO 0x00000200 #define IMAGE_SCN_LNK_REMOVE 0x00000800 #define IMAGE_SCN_LNK_COMDAT 0x00001000 #define IMAGE_SCN_NO_DEFER_SPEC_EXC 0x00004000 #define IMAGE_SCN_GPREL 0x00008000 #define IMAGE_SCN_MEM_FARDATA 0x00008000 #define IMAGE_SCN_MEM_PURGEABLE 0x00020000 #define IMAGE_SCN_MEM_16BIT 0x00020000 #define IMAGE_SCN_MEM_LOCKED 0x00040000 #define IMAGE_SCN_MEM_PRELOAD 0x00080000 #define IMAGE_SCN_ALIGN_1BYTES 0x00100000 #define IMAGE_SCN_ALIGN_2BYTES 0x00200000 #define IMAGE_SCN_ALIGN_4BYTES 0x00300000 #define IMAGE_SCN_ALIGN_8BYTES 0x00400000 #define IMAGE_SCN_ALIGN_16BYTES 0x00500000 #define IMAGE_SCN_ALIGN_32BYTES 0x00600000 #define IMAGE_SCN_ALIGN_64BYTES 0x00700000 #define IMAGE_SCN_ALIGN_128BYTES 0x00800000 #define IMAGE_SCN_ALIGN_256BYTES 0x00900000 #define IMAGE_SCN_ALIGN_512BYTES 0x00A00000 #define IMAGE_SCN_ALIGN_1024BYTES 0x00B00000 #define IMAGE_SCN_ALIGN_2048BYTES 0x00C00000 #define IMAGE_SCN_ALIGN_4096BYTES 0x00D00000 #define IMAGE_SCN_ALIGN_8192BYTES 0x00E00000 #define IMAGE_SCN_ALIGN_MASK 0x00F00000 #define IMAGE_SCN_LNK_NRELOC_OVFL 0x01000000 #define IMAGE_SCN_MEM_DISCARDABLE 0x02000000 #define IMAGE_SCN_MEM_NOT_CACHED 0x04000000 #define IMAGE_SCN_MEM_NOT_PAGED 0x08000000 #define IMAGE_SCN_MEM_SHARED 0x10000000 #define IMAGE_SCN_MEM_EXECUTE 0x20000000 #define IMAGE_SCN_MEM_READ 0x40000000 #define IMAGE_SCN_MEM_WRITE 0x80000000
IAT 这里不介绍导入地址表的作用,但是它与EAT在二进制安全中的作用是很大的,需要重点学习:
这里先来看看三个结构体,接着再说明他们之间的联系: IMAGE_IMPORT_DESCRIPTOR结构体:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 typedef struct _IMAGE_IMPORT_DESCRIPTOR { union { DWORD Characteristics; DWORD OriginalFirstThunk; } DUMMYUNIONNAME; DWORD TimeDateStamp; DWORD ForwarderChain; DWORD Name; DWORD FirstThunk; } IMAGE_IMPORT_DESCRIPTOR;
IMAGE_THUNK_DATA32结构体:
1 2 3 4 5 6 7 8 9 typedef struct _IMAGE_THUNK_DATA32 { union { DWORD ForwarderString; DWORD Function; DWORD Ordinal; DWORD AddressOfData; } u1; } IMAGE_THUNK_DATA32; typedef IMAGE_THUNK_DATA32 * PIMAGE_THUNK_DATA32;
IMAGE_IMPORT_BY_NAME结构体:
1 2 3 4 typedef struct _IMAGE_IMPORT_BY_NAME { WORD Hint; CHAR Name[1 ]; } IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
逻辑关系
从这里的表开始,指的都是数组【如一般一个PE文件不只导入一个库,要导入多少库就存在库数+1个IMAGE_IMPORT_DESCRIPTOR结构体(即为一个IMAGE_IMPORT_DESCRIPTOR[库数+1]的数组),且最后一个为空。】,导入表首地址RVA值由目录表数组的第二个元素指出IMAGE_DATA_DIRECTORY[1]——–1 2 IMAGE_DATA_DIRECTORY[1 ]-------- #define IMAGE_DIRECTORY_ENTRY_IMPORT 1
上面说了,导入表是IMAGE_IMPORT_DESCRIPTOR结构体数组,每一个元素代表导入的一个库,又知一般情况下一个库里面有很多导出的函数,函数可以通过函数名使用,也可以通过地址直接使用,或是无名函数通过编号使用,故IMAGE_IMPORT_DESCRIPTOR含有这个库导出函数的名称表和地址表首地址指针(如下图)。
通过IMAGE_IMPORT_DESCRIPTOR.OriginalFirstThunk可以找到IMAGE_THUNK_DATA32数组首地址,这个数组记录的就是有名函数的函数信息指针,再通过这个指针可以找到IMAGE_IMPORT_BY_NAME数据,这个结构体记录着单个函数的编号(ordinal)和函数名。
另一边,通过IMAGE_IMPORT_DESCRIPTOR.FirstThunk可以找到IMAGE_THUNK_DATA32数组首地址(此时这个数组用作IAT,即FirstThunk在初始时存储的数据可能和INT一样,但是初始化以后里面存放的是函数真实的地址,而FirstThunk与目录表里面的IAT关系是,所有DLL的FirstThunk合在一起为IAT,会有一个DLL的FirstThunk值与IAT的值一样),里面存的是所有函数的信息的指针,再通过这个指针可以找到IMAGE_IMPORT_BY_NAME数据,这个结构体记录着单个函数的编号(ordinal)和函数名。
PE装载器把导入函数输入至IAT的顺序
读取IMAGE_IMPORT_DESCRIPTOR[i].Name获取库名xxlib
装载相应库LoadLibrary(xxlib)
读取IMAGE_IMPORT_DESCRIPTOR[i]. OriginalFirstThunk获取INT
遍历INT获取相应的IMAGE_IMPORT_BY_NAME的地址addressp
使用GetProcAddress(addressp->Hint|Name)获取函数的起始地址
读取IMAGE_IMPORT_DESCRIPTOR[i].FirstThunk获取IAT首地址
将上面获得的函数起始地址输入到相应的IAT数组中
重复4~7直到INT结束
注意到,上面图中比较特别,INT和IAT指向内容一样,即他们两者相同,但是有时可以不一样,如存在无名函数,这时只能通过序号来获取地址,这时IAT数组就会大于INT。
EAT 吃。。。。导出地址表,和导入相对应,不过一个文件最多就一个 IMAGE_EXPORT_DIRECTORY结构体:
1 2 3 4 5 6 7 8 9 10 11 12 13 IMAGE_typedef struct _IMAGE_EXPORT_DIRECTORY { DWORD Characteristics; DWORD TimeDateStamp; WORD MajorVersion; WORD MinorVersion; DWORD Name; DWORD Base; DWORD NumberOfFunctions; DWORD NumberOfNames; DWORD AddressOfFunctions; DWORD AddressOfNames; DWORD AddressOfNameOrdinals; } IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY; EXPORT_DIRECTORY
导出表相对简单,通过如上图即可清楚其中的逻辑,再附上GetProcAddress()函数原理 ,应该能够很好的理解它:
利用AddressOfNames成员转向函数名称数组
函数名称数组中存储着字符串地址,通过比较字符串查找匹配名
利用 AddressOfNameOrdinals找到ordinal数组
在ordinal中根据匹配名称的数组下标获取对应下标元素值
利用AddressOfFunctions元素找到EAT表
按4找到的索引获取EAT数组元素的值,即那个函数的地址(这里用的索引为ordinal值-base)
注意:在存在无名导出函数时,可以通过ordinal直接从第4步开始获取地址,即使不存在无名导出函数,AddressOfNames的index和ordinal也不一定对应,所以需要从1开始才能保证正确。
基址重定位: 由于imagebase指定的位置可能已经被占用(这一般是发生在DLL中的,因为EXE文件首先被加载,此时拥有一个独占的虚拟空间),PE装载器讲程序装载到了其他地址,于是PE里硬编码的VA就需要被修改,方法为:
找到硬编码的地址
将 对应值 - OriginalImageBase+NewImageBase
第二步很容易实现,于是主要关注怎么找到硬编码地址,其实他们都在编译时就被记录在了基址重定位表中,此表有基址重定位目录偏移指向,结构为:
1 2 3 4 5 6 7 8 9 10 11 typedef struct _IMAGE_BASE_RELOCATION { DWORD VirtualAddress; DWORD SizeOfBlock; } IMAGE_BASE_RELOCATION; typedef IMAGE_BASE_RELOCATION UNALIGNED * PIMAGE_BASE_RELOCATION;
可以将地址相近的组成一个段落,在这里可以看到后面的TypeOffset是注释,说明它并不属于IMAGE_BASE_RELOCATION,可以将IMAGE_BASE_RELOCATION看做段首,它说明段落基址和段落大小,后面接TypeOffset数组表示段落主体,而TypeOffset中高4位指定重定位类型(如下宏定义),低12位表示偏移,于是实际的VA为Offset+VirtualAddress+ImageBase。
1 2 3 4 5 6 7 8 9 10 11 #define IMAGE_REL_BASED_ABSOLUTE 0 #define IMAGE_REL_BASED_HIGH 1 #define IMAGE_REL_BASED_LOW 2 #define IMAGE_REL_BASED_HIGHLOW 3 #define IMAGE_REL_BASED_HIGHADJ 4 #define IMAGE_REL_BASED_MACHINE_SPECIFIC_5 5 #define IMAGE_REL_BASED_RESERVED 6 #define IMAGE_REL_BASED_MACHINE_SPECIFIC_7 7 #define IMAGE_REL_BASED_MACHINE_SPECIFIC_8 8 #define IMAGE_REL_BASED_MACHINE_SPECIFIC_9 9 #define IMAGE_REL_BASED_DIR64 10
内存地址转换 现在再来看看相对内存地址与文件偏移之间的转换,,在这之前要先了解下虚拟内存与物理内存,学汇编时,我们能写代码让CPU直接访问内存,但是实际上用户层是不能直接访问物理内存的,只有内核级权限才能访问它,实际上的内存管理如下:
二级页表 为了更充分利用内存,且兼顾速度,X86采用二级页表的方式,对于每一个进程,系统分配4GB的虚拟内存(不管物理内存实际有多大),其中0X00000000-0X7FFFFFFF为用户进程空间,0X80000000-0XFFFFFFFF为系统空间,每一个进程都拥有这么个4G的虚拟地址地址空间。
用户区 是每个进程真正独立的可用内存空间,进程中的绝大部分数据都保存在这一区域。
主要包括:应用程序代码、全局变量、所有线程的线程栈以及 加载的DLL代码等
每个进程的用户区的虚拟内存空间相互独立,一般不可以直接跨进程访问,这使得一个程序直接破坏另一个程序的可能性非常小。
内核区 中的所有数据是所用进程共享的,是操作系统代码的驻地。
其中包括: 操作系统内核代码, 以及与线程调度、 内存管理、文件系统支持、网络支持、 设备驱动程序相关的代码。
该分区中所有代码和数据都被操作系统保护。用户模式代码无法直接访问和操作:如果应用程序直接对该内存空间内的地址访问,将会发生地址访问违规。
(于是,这可以解释为什么同时用OD调试两个不同的程序,它们的内存地址却相同,因为每一个进程都独享这么一个虚拟空间)
但是虚拟的毕竟是假的,实际操作还是要作用在真实的内存上,即CPU在对内存进行操作时,内存管理单元MMU会将虚拟内存映射到物理内存上,所以出现的情况是虚拟内存的连续空间在实际的物理内存上并不一定连续,那么就需要一张表来记录虚拟内存与物理内存之间的映射关系(又因为用的页式存储,就把这个东东叫做页表,至于后面提到的页,认为是汇编那种段吧,只是比段还小,,,那把它比作磁盘扇区吧)。
由于内存太大,页表可能会很大,为便于查找就把它再细分下,相邻的为一组,再加个组索引即有了两张页表了,第一张表来查属于哪一组的,再到那一组去查找更具体的,下面是图例:
页表中就记录着实际的物理空间,还有一些状态信息(这里大致有个概念,到操作系统笔记详细记录)于是MMU就能实现虚拟地址与物理地址之间的映射了。
对齐 为了方便寻址等,一个PE文件在磁盘中存储与在内存中存储都不是一个结构后就直接紧接着存储下一个结构,而是根据对齐粒度,若是一个部分未达到粒度大小,就是用0填充,而他们的对齐粒度在PE文件里面有记录: 一般来说,在磁盘中存储时是200字节,而映射到内存中时为1000字节。
例如一个.text段在文件中的起始位置是0X00001200,在内存中的起始位置是0X00002000,实际大小是315字节,其后为.data,那么.data在文件中的起始位置为0X00001600,在内存中的起始位置是0X00002000。
转换 已经注意到了,前两篇在查看PE文件时里面存的很多都是VA(Virtual Address虚拟绝对地址)和RVA(Relative Virtual Address虚拟相对地址,相对ImageBase的地址),即在文件中存的很多都是加载到内存中的地址,而加载到内存中的实际为PE文件,我们想要直接在文件中找到那个地址的值,但是不能把地址直接拿来用(由于对齐粒度不同,一般内存中的PE都要比文件中的占的空间大),需要将RVA/VA转换为磁盘上的偏移FO公式如下:
文件偏移地址=所在区段文件起始偏移地址+相对区段偏移地址
相对区段偏移地址=虚拟地址-映像基址-所在区段虚拟起始偏移地址
<==>
文件偏移地址=相对虚拟地址-相对区段偏移地址+所在区段文件起始偏移地址
参考 《逆向工程核心原理》
《黑客免杀攻防》
《逆向工程权威指南》
《恶意软件防护机理》