最近在准备面试相的内容,对pe相关的问题有些生疏了,于是就边看博客复习边整理到论坛上希望对大家有帮助。
在《逆向工程核心原理》这本书接触到了PE文件,但是当时学不进去,感觉很晦涩,虽然是一个结构体一个结构体的进行分析,但还是掌握不了。今天在YouTube上找到了一个视频,看了一下有种恍然大明白的感觉。正巧今天看了一期关于国产大飞机C919的视频很有感想,我们的策略是先整体后局部,即不过分死抠细节,不把国产化率排在首位,先造出来一个大飞机,于是我们就有了一张蓝图,知道方向在哪里。学习PE文件这里也是,先整体的过一遍,知道个轮廓,在学起来会容易和有趣不少。
强烈推荐这个PE教程
PE文件(Portable Executable file),是一种可执行文件格式,满足此格式的文件都可以在Windows操作系统运行。在Linux系统运行的文件是ELF。
通过后缀名判断文件格式是不靠谱的,因为后缀名是可以随意更改的,我们用一个十六进制编辑器打开一个文件,如果他的开头是MZ,且0x3c--0x3f所指示的偏移地址处的值是PE那么我们几乎可以肯定这是个PE文件。
从DOS头到节区头是PE头部分,其下的节区合成PE体。
来几张高清大图(可以下载文末附件查看)
我们学习PE文件就是学习这些结构体,先对结构体来个介绍。我们可以通过PEView查看pe文件,将notepad.exe拖入。我们先来分析一下存储时的PE文件,没错PE文件运行时和在硬盘中存储时是不同的。
DOS部分 (早期为了兼容DOS所设计)
IMAGE_DOS_HEADER (64字节)
MS_DOS Stu ,DOS存根,大小是不固定的,链接器会在这里插入数据,不影响程序运行,病毒程序可以插入在这里。虽然倒下不固定,但我们可以通过IMAGE_DOS_HEADER结构体的最后一个成员PE头开始的位置,用这个值减去64即DOS存根的大小。
PE文件头
IMAGE_NT_HEADERS 即PE头结构体,包括三个部分PE标识 ,标准PE头 ,扩展PE头
IMAGE_FILE_HEADER (20字节)
IMAGE_OPTIONAL_HEADER (32位)(224字节)(可扩展),IMAGE_FILE_HEADER结构体的一个成员记录该结构体的大小。
节表 (记录节的信息)
IMAGE_SECTION_HEADER (40字节)
有几个节区就有几个这样的结构体
IMAGE_SECTION_HEADER .text
节表后面是一些意义不大的数据,我们可以有效利用这些空间,所以节表和节表数据是不相邻的。
在可选头结构体中有一个成员记录PE头 (DOS头+PE文件头+节表)的大小
大小要满足是最小单位的整数倍,这样做可以提高效率。牺牲一定空间换取时间。节数据也要满足文件对齐。文件对齐上面的成员记录着内存对齐的大小,即程序加载到内存中后的对齐单位
磁盘状态(存储)
内存状态(运行)
Q:为什么文件对齐参数与内存对齐参数不相同?
A:因为它们分别用于优化文件I/O和虚拟内存管理。
IMAGE_DOS_HEADER结构体是为了早期16位程序而准备的,现在已经弃用除了头尾两个成员,其余的都是可以更改的
更改之后不影响程序运行。
4个字节的签名。更改之后不可运行。
长度20字节。
两个字节,标记可以程序可以运行在什么样的CPU上。 任意:0 ;Intel 386以及后续:14C ;x64:8664
两个字节记录节的数目。
时间戳,四个字节,在我的工具尚未能显示,我没有权限。
程序的时间戳是指在Windows操作系统上编译或链接可执行文件时,由编译器或链接器自动插入的一个时间戳。它通常存储为32位无符号整数,表示从1970年1月1日00:00:00(格林威治时间)开始的秒数。可以更改。
调试相关,不关注,共8个字节
两个字节,因为可选头长度是不固定的,该成员记录MAGE_OPTIONAL_HEADER的大小,32位默认是0xE0,64位默认是0xF0。如果可选头长度更改,也要对应修改这里。
两个字节,记录文件属性
010F -->0000 0001 0000 1111,每一个位都有意义,比如下标为1的地方是1,那么代表该文件是可执行文件。
两个字节,标志程序是32位还是64位(最准确)。PE32:10B ;PE32+:20B
4个字节,程序的入口。
记录代码开始的位置,记录的是相对Image Base的距离。PE文件在内存中展开后最前面都是数据,需要一个值告诉操作系统从哪里开始运行。
那么么该程序就从01000000+0000739D=0100739D, OD会在程序开始设置一个断点,地址就是这里。对这里进行修饰会增加逆向分析的难度,因为调试机器找不到程序入口。
4字节,内存 镜像基址。对于32位机器,操作系统会位每个进程分配一个4GB的虚拟地址空间,之所以是4GB,是因为32位操作系统指针长度为4字节32为,所以寻址能力是2的32次方。该成员指明程序在虚拟地址空间的何处展开,即基地址。
内存对齐大小。
文件对齐大小。
文件在内存中展开时的大小。
四字节,所有头+节表按照文件对齐后的大小。
校验和,以两字节为单位,将所有的数据相加。用来判断程序是否受到修改,但是我们可以通过修改其他数值来平衡,所以意义不大。
学习节表之前首先要知道PE文件的两种状态
即文件 状态和内存 状态
节表以结构体的形式描述节的信息,每个节表40字节
8字节,当前节的名字,可以随意更改。
当前这个节未对齐时的大小,即实际大小。实际大小有可能会比Size of Raw Data大,因为未初始化的全局变量在文件中是不占空间的。在内存中展开时以什么为基准呢?答案是谁大按谁,如果Vitual Size>Size of Raw Data,则按照Vitual Size展开,反之则按照Size of Raw Data。
在内存中的偏移地址,加上ImageBase则是内存中的真实地址。
文件对齐后的大小,Vitual Size的值是7748,文件对其大小是100,Size of Raw Data的值为7800。
当前节在文件中从何处开始
四字节,节的属性。
60 00 00 20 -->0110 0000 0000 0000 0000 0000 0010 0000
可以看到下标为5的位是1,代表该节中含有代码。
用一个简单的程序开始RVA.c
我们的任务是通过地址找到该全局变量,然后改变它的值。
我们运行程序,看到其地址是00bca000,这是在内存中的地址,我们知道PE文件在内存中和在文件中展开是不一样的,我们就是要通过内存中的这个地址找到文件中对应的位置。
相对虚拟地址(Relative virtual address) RVA =内存地址-ImageBase
查看其Image Base得到 RVA=D0A000-400000=90 A000
文件偏移地址(File offset address) FOA
他的位置是在00CAA000处,这里的位置即内存中的位置,不用想,他肯定不在头中。我们必须将运行中的程序进行分析,因为基地址也是随机化的。
Image Base=00C90000,全局变量的地址是00CAA000
此处的RVA即在文件中的偏移量,由此我们确定全局变量在data节中,差值为0,那么我们查看该节在文件中的位置即Point to Raw Data=8000.跳转到此处,其值为0001E240即十六进制的123456
修改并保存
我大为震惊。其实这个过程遇到了很多麻烦,基地址随机是在这个过程中认识到的。这个过程挺美妙的,虽然是个很简单的东西,却体现出了逆向的的思想与魅力。
我们要插入这样一个代码
功能呢就是弹出一个这样的错误窗口。这个程序的核心就是调用了一个messagebox函数弹出了一个窗口。由于我们是要对pe文件进行操作,所以肯定不是将代码写入,而是要将机器码插入。就是利于栈传第四个参数0,然后call。
构造要写入的代码 6A 00 6A 00 6A 00 6A 00 E8(call)xx xx,我们看到的FF是(near jump)要使用一个什么导入表(后面会学),而我们插入的时候E8利用相对偏移量调用函数。E8后面跟四个字节的相对偏移量offset,怎么计算?还好了解过汇编,offset=要跳转的地方的地址-call指令下面一条指令的地址。
将图一标记位置改为call 1c1023 得到图二,后面的0A=1c1023-1c1019.我们的目标指示让他弹出窗口,不能破坏程序运行,所以弹出之后我们还要跳回到初始位置,让程序正常运行,我们使用JMP(E9)指令来实现该操作,E9后面也是跟偏移量,用法与E8相同。找到messagebox的地址75858A70。根据call指令的位置来计算。
E8所在的位置是3A8,这是文件中的位置,我们要考虑的是运行时的位置,所以要把这个地址加上Image Base =01000000,所以最后得到的 offset=75858A70-010003A8-5=7485 86C3.最后跳转到程序的入口处,查看可选头的成员入口值为739D加上Image Base得到0100739D. offset=0100739D-010003AD-5=6FEB
将程序入口点指向我们插入的代码,
也就是修改扩展pe头里的入口值
在108处占4个字节,我们插入的程序起始位置是000003A0.然后,然后就没然后了,程序运行不起,ida和od看到的东西都很奇怪,延误了两天,这里我直接说一下踩的坑,我随意找了一个空白处填充机器码,但后来发现不是所有的位置都能发生跳转,然后就将指令插入在了text段的末尾,然后发现跳转的有些差异,不会跳转在我设置的地方,后知后觉的发现,视频里的老哥演示的的时候拿的是一个文件对齐和内存对齐相同大小的程序,我用的程序是一个不同的,所以就要计算一下 用我们前面学习到的知识。下面理一下步骤
第一次尝试的时候成功弹出了窗口,但是关闭之后没有出现记事本,由此我们可以判断执行完我们的代码后,没有成功的返回原入口。此时才想起来补码写错了,插入的位置是87b0,入口点是739d,739d-83b0-5=
然后我就在e9后面填充了E8 EF 00 00,意识到返回出现问题后,想到了有符号数的符号扩展,应该扩展其符号位即1,所以正确的补码形式是FF FF EF E8,填充进去就是E8 EF FF FF.
删除一个节比较简单,尤其是删除最后一个节,.reloc节区是基址重定位表,删除这个节区对程序的正常运行没有影响,而且删除这个节区可以将文件的大小缩减。下面说一下步骤:
28个字节
因为是最后一个节区,所以我们只要将C000即后面的东西删除即可
将5改成4即可
原来程序在内存中展开的大小为11000
减去删除的节在内存中展开的大小即可
尝试运行,一切正常。左边是删除roloc节区的程序,可以看到大小发生了改变。
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
最后于 2024-5-21 14:12
被马先越编辑
,原因: 图片不清晰
上传的附件: