VA
va(虚拟地址):虚拟地址是内存中的地址,每一个内存空间都有一个地址表示,这就是虚拟地址如打开OD,加载程序,里面的每一个地址都是虚拟地址,VA如下图,PE文件加载进内存的VA都是由imageBase(基址)+RVA(相对虚拟地址)形成的绝对虚拟地址VA
RVA
RVA(相对虚拟地址):相对虚拟地址相对的是选项头里面的字段ImageBase(程序基址),PE结构中大部分字段的存的就是RVA 举例:假如你的ImageBase是00400000 节表头中的VirtualAddress是1000 这时你的节表数据映射到内存的VA(绝对虚拟地址)就是00401000
FOA
FOA(文件偏移地址):相对于文件开始的位置偏移,PE数据存在文件中的地址就是FOA,举例:你的文件开始偏移为0 ,你的节表头字段中的PointerToRawData为400 这时你的节表数据在文件中的地址就是400这个地址,如下图表示
这是400地址就是你节表的数据所在的位置,节表在文件中占多大看你在文件中的对齐大小,必须是对齐值的倍数,节表在内存中占多大也看内存对齐值,也必须是对齐值的倍数,对齐值在选项头字段FileAlignment(文件对齐)和SectionAlignment(内存对齐)
RVA转FA
例如你的RVA=1100,这是看你的RVA属于哪个节,如果RVA>=节表的RVA&&<节表内存大小(记得算对齐值)说明属于这个节,这就说明是属于这个节了,这时候查看这个节在文件中的地址FA,这就可以定位这个RVA在文件中的FA了,如果这个节的FA位400,就可以算出你的FA=500,公式如下
- 偏移=RVA-节的RVA
- FOA=偏移+节的FOA 求出的FOA就是你的RVA在文件中的位置(FOA)
VA转FOA
和上面差不多,只是先算出RVA,然后就想RVA转FA
- RVA=VA=ImageBase(注意如果你的基址不是可选头里面的建议基址,就自己用od查看一下实际基地址)
- 偏移=RVA-节的RVA
- FOA=偏移+节的FOA 求出的FA就是你的VA在文件中的位置(FOA)
PE文件加载(映射)进内存结构如下
其他的FA转RVA是一样的这个只要求出偏移,然后加上偏移就可以了,看下面的PE加载进内存的图结构就知道了,图是引用百度的
导入表
导入表的作用:记录可执行文件或者一个dll(都可以称为PE文件)所用到的其他模块的导函数
导入表记录的信息:用了哪些模块(用了哪些dll),用了dll的哪些函数
导入表结构体定义如下
//最后会以全0的结构为结束,其中每一项是一个结构,一项8个字节,
typedef struct _IMAGE_IMPORT_DESCRIPTOR
{
union
{
DWORD Characteristics;
DWORD OriginalFirstThunk;
}; //指向导入函数名,如果为0,则找FirstThunk
DWORD TimeDateStamp; //时间,一般不用
DWORD ForwarderChain; //链表前一个结构,一般不用
DWORD Name; // DLL名称的RVA偏移通过偏移可以找到DLL名称
DWORD FirstThunk; // IAT 的RVA偏移.和originalFirstThunk不同,如果地址指向的值为 0,则直接跳过当前轮,加载下一个DLL
//IAT结构体类型
typedef struct _IMAGE_THUNK_DATA32
{
//共用体,如果高位大于8则表示这是一个序号,小于8则表示为一个字符串地址RVA,也就是最高一位是否为1
union
{
PBYTE ForwarderString;
PDWORD Function;
DWORD Ordinal;
PIMAGE_IMPORT_BY_NAME AddressOfData;
} u1;
} IMAGE_THUNK_DATA32;
导入表在PE中的结构如下图
系统加载导入表流程
- 找到数据目录第2项,跳转到导入表起始地址
- 遍历模块数组(系统遍历到20个字节的全0结尾,表示模块加载结束),数组每个元素结构_IMAGE_IMPORT_DESCRIPTOR
- 通过字段OriginalFirstThunk或字段FirstThunk去IAT(导入函数地址表)或INT中获取函数名或序号,然后利用函数LoadLibrary获取模块基址,函数GetProcAddress获取函数的地址覆盖到字段FirstThunk指向的IAT中,这样循环遍历每个模块,把每个模块的导入函数都覆盖到对应模块的IAT中,可以参照上图来理解流程
- 其实调用api的时候,如果是隐式加载用的就是导入表IAT中存的函数地址,如果是显示加载(GetProcAddress)用的就是导出表的存的函数地址