标题: 程序员的片段(3)--为什么通过.hash/.gnu.hash定位符号
创建: 2015-07-13 15:40 更新: 链接: http://scz.617.cn/unix/201507131540.txt
决定以一种不那么严谨的风格写写程序员的片段。有些是一般性回忆,有些是带点启 发性的流水帐。什么动机、什么目的呢?啥也没有,就是扯淡。由于不严谨,不是正 经技术文章,文中内容万不可当真,我就那么一写,你就那么一看。
通过Program Header Table[PT_DYNAMIC]可以找到加载到内存的.dynamic,进而找到 加载到内存的某些section,想直观一点,看这个输出:
$ readelf -Wd /lib/i386-linux-gnu/i686/cmov/libc.so.6
Dynamic section at offset 0x1a5da8 contains 26 entries: Tag Type Name/Value 0x00000001 (NEEDED) Shared library: [ld-linux.so.2] 0x0000000e (SONAME) Library soname: [libc.so.6] 0x0000000c (INIT) 0x198c0 0x00000019 (INIT_ARRAY) 0x1a41e8 0x0000001b (INIT_ARRAYSZ) 12 (bytes) 0x00000004 (HASH) 0x1a09b4 // .hash 0x6ffffef5 (GNU_HASH) 0x1b8 // .gnu.hash 0x00000005 (STRTAB) 0xd438 // .dynstr 0x00000006 (SYMTAB) 0x3ec8 // .dynsym 0x0000000a (STRSZ) 23846 (bytes) 0x0000000b (SYMENT) 16 (bytes) 0x00000003 (PLTGOT) 0x1a6000 // .got.plt 0x00000002 (PLTRELSZ) 96 (bytes) 0x00000014 (PLTREL) REL 0x00000017 (JMPREL) 0x172e8 // .rel.plt 0x00000011 (REL) 0x148d8 // .rel.dyn 0x00000012 (RELSZ) 10768 (bytes) 0x00000013 (RELENT) 8 (bytes) 0x6ffffffc (VERDEF) 0x1440c 0x6ffffffd (VERDEFNUM) 33 0x0000001e (FLAGS) STATIC_TLS 0x6ffffffe (VERNEED) 0x14898 0x6fffffff (VERNEEDNUM) 1 0x6ffffff0 (VERSYM) 0x1315e 0x6ffffffa (RELCOUNT) 1253 0x00000000 (NULL) 0x0 // marks the end of .dynamic section
Type列的值,前面多个前缀"DT_",就是ELF规范里的标准定义,比如DT_SYMTAB。有 些人被(SYMTAB)给骗了,以为这里对应.symtab,其实看DT_SYMTAB,就知道对应的是 .dynsym。同理,(STRTAB)对应.dynstr,而不是.strtab。
GOT[0]等于_DYNAMIC,指向内存中的.dynamic。
正经程序通过.hash/.gnu.hash定位符号,可以提升效率。但某些不正经程序也这么 干,意义何在,为什么不直接遍历.dynsym?与boywhp讨论了一番,记录备忘。
首先,所有的讨论都基于Execution View,而不是Linking View。可以简单理解成内 存视图与文件视图的区别。
boywhp提到,.dynsym的项数无法通过内存中的.dynamic得到的,通过.hash/.gnu.hash 定位符号就可以避开这个问题。这算是一个不大不小的理由。
如何确定内存中的.dynsym范围?通过.dynamic可以找到.dynstr。一般而言,内存中 .dynstr紧挨在.dynsym之后,"info files"可以直观看到二者的内存布局。因此,我 们可以确定内存中的.dynsym范围。boywhp找来的一篇文章里也提到这点:
基于Android的ELF PLT/GOT符号重定向过程及ELF Hook实现 - 低端码农 [2014-10-27] http://blog.csdn.net/l173864930/article/details/40507359
如果目标Object有.hash,可以取.hash[1],即nchain:
nchain = sizeof( .dynsym ) / sizeof( Elf32_Sym )
换句话说,这就是.dynsym的项数。
如果目标Object只有.gnu.hash,没法简单确定Hash Values[]的元素个数,为了确定 它实际意味着已经确定dynsymcount,变成鸡蛋问题。但始终可以用.dynstr来界定 .dynsym。
我的观点是,对于不正经程序,直接遍历.dynsym定位符号即可,尤其没必要去理解 .gnu.hash。.gnu.hash就是一高端程序员们的变态实现,不是我们这种常人用的。相 比之下直接遍历.dynsym属于是个人就会的技术,跟AK-47似的。
有些目标Object只有.hash,有些只有.gnu.hash,还有一些两者都有。.hash是过时 的技术,为了保持向后兼容性可能会同时提供两者,如果只提供后者说明太新了,且 不打算向后兼容。
Linux ELF Hook Demo - boywhp [2013-09-04] http://bbs.pediy.com/showthread.php?p=1271913
从上文中我学了两招,一是dl_iterate_phdr()的使用,二是dlopen()返回值的Hacking, 原来那是个"struct link_map *"。
关于"为什么通过.hash/.gnu.hash定位符号"到这儿就没了,后面是一些发散。如果 你看了前面"低端码农"那篇,我们可以往下继续讨论。为方便陈述,称之为A文。
A文关于重定向的讨论,我觉得组织得不太给力。其实x86与ARM在重定向处理的逻辑 层面并无差异,就算以伪代码介绍都无所谓。单就理解重定向逻辑而言,还可以看这 篇:
PLT and GOT, the key to code sharing and dynamic libraries - [2011-05-10] https://www.technovelty.org/linux/plt-and-got-the-key-to-code-sharing-and-dynamic-libraries.html
本篇写得也不好,很多地方没有讲清楚,逻辑上不清晰。但它也有好的一面,就是它 提供了几个最小化的test.c,以及相应的readelf、objdump演示,对ARM上重定向细 节感兴趣的可以用同样的test.c、同样的readelf、objdump操作来加强感性认识。这 种最小化的test.c让我们聚焦于重定位本身。
因为我没有实际在ARM/Linux上调试过什么,后面的讨论是个人基于ELF处理普遍原理 的推测性讨论,如有问题,请指正。
A文中提到:
结果发现GOT[2]居然指向0,这是因为Android不支持RTLD_LAZY,所以当.so被加载时,
linker会把GOT[n](n>=2)对应的函数都提前找出来,因此这里GOT[2]的代码实际上不
会被执行。在目前的Android上,并不存在完整的PLT/GOT链接过程。猜想这主要是出
于稳定性考虑的。
作者是在静态反汇编时注意到GOT[2]等于0。单说x86,dynamic linker在将控制权交 到_start()之前会将GOT[1]、GOT[2]初始化成特定值,GOT[2]实际指向 _dl_runtime_resolve(),而在此之前的静态上下文里,GOT[1]、GOT[2]初值就是0。 这与dlopen()是否支持RTLD_LAZY无关,也与LD_BIND_NOW、-Wl,-z,now无关。
在内存中做ELF Hook时,要注意BIND_NOW情形,.so加载完成后GOT[]所在内存被设成 只读。
Android不支持RTLD_LAZY?我怎么放狗能搜到一堆Android代码在用RTLD_LAZY。是我 搞错了什么吗?
A文中提到:
IDA和objdump对于PLT/GOT的反汇编结果有些差别
反汇编.got.plt section时,IDA有特殊处理。看Segments(Shift-F7),在最后一行 多了一个extern,实际是不存在的,是IDA歪歪出来的,它只是方便IDA在.got.plt中 填写一些可读性较好的符号,再直白点,IDA"假装"完成了GOT[]的重定向处理。再看 .plt,也被特殊处理,比如PLT[i]的第二条、第三条指令都被抹去了,原因仍然是 IDA"假装"完成了GOT[]的重定向处理。如果切到"Hex View-1"窗口查看.plt,很多地 址显示??,而实际上这些地址在静态文件中有对应数据。不知这算是IDA的NB之处呢 还是留给我们的坑?
A文中还引了:
Redirecting functions in shared ELF libraries - Anthony Shoumikhin [2013-07-25] http://www.codeproject.com/Articles/70302/Redirecting-functions-in-shared-ELF-libraries
该文也是在内存中进行ELF Hook,不过它借助静态文件中的Section Header Table[]。 从这个意义上说,A文说它是基于链接视图对ELF进行解析也不算错。该文定位内存中 的.rel.plt,遍历.rel.plt找到待修改的GOT[i]。.rel.plt[i]的r_offset就是用于 定位GOT[j]的。该文作者对ELF的理解有不少问题,其水平与Silvio Cesare不是一个 层次。
下面应该是A文作者的个人主页:
http://www.im-boy.net http://blog.csdn.net/L173864930/
第一个应该是永久域名,第二个是重定向过来的。
A文作者折腾了很多Android相关的东西,感兴趣的可以遍历一下。他在github上有个 针对Android的AllHookInOne项目:
https://github.com/boyliang/AllHookInOne
关于A文到这儿就没了,后面是一些非技术的发散。
我看到Ali Bahrami的文章,觉得很棒,微博推荐了一下:
GNU Hash ELF Sections - Ali Bahrami [email protected] [2008-10-21] https://blogs.oracle.com/ali/entry/gnu_hash_elf_sections (介绍.gnu.hash最好的文章,没有之一。成文时作者尚属Sun公司)
http://weibo.com/1273725432/CqEUFvRpq
然后引出与boywhp的讨论,进而从boywhp的源码里学了两个新招。通过boywhp找到A 文作者的个人主页及GITHUB上的项目。对此次讨论进行记录备忘时又查出我以前写的 文档中的几处BUG。还有一点,我突然意识到,手头这个样本有可能也在做ELF Hook, 它的种种对抗分析与调试的手段好像都在试图掩盖这个可能性,而这种可能性在我与 boywhp讨论之前从未去想过。
这就是讨论、交流及总结的最直观好处。
很多年前一群Unix程序员在APUE小站上也曾进行过很多这种讨论。比如我翻译 《RFC 1928意译版(非直译版)》时,就曾与knightmare、shixudong进行过很有意义 的讨论,并在最终成文时将两次讨论附于其中。
最后是个八卦,我写信给Ali Bahrami,提到文中有个URL应该从sun修正成oracle, 顺便说我恨Oralce、喜欢Sun,他回信中有这么一句:
Sun was one of a kind, and we all miss it.