缓冲区溢出的那些事儿

缓冲区溢出是引发程序崩溃的常见原因之一,也常常被黑客利用来发起攻击。缓冲区溢出引发的bug常常难以定位。那么缓冲区溢出问题有什么现象呢?如何找出蛛丝马迹从而何定位缓冲区溢出问题呢?

什么是缓冲区?缓冲区就是在程序中开辟的一块区域。这块区域可以在堆上也可以在栈上。

1
2
3
4
5
6
int function()
{
char buffer1[256]; // 在栈上的缓冲区
char *buffer2 = (char *)malloc(sizeof(char)*256); //在堆上的缓冲区
}

一般来说,用malloc分配的内存区域在堆上;直接声明的缓冲区数组是在栈上。堆上的缓冲区溢出和栈上的缓冲区后果是不一样的。在堆上的缓冲区溢出的后果一般,可能覆盖了临近的内存,当然如果临近的内存中有函数指针,也可能照成程序运行到错误的地址。在栈上的缓冲区溢出的后果往往更严重,因为可能破坏了程序的返回地址。下面我们分别对这两种情况进行讨论。

栈上的缓冲区溢出

我们有个程序运行中崩溃,生成core文件,用gdb查看堆栈如下:

1
2
3
(gdb) bt
#0 0x00000000004004f7 in f () at main.c:11
Cannot access memory at address 0x646c726f77206f74

堆栈显示在函数f()函数处崩溃,崩溃的指令地址是0x4004f7,崩溃的原因是不能访问内存0x646c726f77206f74区域。同时我们找不到f的调用者。
我们通过汇编,进一步分析问题:

1
2
3
4
(gdb) disas f
Dump of assembler code for function f:
0x00000000004004f6 <f+94>: leaveq
0x00000000004004f7 <f+95>: retq #程序在返回时出错

发现程序在运行到retq指令出错。retq是程序结束时返回到调用者的指令,返回地址记录在栈上,结合程序不能访问内存0x646c726f77206f74区域,而且我们找不到调用者,我们有理由怀疑是程序的栈被破坏了。寄存器rsp和rbp中保存了栈的起始位置,如下图。
image_1b2g8vaneki8o8su5a1dhh1k1v9.png-27kB
所以我们接下来来检查寄存器rsp和rbp,看能否有所发现:

1
2
3
4
(gdb) p (char *)$rsp
$1 = 0x7fffffffde30 "Hello world, Hello world, Hello world"
(gdb) p (char *)$rbp
$2 = 0x7fffffffde40 "lo world, Hello world"

额,他们之中居然是字符串,而不是地址。很显然是缓冲区溢出覆盖了rsp和rbp。那么程序不能访问的地址0x646c726f77206f74是什么呢?

1
2
3
4
5
6
7
8
9
10
(gdb) p (char)0x64
$3 = 100 'd'
(gdb) p (char)0x6c
$4 = 108 'l'
(gdb) p (char)0x72
$5 = 114 'r'
(gdb) p (char)0x6f
$6 = 111 'o'
(gdb) p (char)0x77
$7 = 119 'w'

发现地址0x646c726f77206f74其实就是字符串world。
好了,问题定位出来了,那么如何来找到代码中的位置从而修复问题呢?我们在代码中搜索world这个字符串,发现如下函数,在栈上申请了2个字节,却写入了大于2个字节的内容。

1
2
3
4
5
6
7
int f()
{
char buff[2];
strcpy(buff,"Hello world, Hello world, Hello world");
printf("%s\n",buff);
return 0;
}

堆上的的缓冲区溢出

程序在core之后,我们看堆栈时往往发现最上层的函数是问号。

1
2
3
4
(gdb) bt
#0 0x000000646c726f77 in ?? ()
#1 0x0000000000400568 in f () at main.c:18
#2 0x000000000040057d in main () at main.c:24

也就是说这个地址gdb并不知道对应的是什么函数。我们只好从第#1处开始寻找蛛丝马迹了。

1
2
3
4
5
6
(gdb) info locals
fp = 0x602030
(gdb) p *fp
$1 = (FP) 0x646c726f77
(gdb) p (char *)fp
$2 = 0x1b17c030 "world"

进一步调试我们发现fp指向的地址变为world了。这也是一个典型的缓冲区溢出问题。出问题的代码定位如下:

1
2
3
4
5
6
7
8
9
int f()
{
char *buf1 = (char *)malloc(sizeof(char)*1);
FP *fp = (FP *)malloc(sizeof(FP)*1);
*fp = msg;
strcpy(buf1,"Hello world, Hello world, Hello world");
(*fp)();
return 0;
}

当然,在实际程序中不一定都是字符串引起的缓冲区溢出,也有可能是二进制。这时候就需要我们把握缓冲区溢出的几个现象来定位问题。缓冲区溢出的现象我们总结一下:

  1. ret的时候发生错误,并且rsp rbp的地址不合理。怀疑是栈上的缓冲区溢出。
  2. 程序的函数指针错误或者临近的内存区域被破坏,怀疑是堆上的缓冲区溢出。
  3. 发生缓冲区溢出时,根据错误位置来查找代码中可疑的溢出缓冲区。

定位问题往往很难,那么我们如何在编程的时候就确保不会发生缓冲区溢出呢?

编译可以自己检查缓冲区溢出的程序

如果我们有程序的代码,可以在编译时加上-fsanitize=address

1
gcc -g -O0 -fsanitize=address -o main main.c

这样编译过后的程序就会自带缓冲区溢出保护功能。如果运行时发生缓冲区溢出问题就会报错:
image_1b2ddtar8ar31lv61upg143j1i449.png-173.4kB
输出的是彩色的,好漂亮。怎么做到的呢?参考这里
对于堆上缓冲区溢出这种方法同样有效。

valgrind检查缓冲区溢出

如果我们没有程序的代码,或者不允许我们再次编译程序,但是需要我们定位问题,那么可以用valgrind工具来检查缓冲区溢出的问题

1
2
3
4
5
$ valgrind --leak-check=full ./heap
==3854== Invalid write of size 8
==3854== at 0x4005B4: f (heap.c:17)
==3854== by 0x40060B: main (heap.c:24)
==3854== Address 0x5203040 is 0 bytes inside a block of size 1 alloc'd

valgrind会报Invalid write of size 8,发现非法内存写入和详细的位置。

在程序中加上保护

缓冲区溢出往往是程序员的责任。根据缓冲区溢出的原理,自己在容易发生溢出的函数中加上保护。在栈底加个MAGICNUMBER,如果这个值被改了,那么说明是栈被破坏了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include<stdio.h>
#include<stdlib.h>
#define MAGIC 1989
#define F_INTER int __a=MAGIC;
#define F_EXIT \
if(__a!=MAGIC) \
printf("stack error in %s, magic=%d\n",__func__,__a);
void foo()
{
F_INTER
int buf[1]={0};
buf[0]=1;
buf[1]=2;
buf[2]=3;
buf[3]=4;
buf[4]=5;
buf[5]=6;
buf[6]=7;
F_EXIT
return;
}
int main()
{
foo();
return 0;
}
1
2
3
$ ./main2
stack error in foo, magic=4
Segmentation fault (core dumped)

程序在报错之前,会打出缓冲区溢出错误。方便直接定位问题。
好了,看到这里相信你对缓冲区溢出的那些事儿了解一些了吧,但愿你在下次遇到缓冲区的问题能够快速定位。

如果还想看到更多此类文章,请移步到小宇的博客

文章目录
  1. 1. 栈上的缓冲区溢出
  2. 2. 堆上的的缓冲区溢出
  3. 3. 编译可以自己检查缓冲区溢出的程序
  4. 4. valgrind检查缓冲区溢出
  5. 5. 在程序中加上保护
|