查看原文
其他

原创 | 深入解析pe结构(上)

k0e1y SecIN技术平台 2024-05-25

内存分配与文件读写

宏定义

1.无参数的宏定义
正常例子:
#define TRUE 1#define PI 3.1415926#define FALSE 0

注意:只作字符序列的替换工作,不作任何语法的检查
特例:
#include<stdio.h># define TEST 1*1+2int main() {printf("%d", 2*TEST+2);}

如果先把TEST=3计算出来 带入结果是8 但是运行结果却为6
因为C语言会直接把TEST替换成11+2,变成21*1+2+2,结果为
2.带参数的宏定义
例子:
#include<stdio.h># define MAX(A,B) ((A) > (B)?(A):(B))int main() {printf("%d", MAX(1,2));}

注意:宏名标识符与左圆括号之间不允许有空白符,应紧接在一起
与函数的区别:函数需要额外的内存空间,而宏定义不需要

头文件与重复包含问题

如下一段代码,根据c语言的特性,如果函数定义在main函数以下,那么执行这个函数会报错
#include<stdio.h>#include<windows.h>#include"test.h"int main() {

Function();
}void Function() {printf("hello world");}

因此为了避免报错,我们可以在main函数以上进行一个空定义就可以正常运行
#include<stdio.h>#include<windows.h>#include"test.h"void Function(); //添加一个空定义int main() {

Function();
}void Function() {printf("hello world");}

于是我们可以把void Function();添加到一个头文件中,包含头文件即可
重复包含问题
当x.h  y.h两个头文件都包含了z.h头文件时,如果一个程序同时包含了x和y头文件,就相当于重复包含了两次z.h,这时候会引起编译报错

避免方式:
在新版visual studio code中创建头文件时会自动添加#pragma once来避免
在vc++6.0中,可以利用如下代码来避免
#if !defined(ZZZ) //其中zzz任意起名,越乱越好为了确保不会重复#define ZZZ
中间是为了避免重复的代码
#endif

内存的分配释放

int* ptr;//声明指针
//在堆中申请内存,分配128个intptr = (int *)malloc(sizeof(int)*128);
//无论申请的空间大小 一定要进行校验 判断是否申请成功if(ptr == NULL){return 0;}
//初始化分配的内存空间memset(ptr,0,sizeof(int)*128);
//使用。。。*(ptr) = 1;
//使用完毕 释放申请的堆空间free(ptr);
//将指针设置为NULLptr = NULL;

文件读写练习
题目:将记事本的.exe文件读取到内存,并返回读取后在内存中的地址,并将内存中的数据存储到一个文件中,(.exe格式),然后双击打开,看是否能够使用
#include<windows.h>#include<stdio.h>int main() { int size;FILE* fp_in, *fp_out;fp_in = fopen("C:\\Users\\11502\\Desktop\\feige.exe", "rb"); //读取二进制文件if (fp_in == NULL) { //如果读取失败报错停止printf("Fail to open file!\n");exit(0); } fseek(fp_in, 0, SEEK_END); //将文件指针移向文件尾size = ftell(fp_in); //计算文件指针离开头的位置,也就是计算文件的大小rewind(fp_in); //将文件指针移动开头
printf("文件的大小为%d\n", size);char* buffer = (char*)malloc(sizeof(char) * size); //根据文件的大小申请空间
if (buffer == NULL) { //如果申请失败报错printf("Failed to allocate memory\n");exit(0);}fread(buffer, size, 1, fp_in); //将读取的内容写入buffer内存
fclose(fp_in);
printf("在内存中的地址为%x\n", buffer);
fp_out = fopen("C:\\Users\\11502\\Desktop\\feige222.exe", "wb"); //申请文件指针fwrite(buffer, size, 1, fp_out); //写入二进制文件
fclose(fp_out);
free(buffer);
return 0;}
成功执行

DOS头与PE头解析

现在正式进入pe结构的学习

pe文件结构图表

内存对齐与硬盘对齐

在 Windows 操作系统中:
硬盘对齐:通常为 512 字节,也可以是 4KB。200h
内存对齐:在 x86 架构中通常是 4 字节,而在 x64 架构中通常是 8 字节。1000h
在内存中会比在硬盘中占据的空间大,pe文件在内存中大小会被拉伸

当我们用c语言申请一块1字节的内存空间
#include<stdio.h>#include<windows.h>int main() {void* ptr = VirtualAlloc(NULL, 1, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);printf("%x\n", ptr);system("pause");return 0;}

在x32dbg中发现其实是占用了1000h字节的空间,这就是内存对齐的结果

当我们分别用winhex打开磁盘文件和内存文件时也会发现起始地址和文件长度不同
磁盘文件

内存映像文件

DOS头

dos头结构,根据大小相加可以看到DOS头占了64个字节
typedef struct _IMAGE_DOS_HEADER { WORD e_magic; WORD e_cblp; WORD e_cp; WORD e_crlc; WORD e_cparhdr; WORD e_minalloc; WORD e_maxalloc; WORD e_ss; WORD e_sp; WORD e_csum; WORD e_ip; WORD e_cs; WORD e_lfarlc; WORD e_ovno; WORD e_res[4]; WORD e_oemid; WORD e_oeminfo; WORD e_res2[10]; DWORD e_lfanew; } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

我们用winhex加载之后对应寻找即可

由于计算机是小端排序,word大小是2个字节 e_magic的内容是5A4D,根据winhex计算即可
typedef struct _IMAGE_DOS_HEADER { WORD e_magic; //5A 4D WORD e_cblp; //00 90 WORD e_cp; //00 03 WORD e_crlc; //00 00 WORD e_cparhdr; //00 04 WORD e_minalloc; //00 00 WORD e_maxalloc; //FF FF WORD e_ss; //00 00 WORD e_sp; //00 B8 WORD e_csum; //00 00 WORD e_ip; //00 00 WORD e_cs; //00 00 WORD e_lfarlc; //00 40 WORD e_ovno; //00 00 WORD e_res[4]; //因为是数组所以占8位 00 00 00 00 00 00 00 00 WORD e_oemid; //00 00 WORD e_oeminfo; //00 00 WORD e_res2[10]; //20个00 DWORD e_lfanew; //00 00 00 E8 } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

我们可以使用软件来进行对比查看

当DOS头找完之后,根据上方的PE结构图表我们需要找PE头

在DOS头最后的e_lfanew,他的值就是PE头开始的位置,也就是E8的位置

而我圈框的地方也就是DOS头到PE头之间,全部为垃圾数据,因为早期是16位的电脑,现在都是32位和64位的电脑,为了保持兼容性,所以出现了上面的垃圾数据,里面的内容默认是编译器自动添加,我们可以随意修改对PE文件没有任何影响

观察标准PE头,开始是4字节的一个标识也就是00 00 45 50,之后是两个结构体,第一个是标准PE头(IMAGE_FILE_HEADER)
第二个是可选PE头(IMAGE_OPTIONAL_HEADER)

标准pe头

Machine用来确定运行程序的cpu型号
NumberOfSections图中说是区块的数量,其实是节的数量,如果内容为3也就说明有3节,如下图在扩展pe头(OPTION PE)之后就是节

TimeDateStamp为时间戳,记录软件生成的时间,如果时间戳大小超出范围则自然溢出,可以修改
SizeOfOptionalHeader为可选pe头的大小,32位默认E0h,64位默认F0h ,可以被手动修改
Characteristics一共四字节 16位 每一位都有不同的含义,可以通过软件来查看,但是在软件上一共有15位,因为第六位有个此标志保留没有显示出来,软件标相当于1,没有标相当于0,这样可以算出二进制0000 0001 0000 1111

扩展pe头

Magic 说明文件类型:10B 32位下的PE文件 20B 64位下的PE文件
AddressOfEntryPoint(OEP)是程序的入口点(偏移)需要加ImageBase(内存镜像基址)的值才是程序真正入口点
而我们用winhex通过内存加载的值就是ImageBase的值,而通过OD加载的是OEP+ImageBase的值

oep可改,但是改完之后得让他可以运行,之后会学到

再次进入od发现入口地址已经改变,但是没有加节运行起来会报错
SizeOfHeaders所有头加节表的总大小,节表存储着每一节的属性信息

这里放出海东老师总结的重点(带)*

编写程序读取一个.exe文件,输出所有的PE头信息.
#include<stdio.h>#include<windows.h>LPVOID ReadPEFile(LPSTR lpszFile){FILE* pFile = NULL;DWORD fileSize = 0;LPVOID pFileBuffer = NULL;
//打开文件pFile = fopen(lpszFile, "rb");if (!pFile){printf(" 无法打开 EXE 文件! ");return NULL;}//读取文件大小fseek(pFile, 0, SEEK_END);fileSize = ftell(pFile);fseek(pFile, 0, SEEK_SET);//分配缓冲区pFileBuffer = malloc(fileSize);
if (!pFileBuffer){printf(" 分配空间失败! ");fclose(pFile);return NULL;}//将文件数据读取到缓冲区size_t n = fread(pFileBuffer, fileSize, 1, pFile);if (!n){printf(" 读取数据失败! ");free(pFileBuffer);fclose(pFile);return NULL;}//关闭文件fclose(pFile);return pFileBuffer;}
VOID PrintNTHeaders(){LPVOID pFileBuffer = NULL;PIMAGE_DOS_HEADER pDosHeader = NULL;PIMAGE_NT_HEADERS pNTHeader = NULL;PIMAGE_FILE_HEADER pPEHeader = NULL;PIMAGE_OPTIONAL_HEADER32 pOptionHeader = NULL;PIMAGE_SECTION_HEADER pSectionHeader = NULL;
pFileBuffer = ReadPEFile((char*)"C:\\Users\\11502\\Desktop\\feige.exe");if (!pFileBuffer){printf("文件读取失败\n");return;}
//判断是否是有效的MZ标志if (*((PWORD)pFileBuffer) != IMAGE_DOS_SIGNATURE){printf("不是有效的MZ标志\n");free(pFileBuffer);return;}pDosHeader = (PIMAGE_DOS_HEADER)pFileBuffer; //一个头的结构体对这个头的起始地址强制类型转换就相当于生成了这个头的结构体//打印DOC头printf("********************DOS头********************\n");printf("MZ标志:%x\n", pDosHeader->e_magic);printf("PE偏移:%x\n", pDosHeader->e_lfanew);//判断是否是有效的PE标志if (*((PDWORD)((DWORD)pFileBuffer + pDosHeader->e_lfanew)) != IMAGE_NT_SIGNATURE) //由于e_lfanew是doword所以要转dword{printf("不是有效的PE标志\n");free(pFileBuffer);return;}pNTHeader = (PIMAGE_NT_HEADERS)((DWORD)pFileBuffer + pDosHeader->e_lfanew); //打印NT头printf("********************NT头********************\n");printf("NT:%x\n", pNTHeader->Signature);pPEHeader = (PIMAGE_FILE_HEADER)(((DWORD)pNTHeader) + 4);printf("********************PE头********************\n");printf("PE:%x\n", pPEHeader->Machine);printf("节的数量:%x\n", pPEHeader->NumberOfSections);printf("SizeOfOptionalHeader:%x\n", pPEHeader->SizeOfOptionalHeader);//可选PE头pOptionHeader = (PIMAGE_OPTIONAL_HEADER32)((DWORD)pPEHeader + IMAGE_SIZEOF_FILE_HEADER);printf("********************OPTIOIN_PE头********************\n");printf("OPTION_PE:%x\n", pOptionHeader->Magic);//释放内存free(pFileBuffer);}int main() {PrintNTHeaders();}

节表


c++联合体

当我们需要存储一个内容来辨识某人的身份,用学号或者身份证号之一就可以,但是如果我们设置一个结构体,其中总有一块空间是浪费的
struct Student{char 学号;int 身份证号;}

于是引出了联合体,联合体所有变量共用一块内存空间,内存空间的大小取决你最大的数据类型的大小
union Student{char 学号;int 身份证号;}

示例代码
#include<stdio.h>#include<windows.h>union TestUnion{char x;int y;};int main() {TestUnion test;test.y = 0x12345678;printf("%x\n", test.y);printf("%x\n", test.x);test.x = 0x99;printf("%x\n", test.y);system("pause");}

节表结构

节表的位置为DOS头中获取到e_lfanew的值+4(pe标识)+20(标准pe头大小)+标准pe头中SizeOfOptionalHeader(可选pe头的大小)的值

typedef struct _IMAGE_SECTION_HEADER { BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; union { DWORD PhysicalAddress; DWORD VirtualSize; } Misc; DWORD VirtualAddress; DWORD SizeOfRawData; DWORD PointerToRawData; DWORD PointerToRelocations; DWORD PointerToLinenumbers; WORD NumberOfRelocations; WORD NumberOfLinenumbers; DWORD Characteristics;} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

Name参数8位,指节的名字,如果我们要用c语言读取名字,c语言要字符串以0结尾,但是他的名字是任意的,如果占了八位,这时我们读取完名字由于没有0填充可能会出现越界行为,因此读取名字时不能用char *,要用char [9]最后一位存0
Misc是该节在内存中没有对齐前的真实尺寸,该值可以不准确,因此可以被修改,例如A到B就是没有对齐前的大小,而后面的0是为了对齐而填充的。

VirtualAddress是节区在内存中的偏移地址。加上ImageBase才是在内存中的真正地址.
SizeOfRawData 节在文件中对齐后的尺寸
PointerToRawData 节区在文件中的偏移,找到偏移为400,找到400即可,因为在文件中是0开始

找到text段

Characteristics 节的属性,我们可以根据对照表

Characteristics的值是不同的属性相加而成,所以60000020的意思是包含可执行代码,可读,可执行

练习:编写程序打印节表中的信息
在之前程序加上这一段即可
//节表pSectionHeader = (PIMAGE_SECTION_HEADER)((DWORD)pOptionHeader + pPEHeader->SizeOfOptionalHeader);printf("********************SECTION_PE头********************\n");printf("OPTION_PE:%x\n", pSectionHeader->SizeOfRawData);


FileBuffer与ImageBuffer

FileBuffer与ImageBuffer的相互转换(重要)

FileBuffer是在硬盘上的存储状态 ImageBuffer是在内存上存储状
虽然拥有了ImageBuffer的状态但是还不能完全满足运行的条件,但是这是让程序运行的第一步,所以我们要学会将FileBufeer手动转换为ImageBuffer

首先思考一下该流程在c语言的实现过程(FileBuffer ImageBuffer ImageBuffer)

1.读取文件内容并申请内存将内容放到内存中(FileBuffer)2.再申请一块内存存放ImageBuffer,申请的大小在扩展PE头中的SizeOfImage有记录3.由图可知DOS头到节表的部分没有改变可以直接用SizeOfHeaders的大小的数据复制到内存中4.根据NumberOfSections获取表的数量,写一个循环写入即可memcpy((PVOID)((DWORD)pImageTemp + pSectionHeaderTemp->VirtualAddress), (PVOID)((DWORD)pFileBuffer + pSectionHeaderTemp->PointerToRawData), pSectionHeaderTemp->SizeOfRawData);
ImageBuffer To FileBuffer 流程与上方大同小异
C语言实现代码
#include<stdio.h>#include<stdlib.h>#include<windows.h>DWORD ToLoaderPE(LPSTR file_path, PVOID* pFileBuffer); //加载pe返回文件大小DWORD CopyFileBufferToImageBuffer(PVOID pFileBuffer, PVOID* pImageBuffer); //FileBuffer转ImageBufferDWORD CopyImageBufferToNewFileBuffer(PVOID pImageBuffer, PVOID* pNewFileBuffer); //ImageBuffer转FileBufferBOOL MemoryToFile(PVOID pMemBuffer, DWORD size, LPSTR lpszFile); //FileBuffer存盘
char file_path[] = "C:\\Users\\11502\\Desktop\\feige.exe";char write_file_path[] = "C:\\Users\\11502\\Desktop\\feige666.exe";
//返回PE文件大小DWORD ToLoaderPE(LPSTR file_path, PVOID* pFileBuffer){FILE* pFile = NULL;DWORD FileSize = 0;PVOID pFileBufferTemp = NULL;
pFile = fopen(file_path, "rb");
if (!pFile){printf("(ToLoaderPE)Can't open file!\n");return 0;}
fseek(pFile, 0, SEEK_END);FileSize = ftell(pFile);printf("FileBuffer: %#x\n", FileSize);fseek(pFile, 0, SEEK_SET);pFileBufferTemp = malloc(FileSize);
if (!pFileBufferTemp){printf("(ToLoaderPE)Allocate dynamic memory failed!\n");fclose(pFile);return 0;}
DWORD n = fread(pFileBufferTemp, FileSize, 1, pFile);
if (!n){printf("(ToLoaderPE)Read file failed!\n");free(pFileBufferTemp);fclose(pFile);return 0;}*pFileBuffer = pFileBufferTemp;pFileBufferTemp = NULL;fclose(pFile);return FileSize;}
DWORD CopyFileBufferToImageBuffer(PVOID pFileBuffer, PVOID* pImageBuffer){PIMAGE_DOS_HEADER pDosHeader = NULL;PIMAGE_NT_HEADERS pNTHeader = NULL;PIMAGE_FILE_HEADER pPEHeader = NULL;PIMAGE_OPTIONAL_HEADER32 pOptionHeader = NULL;PIMAGE_SECTION_HEADER pSectionHeader = NULL;
PVOID pImageTemp = NULL;
if (!pFileBuffer){printf("(CopyFileBufferToImageBuffer)Can't open file!\n");return 0;}
if (*((PWORD)pFileBuffer) != IMAGE_DOS_SIGNATURE){printf("(CopyFileBufferToImageBuffer)No MZ flag, not exe file!\n");return 0;}
pDosHeader = (PIMAGE_DOS_HEADER)pFileBuffer;
if (*((LPDWORD)((DWORD)pFileBuffer + pDosHeader->e_lfanew)) != IMAGE_NT_SIGNATURE){printf("(CopyFileBufferToImageBuffer)Not a valid PE flag!\n");return 0;}
pNTHeader = (PIMAGE_NT_HEADERS)((DWORD)pFileBuffer + pDosHeader->e_lfanew);pPEHeader = (PIMAGE_FILE_HEADER)(((DWORD)pNTHeader) + 4);pOptionHeader = (PIMAGE_OPTIONAL_HEADER32)((DWORD)pPEHeader + IMAGE_SIZEOF_FILE_HEADER);pSectionHeader = (PIMAGE_SECTION_HEADER)((DWORD)pOptionHeader + pPEHeader->SizeOfOptionalHeader);
pImageTemp = malloc(pOptionHeader->SizeOfImage);
if (!pImageTemp){printf("(CopyFileBufferToImageBuffer)Allocate dynamic memory failed!\n");free(pImageTemp);return 0;}
memset(pImageTemp, 0, pOptionHeader->SizeOfImage);memcpy(pImageTemp, pDosHeader, pOptionHeader->SizeOfHeaders); //这里的pDosHeader改成pFileBuffer也可以
PIMAGE_SECTION_HEADER pSectionHeaderTemp = pSectionHeader;
for (int n = 0; n < pPEHeader->NumberOfSections; n++, pSectionHeaderTemp++){memcpy((PVOID)((DWORD)pImageTemp + pSectionHeaderTemp->VirtualAddress), (PVOID)((DWORD)pFileBuffer + pSectionHeaderTemp->PointerToRawData), pSectionHeaderTemp->SizeOfRawData); //将FileBuffer中的存储,复制到在ImageBuffer中的存储printf("VirtualAddress%d: %#10x PointerToRawData%d: %#10x\n", n, (DWORD)pImageTemp + pSectionHeader->VirtualAddress, n, (DWORD)pFileBuffer + pSectionHeader->PointerToRawData);}
*pImageBuffer = pImageTemp;pImageTemp = NULL;return pOptionHeader->SizeOfImage;}
DWORD CopyImageBufferToNewFileBuffer(PVOID pImageBuffer, PVOID* pNewFileBuffer){PIMAGE_DOS_HEADER pDosHeader = NULL;PIMAGE_NT_HEADERS pNTHeader = NULL;PIMAGE_FILE_HEADER pPEHeader = NULL;PIMAGE_OPTIONAL_HEADER32 pOptionHeader = NULL;PIMAGE_SECTION_HEADER pSectionHeader = NULL;
LPVOID pTempNewbuffer = NULL;
if (!pImageBuffer){printf("(CopyImageBufferToNewBuffer)Can't open file!\n");return 0;}
if (*((PWORD)pImageBuffer) != IMAGE_DOS_SIGNATURE){printf("(CopyImageBufferToNewBuffer)No MZ flag, not exe file!\n");return 0;}
pDosHeader = (PIMAGE_DOS_HEADER)pImageBuffer;if (*((PDWORD)((DWORD)pImageBuffer + pDosHeader->e_lfanew)) != IMAGE_NT_SIGNATURE){printf("(CopyImageBufferToNewBuffer)Not a valid PE flag!\n");return 0;}
pNTHeader = (PIMAGE_NT_HEADERS)((DWORD)pImageBuffer + pDosHeader->e_lfanew);pPEHeader = (PIMAGE_FILE_HEADER)((DWORD)pNTHeader + 4); // 这里必须强制类型转换pOptionHeader = (PIMAGE_OPTIONAL_HEADER32)((DWORD)pPEHeader + IMAGE_SIZEOF_FILE_HEADER);pSectionHeader = (PIMAGE_SECTION_HEADER)((DWORD)pOptionHeader + pPEHeader->SizeOfOptionalHeader);
//获取new_buffer的大小int new_buffer_size = pOptionHeader->SizeOfHeaders;for (DWORD i = 0; i < pPEHeader->NumberOfSections; i++){new_buffer_size += pSectionHeader[i].SizeOfRawData; // pSectionHeader[i]另一种加法}// 分配内存(newbuffer)pTempNewbuffer = malloc(new_buffer_size);if (!pTempNewbuffer){printf("(CopyImageBufferToNewBuffer)Allocate dynamic memory failed!\n");return 0;}memset(pTempNewbuffer, 0, new_buffer_size);memcpy(pTempNewbuffer, pDosHeader, pOptionHeader->SizeOfHeaders);// 循环拷贝节区PIMAGE_SECTION_HEADER pTempSectionHeader = pSectionHeader;for (DWORD j = 0; j < pPEHeader->NumberOfSections; j++, pTempSectionHeader++){//PointerToRawData节区在文件中的偏移,VirtualAddress节区在内存中的偏移地址,SizeOfRawData节在文件中对齐后的尺寸memcpy((PDWORD)((DWORD)pTempNewbuffer + pTempSectionHeader->PointerToRawData), (PDWORD)((DWORD)pImageBuffer + pTempSectionHeader->VirtualAddress), pTempSectionHeader->SizeOfRawData);} //返回数据*pNewFileBuffer = pTempNewbuffer; //暂存的数据传给参数后释放pTempNewbuffer = NULL;return new_buffer_size; // 返回计算得到的分配内存的大小}
BOOL MemoryToFile(PVOID pMemBuffer, DWORD size, LPSTR lpszFile){FILE* fp;fp = fopen(lpszFile, "wb");if (fp != NULL){fwrite(pMemBuffer, size, 1, fp);}fclose(fp);return 1;}
VOID operate(){LPVOID pFileBuffer = NULL;LPVOID pNewFileBuffer = NULL;LPVOID pImageBuffer = NULL;
DWORD ret1 = ToLoaderPE(file_path, &pFileBuffer); // &pFileBuffer(void**类型) 传递地址对其值可以进行修改printf("exe->filebuffer 返回值为计算所得文件大小:%#x\n", ret1);
DWORD ret2 = CopyFileBufferToImageBuffer(pFileBuffer, &pImageBuffer);printf("filebuffer -> imagebuffer返回值为计算所得文件大小:%#x\n", ret2);DWORD ret3 = CopyImageBufferToNewFileBuffer(pImageBuffer, &pNewFileBuffer);printf("imagebuffer -> newfilebuffer返回值为计算所得文件大小:%#x\n", ret3);MemoryToFile(pNewFileBuffer, ret3, write_file_path);
free(pFileBuffer);free(pNewFileBuffer);free(pImageBuffer);}
int main(){operate();getchar();return 0;}

RVA与FOA的转换

RVA就是相对虚拟地址(运行时的位置)(1234),FOA(434)就是文件偏移地址(磁盘静态的位置)
#include<windows.h>#include<stdio.h>
#define FILE_PATH "文件地址"
int GetFileLength(FILE* pf, DWORD* Length){int ret = 0;
fseek(pf, 0, SEEK_END);*Length = ftell(pf);fseek(pf, 0, SEEK_SET);
return ret;}
int MyReadFile(void** pFileAddress){int ret = 0;DWORD Length = 0;//打开文件FILE* pf = fopen(FILE_PATH, "rb");if (pf == NULL){ret = -1;printf("func ReadFile() Error!\n");return ret;}
//获取文件长度ret = GetFileLength(pf, &Length);if (ret != 0 && Length == -1){ret = -2;printf("func GetFileLength() Error!\n");return ret;}
//分配空间*pFileAddress = (PVOID)malloc(Length);if (*pFileAddress == NULL){ret = -3;printf("func malloc() Error!\n");return ret;}memset(*pFileAddress, 0, Length);
//读取文件进入内存fread(*pFileAddress, Length, 1, pf);
fclose(pf);return ret;}
int RVA_TO_FOA(PVOID FileAddress, DWORD RVA, PDWORD pFOA){int ret = 0;
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)(FileAddress);PIMAGE_FILE_HEADER pFileHeader = (PIMAGE_FILE_HEADER)((DWORD)pDosHeader + pDosHeader->e_lfanew + 4);PIMAGE_OPTIONAL_HEADER32 pOptionalHeader = (PIMAGE_OPTIONAL_HEADER32)((DWORD)pFileHeader + sizeof(IMAGE_FILE_HEADER));PIMAGE_SECTION_HEADER pSectionGroup = (PIMAGE_SECTION_HEADER)((DWORD)pOptionalHeader + pFileHeader->SizeOfOptionalHeader);
//RVA在文件头中 或 SectionAlignment 等于 FileAlignment 时RVA等于FOAif (RVA < pOptionalHeader->SizeOfHeaders || pOptionalHeader->SectionAlignment == pOptionalHeader->FileAlignment){*pFOA = RVA;return ret;}
//循环判断RVA在节区中for (int i = 0; i < pFileHeader->NumberOfSections; i++){if (RVA >= pSectionGroup[i].VirtualAddress && RVA < pSectionGroup[i].VirtualAddress + pSectionGroup[i].Misc.VirtualSize){*pFOA = pSectionGroup[i].PointerToRawData + RVA - pSectionGroup[i].VirtualAddress;
return ret;}}
//没有找到地址ret = -4;printf("func RAV_TO_FOA() Error: %d 地址转换失败!\n", ret);return ret;}

int main() {PVOID pFileAddress;DWORD FOA;
MyReadFile(&pFileAddress);RVA_TO_FOA(pFileAddress, (DWORD)0x1080, &FOA);printf("%x", FOA);


return 0;}
注意以上所有的操作均是在全部代码已初始化的条件上完成的


代码节空白区添加代码

目的及思路分析

这里有一个程序,运行之后就会弹出一个对话框,我们如何把这个功能移植到飞鸽传书上呢,这时就需要往空白的节区添加一段我们想要的代码来实现

我们的思路是修改OEP(入口点)为空白区添加代码的位置,然后利用call调用MessageBoxA,随后在使用jmp跳回到程序真正的运行位置

硬编码

我们知道程序在运行的时候都是0和1的形式,并且pe结构都是16进制,我们不可能真正的写汇编代码如call,jmp
这时计算机为我们提供了硬编码这种东西,他是16进制字符,不同的16进制字符表示了不同的含义,那如何知道call和jmp表示的16进制字符呢
将MessageBoxA写入一个函数,这样调用函数时就会在汇编层面调用call
#include<windows.h>void function(){MessageBoxA(0, 0, 0, 0);}int main() {function();return 0;}

我们f9给funciton()下断点,f5运行,右键进入反汇编界面,发现call了function,并且可以看到他的硬编码E8 8D 3B FF FF

如果没有显示硬编码把Code Bytes选上即可

我们在call那里选中看f11可以看到jmp的硬编码E9 01 00 00 00

通过上面我们可以看到E8和E9分别代表着call和jmp,后面的4个字节代表的是要跳转的地址,其实这四字节并非真正要跳转的地址,而是根据这四字节计算机会自动计算出要跳转的真正地址
如果把后面这四字节当做X,跳转地址有一个计算公式
真正要跳转的地址 = E8这条指令的下一行地址 + X
而我们在跳转时候我们关心的是X的值,根据上方公式
X = 真正要跳转的地址 - E8这条指令的下一行地址
观察如下图,由于jmp和call都占了五个字节,于是下一条指令的地址就是当前地址+5,因此 
X = 要跳转的地址 - (E8的地址 + 5)

通过上面我们只知道了如何call messagebox,但是我们还要确定调用之前的准备工作,对messagebox下断点

发现在call之前还调用了四次push 6A 00我们也要记录下来

MessageBoxA的地址

我们既然要弹窗,肯定要用call调用MessageBoxA的地址,由于他是存在于系统文件user32.dll中,所以MessageBox函数的入口点地址也是固定的
因此我们任意载入一个带有弹窗的软件,使用bp MessageBoxA添加断点

点击b就可以看到MessageBoxA的入口地址77E5425F并且是永恒不变的

修改PE文件

由于飞鸽传书这个软件的文件对齐和内存对齐都是1000,因此不需要考虑对齐问题
通过软件查看text表,表是从1000开始从46000(45000+1000)结束

先找一个空白位置,添加4个6A00 然后E8调用messagebox

要计算X,拿出我们之前的公式
X = 要跳转的地址 - (E8的地址 + 5)
要跳转的地址是77E5425F,E8的地址是450B8,但是这只是相对地址,我们真正在内存中执行时需要加上imageBase的地址

因此X=77E5425F-(4450B8+5) X=77 A0 F1 A2 (要倒着写)
使用jmp跳转到程序入口点AddressOfEntryPoint,000441EC X = 4441ec - 4450c2 FF FF F1 2A

注意拿计算器计算时需要选择双字DWORD

找到OEP的地址

将OEP修改为我们添加代码的入口处450BD,因为oep本身就是偏移地址,因此没必要加imagebase

修改完毕保存即可查看效果,同理我们可以添加我们自己生成的shellcode

对齐大小不同情况下的计算方法

选择Notepad发现文件和内存对齐大小不同,这也是大多数软件的情况

根据表格找到Misc+shellcode的大小+Imagebase+virtualaddress就是在Imagebuffer中的内存位置,因此可以找到call和jmp函数的位置
oep的偏移地址就是Misc+shellcode的大小+virtualaddress
大家可以自行使用notepad进行测试

c语言实现:
//func.c 函数的实现
#include <Windows.h>#include<stdio.h>#define FILEPATH "D:/ipmsg.exe"#define FILEPATH_NEW "D:/ipmsg_new.exe"
DWORD ReadPEFile(IN LPSTR lpszFile, OUT LPVOID* pFileBuffer);
DWORD CopyFileBufferToImageBuffer(IN LPVOID pFileBuffer, OUT LPVOID* pImageBuffer);
DWORD CopyImageBufferToNewBuffer(IN LPVOID pImageBuffer, OUT LPVOID* pNewBuffer);
BOOL MemeryTOFile(IN LPVOID pMemBuffer, IN size_t size, OUT LPSTR lpszFile);
DWORD RvaToFileOffset(IN LPVOID pFileBuffer, IN DWORD dwRva);
VOID TestAddCodeIncodeSec();
//打印PE文件信息VOID PrintNTHeaders();

#define FILEPATH_IN#define FILEPATH_OUT#define SHELLCODELENGTH 0x12 //添加代码长度#define MESSAGEBOXADDR 0x754EF280 //执行代码的函数入口地址,就是实际要跳转的地址
//全局变量声明BYTE shellCode[] ={ 0x6A,00,0x6A,00,0x6A,00,0x6A,00, 0xE8,00,00,00,00, 0xE9,00,00,00,00,};
DWORD ReadPEFile(LPSTR lpszFile, OUT LPVOID* pFileBuffer){FILE* pFile = NULL;DWORD fileSize = 0;//LPVOID pFileBuffer = NULL;
//打开文件pFile = fopen(lpszFile, "rb");if (!pFile){printf(" 无法打开 EXE 文件! ");return NULL;}//读取文件大小fseek(pFile, 0, SEEK_END);fileSize = ftell(pFile);fseek(pFile, 0, SEEK_SET);//分配缓冲区*pFileBuffer = malloc(fileSize);
if (!(*pFileBuffer)){printf(" 分配空间失败! ");fclose(pFile);return NULL;}//将文件数据读取到缓冲区size_t n = fread(*pFileBuffer, fileSize, 1, pFile);if (!n){printf(" 读取数据失败! ");free(*pFileBuffer);fclose(pFile);return NULL;}//关闭文件fclose(pFile);return fileSize;}


DWORD CopyFileBufferToImageBuffer(IN LPVOID pFileBuffer, OUT LPVOID* pImageBuffer){
PIMAGE_DOS_HEADER pDosHeader = NULL;PIMAGE_NT_HEADERS pNTHeader = NULL;PIMAGE_FILE_HEADER pPEHeader = NULL;PIMAGE_OPTIONAL_HEADER32 pOptionHeader = NULL;PIMAGE_SECTION_HEADER pSectionHeader = NULL;LPVOID PfileSectionp = NULL;LPVOID PImageSectionp = NULL;
//判断是否是有效的MZ标志if (*((PWORD)pFileBuffer) != IMAGE_DOS_SIGNATURE){printf("不是有效的MZ标志\n");free(pFileBuffer);return NULL;}pDosHeader = (PIMAGE_DOS_HEADER)pFileBuffer;//判断是否是有效的PE标志if (*((PDWORD)((DWORD)pFileBuffer + pDosHeader->e_lfanew)) != IMAGE_NT_SIGNATURE){printf("不是有效的PE标志\n");free(pFileBuffer);return NULL;}
pNTHeader = (PIMAGE_NT_HEADERS)((DWORD)pFileBuffer + pDosHeader->e_lfanew);pPEHeader = (PIMAGE_FILE_HEADER)(((DWORD)pNTHeader) + 4);pOptionHeader = (PIMAGE_OPTIONAL_HEADER32)((DWORD)pPEHeader + IMAGE_SIZEOF_FILE_HEADER);
*pImageBuffer = malloc(pOptionHeader->SizeOfImage);if (*pImageBuffer == NULL){printf("image申请空间失败\n");free(*pImageBuffer);return NULL;}
memset(*pImageBuffer, 0, pOptionHeader->SizeOfImage);//空间初始化为0
memcpy(*pImageBuffer, pFileBuffer, pOptionHeader->SizeOfHeaders);//复制整个PE头ImageBuffer
//*pImageBuffer =(char*)*pImageBuffer+1024; //改变指针指向位置的算法
printf("pImageBuffer当前位置:%p \n", *pImageBuffer);
pSectionHeader = (PIMAGE_SECTION_HEADER)((DWORD)pOptionHeader + pPEHeader->SizeOfOptionalHeader);//节目录的起始位置=可选头位置+可选头大小
printf("********************复制节********************\n");for (size_t i = 0; i < pPEHeader->NumberOfSections; i++){
PImageSectionp = (char*)*pImageBuffer + pSectionHeader->VirtualAddress;printf("复制第%i个节:%p \n", i + 1, PImageSectionp);//pFileBuffer = (char*)pFileBuffer + pSectionHeader->PointerToRawData;
PfileSectionp = (char*)pFileBuffer + pSectionHeader->PointerToRawData;;memcpy(PImageSectionp, PfileSectionp, pSectionHeader->SizeOfRawData);//复制整个PE头ImageBufferpSectionHeader++;}

return pOptionHeader->SizeOfImage;
}


DWORD CopyImageBufferToNewBuffer(IN LPVOID pImageBuffer, OUT LPVOID* pNewBuffer){
int newBuffersize;

PIMAGE_DOS_HEADER pDosHeader = NULL;PIMAGE_NT_HEADERS pNTHeader = NULL;PIMAGE_FILE_HEADER pPEHeader = NULL;PIMAGE_OPTIONAL_HEADER32 pOptionHeader = NULL;PIMAGE_SECTION_HEADER pSectionHeader = NULL;LPVOID PnewSectionp = NULL;LPVOID PImageSectionp = NULL;
//判断是否是有效的MZ标志if (*((PWORD)pImageBuffer) != IMAGE_DOS_SIGNATURE){printf("不是有效的MZ标志\n");free(pImageBuffer);return NULL;}pDosHeader = (PIMAGE_DOS_HEADER)pImageBuffer;//判断是否是有效的PE标志if (*((PDWORD)((DWORD)pImageBuffer + pDosHeader->e_lfanew)) != IMAGE_NT_SIGNATURE){printf("不是有效的PE标志\n");free(pImageBuffer);return NULL;}
pNTHeader = (PIMAGE_NT_HEADERS)((DWORD)pImageBuffer + pDosHeader->e_lfanew);pPEHeader = (PIMAGE_FILE_HEADER)(((DWORD)pNTHeader) + 4);pOptionHeader = (PIMAGE_OPTIONAL_HEADER32)((DWORD)pPEHeader + IMAGE_SIZEOF_FILE_HEADER);pSectionHeader = (PIMAGE_SECTION_HEADER)((DWORD)pOptionHeader + pPEHeader->SizeOfOptionalHeader);
pSectionHeader = pSectionHeader + (pPEHeader->NumberOfSections - 1);//最后一个节的位置newBuffersize = pSectionHeader->PointerToRawData + pSectionHeader->SizeOfRawData;//通过在文件中最后一个节的位置加上节在文件中对齐的大小就是文件大小,也就是要开辟空间的大小pSectionHeader = pSectionHeader - (pPEHeader->NumberOfSections - 1);//恢复到一个节的位置
*pNewBuffer = malloc(newBuffersize); //申请新空间,空间大小应该是文件大小if (*pNewBuffer == NULL){printf("image申请空间失败\n");free(*pNewBuffer);return NULL;}memset(*pNewBuffer, 0, newBuffersize);//空间初始化为0
memcpy(*pNewBuffer, pImageBuffer, pOptionHeader->SizeOfHeaders);//复制整个PE头到内存空间
printf("********************复制节到文件********************\n");for (size_t i = 0; i < pPEHeader->NumberOfSections; i++){
PImageSectionp = (char*)pImageBuffer + pSectionHeader->VirtualAddress;printf("复制第%i个节:%p \n", i + 1, PImageSectionp);//pFileBuffer = (char*)pFileBuffer + pSectionHeader->PointerToRawData;
PnewSectionp = (char*)*pNewBuffer + pSectionHeader->PointerToRawData;;memcpy(PnewSectionp, PImageSectionp, pSectionHeader->SizeOfRawData);//复制整个PE头ImageBufferpSectionHeader++;}

return newBuffersize;
}

BOOL MemeryTOFile(IN LPVOID pMemBuffer, IN size_t size, OUT LPSTR lpszFile){
printf("复制到文件%s,内存地址%p,大小%i\n", lpszFile, pMemBuffer, size);
FILE* fp;fp = fopen(lpszFile, "wb");//缓存内容写入到文件if (fwrite(pMemBuffer, size, 1, fp)){printf("存盘成功\n");
}else{printf("存盘失败\n");}
fclose(fp);
}
DWORD RvaToFileOffset(IN LPVOID pFileBuffer, IN DWORD dwRva){PIMAGE_DOS_HEADER pDosHeader = NULL;PIMAGE_NT_HEADERS pNTHeader = NULL;PIMAGE_FILE_HEADER pPEHeader = NULL;PIMAGE_OPTIONAL_HEADER32 pOptionHeader = NULL;PIMAGE_SECTION_HEADER pSectionHeader = NULL;DWORD VirtualOffset;DWORD foa;//判断是否是有效的MZ标志if (*((PWORD)pFileBuffer) != IMAGE_DOS_SIGNATURE){printf("不是有效的MZ标志\n");free(pFileBuffer);return NULL;}pDosHeader = (PIMAGE_DOS_HEADER)pFileBuffer;//判断是否是有效的PE标志if (*((PDWORD)((DWORD)pFileBuffer + pDosHeader->e_lfanew)) != IMAGE_NT_SIGNATURE){printf("不是有效的PE标志\n");free(pFileBuffer);return NULL;}
pNTHeader = (PIMAGE_NT_HEADERS)((DWORD)pFileBuffer + pDosHeader->e_lfanew);pPEHeader = (PIMAGE_FILE_HEADER)(((DWORD)pNTHeader) + 4);pOptionHeader = (PIMAGE_OPTIONAL_HEADER32)((DWORD)pPEHeader + IMAGE_SIZEOF_FILE_HEADER);pSectionHeader = (PIMAGE_SECTION_HEADER)((DWORD)pOptionHeader + pPEHeader->SizeOfOptionalHeader);

//判断这个内存偏移地址是在哪个节for (size_t i = 0; i < pPEHeader->NumberOfSections; i++){
printf("第%i个节的RVA:%p \n", i + 1, pSectionHeader->VirtualAddress);printf("第%i个节的Mics:%p \n", i + 1, pSectionHeader->Misc.VirtualSize);if ((dwRva >= pSectionHeader->VirtualAddress) && dwRva <= (pSectionHeader->VirtualAddress + pSectionHeader->Misc.VirtualSize)){
VirtualOffset = dwRva - pSectionHeader->VirtualAddress;foa = pSectionHeader->PointerToRawData + VirtualOffset;printf("在第%i个节的VirtualAddress:%p \n", i + 1, pSectionHeader->VirtualAddress);printf("在第%i个节的Mics:%p \n", i + 1, pSectionHeader->Misc.VirtualSize);printf("VirtualOffset偏移:%x \n", VirtualOffset);
return foa;}
pSectionHeader++;}


return NULL;}

VOID TestAddCodeIncodeSec(){LPVOID pFileBuffer = NULL;LPVOID pImageBuffer = NULL;LPVOID pNewBuffer = NULL;size_t size;DWORD foa;
PBYTE codeBegin;//添加代码的位置
PIMAGE_DOS_HEADER pDosHeader = NULL;PIMAGE_NT_HEADERS pNTHeader = NULL;PIMAGE_FILE_HEADER pPEHeader = NULL;PIMAGE_OPTIONAL_HEADER32 pOptionHeader = NULL;PIMAGE_SECTION_HEADER pSectionHeader = NULL;
if (!ReadPEFile((char *)FILEPATH, &pFileBuffer)){printf("文件读取失败\n");return;}
printf("newbuffer开始的位置:%p \n", pNewBuffer);CopyFileBufferToImageBuffer(pFileBuffer, &pImageBuffer);
pDosHeader = (PIMAGE_DOS_HEADER)pImageBuffer;pNTHeader = (PIMAGE_NT_HEADERS)((DWORD)pImageBuffer + pDosHeader->e_lfanew);pPEHeader = (PIMAGE_FILE_HEADER)(((DWORD)pNTHeader) + 4);pOptionHeader = (PIMAGE_OPTIONAL_HEADER32)((DWORD)pPEHeader + IMAGE_SIZEOF_FILE_HEADER);pSectionHeader = (PIMAGE_SECTION_HEADER)((DWORD)pOptionHeader + pPEHeader->SizeOfOptionalHeader);
//判断是否有足够的空间添加shellCodeif (((pSectionHeader->PointerToRawData) - (pSectionHeader->Misc.VirtualSize)) < SHELLCODELENGTH){printf("代码区空闲区别不够");free(pFileBuffer);free(pImageBuffer);
return;}//计算出codeBegin代码添加的起始位置codeBegin = (PBYTE)((DWORD)pImageBuffer + pSectionHeader->VirtualAddress + pSectionHeader->Misc.VirtualSize);memcpy(codeBegin, shellCode, sizeof(shellCode));//复制代码带空白区域//计算E8后面的硬编码=真实要执行的地址-((模块加载基址imageBase+E8下一条地址距离模块基址的偏移))DWORD callAddr = MESSAGEBOXADDR - ((pOptionHeader->ImageBase + ((DWORD)codeBegin + 0xD - (DWORD)pImageBuffer)));*(PDWORD)(codeBegin + 9) = callAddr;//修正E8 后面的硬编码//计算E9后面的硬编码=真实要执行的地址-((模块加载基址imageBase+E9下一条地址距离模块基址的偏移))DWORD jmpAddr = (pOptionHeader->ImageBase + pOptionHeader->AddressOfEntryPoint) - ((pOptionHeader->ImageBase + ((DWORD)codeBegin + 0xD + 5 - (DWORD)pImageBuffer)));*(PDWORD)(codeBegin + 9 + 5) = jmpAddr;//修正E9 后面的硬编码
//修改入口地址pOptionHeader->AddressOfEntryPoint = (DWORD)codeBegin - (DWORD)pImageBuffer;size = CopyImageBufferToNewBuffer(pImageBuffer, &pNewBuffer);printf("newbuffer里的pImageBuffer当前位置:%p \n", pNewBuffer);




MemeryTOFile(pNewBuffer, size, (char *)FILEPATH_NEW);foa = RvaToFileOffset(pFileBuffer, 8224);printf("FOA偏移:%x \n", foa);
}
VOID PrintNTHeaders(){LPVOID pFileBuffer = NULL;PIMAGE_DOS_HEADER pDosHeader = NULL;PIMAGE_NT_HEADERS pNTHeader = NULL;PIMAGE_FILE_HEADER pPEHeader = NULL;PIMAGE_OPTIONAL_HEADER32 pOptionHeader = NULL;PIMAGE_SECTION_HEADER pSectionHeader = NULL;

if (!ReadPEFile((char *)FILEPATH, &pFileBuffer)){printf("文件读取失败\n");return;}
//判断是否是有效的MZ标志if (*((PWORD)pFileBuffer) != IMAGE_DOS_SIGNATURE){printf("不是有效的MZ标志\n");free(pFileBuffer);return;}pDosHeader = (PIMAGE_DOS_HEADER)pFileBuffer;//打印DOC头printf("********************DOC头********************\n");printf("MZ标志:%x\n", pDosHeader->e_magic);printf("PE偏移:%x\n", pDosHeader->e_lfanew);//判断是否是有效的PE标志if (*((PDWORD)((DWORD)pFileBuffer + pDosHeader->e_lfanew)) != IMAGE_NT_SIGNATURE){printf("不是有效的PE标志\n");free(pFileBuffer);return;}pNTHeader = (PIMAGE_NT_HEADERS)((DWORD)pFileBuffer + pDosHeader->e_lfanew);//打印NT头printf("********************NT头********************\n");printf("NT:%x\n", pNTHeader->Signature);pPEHeader = (PIMAGE_FILE_HEADER)(((DWORD)pNTHeader) + 4);printf("********************PE头********************\n");printf("PE:%x\n", pPEHeader->Machine);printf("节的数量:%x\n", pPEHeader->NumberOfSections);printf("时间戳:%x\n", pPEHeader->TimeDateStamp);printf("指向符号表:%x\n", pPEHeader->PointerToSymbolTable);printf("符号表中的符号数量:%x\n", pPEHeader->NumberOfSymbols);printf("可选PE头大小SizeOfOptionalHeader:%x\n", pPEHeader->SizeOfOptionalHeader);printf("文件属性:%x\n", pPEHeader->Characteristics);//可选PE头pOptionHeader = (PIMAGE_OPTIONAL_HEADER32)((DWORD)pPEHeader + IMAGE_SIZEOF_FILE_HEADER);printf("********************OPTIOIN_PE头********************\n");printf("OPTION_PE:%x\n", pOptionHeader->Magic);printf("文件对齐尺寸:%x\n", pOptionHeader->FileAlignment);printf("DOS头+PE头+可选PE头+节表大小:%x\n", pOptionHeader->SizeOfHeaders);printf("内存中文件镜像大小:%x\n", pOptionHeader->SizeOfImage);
//节表pSectionHeader = (PIMAGE_SECTION_HEADER)((DWORD)pOptionHeader + pPEHeader->SizeOfOptionalHeader);printf("********************节表信息********************\n");for (size_t i = 0; i < pPEHeader->NumberOfSections; i++){printf("------------------节表%i------------------\n", i + 1);printf("名称:%s\n", pSectionHeader->Name);printf("真实尺寸:%.8x\n", pSectionHeader->Misc);printf("RVA地址:%.8x\n", pSectionHeader->VirtualAddress);printf("文件对齐后尺寸:%.8x\n", pSectionHeader->SizeOfRawData);printf("文件中偏移:%.8x\n", pSectionHeader->PointerToRawData);printf("行号表的位置:%.8x\n", pSectionHeader->PointerToLinenumbers);printf("重定位表个数:%.8x\n", pSectionHeader->NumberOfRelocations);printf("行号数量:%.8x\n", pSectionHeader->NumberOfLinenumbers);printf("节属性:%.8x\n", pSectionHeader->Characteristics);pSectionHeader++;

}
//释放内存free(pFileBuffer);}

这部分主要进行了节表相关的讲解,下部分会进行动态链接库导入导出表的讲解
pe结构只有多看多练才能真正属于自己


往期推荐



原创 | 2023 CISCN 第十六届全国大学生信息安全竞赛初赛 WriteUp

原创 | CVE-2022-24481

原创 | 一文带你理解AST Injection


继续滑动看下一个
向上滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存