ctfshow-pwn第二课-ELF文件基础
静态链接
静态连接是将多个文件链接在一起并生成可执行文件的过程
示例代码:
a.c
extern int shared;
extern void swap(int* a,int* b);
int main(){
int a=10;
swap(&a,&shared);
}
b.c
int shared = 1;
int swap(int* a,int* b){
*a ^= *b ^= *a ^= *b;
}
将这a.c
和b.c
编译,使用下方命令
gcc -c a.c b.c -fno-stack-protector -m32
参数:
-c
: 在第一课中使用过,将a.c
编译为a.o
文件不进行链接-fno-stack-protector
: 禁用栈保护。默认会开启栈保护(GCC默认会增加一些代码来检测栈溢出),删除就会让编译的程序更小方便我们分析-m32
: 将a.c
编译为32可执行文件
使用上述gcc命令会将a.c
和b.c
编译为a.c
和b.o
的可执行文件,由于没有做链接,是无法执行的。
我们可以通过分析a.o
和b.o
来更深入的理解ELF文件的格式
可以通过下面的命令静态链接a.o
和b.o
这两个文件
ld -m elf_i386 a.o b.o -e main -o ab
参数详解:
ld
: 该命令用于将.o
文件链接为库文件或者可执行文件-m elf_i386
: 制定链接输入的格式为32
x86结构的ELF可执行文件-e main
: 指定程序的入口为main
,一般C语言的默认就是main-o ab
: 链接后生成的文件名为ab
通过上述步骤编译后的可执行文件运行会出现Segmentation fault
错误,但是不影响我们使用objdump
分析该程序
接着我们分析.text
节,使用objdump -h
分别查看a.o
和b.o
此时需要注意.text
的size(大小)
a.o
b.o
可以看到a.o
和b.o
的.text
大小分别为4a 43
,相加的结果为8d
接着使用objdump -h
查看通过静态链接后ab
的.text
的大小为91
可以看出,链接后多了91 - 8d = 4
个字节。
接着我们使用objdump -j .text -s
分别查看a.o b.o ab
的.text
段内容
a.o
和b.o
ab
通过仔细观察会发现,a.o
和b.o
的数据是不变的,但是中间多了四个字节8b04 24c3
,这四个字节是固定的。
后续在学习到汇编会知道这里为什么是8b 04 24 c3
接着我们分析a.o
和ab
的一些区别,使用objdump -h
分别查看这两个文件
.text
我们已经分析过了,这里注意的是VMA(Virtual Memory Address)
和LMA(Load Memory Address)
,一般情况下这两个值相同。
在没有编译的情况下a.o
的VMA值为0,但是在编译后相似的节被合并,而且分配了虚拟地址
符号表
使用readelf -S
查看ab
文件的节表头
可以看到符号表.symtab
的size
为110
接着使用readelf -x .symtab
查看ab
文件的符号表内容
符号表中16个字节为一个符号位,所以我们可以使用110 / 16 = 12
注意是16进制计算,这里算出是12个
但是我们刚刚看到的有17个包,这里不知道为什么,后续解决,猜测可能是有的没有暂用实际的字节
接着我们分析一下符号表的结构,一共16个字节,我们可以将这16个字节使用4个字节为一组分为4组
- 1组: 符号名索引
- 2组: VMA地址
- 3组: 符号大小
- 4组: 第四组的第一个字节表示符号的绑定信息
重定位表
链接器要知道那些命令被调整,需要给链接器一个清单,这个清单就是重定位表
重定位表就是ELF中的一个节,例如text节重定位表就是rel.text,data节的重定位表就是rel.data
使用objdump -r
可以查看重定位表
ELF文件运行发生了什么?
下面部分来自chatgpt生成
ELF(Executable and Linkable Format)文件是Unix和Unix-like系统(如Linux)中的标准二进制文件格式,用于存储程序或库。在ELF文件中,段(segment)和节(section)是两个重要的组成部分,它们在程序加载和运行的过程中扮演着不同的角色。
段(Segment):
段在ELF文件中主要用于描述程序在内存中的布局。一个ELF文件通常包含多个段,每个段都对应着程序运行时所需的一部分内存空间。当操作系统加载ELF文件到内存中时,它会根据段的信息来分配和初始化相应的内存区域。
具体来说,段的作用包括:
- 定义内存布局:段描述了程序在虚拟内存中的布局,包括哪些部分需要加载到内存中,以及它们在内存中的位置。
- 加载和初始化:当程序被加载到内存时,操作系统会根据段的信息来执行加载和初始化操作,确保程序能够正确地运行。
节(Section):
节是ELF文件中的逻辑组成部分,它们包含程序的代码、数据以及其他相关信息。节在文件中是连续存储的,但它们在内存中的布局可能并不连续。与段相比,节更多地关注于程序在磁盘上的存储和组织方式。
节的作用包括:
- 代码和数据存储:节用于存储程序的机器代码、初始化数据、未初始化数据等。这些代码和数据在程序运行时会被加载到相应的内存段中。
- 符号和调试信息:节还可以包含程序的符号表(用于链接和调试)、调试信息(用于调试器分析程序状态)等。这些信息对于程序的开发和调试至关重要。
从程序在计算机上运行的视角分析:
- 加载过程:当操作系统启动一个ELF程序时,它会首先读取ELF文件的头部和段表信息,确定需要加载哪些段到内存中。然后,根据段的信息,操作系统将相应的节内容加载到内存中。
- 初始化与准备:加载完成后,操作系统会进行一系列的初始化操作,如设置程序的入口点、初始化全局变量等。同时,它还会为程序分配所需的堆和栈空间。
- 执行过程:一旦初始化完成,操作系统会跳转到程序的入口点(通常是
_start
函数或libc_start_main
),开始执行程序中的代码。在执行过程中,程序会访问和操作加载到内存中的节数据。 - 动态链接与加载:如果ELF文件依赖于其他共享库,操作系统会在程序执行过程中进行动态链接和加载。这涉及到解析符号表、重定位表等动态段中的信息,以确保程序能够正确地调用库函数。
通过段和节的组织和协作,ELF文件能够以一种高效且灵活的方式在计算机上运行程序。这种设计既考虑了程序在内存中的布局和访问效率,又兼顾了程序在磁盘上的存储和可移植性。
静态库链接
静态库是一组目标文件的集合。静态库不能直接执行,是开发环境附带的语言库,对操作系统的API进行二次封装,方便开发程序进行调用。常见的理解为stdio.h
库。
静态库一般由编译厂商提供,Windows下的Visual C++附带了多个版本的C/C++运行库,Linux下的glibc是提供了libc.a这个静态库文件,里面分别包含了949和1400个目标文件。
简单理解就是静态库文件就是把常用的功能代码给写好,写好后哦通过gcc -o来生成.o文件,方便开发者链接使用
通常我们使用的printf
就是静态库里面附带的函数
使用objdump -t
可以查看函数和目标文件之间的关系
进程虚拟地址空间
32位操作系统的最大寻址范围是4GB
64为操作系统的最大寻址范围是2的64次方GB,通常操作系统的虚拟地址空间不需要这么大的空间,所以通常64位系统的虚拟地址寻址大小为48位,也就是2的48次方GB
堆和栈
进程栈初始化
进程刚启动的时候,需要得到系统的启动参数和系统的环境变量,这些数据需要保存到一个地方供进程使用
保存的这个地方需要和进程进行绑定,因为不同的进程拥有不同的启动参数,也就是需要一个进程的私有空间来存放
这个存放启动参数和操作系统环境变量的进程的私有空间就是栈空间
栈是一片内存空间,仅供程序使用,栈的内存地址增长方向是向下增长,即先用高地址,依次往下填充
最后一个地址就是栈顶,内存地址最低的位置