读取ELF文件头必须先验证魔数“\x7fELF”、EI_CLASS(64位)、EI_DATA(字节序),再安全解析;修改e_entry需确保其落在PT_LOAD段虚拟地址范围内,并整块覆写64字节头部,最后调用fsync。

读取 ELF 文件头必须先验证魔数和架构
直接用 fread 读 sizeof(Elf64_Ehdr) 不等于拿到有效头——很多“ELF 文件”其实是脚本、损坏文件或非目标架构(比如在 x86_64 上打开 arm64 的 ELF)。不检查就往下读,e_type、e_machine 等字段全是垃圾值。
- 先读前 4 字节,确认是
"\x7fELF"(即ehdr.e_ident[EI_MAG0]到[EI_MAG3]) - 检查
ehdr.e_ident[EI_CLASS]:值为ELFCLASS64才能当Elf64_Ehdr*解析,否则可能是 32 位 - 检查
ehdr.e_ident[EI_DATA]:ELFDATA2LSB或ELFDATA2MSB决定字节序,硬按本地序 reinterpret_cast 会错读e_entry、e_phoff等字段
修改 e_entry 后必须重写整个 ELF 头,不能只改偏移处的 8 字节
ELF 头本身是固定长度(sizeof(Elf64_Ehdr) == 64),但它的位置不一定在文件开头之后立刻开始——虽然绝大多数情况下是,可 e_ident[EI_PAD] 区域可能被工具填充,且某些链接器(如 gold)会在头前塞注释段。更关键的是:e_entry 是程序入口虚拟地址,不是文件偏移;你改的是内存布局概念,但文件里它只是头中一个字段。
- 用
lseek(fd, 0, SEEK_SET)定位到文件起始,再read()全部头数据到缓冲区 - 修改缓冲区里的
ehdr.e_entry字段(注意字节序!) - 用
lseek(fd, 0, SEEK_SET)回到开头,write(fd, &ehdr, sizeof(ehdr))整块覆写——只写 8 字节大概率破坏e_phnum或e_shnum,导致readelf报Bad value - 改完后记得
fsync(fd),否则chmod +x后执行仍走旧入口
修改后 ELF 可能无法加载:内核校验和动态链接器不买账
Linux 内核在 load_elf_binary() 中会检查 e_entry 是否落在某个 PT_LOAD 段的 p_vaddr..p_vaddr+p_memsz 范围内;glibc 的 _dl_start() 也会验证程序头表合法性。你把 e_entry 改成一个没映射的地址(比如 0x1000),execve() 直接返回 -ENOEXEC,错误信息就是 Exec format error,不提示具体哪错了。
- 用
readelf -l your_bin查看所有LOAD段的VirtAddr和MemSize,确保新e_entry落在其中一段内 - 不要改
e_phoff、e_phnum这类描述程序头表位置/数量的字段——除非你同时重写了整个程序头表,否则动态链接器找不到PT_INTERP就放弃加载 -
strip过的二进制可能删掉了.interp段,此时它变成静态链接,e_entry必须指向实际代码段起始,否则 SIGSEGV
用 libelf 比裸 read/write 更安全,但要注意版本和符号可见性
手撸解析容易漏掉对齐、扩展字段或新版 ELF 标准(如 ET_LOOS 类型),libelf 封装了这些细节。但它默认不导出写入接口——elf_update(elf, ELF_C_WRITE) 在较老系统(如 CentOS 7 自带 elfutils 0.160)中仅支持只读模式,调用后返回 ELF_C_NULL,且不报错。
- 编译时加
-lelf,运行时确保libelf.so.1版本 ≥ 0.168(Ubuntu 20.04+ / Fedora 32+ 默认满足) - 必须用
elf_version(EV_CURRENT)初始化,否则elf64_getehdr()返回NULL - 修改
e_entry后,要调用gelf_update_ehdr(elf, &ehdr),再elf_update(elf, ELF_C_WRITE),缺一不可 - 别依赖
libelf自动处理段对齐——它不会帮你把新代码段塞进文件末尾并更新e_shoff,那属于重链接范畴,得用objcopy或ld
改 ELF 头看着只是几个字段的事,但每个字段背后都绑着内核加载逻辑、动态链接器协议和 mmap 布局规则。最常被忽略的是:你改的不是“一个数字”,而是整个程序生命周期的起点坐标。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











