
1. 为什么我又开了个PWN刷题坑BUUCTF上的PWN题我前前后后刷了不少但说实话最开始的体验很劝退。一是环境配置就折腾了整整两天二是很多writeup默认你已经懂了一堆前置知识根本不会告诉你那些坑。这次开刷题2是我把第一阶段踩过的坑整理完之后重新给自己定了一个目标不只看writeup而是真正把每道题从信息收集到exp编写完整跑通同时把平台新收录的题目和新出现的玩法也一并过一遍。这一篇记录的主要是第二阶段的内容。题不再只挑简单的栈溢出打了开始往ROP链构造、格式化字符串、堆利用入门这些方向走。如果你也是PWN新手正在为看题五分钟做不出来两小时发愁这篇应该能帮你省下不少摸黑探路的时间。先说清楚这篇记录适合谁已经会基本的环境配置能跑通简单的栈溢出 exp知道 pwntools 基本用法但面对中档题时觉得思路断裂的人。如果你连checksec、cyclic、gdb断点这些都没概念建议先把基础题过一遍再回来看这篇不然很多细节会感觉跳步。我刷题的硬件环境很简单Ubuntu 22.04虚拟机分配了4核8G本地装了pwntools、gdb、peda、ROPgadget、one_gadget再加上libc-database做libc识别。这套组合在BUUCTF上足够用了没必要上太复杂的方案。2. 刷题前的环境与平台准备2.1 BUUCTF 平台的正确打开方式很多人上来就在BUUCTF的网页上直接看题目附件然后下载下来就开始分析。实际刷PWN题有个更好的路径题目页面会给出一个docker启动的远程环境地址形如node4.buuoj.cn:xxxxx这个地址加端口就是你的远程靶机。做题时本地分析用附件调试通了之后打远程用这个地址。平台本身会不定期更新题目我在刷题时发现右侧的题目列表里有些题目会标注已离线或者直接找不到这种题建议直接放弃别浪费时间。另外BUUCTF的附件下载链接在部分浏览器里会跳转失败我用的方案是直接右键复制链接然后用wget下载到本地。wget https://buuoj.cn/attachments/xxxxxx -O filename.tar.gz tar -xvf filename.tar.gz这种操作方式在连续做题时比在网页上一道一道点快得多也便于批量管理题目文件。2.2 本地环境常见坑位与解法在开始大量刷题之前先把环境问题一次说透免得中途卡住。BUUCTF的很多例题编译时间比较早动态链接的libc版本和本机对不上最典型的报错是/lib/i386-linux-gnu/libc.so.6: version GLIBC_2.34 not found。这类问题在打老题时几乎每天都会遇到。我的常规处理方案有两种第一种是本地直接下载对应版本的libc用pwninit工具自动patch。pwninit在GitHub上有项目它做的事情很简单根据提供的二进制文件自动从libc-database中匹配libc版本生成patch后的新二进制和对应的ld文件。这样你在本地跑exp时用的是和目标环境一致的libc调试出来的偏移量才是准的。# 安装pwninit cargo install pwninit # 在题目目录下操作 pwninit --bin ./pwn --libc ./libc-2.27.so第二种是直接远程调本地只做静态分析。这种方法适合计算ROP链时不需要精确libc偏移的场景比如使用ret2dlresolve或者题目本身没有泄露libc的需求。远程环境和本地环境最大的区别在于远程的aslr每次都在变你的exp必须能自己泄露地址并计算偏移不能写死任何地址。提示刷题初期最容易犯的错误就是本机调试通了换远程直接崩。绝大多数原因就是地址写死、栈偏移不一致、或者gadget找错了。建议从一开始就用context.log_level debug开着跑几道题看懂了数据流后再关掉。2.3 常用工具的进阶用法很多教程都会讲checksec、cyclic、gdb但实际做题过程中工具用得好不好直接决定了效率。我在这轮刷题中真正高频使用的工具组合是这样分工的checksec用于确认保护策略这一步决定了你这道题的利用方向。比如NX enabled PIE disabled基本就是栈ROP的常见组合Partial RELRO则意味着可能有机会改GOT表。ROPgadget用于找gadget但我建议不太依赖工具而是自己在gdb里确认几个关键gadget是否存在。举个例子当你在找pop rdi; ret时ROPgadget给你的结果是一回事这个gadget在二进制中的实际地址是否可写、是否被PIE随机化影响又是另一回事。gdb peda是调试主力我尤其喜欢的是在溢出点下断点后直接pattern search看偏移以及用x/20gx $rsp观察栈上数据排布。这里有个小技巧在构造ROP链时每加一个gadget就在gdb里跑一遍确认栈顶指针一步步按预期走而不是一次性写完整个exp再调。分段验证是减少调试痛苦的最好方式。3. 基础栈溢出题型的复盘与深挖3.1 无保护栈溢出的标准打法BUUCTF早期题里有相当一部分是只开了NX的纯栈溢出checksec结果长得几乎一样Canary找不到、PIE没开、RELRO是Partial。这类题打多了以后我总结了一套固定流程速度可以提到五分钟一题。以一道典型的题为例漏洞点是一个gets或read明显的栈溢出程序本身自带一个system(/bin/sh)或类似后门函数。做题思路很简单checksec确认无canary、无PIE。用cyclic 200配合gdb确定偏移。具体操作是先把cyclic 200作为输入传入崩溃后用cyclic -l 地址算出偏移。找到后门函数地址用payload bA * offset p64(addr)打过去。这个流程里最容易翻车的环节是偏移计算。因为gets遇到换行会结束输入如果你的payload里出现了0x0a这个字节gets会提前截断导致利用失败。更隐蔽的情况是payload中存在空字节0x00read不会截断但strcpy或gets会。所以我在构造payload之前会先看一眼漏洞函数用的是哪个输入函数。这道题我在本地跑通后打远程发现串不通报EOF错排查了半天才发现远程环境是一个更新的Ubuntu栈上多了一些无关数据导致实际的偏移比本地多八字节。这个现象在老题中特别常见所以在exp里用p64(addr)拼payload时宁可先多测试几组偏移也不要迷信本地结果。3.2 在栈溢出里做函数复用还有一类题更阴险后门函数是有了但不直接给/bin/sh而是要求你给它传参数。典型场景是程序里有个system函数但system(/bin/sh)这个调用不存在你要自己构造一个函数调用。这时就要引入ROP的第一个标准思路在栈上布置函数参数。以x64为例函数调用的前六个参数分别存在rdi、rsi、rdx、rcx、r8、r9。如果二进制里恰好有pop rdi; ret这个gadget你就可以在栈上依次写下payload bA * offset payload p64(pop_rdi_addr) # 把栈上下一个qword弹到rdi payload p64(binsh_addr) # 这个值会被弹到rdi也就是函数参数 payload p64(system_addr) # 跳转到system执行这道题的难点在于两点第一个是/bin/sh字符串去哪找。如果二进制里没有可以通过gdb在libc里搜索或者用libc.search(b/bin/sh)找到偏移但前提是你得先知道libc版本。如果libc版本未知就要先去泄露地址。第二个是找pop rdi; ret这个gadget时别只看二进制本身如果二进制是动态链接的libc里肯定也有这个gadget但那需要先知道libc加载基址这就回到了泄露地址的问题。我的经验是先检查二进制自带gadget如果找不到再考虑泄露libc。大部分BUUCTF中档题会故意在程序里塞一个pop rdi; ret方便你做题毕竟出题人的目的是考栈布局而不是考gadget搜藏。3.3 ret2csu 技巧详解这次刷题计划里有一个在热搜词里反复出现的题目模式[newstarctf 公开赛赛道]ret2csu1。ret2csu是一个很经典的技巧用来解决二进制里找不到pop rdi; ret的问题。先解释csu是什么在glibc的动态链接器启动代码里有一段通用的函数序言/尾声代码通常位于__libc_csu_init函数中。这段代码提供了固定的gadget序列可以让你弹出一组寄存器然后调用任意函数。标准结构是两个pop序列加一个call。我第一次接触这个技巧时一头雾水因为那串gadget代码很长完全不知道每个字节对应什么。后来自己动手拆了一遍发现只要理解了布局逻辑ret2csu其实就是一段万能跳板。通用的ret2csu payload模板长这样payload bA * offset # 第一个gadgetpop rbx; pop rbp; pop r12; pop r13; pop r14; pop r15; ret payload p64(csu_pop_addr) payload p64(0) # rbx payload p64(1) # rbp用于call的计数 payload p64(got_addr) # r12call的地址通常填要调用的函数地址 payload p64(rdi_val) # r13会被mov到rdi payload p64(rsi_val) # r14会被mov到rsi payload p64(rdx_val) # r15会被mov到rdx # 第二个gadgetmov rdx, r15; mov rsi, r14; mov edi, r13d; call [r12rbx*8] payload p64(csu_call_addr) # 注意XOR和add的规则最后还要有三次add rsp, 8 payload bA * 0x38这个模板虽然看起来长但结构非常固定。我第一次照着模板打ret2csu1这道题时翻车点是call [r12rbx*8]这块没理解透r12不是你直接填函数地址的地方而是要填一个存放函数地址的地址也就是got表项。很多writeup里直接填了函数地址导致跑崩了。注意ret2csu里call [r12rbx*8]这个指令使用的是间接寻址r12和rbx组合计算出的地址指向的内存里存的才是要调用的函数地址。常规做法是r12填函数的GOT地址rbx清零。3.4 从格式化字符串洞到任意地址读写刷题过程中碰到的第二个高频题型是格式化字符串漏洞。这类题在BUUCTF里数量不少而且难度跨度很大从让你直接泄露栈上内容到要你利用格式化字符串实现任意写应有尽有。格式化字符串漏洞的本质是printf族函数的格式化参数与实参不匹配。当你在程序中写printf(user_input)而不是printf(%s, user_input)时用户输入的内容会被当作格式化字符串解析。这会带来两个能力第一个能力是栈数据泄露。%p可以按顺序打印栈上的内容%N$p可以直接打印第N个参数位置的内容。通过尝试不同的N值你可以把栈上各种地址读出来包括返回地址、libc函数地址甚至环境变量地址。第二个能力是任意地址写。利用%N$hn或%N$hhn可以往指定地址写入数据。原理是printf在遇到%n时会把当前已经输出的字符数写到对应参数指向的地址。配合%N$hn定位到特定参数位置就能实现任意四字节或两字节的写入。我在做这道题时遇到的问题是怎么确定我的输入参数在栈上的第几个位置。这里有个标准做法就是先用一串规律性的输入比如AAAA.%p.%p.%p...然后观察输出里的0x41414141出现在第几个位置那个位置就是你的输入起点。之后所有%N$操作都以这个位置为基准计算。格式化字符串利用里有个关键细节是%hn和%hhn的写字节数差异。如果目标地址之间的偏移值较大直接用%hn写两字节会比用%hhn写单字节更高效但要注意单次printf的输出字符数巨大时会被截断。我的一般做法是拆分写入先把地址放在payload头部然后在格式串中逐段设置输出长度。4. 高版本libc下的栈迁移与泄露4.1 栈溢出保护策略的变化BUUCTF上的新题和早期题在保护策略上有明显区别。早年的题基本就是Canary有没有、PIE开没开这两种区分。近两年的新题开始出现更多组合Full RELRO、Canary、PIE全开甚至有的题同时开了沙箱seccomp这就不能直接execve(/bin/sh)拿shell了得用ORWOpen-Read-Write方式把flag读出来再输出。如果只是PIE全开利用思路通常是利用格式化字符串泄露程序基址或者利用栈溢出时的部分覆盖partial overwrite技巧跳转到已知位置。如果同时有Canary你还要先泄露canary值这个值每次进程启动都变必须在同一连接里完成泄露和利用。我在某道模拟赛题里遇到一个典型的全开题目程序是一个菜单题有两次输入机会第一次输入可以打印栈内容第二次输入有明显溢出。我的做法是第一次机会用格式化字符串泄露canary和__libc_start_main的返回地址通过libc-database算出libc基址。第二次机会重新计算偏移和ROP链一次性打出system(/bin/sh)。这个思路在BUUCTF很多新题里都是通用骨架先泄露关键值再利用输入点。关键点在于两次机会之间的所有状态都必须保住一旦连接断开所有地址失效只能重新来过。4.2 栈迁移的核心思路有些题确实不够你堆一条完整ROP链栈空间被程序设定得很小或者有效溢出字节数很少。这时候就要上栈迁移stack pivot技术。栈迁移的基本思想是你无法在当前栈帧里放下完整payload但你可以先溢出修改rsp寄存器让它指向你控制的另一块内存区域比如bss段或mmap出来的区域然后利用这块区域继续布置ROP链。在x64下常用的迁移方法是利用leave; ret这个gadget。leave指令等价于mov rsp, rbp; pop rbpret则是pop rip。如果你能修改栈上的返回地址为leave; ret的地址并且控制当前函数的rbp值那么leave; ret执行时CPU会先把rsp设为伪造的rbp然后从伪造的rbp指向的位置取出下一个跳转地址。我在实际做题里用栈迁移时最常踩的坑是忘记在迁移后保住第二次的栈平衡。迁移到bss段后bss段本身可能与栈的性质不同比如有对齐要求或者与GOT区相邻导致写入冲突。建议迁移的目标地址不要选在.data段前面而是选在bss段后部或者干脆找个mmap区域。另外一个细节是在栈迁移前先确保溢出点能让你连续写足够多数据到目标内存。比如read(0, buf, 0x100)只能读256字节如果目标区域在bss而buf也是bss那没问题但如果你通过gets读入并存在栈上再用sprintf拷贝就可能导致拷贝长度不可控。这些都要在gdb里实际验证不能凭空推。4.3 多阶段payload与沙箱绕过碰到沙箱保护时system(/bin/sh)这条路基本断掉你需要改成ORW链。ORW的核心是构造三个系统调用open打开flag文件read把内容读进内存write输出到stdout。在x64下做ORW难点在于构造open的路径参数。调用open(/flag, O_RDONLY)时参数一是flag字符串的地址。如果flag字符串不在已知内存位置你还要想办法把路径字符串写到已知可读的位置。常见方案是在payload里直接包含/flag字符串并把字符串地址算出来。这类payload往往非常长因为除了三个系统调用的参数布局之外还要考虑到syscall指令的gadget位置。如果二进制里本身有syscall; ret或syscall指令直接复用即可。如果找不到那就得靠read系统调用把自己写进去形成SROPSigreturn Oriented Programming或者直接在栈上构造完整的syscall序列。我在一道赛题里遇到的是open(/flag).read(fd, base, 0x100).write(1, base, 0x100)全流程构建。那时因为找不到直接的syscall最终用了libc里的syscallgadget并配合栈迁移。整个过程跑通之后我强烈建议你也在本地模拟一次完整ORW因为这道题的细节对后续做堆题也有帮助。重点是确认每段gadget执行完后的栈指针是否回到预期位置很多失败都是从栈偏移差八字节开始的。5. 堆题入门的核心概念与实际心路5.1 从理解堆布局开始刷题计划进行到中段时我开始向堆利用方向转。BUUCTF上堆题数量不少但入门难度很微妙太简单的直接教你改__free_hook太复杂的直接上tcache poisoning。没有系统梳理的话很容易卡在fastbin和unsorted bin的切换规则上出不来。堆题的第一步不是找漏洞点而是理解glibc的堆布局。在glibc 2.27及以前的版本中堆块的结构是chunk header占16字节prev_sizesize用户可用数据区域从chunk_start 16开始。size字段的低三bit是标志位PREV_INUSE表示前一个chunk是否在使用IS_MMAPPED表示是否为mmap分配NON_MAIN_ARENA表示是否在主线程arena之外。带着这个基础理解去刷题你会发现大多数题目套路其实很固定有UAFuse-after-free漏洞释放后可以再读/写对应内存。有double free漏洞同一块内存被释放两次。有堆溢出漏洞当前堆块的内容可以覆盖下一个chunk的header。BUUCTF有许多题目就是围绕UAF展开的。我第一道完整跟下来的堆题是一个菜单题提供add/edit/delete/show四个操作。漏洞点在于delete操作没有把指针置空导致后续edit或show仍能访问已释放的chunk。通过UAF泄露libc后再把__free_hook改成system然后free一个内容为/bin/sh的chunk就能拿到shell。5.2 tcache机制与double free实战从glibc 2.26开始malloc引入了tcache机制。tcache是一个per-thread的缓存每个bin最多缓存7个chunksize范围从0x20到0x410。tcache的引入让堆题变得简单很多因为tcache的入队和出队没有强校验double free在tcache范围内非常容易触发。实际利用时double free最典型的场景是这样的add(0x20, bA) # chunk A add(0x20, bB) # chunk B free(A) free(A) # double free # 此时tcache bin里有A - A add(0x20, bC) # 拿到A add(0x20, bD) # 又拿到A在glibc老版本2.29之前中free调_int_free时不会检查当前chunk是否已经在tcache中所以double free直接成立。拿到两次同一块内存后你可以修改它的fd指针指向任意目标地址从而在后续malloc中把这块目标地址返回给用户。常见目标就是__free_hook。我在实际刷题时遇到过一个不太一样的坑在add后edit时程序用的输入是read(0, buf, size)但size是用户可控的。这意味着你在修改chunk内容时可以写入超过chunk实际大小的数据从而覆盖到下一个chunk的header。如果目标环境开启了tcache的double free检查glibc 2.29以后才有你还需要绕过它的key字段校验方法是把目标chunk的key字段改写为合法值。实操提示写堆题exp前先把glibc版本确认了。很多BUUCTF题会直接给出libc.so文件用strings libc.so | grep GNU C Library就能看到版本号。不同版本的tcache检查逻辑差异很大exp的结构也随之不同。5.3 用unsorted bin泄露libc堆题中最常用的libc泄露方式是释放一个large chunk使其进入unsorted bin。当chunk被释放并进入unsorted bin时它的fd和bk指针会指向main_arena中的某个偏移值。如果程序提供show功能你就可以直接读出这个指针再减去固定的偏移量得到libc基址。常见操作流程是分配一个大chunk如0x500然后释放它。它会进入unsorted bin由于不是fastbin它不会马上被复用。show输出它的fd/bk指针。用fd_offset libc.symbols[main_arena] offset计算libc基址。这里需要说明的是main_arena的偏移在不同glibc版本里不一样。我在做glibc 2.23与2.27的两道类似题时用同一个泄露公式只能在一道题上跑通原因是main_arena与unsorted_chunks(av)的位置偏移在版本间有变化。如果不确定偏移最简单的办法是在本地对应版本的libc下用gdb调试p main_arena和p main_arena-bins[1]两者的差值就是你要的偏移。把调到的值记下来exp里直接用就可以了。5.4 简单的tcache poisoning实操当你能控制一个tcache chunk的fd指针后下一步就是把它指向任意地址然后连续malloc两次第二次返回你的目标地址。这是tcache poisoning的基本操作。我在一道题里构造的利用链是先申请两个0x20的chunk释放第一个进入tcache再释放第二个进入tcache。然后通过UAF修改第一个此时在tcache bin里的fd指针把它改为target_addr - 0x10因为malloc返回的是用户区指针chunk头在低16字节处。连续两次malloc(0x20)后第二次返回target_addr - 0x10然后写入数据时实际写到了target_addr处。这个手法可以用在改__free_hook、改malloc_hook、改GOT表、甚至改stack地址上。BUUCTF里大量中等堆题都是这个套路。我第一次自己独立完成这道题时最大的障碍是计算目标地址的偏移。当你通过泄露得到libc基址后要用libc.sym[__free_hook]得到符号地址然后注意malloc返回地址与chunk头的关系别忘了减那16字节否则你写的位置会偏移。注意__free_hook是glibc中用于调试的hook函数指针在正常情况下是NULL。如果你把它改成system的地址然后执行free(ptr)实际会调用system(ptr)。前提是你free的chunk内容为/bin/sh字符串。5.5 堆题菜单处理与信息管理BUUCTF的堆题几乎都以菜单形式呈现每个选项对应一个操作。做题效率高的方式是把菜单交互封装成python函数避免每次交互时都手写sendlineafter。我的封装模板大概长这样def add(size, content): p.sendlineafter(b, b1) p.sendlineafter(bsize:, str(size).encode()) p.sendlineafter(bcontent:, content) def delete(index): p.sendlineafter(b, b2) p.sendlineafter(bindex:, str(index).encode()) def show(index): p.sendlineafter(b, b3) p.sendlineafter(bindex:, str(index).encode()) def edit(index, content): p.sendlineafter(b, b4) p.sendlineafter(bindex:, str(index).encode()) p.sendlineafter(bcontent:, content)这类封装的价值不只是省时间而是避免因为菜单提示符写错导致交互失败。很多题目在sendlineafter里包含特殊字符比如或:如果提示符写错一位程序就一直等不到你的输入exp卡死在那里。调试这类问题时用context.log_leveldebug一眼就能看出来。6. 从单个题目到解题方法论6.1 信息收集清单在做完大量题目后我发现自己的解题速度提升最大的节点不是在掌握某个新技巧时而是形成了一套固定的信息收集流程。每拿到一道题我按顺序做这些事情file看二进制架构是x64还是x86、静态还是动态链接。checksec确认安全策略。strings快速查看是否有后门函数、/bin/sh、flag相关提示。运行一次程序观察菜单和交互流程。IDA/gdb 定位关键输入函数和缓冲区位置。用cyclic找偏移如果存在明显的栈溢出。这套流程大约花五分钟。做完之后这道题的利用方向基本就定了。很多人刷题感觉没思路其实是在信息收集阶段漏了关键信息比如没发现程序自带system函数或者没注意到read只读了指定长度导致溢出不可控。6.2 从writeup中学什么我看到很多新手刷题时直接把题目的writeup从头抄到尾跑通之后就觉得会了。这种做法效率极低。我自己的习惯是拿到writeup后先不看exp的完整代码只看它的思路流程图。好的writeup会告诉你这道题利用链是什么为什么选这个目标中间哪个步骤是关键我需要的是这个为什么的答案而不是那几行python代码。看完思路之后我自己动手写exp遇到卡点再回头翻writeup。这样一轮下来技巧才是自己的。有些BUUCTF题目下面别人写的writeup很简略甚至直接贴exp不解释。遇到这种我的做法是打开exp逐行看看到不理解的地方就用gdb去验证。比如exp里写payload bA*0x28 p64(pop_rdi)我会在gdb里确认栈上从输入起点到返回地址的偏移是否真的差0x28如果是继续如果不是自己重新算。这个过程虽然慢但对理解漏洞本质很有帮助。6.3 本地到远程的无缝切换本地exp打通后打远程最常见的问题是通宵写好的exp一交就Timeout。绝大多数情况是以下几种原因一是本地libc版本和远程不一致导致泄露地址计算的偏移全错。解决方法是用pwninit或手动patch libc到目标版本确保本地环境和远程一致。二是exp中的交互时序问题。本地调试时可能因为gdb附加导致时间变慢远程环境不会等你。如果你的exp里有多次sleep或者较为复杂的交互需要考虑把不必要的sleep都去掉尽量让exp保持稳定的快速执行。三是远程环境的ASLR导致栈地址每次不一样如果你的payload中包含栈地址比如ret2csu或栈迁移必须先从远程泄露一次栈地址再构造ROP链不能在本地写死。经验当远程打不通时不要反复提交同样的exp浪费时间。先在本地用远程相同的libc和patch环境复现再考虑网络或交互问题。如果本地同样崩说明你的exp本身有bug问题一定出在payload的构造细节上。7. 刷题过程中的常见问题与速查7.1 环境类问题问题1运行题目二进制时报错GLIBC_2.34 not found原因是本机libc版本过低而题目二进制要求的libc较新或者反过来。解决办法是用patchelf配合对应版本的libc运行。patchelf --set-interpreter ./ld-2.27.so ./pwn patchelf --set-rpath . ./pwn ./pwn问题2本地打shell成功远程报错优先检查libc版本是否匹配。BUUCTF题目页面通常会给出远程的libc版本提示有时需要到评论区找附件。拿到libc后用python3 pwninit自动patch。问题3pwntools连接远程超时检查靶机地址是否输入正确一些地址形如node4.buuoj.cn:12345如果端口被占用或被防火墙拦remote()会直接超时。建议先用nc命令测试连通性。7.2 payload构造类问题问题1p64地址里有0x0a换行符如果漏洞函数用的是gets遇到0x0a就结束读取。解决办法是找其他地址或者使用read函数进行输入尽量让payload不包含换行字节。问题2偏移计算对不上cyclic找偏移时如果程序在ret位置崩溃cyclic -l返回的是返回地址的位置但实际padding长度可能差8字节。因为返回地址本身占8字节你需要把输入起点到返回地址的距离算清楚。常见做法是用cyclic生成大量数据观察崩溃时rsp指向的字节序列在pattern中的下标而不是直接拿cyclic -l的结果。问题3双阶段输入第二阶段覆盖了第一阶段的地址比如你先通过格式化字符串泄露了地址然后又用栈溢出打ROP但第二阶段的payload长度把第一段泄露的关键数据给覆盖了。解决办法是在构造payload时预留好第一阶段的输出缓冲区位置或者把两段输入分开到不同内存区域。7.3 堆题常见问题问题1double free直接double corruptedglibc 2.29以后的tcache加入了key字段double free如果不对key做处理会触发检查直接abort。解决方法是释放两次A之后再把A的key字段改写为别的值比如改成main_arena...。问题2tcache poisoning失败检查fd指针是否指向了正确的地址。很多教程是直接写p64(target)但malloc实际返回的地址是target-0x10chunk头如果你希望最终malloc返回__free_hook那fd需要设为__free_hook-0x10。问题3unsorted bin 泄露值算不对libc基址main_arena偏移在不同版本中不同。通过libc-database查版本后用gdb本地确认偏移或者直接用one_gadget / LibcSearcher搜索。经验是把泄露出的地址和已有libc符号表做比较验证计算结果的合理性。7.4 交互类问题问题1菜单提示符匹配不准确sendlineafter里的字符串要和实际输出完全一致包括末尾的冒号、空格、换行符。如果不确定用p.recvuntil(b)改成p.recv()直接接收完整输出然后用splitlines清理。问题2程序输出过多导致sendafter错过提示符有些程序输出很长比如会打印一堆菜单说明直接recvuntil容易等太久。建议用recvuntil加上超时控制或者在开局时先手动缓冲几行输出让交互位置对齐。8. 刷题总结与下一步计划这轮buuctf-pwn-刷题2做完之后我的最大感受是PWN的难点不在某一个具体的利用技巧而在于串起一条完整的、可验证的逻辑链。从信息收集、偏移计算、漏洞判断、地址泄露、payload构造到远程验证每一步都可能让你前功尽弃。以我个人的经验BUUCTF刷题最好的节奏不是一天刷十道而是每道题从拿到手到打穿远程全程复盘一遍。复盘时重点记录三件事第一这道题用了什么新技巧第二哪些环节是我想当然导致返工的第三如果再遇到类似题型我应该从哪个突破口优先入手。接下来我的计划是继续往上挑战更难一档的题源重点放在off-by-one、largebin attack、IO_FILE相关的题目上。BUUCTF平台上有不少近两三年的赛题覆盖这些点用它们练手比闷头看书强得多。如果你也在这个阶段可以挑几道题试着按我这篇记录的方法自己走一遍很多经验只有踩过坑才能真正留下来。