PE学习2
5、RVA与FOA的转换
引入问题:
如果想改变一个全局变量的初始值,该怎么做?
如果一个变量没有赋初值的话,那么他在硬盘的时候,不回存放在PE文件中,直到在内存中展开的时候才会在文件中。而如果有初始值的话,就会一直存在。
如果我们想要从一个运行起来的PE文件中去找到未运行的文件中的一个全局变量的地址,并且修改对应的值的话。就要实现RVA到FOA的转化。
RVA:相对虚拟地址,即为你在运行后的文件中的地址减去开始的地址(即为扩展PE头中的ImageBase所指向的地址)
FOA:文件偏移地址,即为未运行的文件变量所对应的地址。
变化的方式分为以下几个步骤:
<1> 首先判断RVA是否在头部 在的话直接返回
FOA = RVA 因为头部的数据不管是在内存中还是在硬盘中,对应的位置相距于最开始的位置都是相同的,拉伸展开并不会影响到。
<2> 判断RVA位于哪个节: RVA>= 节.VirtualAddress RVA <= 节.VirtualAddress +当前节内存对齐后的大小
差值 = RVA -节.VirtualAddress;
<3> FOA = 节.PointerToRawData +差值;
只要找到了变量对应在节的初识地址的差值,我们就能知道,相应位置。因为每个节中的数据并未拉伸。PointerToRawData 一定要对应正确的那一个节。
但是如今的操作系统,文件对齐和内存对齐都是相同的。所以我们只用找到差值,然后去文件中对应就行。
6、扩大节
为什么扩大节?
我们可以在任意空白区添加自己的代码,但如果添加的代码比较多,空 白区不够我们就需要扩大节。
扩大哪一节呢?
一般来说我们主要是扩大最后一节,扩大其他节意味着在该节之后的所有节都会受到影响,并作出调整。 虽然来说扩大其他节,也是行得通的,但是避免造成麻烦,所以我们就扩大最后一节。
节表数据结构说明
扩大节的步骤: (1)分配一块新的空间,大小为S (2)将最后一个节的SizeOfRawData和VirtualSize改成N(这两个属性那个大选哪一个进行修改,另一个较小的同那个更大的一同修改) N = (SizeOfRawData或者VirtualSize 内存对齐后的值) + S (3)修改SizeOflmage大小(扩展PE头的内存镜像大小)
如果将扩大的节的内容设置为可执行的,就需要将节表中最后的一个DWORD Characteristics(节的属性)修改为可执行的。
7、新增节
在节与节或者头与头的空白区域添加代码,在攻防中,攻的一方就会利用这种技术添加病毒,防的一方就会添加壳之类的。
但是如果说空白区域放不下所编写的代码,我们就需要新增一个节(虽然也可以扩大节)
3、新增节的步骤: <1>判断是否有足够的空间,可以添加一个节表.(一个节表的大小为40个字节) <2>在节表中新增一个成员. <3> 修改PE头中节的数量. <4> 修改sizeOflmage的大小. <5>再原有数据的最后,新增一个节的数据(内存对齐的整数倍). <6>修正新增节表的属性.
8、导出表
一个可执行程序是由一个PE文件组成的吗?
一个程序除了exe文件还包含了很多的dll文件,即为动态链接库。在任何一个pe文件中都有一个导入表,其详细的记录了要使用的其他的pe文件,包括其他pe文件的名字和其他pe文件中的函数。而导出表中存储的是,当前的pe文件提供了哪些函数给别的pe文件使用。 通常情况下,exe文件不提供导出表,即一般exe文件不提供函数给别的pe文件使用,但是并不代表他不能提供函数给别的pe文件使用(因为本质上其也是pe文件,也存在导出表)
(1)如何定位导出表
扩展pe头中的最后一个成员是一个结构体数组,其中包含了十六个结构体
其中 IMAGE_DIRECTORY_ENTRY_EXPORT 这个成员就是代表了导出表,而这个结构体中的成员
struct _IMAGE_DATA_DIRECTORY{
0x00 DWORD VirtualAddress;
0x04 DWORD Size,
}
其存储了导出表在哪里以及导出表有多大。IMAGE_DATA_DIRECTORY DataDirectory[16] 其他的结构体成员也是如此,即包含了对应的位置和大小。
(2)导出表结构
typedef struct _IMAGE_EXPORT_DIRECTORY{
DWORD Characteristics;//未使用
DWORD TimeDateStamp;//时间戳
WORD MajorVersion;//未使用
WORD MinorVersion;//未使用
DWORD Name;//指向该导出表文件名字符串
DWORD Base;//导出函数起始序号
DWORD NumberOfFunctions;//所有导出函数的个数
DWORD NumberOfNames;//以函数名字导出的函数个数
DWORD AddressOfFunctions://导出函数地址表RVA
DWORD AddressOfNames;//导出函数名称表RVA
DWORD AddressOfNameOrdinals;//导出函数序号表RVA
}IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT DIRECTORY;
虽然导出表这个结构中,只包含了40个字节,但是其实结构体成员中的Name, AddressOfFunctions,AddressOfNames,AddressOfNameOrdinals实际上是指针分别指向了名字以及其他表的位置。_IMAGE_DATA_DIRECTORY中的size成员,所指的大小,其也包含了指针所指向的表的大小。
TimeDateStamp 代表了时间戳,从1970年0时0分0秒开始,每过一秒加一直到直到编译这个dll为止(与标准pe头的第二个成员--时间戳,也是一样的)
Name即为指向该导出表文件名字符串,其实质上是一 个指针,这个Name的数值即为指向的字符串的位置。如果对其进行修改,是不影响其导出表的。只是名字被修改而已。
NumberOfFunctions是指所有导出函数的个数;NumberOfNames是以函数名字导出的函数个数,因为dll有两种导出方式,一种是以名字导出,一种是以序号导出,所以也就可以推断出以序号导出的函数个数。(因为有函数名字导出的就比较一目了然,就能够很容易的利用这个导出函数,但是如果不想要别人能够利用导出函数的话,就可以通过函数序号导出,隐藏自己函数的实现细节。但是如果了解底层,了解汇编的话,通过工具跟进函数也能知道对应函数的作用。)
然后是最重要的三张表
(1) AddressOfFunctions函数地址表,其值也是对应的地址,而且地址表的大小也是四个字节。再根据导出函数的个数来推断,就可以找出对应的函数的地址。(其函数的个数,实质上是通过最大的序号减去最小的序号算出的。但是有可能对应的有序号的没有函数,NumberOfFunctions所指的函数个数大于其实际导出的函数个数,我们就可以通过 在函数地址表中找到其中的地址是否有为0的,即没有的函数),之后我们就可以通过函数地址表中的地址找到对应函数。但是因为其对应的是16进制的数,然后我们可以将其复制到OD中,通过OD就可以得到他对应的汇编代码,通过阅读汇编代码我们就能知道对应函数的具体实现是什么。
(2)AddressOfNames函数名称表,其存储的并不是对应的函数的名字,而是数值,其数值指向了对应的名字(字符串)
(3)AddressOfNameOrdinals函数序号表,这个函数序号表中的成员和函数名称表中的成员个数相同,因为这个序号表本身就是给函数名称表用的。 函数序号表一样是存储了指针,指向的才是对应的函数序号,而与函数地址和函数名称不同的是,函数序号是两个字节的。函数地址和函数名称都是四个字节的。
当我们想要通过函数的名称去找某一个函数的时候,我们可以利用API 中的一个函数
FARPROC GetProcAddress{
HMODULE hModule;// DLL模块句柄
LPCSTR 1pProcName; //函数名
}
该函数有两个参数,第一个DLL模块句柄,就是指当前的pe文件在内存中展开的时候的起始位置是在哪。第二个参数有两种情况,一种情况是函数名,另外一种情况是函数序号。当我们利用该函数查找函数的地址的时候,就是通过其函数名,在函数名称表的位置,如第0个位置,之后它就会在序号表中的第0个位置取得序号,如取得序号4,然后在函数地址表中的第4个位置,找到对应的函数地址了。(即可以理解为函数序号表就是一个索引的数组,从函数名称表中找到对应的之后,再在函数地址表中找到对应的地址) 而当该函数通过函数序号来找的时候,它就会将得到的序号与导出表中的Base成员(导出函数起始序号)进行相减,然后得到的这个数值就是函数地址表中对应的位置了。