文章结构
0.什么是“应用程序的安全”?
许多攻击并不直接利用操作系统内核的弱点,而是攻击不安全的程序。
这些程序甚至是非内核的操作系统程序 (如更改密码程序),它的运行权限要高于普通用户的权限。因此,要保护这些应用程序免受特权提升的攻击。
1. 编译与链接
编译:将源程序转换成处理器能执行的机器代码指令的过程。
使用静态链接和动态链接都能对程序进行编译。
静态链接:程序执行期间所需的所有共享库都要复制到磁盘上的编译程序中。从安全角度而言,这样做会更加安全,但因为重复代码会占用额外的空间,许多程序还要使用这些空间,所以会很不方便,另外,这还可能限制调试选项。
动态链接:当程序真正运行时,才会加载共享库。
既节省硬盘空间,又允许开发者将代码模块化。
当执行程序时,加载程序确定需要哪些共享库,然后在磁盘上找到这些库,并将它们导入进程的地址空间。也就是说,不需要重新编译整个应用程序。
例:DLL注入。为了修复DLL产生的漏洞,可能只需要改变一个DLL,但可能会影响许多其他程序。通过共享库向程序注入任意代码的过程称为DLL注入。对于调试,DLL注入非常有用,程序员无需重新编译代码就能轻松地改变应用程序的功能。但是这种技术也构成了潜在的安全风险,因为通过这种技术,恶意方能向合法程序注入自己的代码。
2.简单的缓冲区溢出攻击
攻击者提供的输入可能会超过缓冲区的大小,而程序仍会尽职尽责地向缓冲区复制所分配地输入。这种复制可能会覆盖内存缓存区位置外的数据,从而使攻击者获得整个进程的控制权,并在计算机上执行任意代码。
允许特权提升、针对应用程序攻击的一个典型示例就是【缓冲区溢出攻击】
在任何情况下,都会在内存中为程序分配固定大小的缓冲区,用于存储信息,必须注意,要确保安全地向缓冲区复制用户提供地数据,并进行边界检查。
例:最简单的溢出条件使对整数在内存中表示的溢出——算术溢出
32位整数:0x00000001~0x7fffffff为正数(1~2147483647)
0x80000000 ~0xffffffff为负数(-2147483648~-1)
0x7fffffff + 1 = -2147483648 0x80000000-1 = 2147483647
攻击者通过发送大量的连接请求,使连接计数器溢出,计数值变为零或负数,攻击者就可以危害上述系统。
为了防止这种类型的攻击,必须使用安全的编程实践,以确保整数不会无限制地递增或递减。
3. 基于堆栈的缓冲区溢出
堆栈是进程内存地址空间的的组成部分,它包含与函数调用相关联的数据。堆栈由帧组成,每个帧都与激活的调用相关联。帧存储着局部变量、调用参数和父进程调用的返回地址。
在缓冲区溢出攻击中,程序将攻击者提供的输入盲目地复制到比输入小地缓冲区。通常是使用了不检查地C库函数,如strcpy()和gets()。
argv[1],这个字符串不论多长,执行的时候,都会把它复制到buf这个内存中,越界之后就会把后面的内容给破坏掉,如果argv是设计好的恶意代码,可能就会产生严重后果。
在基于堆栈的缓冲区溢出攻击中,攻击者可以覆盖与局部变量内存相邻的缓冲区,会产生意外的操作。
在堆栈溢出攻击中,攻击者利用堆栈缓冲区的脆弱性,在堆栈中注入恶意代码,并覆盖当前例程返回的地址,以便当前例程终止时,将执行权限传递给攻击者的恶意代码,而不是传递给调用例程。因此,出现这种上下文切换时,代表攻击者的进程将执行恶意代码。
堆栈溢出攻击,假设攻击者知道返回地址的位置。
(a)在攻击前,返回地址指向程序代码的位置。
(b)利用未受保护的缓冲区,攻击者注入地址空间输入,该输入包括填充的返回地址位置、指向下一个内存位置的修改后的返回地址和恶意代码。在当前例程执行完成后,控制传递给恶意代码。
攻击者面临的问题:猜测缓冲区中返回地址的位置,并确定用什么地址覆盖返回地址,以便攻击者的代码得以执行。
操作系统设计的本质形成了这种挑战。究其原因共有两个:
其一,进程不能访问其他进程的地址空间,因此恶意代码必须驻留在被利用进程的地址空间内。正因为如此,恶意代码通常将自身保存在缓冲区之中,当进程启动时,将自己作为参数传递给进程,或者保存在用户的外壳环境中,然后再导入进程的地址空间。
其二,给定进程的地址空间是不可预测的,当程序在不同计算机上运行时,地址空间可能会发生变化。
所以,要确切知道在堆栈中缓冲区的主流位置也是非常困难的,必须进行猜测。
攻击者为了克服这些挑战,已开发了若干项技术。下面就介绍三种常见的方法。
(1)NOP指令滑动
NOP的核心思想就是【猜】。
通过递增目标的大小,攻击者能成功地猜测出代码在内存中地位置。
利用这项技术,攻击者制造有效负载,该负载包含使缓冲区溢出的适量数据、猜测出地进程地址空间中合理地返回地址、大量NOP指令和恶意代码。
当为脆弱的程序提供该有效负载时,程序将有效负载复制到内存,覆盖攻击者猜测的返回地址。在成功的攻击中,进程会跳转到猜测的返回地址,这会需要大量的NOP指令,然后,处理器划过所有NOP指令,直到最后到达恶意代码,并执行恶意代码。
外壳代码:在漏洞中包括的恶意代码称为外壳代码(shellcode)。
尽管NOP指令滑动使基于堆栈的缓冲区溢出攻击更容易成功,但仍需要大量的猜测,而猜测并不十分可靠。另一种称为【跳转到寄存器】或【跳转】的技术更为精确。
(2)跳转
在初始化时,大多数进程将外部库的内容加载到自己的地址空间内。因为是将这些外部库加载到内存预留段的进程地址空间之中,所以可以预测外部库的内存位置。攻击者可以利用这些外部库的知识,执行跳转攻击。
例:攻击者直到Windows核心系统DLL中特定的程序集代码指令,假设该指令告诉处理器跳转到某一地址,该地址存储在处理器的某一寄存器中,如果攻击者可以设法把寄存器指向恶意代码的地址,覆盖已知指令地址的当前函数的返回地址,然后再返回,应用程序将跳转,并执行jmp esp指令,结果是执行攻击者的恶意代码。
(3)return-to-libc攻击
Return-to-libc攻击也利用了运行时外部库的加载,是使用C库的函数,即libc的函数。
如果攻击者能在脆弱进程的地址空间内确定C库函数的地址,如system()和execv()的位置,将使用这些信息强制程序调用函数。攻击者可以使缓冲区溢出,用所需库函数的地址覆盖返回地址。libc函数完成执行时,攻击者必须提供一个新地址,libc函数将返回这个新地址,按地址指向该函数的任何参数。
当脆弱的堆栈帧返回时,它讲调用所选择的函数,该函数使用所提供的参数,攻击者就能达到完全控制系统。这项技术的优势在于:在堆栈不执行任何代码。堆栈只包含已有函数的参数,而不是实际的外壳代码。因此,甚至堆栈被标记为不可执行时,也能实施这种攻击。
防止基于堆栈的缓冲区溢出攻击
1)缓冲区溢出的根源不在于操作系统本身,而在于不安全的编程实践。
如:用strncpy()代替strcpy().
2)许多OS都采用了保护机制,用于检测是否发生了基于堆栈的而缓冲区溢出。若溢出,就控制恶意代码的重定向。
如:在缓冲区和控制数据之间设置一个cannary值,定期检查cannary值的完整性,如果值被修改,则表明已溢出,就防止恶意代码的执行。
3)Point-Guard:增加了指针的XOR编码,在指针使用前后都包含返回地址。
4)通过在内存的堆栈段设置非执行权限,以防止在堆栈上运行代码。
5)地址空间布局随机化,它随机地重新安排进程地址空间的数据,使攻击者很难预测:为了执行代码,需要跳转到哪个位置。
4. 基于堆的缓冲区溢出攻击
从安全角度来看,堆存在的问题与堆栈类似。
基于堆的溢出通常比流行的基于堆栈的缓冲区溢出更复杂。
与堆栈不同,如果改变了程序的执行,堆会包含控制数据,堆本质上是用于数据的大量空闲空间。堆溢出不是直接修改控制,而是修改堆中的数据或者滥用函数及管理堆内存的宏,从而执行任意代码。
防止基于堆的缓冲区溢出攻击
1)地址空间的随机化 能防止攻击者可靠地猜测内存地位置。
2)一些系统使堆不可执行(代码)。
3)安全编程——最重要地预防措施。
6. 竞争条件
竞争条件是指在任何情况下程序的行为都是无意的,只取决于特定事件的分时。
一个典型的实例就是C函数的access()和open()的使用。
在对access()和open()的调用之间有微小的、几乎不可察觉的时间延迟。攻击者可以利用这两个调用间的微小延迟来改变有问题的文件。
举例来说,嘉定攻击者提供A文件作为参数,它是允许访问的。但在调用access()之后,攻击者快速用其他没有权限读取的文件的符号链接替换A文件的。这样,程序会调用open()打开符号链接的文件。
不过这个替换操作肯定不是人手动来实现的。
一般来说,这种类型的脆弱性也称为【检查时间/使用时间的问题】。
这个问题的解决方案呢,也不难,主要还是要求程序员进行安全编程。
这个例子中,就尽量避免使用access(),程序在调用access()之前,使用seteuid()去掉它的特权。这样,如果用户运行的程序没有权限打开制定的文件,则调用open()会失败。
getuid :返回一个调用程序的真实用户ID。当前运行位置程序的执行者。
geteuid :返回一个有效用户的ID。euid是最初执行程序时所有的ID。