继续讲ELF文件的详细格式,还是看下面的图:
ELF文件由以下几个部分组成:
(1) ELF header
(2) Section Header Table/Program Header Table
(3) Sections/Segments
ELF Header
ELF Header在文件最开始处,长度固定为52个字节。详细定义如下图:
其中e_ident是一个16字节的数组,内容固定为7F 45 4C 46(即字符串ELF),这部分的作用就是表示这是一个ELF格式的文件。e_type表示文件的类型,值为1时表示可重定位文件,2表示可执行我文件,3表示共享目标文件。Android系统中的so文件,e_type=3。e_machine和e_version表示文件的CPU体系结构和版本。e_entry表示程序的入口地址,so文件没有入口地址,此处为0。e_phoff表示Program Header Table在文件中的偏移量。e_shoff表示Section Header Table在文件中的偏移量。e_phentsizze表示 Program Header Table的大小。e_phnum表示Program Header的数量。同理,e_shentsize和e_shnum表示Section Header Table的大小和Section Header的数量。e_shstrndx表示Section名称字符串表的索引,这个估计不好理解,没关系,后文会举例说明。
上段介绍的内容,看一眼就算了,具体的格式细节并不重要,理解其含义才是有必要的。组成ELF文件(so文件)的真正内容,是各个Section,每个Section有一个Header,ELF格式把所有的Header排列在一起存放,这就是Section Header Table。同时,几个Section可以组成一个Segment,每个Segment也都有一个Header,这些Header也是排列在一起存放,就是Program Header Table。ELF的header,除了标记是ELF格式外,就是记录这两个Table在什么地方,有多少个Segment/Sectioin,上面的一切详细定义,都是为了这个服务的。
以Android 7.0中的libmedia.so为例,通过readelf命令查看其ELF Header:
如图,Program header table偏移位置为52,Section Header Table偏移位置为848376。
Section Header Table
Section Header Table是把多个Section Header按顺序排列到一起,在文件中的位置和数量由ELF header指定,通过ELF Header可快速定位Section Header。
详细定义如图:
每个Section Header占40个字节,sh_name表示名称,此处只有4个字节,存储不下字符串,实际存放的是在字符串Section中的索引。sh_type表示类型,可以有很多种类型,甚至可以自定义类型,每个类型的Section都有其自己的格式。后续的内容,指定了Section在文件中的偏移位置、大小等信息。
同样,可以用readelf -S 命令,查看Section Header。
如图,libmedia.so共有29个Section Header,类型有STRTAB、DYNSYM、PROGBITS等。
Section
每个类型的Section都有其独有的格式,这里介绍几种。
STRTAB
字符串Section。
这种类型的格式最简单,其实就是把字符串紧密的排列到一起,以0分隔字符串。其中第一个字符串为NULL,第二个为.shstrtab等待。
在上面介绍Section Header时,图中最后一个Section Header的sh_name值为1,这个1就是在名为shstrtab的字符串section中的索引,即shstrtab。
同样,第5个Section,名为.dynstr的Section,类型也为STRTAB。其文件地址为0xCF70,实际内容为:
可见,这里面保存的是程序中用到的各个字符串,包括函数名称、字符串常量等。这个.dynstr很重要,后续还会用到。
符号表(Symbol Table)Section
我们最常用的就是符号表了,如上面介绍Section Header的图,其中.dynsym Section,类型DYNSYM,就是符号表。符号表,也是有一些符号紧密排列组成,符号的格式如下:
每个符号固定占用16个字节,因此,根据符号表的大小,除以16就是其中符号的数量。
其中st_name表示名称,当然这也是在字符串Section(.dynstr)中的索引。
st_info比较复杂,其高4位表示类型,类型有STT_OBJECT、STT_FUNC、STT_SECTION等几种,其中STT_FUNC才表示这是一个函数符号,低4位表示类型的绑定信息,取值有局部符号、全局符号、弱符号几种。
当为函数符号时,st_shndx表示函数的定义在那个Section中,st_value表示在文件中的具体偏移地址。
Program Header Table
与上面讲的Section Header Table一样,Program Header Table也是将一些Segment Header顺序 排放到一起,详细定义如下:
不具体解释每个字段了,其作用也是存放各个Segment的信息,并指向各自的Segment。
用readelf -l 命令查看libmedia.so:
Program Header,是用于执行的。其中需要注意,名为LOAD的Header,其虚拟地址VirtAddr可能不为0,在将so映射到内存时,地址会加上VirtAddr的值。
实践
ELF文件详细格式基本都介绍完了,看了这些有什么用么?
用处是可以帮你更好的理解Hook过程,并定位其中出现的问题。
一般来讲,native hook大致过程如下:
dlopen("libmedia.so") 通过dlopen打开一个so文件
dlsym("fun_name") 获得一个函数的地址。
有了地址以后,就可以替换入口,实现hook了。
其中,重要的是dlsym函数,即通过函数名找到其对应的地址。dlsym功能就是在符号表中查找函数。
明白了ELF格式,就能知道dlsym内部究竟做了什么。虽然有现成的系统函数,但是了解其机制和原理,对深入理解问题是很有帮助的,另外,在Android 7.0上,dlopen和dlsym也不能用于native hook,具体原因不在这里分析。下面通过hook系统libmedia.so中的_ZN7android11AudioRecord4readEPvjb函数为例,说明具体的过程。
打开libmedia.so文件,读取文件头部的ELF Header。
从ELF Header中定位到Program Header Table,查找其中类型为LOAD的Header,看其VirtAddr内容,记录最小的VirtAddr内容。本例中,VirtAddr=0x1d000。
从ELF Header中定位到Section Header Table,并通过ELF Header查看.shstrtab在Section Header Table中的索引,这个.shstrtab保存了各个Section Header的名称。
在Section Header Table中,找到符号表(.dynsym)和字符串(.dynstr)两个Section。
遍历符号表中的每个符号,符号中的st_name为在.dynstr中的索引,跟进这个可以找到符号名称。同时,需要跟进st_info过滤掉类型不是函数的符号。
找到与指定函数名称匹配的符号。st_value为函数的偏移地址,本例中,偏移为0x81DF1,这个偏移是自动加上VirtAddr的结果,这样方便加载程序,但我们还需要减去VirtAddr才能找到函数的定义。结果为:0x64DF1。
验证
使用objdump命令,反编译libmedia.so,找到_ZN7android11AudioRecord4readEPvjb函数:
以二进制打开libmedia.so文件,定位到0x64DF0位置:
如图,验证0x64DF0处的内容就是函数_ZN7android11AudioRecord4readEPvjb的定义(指令集)。
有了函数地址,再Hook就非常简单了。
参考:
1 官方文档《ELF_Format》
2 《ELF文件系统格式》
领取专属 10元无门槛券
私享最新 技术干货