dadi1977 2005-9-12 14:11
	[转帖]Unix/ELF文件格式及病毒分析
★ 介绍 
本文介绍了Unix病毒机制、具体实现以及ELF文件格式。简述了Unix病毒检测和反检 
测技术,提供了Linux/i386架构下的一些例子。需要一些初步的Unix编程经验,能够 
理解Linux/i386下汇编语言,如果理解ELF本身更好。 
本文没有任何实际意义上的病毒编程技术,仅仅是把病毒原理应用到Unix环境下。这 
里也不打算从头介绍ELF规范,感兴趣的读者请自行阅读ELF规范。 
★ 感染 ELF 格式文件 
进程映象包含"文本段"和"数据段",文本段的内存保护属性是r-x,因此一般自修改 
代码不能用于文本段。数据段的内存保护属性是rw-。 
段并不要求是页尺寸的整数倍,这里用到了填充。 
关键字: 
[...]   一个完整的页 
M       已经使用了的内存 
P       填充 
页号 
#1    [PPPPMMMMMMMMMMMM]   
#2    [MMMMMMMMMMMMMMMM]   |-- 一个段 
#3    [MMMMMMMMMMMMPPPP]  / 
段并没有限制一定使用多个页,因此单页的段是允许的。 
页号 
#1    [PPPPMMMMMMMMPPPP]  <-- 一个段 
典型的,数据段不需要从页边界开始,而文本段要求起始页边界对齐,一个进程映象 
的内存布局可能如下: 
关键字: 
[...]   一个完整的页 
T       文本段内容 
D       数据段内容 
P       填充 
页号 
#1    [TTTTTTTTTTTTTTTT]    <-- 文本段内容 
#2    [TTTTTTTTTTTTTTTT]    <-- 文本段内容 
#3    [TTTTTTTTTTTTPPPP]    <-- 文本段内容(部分) 
#4    [PPPPDDDDDDDDDDDD]    <-- 数据段内容(部分) 
#5    [DDDDDDDDDDDDDDDD]    <-- 数据段内容 
#6    [DDDDDDDDDDDDPPPP]    <-- 数据段内容(部分) 
页1、2、3组成了文本段 
页4、5、6组成了数据段 
从现在开始,为简便起见,段描述图表用单页,如下: 
页号 
#1    [TTTTTTTTTTTTPPPP]    <-- 文本段 
#2    [PPPPDDDDDDDDPPPP]    <-- 数据段 
在i386下,堆栈段总是在数据段被给予足够空间之后才定位的,一般堆栈位于内存高 
端,它是向低端增长的。 
在ELF文件中,可装载段都是物理映象: 
    ELF Header 
    . 
    . 
    Segment 1    <-- 文本段 
    Segment 2    <-- 数据段 
    . 
    . 
每个段都有一个定位自身起始位置的虚拟地址。可以在代码中使用这个地址。 
为了插入寄生代码,必须保证原来的代码不被破坏,因此需要扩展相应段所需内存。 
文本段事实上不仅仅包含代码,还有 ELF 头,其中包含动态链接信息等等。如果直 
接扩展文本段插入寄生代码,带来的问题很多,比如引用绝对地址等问题。可以考虑 
保持文本段不变,额外增加一个段存放寄生代码。然而引入一个额外的段的确容易引 
起怀疑,很容易被发现。 
向高端扩展文本段或者向低端扩展数据段都有可能引起段重叠,在内存中重定位一个 
段又会使那些引用了绝对地址的代码产生问题。可以考虑向高端扩展数据段,这不是 
个好主意,有些Unix完整地实现了内存保护机制,数据段是不可执行的。 
段边界上的页填充提供了插入寄生代码的地方,只要空间允许。在这里插入寄生代码 
不破坏原有段内容,不要求重定位。文本段结尾处的页填充是个很好的地方,最后看 
上去象下面这个样子: 
关键字: 
[...]   一个完整的页 
V       寄生代码 
T       文本段内容 
D       数据段内容 
P       填充 
页号 
#1    [TTTTTTTTTTTTVVPP]    <-- 文本段 
#2    [PPPPDDDDDDDDPPPP]    <-- 数据段 
一个更完整的ELF可执行布局如下: 
    ELF Header 
    Program header table 
    Segment 1 
    Segment 2 
    Section header table 
    Section 1 
    . 
    . 
    Section n 
典型的,额外的节(那些没有相应段的节)用于存放调试信息、符号表等等。 
下面是一些来自 ELF 规范的内容: 
ELF 头位于最开始,保存一张"road map",描述了文件的组织结构。节保存大量链接 
信息、符号表、重定位信息等等。 
如果存在一个"program header table",将告诉操作系统如何建立进程映象(执行一 
个程序)。可执行文件必须有一个"program header table",可重定位的文件不需要 
该表。"section header table"描述了文件的节组织。每个节在该表中都有一个表项, 
表项包含了诸如节名、节尺寸等信息。链接过程中被用到的文件自身必须有一个 
"section header table",其他目标文件可有可无该表。 
插入寄生代码之后,ELF 文件布局如下: 
    ELF Header 
    Program header table 
    Segment 1   - 文本段(主体代码) 
                - 寄生代码 
    Segment 2 
    Section header table 
    Section 1 
    . 
    . 
    Section n 
寄生代码必须物理插入到ELF文件中,文本段必须扩展以包含新代码。 
下面的信息来自/usr/include/elf.h 
/* The ELF file header.  This appears at the start of every ELF file.  */ 
#define EI_NIDENT (16) 
typedef struct 
{ 
    unsigned char e_ident[EI_NIDENT];  /* Magic number and other info */ 
    Elf32_Half    e_type;              /* Object file type */ 
    Elf32_Half    e_machine;           /* Architecture */ 
    Elf32_Word    e_version;           /* Object file version */ 
    Elf32_Addr    e_entry;             /* Entry point virtual address */ 
    Elf32_Off     e_phoff;             /* Program header table file offset */ 
    Elf32_Off     e_shoff;             /* Section header table file offset */ 
    Elf32_Word    e_flags;             /* Processor-specific flags */ 
    Elf32_Half    e_ehsize;            /* ELF header size in bytes */ 
    Elf32_Half    e_phentsize;         /* Program header table entry size */ 
    Elf32_Half    e_phnum;             /* Program header table entry count */ 
    Elf32_Half    e_shentsize;         /* Section header table entry size */ 
    Elf32_Half    e_shnum;             /* Section header table entry count */ 
    Elf32_Half    e_shstrndx;          /* Section header string table index */ 
} Elf32_Ehdr; 
e_entry 保存了程序入口点的虚拟地址。 
e_phoff 是"program header table"在文件中的偏移。因此为了读取 
"program header table",需要调用lseek()定位该表。 
e_shoff 是"section header table"在文件中的偏移。该表位于文件尾部,在文本段 
尾部插入寄生代码之后,必须更新e_shoff指向新的偏移。 
/* Program segment header.  */ 
typedef struct 
{ 
    Elf32_Word p_type;    /* Segment type */ 
    Elf32_Off  p_offset;  /* Segment file offset */ 
    Elf32_Addr p_vaddr;   /* Segment virtual address */ 
    Elf32_Addr p_paddr;   /* Segment physical address */ 
    Elf32_Word p_filesz;  /* Segment size in file */ 
    Elf32_Word p_memsz;   /* Segment size in memory */ 
    Elf32_Word p_flags;   /* Segment flags */ 
    Elf32_Word p_align;   /* Segment alignment */ 
} Elf32_Phdr; 
可装载段(文本段/数据段)在"program header"中由成员变量p_type标识出是可装载 
的,其值为PT_LOAD (1)。与"ELF header"中的e_shoff一样,这里的p_offset成员 
必须在插入寄生代码后更新以指向新偏移。 
p_vaddr 指定了段的起始虚拟地址。以p_vaddr为基地址,重新计算e_entry,就可以 
指定程序流从何处开始。 
可以利用p_vaddr指定程序流从何处开始。 
p_filesz 和 p_memsz 分别对应该段占用的文件尺寸和内存尺寸。 
.bss 节对应数据段里未初始化的数据部分。我们不想让未初始化的数据占用文件空 
间,但是进程映象必须保证能够分配足够的内存空间。.bss 节位于数据段尾部,任 
何超过文件尺寸的定位都假设位于该节中。 
/* Section header.  */ 
typedef struct 
{ 
    Elf32_Word sh_name;       /* Section name (string tbl index) */ 
    Elf32_Word sh_type;       /* Section type */ 
    Elf32_Word sh_flags;      /* Section flags */ 
    Elf32_Addr sh_addr;       /* Section virtual addr at execution */ 
    Elf32_Off  sh_offset;     /* Section file offset */ 
    Elf32_Word sh_size;       /* Section size in bytes */ 
    Elf32_Word sh_link;       /* Link to another section */ 
    Elf32_Word sh_info;       /* Additional section information */ 
    Elf32_Word sh_addralign;  /* Section alignment */ 
    Elf32_Word sh_entsize;    /* Entry size if section holds table */ 
} Elf32_Shdr; 
sh_offset 指定了节在文件中的偏移。 
为了在文本段末尾插入寄生代码,我们必须做下列事情: 
    * 修正"ELF header"中的 p_shoff  
    * 定位"text segment program header" 
        * 修正 p_filesz  
        * 修正 p_memsz  
    * 对于文本段phdr之后的其他phdr 
        * 修正 p_offset  
    * 对于那些因插入寄生代码影响偏移的每节的shdr 
        * 修正 sh_offset  
    * 在文件中物理地插入寄生代码到这个位置 
      text segment p_offset + p_filesz (original) 
这里存在一个大问题,ELF 规范中指出, 
    p_vaddr mod PAGE_SIZE ==  p_offset mod PAGE_SIZE 
为了满足这个要求: 
    * 修正"ELF header"中的 p_shoff ,增加 PAGE_SIZE 大小 
    * 定位"text segment program header" 
        * 修正 p_filesz  
        * 修正 p_memsz  
    * 对于文本段phdr之后的其他phdr 
        * 修正 p_offset ,增加 PAGE_SIZE 大小 
    * 对于那些因插入寄生代码影响偏移的每节的shdr 
        * 修正 sh_offset ,增加 PAGE_SIZE 大小 
    * 在文件中物理地插入寄生代码以及填充(确保构成一个完整页)到这个位置 
      text segment p_offset + p_filesz (original) 
我们还需要修正程序入口点的虚拟地址,使得寄生代码先于宿主代码执行。同时需要 
在寄生代码尾部能够跳转回宿主代码原入口点继续正常流程。 
    * 修正"ELF header"中的 p_shoff ,增加 PAGE_SIZE 大小 
    * 修正寄生代码的尾部,使之能够跳转回宿主代码原入口点 
    * 定位"text segment program header" 
        * 修正 "ELF header"中的 e_entry ,指向 p_vaddr + p_filesz 
        * 修正 p_filesz  
        * 修正 p_memsz  
    * 对于文本段phdr之后的其他phdr 
        * 修正 p_offset ,增加 PAGE_SIZE 大小 
    * 对于文本段的最后一个shdr 
        * 修正sh_len(应该是sh_size吧,不确定),增加寄生代码大小 
    * 对于那些因插入寄生代码影响偏移的每节的shdr 
        * 修正 sh_offset ,增加 PAGE_SIZE 大小 
    * 在文件中物理地插入寄生代码以及填充(确保构成一个完整页)到这个位置 
      text segment p_offset + p_filesz (original) 
病毒可以随机遍历一个目录树,寻找那些e_type等于 ET_EXEC 或者 ET_DYN 的文件, 
加以感染,这分别是可执行文件和动态链接库文件。 
★ 分析Linux病毒 
病毒要求不使用库,避开libc,转而使用系统调用机制。 
为了动态申请堆内存用于phdr table和shdr table,应该使用brk系统调用。 
利用与缓冲区溢出相同的技术取得常量字符串的地址。 
使用gcc -S编译c代码,观察调整asm代码。 
注意在进入/离开寄生代码的时候保存/恢复寄存器。 
利用objdump -D观察调整一些需要确定的偏移量。 
★ 检测病毒 
这里描述的病毒很容易检测。最显眼的是程序入口点不在常规节中,甚至干脆不在任 
何节中。清理病毒的过程和感染病毒的过程类似。 
用objdump --all-headers很容易定位程序入口点,用objdump --disassemble-all 
跟踪下去就可以得到程序原入口点。 
缺省程序入口点是_start,但是可以在链接的时候更改它。 
★ 结论 
Unix病毒尽管不流行,但的确可行。