Base of Vulnerability Analysis
Published:
漏洞分析基础按照程序编译、执行和分析划分,主要描述一下编译部分。执行是把程序装载进内存然后配合处理器逐步执行,涉及到汇编指令和内存地址,分析主要是静态分析和动态分析的指令和工具,这俩部分没啥总结的必要,用的时候查就完了
编译分为四个阶段:预处理(Preprocessing)、编译(Compilation)、汇编(Assembly)、链接(Linking)。
预处理阶段:
- 预处理器(cpp)将所有的#define删除,并且展开所有的宏定义。
- 处理所有的条件预编译指令,比如#if、#ifdef、#elif、#else、#endif等。
- 处理#include预编译指令,将被包含的文件直接插入到预编译指令的位置。
- 删除所有的注释。
- 添加行号和文件标识,以便编译时产生调试用的行号及编译错误警告行号。
- 保留所有的#pragma编译器指令,因为编译器需要使用它们。
编译阶段:
编译器(ccl)将预处理完的文本文件进行一系列的词法分析、语法分析、语义分析和优化,翻译成包含一个汇编语言程序的文本文件。 编译过程可分为6步:扫描(词法分析)、语法分析、语义分析、源代码优化、代码生成、目标代码优化。
- 词法分析:扫描器(Scanner)将源代的字符序列分割成一系列的记号(Token)。lex工具可实现词法扫描。
- 语法分析:语法分析器将记号(Token)产生语法树(Syntax Tree)。yacc工具可实现语法分析(yacc: Yet Another Compiler Compiler)。
- 语义分析:静态语义(在编译器可以确定的语义)、动态语义(只能在运行期才能确定的语义)。
- 源代码优化:源代码优化器(Source Code Optimizer),将整个语法书转化为中间代码(Intermediate Code)(中间代码是与目标机器和运行环境无关的)。中间代码使得编译器被分为前端和后端。编译器前端负责产生机器无关的中间代码;编译器后端将中间代码转化为目标机器代码。
- 目标代码生成:代码生成器(Code Generator).
- 目标代码优化:目标代码优化器(Target Code Optimizer)。
汇编阶段:
汇编器(as)将汇编指令翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在二进制文件中。
链接阶段:
连接器(ld)就负责合并程序执行所需的各种可重定位目标程序文件,得到一个可执行目标文件(或者称为可执行文件),可以被加载到内存中,由系统执行。(链接程序运行需要的一大堆目标文件,以及所依赖的其它库文件,最后生成可执行文件)
静态链接:
静态连接器(static linker)以一组可重定位目标文件和命令行参数作为输入,生成一个完全链接的、可以加载和运行的可执行目标文件作为输出。输入的可重定位目标文件由各种不同的代码和数据节(selection)组成,每一节都是一个连续的字节序列。
ELF头(ELF header)以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息。其中包括ELF头的大小、目标文件的类型(如可重定位、可执行或者共享的)、机器类型(如x86-64)、节头部表(section header table)的文件偏移、以及节头部表中条目大小和数量。不同节的位置和大小是由节头部表描述的,其中目标文件中每个节都有一个固定大小的条目(entry)。
夹在ELF头和节头部表之间的都是节。一个典型的ELF可重定位目标文件包含下面几个节:
- .text:已编译程序的机器代码。
- .rodata:只读数据,比如printf语句中的格式串和开关语句的跳转表。
- .data:已初始化的的全局和静态C变量。局部C变量在运行时被保存在栈中,即不出现在.data节中,又不出现在.bss节中。
- .bss:未初始化的全局和静态C变量,以及所有被初始化为0的全局或静态变量。在目标文件中这个节不占据实际的空间,它仅仅是一个占位符。目标文件格式区分已初始化和未初始化变量是为了空间效率:在目标文件中,未初始化变量不需要占据任何实际的磁盘空间。运行时,在内存中分配这些变量,初始值为0。
- .symtab:一个符号表,它存放着在程序中定义和引用的函数和全局变量的信息。
- .rel.text:一个.text节中位置的列表,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。一般而言,任何调用外部函数或者引用全局变量的指令都需要修改。另一方面,调用本地函数的指令则不需要修改。
- .rel.data:被模块引用或定义的所有全局变量的重定位信息。一般而言,任何已初始化的全局变量,如果它的初始值是一个全局变量地址或者外部定义函数的地址,都需要被修改。
- .debug:一个调试符号表,其条目是程序定义的局部变量和类型定义,程序中定义和引用全局变量,以及原始的C源文件。
- .line:原始C源程序中的行号和.text节中的机器指令之间的映射。
- .strtab:一个字符串表,其内容包括.symtab和.debug节中的符号表,以及节头部中的节名字。字符串表就是以null结尾的字符串序列。
静态链接过程:
- 空间和地址分配:扫描所有的输入目标文件,获得他们的各个段的长度、属性和位置,并且将输入目标文件中的符号表中所有的符号定义和符号引用收集起来,统一放到一个全局符号表。这样,链接器将能够获得所有输入目标文件的段长度,并且将它们合并,计算出输出文件中各个段合并后的长度与位置,并利用格式一致的进程虚拟地址空间建立映射关系。
- 符号解析(symbol resolution)和重定位(relocation)
- 符号表:每个可重定位目标模块都有一个符号表,它包含模块定义和引用的符号的信息。在链接器的上下文中,有三种不同的符号(由模块定义并能被其他模块引用的全局符号,全局链接器符号对应非静态的函数和全局变量;由其他模块定义并被模块引用的全局符号,这些符号被称为外部符号,对应于在其他模块定义的非静态函数和全局变量;只被模块定义和引用的局部符号,它们对应于带static属性的C函数和全局变量,这些符号在模块任何位置都可见,但是不能被其它模块引用。).symtab中的符号表不包含对应于本地非静态程序变量的任何符号,这些符号在运行时在栈中被管理。Cstatic属性的本地过程变量是不在栈中管理的。
- 符号解析:目标文件定义和引用符号,每个符号对应于一个函数、一个全局变量或一个静态变量(即C语言中任何以static属性声明的变量)。符号解析的目的是将每个符号引用正好和一个符号定义关联起来。如果找不到,则会出现编译时错误。链接器解析符号引用的方法是将每个引用与它输入的可重定位目标文件的符号表中的一个确定的符号定义关联起来。当编译器遇到一个不是在当前模块中定义的符号(变量和函数名)时,会假设该符号是在其他某个模块中定义的,生成一个链接器符号表条目,并把它交给链接器处理。如果链接器在它的任何输入模块中都找不到这个被引用符号的定义,就输出一条错误信息并终止。
- 在编译时,编译器向汇编器输出每个全局符号,或者是强(strong)或者是弱(weak),而汇编器把这个信息隐含地编码在可重定位目标文件的符号表里。函数和已初始化的全局变量是强符号,未初始化的全局变量是弱符号。根据强弱符号的定义,Linux链接器使用下面的规则来处理多重定义的符号名:
- 不允许有多个同名的强符号。
- 如果有一个强符号和多个弱符号同名,那么选择强符号。
- 如果有多个弱符号同名,那么从这些弱符号中任意选择一个。
- 重定位:编译器和汇编器生成从地址0开始的代码和数据节。链接器通过把每个符号定义与一个内存位置关联起来,从而重定位这些节,然后修改所有对这些符号的引用,使它指向这个内存位置。重定位步骤合并输入模块,并为每个符号分配运行时的地址。重定位有两步组成:
- 重定位节和符号定义:链接器将所有的相同类型的节合并成为同一类型的新的聚合节。例如,来自所有输入模块的.data节被全部合并成为一个节,这个节称为输出的可执行目标文件的.data节。然后链接器将运行时内存地址赋给新的聚合节,赋给输入模块定义的每一个节,以及赋给输入模块定义的每个符号。当这一步完成时,程序中的每条指令和全局变量都有唯一的运行时内存地址了。
- 重定位节中的符号引用:链接器修改代码节和数据节中对每个符号的引用,使的它们指向正确的运行地址。要执行这一步,链接器依赖于可重定位目标模块中称为重定位条目的数据结构。链接完成后,链接器就把多个目标文件合并成为了一个可执行的目标文件。
动态链接共享库:
静态库和所有的软件一样,需要定期地维护和更新,使用时需要显式地将他们的程序与更新的库重新链接。静态链接的情况下,每个可执行模块都要保存和加载一个静态库的编译副本,极大地浪费内存空间。共享库(shared library)致力于解决静态库缺陷。共享库是一个目标模块,在运行或加载时,可以加载到任意的内存地址,并和一个在内存中的程序链接起来。这个过程称为动态链接(dynamic linking),是由一个叫动态链接器(dynamic linker)的程序来执行的。
在任何给定的文件系统中,对于一个库只有一个.so文件。所有引用该库的可执行目标文件共享这个.so文件中的代码和数据,而不是像静态库的内容那样被复制和嵌入到引用它们的可执行文件中。
- 一个.text节的一个副本可以被不同的正在运行的进程共享。
- 文件的形式使得它在运行时可以和动态库链接。基本思路是当创建可执行文件时,静态执行一些链接,然后在程序加载时,动态完成链接。此时,没有任何动态库的代码和数据节真的被复制到可执行文件中。反之,链接器复制了一些重定位和符号表的信息,它们使得运行时可以解析对动态库中代码和数据的引用。
- 当加载器加载和运行可执行文件时,首先加载部分链接的可执行文件,随后注意到其中包含的.interp节,这个节包含动态链接器的路径名,动态链接器本身就是一个共享目标。加载器不会像它通常所作地那样将控制传递给应用,而是加载和运行这个动态链接器。然后,动态链接器通过执行下面的重定位完成链接任务:
- 重定位动态库的文本和数据到某个内存段。
- 重定位动态库的文本和数据到另一个内存段。
- 重定位可执行文件中所有对动态库定义和符号的引用。
- 最后动态链接器将控制传递给应用程序。共享库的位置就固定了,并且在程序执行的过程中都不会变。