前言:

  • 这里加上PWN入门其实也不太准确,但是笔者个人认为这是学习PWN的必经之路。需要一步步稳扎稳打。基础原理不牢固往往在前期表现得没有那么明显,别人说什么相关的东西,自己都懂一点,但是要自己讲却讲不出个所以然,这也是基础不扎实的体现之一。自己写这个系列原因之一也不乏有自己基础知识太过于薄弱,在此写成文章也希望对读者有些许帮助。如果发现有误希望你能够及时联系我进行修正。
  • 二进制安全是一个比较偏向于底层的,对基础知识要求比较高,本系列为笔者本人对知识系统的回顾与总结,包括但不限于自己之前所学以及一些自己的见解,主要是对基础原理内容进行概述。

I、二进制文件

1.1源代码到可执行文件

1.1.1编译原理:

编译过程分为:
(1)词法分析(Lexical analysis):读入源程序的字符流,输出为有意义的词素(Lexeme);
(2)语法分析(Syntax analysis):根据各个词法单元的第一个分量来创建树型的中间表示形式,通常是语法树(Syntax tree);
(3)语义分析(Semantic analysis):使用语法树和符号表中的信息,检测源程序是否满足语言定义的语义约束,同时收集类型信息,用于代码生成、类型检查和类型转换;
(4)中间代码生成和优化:根据语义分析输出,生成类机器语言的中间表示,如三地址码。然后对生成的中间代码进行分析和优化;
(5)代码的生成和优化:把中间形式映射到目标机器语言。

大致流程可以细分为:
字符流->【词法分析器】-符号流->【语法分析器】-语法树->【语义分析器】-语法树->【中间代码生成器】-中间表示形式->【机器无关代码优化器】-中间表示形式->【代码生成器】-目标机器语言->【机器相关代码优化器】-目标机器语言->

1.1.2 GCC编译过程

GCC编译主要包括四个阶段:
预处理(Preprocess)---》编译(Compile)---》汇编(Assemble)---》链接(Link)

1.1.3预处理阶段

预处理规则:

  1. 递归处理“#include”预处理指令,将对应文件的内容复制到该指令的位置;
  2. 删除所有的“#define”指令,并且在其引用的位置递归地展开所有的宏定义;
  3. 处理所有条件预处理指令:“#if”、“#ifdef”、“#elif”、“#endif”;
  4. 删除所有的注释;
  5. 添加行号和文件名标识。

1.1.4编译阶段

GCC编译的第二阶段是编译,该阶段将预处理文件进行一系列的词法分析、语法分析、语义分析及优化,最终生成汇编代码。(实际在GCC的实现中,已经将预处理和编译合并处理)

GCC的优化策略会将一些函数进行替换以提高函数性能。

1.1.5汇编阶段

GCC编译的第三阶段是汇编,汇编器根据汇编指令和机器指令的对照表进行翻译,将a.s汇编成a.o。在命令中添加参数

“-c”操作对象可以是a.s也可以是a.c,经过预处理、编译和汇编直接生成目标文件。

1.1.6链接阶段

GCC的第四阶段是链接,可分为静态链接和动态链接。GCC默认使用动态链接,天界编译选项“-static”可以指定使用静态链接。这一阶段将目标文件及其依赖库进行链接,生成可执行文件,主要包括地址和空间分配(Address and Storage Allocation)、符号绑定(Symbol Binding)和重定位(Relocation)等操作。
链接操作由链接器(ld.so)完成,结果就得到一个静态链接的可执行文件(Executable File),其中包含了大量的库文件。

通过链接操作,对象文件中无法确定的符号地址已经被修正为实际的符号地址,程序也就可以被加载到内存中正常执行了。

1.2 ELF文件格式

ELF(Executable and Linkable Format),即“可执行可链接格式”。Linux系统上所运行的就是ELF格式的文件。

1.2.1 ELF文件类型

ELF文件分为三种类型,可执行文件(.exec)、可重定位文件(.rel)和共享目标文件(.dyn)

  • 可执行文件(executable file):经过链接的、可执行的目标文件,通常也被称之为程序。
  • 可重定位文件(relocatable file):由源文件编译而成且尚未链接的目标文件,通常以“.o”作为扩展名。用于与其他目标文件进行链接以构成可执行文件或动态链接库,通常是一段位置独立的代码(Position Independent Code,PIC)。
  • 共享目标文件(shared object file):动态链接库文件。用于在链接过程中与其他动态链接库或可重定位文件一起构建新的目标文件,或者在可执行文件加载时,链接到进程中作为运行代码的一部分。

其实还有一种核心转储文件(Core Dump file)作为进程意外终止时进程地址空间的转储,也是ELF文件的一种。在使用gdb读取这类文件可以辅助调试查找程序崩溃的原因。

1.2.2 ELF文件结构

Linux下的可执行文件格式为ELF(Executable and Linkable Format),类似Windows的PE格式。PWN手最需要了解的就是ELF头 (ELF header)、节(Section)、段(Segment)的概念。

大致结构整理为:

807220-faff608c1d913c16.png

ELF头必须在文件的开头,表示这是个ELF文件及其基本信息。ELF头包括ELF的magic code、程序运行的计算机架构、程序入口、段表和节表的位置和长度等内容,可以通过“readelf -h”命令读取其内容,一般多用于寻找一些程序的入口。值得注意的是文件头部存在魔术字符(7f 45 4c 46),即字符串“\177ELF”,当文件被映射到内存的时候,可以通过搜索该字符确定映射地址,这在dump内存时非常有用。
ELF文件由多个节(Section)组成,其中存放各种数据。描述节的各种信息的数据统一存放在节头表中。ELF中的节用来存放各种各样不同的数据,主要包括:
  • .text节===存放一个程序运行所需的所有代码。
  • .rdata节===存放程序使用到的不可修改的静态数据,如字符串等。
  • .data节===存放程序可修改数据,如C语言中已经初始化的全局变量等。
  • .bss节===用于存放程序的可修改数据,与.data不同的是,这些数据没有被初始化,所以没有占用ELF空间。虽然在节头表中存在.bss节,但是文件中并没有对应的数据。在程序开始执行后,系统才会申请一块空内存来作为实际的.bss节。
  • .plt节和.got节===程序调用动态链接库(.so文件)中函数时,需要这两个节配合,以获取被调用函数的地址。

在审视目标文件时,可以通过两种视角来进行划分,一种是链接视角,通过节(Section)来进行划分,另一种是运行视角,通过段(Segment)来进行划分

两种视角如下所示:

pwn3.jpg

链接视角:用于保存已初始化的全局变量和静态变量,BSS节则用于保存未初始化的全局变量和局部静态变量。
通常目标文件都会包含代码(.text)、数据(data)和BSS(.bss)三个节。其中代码节用于保存可执行的机器指令,数据节

1.2.3可执行文件的装载(运行视角):

运行可执行文件时,首先将可执行文件和动态链接库装载到进程空间形成进程镜像,每个进程都具有独立的虚拟地址空间,这个空间由段头表中的程序头(program header)决定。

​ 每个段包含一个或多个节,相当于对节进行分组。

​ 不关心节的实际内容,而是关心节的读、写、执行权限,将不同权限的节分组即可同时装载多个节。如.data和.bss具有读和写的权限,而.text和.plt.got具有读和执行的权限。

常见段:

PT_LOAD:可执行文件至少有一个,描述可装载的节;动态链接的可执行文件则包含两个,将.data和.text分开存放
PT_DYNAMIC:包含动态链接的必要信息,如共享库列表和GOT重定位表等。
PT_NOTE:保存了系统相关的附加信息
PT_INTERP:将段的位置和大小信息存储在一个字符串中
PT_PHDR:保存了程序头标本身的位置和大小

1.3静态链接

1.3.1地址空间分配

当两个或多个不同目标文件组成一个可执行文件时,就需要进行链接(linking)。链接由链接器(linker)完成,根据发生的时间不同,可分为编译时链接(compile time)、加载时链接(load time)和运行时链接(run time)。
将目标文件链接时,最简单的办法是按序叠加,容易造成内存的浪费。
另一种办法是相似节合并。比如将main.o与func.o的.text节合并为新的.text节,两者的.data节合并为新的.data节:
首先对各个节的长度、属性、偏移进行分析,然后将目标文件中的符号表的符号定义与符号引用同一生成为全局符号表
最后读取输入文件的各类信息对符号进行解析、重定位等操作。相似节的合并就发生在重定位时。完成后,程序中的每条指令和全局变量就都有唯一的运行时内存地址了。

1.3.2静态链接的详细过程

为了构造可执行文件,链接器必须完成两个重要工作:符号解析(symbol resolution)和重定位(relocation)。其中符号解析是将每个符号的定义与一个内存地址进行关联,然后修改符号引用,使其指向这个内存地址。

VMA(Virtual Memory Address)是虚拟地址,LMA(Load Memory Address)是加载地址,一般情况下是相同的。尚未进行链接的目标文件.o的VMA都是0。而在链接完成后的.ELF文件中,相似节被合并,且完成了虚拟地址的分配。

可重定位文件中最重要的就是包含重定位表,告诉链接器如何修改节的内容。每一个重定位表对应一个需要被重定位的节。

​ 如.rel.text的节用于保存.text节的重定位表,包含两个入口,shared的类型R_X86_64_32用于绝对寻址,CPU直接使用指令中编码的32位作为有效地址;func的类型R_X86_64_32用于相对寻址,CPU将指令中编码的32位值加上PC(下一条指令地址)的值作为有效地址。

1.3.3静态链接库

​ .a为后缀名的文件是静态链接库文件,如libc.a。
一组静态链接库可以视为一组目标文件经过压缩打包后形成的集合。人们将执行各种编译任务时需要的不同目标文件进行压缩、编号和索引,就形成了libc.a

1.4动态链接库

1.4.1动态链接定义

随着目标文件增多,每加入一个相同的静态链接库都会增加内存开销。
​ 于是,不把库和代码链接到一个可执行文件,而是分开到两个独立的模块,真正运行时再链接,且一个系统库被多个程序共同使用,这就是动态链接。

                            

      静态链接                                动态链接                            
               Memory                               Memory              
 a1.ELF       |———————| 0XFFFFFFFF    a1.ELF       |———————| 0XFFFFFFFF
 ======       |———————|               ======       |———————|
|a1.o  |      | a1.o  |              |a1.o  |      | a1.o  |
 ======       |———————|               ======       |———————| 
|lib.o |      | lib.o |                            |       |
 ======       |———————|               a2.ELF       |       |  
              |       |               ======       |       |
 a2.ELF       |       |              |a2.o  |      |———————|           
 ======       |———————|               ======       | a2.o  |
|a2.o  |      | a2.o  |                            |———————|
 ======       |———————|               ======       |———————|
|lib.o |      | lib.o |              |lib.o |      | lib.o |
 ======       |———————| 0X00000000    ======       |———————| 0X00000000
             

1.4.2位置无关代码

可以加载而无需重定位的代码即位置无关代码(Position-Independ Code,PIC)。通过PIC,一个共享库的代码可以被无限多的进程所共享。

GOT

​ 由于一个程序(或共享库)的数据段和代码段的相对距离总是保持不变,因此指令常量和变量的距离是一个运行时常量,与绝对内存地址无关。因此有了全局偏移量表(Global Offset Table,GOT),位于数据段的开头,用于保存全局变量和库函数的引用,每个条目占8字节,在加载时会进行重定位并填入符号的绝对地址。

​ 为了引入RELRO保护机制,GOT被拆分为.got节和.got.plt节两个部分。不需要延迟绑定的前者用于保存全局变量引用,加载到内存后标记为只读。需要延迟绑定的后者用于保存函数引用,具有读写权限。

1.4.3延迟绑定

函数第一次被调用时动态链接器才进行符号查找、重定位等操作,未调用则不绑定。

​ ELF文件通过过程链接表(PLT)和GOT表的配合来实现。

​ 位于代码段的.plt节的PLT是一个数组,每个条目占16字节。

PLT[0]用于跳转到链接器
PLT[1]用于调用系统启动函数(我们熟悉的main函数)
PLT[2]开始是各个函数的条目
而对于GOT,每个条目占8字节:

GOT[0]和GOT[1]保存用于解析函数地址的两个地址。
GOT[2]是动态链接器的入口
GOT[3]开始是被调用的各个函数的条目,默认是指向PLT条目的第二条指令,完成绑定后才会被修改为函数的实际地址。

GOT和PLT的作用
    ELF文件中通常存在.GOT.PLT和.PLT这两个特殊的节,ELF编译时无法知道libc等动态链接库的的加载地址。如果一个程序想调用动态链接库中的函数,就必须使用.GOT.PLT和.PLT配合完成调用。
    ELF中所有用到的外部动态链接库函数都会有对应的PLT项目。
    .PLT表还是一段代码,作用是从内存中取出一个地址然后跳转,取出的地址便是函数的实际地址,而这个存放函数的实际地址的地方就是.GOT.PLT表。
    .GOT.PLT表其实就是一个函数指针数组,数组中保存着ELF中所有用到的外部函数地址。.GOR.PLT表的初始化工作则由操作系统来完成。
    当然,由于Linux非常特殊的Lazy Binding机制,在没有开启Full Rello的ELF中,.GOT.PLT表的初始化是在第一次调用该函数的过程中完成的。即某个函数必须被调用过,.GOT.PLT表才会存放函数的真实地址。
    在这一章讲这个实际上是偏向于这个对PWN来说的作用:首先,.PLT可以直接调用某个外部函数,这在栈溢出中会有很大的帮助;其次,由于.GOT.PLT中通常会存放libc中函数的地址,在漏洞利用中可以通过读取.GOT.PLT来获得libc的地址,或者通过写.GOT.PLT来控制程序的执行流。

标签: none

暂无评论