ELF字符串表(.strtab/.dynstr)是以\0分隔的连续C字符串集合,无长度字段;直接fopen+fseek修改会破坏后续字符串偏移,因其他节中引用的是固定offset而非指针,导致符号解析失败或ELF解析崩溃。

什么是ELF字符串表,为什么不能直接用 fopen + fseek 修改
ELF 文件中的字符串表(如 .strtab 或 .dynstr)是连续的、以 \0 分隔的 C 风格字符串集合,本身不带长度字段。直接在原地增删字符串会破坏后续所有字符串的偏移——因为其他节(.symtab、.dynamic 等)里保存的都是指向该表的 offset,不是指针。一旦你把某个字符串变长,后面所有 offset 就全错,链接器或加载器读取时大概率崩溃或符号解析失败。
所以「修改字符串表」本质上是:定位目标字符串 → 计算其在字符串表中的 offset → 判断是否有足够空间容纳新内容 → 若无,则需重分配字符串表并更新所有引用它的节头和段头 → 最后重写整个 ELF 文件。
- 常见错误现象:
readelf -s显示符号名乱码、ldd报invalid ELF header、objdump解析失败 - 关键约束:字符串表本身不可扩容(除非重排节区),且必须保持 NUL 终止、无空洞
- 真正安全的做法是:只替换等长字符串(如 patch 一个库名
"libc.so.6"→"libc_patched.so.6"不行,但 →"libc_mod.so.6"可能可行)
如何定位并读取 .strtab 或 .dynstr 的内容
需手动解析 ELF 头和节头表(Section Header Table),不能依赖 libelf(除非你明确控制其行为)。核心步骤:读 ELF header → 找到 e_shoff(节头表起始偏移)→ 读 e_shnum 个 Elf64_Shdr → 遍历找 sh_name 对应节名字符串表(即 .shstrtab)→ 再用它解出 .strtab / .dynstr 的 sh_offset 和 sh_size。
注意:sh_name 是索引,要先读 .shstrtab 节内容才能知道某节叫什么;而 .shstrtab 自身的名称又存储在它自己里面——这是 ELF 的自指结构,必须按顺序解析。
- 使用场景:你想 patch 某个符号名(比如把
"malloc"改成"my_malloc"),就得先从.symtab找到该符号的st_name字段,再用它查.strtab - 参数差异:
.strtab存静态符号名,.dynstr存动态符号名(DT_SYMTAB引用它),二者结构相同但用途隔离,不能混用 - 示例关键代码片段(伪):
uint64_t strtab_off = shdr[strtab_idx].sh_offset; uint64_t strtab_sz = shdr[strtab_idx].sh_size; std::vector<char> strtab_data(strtab_sz); fseek(f, strtab_off, SEEK_SET); fread(strtab_data.data(), 1, strtab_sz, f); // 然后遍历找 "\0" 分隔的字符串 </char>
修改字符串时,为什么必须检查 sh_addralign == 1 和空字符填充
ELF 规范要求字符串表节( ELF 规范要求字符串表节( ELF 规范要求字符串表节(SHT_STRTAB)的对齐值 sh_addralign 必须为 1,意味着它可从任意字节地址开始,且内容就是纯字节流。但这不等于你可以随意插入 —— 字符串之间必须严格用单个 分隔,结尾也必须是 SHT_STRTAB)的对齐值 sh_addralign 必须为 1,意味着它可从任意字节地址开始,且内容就是纯字节流。但这不等于你可以随意插入 —— 字符串之间必须严格用单个 \0 分隔,结尾也必须是 \0,中间不能有额外空字符(否则 readelf 会截断或跳过)。,中间不能有额外空字符(否则 SHT_STRTAB)的对齐值 sh_addralign 必须为 1,意味着它可从任意字节地址开始,且内容就是纯字节流。但这不等于你可以随意插入 —— 字符串之间必须严格用单个 \0 分隔,结尾也必须是 \0,中间不能有额外空字符(否则 readelf 会截断或跳过)。readelf 会截断或跳过)。
更隐蔽的问题是:某些工具(如 strip)会在字符串表末尾补零到页对齐,而你的修改若忽略这点,会导致文件大小变化后,后续节的磁盘位置错位。
- 容易踩的坑:用
std::string::c_str()直接 memcpy 进去,结果忘了末尾多了一个隐式\0,导致覆盖下一个字符串首字节 - 正确做法:确认目标字符串旧长度(含末尾
\0),新字符串长度(含末尾\0),仅当两者相等时才原地覆写;否则必须走重分配流程 - 性能影响:重分配字符串表意味着要更新所有引用它的节头(
.symtab、.rela.dyn等的sh_link)、可能还要改程序头(PT_DYNAMIC中的DT_STRTAB),I/O 开销大且易出错
有没有更稳妥的替代方案?
直接手改 ELF 字符串表属于高危操作,几乎没有生产级工具这么做。真实工程中更常用的是:
- 用
patchelf --set-interpreter或--replace-needed:它内部做了完整的节重排和重定位,比你自己实现可靠得多 - 编译时控制:用
-Wl,--dynamic-list或version script控制导出符号名,避免后期 patch - 运行时劫持:通过
LD_PRELOAD或修改.dynamic中的DT_NEEDED来替换依赖库,不动字符串表本身 - 若真需改符号名,优先考虑
objcopy --localize-symbol或--globalize-symbol,它们调用的是 binutils 底层稳定接口
真正底层修改 ELF 字符串表,往往只出现在 binary instrumentation、CTF 逆向或定制 loader 场景里,且必须伴随完整的节头/程序头校验和重写逻辑。漏掉任何一个 sh_offset 或 sh_size 的同步更新,文件就废了。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











