【PWN之路】二进制基础
前言
CTF越来越火,没想到我们单位也组织战队参赛,可惜的是PWN学习路径过于陡峭,短期又看不到成绩,此位空缺。想到大学时跟着台湾ROCK老师搞过一阵逆向,兴趣也有,决定从头复习一遍,谁叫我们大学那会没有CTF这种东西呢。以下内容为本人的学习笔记,边学边记录,不对的地方欢迎大家指正。
提示:本内容仅供技术交流,不可用于违法用途
一、编译原理
1.编译器
编译器简单理解就是将一种语言(源语言)转换为等价的另一种语言(目标语言)的程序。
编译器分为前端(Front end)和后端(Back end)两部分。
- 前端:机器无关,把源程序分解成组成要素和相应的语法结构,通过这个结构创建源程序的中间表示,同时收集和程序相关的信息,存放到符号表中;
- 后端:机器相关,根据中间表示和符号表构造目标程序。
编译过程大致为5个步骤:
- 词法分析(Lexical analysis)-》输出有意义的词素(Lexeme)
- 语法分析(Syntax analysis)-》产生语法树(Syntax tree)
- 语义分析(Semantic analysis)
- 中间代码生成和优化
- 代码生成和优化
2.GCC编译过程
GCC编译主要有四个阶段:预处理(Preprocess)、编译(Compile)、汇编(Assemble)、链接(Link)。
三个工具组件:ccl(第一和第二阶段)、as(第三阶段)、collect2(对ld命令的封装,第四阶段)
1. 预处理阶段:处理以#开头的预处理指令,如#include、#define等,转换后直接插入程序文本中,通常以.i作为拓展名。
GCC -E hello.c -o hello.i #单独执行预处理
2. 编译阶段:执行词法分析、语法分析、语义分析以及优化,生成汇编代码。
#GCC中预处理已经和编译合并处理
GCC -S hello.c -o hello.s
#masm指定为intel格式,fno-asynchronous-unwind-tables生成没有cfi宏的汇编指令,以提高可读性。
GCC -S hello.i -o -masm=intel -fno-asynchronous-unwind-tables
3.汇编阶段:根据汇编指令和机器指令的对照表进行翻译,将.s翻译为目标文件.o。
GCC -c hello.c -o hello.o
GCC -c hello.s -o hello.o
4.链接阶段:分为静态链接和动态链接两种方式,默认使用动态链接,使用-static指定使用静态链接。将目标文件和依赖库进行链接,主要包括地址和空间分配、符号绑定、重定位等。
GCC hello.o -o hello
两个常用命令:file和objdump
file 文件名:查看文件详细信息
objdump -sd hello -M intel:反汇编
二、ELF文件格式
ELF(Executable and Linkable Format):可执行可链接格式,在Linux上运行,在/usr/include/elf.h中定义。
1.ELF文件类型
1. 可执行文件(.exec)
2. 可重定位文件(.rel)
3. 共享目标文件(.dyn)
2.ELF文件结构
视角选择:
链接视角:通过节(Section)来进行划分,通常包含代码(.text)、数据(.data)、BSS(.bss)三种节;
运行视角:通过段(Segment)来进行划分。
(1)ELF文件头
ELF文件头(ELF header)位于目标文件最开始的位置,包含整个文件的基本信息。
readelf -h elfDemo.rel #查看elf文件头信息
注意:文件头中存在魔术字符(Magic):7f 45 4c 46 即字符串“\177ELF”,可以通过搜索该字符确定内存映射地址。
(2)节头表
节头表(Section header table)包含了目标文件中所有节的信息,记录了节的名字、长度、偏移、读写权限等信息。节头表的位置记录在文件头的e_shoff域中。
readelf -S elfdemo.rel#查看目标文件节头表
注意:节头表对于程序运行不是必须的,因为它与程序内存和布局无关,是程序头表的任务,所以常有程序去除节头表,以增加反编译器的分析难度。
(3)代码节(.text)
objdump -x -s -d elfDemo.rel #输出各节的内容
Contents of section .text部分是.text数据的十六进制形式,共0x4e个字节,最左边一列是偏移量,中间四列为内容,最右边一列为ASCII码形式。
Disassembly of section .text部分是反汇编结果。
(4)数据节(.data)
保存以及初始化的全局变量和局部静态变量。
.rodata 保存只读数据,包含只读变量和字符串常量
(5)BSS节(.bss)
用于保存未初始化的全局变量和局部静态变量,没有Contents属性,只是为变量预留了位置,因此该节的sh_offset域也就没有意义。
(6)其他节
字符串表(.strtab)
用来表示符号名和节名,引用字符串表时只需给出字符徐磊在表中的偏移量即可
字符串表的第一个和最后一个字符均为null字符,以确保所有字符串的开始和终止。
符号表(.dynsym和.symtab)
.dynsym保存引用外部文件的符号,只在运行时被解析;
.symtab还保存了本地符号,用于调试和链接。
目标文件通过在该表中的索引值来使用该符号,索引值从0开始,但0的表项不具备实际意义,他表示未定义的符号。
3.可执行文件的装载
当运行一个可执行文件时,首先要将该文件和动态链接库装载到 进程空间中,形成一个进程镜像。
每个进程都有独立的虚拟地址空间,这个空间如何布局是由记录在 段头表 中的程序头决定的。
ELF文件头的e_phoff域给出了段头表的位置。
通过
readelf - l elefDemo.exec
可以看出一个段包含了一个或多个节,相当于对这些节按不同权限进行分组,使之可以同时装载多个节,从而节省资源。
常见的段:
PT_LOAD类型段:用于描述可装载的节(动态链接的可执行文件则包含两个–将.data和.text分开存放)
PT_DYNAMIC动态段:包含动态链接库所必须的信息,如共享库列表、GOT表和重定位表等。
PT_NOTE类型段:保存了系统相关的附加信息,运行并不需要
PT_INTERP段:将位置和大小信息存放在一个字符串中,是对程序解释器的位置的描述。
PT_PHDR段;保存了程序头表本身的位置和大小。
三、静态链接
1.地址空间分配
将两个目标文件链接为一个可执行文件,有两种方式:按序叠加和相似节合并。
- 按序叠加:最简单的方式,如果参与链接的目标文件过多,输出的可执行文件可能会非常零散,不足一页的代码节或数据节也要占一页,浪费内存空间。
- 相似节合并:对应的节进行合并成新的节,当前的连接器采用。
2.详细过程
链接器的两项重要工作:符号解析(Symbol resolution)和重定位(relocation)。
-
符号解析是将每个符号(函数、全局变量、静态变量)的引用与其定义进行关联。
-
重定位是将每个符号的定义与一个内存地址进行关联,然后修改这些符号的引用,使其指向这个内存地址。
使用
objdump -h xxxx.o#VMA是虚拟地址,LMA是加载地址
和
objdump -h xxxx.ELF#VMA是虚拟地址,LMA是加载地址
对比可以发现链接前目标文件.o的虚拟地址VMA均为0,链接后的ELF文件中相似节被合并,且完成了虚拟地址的分配。
符号地址在链接前的.o文件中均为临时地址,不是实际地址,链接后才填入真实地址。
可重定位文件中包含重定位表,用于告诉链接器如何修改节的内容。
3.静态链接库
使用ar工具将一组不同的目标文件进行压缩编号索引形成.a的静态链接库文件。
四、动态链接
由于静态链接浪费空间,且对标准函数做一点改动都要重新编译整个源文件,将系统库和开发的代码分割成两个独立的模块,运行程序时再进行链接,节省硬盘空间,同时系统库在内存中可以被多个程序共同使用,节省物理内存空间,就叫动态链接。
可以加载而无需重定位的代码称为位置无关代码(PIC),是共享库必须具有的属性,通过GCC传递-fpic参数可以生成PIC,通过PIC,一个共享库可以被无限多个进程所共享,从而节约内存资源。
指令和变量之间的距离是一个运行时常量,保持不变,与绝对内存地址无关。
全局偏移量表(GOT),位于段开头用于保存全局变量和库函数的引用,每个条目占8个字节,在加载时进行重定位并填入符号的绝对地址。
为了引入RELRO保护机制,GOT被拆分为.got节和.got.plt节两个部分。
.got节不需要延迟绑定,用于全局变量的引用,加载到内存后被标记为只读。
.got.plt需要延迟绑定用于保存函数的引用,具有读写权限。
延迟绑定:基本思想是当函数第一次被调用时,动态链接器才进行符号查找、重定位等操作,如果未被调用则不进行绑定。
ELF文件通过过程链接表(PLT)和GOT配合实现延迟绑定。