ELF文件分析(一) 文件头和程序头表
0x00 参考
- 资料参考
- 《Linux二进制分析》作者:Ryan O’Neill (好书拜读ing)
- elf的一些宏定义,主要是参考一些解析elf格式的软件的头文件,glibc的elf.h可以跳转到 github 上查看,文章中不会特别把宏的数值展示出来,只会指出某个值对应的宏是什么,因此你可以在这个头文件中查找和学习。
- elf man手册,网页查看,简单的翻译版文件放在蓝奏云上,密码是dkq3
- 工具参考
readelf 和 objdump
1
2
3# readelf和objdump在binutils中
sudo apt update && sudo apt upgrade -y
sudo apt install binutilsimhex/010editor
推荐imhex,主要是现在换成双系统(linux Mint为主力)了,而 imhex 开源免费,此外功能更丰富
安装参考官网:https://github.com/WerWolv/ImHex/blob/master/INSTALL.md
1
2
3
4
5# 先到官网上获取deb文件
# 依赖和deb包下载
sudo apt update && sudo apt upgrade -y
sudo apt install libglfw3 libmbedtls14
sudo apt install ./imhex*.deb
0x01 内容
ELF文件规范有非常丰富的内容,我们主要关注下面这些:
- ELF文件头
- ELF程序头(段信息)
- ELF节头(节信息)
- ELF节
- ELF符号
- ELF重定位
- ELF动态链接
下面是关于64位ELF的一个简单的文件布局,可以对大体位置先留个印象。
0x02 ELF文件头解析
1 | # file文件类型 |
根据 man elf 查询ELF的离线手册,文件头的定义如下:
1 |
|
其中要注意的是:
- e_type 标记文件类别, 有下面这几种
ET_NONE类型未确定ET_REL可重定位文件,有时也称目标文件ET_EXEC可执行文件,又称为程序ET_DYN共享目标文件,又称共享库ET_CORE核心文件,程序崩溃或进程传递 SIGSEGV 信号产生的转储文件,用于调试找错
note
上面的描述不完全。特别的,对于位置无关可执行文件(PIE) , 其也被标记为了ET_DYN, 由于有入口点 _start, 因此可以执行,如下面的实例所示。
- e_ident 这个部分可以继续划分,前4个字节为魔数
0x7F 0x45 0x4C 0x46,而第5个字节EI_CLASS标识了文件的位数,如果是01代表是32位,如果是02则是64位,而对于00这种情况一般代表无效,后续无效值不讨论,只考虑能让加载器正常工作的情形。第六个字节EI_DATA标识文件的字节序(大小端),如果是01代表是小端序,如果是02则是大端序。
info
32位的ELF文件和64位的文件的 e_ehsize, e_phentsize, e_shentsize是不同的, 正常情况下:
- 32位:e_ehsize=52 字节 ; e_phentsize=32 字节 ; e_shentsize=40 字节
- 64位:e_ehsize=64 字节 ; e_phentsize=56 字节 ; e_shentsize=64 字节
- e_machine 机器架构,这个与运行加载相关,机器架构不支持就无法执行
- e_entry 程序入口点,加载器最后将 CPU 的控制权转移到这个地址,从而开始执行程序的代码。
info
对于可执行文件,e_entry一般是直接指向程序真正的第一条指令,通常是C语言运行库(CRT)的一部分,一般是
_start
函数,这一点对静态链接的或者是动态链接的可执行文件都适用。在动态链接的情况下,内核会先启动
PT_INTERP 段指定的动态链接器,由它完成准备工作后,再跳转到
e_entry 指向的 _start 函数,开始执行程序。
- e_phoff 和 e_shoff 分别是程序头偏移量和节头偏移量, 一般 offset 指的是文件偏移,而 address指的是加载到内存后的虚拟地址。
实例
0x03 ELF程序头
1 | # readelf解析 -l 程序头表 |
程序头表项的结构如下,程序头表是一个有程序头表项组成的数组
1 | typedef struct { |
p_type 程序段的类型,具体有下面几种
PT_NULL未定义未使用,成员值未定义,被忽略
PT_LOAD可加载数组元素指定一个可加载段, 由 p_filesz 和 p_memsz 描述。文件中的字节被映射到内存段的开头。 如果段的内存大小 p_memsz 大于或等于文件大小 p_filesz,如果大于则额外字节填充为0, 紧随段的初始化区域。程序头表中可加载段的条目,按 p_vaddr 大小升序排列。
note
一个可执行文件至少存在一个可加载的段 一般程序有 text 和 data 这两个可加载段(注意,不是.text和.data节)
PT_DYNAMIC动态链接动态链接可执行文件特有的,包含但不限于:
- 运行时所需的共享库列表
- 全局偏移表地址(GOT)
- 重定向条目信息
PT_INTERP解释器只将位置和大小信息存放在一个以 null 结尾的字符串中,描述解释器位置
如
/lib64/ld-linux-x86-64.so.2PT_NOTE注释包含特定供应商或者系统相关的附加信息
PT_PHDR程序头保存了程序头表本身的位置和大小,该段只能出现一次,且程序头表是程序内存映像的一部分才能出现。如果出现,在所有可加载段之前。
note
这个地方有点意思,在程序头记录的数据中,把程序头看作数据也当作了一个段。 可以看下面的实例,其中PHDR段的文件偏移是0x40, 这不就是我们的程序头表嘛!
PT_LOPROC, PT_LOPROC这是一个供处理器特定语义使用的范围PT_GNU_STACKGNU拓展
p_flags 段的权限标识
类似与linux的权限管理机制,通过掩码进行组合
PF_R读权限PF_W写权限PF_X执行权限
实例
下面是 imhex 中的,我选择第一段方便查看。
对于具体字段的情况,可以通过 imhex 的模式数据的地方展开看到,就不进行数值上的介绍了。
0x04 关于程序头表、程序头表项和段
- 程序头表(或者简称程序头)是一个由多个程序头表项 组成的数组。
- “每个程序头表项描述了数据在内存中的安排,它定义了一个段——也就是程序执行所必需的一块内存区域。
- 程序头表的本质是运行时内存布局的“装载地图”。它告诉操作系统加载器需要将文件中哪些部分的数据(段)加载到内存的什么位置,并赋予它们何种权限(如只读、可执行等),为程序的最终执行做好准备。
0x05 小问题
关于文件头
- 在十六进制的输出中找到 入口点地址,程序头表地址和节头表地址
- 如何确定位数和大小段?
关于程序段
- 分析内存中段和文件中段的情况,它们是连续的还是不连续的?
- 如何确定段的权限,我可以随意修改他们吗?
- p_filesz不等于p_memsz的情况有哪些?需要用到后面节的相关知识。
- 文件修补(patch)中,我们需要修改某个地址(虚拟地址)的值,根据程序段的知识,我们应该如何知道其在文件中的位置(文件偏移)?