继续PE系列笔记的更新
PE其它笔记索引可前往:
PE文件笔记一 PE介绍
经过前面的实战PE文件笔记八 实战之HOOK程序添加弹窗和PE文件笔记九 实战之HOOK程序添加弹窗续,对PE文件结构有了进一步的了解之后继续来学习对节的操作
此篇笔记为学习节操作之扩大节
扩大节
为什么要扩大节
在先前的实战中,通过在节表和节之间的空白区写入代码并执行来达到了弹窗的效果,但是如果当想要执行的代码量较大时,即空白区的空间不够时,就可以通过扩大节来解决空白区不足的问题
扩大节涉及的结构体成员
涉及的节表成员 |
含义 |
VirtualAddress |
节在内存中的偏移 (RVA) |
Misc |
节的实际大小 |
SizeOfRawData |
节在文件中对齐后的尺寸 |
PointerToRawData |
节区在文件中的偏移 |
涉及的扩展PE头成员 |
含义 |
SizeOfImage |
Image(PE文件)大小 |
扩大哪个节
既然要扩大节,那么选择哪个节进行扩大比较好?
实际上每个节都可以扩大,但是一般来说是选取最后一个节进行扩大,为什么?
因为节是顺序存储的,如果扩大了前面的节就意味着后面的节全部都要相应地修改偏移,比较麻烦
选取最后一个节进行扩大就不会影响到后面的节了
扩大节流程
- 将最后一个节的SizeOfRawData和VirtualSize改成N,N = 节内存对齐后的大小 + 要扩大的大小
- 修改 SizeOfImage大小=SizeOfImage大小+要扩大的大小
- 分配一块新的空间,大小为:节内存对齐增加的大小+要扩大的大小
按流程扩大节
此次依旧以先前的EverEdit.exe为例进行扩大节的演示
修正节表成员
由于选择的是最后一个节进行扩大,于是不涉及VirtualAddress和PointerToRawData的修改,只需修改Misc和SizeOfRawData即可
先用PE工具DIE查看一下最后一个节的成员
得到了
涉及的节表成员 |
值 |
含义 |
Misc |
0x1643a |
节的实际大小 |
SizeOfRawData |
0x16600 |
节在文件中对齐后的尺寸 |
于是可以计算出在节内存对齐后的大小:
节内存对齐后大小 = (max{Misc,SizeOfRawData}÷SectionAlignment)向上取整 × SectionAlignment
即节内存对齐后大小 (= max{0x1643a,0x16600}÷0x1000)向上取整 × 0x1000 =(0x16600÷0x1000)向上取整 × 0x1000 = 0x17000
计算 节内存对齐后大小的原理在PE文件笔记六 节表和节中已经说明,这里不再赘述
得到的节内存对齐后的大小后再加上要扩大的大小:0x1000,即 0x17000+0x1000=0x18000
于是要将Misc和SizeOfRawData都修改为0x00018000
此时节内存对齐增加的大小 = 节内存对齐后大小 - SizeOfRawData(节文件对齐后的大小)
即节内存对齐增加的大小 = 0x17000 - 0x16600 = 0x17000 - 0x16600 =0xA00
用WinHex找到对应的位置
修改数据,注意小端存储
修正SizeOfImage
用PE工具:DIE查看一下扩展PE头中SizeOfImage
得到原本SizeOfImage的大小为0x00298000
要修改为 原本大小+扩大的大小= 0x00298000 + 0x1000 = 0x00299000
在WinHex中找到SizeOfImage对应的位置
修改为:
分配新空间
使用WinHex打开EverEdit,直接拉到文件的末尾,并选中最后一个字节
然后 编辑→粘贴0字节
按"是"
选择插入的大小为:节内存对齐增加的大小 + 要扩大的大小 = 0xA00 + 0x1000=0x1A00 对应十进制为6656
添加完成
保存测试
将文件保存后再打开,发现仍然能够正常运行
代码实现扩大节
//扩大最后一个节
//第一个参数为指向dos头的指针
//第二个参数为指向nt头的指针
//第三个参数为存储指向节指针的数组
//第四个参数为文件句柄
//第五个参数为要扩大的节的大小
void expandSection(_IMAGE_DOS_HEADER* dos, _IMAGE_NT_HEADERS* nt, _IMAGE_SECTION_HEADER** sectionArr, HANDLE hFile, UINT expandSize) {
//获得最后一个节的实际大小
DWORD VirtualSize = sectionArr[nt->FileHeader.NumberOfSections - 1]->Misc.VirtualSize;
//获得最后一个节的文件对齐后的大小
DWORD SizeOfRawData = sectionArr[nt->FileHeader.NumberOfSections - 1]->SizeOfRawData;
//算出最后一个节内存对齐后的大小
UINT SizeInMemory = (UINT)ceil((double)max(VirtualSize, SizeOfRawData) / double(nt->OptionalHeader.SectionAlignment)) * nt->OptionalHeader.SectionAlignment;
printf("Last Section SizeInMemory:%X\n", SizeInMemory);
//根据内存对齐后大小 - 文件对齐后大小 得到 节内存对齐增加的大小
UINT offset = SizeInMemory - sectionArr[nt->FileHeader.NumberOfSections - 1]->SizeOfRawData;
printf("offset:%X\n", offset);
//根据节在文件中的偏移 + 文件对齐后的大小 得到节的末尾
UINT end = sectionArr[nt->FileHeader.NumberOfSections - 1]->PointerToRawData + sectionArr[nt->FileHeader.NumberOfSections - 1]->SizeOfRawData;
printf("end:%X\n", end);
//根据要扩大的节的大小 + 节内存对齐增加的大小 得到 要分配的新空间大小
UINT size = offset + expandSize;
//设置要写入的地址为节末尾
SetFilePointer(hFile, end, NULL, FILE_BEGIN);
//申请要填充的空间
INT* content = (INT*)malloc(expandSize + offset);
//初始化为0
ZeroMemory(content, expandSize + offset);
//WriteFile用于接收实际写入的大小的参数
DWORD dwWritenSize = 0;
BOOL bRet = WriteFile(hFile, content, expandSize + offset, &dwWritenSize, NULL);
if (bRet)
{
printf("expand Section success!\n");
//修正节表成员
sectionArr[nt->FileHeader.NumberOfSections - 1]->Misc.VirtualSize = SizeInMemory + expandSize;
sectionArr[nt->FileHeader.NumberOfSections - 1]->SizeOfRawData = SizeInMemory + expandSize;
//修正SizeOfImage
nt->OptionalHeader.SizeOfImage += expandSize;
}
else {
printf("%d\n", GetLastError());
}
}
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);
}
expandSection(dos, nt, sectionArr, hFile, 0x1000);
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;
}
运行结果
可以看到代码执行以后,达到了和前面手动操作一样的效果,并且程序仍然能够正常运行( •̀ ω •́ )✧
代码说明
代码中修改了先前打开文件的权限,修改其拥有写权限
//读取文件,返回文件句柄
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);
在扩大节的相关代码中用到了WriteFile和SetFilePointer;其它的就是单纯的PE内容了,代码中都有注释,这里也就不再赘述了
总结
- 扩大节一般选取最后一个节进行扩大
- 扩大最后一个节需要修正的节表成员:Misc和SizeOfRawData 以及修正扩展PE头中的SizeOfImage
- 扩大节后要分配的空间除了要扩大的大小外还要加上节内存对齐增加的大小(内存对齐后的大小-文件对齐后的大小)
附件
附上本笔记中分析的EverEdit文件:点我下载