2019年10月2日星期三

动态链接(下)

动态链接(下)

参考《程序员的自我修养》
上一章中我们已经提到了动态链接相比静态链接灵活很多,但是这是以牺牲性能为代价的,而动态链接比静态链接满的主要原因就是因为动态链接下对于全局和静态的数据访问都需要进行复杂的GOT定位,然后间接寻址;对于模块间的调用也要先定位GOT,然后再进行间接跳转,如此,程序运行速度必然会减慢,那么有没有什么优化的方法呢?

延迟绑定实现

在动态链接中,程序模块间包含大量的函数引用(全局变量一般较少,大量的全局变量会导致模块耦合度变大),但显然程序不可能一次性短时使用全部的函数,甚至有些函数可能程序执行完都不会用到(如错误处理函数),所以如果一开始就把所有函数链接好实际上是一种浪费
所以ELF采用了一种叫做延迟绑定的做法,具体方法是通过PLT(procedure Linkable Table)实现的,基本思路就是:函数第一次被用到时才进行绑定(符号查找/重定位等等),所以程序执行时,模块间的调用都没有进行绑定,而是需要用到时才由动态链接器来负责绑定,这种做法可以大大加快程序的启动速度,特别有利于一些有大量函数引用和大量模块的程序。
当我们调用某个外部模块的函数时,通常方式是通过GOT(上篇中讲过)相应的项进行跳转,PLT为了实现延迟绑定,在这个过程中间又增加了一层间接跳转,调用函数并不直接通过GOT跳转,而是通过一个叫做PLT项的结构来进行跳转。每个外部函数在PLT中都对应一个相应的项。PLT方法的实现还依赖于_dll_runtime_resolve()函数(在Glibc中),这个函数需要两个参数,地址绑定发生的模块和函数名(其实是索引),由链接器中的这个函数来完成地址绑定工作
接下来讲讲具体的原理实现(加入在liba.so用到了libc.so中的bar()函数),若我们称bar()在PLT中项的地址为bar@plt,我们来看bar@plt的实现:
bar@plt:
jmp *(bar@GOT)
push n
push moduleID
jump _dll_runtime_resolve
为了实现延迟绑定,链接器并没有一开始就把正确的bar()地址填bar@GOT中,而是填入了指令push n的地址,这个步骤不需要任何查找符号,所以代价很低,这个数字n其实就是bar这个符号引用在重定位表".rel.plt"中的下标,接着再将模块ID压入栈中,调用动态链接器的_dll_runtime_resolve函数来完成符号解析和重定位工作,在_dll_runtime_resolve()完成一系列工作后将bar()的真实地址填入到bar@GOT中
一旦bar这个地址解析完毕,当我们再次调用bar@GOT时,第一条指令就能够跳转到真正的bar()函数中,bar()函数返回的时候会根据堆栈中保存的EIP直接返回到调用者,而不会再继续执行bar@plt的第二条指令开始的那段代码,那段代码只会在符号未被解析时执行一次。多么精巧的构造啊!
上面是PLT的基本原理,PLT真正的结构比它稍微复杂血,ELF将GOT拆分成了两个表".got"和".got.plt"。其中".got"用来保存全局变量的引用地址,".got.plt"用来保存函数引用的地址,另外还有一点特殊之处在于".got.plt"的前三项是有特殊意义的,含义如下:
  • 第一项保存的是".dynamic"段的地址,这个段描述了本模块动态链接的相关信息
  • 第二项保存的是本模块的ID
  • 第三项保存的是_dll_runtime_resolve()的地址
其中,第二项和第三项由动态链接器在装载共享模块时负责将他们初始化,".got.plt"的其他项分别对应每个函数的引用,而实际上为了减少代码的重复,ELF把上面例子中的最后两条指令放在了PLT的第一项,并且规定每一段代码长度是16字节,刚好用来放3条指令,实际的PLT基本结构代码如下:
PLT0:
push *(GOT+4)
jump *(GOT+8)

bar@plt:
jump *(bar@GOT)
push n
jump PLT0
因为PLT本身是一些地址无关代码,所以可以和代码段等一起合并成同一个可读可执行的"Segment"被装载入内存

动态链接相关结构


动态链接的步骤和实现

一.启动动态链接器本身

动态链接器本身也是个共享对象,而这个共享对象有点特殊,1.他本身可以不依赖其他任何共享对象,2.其本身所需要的全局和静态变量的重定位工作由他本身完成。对于第一个任务,我们只需要人为的不使用其他系统库、运行库即可,而对于第二个条件,动态链接器必须在启动时有一段精巧的代码完成这项艰巨的任务而又不能使用任何全局变量和静态变量。这种具有一定限制的启动代码往往被称为"自举"。
动态链接的入口地址即是自举代码的入口,自举代码会首先查找自己的GOT,通过GOT第一项找到.dynamic段的偏移地址,然后通过.dynamic中的信息,自举代码便可以获得动态链接器本身的重定位表和符号表,从而得到动态链接器本身的重定位入口,先将他们重定位。从这一步开始,动态链接器代码中才可以开始使用自己的全局变量和静态变量。(实际上,在动态链接器的自举代码中,甚至不能调用函数,即使是本身的函数,因为当以PIC模式编译共享对象时,对于模块内部函数的调用也是采用跟模块外函数一样的方式,即GOT/PLT方式,所以在GOT/PLT未被重定位前,自举代码不能调用函数,也不能使用任何全局变量)

二. 装载所有需要的共享对象

完成自举后,动态链接器将可执行文件和链接器本身的符号表都合并到一个符号表当中,称为"全局符号表",然后链接器开始在.dynamic段中的DT_NEEDED入口中寻找所有依赖的共享对象,并把它们放入一个集合中,然后从集合中去一个对象,读取ELF头,如果他还有依赖对象,就把该对象也放入集合中,如此循环知道集合为空
如果把依赖关系看作一个图的话,那么这个装载过程就是一个图的遍历的过程,可以使用深度优先算法或者广度优先算法或者是其他顺序,比较常见的是广度优先算法
若出现全局符号介入情况,则后加入的符号会被忽略

三. 重定位和初始化

当上述步骤完成后,链接器开始重新遍历可执行文件和每个共享对象的重定位表,将他们的GOT/PLT中的每个需要重定位的位置进行修正。
重定位完成后,如果某个共享对象又.init段,那么动态链接器会执行.init中的代码,实现共享对象特有的初始化过程,比如最常见的,共享对象中的C++全局/静态对象的构造。如果进程中的可执行文件也有.init段,动态链接器不会执行,因为程序中的.init 和.finit 都是由程序初始化部分代码负责执行

实现

动态链接器本身也是一个可执行程序,_start-->_dl_start-->自举代码-->_dl_start_final-->_dl_sysdep_start-->_dl_main

没有评论:

发表评论

XSS速查表

今天继续XSS的进一步学习,以下内容转载自Freebuf上的文章,原文章来自OWASP的xss备忘录 1.介绍 这篇文章的主要目的是给专业安全测试人员提供一份跨站脚本漏洞检测指南。文章的初始内容是由RSnake提供给 OWASP,内容基于他的XSS备忘录: http://...