0%

Linux-程序执行

阅读更多

1 ELF

ELFExecutable and Linkable Format的缩写,它定义二进制文件,库文件的结构。ELF文件通常是编译器或链接器的输出,并且是二进制格式

1.1 ELF格式详解

ELF大致包含ELF headersprogram header tablesection header table

可以通过man 5 ELF查看详细介绍

1.1.1 ELF headers

可以通过readelf -h <binary>查看详细介绍

对应的数据结构如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typedef struct {
unsigned char e_ident[EI_NIDENT]; // ELF的Magic Number
uint16_t e_type; // 描述了ELF文件的类型
uint16_t e_machine; // 描述了文件面向的架构
uint32_t e_version; // 描述了ELF文件的版本号
ElfN_Addr e_entry; // 执行入口点,如果文件没有入口点,这个域保持0
ElfN_Off e_phoff; // program header table的offset,如果文件没有PH,这个值是0
ElfN_Off e_shoff; // section header table的offset,如果文件没有SH,这个值是0
uint32_t e_flags; // 特定于处理器的标志,32位和64位Intel架构都没有定义标志,因此eflags的值是0
uint16_t e_ehsize; // ELF header的大小,32位ELF是52字节,64位是64字节
uint16_t e_phentsize; // program header table中每个条目的大小
uint16_t e_phnum; // program header table中header的数目。如果文件没有program header table, e_phnum的值为0。e_phentsize乘以e_phnum就得到了整个program header table的大小
uint16_t e_shentsize; // section header table中每个条目的大小
uint16_t e_shnum; // section header table中header的数目。如果文件没有section header table, e_shnum的值为0。e_shentsize乘以e_shnum,就得到了整个section header table的大小
uint16_t e_shstrndx; // section header string table index. 包含了section header table中section name string table
} ElfN_Ehdr;

1.1.2 Section

目标代码文件中的sectionsection header table中的条目是一一对应的。section的信息用于链接器对代码重定位。下面列了系统预定义的section

  • .bss:程序运行时未初始化的数据。当程序运行时,这些数据初始化为0
  • .comment:版本控制信息
  • .ctors:c++构造方法的指针
  • .data/.data1:包含初始化的全局变量和静态变量
  • .debug:符号调试用的信息(与gdb这类调试工具有关)
  • .dtors:c++析构函数的指针
  • .dynamic:动态链接的信息
  • .dynstr:动态链接相关字符串,通常是和符号表中的符号关联的字符串
  • .dynsym:动态链接符号表
  • .fini:程序正常结束时需要执行的指令
  • .got:全局偏移表(global offset table
  • .hash:符号hash表
  • .init:程序运行时需要执行的指令
  • .interp:程序解释器的路径名
  • .line:包含符号调试的行号信息,描述了源程序和机器代码的对应关系(与gdb这类调试工具有关)
  • .plt:过程链接表(Procedure Linkage Table
  • .rodata/.rodata1:只读数据,组成不可写的段
  • .shstrtab:包含section的名字。section header中不是已经包含名字了吗,为什么把名字集中存放在这里?sh_name包含的是.shstrtab中的索引,真正的字符串存储在.shstrtab
  • .strtab:包含字符串,通常是符号表中符号对应的变量名字
  • .symtab:符号表(Symbol Table
  • .text:包含文本或程序的可执行的指令

可以通过readelf -S <name>查看section header table以及section信息

1.1.3 Segment

可执行文件载入内存执行时,是以segment组织的,每个segment对应ELF文件中program header table中的一个条目,用来建立可执行文件的进程映像。比如我们通常说的,代码段数据段,目标代码中的section会被链接器组织到可执行文件的各个segment中(一个segment可以包含0个或多个section),例如.text的内容会组装到代码段中;.data.bss等节的内容会包含在数据段中

可以通过readelf -l <name>查看program header table以及segment信息

1.2 参考

2 程序如何加载执行

系统调用sys_execve的调用栈如下(内核版本3.10.10)

1
2
3
4
5
6
7
8
9
# syscall
sys_execve | fs/exec.c SYSCALL_DEFINE3(execve
do_execve | fs/exec.c
do_execve_common | fs/exec.c
search_binary_handler | fs/exec.c
load_binary | fs/exec.c
⬇️ linux_binfmt --> elf_format
# elf
load_elf_binary | fs/binfmt_elf.c

load_elf_binary的主要步骤如下

  1. 读取并检查ELF headers | elfhdr
  2. 读取并检查program header table | elf_phdr
  3. 处理动态链接PT_INTERP | elf_interpreter
  4. 检查是否可执行栈PT_GNU_STACK | executable_stack
  5. 载入目标程序必要的segmentPT_LOAD
  6. 填写程序的入口地址
    • 如果需要解释器,就通过load_elf_interp函数进行加载
    • 如果不需要解释器,那么入口就是本身的入口地址
  7. 执行前的准备,例如环境变量等
  8. 调用start_thread函数准备执行此程序

2.1 binfmt_misc

在Windows平台上,可以绑定拥有特定扩展名的文件,使用特定的程序打开。比如,PDF文件就使用Acrobat Reader打开。这样做确实极大的方便了用户的使用体验

其实,在Linux平台上,也提供了类似的功能,甚至从某种意义上来说更加的强大。Linux的内核从很早开始就引入了一个叫做Miscellaneous Binary Format(binfmt_misc)的机制,可以通过要打开文件的特性来选择到底使用哪个程序来打开。比Windows更加强大的地方是,它不光光可以通过文件的扩展名来判断的,还可以通过文件开始位置的特殊的字节(Magic Byte)来判断

如果要使用这个功能的话,首先要绑定binfmt_misc,可以通过以下命令来绑定:

1
mount binfmt_misc -t binfmt_misc /proc/sys/fs/binfmt_misc

这样绑定的话,系统重新启动之后就失效了。如果想让系统每次启动的时候都自动绑定的话,可以往/etc/fstab文件中加入下面这行:

1
none  /proc/sys/fs/binfmt_misc binfmt_misc defaults 0 0

绑定完之后,就可以通过向/proc/sys/fs/binfmt_misc/register(这个文件只能写不能读)文件中写入一行匹配规则字符串来告诉内核什么样的程序要用什么样的程序打开(一般使用echo命令)。这行字符串的格式如下:

1
:name:type:offset:magic:mask:interpreter:flags

每个字段都用冒号:分割。某些字段拥有默认值,或者只在前面字段被设置成了某个特定值后才有效,因此可以跳过某些字段的设置,但是必须保留相应的冒号分割符。各个字段的意义如下

  • name:这个规则的名字,理论上可以取任何名字,只要不重名就可以了。但是为了方便以后维护一般都取一个有意义的名字,比如表示被打开文件特性的名字,或者要打开这个文件的程序的名字等
  • type:表示如何匹配被打开的文件,只可以使用E或者M,只能选其一,两者不可共用。E代表只根据待打开文件的扩展名来识别,而M表示只根据待打开文件特定位置的几位魔数(Magic Byte)来识别
  • offset:这个字段只对前面type字段设置成M之后才有效,它表示从文件的多少偏移开始查找要匹配的魔数。如果跳过这个字断不设置的话,默认就是0
  • magic:如果type字段设置成M的话,它表示真正要匹配的魔数;如果type字段设置成E的话,它表示文件的扩展名。对于匹配魔数来说,如果要匹配的魔数是ASCII码可见字符,可以直接输入,而如果是不可见的话,可以输入其16进制数值,前面加上\x或者\\x(如果在Shell环境中的话。对于匹配文件扩展名来说,就在这里写上文件的扩展名,但不要包括扩展名前面的点号(.),且这个扩展名是大小写敏感的,有些特殊的字符,例如目录分隔符正斜杠(/)是不允许输入的
  • mask:同样,这个字段只对前面type字段设置成M之后才有效。它表示要匹配哪些位,它的长度要和magic字段魔数的长度一致。如果某一位为1,表示这一位必须要与magic对应的位匹配;如果对应的位为0,表示忽略对这一位的匹配,取什么值都可以。如果是0xff的话,即表示全部位都要匹配,默认情况下,如果不设置这个字段的话,表示要与magic全部匹配(即等效于所有都设置成0xff)。还有同样对于NUL来说,要使用转义(\x00),否则对这行字符串的解释将到NUL停止,后面的不再起作用;
  • interpreter:表示要用哪个程序来启动这个类型的文件,一定要使用全路径名,不要使用相对路径名
  • flags:这个字段可选,主要用来控制interpreter打开文件的行为。比较常用的是P(请注意,一定要大写),表示保留原始的argv[0]参数。这是什么意思呢?默认情况下,如果不设置这个标志的话,binfmt_misc会将传给interpreter的第一个参数,即argv[0],修改成要被打开文件的全路径名。当设置了P之后,binfmt_misc会保留原来的argv[0],在原来的argv[0]argv[1]之间插入一个参数,用来存放要被打开文件的全路径名。比如,如果想用程序/bin/foo来打开/usr/local/bin/blah这个文件,如果不设置P的话,传给程序/bin/foo的参数列表argv[]["/usr/local/bin/blah", "blah"],而如果设置了P之后,程序/bin/foo得到的参数列表是["/bin/foo", "/usr/local/bin/blah", "blah"]

每次成功写入一行规则,都会在/proc/sys/fs/binfmt_misc/目录下,创建一个名字为输入的匹配规则字符串中name字段的文件

/proc/sys/fs/binfmt_misc/目录下,还缺省存在一个叫做status的文件,通过它可以查看和控制整个binfmt_misc的状态,而不光是单个匹配规则

可以查看当前binfmt_misc是否处于打开状态:

1
cat /proc/sys/fs/binfmt_misc/status

也可以通过向它写入1或0来打开或关闭binfmt_misc

1
2
echo 0 > /proc/sys/fs/binfmt_misc/status    # Disable binfmt_misc
echo 1 > /proc/sys/fs/binfmt_misc/status # Enable binfmt_misc

如果想删除当前binfmt_misc中的所有匹配规则,可以向其传入-1:

1
echo -1 > /proc/sys/fs/binfmt_misc/status

2.2 Relro

通过覆盖.got可以达到漏洞攻击的目的,.got覆盖之所以能成功是因为默认编译的应用程序的重定位表段对应数据区域是可写的(如.got.plt),这与链接器和加载器的运行机制有关,默认情况下应用程序的导入函数只有在调用时才去执行加载(所谓的lazy loading,非内联或显示通过dlxxx指定直接加载),如果让这样的数据区域属性变成只读将大大增加安全性。RELRO(read only relocation)是一种用于加强对数据段的保护的技术,大概实现由linker指定binary的一块经过dynamic linker处理过relocation之后的区域为只读,设置符号重定向表格为只读或在程序启动时就解析并绑定所有动态符号,从而减少对.got攻击。RELRO分为partialfull

2.3 参考