CMU 15-213 Intro to Computer Systems Lecture 9
课程主页:http://www.cs.cmu.edu/afs/cs/academic/class/15213-f15/www/schedule.html
课程资料:https://github.com/EugeneLiu/translationCSAPP
课程视频:https://www.bilibili.com/video/av31289365/
这一讲介绍了机器级编程5:高级主题。
内存布局
x86-64 Linux内存布局
- 堆栈
- 运行时的堆栈(限制为8MB)
- 例如,局部变量
- 堆
- 根据需要动态分配
- 调用malloc(), calloc(), new()时使用
- 数据
- 静态分配的数据
- 例如,全局变量,静态变量,字符串常量
- 文本/共享库
- 可执行机器指令
- 只读
缓存区溢出
回忆:内存引用错误示例
typedef struct {
int a[2];
double d;
} struct_t;
double fun(int i) {
volatile struct_t s;
s.d = 3.14;
s.a[i] = 1073741824; /* Possibly out of bounds */
return s.d;
}
产生如下结果:
fun(0) ➙ 3.14
fun(1) ➙ 3.14
fun(2) ➙ 3.1399998664856
fun(3) ➙ 2.00000061035156
fun(4) ➙ 3.14
fun(6) ➙ Segmentation fault
内存中结构的布局如下:
如果输入大于1,那么会触碰到存储浮点数的内存部分,所以会产生上述错误。
上述现象会产生大问题
- 通常称为“缓存区溢出”
- 当超过分配给数组的内存大小时
- 为什么会产生大问题?
- 这是导致安全漏洞的第一大技术原因
- 总体原因是社会工程/用户无知
- 这是导致安全漏洞的第一大技术原因
- 最常见的形式
- 字符串输入中未经检查的长度
- 特别是对于堆栈上的有界字符数组
- 有时称为stack smashing
考虑如下例子:
/* Get string from stdin */
char *gets(char *dest)
{ int c = getchar();
char *p = dest;
while (c != EOF && c != '\n') {
*p++ = c;
c = getchar();
}
*p = '\0';
return dest;
}
上述代码无法限制读取字符的数量,同样会产生缓存区溢出的问题。
例子
/* Echo Line */
void echo()
{
char buf[4]; /* Way too small! */
gets(buf);
puts(buf);
}
void call_echo() {
echo();
}
unix>./bufdemo-nsp
Type a string:012345678901234567890123
012345678901234567890123
unix>./bufdemo-nsp
Type a string:0123456789012345678901234
Segmentation Fault
反汇编的结果为如下:
echo:
00000000004006cf <echo>:
4006cf: 48 83 ec 18 sub $0x18,%rsp
4006d3: 48 89 e7 mov %rsp,%rdi
4006d6: e8 a5 ff ff ff callq 400680 <gets>
4006db: 48 89 e7 mov %rsp,%rdi
4006de: e8 3d fe ff ff callq 400520 <puts@plt>
4006e3: 48 83 c4 18 add $0x18,%rsp
4006e7: c3 retq
call_echo:
4006e8: 48 83 ec 08 sub $0x8,%rsp
4006ec: b8 00 00 00 00 mov $0x0,%eax
4006f1: e8 d9 ff ff ff callq 4006cf <echo>
4006f6: 48 83 c4 08 add $0x8,%rsp
4006fa: c3 retq
在调用gets函数之前,栈的布局如下:
如果运行如下命令:
unix>./bufdemo-nsp
Type a string:012345678901234567890123
012345678901234567890123
那么布局如下:
如果运行如下命令:
unix>./bufdemo-nsp
Type a string:0123456789012345678901234
Segmentation Fault
那么布局如下:
如果运行如下命令:
unix>./bufdemo-nsp
Type a string:012345678901234567890123
012345678901234567890123
那么布局如下:
此时会返回到其他的位置,例如:
. . .
400600: mov %rsp,%rbp
400603: mov %rax,%rdx
400606: shr $0x3f,%rdx
40060a: add %rdx,%rax
40060d: sar %rax
400610: jne 400614
400612: pop %rbp
400613: retq
这样就会产生安全问题,例如代码注入攻击,缓冲区溢出错误可使远程计算机在受害计算机上执行任意代码。
蠕虫和病毒
- 蠕虫:是一个程序
- 可以自己运行
- 可以将自身的完全正常版本传播到其他计算机
- 病毒:是一段代码
- 将自身添加到其他程序
- 不独立运行
- 两者(通常)在计算机之间传播并造成破坏
如何处理缓存区溢出攻击
- 避免溢出漏洞
- 采用系统级保护
- 让编译器使用“堆栈金丝雀”
避免溢出漏洞
/* Echo Line */
void echo()
{
char buf[4]; /* Way too small! */
fgets(buf, 4, stdin);
puts(buf);
}
例如在之前代码中限制输入字符的数量。
系统级保护
堆栈随机偏移
在程序开始时,在堆栈上分配随机数量的空间
移位整个程序的堆栈地址
使黑客很难预测插入代码的开始
例如5次执行内存分配代码的结果
local 0x7ffe4d3be87c 0x7fff75a4f9fc 0x7ffeadb7c80c 0x7ffeaea2fdac 0x7ffcd452017c
程序每次执行时都会重新定位堆栈
- 不可执行的代码段
- 在传统的x86中,可以将内存区域标记为“只读”或“可写”
- 可以执行任何可读的
- X86-64添加了明确的“执行”权限
- 堆栈标记为不可执行
- 在传统的x86中,可以将内存区域标记为“只读”或“可写”
堆栈“金丝雀”(canary)
- 思想
- 将特殊值(“ canary”)放在在缓存区之外的堆栈上
- 退出函数前检查是否损坏
- GCC实现
- -fstack-protector
- 现在是默认值(之前已禁用)
还是echo那段代码,在调用gets之前堆栈的布局如下:
假设输入为
0123456
那么堆栈的布局为
面向返回的编程攻击
- 挑战(针对黑客)
- 堆栈随机化使得难以预测缓冲区位置
- 将堆栈标记为不可执行,使其很难插入二进制代码
- 替代策略
- 使用现有代码
- 例如,来自stdlib的库代码
- 将片段串在一起以实现总体预期结果
- 无法克服堆栈金丝雀
- 使用现有代码
- 从小工具(Gadget)构造程序
- 以ret结尾的指令序列
- 由单字节0xc3编码
- 每次运行固定的代码位置
- 代码是可执行的
- 以ret结尾的指令序列
Gadget例子
上述代码的一部分可以对应如下操作
ROP执行
联合
- 根据最大元素分配
- 一次只能使用一个字段
例如
union U1 {
char c;
int i[2];
double v;
} *up;
内存布局如下:
对比结构:
struct S1 {
char c;
int i[2];
double v;
} *sp;
内存布局如下:
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Doraemonzzz!
评论
ValineLivere