Windows下32位汇编学习(二)PE文件基本部分解析
引言
想开发反病毒引擎,第一步总是对PE文件进行解析,虽然有点落伍,但这是基础,想开发病毒也同理,很多的病毒样本在使用Indirect Syscall + 调用栈欺骗前,都需要动态调用API,这个时候就需要你了解PE文件的结构,通过解析PE文件找到IAT,进而找到PEB来动态获取地址,所以不想学也得学~
本文会介绍从书中学到的部分知识,以及通过C++和汇编分别实现一个PE文件解析器
本篇博客的学习参考罗云彬老师的《Windows环境下32位汇编语言程序设计》,尤其是第17章的“PE文件”
正文
借用一下罗云彬老师的图,PE文件从文件头到文件尾依次是:DOS部分、PE头、节表、节数据
DOS部分:包括DOS标识和DOS代码,为了兼容DOS系统而保留的部分
PE头:包括PE标识、PE头、PE可选头
节表:下面节数据的起始位置、大小
节数据:就是代码段、数据段
上面是用通俗的语言描述,在代码中是下面的样子
DOS部分
1 | IMAGE_DOS_HEADER { |
常用的就是e_magic和e_lfanew,DOS标识转换成字符就是常见的MZ(MZ是DOS创始人名字的首字符)
PE头
1 | IMAGE_NT_HEADERS { |
可以看到PE头就复杂多了,上面的结构体和注释已经展示了大部分字段的含义,再补充一下DataDirectory,Windows PE文件中,代码段和数据段是按照装入内存后的属性划分的,装入内存后这段数据是可读可执行,那这段数据在装入内存前就位于代码段,装入内存后这段数据是只读,那这段数据在装入内存前就位于只读数据段,可是问题来了,像导出表、导入表装入内存后都是只读,所以都位于数据段,那我们改如何区分它们呢,就是利用DataDirectory,16个IMAGE_DATA_DIRECTORY,序号是0-15,依次表示
1 | 0 导出表 |
还需要注意,这里的偏移指的是程序被装载到内存后,在内存中的偏移,由于内存对齐的最小单位和文件对齐的最小单位不同(还有其他因素),内存中的偏移和文件中的偏移并不一致
节表
1 | IMAGE_SECTION_HEADER { |
例1,想要计算只读数据段中偏移20个字节的数据A在内存中的地址,假如PE被装载的起始地址是0x00400000,那么A在内存中的地址就是0x00400000+VirtualAddress+20Byte
例2,已知一个RVA,想计算在文件中的偏移地址:
1、通过例1的方式分别得到每个节在内存中的地址,节1在内存中的地址Addr1,节2在内存中的地址Addr2
2、Addr1 <= 0x00400000+VirtualAddress+20Byte < Addr1+VirtualSize,通过这个方式判断A在哪个节中
3、RVA - 对应节的起始地址 + PointerOfRawData就是在文件中的偏移
有人可能会问,节在内存中的大小是经过SectionAlignment对齐的,不等于VirtualSize,为什么是Addr1+VirtualSize,假如VirtualSize是0x523,在内存中经过SectionAlignment是0x1000,从0x523开始到后面都被填充0,所以在比较的时候可以忽略后面的部分,只计算Addr1+VirtualSize
节数据
就是代码段、数据段、只读数据段、等等
导入表
1 | IMAGE_IMPORT_DESCRIPTOR { |
程序要使用的函数及相关信息,存储在导入表中
导入表地址可以从IMAGE_NT_HEADERS中的IMAGE_OPTIONAL_HEADER32中的IMAGE_DATA_DIRECTORY中的索引为5的结构体中的VirtualAddress和isize获取地址偏移和大小,一个DLL对应一个IMAGE_IMPORT_DESCRIPTOR,也就是说有多少个DLL就有多少个IMAGE_IMPORT_DESCRIPTOR,最后以一个值全为0的IMAGE_IMPORT_DESCRIPTOR结构结束,同样,包含IMAGE_THUNK_DATA的数组也是,一个IMAGE_THUNK_DATA对应一个导入函数,最后以一个值全为0的IMAGE_THUNK_DAT结构结束,IMAGE_THUNK_DATA本质上是一个4字节,当最高位为1时,表示函数以序号方式导入,这时4字节的低位就是函数的序号,当最高位为0时,表示函数以名称方式导入,这时4字节的值是一个RVA,指向IMAGE_IMPORT_BY_NAME,IMAGE_IMPORT_BY_NAME中的Name1包含导入函数的名称
导出表
1 | IMAGE_EXPORT_DIRECTORY { |
和导入表相对应,存储要给别的程序使用的函数
重定位表
1 | IMAGE_BASE_RELOCATION { |
没有意外的情况下,程序的建议装载地址就是实际装载地址,但是对于EXE文件,如果启用了ASLR(地址空间布局随机化),则实际装载地址会不同,对于DLL文件,如果建议装载地址已经被其他DLL占用了,则实际装载地址会不同,所以需要对代码进行重定位
直接寻址指令的地址 + (实际装载地址 - 建议装载地址),实际装载地址由Windows装载器提供不用我们操心,建议装载地址存储在PE文件的IMAGE_OPTIONAL_HEADER32中的ImageBase中,所以重定位表中只需要存储要修改的直接寻址指令的地址
重定位表由一系列重定位块组成,一个重定位块对应一个内存页,用来描述这个内存页中需要重定位的项,每个重定位块以一个IMAGE_BASE_RELOCATION开头,后面跟着一系列重定位项,每个重定位项占2个字节,SizeOfBlock = 4 + 4 + 2*重定位项数,所以重定位项数 = (SizeOfBlock - 8) / 2,每个重定位项占用2个字节共16位,VirtualAddress + 16位中的低12位就是要重定位指令的最终RVA,16位中的高4位用来表示属性,所有重定位块最终以一个VirtualAddress字段为0的IMAGE_BASE_RELOCATION结构结束。还有一个问题可以借此解答,假如PE的装载地址是0x00400000,代码的起始地址如果和装载地址一样的话,那第一页的重定位块的IMAGE_BASE_RELOCATION结构中的VirtualAddress的RVA就为0x00400000 - 0x00400000 = 0,此时 系统就会认为当前的重定位块结束了,导致重定位失败