解析2025强网拟态BabyStack
BabyStack - CTF PWN题完整技术解析研究摘要本篇文章记录了对CTF题目"babystack"的完整分析和漏洞利用过程。该题目是一道经典的栈溢出题,主要考察对栈结构的理解和变量覆盖技术。 2025-10-28 05:35:23 Author: www.freebuf.com(查看原文) 阅读量:1 收藏

BabyStack - CTF PWN题完整技术解析

研究摘要

本篇文章记录了对CTF题目"babystack"的完整分析和漏洞利用过程。该题目是一道经典的栈溢出题,主要考察对栈结构的理解和变量覆盖技术。

核心发现

  • 漏洞类型:栈缓冲区溢出 → 变量覆盖

  • 漏洞成因:第二次read()允许输入256字节,可覆盖栈上的检查变量

  • 关键偏移:0xf8字节(248字节)精确覆盖

  • 成功条件:将v3变量从0xabc1337覆盖为0x1337abc

  • 最终效果:获得远程shell访问权限

研究方法

  1. 静态分析(objdump、strings、checksec)

  2. 动态调试(GDB断点跟踪)

  3. 偏移计算(栈布局分析)

  4. 本地验证(Python快速测试)

  5. Exploit开发(pwntools自动化)

一、题目概述

1.1 题目信息

  • 题目名称:babystack

  • 题目类型:PWN(二进制漏洞利用)

  • 题目描述:拿到属于你的shell吧 / Get your own shell

二、环境准备与信息收集

2.1 文件基本信息

首先,我们使用file命令查看文件类型:

$ file babystack
babystack: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=4649ed5a3ce2485cb0c8ad02daa27011db41ae3f, not stripped

解读

  • ELF 64-bit:这是一个Linux下的64位可执行文件

  • LSB executable:小端序(Little-Endian)架构

  • x86-64:运行在64位x86处理器上

  • dynamically linked:动态链接,依赖共享库

  • not stripped:未去除符号表,便于调试和分析

2.2 安全保护机制检查

使用checksec工具检查二进制文件的安全保护机制:

$ checksec babystack
[*] '/mnt/hgfs/share/NRT/PWN/babystack/babystack'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        No PIE (0x400000)
    Stripped:   No

保护机制详解

  1. Partial RELRO(部分重定位只读)

    • GOT表部分可写,但本题不涉及GOT劫持

  2. No canary found(无栈保护)

    • 这是关键!没有Stack Canary(栈金丝雀)保护

    • 意味着可以随意进行栈溢出,不会被检测到

    • 栈金丝雀是编译器在栈上放置的特殊值,函数返回前会检查这个值是否被修改

  3. NX enabled(栈不可执行)

    • 栈上的数据不能作为代码执行

    • 无法直接在栈上放置shellcode并执行

    • 需要使用ROP或者其他技术

  4. No PIE(地址不随机化)

    • 程序加载地址固定在0x400000

    • 函数地址、字符串地址等都是固定的,便于利用

  5. Not Stripped(未去除符号)

    • 保留了函数名、变量名等调试信息

    • 方便我们分析程序逻辑

安全总结:这是一道入门级题目,只开启了NX保护,其他保护全部关闭,利用难度很低。

三、程序逻辑分析

3.1 主函数分析

使用objdump反汇编main函数:

0000000000400819 <main>:
  400819:   push   rbp
  40081a:   mov    rbp,rsp
  40081d:   mov    rax,QWORD PTR [rip+0x20086c]        # 601090 <stdin@GLIBC_2.2.5>
  ...
  40087c:   call   400746 <pwn>                        # 调用pwn函数
  400881:   mov    eax,0x0
  400886:   pop    rbp
  400887:   ret

分析:main函数主要做了三件事:

  1. 设置标准输入/输出/错误流的缓冲模式(setvbuf)

  2. 调用pwn()函数(核心逻辑)

  3. 返回0

3.2 pwn函数详细分析

pwn函数是漏洞所在,我们逐步分析:

0000000000400746 <pwn>:
  ; 函数序言
  400746:   push   rbp                    # 保存旧的rbp
  400747:   mov    rbp,rsp                # 设置新的栈帧
  40074a:   sub    rsp,0x120              # 分配0x120(288)字节栈空间

  ; 初始化缓冲区
  400751:   lea    rax,[rbp-0x120]        # 缓冲区起始地址
  400758:   mov    edx,0x110              # memset大小:0x110(272)字节
  40075d:   mov    esi,0x0                # 填充值:0
  400762:   mov    rdi,rax                # 目标地址
  400765:   call   400600 <memset@plt>    # 清零缓冲区

  ; 初始化检查变量
  40076a:   mov    QWORD PTR [rbp-0x10],0xabc1337  # 关键!设置v3=0xabc1337

  ; 第一次输入
  400772:   mov    edi,0x40092e           # "Enter your flag1:"
  400777:   mov    eax,0x0
  40077c:   call   4005f0 <printf@plt>    # 打印提示
  400781:   lea    rax,[rbp-0x120]        # 输入目标:buf
  400788:   mov    edx,0x18               # 读取大小:0x18(24)字节
  40078d:   mov    rsi,rax
  400790:   mov    edi,0x0                # stdin
  400795:   call   400610 <read@plt>      # 第一次read

  ; 第二次输入
  40079a:   mov    edi,0x400940           # "Enter your flag2:"
  40079f:   mov    eax,0x0
  4007a4:   call   4005f0 <printf@plt>    # 打印提示
  4007a9:   lea    rax,[rbp-0x120]
  4007b0:   add    rax,0x18               # 输入目标:buf+0x18
  4007b4:   mov    edx,0x100              # 读取大小:0x100(256)字节 漏洞点!
  4007b9:   mov    rsi,rax
  4007bc:   mov    edi,0x0                # stdin
  4007c1:   call   400610 <read@plt>      # 第二次read

  ; 打印输入内容
  4007c6:   lea    rax,[rbp-0x120]
  4007cd:   lea    rdx,[rax+0x18]         # flag2
  4007d1:   lea    rax,[rbp-0x120]        # flag1
  4007d8:   mov    rsi,rax
  4007db:   mov    edi,0x400952           # "Nice!, %s, your flag2 is %s."
  4007e0:   mov    eax,0x0
  4007e5:   call   4005f0 <printf@plt>

  ; 检查变量是否被修改
  4007ea:   mov    rax,QWORD PTR [rbp-0x10]  # 读取v3的值
  4007ee:   cmp    rax,0x1337abc             # 比较:v3 == 0x1337abc?
  4007f4:   jne    40080c <pwn+0xc6>         # 不等则跳转到失败分支

  ; 成功分支:获得shell
  4007f6:   mov    edi,0x400970              # "GIMME GIMME SHELL /bin/sh"
  4007fb:   call   4005d0 <puts@plt>
  400800:   mov    edi,0x400989              # "/bin/sh"
  400805:   call   4005e0 <system@plt>       # 执行system("/bin/sh") 目标!
  40080a:   jmp    400816 <pwn+0xd0>

  ; 失败分支
  40080c:   mov    edi,0x400991              # "you are a good boy."
  400811:   call   4005d0 <puts@plt>

  400816:   nop
  400817:   leave
  400818:   ret

3.3 伪代码重构

为了更清晰地理解程序逻辑,我们将汇编代码转换为C伪代码:

void pwn() {
    char buf[0x110];          // 缓冲区,位于[rbp-0x120]
    long long v3;             // 检查变量,位于[rbp-0x10]

    memset(buf, 0, 0x110);    // 清零缓冲区
    v3 = 0xabc1337;           // 初始化检查变量

    // 第一次输入
    printf("Enter your flag1:");
    read(0, buf, 0x18);       // 读取24字节到buf

    // 第二次输入
    printf("Enter your flag2:");
    read(0, buf + 0x18, 0x100); // 读取256字节到buf+0x18 危险!

    // 打印输入
    printf("Nice!, %s, your flag2 is %s.", buf, buf + 0x18);

    // 检查v3是否被修改为目标值
    if (v3 == 0x1337abc) {
        puts("you are also a good boy.");
        system("/bin/sh");     // 获得shell 
    } else {
        puts("you are a good boy.");
    }
}

3.4 实际运行观察

正常运行(未利用漏洞)

$ echo -e "test1\ntest2" | ./babystack
Enter your flag1:Enter your flag2:Nice!, test1
, your flag2 is test2
.
you are a good boy.

关键观察

  • 程序接收两次输入

  • 打印输入内容

  • 由于v3保持初始值0xabc1337,不等于0x1337abc

  • 输出"you are a good boy."(失败分支)

成功利用后的输出

$ python3 exploit.py
Enter your flag1:Enter your flag2:Nice!, aaaaaa..., your flag2 is aaaaaa...
you are also a good boy.
$ # 获得shell

注意区别

  • 失败:you are a good boy.

  • 成功:you are also a good boy.+ shell访问权限

四、漏洞原理深度剖析

4.1 栈布局分析

在64位程序中,栈是从高地址向低地址增长的。pwn函数的栈布局如下:

高地址
+------------------+
| 返回地址         | rbp+0x8
+------------------+
| 保存的rbp        | rbp (函数序言push rbp)
+------------------+
| ...              | rbp-0x8
+------------------+
| v3变量           | rbp-0x10   目标变量!初始值0xabc1337
+------------------+
| ...              |
+------------------+
| buf+0x18开始     | rbp-0x108  第二次read的起点
| (第二次输入)     |
| ...              |
+------------------+
| buf开始          | rbp-0x120  第一次read的起点
| (第一次输入)     |
+------------------+
低地址

4.2 漏洞点定位

关键问题:第二次read()存在栈溢出漏洞!

让我们计算一下:

  1. 第二次read的起始位置rbp-0x120+0x18 = rbp-0x108

  2. 第二次read的大小0x100 = 256字节

  3. v3变量的位置rbp-0x10

  4. 从read起点到v3的距离0x108 - 0x10 = 0xf8 = 248字节

问题所在

  • 第二次read允许输入256字节

  • 但从起点rbp-0x108v3变量只有248字节的距离

  • 因此,我们可以通过第二次输入,覆盖掉v3变量的值!

4.3 漏洞利用原理

攻击思路

  1. 第一次输入随意填充(因为只是普通输入,不影响漏洞利用)

  2. 第二次输入构造特殊payload:

    • 前248字节:垃圾数据(填充buf到v3之间的空间)

    • 第249-256字节:将v3覆盖为0x1337abc

为什么要改成0x1337abc而不是0xabc1337?

仔细观察汇编代码:

40076a:   mov    QWORD PTR [rbp-0x10],0xabc1337  # 初始化
4007ee:   cmp    rax,0x1337abc                    # 检查

这里有个有趣的点:

  • 初始化时设置的是0xabc1337

  • 但检查时比较的是0x1337abc

  • 这是出题人故意设置的小陷阱(或者是初始化时的bug)

  • 我们需要覆盖为检查时的值


文章来源: https://www.freebuf.com/articles/others-articles/454496.html
如有侵权请联系:admin#unsafe.sh