Linux环境和相关工具
Linux工具
GDB
GNU调试器
objdump
objdump -D ELF 查看ELF文件中所有节的数据或代码
objdump -d ELF 只查看ELF文件中的程序代码
objdump -tT ELF 查看所有符号
objcopy
要将.data节从一个ELF目标文件复制到另一个文件中,可以使用下面的指令:
objcopy -only-section=.data <infile> <outfile>
strace
system call trace是基于ptrace(2)系统调用的一款工具。通过在一个循环中使用PTRACE_SYSCALL请求来显示运行中程序的系统调用活动相关的信息以及程序执行中捕捉到的信号量。
使用strace命令来跟踪一个基本程序:
strace /bin/ls -o ls.out
使用strace命令附加到一个现存的进程上:
strace -p <pid> -o daemon.out
原始输出将会显示每个系统调用的文件描述编号,系统调用会将文件描述符作为参数:SYS_read(3,buf,sizeof(buf))
如果想查看读入到文件描述符3中的所有数据,可以运行下面的命令:
strace -e read=3 /bin/ls
ltrace
ltrace与strace非常类似。ltrace会解析共享库,并打印出用到的库函数。
ftrace
与ltrace类似,但还可以显示出二进制文件本身的函数调用。
https://github.com/elfmaster/ftrace
readelf
-h 查看ELF文件头
-S 查询节表头
-l 查询程序头表
-s 查询符号表
-e 查询ELF文件头数据
-r 查询重定位入口
-d 查询动态段
ERESI
两篇文章:
Cerberus ELF interface(http://www.phrack.org/archives/issues/61/8.txt)
Embedded ELF debugging(http://www.phrack.org/archives/issues/63/9.txt)
有用的设备和文件
/proc/<pid>/maps
该文件保存了一个进程镜像的布局,通过展现每个内存映射来实现,展现的内容包括可执行文件、共享库、栈、堆和VDSO等。
/proc/kcore
是Linux内核的动态核心文件,即以ELF核心文件的形式所展现出来的原生内存转储。
/boot/System.map
这个文件包含了整个内核的所有符号。
/proc/kallsyms
与System.map类似,区别就是kallsyms是内核所属的/proc的一个入口并且可以动态更新。如果安装了新的LKM,符号会自动添加到里面去。
/proc/iomem
与/proc/<pid>/maps类似,不过它是跟系统内存相关的。
ECFS
extended core file snapshot(扩展核心文件快照)是一项特殊的核心转储技术,专门为进程镜像的高级取证分析所设计。
https://github.com/elfmaster/ecfs
链接器相关环境指针
LD_PRELOAD环境变量
可以设置成一个指定的库路径,动态链接时可以比其他库有更高的优先级。
LD_SHOW_AUXV环境变量
能够通知程序加载器来展示程序运行时的辅助向量。
辅助向量是放在程序栈(通过内核的ELF常规加载方式)上的信息,附带了传递给动态链接器的程序相关的特定信息。
链接器脚本
默认的链接器脚本可以使用ld -verbose查看。
ELF二进制格式
ELF文件类型
ET_NONE:未知类型
ET_REL:重定位文件
ET_EXEC:可执行文件
ET_DYN:共享目标文件
ET_CORE:核心文件
ELF程序头
Elf32_Phdr结构体:
1 | typedef struct{ |
PT_LOAD
一个可执行文件至少有一个PT_LOAD类型的段。这类程序头描述的是可装载的段,也就是这种类型的段将被装载或者映射到内存中。
PT_DYNAMIC
动态段的Phdr。动态段是动态链接可执行文件所特有的,包含了动态链接器所必需的一些信息。在动态段中包含了一些标记值和指针,包括但不限于以下内容:
- 运行时需要链接的共享库列表;
- 全局偏移表(GOT)的地址;
- 重定位条目的相关信息。
32位ELF的动态段的结构体如下:
1 | typedef struct{ |
PT_NOTE
该类型的段可能保存了与特定供应商或者系统相关的附加信息。
NOTE段病毒感染相关信息:http://vxheavens.com/lib/vhe06.html
PT_INTERP
PT_INTERP段只将位置和大小信息存放在一个以null为终止符的字符串中,是对程序解释器位置的描述。
PT_PHDR
该段保存了程序头表本身的位置和大小。Phdr表保存了所有的Phdr堆文件(以及内存镜像)中段的描述信息。
可以用readelf -l
ELF节头
段和节的区分
段是程序执行的必要组成部分,在每个段中,会有代码或者数据被划分为不同的节。节头表是对这些节的位置和大小的描述,主要用于链接和调试。
32位ELF节头的结构:
1 | typedef struct{ |
.text节
该节是保存了程序代码指令的代码节,存在于text段中。此节类型为SHT_PROGBITS
.rodata节
该节保存了只读数据。存在于一个可执行文件的只读段中,即只能在text段中找到.rodata节。该节类型为SHT_PROGBITS。
.plt节
该节中包含了动态链接器调用从共享库导入的函数所必需的相关代码。存在于text段中。此节类型为SHT_PROGBITS。
.data节
.data节存在于data段中,保存了初始化的全局变量等数据。此节类型为SHT_PROGBITS
.bss节
保存了未进行初始化的全局数据,是data段的一部分,占用空间不超过4字节,仅表示这个节本身的空间。程序加载时数据被初始化为0,在程序执行期间可以进行赋值。由于为保存实际的数据,此节类型为SHT_NOBITS
.got.plt节
.got节保存了全局偏移表。.got.plt节跟程序执行有关,因此节类型被标记为SHT_PROGBITS。
.dynsym节
该节保存了从动态共享库导入的动态符号信息,该节保存在text段。节类型为SHT_DYNSYM
.dynstr节
该节保存了动态符号字符串表,表中存放了一系列字符串,这些字符串代码了符号的名称,以空字符作为终止符。
.rel.*节
重定位节保存了重定位相关的信息,这些信息描述了如何在链接或者运行时,堆ELF目标文件的某部分内容或者进程镜像进行补充和修改。节类型为SHT_REL。
.hash节
有时也被称为.gnu.hash,保存了一个用于查找符号的散列表。
.symtab节
该节保存了ElfN_Sym类型的符号信息。该节类型为SHT_SYMTAB
.strtab节
该节保存的是符号字符串表,表中内容会被.symtab的ElfN_Sym结构中的st_name条目引用,此节类型为SHT_STRTAB。
.shstrtab节
该节保存节头子符串表,该表是一个以空字符终止的字符串集合,字符串保存了每个节的节名。有一个名为e_shsrndx的ELF文件头条目会指向.shstrtab节,e_shstrndx中保存了.shstrtab的偏移量。此节类型为SHT_STRTAB。
.ctors和.dtors节
.ctors(构造器)和.dtors(析构器)这两个节保存了指向构造函数和析构函数的函数指针。(构造函数即main函数执行之前需要执行的代码,析构函数是在main函数之后需要执行的代码。)
ELF文件布局
text段:
[.text]:程序代码。
[.rodata]:只读数据。
[.hash]:符号散列表。
[.dynsym]:共享目标文件符号数据。
[.dynstr]:共享目标文件符号名称。
[.plt]:过程链接表。
[.rel.got]:G.O.T 重定位数据。
data段:
[.data]:全局的初始化变量。
[.dynamic]:动态链接结构和对象。
[.got.plt]:全局偏移表。
[.bss]:全局未初始化变量。
ELF符号
.dynsym保存了引用来自外部文件符号的全局符号。.dynsym保存的符号是.symtab所保存符号的子集,.symtab中还保存了可执行文件的本地符号,如全局变量或者代码中定义的本地函数等。
.dynsym是被标记了ALLOC的,而.symtab则没有被标记。ALLOC表示有该标记的节会在运行时分配并装载进入内存,而.symtab不是在运行时必需的,因此不会被装载到内存中。
64位ELF文件符号项的结构:
1 | typedef struct{ |
符号项保存在.symtab和.dynsym节中,因此节头项的大小与ElfN_Sym的大小相等。
st_name
保存了指向符号表中字符串的偏移地址,偏移地址存放着符号的名称。
st_value
存放符号的值(可能是地址或者位置偏移量)
st_size
存放了一个符号的大小。
st_other
定义了符号的可见性。
st_shndx
保存了相关节头表的索引。
st_info
指定符号类型及绑定属性:
1.符号类型:
STT_NOTYPE 符号类型未定义
STT_FUNC 表示该符号与函数或者其他可执行代码关联
STT_OBJECT 表示该符号与数据目标文件关联
2.符号绑定
STB_LOCAL 本地符号在目标文件之外是不可见的
STB_GLOBAL 全局符号对于所有要合并的目标文件来说都是可见的,一个全局符号在一个文件中进行定义后,另一个文件可以对这个符号进行引用。
STB_WEAK 与全局绑定类似,不过比STB_GLOBAL的优先级低。被标记为STB_WEAK的符号有可能被同名的未被标记为STB_WEAK的符号覆盖
对绑定和类型字段进行打包和解包的宏指令
ELF32_ST_BIND(info)或者ELF64_ST_BIND(info):从st_info值中提取出一个绑定。
ELF32_ST_TYPE(info)或者ELF64_ST_TYPE(info):从st_info值中提取类型。
ELF32_ST_TYPE(bind,type)或者ELF64_ST_INFO(bind,type):将一个绑定和类型转换成st_info值。
ELF重定位
要将一个偏移量计算成虚拟地址,可以用下面的公式:
address_of_call+offset+sizeof(addr)
基于二进制修补的重定位代码注入
Eresi(http://www.eresi-project.org)
Quenya (http://www.bitlackeys.org/projects/quenya_32bit.tgz)
ELF动态链接
辅助向量
通过系统调用 sys_execve()将程序加载到内存中时,对应的可执行文件会被映射到内存的地址空间,并为该进程的地址空间分配一个栈。这个栈会用特定的方式向动态链接器传递信息。这种特定的对信息的设置和安排即为辅助向量(auxv)。
辅助向量是一系列Elf_auxv_t结构:
1 | typedef struct{ |
a_type指定了辅助向量的条目类型,a_val为辅助向量的值。
下面是动态链接器所需要的一些最重要的条目类型:
1 |
程序被加载进内存,辅助向量被填充好之后,控制权就交给了动态链接器。动态链接器会解析要链接到进程地址空间的用于共享库的符号和重定位。
GOT
GOT的前3个偏移量
GOT[0]:存放了指向可执行文件动态段的地址,动态链接器利用该地址提取动态链接相关信息。
GOT[1]:存放link_map结构的地址,动态链接器利用该地址来对符号进行解析。
GOT[2]:存放了指向动态链接器_dl_runtime_resolve()函数的地址,该函数用来解析共享库函数的实际符号地址。
动态段
动态段保存了一个由类型为ElfN_Dyn的结构体组成的数组:
1 | typedef struct{ |
d_tag字段保存了类型的定义参数,下面列出常用的比较重要的类型值:
1.DT_NEEDED:保存了所需的共享库名的字符串表偏移量。
2.DT_SYMTAB:动态符号表的地址,对应的节名.dynsym。
3.DT_HASH:符号散列表的地址,对应的节名.hash。
4.DT_STRTAB:符号字符串表的地址,对应的节名.dynstr。
5.DT_PLTGOT:全局偏移表的地址。
d_val成员保存了一个整型值,可以存放各种不同的数据。
d_ptr成员保存了一个内存虚址,可以指向链接器所需要的各种类型的地址。
链接器为每个动态库生成一个link_map结构条目,并将其存入到一个链表中:
1 | struct link_map{ |
编码一个ELF解析器
Linux进程追踪
ptrace
ptrace可以附加到一个进程上并修改内存。
进程寄存器状态和标记
基于ptrace的调试器示例
内存感染类型
感染技术 | 目标结果 | 寄存类型 |
---|---|---|
GOT感染 | 劫持共享库函数 | 进程内存或可执行文件 |
过程链接表(PLT)感染 | 劫持共享库函数 | 进程内存或可执行文件 |
.ctors/.dtors感染 | 将控制流转向恶意代码 | 进程内存或可执行文件 |
Function trampolines(函数蹦床) | 劫持任意函数 | 进程内存或可执行文件 |
共享库注入 | 插入恶意代码 | 进程内存或可执行文件 |
重定位代码注入 | 插入恶意代码 | 进程内存或可执行文件 |
直接修改text段 | 插入恶意代码 | 进程内存或可执行文件 |
进程占用(将整段程序注入地址空间) | 运行隐藏在现存进程中的完全不同的可执行文件 | 进程内存 |
进程镜像重建
重建可执行文件的目标
- 进程ID作为参数,将该ID对应的进程镜像重建成对应的可执行文件。
- 构建节头的最小集,以便可以使用objdump和gdb这样的工具进行更精确的分析。
重建过程算法
1.定位可执行文件(text段)的基址。
2.通过解析ELF文件头来定位程序头表。
3.解析程序头表,找出数据段。
4.将数据段读到缓存中,并定位数据段中的动态段,然后定位GOT。使用动态段中的d_tag来定位GOT。
5.一旦定位到GOT,就需要将GOT恢复到运行之前的装填。
6.需要修改为puts()保留的GOT条目,重新指向PLT存根代码,这段代码的作用是将GOT偏移地址压入栈。
7.选择性地重建节头表。然后将text段和data段(以及节头表)写到磁盘。
ELF病毒技术
ELF病毒技术
ELF病毒的首要目标是劫持控制流,暂时改变程序执行的路径来执行寄生病毒。
ELF病毒寄生代码感染方法
Silvio填充感染
这种方法利用了内存中text段和data段之间存在的一页大小的填充空间。
相关论文:Unix ELF parasites and viruses(http://vxheaven.org/lib/vsc01.html)
.text感染算法
1.将ELF文件头中的ehdr->e_shoff增加PAGE_SIZE的大小值。
2.定位text段的phdr。
- 将入口点修改为寄生代码的位置:ehdr->e_entry=phdr[TEXT].p_vaddr+phdr[TEXT].p_filesz。
- 将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。
相关代码示例: http://www.bitlackeys.org/projects/lpv.c
逆向text感染
前提:堆text段进行逆向扩展,在逆向扩展过程中,需要将text段的虚拟地址缩减PAGE_ALIGN(parasite_size)。
逆向text感染算法
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.创建一个新的二进制文件映射出所有的修改,插入真正的寄生代码,然后覆盖掉旧的二进制文件。
相关代码:http://www.bitlackeys.org/projects/text-infector.tgz
相应杀毒程序:http://www.bitlackeys.org/projects/skeksi_disinfect.c
data段感染
data段感染算法
1.将ehdr_e_shoff增加寄生代码的长度。
2.定位data段phdr。
- 将ehdr->e_entry指向寄生代码所在位置:phdr->p_vaddr + phdr->p_filesz
- 将phdr->p_filesz增加寄生代码长度
- 将phdr->p_memsz增加寄生代码的长度
3.调整.bss节头,使其偏移量和地址能够反映出寄生代码的结束位置。
4.设置data段的权限(设置了NX-bit的系统):
- phdr[DATA].p_flags != PF_X;
5.(可选)使用假名为寄生代码添加一个节头。(防止有人运行了/usr/bin/strip
6.创建一个新的二进制文件映射出所有的修改,插入真正的寄生代码,然后覆盖掉旧的二进制文件。
PT_NOTE到PT_LOAD转换感染
原理:将PT_NOTE段的类型改为PT_LOAD,然后将段的位置移到其他所有段之后。当然,也可以通过创建一个PT_LOAD phdr条目来创建一个新的段,但是由于程序在没有PT_NOTE段时仍将执行,因此将其转换为PT_LOAD类型。
PT_NOTE到PT_LOAD转换感染算法
1.定位data段phdr。
- 找到data段结束的地址:ds_end_addr = phdr->p_vaddr + p_memsz
- 找到data段结束的文件偏移量:da_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头的修改和新的段,插入真正的寄生代码。
感染控制流
直接PLT感染
修改PLT代码,使其存放一条完全不同的指令来改变控制流。
函数蹦床
使用某种能够改变控制流的分支指令重写函数代码的前5~7个字节。重写完后调用的的即为要想调用的函数。
重写.ctors/.dtors函数指针
.ctors/.init_array 函数指针会在 main()函数调用之前触发。这就意味着,可以通过重写某个指向正确地址的指针来将控制流指向病毒或者寄生代码。.dtors/.fini_array 函数指针在 main()函数执行完之后才被触发。
GOT感染或PLT/GOT重定向
相关论文: Modern Day ELF Runtime infection via GOT poisoning(http://vxheaven.org/lib/vrn00.html)
感染数据结构
函数指针重写
进程内存病毒和rootkits——远程代码注入技术
共享库注入
.so感染/ET_DYN感染
这项技术可以用来将一个共享库注入到已存在的进程地址空间中,注入共享库后,需要通过PLT/GOT重定向、函数蹦床等将控制流使用前面的感染点之一重定向到共享库。
.so感染——使用LD_PRELOAD
通过设置LD_PRELOAD环境变量,将我们想要的共享库放在其他共享库之前加载。
.so感染——利用open()/mmap()
通过往已存在的进程的text段注入shellcode,并执行shellcode。利用共享库的open/mmap操作,将任何文件注入到进程的地址空间中。
.so感染——利用dloen()
程序可以通过dlopen()/_libc_dlopen_mode()函数凭空加载一个共享库。
但在使用_libc_dlopen_mode()之前,要先得到想要感染进程中的libc基址,解析_libc_dlopen_mode()的符号,然后将符号值st_value与libc相加得到最终地址。
.so感染——使用vdso控制技术
text段代码注入
可执行文件注入
elfdemon: http://www.bitlackeys.org/projects/elfdemon.tgz
重定位代码注入——ET_REL注入
ELF反调试和封装技术
PTRACE_TRACEME技术
进程追踪有一个特性:一个程序在同一时间只能被一个进程追踪。
这项技术的思路就是让程序追踪自身,这样调试器就无法附加到该进程了。
SIGTRAP处理技术
使用这项技术,程序可以设置一个信号处理器来捕获SIGTRAP信号,然后故意发出一个断点指令,信号处理器捕捉到SIGTRAP信号后,会将一个全局变量从0加到1。随后检查这个全局变量,如果为1,则说明未被调试;如果为0,说明正在被调试,为了防止被调试,可以终止进程或者退出。
/proc/self/status技术
/proc/self/status中的”TracerPid”对应值0表示没用被追踪,如果不为0,则表明正在被追踪。
代码混淆技术
通过修改汇编层的代码来引入不明确的分支指令或者未对其指令,使得反汇编程序无法正确地读取字节码文件。
字符串表转换技术
这项技术会打乱每个符号名和节相关信息的顺序,以致可能出现的结果就是所有的节头、函数名和符号名看上去都是乱序混在一起的。
相关代码:http://www.bitlackeys.org/projects/elfscure.c
ELF病毒检测和杀毒
VMA Voodoo(http://www.bitlackeys.org/#vmavudu)
AVU(Anti Virus UNIX):http://www.bitlackeys.org/projects/avu32.tgz
Linux二进制保护
ELF二进制加壳器
加壳器:用来对可执行文件进行压缩或加密,来对代码和数据进行混淆。
存根机制和用户层执行
软件保护器由以下两个程序组成:保护阶段的代码(应用到目标二进制文件上的保护程序)、运行时引擎或存根(与目标二进制文件合并在一起,负责运行时反混淆和反调试的程序。)