二进制漏洞基础
0.1 堆
作用:用于动态分配内存,当我们在程序中使用malloc、calloc、realloc等函数时,分配的内存就来自于堆;当使用free函数时,就是释放堆上的内存
比如char *p=malloc(100);,这句话的意思是从堆上分配100个字符,然后把内存赋值给p 核心特性:堆的地址是由低向高生长的,也就是说,第一次分配堆的内存地址较低,后续不断分配堆内存时,地址会越来越高
1 C语言基础
1.1 程序的编译与链接
1.2 可执行文件
- 广义:文件中的数据是可执行的文件
.out、.exe、.sh、.py
- 狭义:文件中的数据是机器码中的文件
- Windows:PE
- 可执行文件
.exe
- 动态链接库
.dll
- 静态链接库
.lib
- 可执行文件
- Linux:ELF
- 可执行文件
.out
- 动态链接库
.so
- 静态链接库
.a
- 可执行文件
2 Linux基础
- 保护层级:分为四个ring0-ring3
- 一般来说就两个,0和3
- 0为内核
- 3为用户
2.1 权限
用户可以分为多个组
文件和目录等的权限一般是三个,可读可写可执行
赋予一个可执行文件执行权限,就是chmod +x filename
2.1.1 段权限
代码段包含了代码与只读权限
- .text 节
- .rodata 节
- .hash 节
- .dynsym 节
- .dynstr 节
- .plt 节
- .rel.got 节
数据段包含了可读可写权限
- .data 节
- .dynamic 节
- .got 节
- .got.plt 节
- .bss 节
栈段
2.2 虚拟内存和物理内存
- 物理内存很直白,就是内存中实际的地址
- 虚拟内存是物理内存经过MMU转换后的地址(页表)
- 系统会给每个用户进程分配一段虚拟内存空间
所以说我们调试的可执行的程序的内存空间布局都差不多,但是虚拟内存,不是实际的物理内存
2.3 数据的存储方式
2.3.1 大端小端序
- 大端序:数据高位存储在计算机地址的低位,数据低位存储在地址的高位
- 大端序:数据高位存储在计算机地址的高位,数据低位存储在地址的低位
linux数据存储的格式为小端序
2.4 文件描述符
linux中,把一切都看作是文件,当进程打开现有文件或者创建新文件时,内核向进程返回一个文件描述符,文件描述符就是内核为了高效管理已被打开的文件所创建的索引,用来指向被打开的文件,所有执行I/O操作的系统调用都会通过文件描述符
每个文件描述符会与一个打开的文件相对应,不同的文件描述符也可能指向同一个文件
相同文件可以被不同的进程打开,也可以在同一个进程被多次打开
2.4.1 常见操作
我们会在open\read\write这些函数中见到文件描述符
0代表标准输入、1标准输出、2标准错误
read从stdin中读size个数据到buf中
write从buf中取size个数据到stdout中
2.5 ELF文件
2.5.1 结构
linux环境中,二进制可持文件的类型是ELF
elf文件格式比较简单,它的基础信息存在于elf的头部信息中,这些信息包括指令的运行框架、程序入口等等,可以通过readelf -h <elf_name>来查看头部信息
elf文件包含很多个节,每个节中存放着不同的数据,包括
| 名称 | 作用 |
|---|---|
| .text | 存放程序运行的代码 |
| .rdata | 存放一些如字符串等不可修改的数据 |
| .data | 存放已经初始化的可修改的数据 |
| .bss | 存放未被初始化的程序可修改的数据 |
| .plt与.got | 程序动态链接地址 |
2.5.1.1 ELF文件头表
- 记录ELF文件的组织结构
2.5.1.2 程序头表/段表
- 告诉程序如何创建进程
- 生成进程的可执行文件必须拥有此结构
- 重定位文件不一定需要
2.5.1.3 节头表
- 记录ELF文件的节区信息
- 用于链接的目标文件必须拥有此结构
- 其他类型的目标文件不一定需要
2.5.2 进程内存映像
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30【磁盘:ELF 文件 (基于 Section)】 【内存:进程镜像 (基于 Segment)】
+-------------------------------+ +-------------------------------+ 高地址
| ELF Header | | 内核空间 |
+-------------------------------+ +-------------------------------+
| Program Header Table (PHT) | | Stack (向下增长) |
+-------------------------------+ | | |
| .text (代码) | ==> PT_LOAD ==> v |
+-------------------------------+ | |
| .rodata (只读数据) | ==> PT_LOAD ==> | |
+-------------------------------+ | ^ |
| .data (初始化数据) | ==> PT_LOAD ==> | | |
+-------------------------------+ | Heap (向上增长) |
| (磁盘中不占空间的数据) | +-------------------------------+
+-------------------------------+ | Data Segment (R/W) |
| +---------------------------+ |
| | .bss (全0) | | <--- 自动生成
| +---------------------------+ |
| | .data | |
| +---------------------------+ |
+-------------------------------+
| Text Segment (R/X) |
| +---------------------------+ |
| | .rodata | |
| +---------------------------+ |
| | .text | |
| +---------------------------+ |
+-------------------------------+
| 不可访问区 |
+-------------------------------+ 0x00000000
2.6 动态链接库
我们程序开发中都会用到的函数,如read\write\open等
这些函数不需要我们实现,因为系统已经帮我们完成了这些工作,只需要调用就行,存放这些函数的库文件就是动态链接库
通常情况下,我们遇到最多的还是libc.so文件
2.6.1 libc
glibc是linux下c标准库的实现,即GNU C Library
glic本身是GNU旗下的C标准库,后面逐渐成了Linux的标准库,而linux下原本的C标准库逐渐不再维护
Linux下标准库不仅有一个,但最多的还是libc
glic在虚拟内存中可以存在多份,但在实际内存中只有一份
2.7 延迟绑定
在动态链接(Dynamic Linking)中,程序在装载时需要对外部函数和全局变量进行重定位。如果程序在启动时就强行解析并绑定所有调用的外部共享库函数(如 libc 中的 printf、system 等),会极大地拖慢程序的启动速度,尤其是在实际上很多函数在一次运行中可能根本不会被执行到的情况下。
为了解决这个“运行效率不高、浪费资源”的问题,ELF 引入了延迟绑定(Lazy Binding)机制:只有当函数第一次被真正调用时,才去解析它的实际物理地址并进行绑定。
为了实现延迟绑定,ELF 文件依赖两个核心的数据结构:GOT 表和PLT 表。
2.7.1 GOT 表
GOT 表位于数据段 (Data Segment) 中,是用来保存在地址无关代码(PIC, Position-Independent Code)中确定的全局变量和外部函数绝对地址的表。由于它在数据段,因此在程序运行时通常是可读可写的。
在 ELF 文件中,GOT 表被细分为了两个主要部分:
.got表:用于保存全局变量的引用地址。在程序加载时,动态链接器会直接解析并填充这里的地址。.got.plt表:用于保存外部函数引用的真实物理地址。这就是延迟绑定的核心阵地。对于每个需要调用的外部函数,在这个表中都会有一项。- 前三项的特殊用途:
GOT[0]:包含.dynamic段的地址,动态链接器用来提取动态链接相关信息。GOT[1]:包含当前本模块(也就是当前的 ELF 文件)的 ID 信息。GOT[2]:包含动态链接器中解析函数地址的代码入口,即_dl_runtime_resolve()的地址。
- 前三项的特殊用途:
2.7.2 PLT 表
PLT 表位于代码段 中,因此它是只读的。它本质上是一小段一小段的跳板代码(Stub),程序对外部函数的调用,实际上都是先跳到 PLT 表中对应的跳板代码,再由跳板代码决定下一步的走向。
PLT 表的结构通常如下:
PLT[0](公共桩):这是一段特殊的代码,负责跳转到动态链接器去执行符号解析。它会访问GOT[1]和GOT[2]。PLT[1]...PLT[n](函数跳板):每一个外部函数(如printf)都有一个对应的 PLT 桩(例如printf@plt)。
2.7.3 延迟绑定的完整执行流程
以调用 printf 为例,理解“第一次调用”和“后续调用”的区别是掌握延迟绑定的关键:
2.7.3.1 第一次调用外部函数:
- 调用发生:程序代码执行
call printf@plt,跳转到 PLT 表中printf对应的代码桩。 - 首次跳转:
printf@plt的第一条指令是jmp *(printf@got.plt)。由于是第一次调用,此时printf@got.plt中存储的并不是printf的真实地址,而是指向了printf@plt中的下一条指令。 - 准备解析:于是程序没有跳走,而是顺着 PLT 继续往下执行,将
printf在重定位表中的索引(ID)压入栈中(push index)。 - 跳转公共桩:接着跳转到
PLT[0]。 - 执行解析:
PLT[0]将本模块的 ID(GOT[1])压栈,并跳转到GOT[2]中存储的动态链接器解析函数_dl_runtime_resolve()。 - 回填地址与执行:
_dl_runtime_resolve()根据传入的索引,在共享库中找到真实的printf地址,将真实地址写入到printf@got.plt中(这就是绑定的过程),最后直接跳转去执行真实的printf函数。
2.7.3.2 第二次及以后的调用:
- 程序再次执行
call printf@plt。 - 再次执行第一条指令
jmp *(printf@got.plt)。 - 此时,
printf@got.plt里面存储的已经是上一步回填的printf真实地址。 - 程序直接跳转到真实的
printf函数执行,不再经过压栈和_dl_runtime_resolve()解析的过程。
3 攻击前准备
3.1 基础文件信息与依赖检查
file:查看二进制文件的基本信息(如32位/64位、动态/静态链接、是否 stripped 剥离了符号表等)。ldd:查看动态链接程序运行所依赖的共享库(比如依赖的libc版本和路径)。hexdump:以十六进制和 ASCII 码的形式查看文件的底层原始数据。
3.2 静态分析与逆向工程
objdump -d -M intel:反汇编工具。-d表示对可执行代码段进行反汇编,-M intel指定输出更容易阅读的 Intel 汇编格式(代替默认的 AT&T 格式)。readelf -a:全面解析 ELF 文件结构,用来查看程序的段(Segment)、节(Section)、动态链接表、重定位表等详细信息。nm:用来列出目标文件中的符号表,可以快速查找程序中的函数名、全局变量及其对应的内存地址。
3.3 动态调试
gdb:GNU 调试器,Linux 平台下最强大的动态调试工具。用来下断点、单步执行、查看寄存器和内存布局,是编写和调试 exploit 的核心武器。
3.4 环境部署与模拟
socat tcp-l:8888,fork exec:./a.out,reuseaddr:强大的网络工具。这句命令的作用是在本地监听 8888 端口,每当有连接时就通过 fork 创建子进程来运行./a.out。它可以把本地的交互式程序变成一个可以通过网络连接的服务(类似于nc 127.0.0.1 8888),完美模拟线上 Pwn 题目的真实远程靶机环境。
