【计算机系统】缓冲区溢出攻击概念、演示及防御

【计算机系统】缓冲区溢出攻击概念、演示及防御本文详细解释了缓冲区溢出攻击的概念 演示了如何利用漏洞以及防御措施如栈保护 栈随机化和栈破坏检测

大家好,欢迎来到IT知识分享网。

一、缓冲区溢出攻击概念

缓冲区溢出攻击是一种常见的安全漏洞,也被称为缓冲区溢出。它发生在程序尝试向缓冲区

写入数据时,超过了缓冲区的容量导致数据溢出到相邻的内存区域。这种溢出可能破坏程序的堆栈,使程序转而执行其它指令,从而达到攻击的目的。

缓冲区溢出攻击的原理主要是利用程序中存在的缓冲区溢出漏洞。当程序没有仔细检查用户输入的参数时,攻击者可以通过输入超出缓冲区边界的恶意数据来破坏程序的正常执行流程。这些数据可以覆盖程序中的其他数据或函数返回地址,导致程序执行攻击者指定的恶意代码

例如,在一个简单的C语言程序中,如果程序使用固定大小的缓冲区来接收用户输入,而攻击者输入的数据超过该缓冲区的容量,就会发生缓冲区溢出。攻击者可以构造恶意输入,覆盖函数返回地址,使程序在执行完毕后跳转到攻击者指定的恶意代码处执行。

二、缓冲区溢出攻击演示

1.漏洞情况(防御星级✪)

关闭栈保护,关闭栈随机化。

1.1 示例代码

/*testoverflow.c*/ #include <stdio.h> #include "string.h" void outputs(char *str) { char buffer[16]; strcpy(buffer,str);//str to buffer printf("%s \n",buffer); } void hacker(void) { printf("being hacked\n"); } int main(int argc,char *argv[]) { char str[30]; FILE *infile=stdin; fscanf(infile,"%s",str); outputs(str); return 0; }

 1.2 编译【计算机系统】缓冲区溢出攻击概念、演示及防御

 终端输入

gcc -g -no-pie -fno-stack-protector -z execstack testoverflow.c -o testoverflow

 进行编译,其中,

关闭栈保护:-fno-stack-protector

开启栈可执行:-z execstack

关闭栈随机化:-no-pie

关闭栈保护和开启栈可执行后,系统将不会对缓冲区溢出进行保护;关闭栈随机化后,函数所在内存地址将与反汇编结果一致(64位机器相较于32位机器,特殊的地方就是,即使关闭了对栈的保护①②,仍然没有关闭栈随机化③这一机制)。

进行反汇编查看:

【计算机系统】缓冲区溢出攻击概念、演示及防御

【计算机系统】缓冲区溢出攻击概念、演示及防御

【计算机系统】缓冲区溢出攻击概念、演示及防御 【计算机系统】缓冲区溢出攻击概念、演示及防御

可以观察到,在outputs函数中

 lea    -0x10(%rbp),%rax 负责计算局部变量buffer的地址,在栈底寄存器%rbp的低16字节处。因为%rbq本身占64位,即8字节,所以返回地址在%rbp的低24字节处。

【计算机系统】缓冲区溢出攻击概念、演示及防御

那么,想要修改函数返回地址,使其变为hacker()函数的地址,则输入的buffer字符数组的24字节之后就应该为hacker()函数的地址。通过反汇编代码可以看到,hacker()函数的地址为00000000004011d4

可以创建一个txt文件,里面存储要输入的字符数组的内容。overflow.txt内容如下:

68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 d4 11 40 00 00 00 00 00

其中,“68”均为无意义的数字(修改成其他16进制数字均可) ,只有最后的 d4 11 40 00 00 00 00 00 按照小端法存储hacker()函数的地址。

然后,输入

./hex2raw overflow.txt overflow

命令将文本文件转换为ASCII码文件hex2raw.c文件如下(需要的uu可以自取):

#include <stdio.h> #include <string.h> #include <stdlib.h> int main( int argc, const char * argv[]) { FILE *infile, *outfile; int h, i; //printf("%s, %s\n", argv[1], argv[2]); if ( strcmp (argv[1], "-n" )) { if (!(infile = fopen (argv[1], "r" )) || !(outfile = fopen (argv[2], "w+" ))){ printf ( "打开文件错误!\n" ); return 1; } while ( fscanf (infile, "%x" , &h) != EOF) fprintf (outfile, "%c" , h); } else { if (!(infile = fopen (argv[2], "r" )) || !(outfile = fopen (argv[3], "w+" ))){ printf ( "打开文件错误!\n" ); return 1; } for (i = 0; i < 5; i ++){ while ( fscanf (infile, "%x" , &h) != EOF) fprintf (outfile, "%c" , h); fprintf (outfile, "%c" , '\n' ); rewind (infile); //文件内部指针重新指向输入流开头 } } fclose (infile); fclose (outfile); return 0; }

1.3 执行

 将testoverflow文件输入重定向到overflow这个ASCII码文件中去,可以看到,程序运行完outpus()函数后进入了hacker()函数,输出 being hacked ,而我们的代码中却没有任何调用hacker()函数的操作!并且,程序提示段错误。

【计算机系统】缓冲区溢出攻击概念、演示及防御

另外,也可以直接在outputs()函数中调用hacker()的地址,然后直接执行,就不需要运行时的重定向了。(这里注意,代码片段修改后hacker()的地址也会微调!)

【计算机系统】缓冲区溢出攻击概念、演示及防御 【计算机系统】缓冲区溢出攻击概念、演示及防御

2.开启栈随机化的情况(防御星级✪✪)

关闭栈保护,开启栈随机化。

2.1 示例代码

#include <stdio.h> #include "string.h" void outputs(char *str) { char buffer[16]; strcpy(buffer,str);//str to buffer printf("%s \n",buffer); } void hacker(void) { printf("being hacked\n"); } int main(int argc,char *argv[]) { long long p_hacker=(long long)hacker;//hacker的地址 printf("hacker:%llx\n",p_hacker);//64位16进制数 char str[40]="23"; for(int i=24;i<40;i++) { str[i]=(char)p_hacker%256; p_hacker=p_hacker>>8; } outputs(str); return 0; } 

因为开启了栈随机化的情况后(实际上在64位机器上这是默认的,作为一种保护机制)hacker()的内存地址在每一次编译过程中都会实时变化,为了捕捉到它的地址,可以在运行过程中,用一个局部变量p_hacker()将其接收, 然后利用位运算,将p_hacker传递到str[]字符数组的25~40位中(返回地址仍然在%rbp的低24字节处,可以反汇编查验)。

2.2 执行

【计算机系统】缓冲区溢出攻击概念、演示及防御

2.3 另一个示例代码

在上述操作中,我们都需要利用反汇编查看%rax在%rbp的低多少位地址,比如 lea    -0x10(%rbp),%rax就是低16位。那么可不可以不用反汇编,直接修改代码就可以让程序自己计算出返回地址位于buffer的多少个字节后呢?答案是肯定的。

可以利用__builtin_frame_address(x)得到函数的栈帧,也就是得到函数的%rbp的值,即栈帧的栈底的值。

代码如下:

#include <stdio.h> #include "string.h" void outputs(char *str) { void* p=__builtin_frame_address(0); char buffer[16]; int i =0; printf( "rbp:%p\n", p); printf ( "buffer:%p\n",buffer); long long t = (long long)p -(long long) buffer; t+= 8; for(i=0; i<8; i++) //打印原始返回地址 printf("%x ",buffer[t+i]); printf("\n"); for(i=0; i<8; i++) buffer[t+i] = str[i]; for(i=0; i<8; i++) //打印修改后返回地址 printf ("%x ",buffer[t+i]); printf ("\n"); } void hacker(void) { printf ( "being hacked\n"); } int main(int argc, char *argv[]) { long long p_hacker = (long long) hacker;//程序运行时hacker()地址printf ( "hacker:%llxln", p_hacker); char str[8];//前40个字节 for(int i =0; i<8; i++) { //后8个字节填写为程序运行时hacker()地址{ str[i] = p_hacker % 256; p_hacker = p_hacker >> 8; } outputs(str); return 0; }

这样就可以直接通过buffer地址和rbp地址计算出返回地址位于buffer的多少个字节后,不需要进行objdump反汇编了。运行结果如下:

【计算机系统】缓冲区溢出攻击概念、演示及防御

3.开启栈保护和栈随机化的情况(防御星级✪✪✪)

开启栈保护,开启栈随机化。

代码依然是上面的代码

3.1 编译

 使用默认的编译方法,即

gcc -g testoverflow.c -o testoverflow

3.2 执行

可以看到,代码并没有调用hacker()函数,而是提示“已放弃(核心已转储)”。

【计算机系统】缓冲区溢出攻击概念、演示及防御

3.3 原因

通过 objdump -d testoverflow 反汇编,我们可以看到,与之前没有栈保护的汇编代码相比,outputs()函数多了个 xor 比较操作,会在函数的最后利用xor比较原来的数和现在的数,如果不相等,就会出现“已放弃”提示,如果相等,才会正常执行。相当于多了一步复查验操作。

【计算机系统】缓冲区溢出攻击概念、演示及防御

 三、对抗缓冲区溢出攻击的机制

1.栈随机化

栈随机化是一种在每次程序运行时改变栈内存布局的技术,从而增加攻击者预测和利用栈中特定位置信息的难度。具体来说,当程序开始执行时,操作系统会在栈上分配一段随机大小的空间,这个空间不用于存储任何有效的数据或代码,但会导致后续栈帧的位置在每次程序运行时都有所变化。

通过这种方式,攻击者很难确定他们插入的恶意代码或指向这段代码的指针应该放在栈的哪个位置。即使攻击者能够成功插入恶意代码,由于栈位置的随机性,他们也很难确保这段代码能够被执行。因此,栈随机化能够有效地降低缓冲区溢出攻击的成功率。

2.栈破坏检测

栈破坏检测是一种用于检测缓冲区溢出是否发生的机制。在最新的GCC版本中,通过加入一种称为“栈保护者”(stack protector)的机制来实现这一功能。具体来说,编译器会在栈帧中的局部缓冲区与栈状态之间存储一个特殊的金丝雀(canary)值,也称为哨兵值(guard value)。这个值是程序每次运行时随机产生的。

在函数返回或恢复寄存器状态之前,程序会检查这个金丝雀值是否被篡改。如果值被改变,说明有缓冲区溢出发生,程序会立即异常终止,从而防止攻击者利用这个溢出执行恶意代码。通过栈破坏检测,系统能够在攻击者利用缓冲区溢出漏洞之前发现并阻止攻击。

3.限制可执行代码区域

限制可执行代码区域是一种用于消除攻击者向系统插入可执行代码能力的机制。其基本思想是,只有那些包含编译器产生的代码的存储器区域才允许执行代码,而其他区域则只能用于存储数据。

通过限制可执行代码区域,系统能够防止攻击者利用缓冲区溢出漏洞向程序中插入恶意代码并执行。即使攻击者能够成功地将恶意代码写入某个内存区域,由于该区域不允许执行代码,这些代码也无法被执行。

详细介绍见卡耐基梅隆大学的《深入理解计算机系统》第3版3.10.4节

免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://haidsoft.com/118510.html

(0)
上一篇 2025-11-12 17:20
下一篇 2025-11-12 17:33

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注微信