基于安全的一些思考--缓冲区溢出

来源:互联网 发布:网络诈骗安全用语 编辑:程序博客网 时间:2024/06/06 03:15

缓冲区溢出背后根本的原因是C和C++本身是不安全的,他们没有对数组和指针引用进行边界检测。

不安全的字符串操作函数包括:

Strcpy()

Strcat()

Sprintf()

Gets()

Scanf()

 

堆上存储的数据都是通过使用malloc()和new来完成,栈上存放的数据通常包括非静态的局部变量和传值的参数。大多数其他数据都存储在其他区域里。当为大量连续的同类型数据分配一段存储区域时,这个区域叫做缓冲区(buffer)

 

利用堆溢出比栈溢出更难

 

在安全上,任何时候授予特权,都有可能发生权限提升

 

通过利用代码的suid区内的缓冲溢出,许多广泛使用的unix程序(lpr,xterm,eject)被滥用以至于放弃了根权限

 

一个常见的入侵技术就是找到一个suid root程序的缓冲区溢出,然后利用这个溢出来启动一个交互的shell

 

完全控制一台机器指unix机器上得到根用户的权限,或在windows上得到管理员权限

 

永远不要使用gets(),这个函数从标准输入中读取用户输入的文本,直到文件的结尾,或者直到换行标识符才会停止读取,gets()不做任何边界检查,可以用fgets()代替,fgets()有一个用于限制接收字符数量的参数,这种方式提供了一种避免缓冲溢出的机制

如我们不用

void main() {

       charbuf[1024];

       gets(buf);

}

而使用

#difine BUFSIZE 1024

 voidmain(){

      char buf[BUFSIZE];

      fgets(buf,BUFSIZE,stdin);

 }

 

不安全的函数列举如下:
strcpy()

Strcat()

Sprint()

Scanf()

Sscanf()

Fscanf()
vfscanf()

Vsprintf()

Vscanf()

Vsscanf()

Streadd()
strecpy()

Strtrns()

 

 

Strcpy()函数是把源字符串复制到缓冲区,但没有具体说明要复制的字符数。复制的字符个数直接取决于源字符串中字符数。如果知道目标缓冲区的大小,大么可以添加一个明确的检查

if(strlen(src) >= dst_size){

 

}

else{

       strcpy(dst,src);

}

一个更简单的方法是使用strcpy()库例程:

strncpy(dst,src,dst_size-1);

dst[dst_size-1] =  '\0';

当src比dst大时,函数并不会抛出错误,只是在目标缓冲区的空间被占满后停止复制,此时strncpy()返回-1。当src比dst大时,它将留出空间以便在dst字符串后面存放一个null字符

另外一种确保使用strcpy()不会溢出的方法就是在需要时给它分配好内存空间,如:

dst = (char *)malloc(strlen(src)+1);

strcpy(dst,src);

 

strcat()和strcpy()很像,不过它是把字符串接在缓冲区的结尾处。更安全的相似函数为strncat()。使用strncat()时需要注意目标缓冲区中还有多少剩余空间。这个函数只会限制你复制的字符数,而不关注复制完成之后整个字符串的长度

 

sprinf()和csprinf()设置文本格式,并将文本保存到缓冲区,可以直接用来模仿strcpy()的功能

 

scanf()家族的其他函数也是类似问题,比较下面两段程序

void main(int argc,char **argv){

       charbuf[256];

       sscanf(argv[0],"%s",&buf);

}

void main(int argc,char **argv){

       charbuf[256];

       sscanf(argv[0],"%255s",&buf);

}

第一段代码如果读入的字符超过缓冲区的空间就会发生溢出

第二段代码中%和s之间的255限制了最多能将argv[0]中的255个字符存储到可变缓冲变量buf中

 

Streadd()和strecpy()函数的问题在于会将一个可能含有无法读取字符的字符串翻译为可打印的字符串。例如:

#include <libgen.h>

 voidmain(int argc,char **argv){

      char buf[20];

      streadd(buf,"\t\n","");

      printf("%s\n",buf);

 }

输出的结果为\t\n

而不是打印出空白

考虑最差的情况,如果输入缓冲区仅有一个字符,假设为ascii 001(A),使用上述函数时会打印出\001,即字符串增长了4倍,一旦空间没有足够分配,使得输出缓冲区一直为输入缓冲区的4倍,那么缓冲区溢出就很有可能出现

 

Strtrns()比较少见,因为很难理解,strtrns()将3个字符串和一个储存结果字符串的缓冲区作为参数。第一个字符串必须复制进缓冲区中。第一个字符串中的字符如果没有在第二个字符串中出现,就会被复制进缓冲区中。如果字符在第一个、第二个字符串中都有出现,则它将会被第三个字符串中同一索引处的字符替代。我们来看看该函数是如何使用的

#include <libgen.h>

 

void main(int argc,char **argv){

       charlower[] = "abcdefghijklmnopqrstuvwxyz";

       charupper[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";

       char*buf;

       if(argc< 2){

              printf("USAGE:%sarg\n",argv[0]);

              return0;

       }      buf = (char *)malloc(strlen(argv[1])+1);

       strtrns(argv[1],lower,upper,buf);

       printf("%s\n",buf);

}

上述代码将argv[1]中所有小写字符转换为大写字符。如果上述代码没有使用malloc()函数分配足够的空间来复制argv[1],则可能会引发缓冲区溢出

 

Realpath()函数接收包含相对路径的字符串。并通过绝对路径的方式将其转换成指向同一文件的字符串,此时,它展开了所有符号链接。该函数有两个参数,第一个是将被转换的字符串,第二个是存储转换结果的缓冲区。当然需要确保缓冲区大小足够大,以处理任何大小的路径。如果传递给它的、待转换的路径的大小大于MAXPATHLEN,则实现realpath()的内部静态缓冲区会溢出。Getopt()以及getpass()函数都可能产生内部静态缓冲区溢出问题,使用时最好设置传递给这些函数的输入长度的阈值。

 

另一个要避免的系统调用是getenv(),使用时最大的问题是:永远不能假定特定环境变量满足任何特定长度。环境变量永远值得仔细考虑。

 

栈破坏是最严重的缓冲区溢出攻击,尤其当被破坏的栈运行在特权模式下时,一个很好的解决该问题的方案是使用非执行栈。一般情况下,利用栈的代码都是在程序栈上编写,并在那里执行

 

栈溢出会产生杠杆效应,使得程序跳转到正在执行的放在堆中的代码处。放在栈中的代码实际上都没被执行,被执行的代码都放在堆中。注意非执行栈会破坏那些依赖于它们行为的程序。

 

有一种叫做金丝雀的栈保护模式:把一些数据放在被分配的栈的尾部,而后在缓冲区溢出之前检查那些数据是否还在那里。

 

在许多系统中,每个进程都有映射到物理内存的虚拟地址空间。我们应该关心那些理论上被允许访问的占用大块连续内存空间的进程,在某些情况下这部分进程可能会被人滥用。

 

从高层上看,内存常被划分被一些不同的区域:

程序参数和程序环境

程序栈。栈的大小会随着程序的执行而不断的变化。通常情况下,它会变小,趋近于堆

堆。随着程序的执行,堆也会增长,通常它会增大,趋近于栈

块存储段(block storage segment,BSS)包含全局有效的数据(如,全局变量)。在初次使用时,BSS常会被清零。

数据段包含已初始化的全局有效数据(通常是全局变量)

文本段包含只读程序代码

 

BSS、数据段和文本段构成了静态内存,即在程序运行之前这些段的大小都是固定的

跟静态内存相比,栈和堆都是动态的。运行时内存分配的直接结果就是堆和栈大小的变化。运行时内存分配包括两类:栈分配和堆分配。程序员与堆之间的接口随着编程语言的不同而不同,C中通过malloc()访问,C++中通过new()来访问

无论何时,调用函数时,分配栈是自动完成的。栈中会保存当前函数调用的环境信息。这类信息的容器被称为活动记录(activation)或者栈帧(stack frame),它是一块连续的存储区。很多东西都可以进入活动记录,而活动记录的内容一般依赖于体系结构和编译器。放入栈帧中的常见数据包括:函数中非静态局部变量值、实参、保存的注册信息,以及到函数返回时程序将跳转到的地址,由于效率的缘故(一个依赖于编译器的因素),这些数据中有许多都被保存到计算机的注册表中而不是栈中。

 

攻击者制造成功的堆溢出是很困难的事。首先,需要找出安全攸关的变量,其次需要制造一个缓冲区,以此来覆盖那个目标变量(否则,没有办法制造溢出,并进入到变量的地址空间)

 

先来研究下堆溢出:

原始程序片段:

void main(int argc,char **argv){

       inti;

       char*str = (char *)malloc(sizeof(char)*4);

       char*super_user = (char *)malloc(sizeof(char)*9);

       strcpy(super_user,"viega");

       if(argc> 1)

              strcpy(str,argv[1]);

       else

              strcpy(str,"xyz");

}

 

先对程序进行修改,打印出两个缓冲区的地址:

void main(int argc,char **argv){

       inti;

       char*str = (char *)malloc(sizeof(char)*4);

       char*super_user = (char *)malloc(sizeof(char)*9);

       printf("Addressof str is:%p\n",str );

       printf("Addressof super_user is %p\n",super_user);

       strcpy(super_user,"viega");

       if(argc> 1)

              strcpy(str,argv[1]);

       else

              strcpy(str,"xyz");

}

接下来,再进行修改,在片段的结尾打印出从str到super_user结束的所有内存

void main(int argc,char **argv){

       inti;

       char*str = (char *)malloc(sizeof(char)*4);

       char*super_user = (char *)malloc(sizeof(char)*9);

       char*tmp;

       printf("Addressof str is:%p\n",str );

       printf("Addressof super_user is %p\n",super_user);

       strcpy(super_user,"viega");

       if(argc> 1)

              strcpy(str,argv[1]);

       else

              strcpy(str,"xyz");

 

       tmp= str;

       while(tmp< super_user + 9){

              printf("%p:%c(0x%x)\n",tmp,isprint(*tmp)? *tmp:'?',(unsigned int)(* tmp));

              tmp+= 1

       }

}

 

注:在printf格式字符串中,%p参数将tmp这个指向内存的指针以十六进制格式的方式打印出来。%c使得字节以字符的方式打印出来。%x以十六进制的方式打印一个整数。由于tmp中元素的值比整数短,如果不当做无符号整数来处理,就会进行符号化扩展(例如,若将字符型0x8A当作有符号整型数来处理,他就会被转化为0Xffffff8A)。因此需要将其转换为无符号整型数。

 

 

栈上总有安全攸关的区域可被用于覆盖—这个区域就是返回地址

一下是栈溢出过程:

在栈结构中找出一块分配在栈中会溢出的缓冲区,并且这个缓冲区允许覆盖它的返回地址—》在我们所攻击的函数返回时,在跳转内存空间上放置一些恶意代码—》改写栈里的返回地址,使之跳转到恶意代码

 

有两类在栈上分配的数据:非静态局部变量和用于函数传递的参数。我们只能把在内存中地址比返回地址低的数据用于溢出。首先要做的是,找出一些函数并映射到栈里。也就是说,在感兴趣的空间中找出参数或者找出本地变量与返回地址之间的关联关系

同样从一个简单的小程序开始:

void test(int i){

       charbuf[12];

}

int main(){

       test(12);

}

测试函数中有一个本地参数和一个静态分配的缓冲区,修改代码来打印出两个变量在内存中的地址

void test(int i){

       charbuf[12];

       printf("&i=%p\n",&i);

       printf("&buf[0]=%p\n",buf);

}

int main(){

       test(12);

}

返回地址是相对于main()函数地址的偏移,改动代码打印出main函数的地址

void test(int i){

       charbuf[12];

       printf("&main= %p\n",&main);

       printf("&i=%p\n",&i);

       printf("&buf[0]=%p\n",buf);

}

int main(){

       test(12);

}

我们希望找到看起来和主函数地址很接近但又更高的地址,我们从变量buf之前8字节开始查找,知道变量i之后的8字节为止。修改代码如下:

void test(int i){

       charbuf[12];

       printf("&main= %p\n",&main);

       printf("&i=%p\n",&i);

       printf("&buf[0]=%p\n",buf);

       for(j=buf-8;j<((char*)&i)+8;j++)

              printf("%p:0x%x\n",j,*(unsignedchar *)j);

}

int main(){

       test(12);

}

为了得到变量i处之上的8字节,我们不得不把变量的地址强制转换为char *.这是因为C语言中,将地址+8时,实际结果将跳转至当前存储的数据类型大小乘以8倍的位置处。这就意味着:整数指针++8的结果是将内存地址增加32个字节而不是所希望的8字节

 

内存地址是4字节的

 

X86采用底字节顺序对多字节原语类型(primitivetype)进行存储

 

ebp寄存器的值被称为基指针(basepointer),它指向当前的栈帧。访问局部变量和参数的代码以基址为标准重新编址

 

栈帧的内容如下:

低地址

       局部变量

       原始基址

       返回地址

       函数的参数

高地址

 

堆栈向着内存地址为0的方向增长,并且以前的栈帧在函数参数的下面

 

 

任意数据与其自身异或(XOR)的结果都是0

 

当你识别出一个可用于溢出的缓冲区时,你一般要计算出缓冲区首地址到返回地址之间的距离。

 

不必准确计算代码在栈中的存储位置

 

如果溢出时存在大小限制的话,可以考虑堆溢出,并把代码放在堆里,而跳转到堆里总是可行的。另一个可选择的办法是把shell代码放在环境变量里,因为它总是存储在栈的最顶端。

 

Windows平台想调用的函数是动态加载的,想计算函数在内存中的位置会很困难。

原创粉丝点击