为什么要合并节
新增节的前提:节表后面要有至少40字节空白,用来存放新的IMAGE_SECTION_HEADER 节表项
● 如果节表后面没有空余空间,无法直接新增节
● 解决方案:合并节,把多个节合并为一个,减少节的总数量,释放节表的空白位置,之后就可以新增节表项
不建议合并 .rsrc资源节、.reloc重定位节,极易程序崩溃
前置概念回顾 IMAGE_SECTION_HEADER 节表关键成员

| 字段 | 说明 |
|---|---|
| Name8 | 节名字,最多 8 字节 |
| Misc(VirtualSize) | 内存中节真实大小(联合体) |
| VirtualAddress | RVA,内存起始地址 |
| SizeOfRawData | 磁盘文件中节大小,要文件对齐 |
| PointerToRawData | 节在文件中的 FOA 偏移 |
| Characteristics | 节属性:可读、可写、可执行等 |
合并节完整步骤
<1> 按照内存对齐展开所有节
把每一节做内存对齐:让每一节的SizeOfRawData和VirtualSize都向上对齐到SectionAlignment(内存对齐,一般 0x1000)
目的:让各个节内存页边界整齐,为合并做准备
<2> 修改第一个节的 VirtualSize、SizeOfRawData
- 取最大值:Max = (SizeOfRawData > VirtualSize) ? SizeOfRawData : VirtualSize
- 将第一个节的SizeOfRawData = VirtualSize = Max
- 合并后第一节新的大小:最后一节.VirtualAddress + Max − SizeOfHeaders,结果再向上做内存对齐
其余所有被合并的节,它们的原始数据物理上是连续的,全部归入第一个节
<3> 设置合并后节的权限 Characteristics
把所有要合并的每一节的 Characteristics 值全部做 按位或 |,结果赋值给合并后的第一节
新Characteristics = 节A.Char | 节B.Char | 节C.Char | ......
例如有的节只读、有的节可写,合并之后权限要同时具备读 + 写
<4> 修改 PE 头的节数量
修改IMAGE_FILE_HEADER → NumberOfSections,把节的总数修改为1(全部合并成单节)
<5> 配套必须修改的其他字段 - SizeOfImage:镜像总大小,按内存对齐向上对齐
- 磁盘实体:文件各个节之间不能有间隙,物理数据连续
- 废弃其余旧节表项(NumberOfSections 减小后加载器不再读取后面多余节表)
合并节、扩大节、新增节三者对比
| 技术 | 适用场景 | 难点风险 |
|---|---|---|
| 洞穴注入 | 少量 shellcode,利用现有空白洞穴 | 空间很小,不能放大量代码 |
| 扩大最后一节 | 空白洞穴放不下,不想改动节表数组 | 不能扩中间节;.rsrc不要随便扩 |
| 新增节 | 大段 shellcode,代码独立干净 | 节表后面必须有 36 字节以上空白区,没有就不能直接新增 |
| 合并节 | 节表尾部没有空白,无法新增节 | 步骤复杂,权限处理不当程序直接崩溃,不建议合并资源、重定位节 |
重点踩坑提醒
- 合并节会改变内存布局,重定位、导入表、资源表极易出错,实操尽量优先新增节 / 扩大最后一节,合并节是兜底手段
- 合并之后的Characteristics不能直接照搬某一节,需要把所有被合并节的属性做按位或
- SizeOfImage一定要向上对齐SectionAlignment,否则 PE 加载失败 0xc000007b
- 物理磁盘数据必须连续,只改节表结构体,文件不调整,直接报错
流程总结:没有节表空余空间 → 执行合并节释放节表位置 → 再执行新增节,放入 shellcode