前面在PE文件笔记十一 新增节学习了关于节的操作之新增节,接下来继续学习节的操作之合并节

合并节

为什么要合并节

在前面新增节中,要判断最后一个节表后面是否有空间用于新增节表,只有当最后一个节表后40个字节全为0时,才能进行新增节的操作;但当条件不满足时又想要新增节,该如何操作?

答案便是:合并节,合并节就是用一个节表描述多个节,这样省下来的节表空间就可以用于新增节了

于是合并节的目的便是:节省节表空间,这样就能实现新增节


合并节涉及的结构体成员

涉及的节表成员 含义
Name 节名称
VirtualAddress 节在内存中的偏移 (RVA)
Misc 节的实际大小
SizeOfRawData 节在文件中对齐后的尺寸
PointerToRawData 节区在文件中的偏移
Characteristics 节的属性
涉及的标准PE头成员 含义
NumberOfSections 节的个数

合并节的流程

  1. 修正内存对齐
  2. 修改第一个节的大小
  3. 修改第一个节的权限
  4. 修改节数量为1
  5. 清空后面的节(可选)

按流程合并节

修正内存对齐

关于修正内存对齐的内容在上一篇笔记 PE文件笔记十二 修正内存对齐中已经详细说明了

这次就以上一篇修正过内存对齐的结果:EverEdit_修正.exe直接进行合并节,在这里省略修正内存对齐的步骤;不了解如何修正内存对齐的可以回顾上一篇笔记

给出修正完内存对齐后 节的信息

image-20210406135858359


Name Misc SizeOfRawData PointerToRawData Characteristics
.text 0x19a000 0x19a000 0x400 0x60000020
.rdata 0x38000 0x38000 0x19a400 0x40000040
.data 0x4b000 0x4b000 0x1d2400 0xc0000040
.rsrc 0x63000 0x63000 0x21d400 0x40000040
.reloc 0x17000 0x17000 0x280400 0x42000040

修改第一个节的大小

修改第一个节的大小为后面 为所有节内存对齐后的大小的和

即修改第一个节的大小为 0x19a000+0x38000+0x4b000+0x63000+0x17000=0x297000

仍然使用PE工具:DIE进行修改

image-20210406191646661


修改后

image-20210406191729130


修改第一个节的权限

既然要用一个节表概括所有的节,那么该节表就必须具备先前所有节表的权限

也就是第一个节的权限 = 所有节的权限 相或

image-20210406192516941


得到新的权限为0xE2000060

使用PE工具:DIE修改

image-20210406192958994


修改后

image-20210406193139515


修改节数量为1

找到标准PE头中的NumberOfSections成员,将其修改为1

image-20210406193233571


修改后

image-20210406193332082


清空后面的节(可选)

其实做完上面一步就已经完成了合并节,但合并节是为了腾出节表空间,于是这里再将后面无用的节表清空掉

使用WinHex找到节表处

image-20210406193717767


选中要清空的部分,编辑→填充选块(快捷键 Ctrl+L)

image-20210406193921600


image-20210406193818020


清空后保存即可

image-20210406194003256


测试运行

程序仍然可以正常运行

image-20210406194149811


并且此时再用PE工具:DIE查看节的信息,也只有一个节了

image-20210406194230742


代码实现合并节

完整代码

 复制代码 隐藏代码// PE.cpp : Defines the entry point for the console application.
//
#include <stdio.h>
#include <malloc.h>
#include <windows.h>
#include <winnt.h>
#include <math.h>
//在VC6这个比较旧的环境里,没有定义64位的这个宏,需要自己定义,在VS2019中无需自己定义
#define IMAGE_FILE_MACHINE_AMD64 0x8664

//向文件中指定位置追加数据
//第一个参数为文件路径
//第二个参数为要追加的数据指针
//第三个参数为要追加的数据大小
//第四个参数为位置偏移
//第五个参数为hMap的指针
//第六个参数为pFile的指针
BOOL appendFile(LPCSTR filePath, PVOID writeData, DWORD sizeOfWriteData, DWORD offset, HANDLE* phMap, PVOID* ppFile) {
HANDLE hFile = CreateFileA(filePath, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, 0);
char newPath[100];
strcpy(newPath, filePath);

strcat(newPath, ".exe");
HANDLE hFile2 = CreateFileA(newPath, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_WRITE, NULL, OPEN_ALWAYS, 0, 0);

//WriteFile用于接收实际写入的大小的参数
DWORD dwWritenSize = 0;

//根据文件句柄创建映射
HANDLE hMap = CreateFileMappingA(hFile, NULL, PAGE_READWRITE, 0, 0, 0);
//映射内容
LPVOID pFile = MapViewOfFile(hMap, FILE_SHARE_WRITE, 0, 0, 0);

BYTE* content = (BYTE*)pFile;
content += offset;

//写入要插入数据前的数据
DWORD size = SetFilePointer(hFile, NULL, NULL, FILE_END);
BOOL bRet;
bRet = WriteFile(hFile2, pFile, offset, &dwWritenSize, NULL);
if (!bRet)return false;
//写入要插入的数据
SetFilePointer(hFile, NULL, NULL, FILE_END);
bRet = WriteFile(hFile2, writeData, sizeOfWriteData, &dwWritenSize, NULL);
if (!bRet)return false;
//写入要插入数据后的数据
SetFilePointer(hFile, NULL, NULL, FILE_END);
bRet = WriteFile(hFile2, content, size - offset, &dwWritenSize, NULL);
if (!bRet)return false;
CloseHandle(hFile);
CloseHandle(hMap);
CloseHandle(*phMap);
UnmapViewOfFile(pFile);
UnmapViewOfFile(*ppFile);
bRet = DeleteFileA(filePath);
if (!bRet)return false;

hFile = CreateFileA(filePath, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_WRITE, NULL, OPEN_ALWAYS, 0, 0);
//根据文件句柄创建映射
hMap = CreateFileMappingA(hFile2, NULL, PAGE_READWRITE, 0, 0, 0);
//映射内容
pFile = MapViewOfFile(hMap, FILE_SHARE_WRITE, 0, 0, 0);
SetFilePointer(hFile, NULL, NULL, FILE_BEGIN);
bRet = WriteFile(hFile, pFile, sizeOfWriteData + size, &dwWritenSize, NULL);
if (!bRet)return false;
CloseHandle(hFile);
CloseHandle(hFile2);
CloseHandle(hMap);
UnmapViewOfFile(pFile);
bRet = DeleteFileA(newPath);
if (!bRet)return false;
hFile = CreateFileA(filePath, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_WRITE, NULL, OPEN_ALWAYS, 0, 0);
//根据文件句柄创建映射
hMap = CreateFileMappingA(hFile, NULL, PAGE_READWRITE, 0, 0, 0);
//映射内容
*ppFile = MapViewOfFile(hMap, FILE_SHARE_WRITE, 0, 0, 0);
*phMap = hMap;
CloseHandle(hFile);
return true;
}

//根据pFile获取PE文件结构
void GetPeStruct32(LPVOID pFile, _IMAGE_DOS_HEADER* dos, _IMAGE_NT_HEADERS* nt, _IMAGE_SECTION_HEADER** sectionArr) {
dos = (_IMAGE_DOS_HEADER*)pFile;

//创建指向PE文件头标志的指针
DWORD* peId;
//让PE文件头标志指针指向其对应的地址=DOS首地址+偏移
peId = (DWORD*)((UINT)dos + dos->e_lfanew);

//创建指向可选PE头的第一个成员magic的指针
WORD* magic;
//让magic指针指向其对应的地址=PE文件头标志地址+PE文件头标志大小+标准PE头大小
magic = (WORD*)((UINT)peId + sizeof(DWORD) + sizeof(_IMAGE_FILE_HEADER));

//根据magic判断为32位程序还是64位程序

//让PE文件头指针指向其对应的地址
nt = (_IMAGE_NT_HEADERS*)peId;
//创建指向块表的指针
_IMAGE_SECTION_HEADER* sectionHeader;
//让块表的指针指向其对应的地址
sectionHeader = (_IMAGE_SECTION_HEADER*)((UINT)nt + sizeof(_IMAGE_NT_HEADERS));
//计数,用来计算块表地址
int cnt = 0;
//比较 计数 和 块表的个数,即遍历所有块表
while (cnt < nt->FileHeader.NumberOfSections) {
//创建指向块表的指针
_IMAGE_SECTION_HEADER* section;
//让块表的指针指向其对应的地址=第一个块表地址+计数*块表的大小
section = (_IMAGE_SECTION_HEADER*)((UINT)sectionHeader + sizeof(_IMAGE_SECTION_HEADER) * cnt);
//将得到的块表指针存入数组
sectionArr[cnt++] = section;

}
}

//修正节表的Misc和SizeOfRawData
//第一个参数为指向dos头的指针
//第二个参数为指向nt头的指针
//第三个参数为存储指向节指针的数组
//第四个参数为文件路径
//第五个参数为文件映射
//第六个参数为文件映射内容指针
//第七个参数为要修正的节表在数组中的下标
void sectionAlignment(_IMAGE_DOS_HEADER* dos, _IMAGE_NT_HEADERS* nt, _IMAGE_SECTION_HEADER** sectionArr, LPCSTR filePath, HANDLE* phMap,LPVOID* ppFile, int n) {

//获得最后一个节的实际大小
DWORD VirtualSize = sectionArr[n]->Misc.VirtualSize;
//获得最后一个节的文件对齐后的大小
DWORD SizeOfRawData = sectionArr[n]->SizeOfRawData;
//计算上一个节内存对齐后的大小
UINT SizeInMemory = (UINT)ceil((double)max(VirtualSize, SizeOfRawData) / (double)nt->OptionalHeader.SectionAlignment) * nt->OptionalHeader.SectionAlignment;
printf("%X\n", SizeInMemory);
//计算差值= 内存对齐后大小 - 文件对齐后大小
UINT offset = SizeInMemory - sectionArr[n]->SizeOfRawData;
printf("%X\n", offset);
//根据节在文件中的偏移 + 文件对齐后的大小 得到节的末尾
UINT end = sectionArr[n]->PointerToRawData + sectionArr[n]->SizeOfRawData;
printf("end:%X\n", end);

//申请要填充的空间
INT* content = (INT*)malloc(offset);
//初始化为0
ZeroMemory(content, offset);
//WriteFile用于接收实际写入的大小的参数
DWORD dwWritenSize = 0;

BOOL bRet=appendFile(filePath, (PVOID)content, offset, end,phMap,ppFile);
GetPeStruct32(*ppFile, dos, nt, sectionArr);
if (bRet) {
//开始修正Misc和SizeOfRawData
sectionArr[n]->Misc.VirtualSize = SizeInMemory;
sectionArr[n]->SizeOfRawData = SizeInMemory;
//修正后面受到影响的节的PointerOfRawData和VirtualAddress
int i;
while (n + 1 <= nt->FileHeader.NumberOfSections - 1) {
n++;
sectionArr[n]->PointerToRawData += offset;
}
}

}

//合并节
//第一个参数为指向dos头的指针
//第二个参数为指向nt头的指针
//第三个参数为存储指向节指针的数组
void combineSection(_IMAGE_DOS_HEADER* dos, _IMAGE_NT_HEADERS* nt, _IMAGE_SECTION_HEADER** sectionArr) {
//所有节内存对齐后的大小的和,这里要求已经修正过内存对齐,只有这样文件对齐大小才会等于内存对齐大小
DWORD allSectionSize = 0;
//所有节的权限,初始为第一个节的权限,和后面的每个节的权限进行或操作
DWORD allSectionCharacteristics = sectionArr[0]->Characteristics;
int i;
for (i = 0; i < nt->FileHeader.NumberOfSections;i++) {
allSectionSize += sectionArr[i]->SizeOfRawData;
allSectionCharacteristics = allSectionCharacteristics | sectionArr[i]->Characteristics;
}
printf("allSectionSize:%X\n", allSectionSize);
printf("allSectionCharacteristics:%X\n", allSectionCharacteristics);

sectionArr[0]->Misc.VirtualSize = allSectionSize;
sectionArr[0]->SizeOfRawData = allSectionSize;
sectionArr[0]->Characteristics = allSectionCharacteristics;

//清零后面的节
for (i = 1; i < nt->FileHeader.NumberOfSections; i++) {
ZeroMemory(sectionArr[i], sizeof(_IMAGE_SECTION_HEADER));
}

//节个数设置为1
nt->FileHeader.NumberOfSections = 1;
}

int main(int argc, char* argv[])
{
//创建DOS对应的结构体指针
_IMAGE_DOS_HEADER* dos;
//读取文件,返回文件句柄
HANDLE hFile = CreateFileA("C:\\Users\\lyl610abc\\Desktop\\EverEdit\\EverEdit_修正.exe", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, 0);
//根据文件句柄创建映射
HANDLE hMap = CreateFileMappingA(hFile, NULL, PAGE_READWRITE, 0, 0, 0);
//映射内容
LPVOID pFile = MapViewOfFile(hMap, FILE_SHARE_WRITE, 0, 0, 0);
//类型转换,用结构体的方式来读取
dos = (_IMAGE_DOS_HEADER*)pFile;
//输出dos->e_magic,以十六进制输出
printf("dos->e_magic:%X\n", dos->e_magic);

//创建指向PE文件头标志的指针
DWORD* peId;
//让PE文件头标志指针指向其对应的地址=DOS首地址+偏移
peId = (DWORD*)((UINT)dos + dos->e_lfanew);
//输出PE文件头标志,其值应为4550,否则不是PE文件
printf("peId:%X\n", *peId);

//创建指向可选PE头的第一个成员magic的指针
WORD* magic;
//让magic指针指向其对应的地址=PE文件头标志地址+PE文件头标志大小+标准PE头大小
magic = (WORD*)((UINT)peId + sizeof(DWORD) + sizeof(_IMAGE_FILE_HEADER));
//输出magic,其值为0x10b代表32位程序,其值为0x20b代表64位程序
printf("magic:%X\n", *magic);
//根据magic判断为32位程序还是64位程序
switch (*magic) {
case IMAGE_NT_OPTIONAL_HDR32_MAGIC:
{
printf("32位程序\n");
//确定为32位程序后,就可以使用_IMAGE_NT_HEADERS来接收数据了
//创建指向PE文件头的指针
_IMAGE_NT_HEADERS* nt;
//让PE文件头指针指向其对应的地址
nt = (_IMAGE_NT_HEADERS*)peId;
printf("Machine:%X\n", nt->FileHeader.Machine);
printf("Magic:%X\n", nt->OptionalHeader.Magic);
//创建一个指针数组,该指针数组用来存储所有的节表指针
//这里相当于_IMAGE_SECTION_HEADER* sectionArr[nt->FileHeader.NumberOfSections],声明了一个动态数组
_IMAGE_SECTION_HEADER** sectionArr = (_IMAGE_SECTION_HEADER**)malloc(sizeof(_IMAGE_SECTION_HEADER*) * nt->FileHeader.NumberOfSections);

//创建指向块表的指针
_IMAGE_SECTION_HEADER* sectionHeader;
//让块表的指针指向其对应的地址
sectionHeader = (_IMAGE_SECTION_HEADER*)((UINT)nt + sizeof(_IMAGE_NT_HEADERS));
//计数,用来计算块表地址
int cnt = 0;
//比较 计数 和 块表的个数,即遍历所有块表
while (cnt < nt->FileHeader.NumberOfSections) {
//创建指向块表的指针
_IMAGE_SECTION_HEADER* section;
//让块表的指针指向其对应的地址=第一个块表地址+计数*块表的大小
section = (_IMAGE_SECTION_HEADER*)((UINT)sectionHeader + sizeof(_IMAGE_SECTION_HEADER) * cnt);
//将得到的块表指针存入数组
sectionArr[cnt++] = section;
//输出块表名称
printf("%s\n", section->Name);
}
CloseHandle(hFile);

int i;
//sectionAlignment(dos, nt, sectionArr, "C:\\Users\\sixonezero\\Desktop\\EverEdit\\EverEdit.exe",hMap, pFile,2);

for (i = 0; i < nt->FileHeader.NumberOfSections; i++) {
sectionAlignment(dos, nt, sectionArr, "C:\\Users\\lyl610abc\\Desktop\\EverEdit\\EverEdit_修正.exe", &hMap, &pFile,i);
}

combineSection(dos, nt, sectionArr);

break;
}

case IMAGE_NT_OPTIONAL_HDR64_MAGIC:
{
printf("64位程序\n");
//确定为64位程序后,就可以使用_IMAGE_NT_HEADERS64来接收数据了
//创建指向PE文件头的指针
_IMAGE_NT_HEADERS64* nt;
nt = (_IMAGE_NT_HEADERS64*)peId;
printf("Machine:%X\n", nt->FileHeader.Machine);
printf("Magic:%X\n", nt->OptionalHeader.Magic);

//创建一个指针数组,该指针数组用来存储所有的节表指针
//这里相当于_IMAGE_SECTION_HEADER* sectionArr[nt->FileHeader.NumberOfSections],声明了一个动态数组
_IMAGE_SECTION_HEADER** sectionArr = (_IMAGE_SECTION_HEADER**)malloc(sizeof(_IMAGE_SECTION_HEADER*) * nt->FileHeader.NumberOfSections);

//创建指向块表的指针
_IMAGE_SECTION_HEADER* sectionHeader;
//让块表的指针指向其对应的地址,区别在于这里加上的偏移为_IMAGE_NT_HEADERS64
sectionHeader = (_IMAGE_SECTION_HEADER*)((UINT)nt + sizeof(_IMAGE_NT_HEADERS64));
//计数,用来计算块表地址
int cnt = 0;
//比较 计数 和 块表的个数,即遍历所有块表
while (cnt < nt->FileHeader.NumberOfSections) {
//创建指向块表的指针
_IMAGE_SECTION_HEADER* section;
//让块表的指针指向其对应的地址=第一个块表地址+计数*块表的大小
section = (_IMAGE_SECTION_HEADER*)((UINT)sectionHeader + sizeof(_IMAGE_SECTION_HEADER) * cnt);
//将得到的块表指针存入数组
sectionArr[cnt++] = section;
//输出块表名称
printf("%s\n", section->Name);
}

break;
}

default:
{
printf("error!\n");
break;
}

}
return 0;
}

合并节代码

 复制代码 隐藏代码//合并节
//第一个参数为指向dos头的指针
//第二个参数为指向nt头的指针
//第三个参数为存储指向节指针的数组
void combineSection(_IMAGE_DOS_HEADER* dos, _IMAGE_NT_HEADERS* nt, _IMAGE_SECTION_HEADER** sectionArr) {
//所有节内存对齐后的大小的和,这里要求已经修正过内存对齐,只有这样文件对齐大小才会等于内存对齐大小
DWORD allSectionSize = 0;
//所有节的权限,初始为第一个节的权限,和后面的每个节的权限进行或操作
DWORD allSectionCharacteristics = sectionArr[0]->Characteristics;
int i;
for (i = 0; i < nt->FileHeader.NumberOfSections;i++) {
allSectionSize += sectionArr[i]->SizeOfRawData;
allSectionCharacteristics = allSectionCharacteristics | sectionArr[i]->Characteristics;
}
printf("allSectionSize:%X\n", allSectionSize);
printf("allSectionCharacteristics:%X\n", allSectionCharacteristics);

sectionArr[0]->Misc.VirtualSize = allSectionSize;
sectionArr[0]->SizeOfRawData = allSectionSize;
sectionArr[0]->Characteristics = allSectionCharacteristics;

//清零后面的节
for (i = 1; i < nt->FileHeader.NumberOfSections; i++) {
ZeroMemory(sectionArr[i], sizeof(_IMAGE_SECTION_HEADER));
}

//节个数设置为1
nt->FileHeader.NumberOfSections = 1;
}

运行结果

image-20210406200009298


image-20210406194149811


说明

  • 合并节就是用一个节表来包括多个节表的信息
  • 可以看到合并节除了修正内存对齐,其余部分都十分简单
  • 合并节之后多出了的节表空间可以用来新增节

附件

附上本笔记中分析的EverEdit文件:点我下载

此次附件中添加了合并完节后的exe

image-20210406202251712

转载自吾爱破解上的一位大佬