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