内存布局

x86-64 Linux 内存布局

栈:Runtime stack(8MB limit),用来存储局部变量等。

堆:根据需要动态进行分配。在调用malloc(),calloc(),new()时,在堆上进行内存分配。

数据段(data):静态地分配数据。如:全局变量,静态变量,字符串常量。

文本段/共享存储区(text/shared Libraries):可执行的机器指令(只读)。

img

让我们看一个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
char big_array[1L<<24]; /* 16MB */
char huge_array[1L<<31]; /* 2GB */
int gobal = 0;
int useless() { return 0; }

int main() {
void *phuge1, *psmall2, *phuge3, *psmall4;
int local = 0;
phuge1 = malloc(1L<<28);
psmall2 = malloc(1L<<8);
phuge3 = malloc(1L<<32);
psmall4 = malloc(1L<<8);
}

img

缓冲区溢出

什么是缓冲区溢出?通常指的是我们去访问超过数组大小的内存。

缓冲区溢出bug可能允许远程计算机在受害计算机上执行任意代码。

一个典型的会发生Buffer Overflow的代码是gets()函数。

1
2
3
4
5
6
7
8
9
10
11
char *gets(char *dest)
{
int c = getchar();
char *p = dest;
while(c!=EOF&&c!='\n') {
*p++ = c;
c = getchar();
}
*p = '\0';
return dest;
}

这段代码没有去限制读入字符的数量,如果传入的dest的大小小于了输入字符串的大小,就会发生缓冲区溢出。

从前面的学习我们知道,一个栈帧的顶部是下一条待执行指令的地址,会使%rip指针跳转到该地址。我们如果得到了Return address处于栈的哪个位置,我们就可以利用缓冲区溢出。注入我们需要执行的代码,并且将Return address替换成我们注入代码的代码地址,就可以实现攻击了。

防止攻击的方法

1.避免代码中的溢出漏洞

  • 用fgets替换gets
  • 用strncpy替换strcpy
  • 不使用scanf读入%s
    • 使用fgets读入字符串
    • 或者使用%ns,这里的n是大小

2.系统级保护

  • 不可执行代码段。x86-64添加了显示的“执行”权限,栈区被标记成不可执行。
  • 随机栈偏移。在程序开始时,在堆栈上分配随机数量的空间,移动整个程序的栈地址。使攻击者难以预测插入代码的首地址。每次程序执行时重新定位栈。

3.栈金丝雀

在分配栈空间时,在栈帧顶部设立一个金丝雀值,当发现金丝雀值被修改时,代表发生了溢出。

面向返回的编程攻击

这种攻击方式存在局限性:只能利用已经存在的代码,且无法绕过栈金丝雀。

ret指令是一条非常特殊的指令,当我们遇到ret时,我们会弹出栈顶元素,然后跳转到这个返回地址,继续执行。

所以当我们的栈顶,是一系列ret前的所需执行代码的地址。我们就可以持续的执行。

举一个例子

1
2
3
4
5
6
7
8
9
10
11
12
code A:
add %ris, %rdi
ret

code B:
mul %rsi, %rdi
add %rdi, %rax
ret

stack:
2
6 // Return address

我们会将栈顶的6弹出,并且将%rip设置为6,所以程序会执行6,7,并且由于8仍然是ret,所以我们会将2也视为Return

address,接着%rip会跳转到2,执行add指令。如此循环,我们可以执行一系列的target code,通过ret串起来。

联合体

分配的内存与最大元素相同。在一个时间只能使用一个字段。

img