PE文件结构

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 或更大,以保证节在内存中符合分页要求

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

地址

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

镜像文件

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

RAV

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

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

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

针对在硬盘上存储文件中的地址,称为 原始存储地址 或 物理地址,表示距离文件头的偏移。

针对加载到内存以后映象中的地址,称为 相对虚拟地址(RVA),表示相对内存映象头的偏移。

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

VA

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

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;iNumberOfSections;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;intHeaders->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 CPeUtil::GetExportTable() { list 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;iNumberOfFunctions;i++){ PExportFunc func=new ExportFunc{}; func->VirtualAddress=funcAddr; for(int j=0;jNumberOfNames;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 CPeUtil::GetImportTable() { list 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 CPeUtil::GetReLocation() { list 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;iVirtualAddress; rvaList.push_back(rva); } prelocOffset++; } pRelocation=(PIMAGE_BASE_RELOCATION)((uintptr_t)pRelocation+pRelocation->SizeOfBlock); } return rvaList;}

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

解析代码

list CPeUtil::GetReLocation() { list 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;iVirtualAddress; rvaList.push_back(rva); } prelocOffset++; } pRelocation=(PIMAGE_BASE_RELOCATION)((uintptr_t)pRelocation+pRelocation->SizeOfBlock); } return rvaList;}

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

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

Reference

https://tttang.com/archive/1553

[an error occurred while processing the directive]