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