文章大部分内容参考自书籍:《linux二进制分析》〔美〕Ryan O'Neill 著 人民邮电出版社
因为本人对二进制ELF方面的内容比较感兴趣,所以想要系统的学习一下linux二进制文件相关的技术以及利用技巧,上面提到的书解决了我很多的疑惑,强烈安利~~
正文开始:
工具: 1、gdb 2、objdump:是优秀的分析简单的elf文件的工具,能快速的进行反编译;缺陷是需要依赖ELF节头,并且不会进行控制流分析,如果要反编译的文件没有节头,那么使用objdump的后果就是无法正确的反编译二进制文件中的代码,甚至都不能打开二进制文件: 1、objdump -D //查看ELF文件所有节的数据或代码 2、objdump -d //只查看ELF文件中的程序代码 3、objdump -tT //查看所有符号 3、objcopy 4、strace 5、ltrace 6、ftrace 7、readelf:解析ELF文件
有用的设备和文件 1、/proc/<pid>/maps //保存了一个进程镜像的布局,展现的内容包括可执行文件、共享库、堆、栈、VDSO等 2、/proc/kcore //linux内核的动态核心文件 3、/boot/System.map //包含了整个内核的所有符号 4、/proc/kallsyms //和System.map类似,区别是可以动态更新,它包含了内核中绝大部分符号 5、/proc/iomem //跟/proc/<pid>/maps相似,不过是系统内存相关的, 6、ECFS //(extended core file snapshot)(ECFS,扩展核心文件快照),是一项特殊的核心转储技术,专门为进程镜像的高级取证分析所设计。
链接器相关环境指针 1、LD_PRELOAD //该环境变量可以设置成一个指定库的路径,动态链接时可以比其他库有更高的优先级。这就允许预加载库中的函数和符号能够覆盖掉后续链接的库中的函数和符号。这在本质上允许你通过重定向共享库函数来进行运行时的修复(他可以绕过反调试,也可以用作用户级rootkit) 2、LD_SHOW_AUXV //该环境变量能够通知程序加载器来展示程序运行时的辅助向量,辅助向量是放在程序栈(通过内核的ELF常规加载方式)上的信息,附带了传递给动态链接器的程序相关的特定信息,这些信息对于反编译和调试非常有用,例如要想获取进程镜像VDSO页的内存地址,就需要查询AT-SYSINFO 3、链接器脚本 //链接器脚本是由链接器解释的,ld链接器程序有其自己解释的一套语言,通过链接器脚本可以控制可执行文件的布局,这在有些时候相当重要
ELF文件组成:一、ELF程序头 定义:ELF程序头是对二进制文件中段的描述,是程序装载必须的一部分。段是在内核装载时被解析的,描述了磁盘上可执行文件的内存布局以及如何映射到内存中 1、PT_LOAD //描述的是可装载的段,也就是说,这种类型的段将被装载或者映射到内存中(eg:text段和data段) 2、PT_DYNAMIC //为动态段,是动态链接可执行文件所特有的,包含了动态链接器所必需的一些信息。在动态段中包含了一些标记值和指针,包括但不限于(1、共享库列表,2、GOT表,3重定位条目的相关信息。还包含了一些结构体,在这些结构体中存放着与动态链接相关的信息。) 3、PT_NOTE //可能保存了与特定供应商或者系统相关的附加信息。不过在可执行文件运行时是不需要这个段的,所以这个段成了很容易被病毒感染的一个地方 4、PT_INTERP //只将位置和大小信息存放在一个以null为终止符的字符串中,在gdb中经常可以看到有关于elf文件位置以及大小的字符串 5、PT_PHDR //保存了程序头表本身的位置和大小。phdr表保存了所有的phde对文件(以及内存镜像)中段的描述信息二、ELF节头 定义:节、不是段。段是程序执行的必要组成部分,在每个段中。会有代码或者数据被划分为不同的节。节头表是对这些节的位置和大小的描述,主要用于链接和调试,(所以说节头对于程序的执行来说并不是必需的),没有节头表,程序依然可以正常执行,也并不意味着节就不存在,只是没有办法通过节头来引用节,对于调试器或者反编译程序来说,只是可以参考的信息变少了而已(需要提醒一点,objdump还有gdb都是依赖节头定位到存储符号数据的节来获取符号信息。如果没有节头,gdb以及objdump几乎无用武之地),但是节头表被破坏或者删除,我们可以重构节头表甚至重构部分符号表,原理就是通过程序头以及保存符号表以及重定位入口信息的DT_TAG 1、.text //保存了程序代码指令的代码节 2、.rodata //保存了只读的数据,他在text段中!!!()因为他是只读的嘛,data段是可读可写的 3、.plt //过程链接表:包含了动态链接器调用从共享库导入函数所必需的相关代码,在text段中 4、.data //在data段中,保存了已初始化的全局变量等数据 5、.bss //在data段中,保存了未初始化的全局变量等数据,占用空间不超过四个字节,因为没存任何东西 6、.got.plt //在data段中,.got保存了全局偏移表,.got和.plt一起提供了对导入的共享库函数的访问入口,由动态链接器在运行时修改。如果攻击者获得了堆或者.bss漏洞的一个指针大小的写语句,就可以对该节任意修改 7、.dynsym //在text段中,保存了从共享库导入的动态符号信息 8、.dynstr //保存了动态符号字符串表,表中存放了一系列字符串,这些字符串代表了符号的名称,以空字符作为终止符 9、.rel.* //保存了重定位相关的信息,也就是字面上的“rel” 10、.hash //保存了一个用于查找符号的散列表 11、.symtab //保存了elfN_Sym类型的符号信息 12、.strtab //保存的是符号字符串表,表中的内容会被.symtab的elfN_Sym结构中的st_name条目引用 13、.shstrtab //保存节头字符串表,该表是一个以空字符终止的字符串的集合,字符串保存了每个节的节名,如.text、.data 等 14、.ctors&.dtors //.ctors(构造器)和.dtors(析构器)这两个节保存了指向构造函数和析构函数的函数指针(黑客或病毒制造者有时会利用构造函数属性实现一个函数,实现类似 PTRACE_TRACEME 这样的反调试功能,这样进程就会追踪自身,调试器就无法附加到这个进程上。通过这种方式,在程序进入 main()函数之前就会先执行反调试的代码) 总结:text段包括:(.text、.rodata、.hash、..dynsym、.dynstr、.plt、.rel.got); data段包括:(.data、.dynamic、.got.plt、.bss) tips: 可以通过readelf -S查看程序节头 可重定位文件无程序头,不过 Linux 中的可加载内核模块(LKM)是个例外,LKM 是 ET_REL 类型的文件,它会被直接加载进内核的内存中并自动进行重定位三、ELF符号 定义:符号是对某些类型的数据或者代码(如全局变量或函数)的符号引用。在大多数共享库和动态链接可执行文件中,存在两个符号表.dynsym和.symtab。.dynsym 保存了引用来自外部文件符号的全局符号,如 printf 这样的库函数,.dynsym 保存的符号是.symtab 所保存符号的子集,.symtab 中还保存了可执行文件的本地符号,如全局变量,或者代码中定义的本地函数等。 那么既然.symtab 中保存了.dynsym 中所有的符号,那么为什么还需要两个符号表呢?(因为.dynsym是被标记为ALLOC的,会在运行时分配并装入内存,而.symtab不是在运行时必需的,不会被装载到内存中),.dynsym 保存的符号只能在运行时被解析,因此是运行时动态链接器所需要的唯一符号。.dynsym 符号表对于动态链接可执行文件的执行来说是必需的,而.symtab 符号表只是用来进行调试和链接的,有时候为了节省空间,会将.symtab 符号表从生产二进制文件中删掉。 1、st_name //保存了指向符号表中字符串表(位于.dynstr 或者.strtab)的偏移地址,偏移地址存放着符号的名称,如 printf。 2、st_value //存放符号的值(可能是地址或者位置偏移量)。 3、st_size //存放了一个符号的大小,如全局函数指针的大小,在一个 32 位系统中通常是 4 字节。 4、st_other //定义了符号的可见性。 5、st_shndx //每个符号表条目的定义都与某些节对应。st_shndx 变量保存了相关节头表的索引。 6、st_info //指定符号类型及绑定属性 符号类型: 1、STT_NOTYPE //符号类型未定义 2、STT_FUNC //表示该符号与函数或者其他可执行代码关联。 3、STT_OBJECT //表示该符号与数据目标文件关联。 符号绑定: 1、STB_LOCAL //本地符号在目标文件之外是不可见的,目标文件包含了符号的定义,如一个声明为 static 的函数。 2、STB_GLOBAL //全局符号对于所有要合并的目标文件来说都是可见的。一个全局符号在一个文件中进行定义后,另外一个文件可以对这个符号进行引用。 3、STB_WEAK //与全局绑定类似,不过比 STB_GLOBAL 的优先级低。被标记为 STB_WEAK 的符号有可能会被同名的未被标记为STB_WEAK 的符号覆盖。 所谓的删除符号表:就是保留.dynsym,丢弃.symtab,这样在IDA里面,函数所表示出来的函数名就是SUB_<address_of_function>的形式四、ELF重定位 定义:重定位就是将符号定义和符号引用进行连接的过程。可重定位文件需要包含描述如何修改节内容的相关信息,从而使得可执行文件和共享目标文件能够保存进程的程序镜像所需的正确信息。重定位条目就是我们上面说的相关信息。(说白了就是.o文件将里面的函数分配内存的过程,重定位实际上是一种给二进制文件打补丁的机制) 基于二进制修补的重定位代码注入:重定位代码注入是黑客、病毒制造者或者任何想修改二进制文件中代码的人常用的一种技术。在二进制文件编译完成并链接到一个可执行文件之后,通过重定位代码技术可以重新链接二进制文件。这就意味着,可以将一个目标文件注入到可执行文件中,更改可执行文件的符号表来指向新注入的功能,并对注入的目标代码进行必要的重定位,那么注入的代码就变成了可执行文件的一部分。(就是将一个.o文件替换原有的函数或者导入表里面的东西,),我们可以用Eresi进行重定位代码注入入(也称 ER_REL 注入)五、ELF动态链接 辅助向量:通过系统调用 sys_execve()将程序加载到内存中时,对应的可执行文件会被映射到内存的地址空间,并为该进程的地址空间分配一个栈。这个栈会用特定的方式向动态链接器传递信息。这种特定的对信息的设置和安排即为辅助向量(auxv)。 程序被加载进内存,辅助向量被填充好之后,控制权就交给了动态链接器。动态链接器会解析要链接到进程地址空间的用于共享库的符号和重定位。默认情况下,可执行文件会动态链接 GNU C 库 libc.so。ldd 命令能显示出一个给定的可执行文件所依赖的共享库列表。 延迟绑定:通过PLT(过程链接表)和 GOT(全局偏移表)。PLT[0]相对于GOT的偏移为GOT[3],下面是 GOT 的 3 个偏移量。 GOT[0]:存放了指向可执行文件动态段的地址,动态链接器利用该地址提取动态链接相关的信息。 GOT[1]:存放 link_map 结构的地址,动态链接器利用该地址来对符号进行解析。 GOT[2]:存放了指向动态链接器_dl_runtime_resolve()函数的地址,该函数用来解析共享库函数的实际符号地址。 下面是对动态链接演示绑定过程的一个总结: 1.调用 fgets@PLT(即调用 fgets 函数)。 2.PLT 代码做一次到 GOT 中地址的间接跳转。 3.GOT 条目存放了指向 PLT 的地址,该地址存放在 push 指令中。 4.push $0x0 指令将 fgets() GOT 条目的偏移量压栈。 5.最后的 fgets() PLT 指令是指向 PLT-0 代码的 jmp 指令。 6.PLT-0 的第一条指令将 GOT[1]的地址压栈,GOT[1]中存放了指向fgets()的 link_map 结构的偏移地址。 7.PLT-0 的第二条指令会跳转到 GOT[2]存放的地址,该地址指向动态链接器的_dl_runtime_resolve 函数,_dl_runtime_resolve 函数会通过把 fgets()函数的符号值加到.got.plt 节对应的 GOT 条目中,来处理R_386_JUMP_SLOT 重定位。 下一次调用 fgets()函数时,PLT 条目会直接跳转到函数本身,而不是再执行一遍重定位过程。 动态段:动态段保存了一个由类型为ElfN_Dyn的结构体组成的数组,d_tag字段保存了类型的定义参数,下面是动态链接器常用的比较重要的类型值 1、DT_NEEDED //保存了所需的共享库名的字符串表偏移量。 2.DT_SYMTAB动态符号表的地址,对应的节名.dynsym。 3.DT_HASH符号散列表的地址,对应的节名.hash(有时命名为.gnu.hash)。 4.DT_STRTAB符号字符串表的地址,对应的节名.dynstr。 5.DT_PLTGOT全局偏移表的地址。 ElfN_Dyn 的 d_val 成员保存了一个整型值,可以存放各种不同的数据,如一个重定位条目的大小。 d_ptr 成员保存了一个内存虚址,可以指向链接器需要的各种类型的地址,如 d_tag DT_SYMTAB 符号表的地址。 可以通过动态段通过动态参数找到特定节的地址,这对重建节头表的取证分析重建非常有帮助,如果去掉了节头表,可以从动态段(.dynstr、.dynsym、.hash 等)读取相关信息来重建部分节头表。其他的段,如 text(文本)段和 data(数据)段等,也可以产生所需的相关信息(如要产生.text 节和.data 节的相关信息)。 动态链接器利用 ElfN_Dyn 的 d_tag 来定位动态段的不同部分,每一部分都通过 d_tag 保存了指向某部分可执行文件的引用,如 DT_SYMTAB 保存了动态符号表的地址,对应的 d_prt 给出了指向该符号表的虚址。 链接器为每个共享库生成一个 link_map 结构的条目,并将其存入到一个链表中
下面为一个简单的ELF解析器,代码参考自上面的书籍:
/* elfparse.c – gcc elfparse.c -o elfparse */ #include <stdio.h> #include <string.h> #include <errno.h> #include <elf.h> #include <unistd.h> #include <stdlib.h> #include <sys/mman.h> #include <stdint.h> #include <sys/stat.h> #include <fcntl.h> int main(int argc, char **argv) { int fd, i; uint8_t *mem; struct stat st; char *StringTable, *interp; Elf32_Ehdr *ehdr; Elf32_Phdr *phdr; Elf32_Shdr *shdr; if (argc < 2) { printf("Usage: %s <executable>\n", argv[0]); exit(0); } if ((fd = open(argv[1], O_RDONLY)) < 0) { perror("open"); exit(-1); } if (fstat(fd, &st) < 0) { perror("fstat"); exit(-1); } /* Map the executable into memory */ mem = mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, fd, 0); if (mem == MAP_FAILED) { perror("mmap"); exit(-1); } /* * The initial ELF Header starts at offset 0 * of our mapped memory. */ ehdr = (Elf32_Ehdr *)mem; /* * The shdr table and phdr table offsets are * given by e_shoff and e_phoff members of the * Elf32_Ehdr. */ phdr = (Elf32_Phdr *)&mem[ehdr->e_phoff]; shdr = (Elf32_Shdr *)&mem[ehdr->e_shoff]; /* * Check to see if the ELF magic (The first 4 bytes) * match up as 0x7f E L F */ if (mem[0] != 0x7f && strcmp(&mem[1], "ELF")) { fprintf(stderr, "%s is not an ELF file\n", argv[1]); exit(-1); } /* We are only parsing executables with this code. * so ET_EXEC marks an executable. */ if (ehdr->e_type != ET_EXEC) { fprintf(stderr, "%s is not an executable\n", argv[1]); exit(-1); } printf("Program Entry point: 0x%x\n", ehdr->e_entry); /* * We find the string table for the section header * names with e_shstrndx which gives the index of * which section holds the string table. */ StringTable = &mem[shdr[ehdr->e_shstrndx].sh_offset]; /* * Print each section header name and address. * Notice we get the index into the string table * that contains each section header name with * the shdr.sh_name member. */ printf("Section header list:\n\n"); for (i = 1; i < ehdr->e_shnum; i++) printf("%s: 0x%x\n", &StringTable[shdr[i].sh_name], shdr[i]. sh_addr); /* * Print out each segment name, and address. * Except for PT_INTERP we print the path to * the dynamic linker (Interpreter). */ printf("\nProgram header list\n\n"); for (i = 0; i < ehdr->e_phnum; i++) { switch(phdr[i].p_type) { case PT_LOAD: /* * We know that text segment starts * at offset 0. And only one other * possible loadable segment exists * which is the data segment. */ if (phdr[i].p_offset == 0) printf("Text segment: 0x%x\n", phdr[i].p_vaddr); else printf("Data segment: 0x%x\n", phdr[i].p_vaddr); break; case PT_INTERP: interp = strdup((char *)&mem[phdr[i].p_offset]); printf("Interpreter: %s\n", interp); break; case PT_NOTE: printf("Note segment: 0x%x\n", phdr[i].p_vaddr); break; case PT_DYNAMIC: printf("Dynamic segment: 0x%x\n", phdr[i].p_vaddr); break; case PT_PHDR: printf("Phdr segment: 0x%x\n", phdr[i].p_vaddr); break; } } exit(0); }
ptrace:(可以用于进程分析和进程镜像重建,还可以是向一个正在运行的进程注入代码,并执行新的代码。) 程序员可以利用 ptrace 附加到一个进程上并修改内存,如代码注入,修改一些比较重要的数据结构,如共享库重定向所需要的 GOT 等。 ptrace 默认会覆盖 mmapa 或 mprotect 的权限,也就意味着用户可以使用 ptrace 往 text 段中写入内容,即便 text 段是只读的。但如果内核采用 pax 或者 grsec 进行了 mprtotect 的限制,加固了段的访问权限,就不可以使用 ptrace 进行修改了。这是一个安全特性。但是仍然有几种通过使用 ptrace 操纵 vsyscall 表绕过这些限制的方法。 ptrace()命令是最常用于用户级内存分析的系统调用命令。事实上,如果想设计一款运行在用户层的取证分析软件,访问其他进程内存唯一的方式就是通过 ptrace 系统调用,或者通过读取 proc 文件系统(除非程序有一些显式的共享内存 IPC 设置)。 ELF 可执行文件在内存中的结构除了数据段的变量、全局偏移表、函数指针和未初始化变量(.bss 节)的变化外,几乎跟在磁盘上一样。这就意味着可以作用于 ELF 二进制文件的病毒或者 rootkit 技术同样可以作用于进程(运行时的代码),作用于进程的病毒更利于攻击者隐藏。 内存感染类型:
进程镜像重建(根据进程还原可执行文件) 可以设计一个软件,将一个进程镜像重建成对应的可执行文件。扩展核心文件快照(ECFS)技术就能够根据进程镜像重构可执行文件,并且对重建功能进行了扩展。 所面临的挑战:PLT/GOT 完整性,添加节头表 (完全可以自行还原) 下面是可执行文件重建过程: 1.定位可执行文件(text 段)的基址。可以通过解析/proc/<pid>/maps实现。 2.通过解析 ELF 文件头(如 Elf64-Ehdr)来定位程序头表。 3.解析程序头表,找出数据段。 4.将数据段读到缓存中,并定位数据段中的动态段,然后定位 GOT。使用动态段中的 d_tag 来定位 GOT。 5.一旦定位到 GOT,就需要将 GOT 恢复到运行之前的状态。这一步骤最关键的是将每个 GOT 条目恢复成最初的 PLT 存根地址,以便在程序运行时进行延迟链接。 6.需要修改为 puts()保留的 GOT 条目,重新指向 PLT 存根代码,这段代码的作用是将 GOT 偏移地址压入栈。 7.选择性地重建节头表。然后将 text 段和 data 段(以及节头表)写到磁盘。 Quenya可以实现将一个进程镜像重建成一个可执行文件的过程 ptrace还可以进行反调试:使用 ptrace 的 PTRACE_TRACEME 请求参数,这样程序就会去追踪进程自身。一个进程同一时间只能被一个 tracer 追踪,因此,如果一个进程已经被追踪了,调试器试图使用 ptrace 附加到这个进程时,就会报 Operation notpermitted(操作不被允许)。PTRACE_TRACEME 也可用来检查程序是否已经被调试 下面的代码片段使用 ptrace 来检查程序是否被追踪:
if (ptrace(PTRACE_TRACEME, 0) < 0) { printf("This process is being debugged!!!\n"); exit(1); }
ELF病毒: 病毒的开发对于黑客和地下技术爱好者来说有极大的吸引力,这倒不是因为开发的病毒可以产生多大的破坏力,而是要设计并成功编码病毒需要面临极大的挑战,只有掌握非传统的编码技术,才能够写出可以寄生在可执行文件和进程中的病毒程序。同时,让寄生程序保持不被发现的状态所需要的技术和解决方案,如多态和变型代码,对程序员来说都是非常独特的挑战。 ELF 病毒的本质:每个可执行文件都有一个控制流,也叫执行路径。ELF 病毒的首要目标是劫持控制流,暂时改变程序的执行路径来执行寄生代码。寄生代码通常负责设置钩子来劫持函数,还会将自身代码复制到没有感染病毒的程序中。一旦寄生代码执行完成,通常会跳转到原始的入口点或程序正常的执行路径上。通过这种方式,宿主程序貌似是正常执行的,病毒就不容易被发现。 设计ELF病毒面临的挑战: 1、寄生代码必须是独立的 :不能够依赖外部链接,需要位置独立,能够动态计算出所在的内存地址,这是因为每次感染的地址都会变化,寄生代码每次注入二进制文件中的位置也会发生变化。如果寄生代码通过地址引用函数或者字符串,硬编码的地址就会改变,寄生代码执行就会失败(解决方案:使用 gcc 的-nostdlib 选项编译第一个病毒可执行文件。也可以使用-fpic –pie 将其编译成位置独立的代码。) 2、字符串存储的复杂度:设计独立的代码时会遇到的一个挑战便是对字符串存储的处理。在病毒代码中处理字符串时,可能会遇到下面的情况:const char *name = "elfmaster"; 开发者一般会避免编写出上面这样的代码,这是因为编译器会将elfmaster 数据存放在.rodata 节中,然后通过地址对字符串进行引用。一旦病毒注入到另一个程序中,这个地址就失效了。这个问题实际上是伴随着前面讨论过的硬编码地址问题而产生的。 (解决方案:使用栈存放字符串,这样才能够在运行时动态分配: char name[10] = {'e', 'l', 'f', 'm', 'a', 's', 't', 'e', 'r','\0'}; 也可以通过使用 gcc 的-N 选项,将 text 段和 data 段合并到一个单独的段中,这个段就拥有了可读+可写+可执行(RWX)权限。之所以说这种方式比较巧妙,是因为全局数据和只读数据,如.data 和.rodata 节,都被合并到了一个单独的段中。通过这种方式,病毒在感染阶段就可以将整个段进行注入,而段中包含了来自.rodata 的字符串数据。这项技术跟 IP相对寻址技术结合起来使用,病毒开发者就可以使用传统的字符串定义了: char *name = "elfmaster"; 病毒代码中可以使用上面这种类型的字符串,而不必使用栈来存放字符串。不过,需要注意的一点是,将所有的字符串存放在全局数据中而不是栈中,会使得寄生代码占用的空间变大,有时候我们并不希望病毒代码体积太大。) 3、寻找存放寄生代码的合理空间:我们面临的挑战不是找到空间存放代码,而是去调整 ELF 二进制文件,以便于能够去使用空间,同时要使得可执行文件看起来正常执行,并能够保证病毒可以潜藏在 ELF 文件中以 ELF 规范正常执行。在修改二进制文件和文件布局时,需要考虑许多问题,如页对齐、偏移调整、地址调整等。(解决方案:很多,下面会讲) 4、将执行控制流传给寄生代码 :完全可以调整 ELF 文件头来将入口点指向寄生代码。但是比较容易被发现。如果入口点被修改后指向了寄生代码,就可以使用 readelf –h 命令查看入口点,立即就能知道寄生代码的位置。(解决方案:可以考虑找一个合适的位置来插入/修改一个分支,通过分支跳转到寄生代码,如插入一个 jmp 或者重写函数指针。一个比较合适的地方就是.ctors 或者.init_array 节,这两个节中存放着函数的指针。如果不介意宿主程序执行完之后再执行寄生代码,可以使用.dtors或.fini_array 节。) ELF病毒寄生代码感染方法 : 1、Silvio 填充感染:它将病毒体限制在了一个内存分页的大小。。在 32 位 Linux 系统上,一页有 4096 字节,在 64 位系统上,可执行文件使用较大的分页,可以到0x200000 字节,能够容纳将近 2MB 的感染代码。这种感染方法利用了内存中 text段和 data 段之间存在的一页大小的填充空间,在磁盘上,text 段和 data 段是紧挨着的,不过可以利用这两个段之间的区域作为病毒体的存放区域 感染算法:1.将 ELF 文件头中的 ehdr->e_shoff 增加 PAGE_SIZE 的大小值。 2.定位 text 段的 phdr。 将入口点修改为寄生代码的位置。 将 phdr[TEXT].p_filesz 增加寄生代码的长度值。 将 phdr[TEXT].p_memsz 增加寄生代码的长度值。 3.对每个 phdr,如果对应的段位于寄生代码之后,则将 phdr[x].p_offset 增加 PAGE_SIZE 大小的字节。 4.找到 text 段的最后一个 shdr,将 shdr[x].sh_size 增加寄生代码的长度值(因为在这个节中将会存放寄生代码)。 5.对每个位于寄生代码插入位置之后的 shdr,将 shdr[x].sh_offset增加 PAGE_SIZE 的大小值。 6.将真正的寄生代码插入到 text 段的 file_base + phdr[TEXT].p_filesz。 2、逆向 text 感染:这种感染方式的前提是对 text 段进行逆向扩展。在逆向扩展过程中,需要将 text 段的虚拟地址缩减 PAGE_ALIGN(parasite_size)。鉴于现在的Linux 系统上所允许的最小虚拟映射地址(/proc/sys/vm/mmap_min_addr)为 0x1000,因此 text 的虚拟地址最多能扩展到 0x1000。不过在 64位的系统上,默认的 text 虚拟地址通常为 0x400000,这样的话,寄生代码可以占用的空间就可以达到 0x3ff000 字节(准确地说,需要减去 sizeof(ElfN_Ehdr)个字节)。 .text 感染有几个比较有吸引力的特点:不仅能允许注入比较大的病毒代码,还允许将入口点指向.text 节。尽管我们会对入口点进行修改,这种感染方法仍然会指向实际的.text 节,而不是像.jcr 或者.en_frame 这样的容易引起怀疑的节。由于插入点是 text 段,因此插入代码也是可执行的(跟Silvio 填充感染算法一样)。这种感染方式比 data 段感染要高明许多,data 段感染尽管允许插入大小不限的病毒代码,不过在开启了 NX(非执行页)-bit的系统上需要更改段的权限。 感染算法:1.将 ehdr->e_shoff 增加 PAGE_ROUND(parasite_len)。 2.找到 text 段和 phdr,保存 p_vaddr 的初始值。 将 p_vaddr 减小 PAGE_ROUND(parasite_len)。 将 p_paddr 减小 PAGE_ROUND(parasite_len)。 将 p_filesz 增加 PAGE_ROUND(parasite_len)。 将 p_memsz 增加 PAGE_ROUND(parasite_len)。 3.找出所有的 p_offset 比 text 的 p_offset 大的 phdr,并将对应的p_offset 增加 PAGE_ROUND(parasite_len);这步操作会将 phdr 前移,为逆向 text 扩展腾出空间。 4.将 ehdr->e_entry 设置为:orig_text_vaddr – PAGE_ROUND(parasite_len) + sizeof(ElfN_Ehdr) 5.将 ehdr->e_phoff 增加 PAGE_ROUND(parasite_len)。 6.创建一个新的二进制文件映射出所有的修改,插入真正的寄生代码,然后覆盖掉旧的二进制文件。 3、data段感染:在未进行 NX-bit 设置的系统上,如 32 位的 Linux 系统,可以在不改变 data段权限的情况下执行 data 段中的代码(即使段权限为可读+可写)。这是一种很不错的感染文件的方式,因为这种感染方式对插入的寄生代码大小没有限制,我们可以轻易在 data 段上追加寄生代码。唯一要注意的是需要为.bss节预留空间。尽管.bss 节不占用磁盘空间,但是它会在程序运行时为那些没有初始化的变量在 data 段末尾分配空间。可以通过从 phdr->p_memsz 中提取 data 段的 phdr->p_filesz 来算出.bss 节会在内存中分配的空间。 感染算法:1.将 ehdr->e_shoff 增加寄生代码的长度。 2.定位 data 段 phdr。 将 ehdr->e_entry 指向寄生代码所在的位置。 将 phdr->p_filesz 增加寄生代码的长度。 将 phdr->p_memsz 增加寄生代码的长度。 3.调整.bss 节头,使其偏移量和地址能够反映出寄生代码的结束位置。 4.设置 data 段的权限。(第 4 步只适用于进行了 NX-bit 设置的系统。在 32 位 Linux 操作系统上,data 段不需要为了执行代码而将权限标为可执行,除非系统内核上安装了 PaX(https://pax.grsecurity.net/)这样的安全加固工具。) 5.此项可选。使用假名为寄生代码添加一个节头。否则,如果有人运行了/usr/bin/strip <infected_program>,会将没有进行节头说明的寄生代码清理掉。 6.创建一个新的二进制文件映射出所有的修改,插入真正的寄生代码,然后覆盖掉旧的二进制文件。 data 段感染的应用场景不一定都是病毒入侵。例如,在写包装器时,通常会将加密的可执行文件存放到存根可执行文件的 data 段中。 4、PT_NOTE 到 PT_LOAD 转换感染:这种方法比较容易实施且比较容易被检测到。原理就是将 PT_NOTE 段的类型改为 PT_LOAD,然后将段的位置移到其他所有段之后。当然,也可以通过创建一个 PT_LOAD phdr条目来创建一个新的段,但是由于程序在没有 PT_NOTE 段时仍将执行,因此将其转换为 PT_LOAD 类型。对于 PT_LOAD 感染来说,并没有太严格的规则。像前面提到的,可以将PT_NOTE 的类型修改成 PT_LOAD,也可以创建新的 PT_LOAD phdr 和段。 感染算法: 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 头的修改和新的段,插入真正的寄生代码。 劫持控制流: 1、PLT 感染:替换点plt的代码 2、函数蹦床:类似于rop 3、重写.ctors/.dtors 函数指针:就是劫持构造和析构函数 4、GOT 感染或 PLT/GOT 重定向:就是got覆写 5、感染数据结构:控制data段里面的数据,就是改变量或者是函数指针的值 6、函数指针重写:既可以归到4,也可以归到5 进程内存病毒和 rootkits——远程代码注入技术: 1、共享库注入(.so注入):.so 感染/ET_DYN 感染||so 感染——使用 LD_PRELOAD||使用 LD_PRELOAD 注入 wicked.so.1||—利用 open()/mmap() shellcode||—使用 dlopen() shellcode||使用 VDSO 控制技术 2、text 段代码注入:在注入的 shellcode 执行完成之后需要立即恢复原先的代码,比较容易被检测到 3、可执行文件注入 4、重定位代码注入——ET_REL 注入:重定位代码注入与共享库注入非常类似,不过不兼容 dlopen()。ET_REL(.o 类型的文件)是可重定位的代码,与 ET_DYN 类似(.so 文件),不过不能够作为单独的文件执行,可以链接到可执行文件或者共享库中。 ELF 反调试和封装技术: 1、PTRACE_TRACEME 技术:PTRACE_TRACEME 技术利用了进程追踪的一项特性—一个程序在同一时间只能被一个进程追踪。几乎所有的调试器,包括 GDB,都会使用ptrace。这项技术的思路就是让程序追踪自身,这样调试器就无法附加到该进程了。 2、SIGTRAP 处理技术:在调试时通常会设置断点,程序执行到断点处会产生一个 SIGTRAP 信号,调试器的信号处理器会捕获到该 SIGTRAP 信号,程序暂停,然后就可以对程序进行检查。使用这项技术,程序可以设置一个信号处理器来捕获SIGTRAP 信号,然后故意发出一个断点指令,信号处理器捕获到 SIGTRAP信号之后,会将一个全局变量从 0 加到 1。随后程序会对这个全局变量进行检查,看是否已经从 0 加到 1 了,如果是,就说明我们自己的程序捕获到了断点,目前还没有被调试器调试。如果否(即为 0),那就说明目前一定存在调试器在对该程序进行调试。为了防止被调试,程序可以选择终止自身进程或者退出。 3、/proc/self/status 技术:每个进程都有动态文件,文件中包含了许多信息,其中就存放了进程是否正在被追踪的相关信息。“TracerPid: 0”表示进程没有被追踪。程序要检查自身是否被追踪,可以打开/proc/slf/status,然后检查这一项的值是否为 0。如果不为 0,则说明程序正在被追踪,就可以终止自身进程或者立即退出。 4、 代码混淆技术:代码混淆技术(也称代码转换技术)是通过修改汇编层的代码来引入不明确的分支指令或者未对齐指令,使得反汇编程序无法正确地读取字节码文件。比如说mov的上面有个jmp指令,jmp就会进行混淆,因为有些调试器没有控制流分析的 5、字符串表转换技术:会打乱每个符号名和节相关信息的顺序,以致可能出现的结果就是所有的节头、函数名和符号名看上去都是乱序混在一起的。这项技术对于逆向工程师来说有很大的误导性,如想找一个名为check_serial_number()的函数,实际上找到的是名为 safe_strcpy()的函数。
linux二进制保护: 加壳器:是一种恶意软件作者或者黑客常用的软件,用来对可执行文件进行压缩或加密,来对代码和数据进行混淆。 存根机制和用户层执行: 首先,我们需要知道,软件保护器实际上是由以下两个程序组成的。 保护阶段的代码:应用到目标二进制文件上的保护程序。 运行时引擎或存根:与目标二进制文件合并在一起,负责运行时反混淆和反调试的程序。 运行时代码(或存根)必须要知道如何对一起合并的二进制文件进行解密或者反混淆。大多数的软件保护机制会有一个相对简单的运行时引擎与被保护的二进制合并在一起,其唯一目的就是对二进制文件进行解密,然后在内存中将控制权交由解密后的二进制文件。这种类型的运行时引擎其实不是一种真正的引擎,我们一般称之为存根。存根通常在没有 libc 链接的情况下进行编译(如使用 gcc-nostdlib),也可以使用静态编译。 一个典型的存根会执行下面的任务: 解密负载文件(原始的可执行文件); 将可执行文件的可装载段映射到内存中; 将动态链接器映射到内存中; 创建栈(使用 mmap); 准备栈相关的信息(argv、envp、辅助向量); 将控制权交由程序的入口点。 存根除了作为用户层执行组件要对可执行文件进行解密并加载到内存之外,也有其他的用途。存根通常会开启反调试和反模拟模式,保护二进制程序,提高对程序调试或者模拟的门槛,从而增加逆向工程的难度。 现存的ELF二进制保护器: 1、DacryFile——Grugq 于 2001 年发布 2、Burneye——Scut 于 2002 年发布 3、Shiva——Neil Mehta 和 Shawn Clowes 于 2003 年发布 4、Maya's Veil——Ryan O'Neill 于 2014 年发布(这本书的作者大佬写的程序QAQ) 保护技术: 1、反调试技术 2、防模拟技术(通过系统调用检测模拟||检测模拟的 CPU 不一致||检测特定指令之间的时延) 3、混淆 4、保护控制流(1、防止ptrace攻击,2、防止安全漏洞的攻击(栈溢出等))
ELF文件感染进阶及ELF二进制取证分析: 检测入口点修改技术:此处的目的是检测 e_entry 是否存放了一个指向标志着二进制文件被异常修改过的地址。 不是由链接器/usr/bin/ld 本身所进行的任何修改都被视为异常,链接器的任务是将所有的 ELF 目标文件进行链接。链接器会创建一个表示正常状态的文件,而其他非常态的修改通常很容易被有经验的人发现。 检测其他形式的控制流劫持: 1、修改.ctors/.init_array 节:就是修改构造函数: 关于防止反调试的.ctors 旁注:一些引用了反调试技术的二进制文件会创建一个合法的构造器,来调用 ptrace(PTRACE_TRACEME,0);。反调试技术能够防止进程被调试器追踪,因为一个进程在同一时间只能被一个追踪器追踪。如果发现二进制文件中存在反调试作用的函数,并且在.ctors 中保存了这个函数的指针,建议将函数指针修改为 0x00000000 或者0xffffffff,这样__libc_start_main()函数就会忽略这一项,从而可以有效地防止反调试。在 GDB 中可以使用 set 命令轻而易举地完成这项任务,假设你想修改.ctors 的入口地址,可以使用 set {long} address = 0xffffffff。 2、检测 PLT/GOT 钩子 3、检测函数蹦床:trampoline 这个术语目前有多个不同的解释,不过最初指的是内联代码修补,即插入 jmp 这样的分支指令,覆盖函数过程序言的前 5~7 个字节。通常,如果被修改过的函数需要暂时实现未修改前的功能,会暂时将函数蹦床替换成初始代码,在执行后立即将蹦床指令替换回来。要检测这样的内联代码钩子非常简单,如果有可以反编译二进制的程序或脚本,甚至可以轻易地实现自动化 检测函数蹦床最快的方式就是定位每个函数的入口点,对代码的前 5~7个字节进行验证,看是否转换成了某种类型的分支指令。可以通过为 GDB 写一个 Python 脚本,从而轻而易举地实现这个功能。 识别寄生代码特征 识别逆向 text 填充感染 识别被保护的二进制文件
进程内存取证分析:攻击者可以在磁盘上修改二进制文件,同样也可以对内存中运行的程序进行修改来达到同样的目的,同时避开文件修改检测程序(如 tripwire)的检测。这种进程镜像热修补可以用来进行函数劫持、注入共享库、执行寄生 shellcode等。这种类型的感染,通常是内存驻留后门、病毒、密钥记录器和隐藏进程等所必需的组件。 进程内存布局:/proc/$pid/maps。 1、可执行文件内存映射:text段(读,执行),RELRO的data段(只读),data段(读写) 2、程序堆:堆通常位于 data 段之后。在 ASLR 技术出现以前,堆是从 data 段地址之后开始扩展的。现如今,堆段在内存中是随机映射的,不过在 maps 文件中,紧随 data 段之后 3、共享库映射 4、栈、VDSO 和 vsyscall:在映射文件的末尾是栈段,紧随栈段之后是 VDSO(Virtual DynamicShared Object,虚拟动态共享目标文件)和 vsyscall。glibc 使用 VDSO 来调用一些经常用到的系统调用,否则可能会产生性能问题。VDSO 通过执行用户层特定的系统调用来进行加速。在 x86_64 位系统上,vsyscall 页已经弃用了,不过在 32 位的系统上,vsyscall 的功能跟 VDSO 相同。 进程内存感染: 感染工具: 1、 Azazel:这是一个 Linux 下使用 LD_PRELOAD 注入的用户级黑客程序,简单有效,其前身是 Jynx。LD_PRELOAD 黑客程序会预加载一个共享目标文件到想要感染的程序中。通常情况下,这样的黑客程序会劫持函数,进行打开、读、写等操作。这些被劫持的函数会显示 PLT 钩子(被 GOT修改)。更多信息可访问 https://github.com/chokepoint/azazel 2、 Saruman:这是一种相对较新的反取证法感染技术,允许用户将完整的动态链接可执行文件注入到现有进程中。注入程序和被注入的程序在同一地址空间中并行运行。这种方式能够实现更加隐蔽且更加高级的远程进程感染。更多信息可访问 https://github.com/elfmaster/saruman 3、 sshd_fucker(phrack .so injection paper):sshd_fucker 是一款紧随 Phrack59 的一篇论文 Runtime process infection 之后的软件。这个软件能够感染 sshd 进程,并劫持传入到 PAM 函数中的用户名和密码。更多信息请访问 http://phrack.org/issues/59/8.html 进程感染技术: 1、注入方法(dll注入 || 重定位目标文件注入 || shellcode注入) 2、劫持可执行文件的相关技术:( PLT/GOT 重定向 || 内联函数钩子 || 修补.ctors 和.dtors || 劫持 VDSO 拦截系统调用) 检测dll注入: 1、映射出进程的地址空间:/proc/<pid>/maps,也可以使用 pmap <pid>命令代替 cat /proc/<pid>/maps命令。因为在映射文件中显示了每个内存段完整的地址范围,以及所有文件映射的完整文件路径,如共享库。 2、查找栈中的 LD_PRELOAD:在栈中可能加载了别的.so文件,可以通过/proc/<pid>/maps查看到 3、检测 PLT/GOT 钩子:可以通过gdb查看got中的地址指向的是否为libc.so里面的函数,如果不是,则说明可能已经被劫持了 4、检查是否操纵 VDSO:查看/proc/<pid>/maps的输出,可以看到映射到进程地址空间的 VDSO 代码保存了通过 syscall(64 位系统)和 sysenter(32 位系统)指令进行系统调用的代码。通常情况下,Linux中的系统调用总是将系统调用编号存入