少女祈祷中...

pe文件结构只能说算一点点基础,顺带学习PE的时候再熟悉熟悉c语言和Windows API

PE文件即Portable Execute Windows下可执行文件的总称,常见的有 DLL,EXE,OCX,SYS 等,PE文件可以说是在各个领域都有涉及,特别是病毒领域,内网渗透中的免杀对抗

(免杀对抗环境从普通的杀软到edr,xdr等设备)

基础知识

PE文件的结构有两种表现形式:一是存储在在硬盘中的文件,二是加载在内存中

如上图可见,当PE文件加载到内存中后,DOS头到最后一个节区头的部分是一致的,而之后节区与节区之间的间隔,内存中的间隔会更大

产生差异是因为内存对齐,主要是下面两点:

  • 操作系统通常以内存分页为单位(通常是 4 KB 或 2 MB 等)来管理内存。加载 PE 文件时,每个节会被分配到与分页边界对齐的内存地址
  • 在 PE 文件的头信息中,SectionAlignment 字段定义了各个节(段)在内存中的对齐方式。这种对齐方式一般也选择 4 KB 或更大,以保证节在内存中符合分页要求

在开始之前,先来点基础概念。对于学过操作系统的来说,这应该很好理解

  1. 地址

一般是指虚拟地址,而非物理地址,我们知道程序运行时候是使用操作系统分配的内存空间,所以用户并不知道具体的物理地址。

  1. 镜像文件

包含以 EXE 文件为代表的 “可执行文件”、以DLL文件为代表的“动态链接库”。因为他们常常被直接“复制”到内存,有“镜像”的某种意思。

  1. RAV

Relatively Virtual Address。偏移(又称“相对虚拟地址”)。相对镜像基址的偏移。

一般来说,PE文件在硬盘上和在内存里是不完全一样的。各个节在硬盘上是连续的,而在内存中是按页对齐的,所以加载到内存以后节之间会出现一些 “空洞”,这样占用的空间就会大一些 。

因为存在这种对齐,所以在 PE 结构内部,表示某个位置的地址采用了两种方式:

  • 针对在硬盘上存储文件中的地址,称为 原始存储地址 或 物理地址,表示距离文件头的偏移。
  • 针对加载到内存以后映象中的地址,称为 相对虚拟地址(RVA),表示相对内存映象头的偏移。

RVA 是当PE 文件被装到内存中后,某个数据位置相对于文件头的偏移量。

  1. VA

Virtual Address。虚拟地址,程序在虚拟内存中被装载的位置

  1. ImageBase:基址

PE文件的优先装载地址。比如,如果该值是400000h,PE装载器将尝试把文件装到虚拟地址空间的400000h处。字眼”优先”表示若该地址区域已被其他模块占用,那PE装载器会选用其他空闲地址。简而言之,就是指定PE文件载入内存时,优先尝试载入的内存起始地址。

作为web手,来看这些的时候,不得不重新审视一下结构体

首先结构体其实类似于数组,也是一段连续的内存块,只不过他内存块中每一块的大小由结构体成员决定

c语言中结构体访问成员的操作,实际转化为汇编中访问 :结构体的基址+成员的偏移量(一般都是一个立即数)

如果已知结构体的定义,并且有结构体在内存中的基址,就可以解析对应的内存区域并读取其中的数据结构。(这些偏移量是在编译期由编译器根据结构体定义、数据类型大小以及对齐规则计算得到的)

PE结构可以大致分为:

  • DOS部分
  • PE文件头
  • 节表(块表)
  • 节数据(块数据)
  • 调试信息

结构如图:

PE 指纹

  • 首先是根据文件的前两个字节是否为4D 5A,也就是’MZ’,
  • 然后后面还存在50 45,也就是PE

DOS头

​ DOS部分主要是为了兼容以前的DOS系统,DOS部分可以分为DOS MZ文件头(IMAGE_DOS_HEADER)和DOS块(DOS Stub)组成,PE文件的第一个字节位于一个传统的MS-DOS头部,称作IMAGE_DOS_HEADER,其结构如下:

(WORD 2字节 16位

DWORD 4字节 32位)

typedef struct _IMAGE_DOS_HEADER {      // DOS .EXE header
WORD e_magic; // Magic number
WORD e_cblp; // Bytes on last page of file
WORD e_cp; // Pages in file
WORD e_crlc; // Relocations
WORD e_cparhdr; // Size of header in paragraphs
WORD e_minalloc; // Minimum extra paragraphs needed
WORD e_maxalloc; // Maximum extra paragraphs needed
WORD e_ss; // Initial (relative) SS value
WORD e_sp; // Initial SP value
WORD e_csum; // Checksum
WORD e_ip; // Initial IP value
WORD e_cs; // Initial (relative) CS value
WORD e_lfarlc; // File address of relocation table
WORD e_ovno; // Overlay number
WORD e_res[4]; // Reserved words
WORD e_oemid; // OEM identifier (for e_oeminfo)
WORD e_oeminfo; // OEM information; e_oemid specific
WORD e_res2[10]; // Reserved words
LONG e_lfanew; // File address of new exe header
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

如果为DOS系统就会执行,打印输出一句话

然后其中比较重要的是e_lfanew 字段 ,是PE文件头的偏移地址

解析的时候读取为char* , 然后赋值给一个结构体变量即可

(相当于把指针赋值给结构体变量,能通过对应的属性偏移值来访问属性)

PE 头

PE 头也叫NT头

PE头位于DOS Stub 后面,是以PE00为起始标记`

对应c语言中如下结构体:

typedef struct _IMAGE_NT_HEADERS {
DWORD Signature; //PE标志,4字节 ,8位16进制数据
IMAGE_FILE_HEADER FileHeader; //文件头
IMAGE_OPTIONAL_HEADER32 OptionalHeader; //可选PE头
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;

其中文件头:

typedef struct _IMAGE_FILE_HEADER {
WORD Machine; //程序允许的CPU型号 如果为0表示能在任何CPU上允许
WORD NumberOfSections; //文件中存在区段的数量
DWORD TimeDateStamp; //时间戳
DWORD PointerToSymbolTable;
DWORD NumberOfSymbols;
WORD SizeOfOptionalHeader; //可选PE头的大小
WORD Characteristics; // 文件属性
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

可选PE头

可选PE头,虽说是可选PE头,但里面包含了很多重要的信息

typedef struct _IMAGE_OPTIONAL_HEADER {

WORD Magic; //值为10B 表示32位程序,若是20B 表示64位程序
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode; //所有代码段的总大小,按照FileAlignment对齐后的大小
DWORD SizeOfInitializedData; //已初始化的数据大小,按照FileAlignment对齐后的大小
DWORD SizeOfUninitializedData; //未初始化的数据段大小,按照FileAlignment对齐后的大小
DWORD AddressOfEntryPoint; //程序入口 OEP
DWORD BaseOfCode; //代码段开始地址
DWORD BaseOfData; //数据段开始地址
DWORD ImageBase; //内存镜像大小
DWORD SectionAlignment; //内存对齐方式
DWORD FileAlignment; //文件对齐大小
WORD MajorOperatingSystemVersion;
WORD MinorOperatingSystemVersion;
WORD MajorImageVersion;
WORD MinorImageVersion;
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
DWORD Win32VersionValue;
DWORD SizeOfImage; //文件在内存中的大小
DWORD SizeOfHeaders; // DOS头+标准PE头+可选PE头+区段头 的大小
DWORD CheckSum;
WORD Subsystem;
WORD DllCharacteristics;
DWORD SizeOfStackReserve;
DWORD SizeOfStackCommit;
DWORD SizeOfHeapReserve;
DWORD SizeOfHeapCommit;
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes; //数据目录表的个数
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; //数据目录表,数组大小为16
} IMAGE_OPTIONAL_HEADER32,*PIMAGE_OPTIONAL_HEADER32;

可选PE头中DataDirectory 数据目录又存放了很多表的地址,如导入\导出表、重定位表等等是一个十分重要的字段,之后会经常使用, 且DataDirectory是一个长度为16的数组

程序在硬盘(文件)和内存中的对齐方式不相同,导致会有rawSize

RAV与FOA

pe文件在硬盘和内存中的对齐方式不尽相同,导致内存中的偏移(RAV)与在文件中的偏移(FOA )不相同(有些地方又把FOA称之为RAW)

很多时候字段表示的是在内存中的偏移,而我们解析pe文件的时候,只是将文件的内容放入一个char* 的buffer 中而不是加载进内存来运行,因此要将RAV转化为FOA

这些不同只存在于不同的区段(区段对齐大小不一样)

区段(节区)头

学过汇编就知道,一个可执行程序是分段的,指令存放在代码段,数据存放在数据段,等等还有其他的区段

常见节区有code、text、data、resource等。

(区段头就是例如数据段、代码段等等的区段的信息)

把PE文件创建成多个节区结构的好处是,可以保证程序的安全性。若把code与data放在一个节区中相互纠缠很容易引发安全问题,即使忽略过程中的烦琐。假设向字符串data写入数据时,由于某个原因导致溢出,那么其下的code就会被覆盖,应用程序就会崩溃。

类别 访问权限
code节区 执行、读取权限
data节区 非执行、读写权限
resource节区 非执行、读取权限

结构体如下:

typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; //区段名称,此处非字符串,不会以0结尾(不能直接使用char)
union {
DWORD PhysicalAddress;
DWORD VirtualSize;
} Misc; //在内存中的真实大小(未对齐)
DWORD VirtualAddress; //区段在内存中的偏移值 + ImageBase 为真正的地址
DWORD SizeOfRawData; // 区段在文件中对齐后的大小
DWORD PointerToRawData; //区段在文件中的偏移地址
DWORD PointerToRelocations;
DWORD PointerToLinenumbers;
WORD NumberOfRelocations;
WORD NumberOfLinenumbers;
DWORD Characteristics; // 节属性如可读,可写,可执行等
} IMAGE_SECTION_HEADER,*PIMAGE_SECTION_HEADER;

解析第一个区段头使用官方定义的宏函数

IMAGE_FIRST_SECTION(pNtHeaders);

在PE文件中,节区头部是一个连续的结构体数组,每个 IMAGE_SECTION_HEADER 结构体大小固定,因此可以通过简单递增指针的方式访问下一个节区。

简单解析的代码

void ParsePEFromBuffer(char *buffer, bool debug) {
//PIMAGE_DOS_HEADER为指针 ,将buffer的地址直接赋值 ,解析结构体属性的时候就直接能按照属性偏移获取
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)buffer;
//检查DOS头 签名
if (pDosHeader->e_magic != 0x5A4D) {
printf("不是有效的PE文件\n");
delete[] buffer;
return;
}

//NT头偏移= buffer 的偏移地址+ PE文件头偏移
PIMAGE_NT_HEADERS pNtHeaders =(PIMAGE_NT_HEADERS) (pDosHeader->e_lfanew +(uintptr_t) buffer);
if (0x4550 != pNtHeaders->Signature) {
printf("不是有效的PE文件结构");
delete[] buffer;
return;
}
//获取PE 文件头
PIMAGE_FILE_HEADER pFileHeader = &pNtHeaders->FileHeader;
if (debug) {
printf("Machine:%x\n", pFileHeader->Machine);
printf("区段数:%d\n", pFileHeader->NumberOfSections);
printf("可选头大小:%d\n", pFileHeader->SizeOfOptionalHeader);
}
//获取可选PE头
PIMAGE_OPTIONAL_HEADER pOptionHeader=&pNtHeaders->OptionalHeader;
if(debug){
if(0x20b==pOptionHeader->Magic){
printf("64位程序\n");
} else{
printf("32位程序\n");
}
printf("程序入口地址偏移:%x\n",pOptionHeader->AddressOfEntryPoint);
}

//使用官方的宏函数 获取第一个区段头
PIMAGE_SECTION_HEADER pSectionHeader= IMAGE_FIRST_SECTION(pNtHeaders) ;
for(int i=0;i<pFileHeader->NumberOfSections;i++){
//区段名为8个长度+ 0 字符串结尾
char name[9]{0};
//区段名不能直接使用char引用,直接copy内存
memcpy_s(name,9,pSectionHeader->Name,8);
printf("----------------第%d个区段--------------------------\n",i+1);
printf("名称:%s\n",name);
printf("内存地址偏移: %x\n",pSectionHeader->VirtualAddress);
printf("区段大小:%d\n",pSectionHeader->SizeOfRawData);
//因为 所在的地方是一个数组,所以+1能够指向下一个元素
pSectionHeader++;
}

FOA 和RAV转化

经过上面变化,才有了RAV和FOA区分,但是只是各个区段部分,发生了变化,PE头和DOS头其实还是没变,DOS头和PE头中RAV=FAO的

​ 大多数的时候都是RAV,如何将其转化为FOA?

尽管相对于的文件头的偏移变了,但是相对于区段起始的偏移并未改变

因此可以得到公式:

FOA-所在区段FOA = RAV- 所在区段的RAV 	=>

FOA = RAV- 所在区段的RAV + 所在区段FOA

还有一点就是如何确定所在的区段,简单的方法就是遍历区段表

比较是否在区段的RAV范围:

RAV>=pSectionHeader->VirtualAddress && RAV< pSectionHeader->VirtualAddress+pSectionHeader->Misc.VirtualSize

转化代码:

DWORD CPeUtil::RavToFoa(DWORD RAV) {
//FOA=数据的FOA+数据的RVA-区段的RVA
PIMAGE_SECTION_HEADER pSectionHeader=IMAGE_FIRST_SECTION(peHeader->ntHeaders);
// 遍历区段,比较RAV,获取所在的区段
for(int i=0;i<peHeader->ntHeaders->FileHeader.NumberOfSections;i++){
if (RAV>=pSectionHeader->VirtualAddress && RAV< pSectionHeader->VirtualAddress+pSectionHeader->Misc.VirtualSize){
return RAV-pSectionHeader->VirtualAddress+pSectionHeader->PointerToRawData;
}
pSectionHeader++;
}
return 0;
}

导入/导出表

导出表

数据目录DataDirectory是可选PE头中的字段是一个数组,每一个元素都指向了一些结构(如导入、导出表、重定位表)

数据目录元素的结构体IMAGE_DATA_DIRECTORY如下:

typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress; //内存中的偏移
DWORD Size;
} IMAGE_DATA_DIRECTORY,*PIMAGE_DATA_DIRECTORY;

dll动态链接库中需要的导出的函数,都会写入导出表 ,导出表是数据目录表中的第一个元素

然而不是只有dll有导出表(一般情况下exe没有导出表)

导出表IMAGE_EXPORT_DIRECTORY 结构体如下:

typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics;
DWORD TimeDateStamp;
WORD MajorVersion;
WORD MinorVersion;
DWORD Name; //指向导出表文件名 RAV
DWORD Base; //导出函数起始序列
DWORD NumberOfFunctions; //导出函数的个数
DWORD NumberOfNames; //以名称导出函数个数
DWORD AddressOfFunctions; //导出函数地址表 RAV
DWORD AddressOfNames; //导出函数名称表 RAV
DWORD AddressOfNameOrdinals; //导出函数序号表 RAV
} IMAGE_EXPORT_DIRECTORY,*PIMAGE_EXPORT_DIRECTORY;

按照名称取出函数(地址表、序列表、名称表之间的关系)

名称表中函数名称所在的索引-> 序列表中对应元素中的序列号 -> 地址表中所对应的元素

解析导出表代码:

list<PExportFunc> CPeUtil::GetExportTable() {
list<PExportFunc> exportFuncList;
IMAGE_DATA_DIRECTORY exportDirectory =pOptionHeader->DataDirectory[0];
if (exportDirectory.VirtualAddress==0){
//没有导出表
printf("not exist export table\n");
return exportFuncList;
}
DWORD offset= RavToFoa(exportDirectory.VirtualAddress);
if (offset==0){
//没有导出表
printf("not found export table\n");
return exportFuncList;
}
PIMAGE_EXPORT_DIRECTORY peExport=PIMAGE_EXPORT_DIRECTORY (buffer+offset);
char* dllName=RavToFoa(peExport->Name)+ buffer;
printf("dll name:%s\n",dllName);
//获取函数地址表数组
DWORD* funcAddr=(DWORD *)(RavToFoa(peExport->AddressOfFunctions)+buffer);
//获取函数序列表数组
WORD* ordinal= (WORD*)(RavToFoa(peExport->AddressOfNameOrdinals)+buffer);
//获取名称表
DWORD* names=(DWORD *)(RavToFoa(peExport->AddressOfNames)+buffer);
for (int i=0;i<peExport->NumberOfFunctions;i++){
PExportFunc func=new ExportFunc{};
func->VirtualAddress=funcAddr;
for(int j=0;j<peExport->NumberOfNames;j++){
if(ordinal[j]==i){
//序列号等于函数地址表元素的索引
char* name= RavToFoa(names[j])+buffer;
func->Ordinal=ordinal[j];
func->Name=name;
break;
}
}
exportFuncList.push_back(func);
funcAddr++;
}
return exportFuncList;
}

导入表

简单的就是:告诉系统你需要用到哪些dll,用哪些函数。

需要导入调用外部函数

调用多个dll就会有如下多个_IMAGE_IMPORT_DESCRIPTOR结构,导入描述符

typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics; //标志 为0表示结束 没有导入描述符了
DWORD OriginalFirstThunk; //RVA指向IMAGE_THUNK_DATA结构数组
} DUMMYUNIONNAME;
DWORD TimeDateStamp;

DWORD ForwarderChain; //链表的前一个结构
DWORD Name; //RVA,指向DLL名字,该名字以’’\0’’结尾
DWORD FirstThunk; //RVA指向IMAGE_THUNK_DATA结构数组
} IMAGE_IMPORT_DESCRIPTOR;

上述结构体中的OriginalFirstThunk和FirstThunk分别指向其对应的INT表和IAT表

  • IAT (Import Address Table) 导入函数地址表
  • INT(Import Name Table)导入函数名称表

加载DLL的方式实际有两种:一种是显示链接(Explicit Linking),程序使用DLL时加载,函数使用完毕时再释放内存; 一种是隐式链接(Implicit Linking),程序开始时就加载DLL,程序终止时再释放占用的内存。(以后遇到再说(:

OriginalFirstThunk: 指向IMAGE_THUNK_DATA结构数组的RVA, 其内容在程序未运行下,
和OriginalFirstThunk内容一样,,如下:

typedef struct _IMAGE_THUNK_DATA64 {
union {
ULONGLONG ForwarderString;
ULONGLONG Function; //导入函数的地址
ULONGLONG Ordinal; //导入函数的序列号
ULONGLONG AddressOfData; //指向IMAGE_IMPORT_BY_NAME,导入名称
} u1;
} IMAGE_THUNK_DATA64;
typedef IMAGE_THUNK_DATA64 *PIMAGE_THUNK_DATA64;

这4个成员是一个共用体, 在不同情况下代表不同的数据

这个值最高位为1的时候,表示函数是一个序号输出值, 低31位会被看做API的导出序号, 当最高位为0时,
这时候这个值是一个指向IMAGE_IMPORT_BY_NAME结构的RVA

typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint;
CHAR Name[1]; //需导入的函数名称(不定长且以\0结尾)
} IMAGE_IMPORT_BY_NAME,*PIMAGE_IMPORT_BY_NAME;

FirstThunk : 指向IAT,不过在不同的情况下IAT内容不一样。

当TimeDateStamp为0的时候表示未绑定,该字段其实跟OriginalFirstThunk 指向的差不多,对应上图PE加载前

当TimeDateStamp不为0,这时候指向的是函数真实地址表,对应上图PE加载后

解析代码:

typedef struct ImportFunc{
char* dllName; //dll名称
bool useName; //是否使用名字导入
char* name; //函数名
DWORD Ordinal; //函数序列号
}ImportFunc,*PImportFunc;

解析函数,(接着前面的代码)

list<PImportFunc> CPeUtil::GetImportTable() {
list<PImportFunc> importFuncList;
IMAGE_DATA_DIRECTORY importDirectory =pOptionHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT];
//获取到导入表首个元素
PIMAGE_IMPORT_DESCRIPTOR pImportTable=(PIMAGE_IMPORT_DESCRIPTOR)(RavToFoa(importDirectory.VirtualAddress)+buffer);
while (pImportTable->OriginalFirstThunk){

//dll名称
char * dllName= RavToFoa(pImportTable->Name)+buffer;

PIMAGE_THUNK_DATA pThunkData=(PIMAGE_THUNK_DATA)(RavToFoa(pImportTable->OriginalFirstThunk)+buffer);
//遍历所有导入函数
while (pThunkData->u1.Function) {
PImportFunc func=new ImportFunc ();
//判断最高位是否为1,判断是否按照序号导入
if (pThunkData->u1.Ordinal & 0x80000000) {
//低31位为序号
//printf("按照序号导入:%d\n",pThunkData->u1.Ordinal & 0x7FFFFFFF) ;
func->dllName=dllName;
func->useName= false;
func->Ordinal=pThunkData->u1.Ordinal & 0x7FFFFFFF;

} else {
PIMAGE_IMPORT_BY_NAME pImportByName = (PIMAGE_IMPORT_BY_NAME) (RavToFoa(pThunkData->u1.AddressOfData) +
buffer);
// printf("按照名称导入:%s\n", pImportByName->Name);
func->dllName=dllName;
func->useName= true;
func->name = pImportByName->Name;
}
importFuncList.push_back(func);
pThunkData++;
}
pImportTable++;
}
return importFuncList;

}

重定位表

重定位(Relocation):代码重定位是把可执行代码从内存的一块区域移动到另外一块地方。但是如果指令中某些操作数没有随着地址的改变而改变,这样势必导致运行出错。如:全局变量的地址包含在机器码中,而局部变量没有包含绝对地址。

​ 重定位信息是在编译时期由编译器生成,并且保存在应用程序中,在程序执行的时候由操作系统予以修正。如果在装载时该位置已经被别的应用程序使用,操作系统会重新选择一个新的基地址。此时,就需要对所有重定位信息进行纠正,纠正的依据就是PE中的重定位表。

重定位表是数据目录中的第6项,索引为5

对应IMAGE_BASE_RELOCATION结构体:

typedef struct _IMAGE_BASE_RELOCATION {
DWORD VirtualAddress;
DWORD SizeOfBlock;
} IMAGE_BASE_RELOCATION;
typedef IMAGE_BASE_RELOCATION UNALIGNED *PIMAGE_BASE_RELOCATION;

其结构也如下:

解析代码:

//解析重定位表
list<DWORD> CPeUtil::GetReLocation() {
list<DWORD> rvaList;
IMAGE_DATA_DIRECTORY relocationDirectory =pOptionHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC];
PIMAGE_BASE_RELOCATION pRelocation=(PIMAGE_BASE_RELOCATION)(RavToFoa(relocationDirectory.VirtualAddress)+buffer);

while (1){
if(pRelocation->VirtualAddress==0){
break;
}
//获取到指向元素的指针
DWORD* prelocOffset =(DWORD*)pRelocation+4;
//获取块元素个数
DWORD number= (pRelocation->SizeOfBlock-sizeof(IMAGE_BASE_RELOCATION))/2;
for(int i=0;i<number;i++){
//判断高两位是否为3
if((*prelocOffset & 0x3000)== 0x3000){
//取低几位,并加上基址
DWORD rva =((*prelocOffset) & 0x0FF) + pRelocation->VirtualAddress;
rvaList.push_back(rva);
}

prelocOffset++;
}
pRelocation=(PIMAGE_BASE_RELOCATION)((uintptr_t)pRelocation+pRelocation->SizeOfBlock);
}
return rvaList;
}

然后中间的部分高四位表示的是类型。低十二位表示的重定位地址

解析代码

list<DWORD> CPeUtil::GetReLocation() {
list<DWORD> rvaList;
IMAGE_DATA_DIRECTORY relocationDirectory =pOptionHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC];
PIMAGE_BASE_RELOCATION pRelocation=(PIMAGE_BASE_RELOCATION)(RavToFoa(relocationDirectory.VirtualAddress)+buffer);

while (1){
if(pRelocation->VirtualAddress==0){
break;
}
//获取到指向元素的指针
DWORD* prelocOffset =(DWORD*)((uintptr_t)pRelocation + sizeof(IMAGE_BASE_RELOCATION));;
//获取块元素个数
DWORD number= (pRelocation->SizeOfBlock-sizeof(IMAGE_BASE_RELOCATION))/2;
for(int i=0;i<number;i++){
//判断高两位是否为3
if((*prelocOffset & 0x3000)== 0x3000){
//取低几位,并加上基址
DWORD rva =((*prelocOffset) & 0x0FF) + pRelocation->VirtualAddress;
rvaList.push_back(rva);
}

prelocOffset++;
}
pRelocation=(PIMAGE_BASE_RELOCATION)((uintptr_t)pRelocation+pRelocation->SizeOfBlock);
}
return rvaList;
}

扩展学习,可以看看另一位师傅 的文章:

PE文件结构从初识到简单shellcode注入

Reference