~/archive/entries / stackoverflow
Stack Overflow 栈溢出-基础

0 名词辨析
本文我们主要讨论的是栈缓冲区溢出,而非栈空间耗尽。
1 Intro
先看一个程序:
#include <stdio.h>
#include <string.h>
void f(void) {
char buffer[16];
printf("Input: ");
gets(buffer);
printf("You entered: %s\n", buffer);
}
int main(void) {
f();
return 0;
}如果你把这个程序提交给AI,他绝对会说:

这是为什么呢?因为程序在内存中主要是由以下方式排列的:
| 高地址-低地址排序 |
|---|
| Stack 栈(局部变量)Heap 堆(动态分配) |
| BSS 未初始化全局变量 |
| Data 已初始化全局变量 |
| Read-Only Data字符串和只读常量 |
| Code 机器指令 |
Text 段 i.e. 代码段,主要保存程序的机器指令。
例如:
mov eax, 1
add eax, 2
ret
Data和BSS主要存储已初始化(赋值后)和未初始化(赋值前)的变量。
回到程序。如果你输入"Hello"时,我们大一的C语言课程学过了,它实际输入的是:
'H' 'e' 'l' 'l' 'o' '\n'
6个char,这是符合buffer数组的定义的。
但是我们如果输入:
"Zeraora_Is_My_Favorite_Pokemon"
由于gets函数不知道我要读取多少个字符,所以他就疯狂读取直到结束,于是Stack后方的程序段就会被我们的输入覆盖,这就是栈(缓冲区)溢出
如果说我们需要构造一些函数调用让程序执行,这是一个最基础的操作。
同样的,read函数也有可能溢出,这就会导致读取到其他地方的信息,从而导致信息泄露。
2 Canary
Canary的重要思想是:
在局部缓冲区和重要控制数据之间放置一个特殊值,在函数返回前检查该值是否被修改。
叫这个名字的原因是,早期矿工会携带金丝雀(canary)进入矿井。金丝雀对有毒气体更加敏感。如果金丝雀先出现异常,就说明环境存在危险。
加入后的程序运行方式就是:
Canary 的检查过程
函数开始时:
- 从安全位置读取 Canary
- 将 Canary 保存到当前栈帧
函数结束前:
- 读取栈帧中的 Canary
- 与原始 Canary 比较
- 如果不同,则调用失败处理函数
- 程序终止
gcc编译也会使用这种方法,例如编译时的常见选项包括:
-fstack-protector
-fstack-protector-strong
-fstack-protector-all
-fstack-protector
只保护编译器认为较危险的函数,例如含有较大字符数组的函数。
-fstack-protector-strong
保护范围更广,通常是较推荐的选择。
-fstack-protector-all
几乎为所有函数添加 Canary 检查。保护范围更大,但也可能增加一定运行开销和代码体积。
但是这也有一个缺陷——我不读取到canary不就行了?
或者说,我知道canary了,这不也形同虚设吗?
所以我们还有另一种方法:
3 NX (No-Execute) & DEP (Data Execution Prevention)
它用于将某些内存区域标记为不可执行。
例如栈通常需要:
可读:是
可写:是
可执行:否
堆通常也类似:
可读:是
可写:是
可执行:否
但是NX阻止的是,在不可执行内存中执行新代码。它不阻止攻击复用已有代码。
(未完待续)
No comments yet