从PE入手的信息收集,让恶意样本无处可逃
那是一个沙尘暴都能上热搜的清晨,我揉了揉眼睛从床上爬起来,顶着一路的艰难险阻来到了实验室,开机,hello 酷狗,登录PC微信,蓝屏。全剧终。
文件名是一个叫做hahahaha的没有后缀的文件,我知道,有任务了。
今天和大家分享一下二进制安全中最基本的知识,PE文件格式。
按照我们一般的逆向分析流程,当然是使用查壳工具来获取信息,想必大家对于这方面的知识已经十分乏味了。下面我们切入正题。
一、 PE文件格式的基础知识
1.1 认识PE文件
PE文件中文的意思就是可执行文件,常见的exe、dll、ocx、sys等都是PE文件格式的。其中我们最常见的是exe和dll,那么它们两个有什么区别呢,exe和dll的区别完全是语义上的,他们使用完全相同的PE格式。唯一的区别就是用一个字段标识出这个文件是exe还是dll。还有许多dll的扩展,如ocx控件和控制面板程序(.cpl文件)这些都是dll,它们拥有一样的实体。
64位的Windows只是对pe文件格式做了一些简单的修饰,新格式交PE32+。没有新的结构加进去,其余的改变知识简单的将以前的32位字段扩展成64位了。对于c++代码,windows文件头的配置使其拥有不明显的区别。
PE文件中的数据结构一般都有32位和64位之分,如IMAGE_NT_HEADERS32、IMAGE_NT_HEADER64等。除了在64位版本中的一些扩展域以外,这些结构几乎总是一样的。在winnt.h都有#defines,它可以选择适当的32位或64位结构并给它们起成与大小无关的别名(在前面的例子中,可以写成IMAGE_NT_HEADERS)。结构选择依赖于用户正在编译的模式(尤其_WIN64是否被定义)。
1.2 整体结构
PE结构一般来说:从起始位置开始依次是DOS头、NT头(也有人叫PE头)、节表以及具体的节
1.3 基地址
程序加载进内存的起始地址。当PE文件通过Windows加载器被装入内存后,内存中的版本被称作模块(Module)。映射文件的起始地址被称为模块句柄(hModule),可以通过模块句柄访问内存中其他的数据结构。这个初始内存地址也称为基地址(ImageBase)。我们可以通过Pchunter工具查看程序基地址。
1.4 相对虚拟地址
又称作RVA,它是一个“相对”地址,或称为“偏移量”。存中的一个简单的相对于PE文件装入地址的偏移位置。顺便说一下,在PE用语里,实际的内存地址被称作虚拟地址(Virtual Address,简称VA),另外也可以把虚拟地址想象为加上首选装入地址的RVA。不要忘了前面提到的装入地址等同于模块句柄。它们之间的关系如下:
虚拟地址(VA)=基地址(ImageBase)+相对虚拟地址(RVA)
1.5 文件偏移地址
当PE文件储存在磁盘上时, 某个数据的位置相对于文件头的偏移 量,称为文件偏移地址(File Offset) 或物理地址(RAW Offset)。文件偏 移地址从PE文件的第一个字节开始计 数,起始值为0。用十六进制工具 (例如WinHex、C32等)打开文件 所显示的地址就是文件偏移地址。、
1.6 结构
1.6.1 DOS头
每一个PE文件都是以一个DOS程序开始的,一旦程序在DOS下执行,DOS就能识别出这是有效的执行体,然后运行DOS stub(DOS块)。DOS stub其实就是一个有效的EXE,如果OS是不支持PE文件的,那么它将显示为一个错误提示
对于DOS头信息我们只需要关注两个字段即可:e_magic和e_lfanew,e_magic字段(一个字大小)需要被设置为值5A4Dh,这个值有个#define,名为IMAGE_DOS_SIGNATURE,ASCII表示法里,它的ASCII值为“MZ”,是MS-DOS的最初创建者之一Mark Zbikowski字母的缩写。e_lfanew字段是真正PE文件头(NT头)的相对偏移,其指出真正PE头的文件偏移位置,它占用4个字节,位于文件开始偏移3Ch字节中。这里是 0x000000D0,也就代表偏移0xd0处是NT头的开始位置。
1.6.2 NT头
有三个成员:PE签名、PE文件头、PE可选头
A、NT头结构信息-PE签名
在一个有效的PE文件里,Signature字段被设置为00004550h,ASCII码字符是“PE00”,#define IMAGE_NT_SIGNATURE定义了这个值。这个值也通常被用来判断是否为标准的PE文件。(通常是跟MZ头一起判断)
B、NT头结构信息-PE文件头
IMAGE_FILE_HEADER(PE文件头)结构包含了PE文件的一些基本信息,最重要的是其中一个域指出了IMAGE_OPTIONAL_HEADER的大小。下面介绍IMAGE_FILE_HEADER结构的各个字段以及对这些字段的额外说明,这个结构也能在COFF格式的OBJ文件的最开始处找到,因此也称为COFF File Header。
C、NT头结构信息-PE可选头
(IMAGE_OPTIONAL_HEADER)是一个可选的结构,但实际上IMAGE_FILE_HEADER结构不足以定义PE文件属性,因此可选映像头中定义了更多的数据,完全不必考虑两个结构区别在哪里,两者连起来就是一个完整的“PE文件头结构”。使用Stud_PE查看文件的可选头结构如下。
(1)Magic:是一个标记字,说明文件是ROM映像(0107h),还是普通可执行的映像(010Bh),一般是010Bh,如是PE32+,则是020Bh。
(2)MajorLinkerVersion:链接程序的主版本号。
(3)MinorLinkerVersion:链接程序的次版本号。
(4)SizeOfCode:所有带有IMAGE_SCN_CNT_CODE属性区块的总共大小(只入不舍),这个值是向上对齐某一个值的整数倍。例如,本例是200h,即对齐的是一个磁盘扇区字节数(200h)的整数倍。通常情况下,多数文件只有一个Code块,所以这个字段和.text块的大小匹配。
(5)SizeOfInitializedData:已初始化数据块的大小,即在编译时所构成的块的大小(不包括代码段)。但这个数据并不太准确。
(6)SizeOfUninitializedData:未初始化数据块的大小,装载程序要在虚拟地址空间中为这些数据约定空间。这些块在磁盘文件中不占空间,就像“UninitializedData”这一术语所暗示的一样,这些块在程序开始运行时没有指定值。未初始化数据通常在.bss块中。
(7)AddressOfEntryPoint:程序执行入口RVA。通常我们说的程序入口点或者OEP就是它。
(8)BaseOfCode:代码段的起始RVA。
(9)BaseOfData:数据段的起始RVA。数据段通常是在内存的末尾,即PE文件头和Code Section之后。可是,这个域的值对于不同版本的微软链接器是不一致的,在64位可执行文件中是不出现的。
(10)ImageBase:文件在内存中的首选装入地址(基地址)。如果有可能(也就是说,目前如果没有其他占据这块地址,它是正确对齐的并且是一个合法的地址,等等),加载器试图在这个地址装入PE文件。如果可执行文件是在这个地址装入的,那么加载器将跳过应用基址重定位的步骤。
(11)SectionAlignment:当被装入内存时的区块对齐大小。每个区块被装入的地址必定是本字段指定数值的整数倍。默认的对齐尺寸是目标CPU的页尺寸。对于运行在Windows 9x/Me下的用户模式可执行文件,最小的对齐尺寸是一页1000h(4KB)。这个字段可以通过链接器的/ALIGN开关来设置。在IA-64上,是按8KB来排列的。
(12)FileAlignment:磁盘上PE文件内的区块对齐大小,组成块的原始数据必须保证从本字段的倍数地址开始。对于x86可执行文件,这个值通常是200h或1000h,这是为了保证块总是从磁盘的扇区开始,这个字段的功能等价于NE格式文件中的段/资源对齐因子。用不同版本的微软链接器默认值会改变。这个值必须是2的幂,其最小值为200h,并且如果SectionAlignment小于CPU的页尺寸,这个域必须与SectionAlignment匹配。链接器开关/OPT:WIN98设置x86可执行文件的文件对齐为1000h,/OPT:NOWIN98设置对齐为200h。
(13)MajorOperatingSystemVersion:要求操作系统的最低版本号的主版本号。随着这么多版本的Windows的到来,这个字段明显地变得不切题了。
(14)MinorOperatingSystemVersion:要求操作系统的最低版本号的次版本号。
(15)MajorImageVersion:该可执行文件的主版本号,由程序员定义。它不被系统使用并可以设置为0,可以通过链接器的/VERSION开关设置它。
(16)MinorImageVersion:该可执行文件的次版本号,由程序员定义。
(17)MajorSubsystemVersion:要求最低子系统版本的主版本号。这个值与下一个字段一起,通常被设置为4,可以通过链接器开关/SUBSYSTEM来设置。
(18)MinorSubsystemVersion:要求最低子系统版本的次版本号。
(19)Win32VersionValue:另一个从来不用的字段,通常被设置为0。
(20)SizeOfImage:映像装入内存后的总尺寸。也就是在内存中所占的大小。它指装入文件从Image Base到最后一个块的大小。最后一个块根据其大小往上取整。
(21)SizeOfHeaders:是MS-DOS头部、PE头部、区块表的组合尺寸。所有这些项目都出现在PE文件中任何代码或数据区块之前。域值四舍五入至文件对齐的倍数。
(22)CheckSum:映像的校验和。IMAGEHLP.DLL中的CheckSumMappedFile函数可以计算这个值。一般的EXE文件可以是0,但一些内核模式的驱动程序和系统DLL必须有一个检验和。当链接器的/RELEASE开关被使用时,校验和被置于文件中。
(23)Subsystem:一个标明可执行文件所期望的子系统(用户界面类型)的枚举值。这个值只对EXE是重要的
(24)DllCharacteristics:DllMain()函数何时被调用,默认为0。
(25)SizeOfStackReserve:在EXE文件里,为线程保留的堆栈大小。它一开始只提交其中一部分,只有在必要时,才提交剩下的部分。
(26)SizeOfStackCommit:在EXE文件里,一开始即被委派给堆栈的内存数量。默认值是4KB。
(27)SizeOfHeapReserve:在EXE文件里,为进程的默认堆保留的内存。默认值是1MB,但是在当前版本的Windows里,堆值在用户不干涉的情况下就能增长超过这个值。
(28)SizeOfHeapCommit:在EXE文件里,委派给堆的内存大小。默认值是4KB。
(29)LoaderFlags:与调试有关,默认为0。
(30)NumberOfRvaAndSizes:数据目录的项数。这个字段从最早的Windows NT发布以来一直是16。
(31)DataDirectory:数据目录表,由数个相同的IMAGE_DATA_DIRECTORY结构组成,指向输出表、输入表、资源块等数据。
(QAQ,百度真香)
1.7 PE区段分析
区段概念:在PE文件头与原始数据之间存在一个区块表(sectio Table),区块表包含每个块在映像中的信息,分别指向不同的区块实体。
区段表:紧跟着NT头后的是区块表它是一个IMAGE_SECTION_HEADER结构数组。每个IMAGE_SECTION_HEADER结构包含了它所关联区块的信息,如位置、长度、属性;该数组的数目由IMAGE_NT_HEADERS.FileHeader.NumberOfSections指出。
IMAGE_SECTION_HEADER结构各个字段的解释如下:
(1)Name:区段名
(2)VirtualSize:指出实际的、被使用的区块大小。
(3)VirtualAddress:区段装载到内存中的RVA。
(4)SizeOfRawData:区段在磁盘文件中所占的大小。
(5)PointerToRawData:区段在磁盘文件中的偏移。
(6)PointerToRelocations:这部分在EXE文件中无意义。
(7)PointerToLinenumbers:行号表在文件中的偏移值。这是文件的调试信息。
(8)NumberOfRelocations:这部分在EXE文件中无意义。
(9)NumberOfLinenumbers:区段在行号表中的行号数目。
(10)Characteristics:块属性。该字段是一组指出块属性(如代码/数据/可读/可写等)的标志。
区段表分析----常见的区段名与他们的作用描述
.text:.text节包含了CPU执行指令。所有其他节存储数据和支持性的信息。一般来说,这是唯一可以执行的节,也应该是唯一包含代码的节。
.data:.data节包含了程序的全局数据,可以从程序的任何地方访问到。本地数据并不存储在这个节中,而是PE文件某个其他位置上。
.rdata:.rdata节通常包含导入与导出函数信息,与Dependency Walker和PEview工具获得的信息是相同的。这个节中还可以存储程序所使用的其他只读数据。有些文件中还会包含.idata和.edata,来导入导出信息。
.rsrc:这个节中包含由可执行文件所使用的资源,而这些内容并不是可执行的,比如图标、图片、菜单项和字符串等。字符串可以存储在.rsrc节中,或者在主程序里。在.rsrc节中进程存储的字符串是为了提供多种语言支持的。
一些注意事项:
1.区段名是可以被随意修改的,所以默认的区段名知识给我们一些参考,很多恶意软件或者加壳之后的软件都会去修改区区段名称等信息。
2.区块的大小是要对齐的,有两种对齐值,一种用于磁盘文件内,另一种用于内存中。PE文件头指出了这两个值,他们可以不同。
3.恶意代码或者壳进场会在区段上下文章,所以掌握区段的一些操作知识对分析恶意代码以及脱壳会有很大的帮助。
1.8 PE文件的输入输出表
1.8.1 输入表(IT、导入表)
可执行文件使用来自于其他DLL的代码或数据时,成为输入。当PE文件装入时,Windows加载器的工作之一就是定位所有被输入的函数和数据,并且让整座被装入的文件可以使用那些地址。这个过程是通过PE文件的输入表来完成的,输入表中保存的是函数名和驻留的DLL名等动态链接所需的信息。
PE文件头的可选映像头中数据目录表的第二成员指向输入表。
输入表以一个IMAGE_IMPORT_DESCRIPTOR(简称IID)数组开始。每个被PE文件隐式链接进来的DLL都有一个LLD,在这个数组中,没有字段指出该结构数组的项数,但它的最后一个单元是NULL,可以由此计算出该数组的项数。例如,某个PE文件从两个DLL文件中引入函数,就存在两个LLD结构来描述这些DLL文件,并在两个DLL结构的最后一个内容全为0的LLD结构作为结束。
1.8.2 输出表
当创建一个DLL时,实际上创建了一组能让EXE或其他DLL调用的一组函数,此时PE装载器根据DLL文件中输出信息修正被执行文件的IAT。当一个DLL函数能被EXE或另一个DLL文件使用时,那就是被输出了。输出的信息就保存在了输出表中,DLL文件通过输出表想系统提供输出函数名,序号和入口地址等信息。EXE文件一般不存在输出表,而大部分DLL文件中存在输出表
注意:输出表(Export Table)中的主要成分是一个表格,内含函数名称、输出序数等。序数是指定DLL中某个函数的16位数字,在所指向的DLL里是独一无二的。在此不提倡仅仅通过序数引出函数这种方法,这会带来DLL维护上的问题。一旦DLL升级或修改,调用该DLL的程序将无法工作。
1.8.3重定位表
当链接器生成一个PE文件时,它假设这个文 件执行时会被装载到默认的基地址处,并且把 code和data的相关地址都写入PE文件中。如果 装入时按默认的值作为基地址装入,则不需要重 定位。但如果可执行文件被装载到虚拟内存的另 一个地址,链接器所登记的那个地址就是错误的 ,这时就需要用重定位表来调整。在PE文件中 ,它往往单独分为一块,用“.reloc”表示。