0x01 不多比比,直接上源码
// 同时遍历PE文件的导入表、导出表的函数名
#include<Windows.h>
#include<stdio.h>
#pragma warning(disable : 4996)
DWORD RVA_2_RAW(char *buf, DWORD Rva, DWORD Raw, BOOL flag);
DWORD Import(char *buf);
DWORD Export(char *buf);
void main()
{
char PATH[] = "C:\\Windows\\System32\\atl.dll"; // 目标PE文件
long PEFileSize; // 偏移字节数
FILE *fp = fopen(PATH, "rb"); // 尝试读取(r)一个二进制(b)文件,成功则返回一个指向文件结构的指针,失败返回空指针
if (fp == NULL)
{
printf("PE文件读取失败!\n");
}
fseek(fp, 0, SEEK_END); // 设置文件流指针指向PE文件的结尾处
PEFileSize = ftell(fp); // 得到文件流指针当前位置相对于文件头部的偏移字节数,即获取到了PE文件大小(字节)
char *buf = new char[PEFileSize]; // 新建一个数组指针buf,指向一个以PE文件字节数作为大小的数组
memset(buf, 0, PEFileSize); // buf指针指向内存中数组的开始位置
// 在这里用0初始化一块PEFileSize大小的内存,即为数组分配内存
fseek(fp, 0, SEEK_SET); // 将文件流指针指向PE文件头部
fread(buf, 1, PEFileSize, fp); // 从给定输入流fp中读取PEFILESize大小个数据项保存到buf字符数组中,每项大小为1字节
// 这样将PE文件读入内存实际上就让buf指向了PE文件的基地址ImageBase
fclose(fp);
Import(buf); // 这个buf在接下来的操作中将一直指向文件的首地址,也就是ImageBase文件基址
Export(buf);
delete buf;
}
// 文件已经读取到内存中了
DWORD RVA_2_RAW(char *buf, DWORD RVA, DWORD RAW, BOOL flag) // RVA为导入表或导出表的RVA;flag为1,RVA转偏移,为0反过来——事实上此程序只用到了1
{
PIMAGE_DOS_HEADER pDOS = (PIMAGE_DOS_HEADER)buf; // 获取DOS头,pDos为PIMAGE_DOS_HEADER结构体的实例,buf则指向PE文件的基地址
PIMAGE_NT_HEADERS pNT = (PIMAGE_NT_HEADERS)(buf + pDOS->e_lfanew); // 获取NT头,pNT为PIMAGE_DOS_HEADER的实例,DOS头的e_lfanew成员指示了NT头的偏移量
PIMAGE_SECTION_HEADER pSection = (PIMAGE_SECTION_HEADER)(buf + pDOS->e_lfanew + 0x18 + pNT->FileHeader.SizeOfOptionalHeader);
// 获取区块表头,pSection为PIMAGE_SECTION_HEADER的实例
// +0x18指向了可选头,加上可选头的大小即指向了Section表头的首部,可选头的大小存放在文件头的成员中
DWORD SectionNumber = pNT->FileHeader.NumberOfSections; // 通过文件头获取区块数(节区数)
DWORD SizeOfAllHeadersAndSectionList = pNT->OptionalHeader.SizeOfHeaders; // 所有头(DOS+NT)+区块表的大小,是一个大小而不是地址
DWORD Imp_Exp_FA = 0; // 导入导出表在磁盘文件中的地址
DWORD SectionRVA = 0; // 暂存每个节区表的RVA
int i = 0;
if (flag)
{
if (RVA < SizeOfAllHeadersAndSectionList) // 如果导入导出表的RVA连节区表都没出,直接返回,因为(DOS+NT头+节区表)在内存中不展开
{
Imp_Exp_FA = RVA;
}
for (; i < SectionNumber;i++) // 有多少节区就循环几次,从第一个节区表开始操作,如果PE文件有N个节,那么区块表就是由N个IMAGE_SECTION_HEADER组成的数组
{
SectionRVA = pSection[i].VirtualAddress; // 该区块加载到内存中的RVA
// 计算该导入导出表的RVA位于哪个区块内
if (RVA > SectionRVA && SectionRVA + pSection[i].Misc.VirtualSize > RVA)// &&后面为:该区块的RVA(该区块在内存中的起始地址) + 该区块没有对齐处理之前的实际大小(磁盘中的大小。Misc是共用体)
{
Imp_Exp_FA = RVA - SectionRVA + pSection[i].PointerToRawData; // (导入导出表的RVA - 所在节区的基址)得到导入导出表相对该节区的偏移量offset,然后offset + 该节区在磁盘文件中的VA = FOA,得到了文件偏移地址(即导入导出表在磁盘文件中的地址)
break; // 找到了就不再遍历节区了
}
}
}
else
{
if (RAW < SizeOfAllHeadersAndSectionList) // 这里就是通过RAW求RVA了(该程序并未用到) 注意文件偏移地址就是在磁盘文件中的地址:RAW==FOA==FA (其实一共就3个概念:VA RVA FA,分别是虚拟绝对地址,虚拟相对地址,文件绝对地址)
{
Imp_Exp_FA = RAW;
}
for (; i < SectionNumber; i++)
{
SectionRVA = pSection[i].PointerToRawData;
if (RAW > SectionRVA && SectionRVA + pSection[i].SizeOfRawData > RAW)
{
Imp_Exp_FA = RAW - SectionRVA + pSection[i].VirtualAddress;
break;
}
}
}
return Imp_Exp_FA;
}
// 导入表一般包含DLL和普通API两部分,因此要分别考虑
DWORD Import(char *buf)
{
PIMAGE_DOS_HEADER pDOS = (PIMAGE_DOS_HEADER)buf;
PIMAGE_NT_HEADERS pNT = (PIMAGE_NT_HEADERS)(buf + pDOS->e_lfanew);
DWORD ImportTableRVA = pNT->OptionalHeader.DataDirectory[1].VirtualAddress; // 获得导入表的RVA,DataDirectory[1]是导入表,0是导出表
DWORD ImportDLL_FA = RVA_2_RAW(buf, ImportTableRVA, 0, 1); // 计算导入DLL在磁盘文件中的地址,这个变量专门保存DLL地址
IMAGE_IMPORT_DESCRIPTOR *pImportTable = (IMAGE_IMPORT_DESCRIPTOR *)(buf + ImportDLL_FA); // pImportTable指向导入的DLL在磁盘文件中的地址
printf("导入表:\n");
while (pImportTable->FirstThunk) // FirstThunk为IMAGE_IMPORT_DESCRIPTOR的成员,是一个PIMAGE_THUNK_DATA类型的实例,指向IAT的RVA,IAT其实就是一个由许多IMAGE_THUNK_DATA结构组成的数组
{
// 打印DLL名字
DWORD ImportDLLName_RVA = pImportTable->Name; // 指向被输入的DLL的名称(ASCII字符串)的RVA,注意只针对DLL
ImportDLL_FA = RVA_2_RAW(buf, ImportDLLName_RVA, 0, 1); // 求出导入函数名在磁盘文件中的物理地址
printf(" %s--------------------------我是DLL哟\n", buf + ImportDLL_FA); // 打印这个DLL的名字
// 打印普通API名字
IMAGE_THUNK_DATA *pThunk = (IMAGE_THUNK_DATA *)(buf + RVA_2_RAW(buf, pImportTable->OriginalFirstThunk, 0, 1));
// OriginalFirstThunk指向包含IMAGE_THUNK_DATA(输入函数名称表)结构的数组 // FirstThunk指向IAT的RVA
while (pThunk->u1.Function) // u1.Function为被输入函数的内存地址
{
char *psFuncName = (char *)buf + RVA_2_RAW(buf, pThunk->u1.AddressOfData + 2, 0, 1);// u1.AddressOfData指向了IMAGE_IMPORT_BY_NAME,+2则指向了Name成员
printf(" %s\n", psFuncName); // 打印函数名
pThunk++; // IAT其实就是一个由许多IMAGE_THUNK_DATA结构组成的数组,因此指向下一个IMAGE_THUNK_DATA结构
}
pImportTable++; // 指针++就是指向下一个内存地址,即指向下一个IMAGE_IMPORT_DESCRIPTOR结构
}
return 0;
}
DWORD Export(char *buf)
{
PIMAGE_DOS_HEADER pDOS = (PIMAGE_DOS_HEADER)buf;
PIMAGE_NT_HEADERS pNT = (PIMAGE_NT_HEADERS)(buf + pDOS->e_lfanew);
DWORD ExportTableRVA = pNT->OptionalHeader.DataDirectory[0].VirtualAddress; // 获得导出表的RVA,DataDirectory[1]是导入表,0是导出表
DWORD ExportAPI_FA = RVA_2_RAW(buf, ExportTableRVA, 0, 1); // 计算导出函数在磁盘文件中的地址
IMAGE_EXPORT_DIRECTORY *pExportTable = (IMAGE_EXPORT_DIRECTORY *)(buf + ExportAPI_FA); // 指向导出函数在磁盘文件中的地址
PDWORD ExportAPIName_FA = (PDWORD)(buf + RVA_2_RAW(buf, pExportTable->AddressOfNames, 0, 1)); // 将 指向函数名地址表的RVA 转化为FA
DWORD ExportAPINameOriginals_FA = (DWORD)(buf + RVA_2_RAW(buf, pExportTable->AddressOfNameOrdinals, 0, 1)); // 将 指向函数名序号表的RVA 转化为FA
DWORD index = 0;
printf("\n导出表:\n");
while (DWORD(ExportAPIName_FA + index) < ExportAPINameOriginals_FA) // AddressOfNameOridinals(0x24)在结构体中的位置就在AddressOfNames(0x20)下面
{
printf(" %s\n", buf + RVA_2_RAW(buf, (DWORD)(*(ExportAPIName_FA + index)), 0, 1));
index++;
}
return 0;
}
`
0x02 效果如下
`
0x03 只遍历导出表
#include "stdafx.h"
#include <stdio.h>
#include <Windows.h>
// 流程:获取PE文件——获取DOS头地址——获取PE头地址——获取数据目录项地址——获取导出表地址——获取成员变量
int main(){
HMODULE hDll = LoadLibraryA("C:\\Windows\\twain_32.dll"); // 获取PE文件的名称
if (!hDll) return 0; // 是否获取到
IMAGE_EXPORT_DIRECTORY* exportDir; // 定义一个导出表结构类型的指针,包括成员为好多重要的字段
int baseAddr = (int)hDll; // 将获取到的库文件名转化为int型基地址,即DOS头地址VA
int RVA, VA;
RVA = *((int*)(baseAddr + 0x3c)); // 通过DOS头找到PE头(NT头)相对于DOS头的偏移量RVA,DOS头偏移0x3c处的成员的值是PE头部的相对虚拟地址,*取其值
VA = baseAddr + RVA; // PE头的绝对地址VA
RVA = *((int*)(VA += 0x78)); // 首先获取DataDirectory(数据目录项)绝对地址VA,因为PE头部的DataDirectory相对于PE头部的偏移为0x78,而且DataDirectory的第一项为导出表目录,即0x78,所以*取其值即取到了导出表的偏移地址RVA
exportDir = (IMAGE_EXPORT_DIRECTORY*)(baseAddr + RVA); // 加上DOS头基地址得到导出表绝对地址VA
// 下面三个都是指向各自表的首地址
int* RVAFunctions = (int*)(baseAddr + exportDir->AddressOfFunctions); // VA = 基址 + 导出函数地址表RVA
int* RVANames = (int*)(baseAddr + exportDir->AddressOfNames); // VA = 基址 + 导出函数名称表RVA
short* Ordinals = (short*)(baseAddr + exportDir->AddressOfNameOrdinals); // VA = 基址 + 导出函数名称序号表
int i, VAFunction, ordinal;
int numName = exportDir->NumberOfNames; // 以名称导出的函数的总数,作为循环打印次数
printf("函数名序号\t函数名\t\t地址VA\n");
for (i=0; i<numName; i++){
ordinal = *(Ordinals + i); // 取(序号表+1)的值,即在序号表中找到是第几个作为索引值
RVA = *(RVAFunctions + ordinal); // 在函数地址表中找到对应函数的地址为RVA
VAFunction = baseAddr + RVA; // 函数绝对地址就是DOS头基地址 + RVA
RVA = *(RVANames + i); // 名称地址表中找到索引
VA = baseAddr + RVA; // 绝对地址为DOS头基址+RVA
printf("%d\t\t%s\t0x%x\n", ordinal, (char*)VA, VAFunction);
}
return 0;
}
·
0x04 效果如下
0x05 判断一个有效的PE文件·
/*********************************************************
* 说明:判断一个文件是否为一个有效的 PE 文件
* 关键字段:
* - IMAGE_DOS_HEADER 中的 e_magic
* - IMAGE_NT_HEADERS 中的 Signature
* 两个字段的值分别要为 0x00005A4D 和 0x00004550
* 相应的宏为 IMAGE_DOS_SIGNATURE 和 IMAGE_NT_SIGNATURE
*********************************************************/
#include <windows.h>
#include <stdio.h>
int main(void)
{
// 1.首先须打开一个文件
HANDLE hFile = CreateFile(
TEXT("x86.exe"),
GENERIC_ALL,
NULL,
NULL,
OPEN_EXISTING,
NULL,
NULL
);
// 2.判断文件句柄是否有效,若无效则提示打开文件失败并退出
if (hFile == INVALID_HANDLE_VALUE)
{
printf("打开文件失败!\n");
CloseHandle(hFile);
exit(EXIT_SUCCESS);
}
// 3.若打开文件成功,则获取文件的大小
DWORD dwFileSize = GetFileSize(hFile, NULL);
// 4.申请内存空间,用于存放文件数据
BYTE * FileBuffer = new BYTE[dwFileSize];
// 5.读取文件内容
DWORD dwReadFile = 0;
ReadFile(hFile, FileBuffer, dwFileSize, &dwReadFile, NULL);
// 6.判断这个文件是不是一个有效的PE文件
// 6.1 先检查DOS头中的MZ标记,判断e_magic字段是否为0x5A4D,或者是IMAGE_DOS_SIGNATURE
DWORD dwFileAddr = (DWORD)FileBuffer;
auto DosHeader = (PIMAGE_DOS_HEADER)dwFileAddr;
if (DosHeader->e_magic != IMAGE_DOS_SIGNATURE)
{
// 如果不是则提示用户,并立即结束
MessageBox(NULL, TEXT("这不是一个有效PE文件"), TEXT("提示"), MB_OK);
delete FileBuffer;
CloseHandle(hFile);
exit(EXIT_SUCCESS);
}
// 6.2 若都通过的话再获取NT头所在的位置,并判断e_lfanew字段是否为0x00004550,
// 或者是IMAGE_NT_SIGNATURE
auto NtHeader = (PIMAGE_NT_HEADERS)(dwFileAddr + DosHeader->e_lfanew);
if (NtHeader->Signature != IMAGE_NT_SIGNATURE)
{
// 如果不是则提示用户,并立即结束
MessageBox(NULL, TEXT("这不是一个有效PE文件"), TEXT("提示"), MB_OK);
delete FileBuffer;
CloseHandle(hFile);
exit(EXIT_SUCCESS);
}
// 7.若上述都通过,则为一个有效的PE文件
MessageBox(NULL, TEXT("这是一个有效PE文件"), TEXT("提示"), MB_OK);
delete FileBuffer;
CloseHandle(hFile);
// 8.结束程序
return 0;
}
·
0x06 相关重要资料
1、VA、RVA、RAW、FOA、FA 是什么鬼?
https://www.cnblogs.com/iBinary/p/7653693.html
2、导入函数中几个重要结构
https://www.cnblogs.com/lanuage/p/7725699.html
3、PE文件结构图,一个带偏移量、一个不带,都要看
https://pan.baidu.com/s/161-6ZKgtm1AR4kP76Yx4eA