文章目录
前言
记录一下网课学习的系统安全知识,预习一下。
C语言的安全问题
C语言是较为底层的一个语言。他能提供很强的硬件控制能力(比如允许直接查看和操纵内存里的值),也能够提高很高的效率。
但相应的缺点是语言比较复杂,很多时候好几行C代码,在python
中只需要一句话就能够完成。
第二点缺点就是安全性了。较高的硬件掌控能力指的不仅仅是开发者,攻击者在攻击成功后,也会相应的获得较高的掌控权。
这里介绍一下C语言的溢出带来的安全问题。
溢出攻击的危害
C语言中,有时会出现溢出的问题,给操作系统带来很大的隐患。
最常见的例子就是gets
函数和strcpy
函数。由于这两个函数没有规定接受的字符串长度上限,在处理过长的字符串时,可能会出现缓冲区溢出的情况。
比如,在使用gets()
时,输入AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
(30个A)。这时,程序会报错segment fault
并崩溃。表面上好像没发生什么,但是在内存中,这串过长的字符在填满了程序分配给他的缓冲区后,会继续覆盖内存中后续的数据。
(注:这里画出的内存栈是由下往上的顺序,现实生活中也有些内存的填充顺序的由上往下的,不过相应的溢出攻击原理类似。)
这个错误乍看起来没有什么坏处,但如果攻击者输入的不是一串超长的A,而是一些可执行的程序,那造成的影响就无法估量了。比如运行一些bash
脚本,具体的可以自行发挥想象力。
防御手段
那么我们要如何防御C语言中的溢出攻击呢?
方法一:避免程序出bug
这个方法就非常的直白了。如果程序里没有任何可能会出现溢出错误的代码,那么程序就是安全的了。
不过现实生活中,难免会出现这样的bug,那就需要一些更加安全的手段了。
方法二:使用工具辅助查找bug
比如在每次可能出现溢出的代码后,写一段检测代码,看看缓冲区之后的内存是否异常。
比如,首先将缓冲区结束位置的下一位当作标志位,设置标志位的值为一个规定好的值,比如0
。随后,在gets()
接收输入后,检查一下那个0
是否被改变了。如果改变了就立刻终止。
这里也有一些细节问题。比如一个攻击者可能会猜测到这个标志值,从而绕过我们的检查。应对方法可以是使用随机值,随机值的要求是位数较多,来防御攻击者暴力枚举猜出这个随机值。
方法三:用对内存安全的语言
换一个不会出现这种问题的语言,不就没有这个麻烦了吗?类似的语言有Java
,Python
,C#
等。
这种方法的一种缺点是,这些语言属于较为高级的语言,因而效率没有C语言那么高。不过近些年来的优化使得这些语言的效率极大地提升。比如Javascript
原本的效率是C语言的1/10
数量级,但目前已经提升到了1/2
。
方法四:设置硬件电子栅栏
程序每次开辟一块堆内存之后,就在内存的尾部设置一个电子栅栏。这是硬件层面的操作,执行起来不是很难。这种方法的缺点是时间开销大。即使是开辟一块很小的内存,也需要花费相同的时间设置这样的一个电子栅栏。
方法五:加长指针
通常指针是32bit
大小的,包含着指针开头所在的地址。但是我们可以加长它,携带上指针末尾的地址,以及指针当前所在位置的地址。
在实际操作中,代码不做任何改变,编译器会自动为我们做额外的工作。
举个例子。在编译下面这段代码时
int* ptr;
*ptr = 10;
++ptr;
遇见++ptr
操作的时候,编译器会在更新指针当前地址后,检查指针当前地址有没有超过指针末尾地址。超过的话就进行相应的错误处理。
但是这个方法也有相应的缺点。
首先,通常的获取数据结构大小的函数会失效。这包含两方面,其一是在使用获取指针大小的函数时,会出现异常。其二,获取结构体大小时,如果结构体里包含加长指针的话,也会出现异常。
最后是在要求原子操作(atomic)的环境中,这种方法无法满足需求。因为代码中的一步++ptr
,会被翻译成包含检查是否超出边界在内的多个步骤。
因为这些缺点,这个方法并不是很常用。
方法六:影子数据结构 —— 袋子边界(baggy bounds)
名字听起来比较迷惑,实际思路很简单,就是在分配每个变量的同时,也记录下这个变量的大小。这样未来检测是否超过边界时会非常快速。
这个思想跟C语言中的静态变量很类似。比如我们想要声明一个数组,就必须给一个固定的长度值(如int a[256];
),这样系统就知道这个变量的边界在哪里。这个方法就是将这个思想用在指针上面。
这个思想实现的具体方式呢,就是把内存分成一个一个空间已知的袋子结构。具体的分配机制如下:
如果一块可用空间的大小大于所需空间的两倍,那就将这块空间一分为二。如果大小合适,就分配给它。释放空间时,释放的空间会和相邻的空间合并。
这个过程可以用下面这个例子的示意图来解释。
尽管这种方式仍然比较浪费空间,但是会让空间分配很快速。