作者Silvio Cesare
编译nixen
摘要:本文讨论了一个修改ELF文件实现共享库调用重定向的方法修改可执行文件的程序连接表(Procedure Linkage Table)可以使被感染的文件调用外部的函数这要比修改LD_PRELOAD环境变量实现调用的重定向优越的多首先不牵扯到环境变量的修改其次是更为隐蔽本文将提供一个基于x/Linux的实现
前言
这是nixen搜集的一组有关Linux系统下病毒的研究文章没有先后的次序文中的代码可能有破坏性只能用于研究如果你用于非法目的后果自负
感染ELF文件程序连接表实现共享库调用的重定向
简介
本文讨论了一个修改ELF文件实现共享库调用重定向的方法修改可执行文件的程序连接表(Procedure Linkage Table)可以使被感染的文件调用外部的函数这要比修改LD_PRELOAD环境变量实现调用的重定向优越的多首先不牵扯到环境变量的修改其次是更为隐蔽本文将提供一个基于x/Linux的实现如果你对UNIX系统病毒比较感兴趣请参考以下网址
(UNIX病毒邮件列表)
~silvio (作者主页)
程序连接表(Procedure Linkage Table)
下面是ELF规范中关于程序连接表的叙述
程序连接表(PLT)
在ELF文件中全局偏移表(Global Offset TableGOT)能够把位置无关的地址定位到绝对地址程序连接表也有类似的作用它能够把位置无关的函数调用定向到绝对地址连接编辑器(link editor)不能解决程序从一个可执行文件或者共享库目标到另外一个的执行转移结果连接编辑器只能把包含程序转移控制的一些入口安排到程序连接表(PLT)中在system V体系中程序连接表位于共享正文中但是它们使用私有全局偏移表(private global offset table)中的地址动态连接器(例如ldso)会决定目标的绝对地址并且修改全局偏移表在内存中的影象因而动态连接器能够重定向这些入口而勿需破坏程序正文的位置无关性和共享特性可执行文件和共享目标文件有各自的程序连接表
表使用绝对地址的程序连接表
PLT:pushl got_plus_
jmp *got_plus_
nop; nop
nop; nop
PLT:jmp *name_in_GOT
pushl $offset
jmp PLT@PC
PLT:jmp *name_in_GOT
pushl $offset
jmp PLT@PC
表位置无关的程序连接表
PLT:pushl (%ebx)
jmp *(%ebx)
nop; nop
nop; nop
PLT:jmp *name@GOT(%ebx)
pushl $offset
jmp PLT@PC
PLT:jmp *name@GOT(%ebx)
pushl $offset
jmp PLT@PC
注意从两个表中可以看出两种方式的指令使用不同的操作数寻址模式但是它们和动态连接器的接口是一样的
下一步动态连接器和程序本身使用程序连接表和全局偏移表共同解析符号引用
当第一次建立程序的内存影象时动态连接器会把全局偏移表的第二和第三个入口设置为特定的值下面会对这些值进行介绍
如果程序连接表是位置无关的需要把全局偏移表地址保存在%ebx中进程影象中的每个共享目标文件都有自己的程序连接表而且程序的执行流程改变时也只能跳转到同一个目标文件的程序连接表入口例如一个程序foo它的动态连接库为barso它们都有自己程序连接表那么foo正文段调用某个程序连接表入口时只能跳转到foo文件自己的程序连接表而不能转到barso的程序连接表中因此在调用程序连接表入口之前函数调用代码应该设置全局偏移表的基址寄存器
为了便于描述我们假设程序要调用另一个目标文件的函数name因此首先需要把程序执行控制权转移到标记为PLT的代码处
这段代码的第一条指令就是跳转到name在全局偏移表的入口地址因为name是在另一个目标文件中的调用所以在初始化时全局偏移表没有保存name的真实地址而只是保存了这段代码第二条指令pushl的地址
因而程序会接着执行第二条指令在栈压入一个重新定位的偏移值(offset)这个重新定位的偏移值是重定位表中的一个位的非负字节偏移值这个特指的重定位入口是R__JMP_SLOT类型的它的偏移值将指定先前jmp指令用到的全局偏移表的入口重定位入口还有一个符号表索引告诉动态连接器哪个符号被引用在这个例子中是name
在栈中压入重定位偏移值以后程序接着就跳转到PLT它是程序连接表的第一个入口pushl指令在栈中压入第二个全局偏移表的入口(got_plus_或者(%ebx))从而给动态连接器一个单字识别信息程序接着跳转到全局偏移表的第三个入口中的地址(got_plus_或者(%ebx))将控制权转移给动态连接器
当动态连接器获得控制权它就会展开栈读出指定的重定位入口找出符号表的值把name的真正地址保存到全局偏移表的name入口中然后将控制权转移给目的目标
因此如果再次调用name就会直接从程序连接表入口转移到name而不必再次调用动态连接器也就是说PLT的jmp指令将转移到name而不是接着执行push指令
LD_BIND_NOW环境变量能够改变动态连接行为如果这个环境变量不为空动态连接器在把控制权交给程序之前会先为程序连接表赋值也就是说在进程初始化期间动态连接器为R__JMP_SLOT类型的重定位入口赋值以便在第一次调用时不必通过动态连接器就能够跳转到目标地址反之如果这个环境变量为空动态连接器就暂不为程序连接表入口赋值不对符号进行解析和重定位直到第一次调用一个程序连接表入口才对其做相应的处理这种方式叫作后期连接(lazy binding)方式
注意后期连接(lazy binding)方式一般会大大提高应用程序的性能因为不必为解析无用的符号浪费动态连接器的开销不过有两种情况例外第一对一个共享目标函数进行初始化处理花费的时间比调用正式的执行时间长因为动态连接器会拦截调用以解析符号而这个函数功能又比较简单第二如果发生错误和动态连接器无法解析符号动态连接器就会终止程序使用后期连接方式这种错误可能会在程序执行过程中随时发生而有些应用程序对这种不确定性有比较严格的限制因此需要关闭后期连接方式在应用程序接受控制权之前让动态连接器处理进程初始化期间发生的这些错误
下面将对其细节做一些解释
因为在编译时共享库的调用不能被连接到程序中所以需要对其做特殊处理直到程序运行时共享库才是有效的PLT就是为了处理这种情况PLT保存调用动态连接器的有关代码由动态连接器对所需例程进行定位
可执行目标是调用PLT的某个入口来实现对共享库例程的调用而不是直接调用共享库例程然后由PLT解析符号表示什么以及进行其它操作
下列代码来自ELF规范
PLT:jmp *name_in_GOT
pushl $offset
jmp PLT@PC
从这段代码中可以得到一些重要的信息这是一个例程调用而不是库调用进程初始化之后name_in_GOT指向后面的push指令offset代表一个重定位偏移值(参见ELF规范)它包含一个符号引用这个符号表示这个库调用使后面的jmp指令能够跳转到动态连接器为了避免下次调用这个共享库例程时重复这个流程动态连接器接着会修改name_in_GOT让其直接指向这个例程这样就能够节约再次调用的时间
上面的叙述总结了PLT在搜索库调用时的重要性因此我们可以修改name_in_GOT使其指向我们自己的代码取代原先库调用实现病毒的传染如果在取代之前我们保存GOT的状态那么还能够重新调用原来的库调用而且可以实现任意库调用的重定向
感染ELF文件
为了实现库调用的重定向需要在可执行目标文件中加入新的代码本文我们将不涉及这方面的问题这在~silvio已经有专门的文章论述
PLT重定向
入口点的算法如下
把正文段标记为可写
保存PLT(GOT)入口
使用新的库调用地址代替PLT(GOT)入口
新的库调用算法如下
实现新的库调用的功能
保存原来的PLT(GOT)入口
调用库调用
再次保存PLT(GOT)如果它被修改了的
使用新的库调用的地址代替PLT(GOT)入口
为了更清楚地解释PLT重定向是如何工作的我们在此解析一段简单的代码在这段代码中被重定向的是printf新的代码是在printf输出一个字符串之前打印一条消息
好吧现在开始
首先保存寄存器
x /* pusha */
把正文段标记为rwx因为正文段通常是不可写的所以为了能够修改PLT我们需要把它改为可写的通过mprotect系统调用
xbxdxxx /* movl $%eax */
xbbxxxx /* movl $text_start%ebx */
xbxxxx /* movl $x%ecx */
xbaxxxx /* movl $%edx */
xcdx /* int $x