PE加载过程
硬盘文件->加载到内存(FileBuffer)->PE Loader加载并拉伸->ImageBuffer(起始位置ImageBase)
DOS头
从0x00~0x3f共0x40字节固定大小
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;
e_magic: pe指纹 "MZ"
e_lfanew: pe头的偏移
其他成员无关紧要
DOS Stub
从dos头到pe头之间是dos存根
dos存根的数据基本没用,主要是在DOS环境运行时执行
我们可以用DosBox的DOS环境运行exe程序
运行结果
查看DosStub处代码
NT/PE头
PE文件头由PE文件头标识,标准PE头,扩展PE头三部分组成
32位
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;//20字节
IMAGE_OPTIONAL_HEADER32 OptionalHeader;//32位0xE0字节 64位0xF0字节
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
64位
typedef struct _IMAGE_NT_HEADERS64 {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER64 OptionalHeader;
} IMAGE_NT_HEADERS64, *PIMAGE_NT_HEADERS64;
Signature=50 40 00 00 'PE\0\0'
FileHeader是标准PE头
OptionalHeader是可选PE头 但是非常重要
标准PE头/文件头
占20字节 在pe文件头标识后即可找到
typedef struct _IMAGE_FILE_HEADER {
WORD Machine; //程序允许的cpu型号,为0代表所有
WORD NumberOfSections; //区段数量
DWORD TimeDateStamp; //时间戳
DWORD PointerToSymbolTable;
DWORD NumberOfSymbols;
WORD SizeOfOptionalHeader; //可选pe头大小 32位默认E0 64位默认F0
WORD Characteristics; //文件属性,每个位有不同含义
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
重要成员
Machine //cpu型号
NumberOfSections //节区数
SizeOfOptionalHeader //可选PE头大小 有默认值,可修改
WORD Characteristics; //属性
可选PE头/扩展头
typedef struct _IMAGE_OPTIONAL_HEADER {
//
// Standard fields.
//
WORD Magic; //用于标识32/64位文件 PE32: 10B PE64: 20B
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode; //所有代码段的总大小 按照FileAlignment对齐后的大小 编译器填入 无用
DWORD SizeOfInitializedData; //所有已初始化数据区块的大小 按文件对齐 编译器填入 没用(可改)
DWORD SizeOfUninitializedData; //所有未初始化数据区块的大小 按文件对齐 编译器填入 没用(可改)
DWORD AddressOfEntryPoint; //程序入口OEP RVA
DWORD BaseOfCode; //代码区起始地址
DWORD BaseOfData; //数据区起始地址
//
// NT additional fields.
//
DWORD ImageBase; //内存镜像基址(程序默认载入基地址)
DWORD SectionAlignment; //内存中对齐大小
DWORD FileAlignment; //文件的对齐大小(存储在硬盘中的对齐值)
WORD MajorOperatingSystemVersion;
WORD MinorOperatingSystemVersion;
WORD MajorImageVersion;
WORD MinorImageVersion;
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
DWORD Win32VersionValue;
DWORD SizeOfImage; //内存中整个PE文件的映射的尺寸,可比实际值大,必须是SectionAlignment的整数倍
DWORD SizeOfHeaders; //所有的头加上节表文件对齐之后的值
DWORD CheckSum; //映像校验和,一些系统.dll文件有要求,判断是否被修改
WORD Subsystem;
WORD DllCharacteristics; //文件特性,不是针对DLL文件的,16进制转换2进制可以根据属性对应的表格得到相应的属性
DWORD SizeOfStackReserve;
DWORD SizeOfStackCommit;
DWORD SizeOfHeapReserve;
DWORD SizeOfHeapCommit;
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes;
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; //数据目录表,结构体数组
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
重要成员:
AddressOfEntryPoint; //程序入口OEP
ImageBase; //内存镜像地址
SectionAlignment; //内存对齐大小
FileAlignment; //文件对齐大小
SizeOfImage; //文件在内存中的大小(按照SectiionAlignment对齐后)
SizeOfHeaders; //DOS头+NT头+标准PE头+可选PE头+节表 按照文件对齐后的总大小
NumberOfRvaAndSizes //数据目录表个数
DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES] //数据目录表 存放导出表导入表等的地址和大小
节表
紧跟在可选头后面的就是节表,PE中的节表以数组形式存在
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; //8字节节区名
unio{ //内存中的大小,该节在没有对齐之前的真实尺寸,该值可以不准确
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;
重要成员
Name[IMAGE_SIZEOF_SHORT_NAME]; 节区名
VirtualSize; 节区大小
VirtualAddress; 节区起始地址
PointerToRawData; 节区文件偏移
Characteristics; 节区属性
打印节表
//打印节表
void showSectionHeaders() {
printf("\n----------SectionHeaders----------\n");
for (DWORD i = 0; i < numberOfSections; i++) {
//逐个读取节表并打印
printf("\n----------Section%d----------\n", i);
printf("Name: %8s\n", pSectionHeader[i].Name);
printf("VirtualSize: %x\n", pSectionHeader[i].Misc.VirtualSize);
printf("VirtualAddress: %x\n", pSectionHeader[i].VirtualAddress);
printf("SizeOfRawData: %x\n", pSectionHeader[i].SizeOfRawData);
printf("PointerToRawData: %x\n", pSectionHeader[i].PointerToRawData);
printf("Characteristics: %x\n", pSectionHeader[i].Characteristics);
printf("\n----------Section%d----------\n", i);
}
printf("\n----------SectionHeaders----------\n");
}
运行结果
代码段空白区添加代码
基本原理
在代码区添加硬编码(汇编代码的十六进制形式),修改oep使得程序开始执行时执行注入的代码
最后再跳转回原始的oep
- 获取函数地址
- 计算偏移
- 代码区手动添加代码
- 修改oep指向shellcode
- 执行完shellcode后跳回oep
注意: 需要先关闭程序的随机基址,在可选头的DllCharacteristics中,将0x40改为0x00即可
)
案例分析
示例程序代码
#include <Windows.h>
#include <stdio.h>
int main() {
MessageBox(0, L"HelloWorld!", L"Title", 0);
return 0;
}
运行后会弹出HelloWorld弹窗,这里仅做简单注入,四个参数全部压入0,此时会弹出错误窗口
分析:
-
首先在.text段中找一段空白代码区用于写入硬编码
这里选取59A0处开始写入 5A00开始是.rdata段
-
确定硬编码
首先是四个参数 6A 00 6A 00 6A 00 6A 00 (4个push 0)
然后是call MessageBox和jmp oep
MessageBox地址可以运行od 输入bp MessageBoxA下断点找到
OEP为411023(ImageBase=400000 ep=11023)
-
计算call和jmp的偏移
call和jmp的硬编码分别为E8 E9 他们后面跟的4字节数据是偏移值
且offset=desAddr-(call/jmp Addr+5)
即偏移值等于目的地址减去自身地址的下个指令地址(自身指令长度为5,所以+5是下个指令地址)
由于.text段的rva=11000 所以va=400000+11000=411000
那么59A0处的RVA=59A0-400+411000=4165A0
call offset=763C0E50-4165AD=75FAA8A3
jmp offset=411023-4165B2=FFFFAA71
-
写入硬编码并修改
写入后的代码![10 text段写入后的代码](10 text段写入后的代码.png)
修改oep 这里改的是rva 将原本的入口点11023改为165A0即可
执行结果
可以看到程序入口点已经被修改为4165A0 并且输出错误弹窗,之后会跳转到原始的OEP处输出HelloWorld弹窗
新增节添加代码
通过.text段空白区注入代码实用性不高,通过新增节可以增大注入代码量,灵活性更高
基本过程:
-
判断是否有足够空间创建新的节表
每个节表占40字节 要保证有80字节空白区(多余40字节用于兼容部分系统)
在节表尾部和段首部之间便是空白区
如果尾部空白区大小不足,可以将PE头整体向上移动,覆盖掉DOS Stub(这段数据不影响程序运行)
![15 节表空闲区](15 节表空闲区.png)
-
创建新的节表
这里可以通过复制.text段的节表实现,复制之后需要调整部分成员
-
矫正PE文件信息
需要修改的成员有
Name // 节区名称
VirtualAddress // 节区的 RVA 地址(在内存中的偏移地址)
SizeOfRawData // 节区文件中对齐后的尺寸
PointerToRawData // 节区在文件中的偏移量
Characteristics // 节区的属性如可读,可写,可执行等
NumberOfSections
SizeOfImage
-
申请新的空间用于存储新的PE文件
-
写入注入代码
-
保存文件
代码实现
//创建新的节区 返回新节区指针
PIMAGE_SECTION_HEADER CreateNewSection(const char* NewSectionName,DWORD NewSectionSize) {
//1. 检查节表空闲区是否足够保存新的节表 80字节
//空白空间起始地址=NT头偏移+NT头大小+所有节表大小
DWORD BlankMemAddr = (NToffset + sizeof(IMAGE_NT_HEADERS)) + numberOfSections * sizeof(IMAGE_SECTION_HEADER);
DWORD BlankMemSize = sizeOfHeaders - BlankMemAddr;//空白空间大小=SizeOfHeaders-各个表头大小-所有节表大小
if (BlankMemSize < sizeof(IMAGE_SECTION_HEADER) * 2)
return NULL;
//2. 申请新的空间
ExpandFileBuffer(NewSectionSize);
PIMAGE_SECTION_HEADER pNewSectionHeader = (PIMAGE_SECTION_HEADER)(FileBuffer + BlankMemAddr);//指向新增的节表
//3. 复制.text段的节表信息
for (DWORD i = 0; i < numberOfSections; i++) {
if (!strcmp((char*)pSectionHeader[i].Name, ".text"))
{
memcpy(pNewSectionHeader, (LPVOID)&pSectionHeader[i], sizeof(IMAGE_SECTION_HEADER));
break;
}
}
//4. 修正PE文件信息
//标准PE头
pFileHeader->NumberOfSections = ++numberOfSections; //NumberOfSections +1
//节区头
memcpy(pNewSectionHeader->Name, NewSectionName,strlen(NewSectionName));//name
pNewSectionHeader->Misc.VirtualSize = NewSectionSize; //virtualsize
//注意这里必须先修改VirtualAddress
//virtualaddress 各段间是紧邻着的 所以可以根据上个段的末尾来确定新段的起始地址 上个段的起始地址+上个段的大小对于0x1000向上取[url=]片[/url]整即可
pNewSectionHeader->VirtualAddress = AlignSize(pSectionHeader[numberOfSections - 2].VirtualAddress + pSectionHeader[numberOfSections - 2].SizeOfRawData, 0x1000);
pNewSectionHeader->SizeOfRawData = NewSectionSize;//SizeOfRawData
//PointerToRawData 文件偏移=上个段的文件起始地址+段在文件中的大小
pNewSectionHeader->PointerToRawData = pSectionHeader[numberOfSections - 2].PointerToRawData + pSectionHeader[numberOfSections - 2].SizeOfRawData;
pNewSectionHeader->Characteristics |= 0x20000000; //Characteristics 可执行权限
//可选头
pOptionalHeader->SizeOfImage = sizeOfImage = sizeOfImage + AlignSize(NewSectionSize,0x1000);//可选PE头 SizeOfImage 必须是内存对齐的整数倍 直接添加一页大小
return pNewSectionHeader;
}
//通过创建新节区的方式注入代码
BOOL InjectCodeByCreateNewSection() {
//1. 创建新的节区
PIMAGE_SECTION_HEADER pNewSectionHeader = CreateNewSection(".inject", 0x1000);
//修正可选头
DWORD OEP = addressOfEntryPoint; //保存OEP
pOptionalHeader->DllCharacteristics &= 0xFFFFFFBF;//取消ASLR随机基址 随机基址的值是0x40 所以和(0xFFFFFFFF-0x40)进行与运算即可
pOptionalHeader->AddressOfEntryPoint = addressOfEntryPoint= pNewSectionHeader->VirtualAddress;//修改EP 注意ep=rva 不用加基址
//2. 将代码写入新的节区
BYTE InjectCode[18] = { //偏移 指令
0x6a,0x00, //0 push 0
0x6a,0x00, //0 push 0
0x6a,0x00, //0 push 0
0x6a,0x00, //0 push 0
0xe8,0x00,0x00,0x00,0x00, //8 call MessageBox MessageBox=0x763C0E50 这个地址会随着系统启动而变化
0xe9,0x00,0x00,0x00,0x00 //13 jmp oep
};
DWORD MessageBoxAddr = 0x76260E50;
//矫正call和jmp地址
*(DWORD*)&InjectCode[9] =OffsetOfCallAndJmp(MessageBoxAddr, imageBase + pNewSectionHeader->VirtualAddress+8) ;
*(DWORD*)&InjectCode[14] = OffsetOfCallAndJmp(OEP, pNewSectionHeader->VirtualAddress + 13);//跳转回oep正常执行程序
memcpy(FileBuffer + pNewSectionHeader->PointerToRawData, InjectCode, sizeof(InjectCode));//将代码写入新的内存空间
//3. 保存文件
return FileBufferWriteToFile(L"InjectCodeByCreateNewSection1.exe");
}
执行结果
inject节表
inject节区
程序运行情况
扩大节
当节表后的空白区大小不够时,可以选择扩大节
注意只能扩大最后一个节区,因为这样才不会影响到后续偏移
基本过程:
-
申请空间存储新的FileBuffer
-
修正Pe信息
需要修正的成员有
SizeOfRawData // 节区文件中对齐后的尺寸
VirtualSize //内存对齐后的尺寸
SizeOfImage //映像大小
-
保存修改后的PE文件
代码
//扩大节区
BOOL ExpandSection(DWORD ExSize) {
//扩大节区大小是针对ImageBuffer而言的,所以我们添加的大小要进行内存对齐
//1. 申请一块新空间
ExpandFileBuffer(ExSize); //注意这个节表指针要在申请新空间之后
PIMAGE_SECTION_HEADER pLastSectionHeader = &pSectionHeader[numberOfSections - 1];//只能扩大最后一个节区
//2. 调整SizeOfImage
//如果VirtualSize+ExSize超过了AlignSize(VirtualSize,0x1000) 那么需要调整,否则不需要改变
//例如vs=0x500 ex=0x400 显然,原始vs内存对齐也会占0x1000 扩展后没有超过0x1000
//取文件大小和内存大小的最大值
//先计算扩展后的内存对齐值和扩展前的内存对齐值之间的差值
DWORD AlignExImage = AlignSize(pLastSectionHeader->Misc.VirtualSize + ExSize, 0x1000) -
AlignSize(max(pLastSectionHeader->Misc.VirtualSize, pLastSectionHeader->SizeOfRawData), 0x1000);//内存对齐后的值
if(AlignExImage >0)//如果差值>0说明需要扩展映像 否则内存对齐的空白区足够存储扩展区
pOptionalHeader->SizeOfImage = sizeOfImage = sizeOfImage + AlignExImage;
//3. 修改文件大小和内存大小 注意要在修改sizeofimage后再更新这两个值
pLastSectionHeader->SizeOfRawData += AlignSize(ExSize, 0x200);//文件大小必须是文件对齐整数倍
pLastSectionHeader->Misc.VirtualSize += ExSize;//由于是内存对齐前的大小,所以直接加上文件对齐后的大小即可
//4. 保存文件
return FileBufferWriteToFile(L"ExpandSectionFile.exe");
}
执行结果
可以看到原始节区VirtualSize=57E RawSize=600
扩大0x1400后
新VirtualSize=57E+1400=197E
新RawSize=600+1400=1A00
Image大小仅增加1000
这是由于400+600=A00没有超过内存对齐大小,原来的内存额外空间可以容纳这400字节,所以映像只需增加一个页
合并节
合并节也是针对ImageBuffer而言,可以直接对Imagebuffer操作
- 将所有节区属性合并到节区1
- 调整节表的文件偏移和内存大小
- 删除其他节表
- 保存文件
代码
//合并所有节区为1个
BOOL CombineSection() {
//1. 直接修改ImageBuffer
PIMAGE_DOS_HEADER pDosHeaderOfImage = (PIMAGE_DOS_HEADER)imageBuffer;
PIMAGE_NT_HEADERS pNtHeadersOfImage = (PIMAGE_NT_HEADERS)(imageBuffer + pDosHeader->e_lfanew);
PIMAGE_FILE_HEADER pFileHeaderOfImage = (PIMAGE_FILE_HEADER)(&pNtHeadersOfImage->FileHeader);
PIMAGE_OPTIONAL_HEADER pOptionalHeaderOfImage = (PIMAGE_OPTIONAL_HEADER)(&pNtHeadersOfImage->OptionalHeader);
PIMAGE_SECTION_HEADER pSectionHeaderOfImage = (PIMAGE_SECTION_HEADER)((DWORD)pOptionalHeaderOfImage + pFileHeaderOfImage->SizeOfOptionalHeader);
//复制节区属性
for (DWORD i = 1; i < numberOfSections; i++) {
pSectionHeaderOfImage[0].Characteristics |= pSectionHeaderOfImage[i].Characteristics;
}
//调整节表
pSectionHeaderOfImage[0].PointerToRawData = pSectionHeaderOfImage[0].VirtualAddress;//文件偏移改为内存偏移
pSectionHeaderOfImage[0].Misc.VirtualSize = pSectionHeaderOfImage[0].SizeOfRawData = sizeOfImage - pSectionHeaderOfImage[0].VirtualAddress;//新的节区大小为所有节区内存大小之和
pOptionalHeaderOfImage->SizeOfHeaders = AlignSize(sizeOfHeaders - (numberOfSections - 1) * sizeof(IMAGE_SECTION_HEADER), 0x200);//调整头大小
//删除其他节表
memset(&pSectionHeaderOfImage[1], 0, sizeof(IMAGE_SECTION_HEADER) * (numberOfSections - 1));
pFileHeaderOfImage->NumberOfSections = 1;
return ImageBufferWriteToFile(L"CombineSectionFromDailyExercise.exe");
}
执行结果
合并前节表包含9个节区,PE文件很紧凑 原文件大小39KB
合并后仅剩一个节区 并且PE文件很空旷,大部分空间是0 新文件大小128KB
合并后节区开始地址为1000 而headers到200处截止 此时又可以添加新的节区
RVA&VA&FOA&RAW
RVA(Relative Virtual Address)VA(Virtual Address)
- RVA(Relative Virtual Address) RVA是相对虚拟地址,它是相对于模块的加载基址(ImageBase)的地址偏移量。在可执行文件中,RVA通常用于指定代码或数据在内存中的位置。RVA是相对于模块内部的地址,不受具体加载地址的影响。
RVA = VA - ImageBase
-
VA(Virtual Address) VA是虚拟地址,它是代码或数据在内存中的真实地址,用于指定在内存中的具体位置。在运行时,操作系统会将RVA转换为真实的VA,即根据模块的加载基址(ImageBase)将RVA映射到内存中的实际地址。
VA = RVA + ImageBase
-
FOA(File Offset Address) 是可执行文件(如PE文件)中的文件偏移地址,它指的是代码或数据在文件中的位置偏移量。与RVA和VA不同,FOA是直接表示在文件中的偏移量,不涉及地址重定位或内存映射。
FOA和RAW均是指文件偏移
总结:
- RVA是相对于模块加载基址的地址偏移量,RVA是在可执行文件中使用的,用于在文件中表示位置
- VA是代码或数据在内存中的真实地址,VA是在运行时使用的,表示内存中的实际位置。
- FOA是代码或数据在文件中距离文件起始位置(0x00)的偏移值
换算
已知RVA求FOA
-
RVA=VA-ImageBase
-
确定RVA所在节区
可通过节区内存起始地址RVA<=RVA<=节区内存起始地址RVA+节区大小
即 VirtualAddress<=RVA<=VirtualAddress+VirtualSize判断RVA应该属于哪个节区
-
FOA=RVA-节区起始地址RVA+节区文件偏移=RVA - VirtualAddress + PointerToRawData
已知FOA求RVA
-
确定FOA所在节区
节区文件起始地址<=FOA<=节区文件起始地址+节区大小
即 PointerToRawData<=FOA<=PointerToRawData+SizeOfRawData
-
RVA=FOA-节区文件起始地址+节区内存起始地址RVA=FOA-PointerToRawData+VirtualAddress
实例
OEP的VA=411023
ImageBase=400000
.text 段
VirtualAddress=11000 (节区内存起始地址RVA) PointerToRawData=400 (节区文件偏移FOA)
VirtualSize(节区在内存中的大小)=5585 SizeOfRawData=5600(节区在文件中的大小)
节区在文件中的大小大于内存中的大小,因为文件对齐所以会有部分填充0的空白区域
注: 这里的VirtualSize并不一定准确,因为加载后内存对齐值为1000 所以实际内存大小应该是6000
所以OEP RVA=411023-400000=11023
显然该RVA属于.text段: 11000<=RVA<=11000+5585
OEP FOA=11023-11000+400=423
通过PETools检查可以发现入口点FOA(即RAW)=0x423 与计算结果一致
换算代码
//RVA转FOA
DWORD RVA2FOA(DWORD RVA) {
DWORD FOA = 0;
//1. 判断RVA属于哪个节区 节区内存起始地址<=RVA<=节区内存起始地址+节区大小 内存大小需要对齐 注意右边界应该是开区间
//2. FOA=RVA-VirtualAddress+PointerToRawData
for (DWORD i = 0; i < numberOfSections; i++) {
if (RVA >= pSectionHeader[i].VirtualAddress && RVA < pSectionHeader[i].VirtualAddress + AlignSize(pSectionHeader[i].Misc.VirtualSize, 0x1000))//成功找到所属节区
{
FOA = RVA - pSectionHeader[i].VirtualAddress + pSectionHeader[i].PointerToRawData;
break;
}
}
return FOA;
}
//FOA转RVA
DWORD FOA2RVA(DWORD FOA) {
DWORD RVA = 0;
//1. 判断FOA属于哪个节区 节区文件起始地址<=FOA<=节区文件起始地址+节区大小 文件大小默认是对齐值
//2. RVA=FOA-PointerToRawData+VirtualAddress
for (DWORD i = 0; i < numberOfSections; i++) {
if (FOA >= pSectionHeader[i].PointerToRawData && FOA < pSectionHeader[i].PointerToRawData + pSectionHeader[i].SizeOfRawData) {
RVA = FOA - pSectionHeader[i].PointerToRawData + pSectionHeader[i].VirtualAddress;
break;
}
}
return RVA;
}
//输入原始大小和对齐值返回对齐后的大小
DWORD AlignSize(DWORD OrigSize, DWORD AlignVal) {
//通过对对齐值取模判断是否对齐,如果对齐则返回原值,否则返回对齐后的值
return OrigSize % AlignVal ? (OrigSize / AlignVal + 1) * AlignVal : OrigSize;
}
静态/动态链接库
静态库
在链接阶段,会将汇编生成的目标文件.o与引用到的库一起链接打包到可执行文件中。因此对应的链接方式称为静态链接。
优点 方便
缺点 二进制代码需要编译到exe中,浪费空间
注: 静态库的二进制代码保存在.lib中
生成静态库
在VS中可以创建静态库项目,建议创建空项目
在静态库项目中创建StaticLib.cpp源文件和StaticLib.h头文件
代码实例
//StaticLib.cpp 源文件保存函数代码
#include <stdio.h>
void func() {
printf("HelloStaticLib!");
}
//StaticLib.h 头文件保存函数声明
void func();
点击生成即可生成静态库得到.lib文件
使用静态库函数
在项目中找到StaticLib.lib和StaticLib.h文件,复制到需要使用该静态库的工程目录中
在工程目录中导入StaticLib.h文件
程序代码
#include <stdio.h>
#include "StaticLib.h" //导入头文件,头文件有函数声明
#pragma comment(lib,"StaticLib.lib")//加载静态库,这里保存函数二进制代码
int main() {
func(); //直接使用静态库中定义的函数即可
return 0;
}
运行结果
动态库
动态库是在程序需要使用时加载,不同程序可以使用同一份dll,大大节省了空间
dll查看工具推荐https://github.com/lucasg/Dependencies
同创建静态库类似,建议使用空项目,创建好后将项目属性中的生成文件修改为动态库Dll即可
然后创建头文件和源文件
导出函数
生成动态库文件有关键字导出和.def导出两种方式
__declspec关键字导出函数
关键字功能解释
extern //表示是全局函数 可供其他函数调用
"C" //按照C语言的方式编译链接,此时函数名不变 C++由于有重载机制,编译出的函数符号名会比较复杂
__declspec(dllexport)//告诉编译器该函数为导出函数
代码实例
//DllTest.h
extern "C" __declspec(dllexport) void func();
//DllTest.cpp
//注意这里.cpp和.h的函数前都需要有__declspec(dllexport) 否则只会生成.dll而没有.lib
//注意都要有extern "C" 即保证函数声明和函数定义要一致
#include<stdio.h>
extern "C" __declspec(dllexport) void func() {
printf("HelloDynamicLib!");
}
点击生成即可得到.dll和.lib文件
.def文件导出函数
首先在源文件目录创建.def文件
代码示例
//DllTest.cpp
#include<stdio.h>
void func1() {
printf("HelloDynamicLib!");
}
int plus(int x, int y) {
return x + y;
}
int sub(int x, int y) {
return x - y;
}
//DllTest.def
LIBRARY "DllTest" //标识工程目录
EXPORTS //导出标识
func1 @15 //函数名@序号
plus @1
sub @3 NONAME //NONAME导出的函数只有序号没有函数名
设置一下链接器,找到输入,修改模块定义文件如下
点击生成即可得到.dll和.lib文件
注意NONAME导出的函数只有序号没有函数名,原来的sub函数在这里是Oridinal_3
使用dll
隐式链接
基本步骤
-
将.dll .lib放到工程目录中
-
使用#pragma comment(lib,"dllname.lib")导入lib文件
静态库的.lib文件保存二进制代码,而dll中的.lib文件仅仅是指示函数位置
-
加入函数声明
extern "C" __declspec(dllexport) void func();
具体示例
首先将.dll和.lib放到工程目录中
程序代码
#pragma comment(lib,"DllTest.lib") //导入.lib
extern "C" __declspec(dllimport) void func();//导入函数声明
int main() {
func();//直接调用
return 0;
}
运行结果
显式链接
基本使用方法
//1. 定义函数指针
typedef int (__stdcall *lpPlus)(int,int);
//2. 声明函数指针
lpPlus plus;
//3. 动态加载dll
HINSTANCE hModule=LoadLibrary("Dllname.dll");
//4. 获取函数地址
plus=(lpPlus)GetProcAddress(hModule,"_Plus@8");
//默认的cdecl可以直接用函数名 如果是__stdcall会导致函数名改变
//5. 调用函数
int a=plus(1,2);
示例程序代码
#include<Windows.h>//包含了win32的函数和数据结构
#include<stdio.h>
int main() {
typedef int(*lpPlus)(int, int);
lpPlus plus;
HINSTANCE hModule = LoadLibrary(L"DllTest.dll");//L是代表宽字符
plus = (lpPlus)GetProcAddress(hModule, "plus");
printf("%d", plus(1, 2));
return 0;
}
运行结果
数据目录表
定义
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;//虚拟地址RVA,数据目录表的起始位置
DWORD Size;//大小
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
表项定义
#define IMAGE_DIRECTORY_ENTRY_EXPORT //0 导出表
#define IMAGE_DIRECTORY_ENTRY_IMPORT //1 导入表
#define IMAGE_DIRECTORY_ENTRY_RESOURCE //2 资源目录
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION //3 异常目录
#define IMAGE_DIRECTORY_ENTRY_SECURITY //4 安全目录
#define IMAGE_DIRECTORY_ENTRY_BASERELOC //5 重定位基本表
#define IMAGE_DIRECTORY_ENTRY_DEBUG //6 调试目录
#define IMAGE_DIRECTORY_ENTRY_COPYRIGHT //7 描述字串 64位为ARCHITECTURE
#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR //8 机器值
#define IMAGE_DIRECTORY_ENTRY_TLS //9 TLS目录
#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG //10 载入配值目录
#define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT //11 绑定输入表
#define IMAGE_DIRECTORY_ENTRY_IAT //12 导入地址表
#define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT //13 延迟载入描述
#define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR//14 COM信息
//第16个保留
可选头的最后两个成员分别定义了数据目录表的个数和数据目录表数组,指向了一些关键表格
DWORD NumberOfRvaAndSizes;
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; //数据目录表,结构体数组
比较重要的有导出表,导入表,重定位表,IAT表
打印数据目录表
//打印数据目录表
void PrintDirectory() {
PIMAGE_DATA_DIRECTORY pDirectory = pOptionalHeader->DataDirectory;
printf("\n**********数据目录表**********\n");
for (DWORD i = 0; i < pOptionalHeader->NumberOfRvaAndSizes; i++) {
switch (i) {
case IMAGE_DIRECTORY_ENTRY_EXPORT:
printf("\n==========导出表==========\n");
break;
case IMAGE_DIRECTORY_ENTRY_IMPORT:
printf("\n==========导入表==========\n");
break;
case IMAGE_DIRECTORY_ENTRY_RESOURCE:
printf("\n==========资源目录==========\n");
break;
case IMAGE_DIRECTORY_ENTRY_EXCEPTION:
printf("\n==========异常目录==========\n");
break;
case IMAGE_DIRECTORY_ENTRY_SECURITY:
printf("\n==========安全目录=========\n");
break;
case IMAGE_DIRECTORY_ENTRY_BASERELOC:
printf("\n==========重定位基本表==========\n");
break;
case IMAGE_DIRECTORY_ENTRY_DEBUG:
printf("\n==========调试目录==========\n");
break;
case IMAGE_DIRECTORY_ENTRY_ARCHITECTURE:
printf("\n==========描述字串==========\n");
break;
case IMAGE_DIRECTORY_ENTRY_GLOBALPTR:
printf("\n==========机器值==========\n");
break;
case IMAGE_DIRECTORY_ENTRY_TLS:
printf("\n==========TLS目录==========\n");
break;
case IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG:
printf("\n==========载入配置目录==========\n");
break;
case IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT:
printf("\n==========绑定输入表==========\n");
break;
case IMAGE_DIRECTORY_ENTRY_IAT:
printf("\n==========导入地址表==========\n");
break;
case IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT:
printf("\n==========延迟导入表==========\n");
break;
case IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR:
printf("\n==========COM信息==========\n");
break;
case 15:
printf("\n==========保留表==========\n");
break;
}
printf("VirtualAddress=%x\nSize=%x\nFOA=%x\n", pDirectory[i].VirtualAddress, pDirectory[i].Size,RVA2FOA(pDirectory[i].VirtualAddress));
}
printf("\n**********数据目录表打印完毕**********\n\n");
}
导出表
导出表记录了pe文件导出的函数,所以.exe和.dll程序都可以导出函数
数据目录表中记录了导出表的地址和偏移 这个地址是RVA,需要转换为FOA
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics;
DWORD TimeDateStamp;
WORD MajorVersion;
WORD MinorVersion;
DWORD Name; // 指向导出表文件名 RVA-> FOA + FileBuffer=char *name
DWORD Base; // 导出函数起始序号
DWORD NumberOfFunctions; // 所有导出函数的个数
DWORD NumberOfNames; // 以函数名称导出的函数个数
DWORD AddressOfFunctions; // 导出函数地址表首地址RVA 记录了所有导出函数的地址 每个表项大小4字节
DWORD AddressOfNames; // 导出函数名称表首地址RVA 每个表项都是函数名的字符串指针RVA 每个表项大小4字节
DWORD AddressOfNameOrdinals; // 导出函数序号表首地址RVA 其中存储的序号为-Base后的值 每个表项大小2字节
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
通过NONAME关键字可以使部分导出函数没有名称仅有地址
关键成员
Name
Base
NumberOfFunctions
NumberOfNames
AddressOfFunctions
AddressOfNames
AddressOfNameOrdinals
具体示例
假设:
导出函数名称表 FuncNameTable
导出函数序号表 FuncOridinalTable
导出函数地址表 FuncAddressTable
函数地址RVA FuncAddress
使用上一节的Dll函数
//DllTest.def
LIBRARY "DllTest"
EXPORTS
func1 @15
plus @1
sub @3 NONAME //sub序号为3 无导出函数名
//DllTest.cpp
#include<stdio.h>
#include<windows.h>
void func1() {
printf("HelloDynamicLib!");
}
int plus(int x, int y) {
return x + y;
}
int sub(int x, int y) {
return x - y;
}
DLL的导出表信息
序号导出
假设已知导出函数序号OridinalNum
那么FuncAddress=FuncAddressTable[OridinalNum-Base]
即导出函数序号-Base值可以直接作为下标查找导出函数地址表得到导出函数地址
已知函数sub的导出序号为3 所以3-1=2直接查找得到其地址
![36 通过序号查找导出函数过程](36 通过序号查找导出函数过程.png)
所以无名函数的序号可以通过遍历导出函数地址表来得到
名称导出
通过函数名称查找函数地址的过程
-
首先查找导出函数名称表,判断数组中哪个字符串和目的函数名称相同
-
将该元素的下标作为索引,查找导出函数序号表
-
将导出函数序号表中该下标元素的内容作为下标查找导出函数地址表,该值即为函数地址
即
if(strcmp(name,FuncNameTable[i])==0)
FuncAddress=FuncAddressTable[FuncOridinalTable[i]];
假设要查找plus函数
plus这个函数名在函数名称表中的下标为1
而FuncOridinalTable[1]=0
所以plus Address=FuncAddressTable[0]=1125d
![](35 导出函数查找过程.png)
注意
- 导出函数地址表中有很多地址为0的项目
由于导出函数地址表的大小=NumberOfFunctions=导出函数最大序号-最小序号
当序号不是连续时,就会用0地址填充多余表项
-
导出函数序号表存储的序号是真实序号-Base
所以序号最小的导出函数对应的存储序号是0
-
导出函数序号表中不保存无名称函数的序号
通过序号查找函数时,将序号值-Base直接作为下标查找导出函数地址表
通过序号查找函数地址
//通过函数序号获取函数地址
DWORD GetFuncAddrByOridinals(WORD OridinalNum) {
DWORD* pExportFuncAddressTable = (DWORD*)(RVA2FOA(pExportDirectory->AddressOfFunctions) + FileBuffer);//导出函数地址表
return pExportFuncAddressTable[OridinalNum - pExportDirectory->Base];//减去Base值作为索引直接查找函数地址
}
通过函数名查找函数地址
代码
//通过函数名获取函数地址
DWORD GetFuncAddrByName(const char* FuncName) {
WORD* pExportFuncOridinalsTable = (WORD*)(RVA2FOA(pExportDirectory->AddressOfNameOrdinals) + FileBuffer);//导出函数序号表
DWORD* pExportFuncAddressTable = (DWORD*)(RVA2FOA(pExportDirectory->AddressOfFunctions) + FileBuffer);//导出函数地址表
DWORD* pExportFuncNamesTable = (DWORD*)(RVA2FOA(pExportDirectory->AddressOfNames) + FileBuffer);//导出函数名称表
DWORD pos = -1,OridinalNum=0;
//1. 通过导出函数名称表得到序号表下标
for (DWORD i = 0; i < pExportDirectory->NumberOfNames; i++) {
//注意导出函数名称表表项是字符串指针 该指针值为RVA
if (strcmp(FuncName, (char*)(RVA2FOA(pExportFuncNamesTable[i])+FileBuffer)) == 0)
{
pos = i;
break;
}
}
if (pos == -1)//查找失败
return 0;
//2. 通过序号表得到序号
OridinalNum = pExportFuncOridinalsTable[pos];
//3. 得到函数地址
return pExportFuncAddressTable[OridinalNum];
}
运行结果和PE工具显示的一致
打印导出表
//根据函数序号返回函数名地址RVA
BYTE* GetFuncNameByOridinals(WORD OridinalNum) {
WORD* pExportFuncOridinalsTable = (WORD*)(RVA2FOA(pExportDirectory->AddressOfNameOrdinals) + FileBuffer);//导出函数序号表
DWORD* pExportFuncNamesTable = (DWORD*)(RVA2FOA(pExportDirectory->AddressOfNames) + FileBuffer);//导出函数名称表
for (DWORD i = 0; i < pExportDirectory->NumberOfNames; i++)
{
if (pExportFuncOridinalsTable[i] == OridinalNum)//实际存储的序号=函数序号-base
return RVA2FOA(pExportFuncNamesTable[i])+FileBuffer;
}
return NULL;//没有找到说明是无名函数
}
//打印导出表详细信息
void PrintExportDirectory() {
printf("\n==========导出表==========\n");
printf("Name: %x (%s)\n",pExportDirectory->Name,(char*)(FileBuffer+RVA2FOA(pExportDirectory->Name)));
printf("Base: %x\n", pExportDirectory->Base);
printf("NumberOfFunctions: \t%x\n", pExportDirectory->NumberOfFunctions);
printf("NumberOfNames: \t\t%x\n", pExportDirectory->NumberOfNames);
printf("AddressOfFunctions: \tRVA=%x\tFOA=%x\n", pExportDirectory->AddressOfFunctions,RVA2FOA(pExportDirectory->AddressOfFunctions));
printf("AddressOfNames: \tRVA=%x\tFOA=%x\n", pExportDirectory->AddressOfNames, RVA2FOA(pExportDirectory->AddressOfNames));
printf("AddressOfNameOrdinals: \tRVA=%x\tFOA=%x\n", pExportDirectory->AddressOfNameOrdinals , RVA2FOA(pExportDirectory->AddressOfNameOrdinals));
WORD* pExportFuncOridinalsTable =(WORD*)(RVA2FOA(pExportDirectory->AddressOfNameOrdinals) + FileBuffer);//导出函数序号表
DWORD* pExportFuncAddressTable = (DWORD*)(RVA2FOA(pExportDirectory->AddressOfFunctions)+ FileBuffer);//导出函数地址表
DWORD* pExportFuncNamesTable = (DWORD*)(RVA2FOA(pExportDirectory->AddressOfNames )+ FileBuffer);//导出函数名称表
printf("\nOridinal\t RVA\t FOA\tFunctionName\n");
for (DWORD i = 0; i < pExportDirectory->NumberOfFunctions; i++) {
if (pExportFuncAddressTable[i] == 0)//地址为零则跳过
continue;
BYTE* FuncName = NULL;
//由于导出函数序号表仅保存有名函数序号,所以有序号必定有名称,否则无名称
//函数序号=函数地址表下标+Base
printf("%08x\t%08x\t%08x\t",i+pExportDirectory->Base, pExportFuncAddressTable[i],RVA2FOA(pExportFuncAddressTable[i]));
//是否存在函数名要单独判断 存储序号=函数序号-Base,故传递i即可
if (FuncName = GetFuncNameByOridinals(i))
printf("%s\n", FuncName);
else
printf("NONAME\n");
}
printf("\n==========导出表结束==========\n");
}
运行结果
查看二进制文件
导出函数地址表 从7CC8开始共0xE个表项
导出函数序号表 从7D0C开始共2个表项 每个表项2字节
存储序号分别是0xe(f func1) 0x0(1 plus)
导出函数名称表 从7D04开始共2个表项 每个表项四字节 指向字符串指针
重定位表
重定位
重定位的概念: 进程拥有独立的4GB虚拟空间,.exe最先被加载,其次加载.dll 显然exe可以占用默认基址400000起始的空间,但是dll默认基址10000000会有冲突
如果能按照预定ImageBase加载则不需要重定位表,所以很多exe程序没有重定位表但是dll有
部分编译生成的地址=ImageBase+RVA (VA绝对地址)
假设全局变量x的RVA=62153 基址400000
那么mov eax,[x] =A1 53 21 46 00
即一些指令中涉及到地址的硬编码是固定写好的(绝对地址)
如果dll模块没有加载到默认的基址处,那么这些使用绝对地址的指令就需要修正
重定位表则记录了需要修正的指令地址
重定位表解析
重定位表定义
数据目录表中第6个表项指向了重定位表
typedef struct _IMAGE_BASE_RELOCATION {
DWORD VirtualAddress; // 重定位数据页面起始地址
DWORD SizeOfBlock; // 重定位块的长度
//WORD TypeOffset[1]; // 重定位项数组
//该数组每个元素占2字节,加上VirtualAddress后才是真实地址
} IMAGE_BASE_RELOCATION;
//最后一个块的值全为0
typedef IMAGE_BASE_RELOCATION*,PIMAGE_BASE_RELOCATION;
重定位表是一块一块存储的,每块的大小不一定相等,通过重定位表起始地址+SizeOfBlock可以查找下一块数据
重定位表的每个块会存储每一页(1000h)需要修改的表项 VirtualAddress即是页面起始地址
所以真正需要修复的地址=VirtualAddress+表项地址
假设VirtualAddress=8000 表项存储 12 34 56 78
那么需要修改的地址为8012 8034 8056 8078 (不考虑下面的高四位标识)
每个重定位项占2字节 其中高四位用于表示这个地址是否需要修改,低12位用于存储偏移值
如果高四位=0011那么需要修改
注意: 由于内存对齐的需要,假设表项有5个共10字节,那么实际表项会多一个空项用于内存对齐
结束时重定位表结构全为0
查看重定位表
打印重定位表
//通过RVA判断所属区段名
PCHAR GetSectionNameByRva(DWORD RVA) {
for (DWORD i = 0; i < numberOfSections; i++) {
if (RVA >= pSectionHeader[i].VirtualAddress && RVA < pSectionHeader[i].VirtualAddress + AlignSize(pSectionHeader[i].Misc.VirtualSize, 0x1000))//成功找到所属节区
return (PCHAR)pSectionHeader[i].Name;
}
}
//打印重定位表的某个块
void PrintRelocationBlock(PIMAGE_BASE_RELOCATION pRelocationBlock) {
PWORD pBlock = (PWORD)((DWORD)pRelocationBlock + 8);//注意每个表项占2字节 但是高4位用来判断是否需要修改
DWORD PageOffset = pRelocationBlock->VirtualAddress;//每个块的虚拟地址即为页面起始地址
printf("序号\t属性\t RVA\t FOA\t指向RVA\n");
for (DWORD i = 0; i < (pRelocationBlock->SizeOfBlock - 8) / 2; i++) {
//每块高四位用作属性判断,低12位才是页内偏移值 还要注意与运算优先级低于+ 不用括号会导致出错
//指向的RVA即需要矫正的地址
printf("%04x\t%4x\t%08x\t%08x\t%08x\n", i, pBlock[i] >> 12, (pBlock[i] & 0x0fff) + PageOffset, RVA2FOA((pBlock[i] & 0x0fff) + PageOffset), *(DWORD*)(FileBuffer + RVA2FOA((pBlock[i] & 0x0fff) + PageOffset)) & 0x00ffffff);
}
}
//打印重定位表
void PrintRelocationTable() {
PIMAGE_BASE_RELOCATION pRelocationTable = pBaseRelocation;
printf("\n==========重定位表==========\n");
printf("序号\t 区段\t RVA\t FOA\t项目数\n");
//表块全为0时结束
DWORD count = 0;
while (pRelocationTable->VirtualAddress || pRelocationTable->SizeOfBlock) {
//项目数=(sizeofBlock-8)/2
printf("%4d\t%8s\t%08x\t%08x\t%08x\n", count++, GetSectionNameByRva(pRelocationTable->VirtualAddress), pRelocationTable->VirtualAddress, RVA2FOA(pRelocationTable->VirtualAddress), (pRelocationTable->SizeOfBlock - 8) / 2);
pRelocationTable = (PIMAGE_BASE_RELOCATION)((DWORD)pRelocationTable + pRelocationTable->SizeOfBlock);//注意这里应该将指针值强转后+块大小指向下一个块
}
pRelocationTable = pBaseRelocation;
count = 0;
while (pRelocationTable->VirtualAddress || pRelocationTable->SizeOfBlock) {
printf("\n==========Block%d==========\n", count++);
PrintRelocationBlock(pRelocationTable);//打印第i个块
pRelocationTable = (PIMAGE_BASE_RELOCATION)((DWORD)pRelocationTable + pRelocationTable->SizeOfBlock);
}
printf("\n==========重定位表结束==========\n");
}
运行结果
导入表
数据目录表第2个表项是导入表,紧跟在导出表后
PE文件可能会有多个导入表以结构体数组形式存在,结束标识为全0
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics;
DWORD OriginalFirstThunk; //RVA 指向INT 导入名称表 存储导入函数名称 IMAGE_THUNK_DATA结构数组
} DUMMYUNIONNAME;
DWORD TimeDateStamp; //时间戳 如果该值为0则说明dll未被绑定 如果为-1则说明该dll被绑定
DWORD ForwarderChain;
DWORD Name; //RVA 指向dll名 以0结尾
DWORD FirstThunk; //RVA 指向IAT 导入地址表 存储导入函数地址 IMAGE_THUNK_DATA结构数组
} IMAGE_IMPORT_DESCRIPTOR;
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;
重要成员
OriginalFirstThunk 指向INT表
FirstThunk 指向IAT表
INT和IAT
INT: 导入名称表 存储导入函数名称
IAT: 导入地址表 存储导入函数地址
这两张表的定义如下
typedef struct _IMAGE_THUNK_DATA32 {
union {
DWORD ForwarderString; //PBYTE
DWORD Function; //PDWORD
DWORD Ordinal; //按序号导入的函数序号
DWORD AddressOfData; //PIMAGE_IMPORT_BY_NAME 指向导入函数名称结构
} u1;
} IMAGE_THUNK_DATA32;
typedef IMAGE_THUNK_DATA32 * PIMAGE_THUNK_DATA32;
IAT表中,该结构存储的是导入函数地址RVA
INT表中,该结构可能存储导入函数序号或者是导入函数名称结构地址RVA
- 当最高位为1时表示按序号导入,此时低31位作为序号值
- 当最高位为0时表示按名称导入,此时低31位作为导入函数名称结构地址RVA
绑定导入
IAT表有两种情况
-
在PE文件加载到内存前,两张表存储的内容一致,加载后修复IAT
PE加载前 INT和IAT都指向IMAGE_IMPORT_BY_NAME 即导入函数名称结构
加载到内存后 IAT表被修复 存储导入函数地址
-
PE文件加载前IAT表已经修复过
此时IAT已经保存了导入函数地址 地址=RVA+ImageBase,这就是绑定导入
导入表的TimeDateStamp为-1时表示已经进行绑定导入,如果为0表示没有绑定导入
优点: 启动程序快
缺点: 如果没有加载到正确基址仍然需要修复IAT表
打印导入表
以DllTest.dll为例,用PEStudy打开
上方区域有三张导入表,分别列出了dllname以及它们INT和IAT的位置
下方区域第一列是IAT表项(即导入函数地址) 第二列是INT表项的FOA(ThunkData) 第三列是INT表项指向的值(*ThunkData)
![44 pestudy导入表解析](44 pestudy导入表解析.png)
打印导入表的代码
//打印INT表
void PrintINT(PIMAGE_IMPORT_DESCRIPTOR pImportTable) {
printf("\n==========INT==========\n");
printf("ThunkRVA\tThunkFOA\tThunkVal\tFuncName\n\n");
PIMAGE_THUNK_DATA32 pThunkData = (PIMAGE_THUNK_DATA32)(RVA2FOA(pImportTable->OriginalFirstThunk) + FileBuffer);
while (pThunkData->u1.Ordinal) {
//最高位为1时表示按序号导入,低31位作为序号值
printf("%08x\t%08x\t%08x\t", FOA2RVA((DWORD)pThunkData - (DWORD)FileBuffer), (DWORD)pThunkData - (DWORD)FileBuffer, pThunkData->u1);
if (pThunkData->u1.Ordinal & 0x80000000) {
printf("%08x\n", pThunkData->u1.Ordinal & 0x7FFFFFFF);
}
//最高位为0时表示按函数名称导入,值作为指向IMAGE_IMPORT_BY_NAME结构体地址的RVA
else
{
PIMAGE_IMPORT_BY_NAME pImportName = (PIMAGE_IMPORT_BY_NAME)(RVA2FOA(pThunkData->u1.AddressOfData) + FileBuffer);
printf("%s\n", pImportName->Name);
}
pThunkData++;
}
}
//打印IAT表
void PrintIAT(PIMAGE_IMPORT_DESCRIPTOR pImportTable) {
printf("\n==========IAT==========\n");
PDWORD pThunkData = (PDWORD)(RVA2FOA(pImportTable->FirstThunk) + FileBuffer);
printf(" FuncRVA\t FuncFOA\tFuncAddr\n");
while (*pThunkData) {
printf("%08x\t%08x\t%08x\n", *pThunkData,RVA2FOA(*pThunkData), *pThunkData+imageBase);
pThunkData++;
}
}
//打印导入表
void PrintImportTable() {
PIMAGE_IMPORT_DESCRIPTOR pImportTable = pImportDescriptor;
printf("\n**********导入表**********\n");
printf("DllName\t\t\t INT RVA\tTimeStamp\tIAT RVA\n");
while (pImportTable->OriginalFirstThunk) {
printf("%-24s%08x\t%08x\t%08x\n", (RVA2FOA(pImportTable->Name) + FileBuffer),pImportTable->OriginalFirstThunk,pImportTable->TimeDateStamp,pImportTable->FirstThunk);
pImportTable++;
}
pImportTable = pImportDescriptor;
while (pImportTable->OriginalFirstThunk) {
printf("\n==========DllName:%s==========\n", RVA2FOA(pImportTable->Name) + FileBuffer);
PrintINT(pImportTable);
PrintIAT(pImportTable);
pImportTable++;
}
printf("\n**********导入表**********\n");
}
运行结果
参考资料
- 滴水逆向三期视频(配套纸质教材)
- PE文件结构从初识到简单shellcode注入
- PE结构详解
- PE文件结构详解精华(从头看下去就能大概了解PE文件结构了)
- C/C++全栈软件安全课(调试、反调试、游戏反外挂、软件逆向)持续更新中~~~~
- [原创]PE数据目录表解析
完整代码
#include<Windows.h>
#include<iostream>
#include <TlHelp32.h>
#include <psapi.h>
using namespace std;
class PEFile {
private:
HANDLE hFile; //文件句柄
HANDLE hProcess; //进程句柄
DWORD ProcessBaseAddr; //进程基址
BYTE* FileBuffer; //文件缓冲指针
BYTE* imageBuffer; //映像缓冲指针
DWORD fileBufferSize; //文件缓冲大小
DWORD imageBufferSize; //映像缓冲大小
//FileBuffer的各个指针
PIMAGE_DOS_HEADER pDosHeader; //Dos头
PIMAGE_NT_HEADERS pNtHeader; //NT头
PIMAGE_FILE_HEADER pFileHeader; //标准PE头
PIMAGE_OPTIONAL_HEADER pOptionalHeader; //扩展PE头
PIMAGE_DATA_DIRECTORY pDataDirectory; //数据目录表
PIMAGE_EXPORT_DIRECTORY pExportDirectory; //导出表
PIMAGE_BASE_RELOCATION pBaseRelocation; //重定位表
PIMAGE_IMPORT_DESCRIPTOR pImportDescriptor; //导入表
PIMAGE_SECTION_HEADER pSectionHeader; //节表
//dos头关键成员
WORD dosSignature; //dos签名
LONG NToffset; //nt头偏移
//NT头关键成员
DWORD peSignature;
//标准PE头关键成员
WORD Machine; //cpu型号
DWORD numberOfSections; //节区数
WORD sizeOfOptionalHeader; //可选pe头大小
//可选PE头关键成员
DWORD addressOfEntryPoint; //程序入口点EP
DWORD imageBase; //内存镜像基址
DWORD sectionAlignment; //内存对齐大小
DWORD fileAlignment; //文件对齐大小
DWORD sizeOfImage; //内存映像大小
DWORD sizeOfHeaders; //各种头的大小
//初始化各个表头指针
void InitHeaders() {
pDosHeader = (IMAGE_DOS_HEADER*)FileBuffer;//DOS头
pNtHeader = (IMAGE_NT_HEADERS*)(FileBuffer + pDosHeader->e_lfanew);//NT头
pFileHeader = (IMAGE_FILE_HEADER*)((DWORD)pNtHeader + sizeof(DWORD));//标准PE头
pOptionalHeader = (IMAGE_OPTIONAL_HEADER*)((DWORD)pFileHeader + sizeof(IMAGE_FILE_HEADER));//可选PE头
pSectionHeader = (IMAGE_SECTION_HEADER*)((DWORD)pOptionalHeader + pFileHeader->SizeOfOptionalHeader);//节表
pDataDirectory = (PIMAGE_DATA_DIRECTORY)(pOptionalHeader->DataDirectory);//数据目录表
pBaseRelocation = (PIMAGE_BASE_RELOCATION)(FileBuffer + RVA2FOA(pDataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress));//重定位表
pImportDescriptor = (PIMAGE_IMPORT_DESCRIPTOR)(FileBuffer + RVA2FOA(pDataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress));//导入表
if (pDataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress != 0)//如果存在导出表则获取导出表地址,否则置空
pExportDirectory = (PIMAGE_EXPORT_DIRECTORY)(FileBuffer + RVA2FOA(pDataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress));//导出表
else pExportDirectory = NULL;
}
//初始化FileBuffer关键成员
void InitKeyMembers() {
//dos头
dosSignature = pDosHeader->e_magic;
NToffset = pDosHeader->e_lfanew;
//NT头
peSignature = pNtHeader->Signature;
//标准PE头 20字节
Machine = pFileHeader->Machine;
numberOfSections = pFileHeader->NumberOfSections;
sizeOfOptionalHeader = pFileHeader->SizeOfOptionalHeader;
//可选头,根据32/64位有不同大小
addressOfEntryPoint = pOptionalHeader->AddressOfEntryPoint;
imageBase = pOptionalHeader->ImageBase;
sectionAlignment = pOptionalHeader->SectionAlignment;
fileAlignment = pOptionalHeader->FileAlignment;
sizeOfImage = pOptionalHeader->SizeOfImage;
sizeOfHeaders = pOptionalHeader->SizeOfHeaders;
}
//打印DOS头
void showDosHeader() {
printf("\n----------DosHeader----------\n");
printf("DosSignature: %x\n", dosSignature);
printf("NtHeaderOffset: %x\n", NToffset);
printf("\n----------DosHeader----------\n");
}
//打印标准Pe头
void showFileHeader() {
printf("\n----------FileHeader----------\n");
printf("Machine: %x\n", Machine);
printf("NumberOfSections: %x\n", numberOfSections);
printf("SizeOfOptionalHeader: %x\n", sizeOfOptionalHeader);
printf("\n----------FileHeader----------\n");
}
//打印可选PE头
void showOptionalHeader() {
printf("\n----------OptionalHeader----------\n");
printf("EntryPoint: %x\n", addressOfEntryPoint);
printf("ImageBase: %x\n", imageBase);
printf("SectionAlignment: %x\n", sectionAlignment);
printf("FileAlignment: %x\n", fileAlignment);
printf("SizeOfImage; %x\n", sizeOfImage);
printf("SizeOfHeaders: %x\n", sizeOfHeaders);
printf("\n----------OptionalHeader----------\n");
}
//打印NT头
void showNtHeader() {
printf("\n-----------NtHeader----------\n");
printf("PeSignature: %x\n", peSignature);
showFileHeader();
showOptionalHeader();
printf("\n-----------NtHeader----------\n");
}
//打印节表
void showSectionHeaders() {
printf("\n----------SectionHeaders----------\n");
for (DWORD i = 0; i < numberOfSections; i++) {
//逐个读取节表并打印
printf("\n----------Section%d----------\n", i);
printf("Name: %8s\n", pSectionHeader[i].Name);
printf("VirtualSize: %x\n", pSectionHeader[i].Misc.VirtualSize);
printf("VirtualAddress: %x\n", pSectionHeader[i].VirtualAddress);
printf("SizeOfRawData: %x\n", pSectionHeader[i].SizeOfRawData);
printf("PointerToRawData: %x\n", pSectionHeader[i].PointerToRawData);
printf("Characteristics: %x\n", pSectionHeader[i].Characteristics);
printf("\n----------Section%d----------\n", i);
}
printf("\n----------SectionHeaders----------\n");
}
//设置FileBuffer
void SetFileBuffer(BYTE* NewFileBuffer) {
if (FileBuffer)
delete[] FileBuffer; //删除原始空间
FileBuffer = NewFileBuffer;//指向新的空间
Init(); //初始化
}
//设置ImageBuffer
void SetImageBuffer(BYTE* NewImageBuffer) {
if (imageBuffer)
delete[] imageBuffer;
imageBuffer = NewImageBuffer;
}
//将FileBuffer拉伸成为ImageBuffer
void FileBufferToImageBuffer() {
//1. 申请空间用于存储Image
imageBuffer = new BYTE[sizeOfImage];
imageBufferSize = sizeOfImage;
if (!imageBuffer)
{
printf("申请空间失败!\n");
system("pause");
return;
}
memset(imageBuffer, 0, sizeOfImage); //初始化内存空间,全部清零
memcpy(imageBuffer, FileBuffer, sizeOfHeaders); //直接复制各个表头
//2. 拉伸FileBuffer并写入ImageBuffer
for (DWORD i = 0; i < numberOfSections; i++) {
memcpy(imageBuffer + pSectionHeader[i].VirtualAddress, FileBuffer + pSectionHeader[i].PointerToRawData, pSectionHeader[i].SizeOfRawData);
//起始地址是imageBase+节区起始地址RVA SizeOfData是节区在文件中保存的数据
//不使用VirtualSize的原因是例如.textbss段 SizeOfData=0 VirtualSize=10000
//显然在文件中没有数据需要写入内存,只是在内存中占用那么多大小的空间而已
}
}
//将ImageBuffer压缩为FileBuffer
void ImageBufferToFileBuffer() {
//1. 申请空间用于存储ImageBuffer压缩后的FileBuffer
DWORD NewFileBufferSize = pSectionHeader[numberOfSections - 1].PointerToRawData + pSectionHeader[numberOfSections - 1].SizeOfRawData;
BYTE* NewFileBuffer = new BYTE[NewFileBufferSize];//最后一个节区的文件起始地址+文件大小即为PE文件大小
memset(NewFileBuffer, 0, NewFileBufferSize);
//2. 将ImageBuffer的内容压缩并写入FileBuffer
for (DWORD i = 0; i < numberOfSections; i++) //复制节区
{
memcpy(NewFileBuffer + pSectionHeader[i].PointerToRawData, imageBuffer + pSectionHeader[i].VirtualAddress, pSectionHeader[i].SizeOfRawData);
//节区文件偏移起始地址 节区内存偏移起始地址 节区文件大小
//注意这里第三个参数不要使用VirtualSize 否则可能会导致缓冲区溢出
//(例如: .textbss段在文件中占用空间为0 但是内存中的大小为0x10000 所以这段没有必要写入文件中)
}
memcpy(NewFileBuffer, imageBuffer, sizeOfHeaders); //复制各个表头
SetFileBuffer(NewFileBuffer); //重新设置FileBuffer
}
//获取进程基址
DWORD GetProcessBaseAddress(HANDLE hProcess) {
HMODULE hMods[1024];
DWORD cbNeeded;
DWORD baseAddress = 0;
if (EnumProcessModules(hProcess, hMods, sizeof(hMods), &cbNeeded)) {
for (unsigned int i = 0; i < (cbNeeded / sizeof(HMODULE)); i++) {
TCHAR szModName[MAX_PATH];
if (GetModuleFileNameEx(hProcess, hMods[i], szModName,
sizeof(szModName) / sizeof(TCHAR))) {
MODULEINFO moduleInfo;
if (GetModuleInformation(hProcess, hMods[i], &moduleInfo, sizeof(moduleInfo))) {
baseAddress = (uintptr_t)moduleInfo.lpBaseOfDll;
break; // We found the first module's base address
}
}
}
}
return baseAddress;
}
public:
//创建进程并获取进程基址
BOOL CreateProcessWrapper(LPCTSTR applicationName, LPTSTR commandLine) {
STARTUPINFO startupInfo;
PROCESS_INFORMATION processInfo;
ZeroMemory(&startupInfo, sizeof(startupInfo));
startupInfo.cb = sizeof(startupInfo);
BOOL success = CreateProcess(
applicationName,
commandLine,
NULL, NULL, FALSE, 0, NULL, NULL,
&startupInfo,
&processInfo
);
if (success) {
hProcess = processInfo.hProcess;
ProcessBaseAddr = GetProcessBaseAddress(hProcess);
}
return success;
}
//将FileBuffer写入文件
BOOL FileBufferWriteToFile(const WCHAR* FileName) {
//创建文件 注意这里第三个参数不能用GENERIC_ALL 推测是由于可执行权限导致出错 仅读写没有问题
//CREATE_ALWAYS 无论文件是否存在都会写入
HANDLE hFile = CreateFile(FileName, GENERIC_READ | GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile == INVALID_HANDLE_VALUE)
return FALSE;
return WriteFile(hFile, FileBuffer, fileBufferSize, NULL, NULL); // 写入文件
}
//将ImageBuffer写入文件
BOOL ImageBufferWriteToFile(const WCHAR* FileName) {
HANDLE hFile = CreateFile(FileName, GENERIC_READ | GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile == INVALID_HANDLE_VALUE)
return FALSE;
return WriteFile(hFile, imageBuffer, sizeOfImage, NULL, NULL);
}
//扩大filebuffer大小 ExSize为文件对齐后的额外空间大小
void ExpandFileBuffer(DWORD ExSize) {
BYTE* NewBuffer = new BYTE[fileBufferSize + ExSize];
memset(NewBuffer + fileBufferSize, 0, ExSize);//额外空间清零
memcpy(NewBuffer, FileBuffer, fileBufferSize);//复制原始数据
fileBufferSize += ExSize;//调整大小
SetFileBuffer(NewBuffer);
}
//扩大imgaebuffer大小
void ExpandImageBuffer(DWORD ExSize) {
BYTE* NewBuffer = new BYTE[imageBufferSize + ExSize];
memset(NewBuffer + imageBufferSize, 0, ExSize);
memcpy(NewBuffer, imageBuffer, imageBufferSize);
imageBufferSize += ExSize;
SetImageBuffer(NewBuffer);
}
PEFile(LPCWCHAR FileName) {
hFile = CreateFile(FileName, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); //打开文件
if (!hFile) {
printf("OpenFileFailure!\n");
exit(0);
}
fileBufferSize = GetFileSize(hFile, NULL); //获取文件大小
FileBuffer = new BYTE[fileBufferSize]; //分配内存空间用于存储文件
if (!FileBuffer) {
printf("AllocFileBufferMemoryFailure!\n");
exit(0);
}
if (!ReadFile(hFile, FileBuffer, fileBufferSize, NULL, NULL)) //读取文件并存储到内存中
{
delete[] FileBuffer;
printf("ReadFileFailure!\n");
exit(0);
}
CloseHandle(hFile);//读取完后关闭文件
InitHeaders();
InitKeyMembers();
FileBufferToImageBuffer();//创建ImageBuffer
hProcess = NULL;
ProcessBaseAddr = 0;
}
//初始化表头指针和关键变量
void Init() {
InitHeaders();
InitKeyMembers();
//InitImageHeaders();
}
//打印Pe文件信息
void showPeFile() {
showDosHeader();
showNtHeader();
showSectionHeaders();
PrintDirectory();
PrintExportDirectory();
PrintRelocationTable();
PrintImportTable();
}
//打印数据目录表
void PrintDirectory() {
PIMAGE_DATA_DIRECTORY pDirectory = pOptionalHeader->DataDirectory;
printf("\n**********数据目录表**********\n");
for (DWORD i = 0; i < pOptionalHeader->NumberOfRvaAndSizes; i++) {
switch (i) {
case IMAGE_DIRECTORY_ENTRY_EXPORT:
printf("\n==========导出表==========\n");
break;
case IMAGE_DIRECTORY_ENTRY_IMPORT:
printf("\n==========导入表==========\n");
break;
case IMAGE_DIRECTORY_ENTRY_RESOURCE:
printf("\n==========资源目录==========\n");
break;
case IMAGE_DIRECTORY_ENTRY_EXCEPTION:
printf("\n==========异常目录==========\n");
break;
case IMAGE_DIRECTORY_ENTRY_SECURITY:
printf("\n==========安全目录=========\n");
break;
case IMAGE_DIRECTORY_ENTRY_BASERELOC:
printf("\n==========重定位基本表==========\n");
break;
case IMAGE_DIRECTORY_ENTRY_DEBUG:
printf("\n==========调试目录==========\n");
break;
case IMAGE_DIRECTORY_ENTRY_ARCHITECTURE:
printf("\n==========描述字串==========\n");
break;
case IMAGE_DIRECTORY_ENTRY_GLOBALPTR:
printf("\n==========机器值==========\n");
break;
case IMAGE_DIRECTORY_ENTRY_TLS:
printf("\n==========TLS目录==========\n");
break;
case IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG:
printf("\n==========载入配置目录==========\n");
break;
case IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT:
printf("\n==========绑定输入表==========\n");
break;
case IMAGE_DIRECTORY_ENTRY_IAT:
printf("\n==========导入地址表==========\n");
break;
case IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT:
printf("\n==========延迟导入表==========\n");
break;
case IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR:
printf("\n==========COM信息==========\n");
break;
case 15:
printf("\n==========保留表==========\n");
break;
}
printf("VirtualAddress=%x\nSize=%x\nFOA=%x\n", pDirectory[i].VirtualAddress, pDirectory[i].Size, RVA2FOA(pDirectory[i].VirtualAddress));
}
printf("\n**********数据目录表打印完毕**********\n\n");
}
//通过函数名获取导出函数地址
DWORD GetFuncAddrByName(const char* FuncName) {
WORD* pExportFuncOridinalsTable = (WORD*)(RVA2FOA(pExportDirectory->AddressOfNameOrdinals) + FileBuffer);//导出函数序号表
DWORD* pExportFuncAddressTable = (DWORD*)(RVA2FOA(pExportDirectory->AddressOfFunctions) + FileBuffer);//导出函数地址表
DWORD* pExportFuncNamesTable = (DWORD*)(RVA2FOA(pExportDirectory->AddressOfNames) + FileBuffer);//导出函数名称表
DWORD pos = -1, OridinalNum = 0;
//1. 通过导出函数名称表得到序号表下标
for (DWORD i = 0; i < pExportDirectory->NumberOfNames; i++) {
//注意导出函数名称表表项是字符串指针 该指针值为RVA
if (strcmp(FuncName, (char*)(RVA2FOA(pExportFuncNamesTable[i]) + FileBuffer)) == 0)
{
pos = i;
break;
}
}
if (pos == -1)//查找失败
return 0;
//2. 通过序号表得到序号
OridinalNum = pExportFuncOridinalsTable[pos];
//3. 得到函数地址
return pExportFuncAddressTable[OridinalNum];
}
//通过函数序号获取导出函数地址
DWORD GetFuncAddrByOridinals(WORD OridinalNum) {
DWORD* pExportFuncAddressTable = (DWORD*)(RVA2FOA(pExportDirectory->AddressOfFunctions) + FileBuffer);//导出函数地址表
return pExportFuncAddressTable[OridinalNum - pExportDirectory->Base];//减去Base值作为索引直接查找函数地址
}
//根据导出函数序号返回导出函数名
PCHAR GetFuncNameByOridinals(WORD OridinalNum) {
WORD* pExportFuncOridinalsTable = (WORD*)(RVA2FOA(pExportDirectory->AddressOfNameOrdinals) + FileBuffer);//导出函数序号表
DWORD* pExportFuncNamesTable = (DWORD*)(RVA2FOA(pExportDirectory->AddressOfNames) + FileBuffer);//导出函数名称表
for (DWORD i = 0; i < pExportDirectory->NumberOfNames; i++)
{
if (pExportFuncOridinalsTable[i] == OridinalNum)//实际存储的序号=函数序号-base
return (PCHAR)(RVA2FOA(pExportFuncNamesTable[i]) + FileBuffer);
}
return NULL;//没有找到说明是无名函数
}
//打印导出表详细信息
void PrintExportDirectory() {
//不存在导出表
if (!pExportDirectory)
{
printf("**********不存在导出表**********\n");
return;
}
printf("\n==========导出表==========\n");
printf("Name: %x (%s)\n", pExportDirectory->Name, (char*)(FileBuffer + RVA2FOA(pExportDirectory->Name)));
printf("Base: %x\n", pExportDirectory->Base);
printf("NumberOfFunctions: \t%x\n", pExportDirectory->NumberOfFunctions);
printf("NumberOfNames: \t\t%x\n", pExportDirectory->NumberOfNames);
printf("AddressOfFunctions: \tRVA=%x\tFOA=%x\n", pExportDirectory->AddressOfFunctions, RVA2FOA(pExportDirectory->AddressOfFunctions));
printf("AddressOfNames: \tRVA=%x\tFOA=%x\n", pExportDirectory->AddressOfNames, RVA2FOA(pExportDirectory->AddressOfNames));
printf("AddressOfNameOrdinals: \tRVA=%x\tFOA=%x\n", pExportDirectory->AddressOfNameOrdinals, RVA2FOA(pExportDirectory->AddressOfNameOrdinals));
WORD* pExportFuncOridinalsTable = (WORD*)(RVA2FOA(pExportDirectory->AddressOfNameOrdinals) + FileBuffer);//导出函数序号表
DWORD* pExportFuncAddressTable = (DWORD*)(RVA2FOA(pExportDirectory->AddressOfFunctions) + FileBuffer);//导出函数地址表
DWORD* pExportFuncNamesTable = (DWORD*)(RVA2FOA(pExportDirectory->AddressOfNames) + FileBuffer);//导出函数名称表
printf("\nOridinal\t RVA\t FOA\tFunctionName\n");
for (DWORD i = 0; i < pExportDirectory->NumberOfFunctions; i++) {
if (pExportFuncAddressTable[i] == 0)//地址为零则跳过
continue;
PCHAR FuncName = NULL;
//由于导出函数序号表仅保存有名函数序号,所以有序号必定有名称,否则无名称
//函数序号=函数地址表下标+Base
printf("%08x\t%08x\t%08x\t", i + pExportDirectory->Base, pExportFuncAddressTable[i], RVA2FOA(pExportFuncAddressTable[i]));
//是否存在函数名要单独判断 存储序号=函数序号-Base,故传递i即可
if (FuncName = GetFuncNameByOridinals(i))
printf("%s\n", FuncName);
else
printf("NONAME\n");
}
printf("\n==========导出表结束==========\n");
}
//打印重定位表的某个块
void PrintRelocationBlock(PIMAGE_BASE_RELOCATION pRelocationBlock) {
PWORD pBlock = (PWORD)((DWORD)pRelocationBlock + 8);//注意每个表项占2字节 但是高4位用来判断是否需要修改
DWORD PageOffset = pRelocationBlock->VirtualAddress;//每个块的虚拟地址即为页面起始地址
printf("序号\t属性\t RVA\t FOA\t指向RVA\n");
for (DWORD i = 0; i < (pRelocationBlock->SizeOfBlock - 8) / 2; i++) {
//每块高四位用作属性判断,低12位才是页内偏移值 还要注意与运算优先级低于+ 不用括号会导致出错
//指向的RVA即需要矫正的地址
printf("%04x\t%4x\t%08x\t%08x\t%08x\n", i, pBlock[i] >> 12, (pBlock[i] & 0x0fff) + PageOffset, RVA2FOA((pBlock[i] & 0x0fff) + PageOffset), *(DWORD*)(FileBuffer + RVA2FOA((pBlock[i] & 0x0fff) + PageOffset)) & 0x00ffffff);
}
}
//打印重定位表
void PrintRelocationTable() {
PIMAGE_BASE_RELOCATION pRelocationTable = pBaseRelocation;
printf("\n==========重定位表==========\n");
printf("序号\t 区段\t RVA\t FOA\t项目数\n");
//表块全为0时结束
DWORD count = 0;
while (pRelocationTable->VirtualAddress || pRelocationTable->SizeOfBlock) {
//项目数=(sizeofBlock-8)/2
printf("%4d\t%8s\t%08x\t%08x\t%08x\n", count++, GetSectionNameByRva(pRelocationTable->VirtualAddress), pRelocationTable->VirtualAddress, RVA2FOA(pRelocationTable->VirtualAddress), (pRelocationTable->SizeOfBlock - 8) / 2);
pRelocationTable = (PIMAGE_BASE_RELOCATION)((DWORD)pRelocationTable + pRelocationTable->SizeOfBlock);//注意这里应该将指针值强转后+块大小指向下一个块
}
pRelocationTable = pBaseRelocation;
count = 0;
while (pRelocationTable->VirtualAddress || pRelocationTable->SizeOfBlock) {
printf("\n==========Block%d==========\n", count++);
PrintRelocationBlock(pRelocationTable);//打印第i个块
pRelocationTable = (PIMAGE_BASE_RELOCATION)((DWORD)pRelocationTable + pRelocationTable->SizeOfBlock);
}
printf("\n==========重定位表结束==========\n");
}
//打印INT表
void PrintINT(PIMAGE_IMPORT_DESCRIPTOR pImportTable) {
printf("\n==========INT==========\n");
printf("ThunkRVA\tThunkFOA\tThunkVal\tFuncName\n\n");
PIMAGE_THUNK_DATA32 pThunkData = (PIMAGE_THUNK_DATA32)(RVA2FOA(pImportTable->OriginalFirstThunk) + FileBuffer);
while (pThunkData->u1.Ordinal) {
//最高位为1时表示按序号导入,低31位作为序号值
printf("%08x\t%08x\t%08x\t", FOA2RVA((DWORD)pThunkData - (DWORD)FileBuffer), (DWORD)pThunkData - (DWORD)FileBuffer, pThunkData->u1.Ordinal);
if (pThunkData->u1.Ordinal & 0x80000000) {
printf("%08x\n", pThunkData->u1.Ordinal & 0x7FFFFFFF);
}
//最高位为0时表示按函数名称导入,值作为指向IMAGE_IMPORT_BY_NAME结构体地址的RVA
else
{
PIMAGE_IMPORT_BY_NAME pImportName = (PIMAGE_IMPORT_BY_NAME)(RVA2FOA(pThunkData->u1.AddressOfData) + FileBuffer);
printf("%s\n", pImportName->Name);
}
pThunkData++;
}
}
//打印IAT表
void PrintIAT(PIMAGE_IMPORT_DESCRIPTOR pImportTable) {
printf("\n==========IAT==========\n");
PDWORD pThunkData = (PDWORD)(RVA2FOA(pImportTable->FirstThunk) + FileBuffer);
printf(" FuncRVA\t FuncFOA\tFuncAddr\n");
while (*pThunkData) {
printf("%08x\t%08x\t%08x\n", *pThunkData,RVA2FOA(*pThunkData), *pThunkData+imageBase);
pThunkData++;
}
}
//打印导入表
void PrintImportTable() {
PIMAGE_IMPORT_DESCRIPTOR pImportTable = pImportDescriptor;
printf("\n**********导入表**********\n");
printf("DllName\t\t\t INT RVA\tTimeStamp\tIAT RVA\n");
while (pImportTable->OriginalFirstThunk) {
printf("%-24s%08x\t%08x\t%08x\n", (RVA2FOA(pImportTable->Name) + FileBuffer),pImportTable->OriginalFirstThunk,pImportTable->TimeDateStamp,pImportTable->FirstThunk);
pImportTable++;
}
pImportTable = pImportDescriptor;
while (pImportTable->OriginalFirstThunk) {
printf("\n==========DllName:%s==========\n", RVA2FOA(pImportTable->Name) + FileBuffer);
PrintINT(pImportTable);
PrintIAT(pImportTable);
pImportTable++;
}
printf("\n**********导入表**********\n");
}
//通过RVA判断所属区段名
PCHAR GetSectionNameByRva(DWORD RVA) {
for (DWORD i = 0; i < numberOfSections; i++) {
if (RVA >= pSectionHeader[i].VirtualAddress && RVA < pSectionHeader[i].VirtualAddress + AlignSize(pSectionHeader[i].Misc.VirtualSize, 0x1000))//成功找到所属节区
return (PCHAR)pSectionHeader[i].Name;
}
}
//RVA转FOA
DWORD RVA2FOA(DWORD RVA) {
DWORD FOA = 0;
//1. 判断RVA属于哪个节区 节区内存起始地址<=RVA<=节区内存起始地址+节区大小 内存大小需要对齐 注意右边界应该是开区间
//2. FOA=RVA-VirtualAddress+PointerToRawData
for (DWORD i = 0; i < numberOfSections; i++) {
if (RVA >= pSectionHeader[i].VirtualAddress && RVA < pSectionHeader[i].VirtualAddress + AlignSize(pSectionHeader[i].Misc.VirtualSize, 0x1000))//成功找到所属节区
{
FOA = RVA - pSectionHeader[i].VirtualAddress + pSectionHeader[i].PointerToRawData;
break;
}
}
return FOA;
}
//FOA转RVA
DWORD FOA2RVA(DWORD FOA) {
DWORD RVA = 0;
//1. 判断FOA属于哪个节区 节区文件起始地址<=FOA<=节区文件起始地址+节区大小 文件大小默认是对齐值
//2. RVA=FOA-PointerToRawData+VirtualAddress
for (DWORD i = 0; i < numberOfSections; i++) {
if (FOA >= pSectionHeader[i].PointerToRawData && FOA < pSectionHeader[i].PointerToRawData + pSectionHeader[i].SizeOfRawData) {
RVA = FOA - pSectionHeader[i].PointerToRawData + pSectionHeader[i].VirtualAddress;
break;
}
}
return RVA;
}
//输入原始大小和对齐值返回对齐后的大小
DWORD AlignSize(DWORD OrigSize, DWORD AlignVal) {
//通过对对齐值取模判断是否对齐,如果对齐则返回原值,否则返回对齐后的值
return OrigSize % AlignVal ? (OrigSize / AlignVal + 1) * AlignVal : OrigSize;
}
//计算call/jmp指令的偏移值 目的地址-(当前指令地址+5)
DWORD OffsetOfCallAndJmp(DWORD DesAddr, DWORD SelfAddr) {
return DesAddr - (SelfAddr + 5);
}
//创建新的节区 返回新节区指针
PIMAGE_SECTION_HEADER CreateNewSection(const char* NewSectionName, DWORD NewSectionSize) {
//1. 检查节表空闲区是否足够保存新的节表 80字节
//空白空间起始地址=NT头偏移+NT头大小+所有节表大小
DWORD BlankMemAddr = (NToffset + sizeof(IMAGE_NT_HEADERS)) + numberOfSections * sizeof(IMAGE_SECTION_HEADER);
DWORD BlankMemSize = sizeOfHeaders - BlankMemAddr;//空白空间大小=SizeOfHeaders-各个表头大小-所有节表大小
if (BlankMemSize < sizeof(IMAGE_SECTION_HEADER) * 2)
return NULL;
//2. 申请新的空间
ExpandFileBuffer(NewSectionSize);
PIMAGE_SECTION_HEADER pNewSectionHeader = (PIMAGE_SECTION_HEADER)(FileBuffer + BlankMemAddr);//指向新增的节表
//3. 复制.text段的节表信息
for (DWORD i = 0; i < numberOfSections; i++) {
if (!strcmp((char*)pSectionHeader[i].Name, ".text"))
{
memcpy(pNewSectionHeader, (LPVOID)&pSectionHeader[i], sizeof(IMAGE_SECTION_HEADER));
break;
}
}
//4. 修正PE文件信息
//标准PE头
pFileHeader->NumberOfSections = ++numberOfSections; //NumberOfSections +1
//节区头
memcpy(pNewSectionHeader->Name, NewSectionName, strlen(NewSectionName));//name
pNewSectionHeader->Misc.VirtualSize = NewSectionSize; //virtualsize
//注意这里必须先修改VirtualAddress
//virtualaddress 各段间是紧邻着的 所以可以根据上个段的末尾来确定新段的起始地址 上个段的起始地址+上个段的大小对于0x1000向上取整即可
pNewSectionHeader->VirtualAddress = AlignSize(pSectionHeader[numberOfSections - 2].VirtualAddress + pSectionHeader[numberOfSections - 2].SizeOfRawData, 0x1000);
pNewSectionHeader->SizeOfRawData = NewSectionSize;//SizeOfRawData
//PointerToRawData 文件偏移=上个段的文件起始地址+段在文件中的大小
pNewSectionHeader->PointerToRawData = pSectionHeader[numberOfSections - 2].PointerToRawData + pSectionHeader[numberOfSections - 2].SizeOfRawData;
pNewSectionHeader->Characteristics |= 0x20000000; //Characteristics 可执行权限
//可选头
pOptionalHeader->SizeOfImage = sizeOfImage = sizeOfImage + AlignSize(NewSectionSize, 0x1000);//可选PE头 SizeOfImage 必须是内存对齐的整数倍 直接添加一页大小
return pNewSectionHeader;
}
//通过创建新节区的方式注入代码
BOOL InjectCodeByCreateNewSection() {
//1. 创建新的节区
PIMAGE_SECTION_HEADER pNewSectionHeader = CreateNewSection(".inject", 0x1000);
//修正可选头
DWORD OEP = addressOfEntryPoint; //保存OEP
pOptionalHeader->DllCharacteristics &= 0xFFFFFFBF;//取消ASLR随机基址 随机基址的值是0x40 所以和(0xFFFFFFFF-0x40)进行与运算即可
pOptionalHeader->AddressOfEntryPoint = addressOfEntryPoint = pNewSectionHeader->VirtualAddress;//修改EP 注意ep=rva 不用加基址
//2. 将代码写入新的节区
BYTE InjectCode[18] = { //偏移 指令
0x6a,0x00, //0 push 0
0x6a,0x00, //0 push 0
0x6a,0x00, //0 push 0
0x6a,0x00, //0 push 0
0xe8,0x00,0x00,0x00,0x00, //8 call MessageBox MessageBox=0x763C0E50 这个地址会随着系统启动而变化
0xe9,0x00,0x00,0x00,0x00 //13 jmp oep
};
DWORD MessageBoxAddr = 0x76260E50;
//矫正call和jmp地址
*(DWORD*)&InjectCode[9] = OffsetOfCallAndJmp(MessageBoxAddr, imageBase + pNewSectionHeader->VirtualAddress + 8);
*(DWORD*)&InjectCode[14] = OffsetOfCallAndJmp(OEP, pNewSectionHeader->VirtualAddress + 13);//跳转回oep正常执行程序
memcpy(FileBuffer + pNewSectionHeader->PointerToRawData, InjectCode, sizeof(InjectCode));//将代码写入新的内存空间
//3. 保存文件
return FileBufferWriteToFile(L"InjectCodeByCreateNewSection1.exe");
}
//扩大节区
BOOL ExpandSection(DWORD ExSize) {
//扩大节区大小是针对ImageBuffer而言的,所以我们添加的大小要进行内存对齐
//1. 申请一块新空间
ExpandFileBuffer(ExSize); //注意这个节表指针要在申请新空间之后
PIMAGE_SECTION_HEADER pLastSectionHeader = &pSectionHeader[numberOfSections - 1];//只能扩大最后一个节区
//2. 调整SizeOfImage
//如果VirtualSize+ExSize超过了AlignSize(VirtualSize,0x1000) 那么需要调整,否则不需要改变
//例如vs=0x500 ex=0x400 显然,原始vs内存对齐也会占0x1000 扩展后没有超过0x1000
//取文件大小和内存大小的最大值
//先计算扩展后的内存对齐值和扩展前的内存对齐值之间的差值
DWORD AlignExImage = AlignSize(pLastSectionHeader->Misc.VirtualSize + ExSize, 0x1000) -
AlignSize(max(pLastSectionHeader->Misc.VirtualSize, pLastSectionHeader->SizeOfRawData), 0x1000);//内存对齐后的值
if (AlignExImage > 0)//如果差值>0说明需要扩展映像 否则内存对齐的空白区足够存储扩展区
pOptionalHeader->SizeOfImage = sizeOfImage = sizeOfImage + AlignExImage;
//3. 修改文件大小和内存大小 注意要在修改sizeofimage后再更新这两个值
pLastSectionHeader->SizeOfRawData += AlignSize(ExSize, 0x200);//文件大小必须是文件对齐整数倍
pLastSectionHeader->Misc.VirtualSize += ExSize;//由于是内存对齐前的大小,所以直接加上文件对齐后的大小即可
//4. 保存文件
return FileBufferWriteToFile(L"ExpandSectionFile.exe");
}
//合并所有节区为1个
BOOL CombineSection() {
//1. 直接修改ImageBuffer
PIMAGE_DOS_HEADER pDosHeaderOfImage = (PIMAGE_DOS_HEADER)imageBuffer;
PIMAGE_NT_HEADERS pNtHeadersOfImage = (PIMAGE_NT_HEADERS)(imageBuffer + pDosHeader->e_lfanew);
PIMAGE_FILE_HEADER pFileHeaderOfImage = (PIMAGE_FILE_HEADER)(&pNtHeadersOfImage->FileHeader);
PIMAGE_OPTIONAL_HEADER pOptionalHeaderOfImage = (PIMAGE_OPTIONAL_HEADER)(&pNtHeadersOfImage->OptionalHeader);
PIMAGE_SECTION_HEADER pSectionHeaderOfImage = (PIMAGE_SECTION_HEADER)((DWORD)pOptionalHeaderOfImage + pFileHeaderOfImage->SizeOfOptionalHeader);
//复制节区属性
for (DWORD i = 1; i < numberOfSections; i++) {
pSectionHeaderOfImage[0].Characteristics |= pSectionHeaderOfImage[i].Characteristics;
}
//调整节表
pSectionHeaderOfImage[0].PointerToRawData = pSectionHeaderOfImage[0].VirtualAddress;//文件偏移改为内存偏移
pSectionHeaderOfImage[0].Misc.VirtualSize = pSectionHeaderOfImage[0].SizeOfRawData = sizeOfImage - pSectionHeaderOfImage[0].VirtualAddress;//新的节区大小为所有节区内存大小之和
pOptionalHeaderOfImage->SizeOfHeaders = AlignSize(sizeOfHeaders - (numberOfSections - 1) * sizeof(IMAGE_SECTION_HEADER), 0x200);//调整头大小
//删除其他节表
memset(&pSectionHeaderOfImage[1], 0, sizeof(IMAGE_SECTION_HEADER) * (numberOfSections - 1));
pFileHeaderOfImage->NumberOfSections = 1;
return ImageBufferWriteToFile(L"CombineSection1.exe");
}
~PEFile() {
if (FileBuffer) //释放空间
delete[] FileBuffer;
if (imageBuffer)
delete[] imageBuffer;
if (hProcess)
CloseHandle(hProcess);
}
};
int main() {
//PEFile peFile = PEFile(L"C:\\Users\\admin\\Desktop\\DailyExercise.exe");
PEFile peFile = PEFile(L"C:\\Users\\admin\\Desktop\\DllTest.dll");
peFile.showPeFile();
return 0;
}
附: 示例程序文件
示例程序.zip
(66.85 KB, 下载次数: 34)