PE文件解析基础
PE加载过程硬盘文件->加载到内存(FileBuffer)->E Loader加载并拉伸->ImageBuffer(起始位置ImageBase) DOS头从0x00~0x3f共0x40字节固定大小 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| 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头的偏移 其他成员无关紧要
PE文件解析基础
DOS Stub从dos头到pe头之间是dos存根
PE文件解析基础
dos存根的数据基本没用,主要是在DOS环境运行时执行 我们可以用DosBox的DOS环境运行exe程序 运行结果
PE文件解析基础
查看DosStub处代码
PE文件解析基础
NT/PE头PE文件头由PE文件头标识,标准PE头,扩展PE头三部分组成 32位 1
2
3
4
5
| 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位 1
2
3
4
5
| 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' FileHeader是标准PE头 OptionalHeader是可选PE头 但是非常重要 标准PE头/文件头占20字节 在pe文件头标识后即可找到 1
2
3
4
5
6
7
8
9
| 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头/扩展头1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
| 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;
|
重要成员: 1
2
3
4
5
6
7
8
| AddressOfEntryPoint; //程序入口OEP
ImageBase; //内存镜像地址
SectionAlignment; //内存对齐大小
FileAlignment; //文件对齐大小
SizeOfImage; //文件在内存中的大小(按照SectiionAlignment对齐后)
SizeOfHeaders; //DOS头+NT头+标准PE头+可选PE头+节表 按照文件对齐后的总大小
NumberOfRvaAndSizes //数据目录表个数
DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES] //数据目录表 存放导出表导入表等的地址和大小
| 节表紧跟在可选头后面的就是节表,PE中的节表以数组形式存在 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| 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; 节区属性 打印节表 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| //打印节表
void showSectionHeaders() {
printf("----------SectionHeaders----------");
for (DWORD i = 0; i < numberOfSections; i++) {
//逐个读取节表并打印
printf("----------Section%d----------", i);
printf("Name: %8s", pSectionHeader .Name);
printf("VirtualSize: %x", pSectionHeader.Misc.VirtualSize);
printf("VirtualAddress: %x", pSectionHeader.VirtualAddress);
printf("SizeOfRawData: %x", pSectionHeader.SizeOfRawData);
printf("ointerToRawData: %x", pSectionHeader.PointerToRawData);
printf("Characteristics: %x", pSectionHeader.Characteristics);
printf("----------Section%d----------", i);
}
printf("----------SectionHeaders----------");
}
|
运行结果
PE文件解析基础
代码段空白区添加代码基本原理在代码区添加硬编码(汇编代码的十六进制形式),修改oep使得程序开始执行时执行注入的代码 最后再跳转回原始的oep - 获取函数地址
- 计算偏移
- 代码区手动添加代码
- 修改oep指向shellcode
- 执行完shellcode后跳回oep
注意: 需要先关闭程序的随机基址,在可选头的DllCharacteristics中,将0x40改为0x00即可
PE文件解析基础
案例分析示例程序代码 1
2
3
4
5
6
7
8
| #include
#include
int main() {
MessageBox(0, L"HelloWorld!", L"Title", 0);
return 0;
}
|
运行后会弹出HelloWorld弹窗,这里仅做简单注入,四个参数全部压入0,此时会弹出错误窗口 分析: 首先在.text段中找一段空白代码区用于写入硬编码 这里选取59A0处开始写入 5A00开始是.rdata段
PE文件解析基础
确定硬编码 首先是四个参数 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)
PE文件解析基础
计算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
PE文件解析基础
写入硬编码并修改 写入后的代码
PE文件解析基础
修改oep 这里改的是rva 将原本的入口点11023改为165A0即可
PE文件解析基础
执行结果 可以看到程序入口点已经被修改为4165A0 并且输出错误弹窗,之后会跳转到原始的OEP处输出HelloWorld弹窗
PE文件解析基础
新增节添加代码通过.text段空白区注入代码实用性不高,通过新增节可以增大注入代码量,灵活性更高 基本过程: 代码实现 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
| //创建新的节区 返回新节区指针
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 .Name, ".text"))
{
memcpy(pNewSectionHeader, (LPVOID)&pSectionHeader, 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->ointerToRawData = 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->ointerToRawData, InjectCode, sizeof(InjectCode));//将代码写入新的内存空间
//3. 保存文件
return FileBufferWriteToFile(L"InjectCodeByCreateNewSection1.exe");
}
|
执行结果 inject节表
PE文件解析基础
inject节区
PE文件解析基础
程序运行情况
PE文件解析基础
扩大节当节表后的空白区大小不够时,可以选择扩大节 注意只能扩大最后一个节区,因为这样才不会影响到后续偏移 基本过程: 申请空间存储新的FileBuffer 修正Pe信息 需要修正的成员有 1
2
3
| SizeOfRawData // 节区文件中对齐后的尺寸
VirtualSize //内存对齐后的尺寸
SizeOfImage //映像大小
|
保存修改后的PE文件
代码 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| //扩大节区
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
PE文件解析基础
Image大小仅增加1000 这是由于400+600=A00没有超过内存对齐大小,原来的内存额外空间可以容纳这400字节,所以映像只需增加一个页
PE文件解析基础
合并节合并节也是针对ImageBuffer而言,可以直接对Imagebuffer操作 - 将所有节区属性合并到节区1
- 调整节表的文件偏移和内存大小
- 删除其他节表
- 保存文件
代码 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| //合并所有节区为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.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文件解析基础
合并后仅剩一个节区 并且PE文件很空旷,大部分空间是0 新文件大小128KB 合并后节区开始地址为1000 而headers到200处截止 此时又可以添加新的节区
PE文件解析基础
RVA&VA&FOA&RAWRVA(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求FOARVA=VA-ImageBase 确定RVA所在节区 可通过节区内存起始地址RVA<=RVA<=节区内存起始地址RVA+节区大小 即 VirtualAddress<=RVA<=VirtualAddress+VirtualSize判断RVA应该属于哪个节区 FOA=RVA-节区起始地址RVA+节区文件偏移=RVA - VirtualAddress + PointerToRawData
已知FOA求RVA节区文件起始地址<=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
PE文件解析基础
所以OEP RVA=411023-400000=11023 显然该RVA属于.text段: 11000<=RVA<=11000+5585 OEP FOA=11023-11000+400=423 通过PETools检查可以发现入口点FOA(即RAW)=0x423 与计算结果一致
PE文件解析基础
换算代码1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
| //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.VirtualAddress && RVA < pSectionHeader.VirtualAddress + AlignSize(pSectionHeader.Misc.VirtualSize, 0x1000))//成功找到所属节区
{
FOA = RVA - pSectionHeader.VirtualAddress + pSectionHeader.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.PointerToRawData && FOA < pSectionHeader.PointerToRawData + pSectionHeader.SizeOfRawData) {
RVA = FOA - pSectionHeader.PointerToRawData + pSectionHeader.VirtualAddress;
break;
}
}
return RVA;
}
//输入原始大小和对齐值返回对齐后的大小
DWORD AlignSize(DWORD OrigSize, DWORD AlignVal) {
//通过对对齐值取模判断是否对齐,如果对齐则返回原值,否则返回对齐后的值
return OrigSize % AlignVal ? (OrigSize / AlignVal + 1) * AlignVal : OrigSize;
}
| 静态/动态链接库静态库在链接阶段,会将汇编生成的目标文件.o与引用到的库一起链接打包到可执行文件中。因此对应的链接方式称为静态链接。 优点 方便 缺点 二进制代码需要编译到exe中,浪费空间 注: 静态库的二进制代码保存在.lib中 生成静态库在VS中可以创建静态库项目,建议创建空项目
PE文件解析基础
在静态库项目中创建StaticLib.cpp源文件和StaticLib.h头文件 代码实例 1
2
3
4
5
6
7
8
| //StaticLib.cpp 源文件保存函数代码
#include
void func() {
printf("HelloStaticLib!");
}
//StaticLib.h 头文件保存函数声明
void func();
|
点击生成即可生成静态库得到.lib文件 使用静态库函数在项目中找到StaticLib.lib和StaticLib.h文件,复制到需要使用该静态库的工程目录中
PE文件解析基础
在工程目录中导入StaticLib.h文件
PE文件解析基础
PE文件解析基础
程序代码 1
2
3
4
5
6
7
| #include
#include "StaticLib.h" //导入头文件,头文件有函数声明
#pragma comment(lib,"StaticLib.lib")//加载静态库,这里保存函数二进制代码
int main() {
func(); //直接使用静态库中定义的函数即可
return 0;
}
|
运行结果
PE文件解析基础
动态库动态库是在程序需要使用时加载,不同程序可以使用同一份dll,大大节省了空间 同创建静态库类似,建议使用空项目,创建好后将项目属性中的生成文件修改为动态库Dll即可 然后创建头文件和源文件 导出函数生成动态库文件有关键字导出和.def导出两种方式 __declspec关键字导出函数关键字功能解释 1
2
3
| extern //表示是全局函数 可供其他函数调用
"C" //按照C语言的方式编译链接,此时函数名不变 C++由于有重载机制,编译出的函数符号名会比较复杂
__declspec(dllexport)//告诉编译器该函数为导出函数
|
代码实例 1
2
3
4
5
6
7
8
9
10
| //DllTest.h
extern "C" __declspec(dllexport) void func();
//DllTest.cpp
//注意这里.cpp和.h的函数前都需要有__declspec(dllexport) 否则只会生成.dll而没有.lib
//注意都要有extern "C" 即保证函数声明和函数定义要一致
#include
extern "C" __declspec(dllexport) void func() {
printf("HelloDynamicLib!");
}
|
点击生成即可得到.dll和.lib文件 .def文件导出函数首先在源文件目录创建.def文件
PE文件解析基础
代码示例 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| //DllTest.cpp
#include
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导出的函数只有序号没有函数名
|
设置一下链接器,找到输入,修改模块定义文件如下
PE文件解析基础
点击生成即可得到.dll和.lib文件 注意NONAME导出的函数只有序号没有函数名,原来的sub函数在这里是Oridinal_3
PE文件解析基础
使用dll隐式链接基本步骤 将.dll .lib放到工程目录中 使用#pragma comment(lib,"dllname.lib")导入lib文件 静态库的.lib文件保存二进制代码,而dll中的.lib文件仅仅是指示函数位置 加入函数声明 extern "C" __declspec(dllexport) void func();
具体示例 首先将.dll和.lib放到工程目录中
PE文件解析基础
程序代码 1
2
3
4
5
6
| #pragma comment(lib,"DllTest.lib") //导入.lib
extern "C" __declspec(dllimport) void func();//导入函数声明
int main() {
func();//直接调用
return 0;
}
|
运行结果
PE文件解析基础
显式链接基本使用方法 1
2
3
4
5
6
7
8
9
10
11
| //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);
|
示例程序代码 1
2
3
4
5
6
7
8
9
10
| #include//包含了win32的函数和数据结构
#include
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;
}
|
运行结果
PE文件解析基础
数据目录表定义 1
2
3
4
| typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;//虚拟地址RVA,数据目录表的起始位置
DWORD Size;//大小
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
|
表项定义 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| #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个保留
|
可选头的最后两个成员分别定义了数据目录表的个数和数据目录表数组,指向了一些关键表格 1
2
| DWORD NumberOfRvaAndSizes;
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; //数据目录表,结构体数组
|
比较重要的有导出表,导入表,重定位表,IAT表 打印数据目录表 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
| //打印数据目录表
void PrintDirectory() {
PIMAGE_DATA_DIRECTORY pDirectory = pOptionalHeader->DataDirectory;
printf("**********数据目录表**********");
for (DWORD i = 0; i < pOptionalHeader->NumberOfRvaAndSizes; i++) {
switch (i) {
case IMAGE_DIRECTORY_ENTRY_EXPORT:
printf("==========导出表==========");
break;
case IMAGE_DIRECTORY_ENTRY_IMPORT:
printf("==========导入表==========");
break;
case IMAGE_DIRECTORY_ENTRY_RESOURCE:
printf("==========资源目录==========");
break;
case IMAGE_DIRECTORY_ENTRY_EXCEPTION:
printf("==========异常目录==========");
break;
case IMAGE_DIRECTORY_ENTRY_SECURITY:
printf("==========安全目录=========");
break;
case IMAGE_DIRECTORY_ENTRY_BASERELOC:
printf("==========重定位基本表==========");
break;
case IMAGE_DIRECTORY_ENTRY_DEBUG:
printf("==========调试目录==========");
break;
case IMAGE_DIRECTORY_ENTRY_ARCHITECTURE:
printf("==========描述字串==========");
break;
case IMAGE_DIRECTORY_ENTRY_GLOBALPTR:
printf("==========机器值==========");
break;
case IMAGE_DIRECTORY_ENTRY_TLS:
printf("==========TLS目录==========");
break;
case IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG:
printf("==========载入配置目录==========");
break;
case IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT:
printf("==========绑定输入表==========");
break;
case IMAGE_DIRECTORY_ENTRY_IAT:
printf("==========导入地址表==========");
break;
case IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT:
printf("==========延迟导入表==========");
break;
case IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR:
printf("==========COM信息==========");
break;
case 15:
printf("==========保留表==========");
break;
}
printf("VirtualAddress=%xSize=%xFOA=%x", pDirectory.VirtualAddress, pDirectory.Size,RVA2FOA(pDirectory.VirtualAddress));
}
printf("**********数据目录表打印完毕**********");
}
| 导出表导出表记录了pe文件导出的函数,所以.exe和.dll程序都可以导出函数 数据目录表中记录了导出表的地址和偏移 这个地址是RVA,需要转换为FOA 1
2
3
4
5
6
7
8
9
10
11
12
13
| 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关键字可以使部分导出函数没有名称仅有地址 关键成员 1
2
3
4
5
6
7
| Name
Base
NumberOfFunctions
NumberOfNames
AddressOfFunctions
AddressOfNames
AddressOfNameOrdinals
|
具体示例 假设: 导出函数名称表 FuncNameTable 导出函数序号表 FuncOridinalTable 导出函数地址表 FuncAddressTable 函数地址RVA FuncAddress 使用上一节的Dll函数 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| //DllTest.def
LIBRARY "DllTest"
EXPORTS
func1 @15
plus @1
sub @3 NONAME //sub序号为3 无导出函数名
//DllTest.cpp
#include
#include
void func1() {
printf("HelloDynamicLib!");
}
int plus(int x, int y) {
return x + y;
}
int sub(int x, int y) {
return x - y;
}
|
DLL的导出表信息
PE文件解析基础
序号导出假设已知导出函数序号OridinalNum 那么FuncAddress=FuncAddressTable[OridinalNum-Base] 即导出函数序号-Base值可以直接作为下标查找导出函数地址表得到导出函数地址 已知函数sub的导出序号为3 所以3-1=2直接查找得到其地址
PE文件解析基础
所以无名函数的序号可以通过遍历导出函数地址表来得到 名称导出通过函数名称查找函数地址的过程 首先查找导出函数名称表,判断数组中哪个字符串和目的函数名称相同 将该元素的下标作为索引,查找导出函数序号表 将导出函数序号表中该下标元素的内容作为下标查找导出函数地址表,该值即为函数地址
即 1
2
| if(strcmp(name,FuncNameTable)==0)
FuncAddress=FuncAddressTable[FuncOridinalTable];
|
假设要查找plus函数 plus这个函数名在函数名称表中的下标为1 而FuncOridinalTable[1]=0 所以plus Address=FuncAddressTable[0]=1125d
PE文件解析基础
注意由于导出函数地址表的大小=NumberOfFunctions=导出函数最大序号-最小序号 当序号不是连续时,就会用0地址填充多余表项
PE文件解析基础
通过序号查找函数地址1
2
3
4
5
| //通过函数序号获取函数地址
DWORD GetFuncAddrByOridinals(WORD OridinalNum) {
DWORD* pExportFuncAddressTable = (DWORD*)(RVA2FOA(pExportDirectory->AddressOfFunctions) + FileBuffer);//导出函数地址表
return pExportFuncAddressTable[OridinalNum - pExportDirectory->Base];//减去Base值作为索引直接查找函数地址
}
| 通过函数名查找函数地址代码 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| //通过函数名获取函数地址
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)+FileBuffer)) == 0)
{
pos = i;
break;
}
}
if (pos == -1)//查找失败
return 0;
//2. 通过序号表得到序号
OridinalNum = pExportFuncOridinalsTable[pos];
//3. 得到函数地址
return pExportFuncAddressTable[OridinalNum];
}
|
运行结果和PE工具显示的一致
PE文件解析基础
打印导出表1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
| //根据函数序号返回函数名地址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 == OridinalNum)//实际存储的序号=函数序号-base
return RVA2FOA(pExportFuncNamesTable)+FileBuffer;
}
return NULL;//没有找到说明是无名函数
}
//打印导出表详细信息
void PrintExportDirectory() {
printf("==========导出表==========");
printf("Name: %x (%s)",pExportDirectory->Name,(char*)(FileBuffer+RVA2FOA(pExportDirectory->Name)));
printf("Base: %x", pExportDirectory->Base);
printf("NumberOfFunctions: %x", pExportDirectory->NumberOfFunctions);
printf("NumberOfNames: %x", pExportDirectory->NumberOfNames);
printf("AddressOfFunctions: RVA=%x FOA=%x", pExportDirectory->AddressOfFunctions,RVA2FOA(pExportDirectory->AddressOfFunctions));
printf("AddressOfNames: RVA=%x FOA=%x", pExportDirectory->AddressOfNames, RVA2FOA(pExportDirectory->AddressOfNames));
printf("AddressOfNameOrdinals: RVA=%x FOA=%x", 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("Oridinal RVA FOA FunctionName");
for (DWORD i = 0; i < pExportDirectory->NumberOfFunctions; i++) {
if (pExportFuncAddressTable == 0)//地址为零则跳过
continue;
BYTE* FuncName = NULL;
//由于导出函数序号表仅保存有名函数序号,所以有序号必定有名称,否则无名称
//函数序号=函数地址表下标+Base
printf("%08x %08x %08x ",i+pExportDirectory->Base, pExportFuncAddressTable,RVA2FOA(pExportFuncAddressTable));
//是否存在函数名要单独判断 存储序号=函数序号-Base,故传递i即可
if (FuncName = GetFuncNameByOridinals(i))
printf("%s", FuncName);
else
printf("NONAME");
}
printf("==========导出表结束==========");
}
|
运行结果
PE文件解析基础
查看二进制文件 导出函数地址表 从7CC8开始共0xE个表项
PE文件解析基础
导出函数序号表 从7D0C开始共2个表项 每个表项2字节 存储序号分别是0xe(f func1) 0x0(1 plus)
PE文件解析基础
导出函数名称表 从7D04开始共2个表项 每个表项四字节 指向字符串指针
PE文件解析基础
重定位表重定位重定位的概念: 进程拥有独立的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个表项指向了重定位表 1
2
3
4
5
6
7
8
| 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
PE文件解析基础
查看重定位表
PE文件解析基础
打印重定位表1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
| //通过RVA判断所属区段名
PCHAR GetSectionNameByRva(DWORD RVA) {
for (DWORD i = 0; i < numberOfSections; i++) {
if (RVA >= pSectionHeader.VirtualAddress && RVA < pSectionHeader.VirtualAddress + AlignSize(pSectionHeader.Misc.VirtualSize, 0x1000))//成功找到所属节区
return (PCHAR)pSectionHeader.Name;
}
}
//打印重定位表的某个块
void PrintRelocationBlock(PIMAGE_BASE_RELOCATION pRelocationBlock) {
PWORD pBlock = (PWORD)((DWORD)pRelocationBlock + 8);//注意每个表项占2字节 但是高4位用来判断是否需要修改
DWORD PageOffset = pRelocationBlock->VirtualAddress;//每个块的虚拟地址即为页面起始地址
printf("序号 属性 RVA FOA 指向RVA");
for (DWORD i = 0; i < (pRelocationBlock->SizeOfBlock - 8) / 2; i++) {
//每块高四位用作属性判断,低12位才是页内偏移值 还要注意与运算优先级低于+ 不用括号会导致出错
//指向的RVA即需要矫正的地址
printf("%04x %4x %08x %08x %08x", i, pBlock >> 12, (pBlock & 0x0fff) + PageOffset, RVA2FOA((pBlock & 0x0fff) + PageOffset), *(DWORD*)(FileBuffer + RVA2FOA((pBlock & 0x0fff) + PageOffset)) & 0x00ffffff);
}
}
//打印重定位表
void PrintRelocationTable() {
PIMAGE_BASE_RELOCATION pRelocationTable = pBaseRelocation;
printf("==========重定位表==========");
printf("序号 区段 RVA FOA 项目数");
//表块全为0时结束
DWORD count = 0;
while (pRelocationTable->VirtualAddress || pRelocationTable->SizeOfBlock) {
//项目数=(sizeofBlock-8)/2
printf("%4d %8s %08x %08x %08x", 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("==========Block%d==========", count++);
PrintRelocationBlock(pRelocationTable);//打印第i个块
pRelocationTable = (PIMAGE_BASE_RELOCATION)((DWORD)pRelocationTable + pRelocationTable->SizeOfBlock);
}
printf("==========重定位表结束==========");
}
|
运行结果
PE文件解析基础
导入表数据目录表第2个表项是导入表,紧跟在导出表后 PE文件可能会有多个导入表以结构体数组形式存在,结束标识为全0 1
2
3
4
5
6
7
8
9
10
11
| 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和IATINT: 导入名称表 存储导入函数名称 IAT: 导入地址表 存储导入函数地址 这两张表的定义如下 1
2
3
4
5
6
7
8
9
| 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 即导入函数名称结构
PE文件解析基础
加载到内存后 IAT表被修复 存储导入函数地址
PE文件解析基础
PE文件加载前IAT表已经修复过 此时IAT已经保存了导入函数地址 地址=RVA+ImageBase,这就是绑定导入 导入表的TimeDateStamp为-1时表示已经进行绑定导入,如果为0表示没有绑定导入 优点: 启动程序快 缺点: 如果没有加载到正确基址仍然需要修复IAT表
打印导入表以DllTest.dll为例,用PEStudy打开 上方区域有三张导入表,分别列出了dllname以及它们INT和IAT的位置 下方区域第一列是IAT表项(即导入函数地址) 第二列是INT表项的FOA(ThunkData) 第三列是INT表项指向的值(*ThunkData)
PE文件解析基础
打印导入表的代码 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
| //打印INT表
void PrintINT(PIMAGE_IMPORT_DESCRIPTOR pImportTable) {
printf("==========INT==========");
printf("ThunkRVA ThunkFOA ThunkVal FuncName");
PIMAGE_THUNK_DATA32 pThunkData = (PIMAGE_THUNK_DATA32)(RVA2FOA(pImportTable->OriginalFirstThunk) + FileBuffer);
while (pThunkData->u1.Ordinal) {
//最高位为1时表示按序号导入,低31位作为序号值
printf("%08x %08x %08x ", FOA2RVA((DWORD)pThunkData - (DWORD)FileBuffer), (DWORD)pThunkData - (DWORD)FileBuffer, pThunkData->u1);
if (pThunkData->u1.Ordinal & 0x80000000) {
printf("%08x", 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", pImportName->Name);
}
pThunkData++;
}
}
//打印IAT表
void PrintIAT(PIMAGE_IMPORT_DESCRIPTOR pImportTable) {
printf("==========IAT==========");
PDWORD pThunkData = (PDWORD)(RVA2FOA(pImportTable->FirstThunk) + FileBuffer);
printf(" FuncRVA FuncFOA FuncAddr");
while (*pThunkData) {
printf("%08x %08x %08x", *pThunkData,RVA2FOA(*pThunkData), *pThunkData+imageBase);
pThunkData++;
}
}
//打印导入表
void PrintImportTable() {
PIMAGE_IMPORT_DESCRIPTOR pImportTable = pImportDescriptor;
printf("**********导入表**********");
printf("DllName INT RVA TimeStamp IAT RVA");
while (pImportTable->OriginalFirstThunk) {
printf("%-24s%08x %08x %08x", (RVA2FOA(pImportTable->Name) + FileBuffer),pImportTable->OriginalFirstThunk,pImportTable->TimeDateStamp,pImportTable->FirstThunk);
pImportTable++;
}
pImportTable = pImportDescriptor;
while (pImportTable->OriginalFirstThunk) {
printf("==========DllName:%s==========", RVA2FOA(pImportTable->Name) + FileBuffer);
PrintINT(pImportTable);
PrintIAT(pImportTable);
pImportTable++;
}
printf("**********导入表**********");
}
|
运行结果
PE文件解析基础
参考资料完整代码1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
| #include
#include
#include
#include
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("----------DosHeader----------");
printf("DosSignature: %x", dosSignature);
printf("NtHeaderOffset: %x", NToffset);
printf("----------DosHeader----------");
}
//打印标准Pe头
void showFileHeader() {
printf("----------FileHeader----------");
printf("Machine: %x", Machine);
printf("NumberOfSections: %x", numberOfSections);
printf("SizeOfOptionalHeader: %x", sizeOfOptionalHeader);
printf("----------FileHeader----------");
}
//打印可选PE头
void showOptionalHeader() {
printf("----------OptionalHeader----------");
printf("EntryPoint: %x", addressOfEntryPoint);
printf("ImageBase: %x", imageBase);
printf("SectionAlignment: %x", sectionAlignment);
printf("FileAlignment: %x", fileAlignment);
printf("SizeOfImage; %x", sizeOfImage);
printf("SizeOfHeaders: %x", sizeOfHeaders);
printf("----------OptionalHeader----------");
}
//打印NT头
void showNtHeader() {
printf("-----------NtHeader----------");
printf(" eSignature: %x", peSignature);
showFileHeader();
showOptionalHeader();
printf("-----------NtHeader----------");
}
//打印节表
void showSectionHeaders() {
printf("----------SectionHeaders----------");
for (DWORD i = 0; i < numberOfSections; i++) {
//逐个读取节表并打印
printf("----------Section%d----------", i);
printf("Name: %8s", pSectionHeader .Name);
printf("VirtualSize: %x", pSectionHeader.Misc.VirtualSize);
printf("VirtualAddress: %x", pSectionHeader.VirtualAddress);
printf("SizeOfRawData: %x", pSectionHeader.SizeOfRawData);
printf("ointerToRawData: %x", pSectionHeader.PointerToRawData);
printf("Characteristics: %x", pSectionHeader.Characteristics);
printf("----------Section%d----------", i);
}
printf("----------SectionHeaders----------");
}
//设置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("申请空间失败!");
system("pause");
return;
}
memset(imageBuffer, 0, sizeOfImage); //初始化内存空间,全部清零
memcpy(imageBuffer, FileBuffer, sizeOfHeaders); //直接复制各个表头
//2. 拉伸FileBuffer并写入ImageBuffer
for (DWORD i = 0; i < numberOfSections; i++) {
memcpy(imageBuffer + pSectionHeader.VirtualAddress, FileBuffer + pSectionHeader.PointerToRawData, pSectionHeader.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.PointerToRawData, imageBuffer + pSectionHeader.VirtualAddress, pSectionHeader.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, szModName,
sizeof(szModName) / sizeof(TCHAR))) {
MODULEINFO moduleInfo;
if (GetModuleInformation(hProcess, hMods, &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!");
exit(0);
}
fileBufferSize = GetFileSize(hFile, NULL); //获取文件大小
FileBuffer = new BYTE[fileBufferSize]; //分配内存空间用于存储文件
if (!FileBuffer) {
printf("AllocFileBufferMemoryFailure!");
exit(0);
}
if (!ReadFile(hFile, FileBuffer, fileBufferSize, NULL, NULL)) //读取文件并存储到内存中
{
delete[] FileBuffer;
printf("ReadFileFailure!");
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("**********数据目录表**********");
for (DWORD i = 0; i < pOptionalHeader->NumberOfRvaAndSizes; i++) {
switch (i) {
case IMAGE_DIRECTORY_ENTRY_EXPORT:
printf("==========导出表==========");
break;
case IMAGE_DIRECTORY_ENTRY_IMPORT:
printf("==========导入表==========");
break;
case IMAGE_DIRECTORY_ENTRY_RESOURCE:
printf("==========资源目录==========");
break;
case IMAGE_DIRECTORY_ENTRY_EXCEPTION:
printf("==========异常目录==========");
break;
case IMAGE_DIRECTORY_ENTRY_SECURITY:
printf("==========安全目录=========");
break;
case IMAGE_DIRECTORY_ENTRY_BASERELOC:
printf("==========重定位基本表==========");
break;
case IMAGE_DIRECTORY_ENTRY_DEBUG:
printf("==========调试目录==========");
break;
case IMAGE_DIRECTORY_ENTRY_ARCHITECTURE:
printf("==========描述字串==========");
break;
case IMAGE_DIRECTORY_ENTRY_GLOBALPTR:
printf("==========机器值==========");
break;
case IMAGE_DIRECTORY_ENTRY_TLS:
printf("==========TLS目录==========");
break;
case IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG:
printf("==========载入配置目录==========");
break;
case IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT:
printf("==========绑定输入表==========");
break;
case IMAGE_DIRECTORY_ENTRY_IAT:
printf("==========导入地址表==========");
break;
case IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT:
printf("==========延迟导入表==========");
break;
case IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR:
printf("==========COM信息==========");
break;
case 15:
printf("==========保留表==========");
break;
}
printf("VirtualAddress=%xSize=%xFOA=%x", pDirectory.VirtualAddress, pDirectory.Size, RVA2FOA(pDirectory.VirtualAddress));
}
printf("**********数据目录表打印完毕**********");
}
//通过函数名获取导出函数地址
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) + 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 == OridinalNum)//实际存储的序号=函数序号-base
return (PCHAR)(RVA2FOA(pExportFuncNamesTable) + FileBuffer);
}
return NULL;//没有找到说明是无名函数
}
//打印导出表详细信息
void PrintExportDirectory() {
//不存在导出表
if (!pExportDirectory)
{
printf("**********不存在导出表**********");
return;
}
printf("==========导出表==========");
printf("Name: %x (%s)", pExportDirectory->Name, (char*)(FileBuffer + RVA2FOA(pExportDirectory->Name)));
printf("Base: %x", pExportDirectory->Base);
printf("NumberOfFunctions: %x", pExportDirectory->NumberOfFunctions);
printf("NumberOfNames: %x", pExportDirectory->NumberOfNames);
printf("AddressOfFunctions: RVA=%x FOA=%x", pExportDirectory->AddressOfFunctions, RVA2FOA(pExportDirectory->AddressOfFunctions));
printf("AddressOfNames: RVA=%x FOA=%x", pExportDirectory->AddressOfNames, RVA2FOA(pExportDirectory->AddressOfNames));
printf("AddressOfNameOrdinals: RVA=%x FOA=%x", 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("Oridinal RVA FOA FunctionName");
for (DWORD i = 0; i < pExportDirectory->NumberOfFunctions; i++) {
if (pExportFuncAddressTable == 0)//地址为零则跳过
continue;
PCHAR FuncName = NULL;
//由于导出函数序号表仅保存有名函数序号,所以有序号必定有名称,否则无名称
//函数序号=函数地址表下标+Base
printf("%08x %08x %08x ", i + pExportDirectory->Base, pExportFuncAddressTable, RVA2FOA(pExportFuncAddressTable));
//是否存在函数名要单独判断 存储序号=函数序号-Base,故传递i即可
if (FuncName = GetFuncNameByOridinals(i))
printf("%s", FuncName);
else
printf("NONAME");
}
printf("==========导出表结束==========");
}
//打印重定位表的某个块
void PrintRelocationBlock(PIMAGE_BASE_RELOCATION pRelocationBlock) {
PWORD pBlock = (PWORD)((DWORD)pRelocationBlock + 8);//注意每个表项占2字节 但是高4位用来判断是否需要修改
DWORD PageOffset = pRelocationBlock->VirtualAddress;//每个块的虚拟地址即为页面起始地址
printf("序号 属性 RVA FOA 指向RVA");
for (DWORD i = 0; i < (pRelocationBlock->SizeOfBlock - 8) / 2; i++) {
//每块高四位用作属性判断,低12位才是页内偏移值 还要注意与运算优先级低于+ 不用括号会导致出错
//指向的RVA即需要矫正的地址
printf("%04x %4x %08x %08x %08x", i, pBlock >> 12, (pBlock & 0x0fff) + PageOffset, RVA2FOA((pBlock & 0x0fff) + PageOffset), *(DWORD*)(FileBuffer + RVA2FOA((pBlock & 0x0fff) + PageOffset)) & 0x00ffffff);
}
}
//打印重定位表
void PrintRelocationTable() {
PIMAGE_BASE_RELOCATION pRelocationTable = pBaseRelocation;
printf("==========重定位表==========");
printf("序号 区段 RVA FOA 项目数");
//表块全为0时结束
DWORD count = 0;
while (pRelocationTable->VirtualAddress || pRelocationTable->SizeOfBlock) {
//项目数=(sizeofBlock-8)/2
printf("%4d %8s %08x %08x %08x", 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("==========Block%d==========", count++);
PrintRelocationBlock(pRelocationTable);//打印第i个块
pRelocationTable = (PIMAGE_BASE_RELOCATION)((DWORD)pRelocationTable + pRelocationTable->SizeOfBlock);
}
printf("==========重定位表结束==========");
}
//打印INT表
void PrintINT(PIMAGE_IMPORT_DESCRIPTOR pImportTable) {
printf("==========INT==========");
printf("ThunkRVA ThunkFOA ThunkVal FuncName");
PIMAGE_THUNK_DATA32 pThunkData = (PIMAGE_THUNK_DATA32)(RVA2FOA(pImportTable->OriginalFirstThunk) + FileBuffer);
while (pThunkData->u1.Ordinal) {
//最高位为1时表示按序号导入,低31位作为序号值
printf("%08x %08x %08x ", FOA2RVA((DWORD)pThunkData - (DWORD)FileBuffer), (DWORD)pThunkData - (DWORD)FileBuffer, pThunkData->u1.Ordinal);
if (pThunkData->u1.Ordinal & 0x80000000) {
printf("%08x", 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", pImportName->Name);
}
pThunkData++;
}
}
//打印IAT表
void PrintIAT(PIMAGE_IMPORT_DESCRIPTOR pImportTable) {
printf("==========IAT==========");
PDWORD pThunkData = (PDWORD)(RVA2FOA(pImportTable->FirstThunk) + FileBuffer);
printf(" FuncRVA FuncFOA FuncAddr");
while (*pThunkData) {
printf("%08x %08x %08x", *pThunkData,RVA2FOA(*pThunkData), *pThunkData+imageBase);
pThunkData++;
}
}
//打印导入表
void PrintImportTable() {
PIMAGE_IMPORT_DESCRIPTOR pImportTable = pImportDescriptor;
printf("**********导入表**********");
printf("DllName INT RVA TimeStamp IAT RVA");
while (pImportTable->OriginalFirstThunk) {
printf("%-24s%08x %08x %08x", (RVA2FOA(pImportTable->Name) + FileBuffer),pImportTable->OriginalFirstThunk,pImportTable->TimeDateStamp,pImportTable->FirstThunk);
pImportTable++;
}
pImportTable = pImportDescriptor;
while (pImportTable->OriginalFirstThunk) {
printf("==========DllName:%s==========", RVA2FOA(pImportTable->Name) + FileBuffer);
PrintINT(pImportTable);
PrintIAT(pImportTable);
pImportTable++;
}
printf("**********导入表**********");
}
//通过RVA判断所属区段名
PCHAR GetSectionNameByRva(DWORD RVA) {
for (DWORD i = 0; i < numberOfSections; i++) {
if (RVA >= pSectionHeader.VirtualAddress && RVA < pSectionHeader.VirtualAddress + AlignSize(pSectionHeader.Misc.VirtualSize, 0x1000))//成功找到所属节区
return (PCHAR)pSectionHeader.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.VirtualAddress && RVA < pSectionHeader.VirtualAddress + AlignSize(pSectionHeader.Misc.VirtualSize, 0x1000))//成功找到所属节区
{
FOA = RVA - pSectionHeader.VirtualAddress + pSectionHeader.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.PointerToRawData && FOA < pSectionHeader.PointerToRawData + pSectionHeader.SizeOfRawData) {
RVA = FOA - pSectionHeader.PointerToRawData + pSectionHeader.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.Name, ".text"))
{
memcpy(pNewSectionHeader, (LPVOID)&pSectionHeader, 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->ointerToRawData = 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->ointerToRawData, 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.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;
}
|
附: 示例程序和修改后的程序
提取码下载:
|