如何用C++操作PE二进制文件的节地址表并添加只读新节
给PE文件添加只读可访问新节(直接操作字节实现)
既然你已经用libpeconv加载了PE二进制数据并在EOF添加了节内容,接下来核心是手动修改PE结构的关键字段——节表项、节数量,以及按需调整节对齐和节属性,以下是具体步骤和代码实现:
核心步骤
定位PE关键结构
通过libpeconv的get_nt_headers拿到NT头,从中获取文件头(IMAGE_FILE_HEADER)和可选头(IMAGE_OPTIONAL_HEADER),同时找到最后一个节的表头,用于计算新节的虚拟地址和文件偏移。计算新节的参数
- 虚拟地址(VirtualAddress):最后一个节的
VirtualAddress+Misc.VirtualSize,然后对齐到可选头中的SectionAlignment值。 - 文件偏移(PointerToRawData):直接用原始PE数据的总大小(因为新节加在EOF)。
- 文件大小(SizeOfRawData):新节的实际数据大小对齐到可选头中的
FileAlignment值,不足部分填0。 - 节属性(Characteristics):设置为
IMAGE_SCN_CNT_INITIALIZED_DATA | IMAGE_SCN_MEM_READ,对应只读的初始化数据,无执行权限。
- 虚拟地址(VirtualAddress):最后一个节的
写入新节表项
节表紧跟在NT头之后,每个节表项是40字节的IMAGE_SECTION_HEADER。找到最后一个节表项的位置,往后写入构造好的新节表项。更新节数量
修改文件头中的NumberOfSections字段,加1。(可选)修改节对齐值
如果需要调整SectionAlignment,直接修改可选头中的对应字段即可——因为libpeconv没提供封装接口,直接操作内存中的结构体字段就行(Windows是小端系统,结构体赋值自动对应字节序)。
代码实现(C++)
#include <libpeconv.h> #include <cstring> #include <cstdlib> bool add_readonly_section(unsigned char* &pe_data, size_t &pe_size, const unsigned char* sec_content, size_t content_size) { IMAGE_NT_HEADERS* nt_hdrs = libpeconv::get_nt_headers(pe_data); if (!nt_hdrs) return false; IMAGE_FILE_HEADER* file_hdr = &nt_hdrs->FileHeader; IMAGE_OPTIONAL_HEADER* opt_hdr = &nt_hdrs->OptionalHeader; // 获取最后一个节的表头 const size_t last_sec_idx = file_hdr->NumberOfSections - 1; IMAGE_SECTION_HEADER* last_sec = libpeconv::get_section_hdr(pe_data, last_sec_idx); if (!last_sec) return false; // 计算新节的虚拟地址和文件参数 const DWORD new_va = libpeconv::align_up(last_sec->VirtualAddress + last_sec->Misc.VirtualSize, opt_hdr->SectionAlignment); const DWORD raw_ptr = static_cast<DWORD>(pe_size); const DWORD raw_size = static_cast<DWORD>(libpeconv::align_up(content_size, opt_hdr->FileAlignment)); // 扩展PE内存块,添加新节内容 const size_t new_pe_size = pe_size + raw_size; unsigned char* new_pe = static_cast<unsigned char*>(realloc(pe_data, new_pe_size)); if (!new_pe) return false; memcpy(new_pe + pe_size, sec_content, content_size); memset(new_pe + pe_size + content_size, 0, raw_size - content_size); // 填充对齐的空白为0 // 构造新节表项 IMAGE_SECTION_HEADER new_sec{}; memcpy(new_sec.Name, ".mysec", 6); // 节名最多8字节,自动补0 new_sec.Misc.VirtualSize = static_cast<DWORD>(content_size); new_sec.VirtualAddress = new_va; new_sec.SizeOfRawData = raw_size; new_sec.PointerToRawData = raw_ptr; // 设置只读属性:初始化数据 + 只读内存 new_sec.Characteristics = IMAGE_SCN_CNT_INITIALIZED_DATA | IMAGE_SCN_MEM_READ; // 写入新节表项到节表末尾 const size_t sec_table_off = libpeconv::get_sections_table_offset(nt_hdrs); const size_t new_sec_off = sec_table_off + (file_hdr->NumberOfSections * sizeof(IMAGE_SECTION_HEADER)); memcpy(new_pe + new_sec_off, &new_sec, sizeof(IMAGE_SECTION_HEADER)); // 更新节数量 file_hdr->NumberOfSections += 1; // (可选)修改节对齐值,示例改为0x2000 // opt_hdr->SectionAlignment = 0x2000; // 更新传入的PE数据指针和大小 pe_data = new_pe; pe_size = new_pe_size; return true; }
注意事项
- 操作后记得用PE验证工具(比如PEView)或者libpeconv的
is_valid_pe函数检查PE的合法性,避免结构错误导致程序崩溃。 - 节名长度不能超过8字节,否则会被PE加载器截断;如果需要长节名,需要用PE的扩展节名机制,但一般场景不需要。
- 确保
realloc后的内存块有效,操作完成后要正确释放(如果不再使用)。 - 所有PE结构字段都是小端字节序,在Windows平台直接操作结构体即可,跨平台的话需要手动转换字节序。
内容的提问来源于stack exchange,提问作者Leonyya
相关产品推荐
相关产品推荐

