对于为何要保护堆栈,请以“缓冲区溢出”,“堆栈”为关键词google一下,本文不再赘述。只要你的程序要调用函数,那么就要使用堆栈,不进行函数调用的程序已经很少了吧,难道你能忍受通篇的jmp,jne...等等手工作坊的方法吗?在linux和windows上,保护栈的方式最重要的莫过于两种, 一个是使用堆栈安全cookie;另一个是使栈不可执行。
上面提到的两种方式中,安全cookie提供了更大的保护,而不可执行栈在遇到溢出代码放到堆的时候就很难奏效了,先看看安全cookie是怎么一回事。 在传统的函数调用时,栈自下而上是:参数-->返回地址-->老的栈底指针-->局部变量-->...如果局部变量发生向下溢 出,覆盖了函数的返回地址,那么程序一点脾气也没有,乖乖听任你的摆布,但是这种错误的行为是在被调函数返回的时候发生的,如果被调函数有漏洞,那么我们希望的是在它返回的时候,也就是出了它的控制范围的时候主动地报出错误,而不是将错就错,那么就需要一种有效的方式来检测到错误的发生,于是安全 cookie就临危受命了,它实际上就是在参数-->返回地址-->老的栈底指针-->局部变量-->...中间插入了一个 cookie,使得这个结构变为了参数-->返回地址-->老的栈底指针-->cookie-->局部变量-->...这 个cookie是每个程序映像都有的一个数值,最好就是一个随机值,当函数调用的时候,编译器自动插入(编译的时候编译器知道何时进行函数调用,比如 call)一个cookie,这个cookie在程序启动的时候被初始化为随机值(如果不是随机值,我们考虑最极端的情况,比如就是1,那么攻击者就知道 把kookie的位置覆盖为1就不会导致cookie检查失败了),当程序返回的时候系统会检查堆栈的cookie和程序的cookie是否一致,如果不 一致,那么就报错。
以上的方式很不错吗?考虑一下以下的问题:如果攻击者知道程序的cookie所存放的位置,那么他就会知道cookie的值,于是他就知道应该将堆栈中 cookie位置的值覆盖成什么,即使你将cookie的存放位置设为不可读也不好,因为攻击者总能通过各种淫乱的手段达到目的,试问你是保护 cookie函数保护函数返回值?另外一个问题:当cookie检查失败,该怎么办?就算cookie检查失败,缓冲区确实溢出,但是此时操作系统内核并 不知道发生的一切(前提是只要别溢出到内核空间),于是想让系统在检查失败时就自动陷入内核是不可能的,一切必须手动进行,在用户空间进行,如果想让内核帮忙处理,就要手动进行陷入(x86种int指令),如果不需要内核处理就在用户空间了断,不管哪种方式,都要在检查失败后跳到一段代码,我们姑且把它叫做异常处理代码,那么问题来了,如果攻击者将这段异常处理代码攻击了怎么办,这不成了个怪圈了吗?是的,这是个怪圈,缓冲区溢出错误既然发生,你就谁也别 怪,错就错在你的代码写的不严密有漏洞,指望缓冲区溢出检查机制无论怎样结果都是不可信的,记住,计算机系统中只有一种可信的软件,就是操作系统内核(存在内核的前提下,当然不包括裸奔的单片机),而用户空间的缓冲区溢出又没有严重到内核必须接管的地步(它可没有缺页异常严重),既然用户空间的任何机制都 不可信,那么你还指望所谓严密的缓冲区溢出检查机制吗?
对于cookie的保护,我倒是有个想法,但是还没有实现,先说说。就是不再使用每个程序一个的cookie,而是在函数调用的时候通过函数名,参数,参 数个数等信息通过一个单向的签名算法生成一个cookie,等函数返回的时候,再通过上述信息通过同一个算法生成cookie,然后比较两个 cookie,如果相等,放行,不相等,节哀...对于异常处理的保护,根本就没有什么办法,唯一的办法就是书写安全无漏洞的代码。
下面谈谈第二种栈保护机制,就是使栈不可执行,实际上在x86上有两种实现方式,基于段的实现和基于页的实现。这种保护仅仅对于防止攻击代码在堆栈上的攻击方式有效,而对于堆溢出攻击则一筹莫展。
在linux2.4内核中,并没与在发布内核实现栈的不可执行,栈的不可执行是通过补丁实现的,我们来看一个简单的实现,它是基于段的:linux使用平 坦的段,就是不管代码段还是数据段都使用0作为起始地址,4G作为界限,这样的话,堆栈段其实是数据,数据段和代码段都是可以执行的,这个不可执行栈的补丁就是将代码段缩减,如何缩减呢?在x86机器上,栈是向下伸展的,对于linux,栈低就是0XC0000000,然后向低地址伸展,而且linux的 栈可以在运行中按需通过缺页中断动态增加大小,如果限制死linux堆栈的大小,比如256M(够大了),那么就可以把代码段缩减到以0为起始地址,界限为2.9G多,那么一旦执行栈里的数据,便会产生通用保护异常,这种方式具体就是重设USER_CS的值,但是2.4的内核的信号处理机制中,在信号处理 函数返回的时候需要调用sigreturn,2.4内核的setup_frame就是在栈上放置int 0x80执行码来进入sys_sigreturn的,而栈在补丁的作用下已经不可执行,那么信号返回的时候就会有通用保护异常发生,于是为了信号处理可以正常进行,通用保护异常处理中必须过滤信号处理的栈执行,这种方式相当笨拙,起码我是这样认为的,段机制既然已经是一个阑尾,为何还要在那上面大做文章 呢?于是2.6内核的基于页的栈不可执行保护给人展示了一种更好的方式。
进程地址空间是由vma组成的,而栈也是地址空间的一部分,实际上它就是一个vma,这个vma可以按需通过缺页异常动态增长,如果将这个vma的标志设 置为不可执行(setup_arg_pages),那么这个标志在缺页后分配页面的时候将直接体现在页表项的可执行位上(do_page_fault) ,而任何数据(包括代码和数据)的访问最终都将通过页表项进行,于是我们只需要在页表项上这么一卡,栈自然就不可执行了,这样,通过vma的标志就搞定了 这一切,通用保护异常处理也就不需要过滤任何情况了。
本文转自 dog250 51CTO博客,原文链接:http://blog.51cto.com/dog250/1273465