PE文件格式学习,实例大杂烩~
查看PE文件
第一个姿势-PEView
从定位表偏移40上面就是new EXE Header偏移E0,转到E0如图
继续往下到E4,映像文件头:
看到目标处理器类型为i386,有三个区段(可以从左侧看出.text,.data,.rsrc),此软件存在bug,读取时间会出错,跳过创建时间。扩展头的大小为E0字节,PE属性为在32位下可执行不包含行列号和重定位表.继续到扩展头:
含code属性的区段对齐后总大小为7800H,入口点RVA为739DH,代码段偏移为1000H,数据段为9000H,加载基址为01000000H,文件在内存的对齐单位为1000H在磁盘上为200H
看到入口点地址为0100739DH=01000000H+739DH
由于对齐单位为200H然后这里只有一个.text段含code属性(见后面段属性),那么大小就是7800H。
这里可以看到数据段的确被加载到了RVA为9000H的位置,而且载入内存后映像总大小为1300H(资源段起始B000H大小为8000H,文件头起始位0H),这里看不出所示的文件头大小为400H因为内存对齐单位为1000H,所以这里PE头显示的也是1000H而不是在文件中对齐后的400H。接着写到需要运行在Windows的GUI系统上。最后写到目录表有10H个,即16个:
看到,此文件没有导出表,导入表指向的RVA地址为7604H,转换为文件偏移为6A04H(转换方法:7604H(RVA)-1000H(所在段起始偏移,从内存可以看出,也可以从PE文件推出)+400H(PE文件可以看出,见下文),也可通过偏移量转换工具自动转换),导入/出表的内容明天再记吧。。。
这里可以看到,扩展头结束偏移为1D4,大小为4字节,起始偏移为F8,相减刚好等于上一部分写到的大小。
继续看区段表:
第一个是.text段的区段表,可以看到名字.text,载入内存后大小7748H(文件中当然也这么大),RVA为1000H,文件中对齐后大小为7800H,在文件中的起始偏移为400H,然后区段的属性就有前面用到的code属性,还有可执行,可读属性。
第二种姿势-WinHex
这里用它来找到导入目录表吧
PE头位置为00 00 00 E0(小端序),继续
找到PE头50 45 00 00(这里之所以不倒序是大小端只针对多字节类型数据),按照之前那张表,找到EP地址为00 00 73 9D,加载基址为01 00 00 00 ,继续找
找到了导入目录的地址为00 00 76 04,大小为C8,因为这是一个RVA,所以需要转换为文件偏移才能在文件中找到它,根据公式
文件偏移地址=相对虚拟地址-相对区段偏移地址+所在区段文件起始偏移地址
一直相对虚拟地址00 00 76 04
需要找到它在内存中的相对区段偏移地址<-需要知道区段在文件和内存中的对齐大小
需要区段文件起始偏移地址<-需要找到区段表的起始位置<-目录表的个数(或扩展头的大小)
回过头来找吧:
这里可见对齐大小还是标准的00 00 02 00 和00 00 10 00
这里可见目录数为00 00 00 10即16个,再往下找第一个区段头:
这里很幸运,在第一个里面,因为.text区段的RVA为00 00 10 00,大小为00 00 78 00所以相对区段偏移00 00 76 04在这个区段,它在文件中的起始偏移为00 00 04 00,即可算得00 00 76 04的文件偏移为6A04,这里发现好像并没有用到对齐。。。太复杂了,可以的话还是用PE查看工具比较好。导入/表明天继续,看这个眼睛疼。。。
导出表
查看msvbvm50.dll的导出表,先使用PEView查看(此dll的RVA和RAW大多都一样,下面就不写计算过程,不要混淆了)
转向导出表(仔细看左侧记录的位置)
00 0F 76 44不在任何标注出的结构里,使用winhex查看
函数索引值基数为00 00 00 64(十进制100),实际导出函数个数00 00 07 77H其中有名函数00 00 02 58H个,接着看到导出函数地址数组地址为00 0F 4A 58,函数名称地址数组地址为00 0F 68 34,其实就对应解析出的EXPORT Address Table 和EXPORT Name Pointer Table(可以看出它们都是数组,它们的值依然为地址)
这里看到导出地址表中的数据都是按照序号/导出名来排列,但是实际在内存中的位置却是打乱的(data为内存中的位置)
继续跟踪发现,EXPORT Name Pointer Table的数据指向EXPORT Names,发现它指向的位置并没有ordinal值,猜测是通过下面那张表自动生成的
接着来到序号(Ordinal)地址数组,因为这个数组和EXPORT Name Pointer Table里的数组是一一对应的,所以上面是可以解析出那种样子。
这里的导出序号-基数(64H)即为真正的序号,例如BASIC_CLASS_AddRef的序号为0137H-64H=D3H
接着来理一下导出表的运行逻辑:
- 利用AddressOfNames成员转向EXPORT Name Pointer Table
- 通过比较EXPORT Name Pointer Table中每个地址字符串查找匹配名
- 利用 AddressOfNameOrdinals找到EXPORT Ordinal Table找到导出序号
- 导出序号减去基为实际序号
- 利用AddressOfFunctions元素找到EAT表
- 按4找到的实际序号获取EAT数组元素的值,即那个函数的地址
(这里就指向了那个导出函数的地址)
导入表
继续看他的导入表
转到00 0F 99 40
看到它导入了多个库,以空结构体结束,看第二个吧,名称为USER32.dll,导入名称表的RVA为 00 0F 9C B8H,跟过去
看到上一个导入名称表(INT以\0结束),接着就是USER32.dll的导出值,它可能是被导入的序号,也可能是名称地址,若最高位为1则证明它是序号,为0则代表它是名称的地址,继续跟入
它是一个以两字节开头作为序号,后接一个字符串作为名称的结构体组成,这里看到DedConnect这个导出函数的序号为00 5B。继续上一个结构,导入地址表的地址是00 00 12 78H,跟入
这里就是它的实际地址(VA)然而并没有多大用,因为dll的加载地址可能不是这里,在加载dll时会重写实际位置。
PE装载器把导入函数输入至IAT的顺序:
- 读取IMAGE_IMPORT_DESCRIPTOR[1].Name获取库名USER32.dll
- 装载相应库LoadLibrary(USER32.dll)
- 读取IMAGE_IMPORT_DESCRIPTOR[1].OriginalFirstThunk获取INT
- 遍历INT获取相应的IMAGE_IMPORT_BY_NAME的地址addressp
- 使用GetProcAddress(addressp->Hint|Name)获取函数的起始地址
- 读取IMAGE_IMPORT_DESCRIPTOR[1].FirstThunk获取IAT首地址
- 将上面获得的函数起始地址输入到相应的IAT数组中
重复4~7直到INT结束
重定位
依然看这里,找到重定位表的地址00 12 D0 00 (这里用了pe查看工具就可以不看这个,用十六进制查看器只能靠这里找到它)
基址重定位长相就是这样,一节一节的,大小没什么规律,不过先是两个4字节的描述信息,这里表名这一节的大小为7CH,这一节的基址为1000H,下面每一条数据表示那一个位置有被硬编码,需要重定位的数据,这个数据低12位代表偏移,高4位代表重定位类型
例如第一条数据3A 9F二进制表示为0011101010011111B 则重定位类型为0011B,根据微软定义#define IMAGE_REL_BASED_HIGHLOW 3
意味着这个地址的数据4字节都会被更新,然后101010011111B为A 9FH,那么这条数据的实际地址为A9FH+1000H+ImageBase=74001A9FH,可以看到这里的确有一条硬编码的地址:
删除.reloc节区
.reloc节区即基址重定位数据存放节区,删除后就不需要重定位,对于EXE来说几乎没什么影响(它总是能被加载到imagebase指定的区域,只是丧失了ASLR功能)
步骤:
整理.reloc节区头
文件偏移为02E8~(030C+4)将之用0覆盖(随意啦,可以不覆盖,主要是为了看效果)
删除.reloc节区
将它填充为0
也可以直接删除,为了后面做更多更改,这里使用删除,删除后文件大小也变了
修改IMAGE_FILE_HEADER
它记录着节区数,减一
讲道理似乎还应该更改Characteristics值,添加不包含重定位信息
修改IMAGE_OPTIONAL_HEADER
这里就只有内存中映像尺寸需要更改,因为把重定位节区删除了,他应该减少,应该减去内存中对齐后大小47DF4-43600=47F4对齐为5000H,需要减这么多
完成!运行没毛病(好吧,其实这个程序本身也不支持重定位,只是有重定位节区而已。。。)
Upack PE文件头详细分析
据说分析这个会颠覆对PE的认识。。。
真的颠覆了。。。嗷,原来是Peview不能正确识别,,,换Stud_PE
果然好多了。。。然而看不出什么,继续winhex
吱吱?MZ下面就是PE,中间还夹着KERNEL32.DLL这个导入库名,说好的结构呢,好吧跟着标准结构来,PE头指针在DOS头最后
好呗,没毛病,dos头和pe头其实是重复的(这里软件识别出了点问题,dos应该从第一行开始),不过dos头只用到了e_magic和e_lfanew其他部分当然可以被COFF头占用,至于e_lfanew这一位,也是BaseOfCode即代码段数据起始RVA,似乎没被装载器用到
好吧,开启上帝模式!里面会遇到很多奇怪的问题!看COFF里面写着,区段数为3个,扩展头的大小为01 48H,这和通常见到的E0不一样,那么就可以知道28H01 70H之间全是扩展头,打开扩展头:D7H,剩下的地方就可以填充Upack的解码代码了
扩展头后接的目录表有0A个,也与通常的10H个不一样,那么说明实际上扩展头只占用了28H
继续往下,到节区表,这个作者是一个会持家的好男人,能省就省,把PointerToRelocations,PointerToLinenumbers,NumberOfRelocations,NumberOfLinenumbers这些东西全部废物利用了,存一些其他数据,至于到底什么数据就只有分析了才知道
看上面的节区,有三个,其中第一个和第三个在磁盘上的位置和大小一样即由同一区域映射而且00 00 00 10H这个位置是PE头开始的位置,它们会被映射到不同的内存空间,第二块很大实际上是加密后的notepad.Exe文件,这里可以看到第一个区段的虚拟偏移为1000H,物理偏移却为10H,明显物理偏移未对齐,PE装载器会自动对其进行对齐(变成00H)。
继续找导入表,走你
计算得出RAW为02 71 EE-27000+0=1EE
厉害了word哥,这里看起来不符合PE规范,因为他不是以一个空结构体结束,但是,uplook,下面偏移200咯,就是第二个区段,然额这里是第三个区段,磁盘上前面的区域会被映射到内存后面的区域,内存对齐大小为1000,实际使用大小为1F0,那么后面会用0填充,于是就有了空结构体!
再走着,分析这个孤独的导入库,库名地址在00 00 00 02H
导入地址表RAW为00 00 11 E8-1000H+0=1E8H
继续跟进00 00 00 28H,00 00 00 BE H
终于完了,对PE结构又有新的了解了,对于这类程序,根着PE装载器的逻辑来吧,除非特别的规定,按照COFF规范来总没毛病。
扞UPX
Today调试UPX压缩的notepad,先看看加密前的EP处代码,等下解压完好作判断吧
先放在这里,接着运行压缩后程序,一打开ollydbg就是一个下马威,和书上还不一样,因为书上是英文啦
等等,静态分析了那么就pe,插入一张总览图:
还算正常,好了,continue。。。再等等,再来一张图分析一下,ep在第二个节区,注意这个软件应该是汉化有问题,虚拟地址应该是虚拟大小
第一个节区不占用磁盘空间,在内存中的偏移为1000H,大小为10000H,属性为包含未初始化数据,可执行可读写
第二个节区占用磁盘空间4600H,在磁盘上的起始偏移为400H,在内存中的起始偏移为11000H,大小为5000H,属性为包含已初始化数据,可执行可读写
第三个节区占用磁盘空间7200H,在磁盘上的起始偏移为4A00H,在内存中的起始偏移为16000H,大小为8000H,属性为包含已初始化数据,可读写
第三个节区是纯数据,第二个节区含代码,第一个节区初始为空且可执行,猜测是二运行解压三到一,具体的只能调试时看看了
正式开始
准备工作:
循环一:
接着对esi做了很多操作,因为猜测最后解码数据会放到第一区段,这里就不关它在第二区段做了什么,跳过直到队EDI操作(EDI指向第一区段),这里执行了一次,将00填入首位后后移一位
这里开始第一个循环了,仔细看EDX与EDI相差一,之前将EDI指向的前一个位置(即当前EDX指向位置)置为0了,那么这里的循环就是往这个区域填充0,结束时EDI偏移到了36C处
循环二:
这次又跳上去了,这里把01 00 13 6C置为01 即01 00 13 69成为了01 00 00 00(小端序),接着EDI指向36D啦
继续走你,,,又上去了,将36D置为01,再继续走你,EDI为370H了
再继续。。。。。。不玩了,看书上的,我一定用错方法了,嗷,这里的循环是解压代码的
循环三:
真心不懂是怎么调试到这里的。。。
循环四:
UPX在第二个区段会保存原来的IAT名称字符串,使用GetProcAddress()可获得地址,讲之写到EBX所指向的原IAT位置
接着将控制权交给第一区域的原程序入口点,这条跳转就进入了第一区段
刘爷爷:嘿!硬是呢!这里和没压缩之前是一样的!
快速寻找OEP
使用那个什么定律,因为解压代码在pushad和popad之间,可以在堆栈上设置断点,使之刚好在popad处暂停
设置硬件断点,它可以从调试菜单看到是否设置成功
来源
《逆向工程核心原理》