发明内容
本发明要解决的技术问题是提供一种检测堆栈帧破坏的方法,能够在第一现场检测到堆栈帧被破坏,并能够在第一现场检测到程序即将或已经溢出。
为了解决上述问题,本发明提出了一种检测堆栈帧破坏的方法,包括以下步骤:
(1)在任务控制块中增加反向跟踪栈指针RTSP字段,在创建任务时初始化所述反向跟踪栈指针RTSP,所述反向跟踪栈指针RTSP指向存放返回地址LR的安全备份的内存区域,备份堆栈帧的生长方向由低地址到高地址;(2)在函数入桩时备份返回地址LR,在函数出桩时,判断当前堆栈帧中返回地址LR与备份的返回地址LR是否一致,如果不一致,则检测到所述堆栈帧被破坏,具体为:函数入桩时,若任务栈无溢出,直接将返回地址LR备份到反向跟踪栈指针RTSP指向的内存区域,并将反向跟踪栈指针RTSP更新为反向跟踪栈指针RTSP加上备份的返回地址LR所占空间的大小;函数出桩时,将更新的反向跟踪栈指针RTSP减去返回地址LR所占空间的大小,访问该内存区域,从中读取对应函数入桩中备份的返回地址LR,并将减去返回地址LR的反向跟踪栈指针RTSP写入到地址内存中,如果当前堆栈帧中返回地址LR与备份的返回地址LR不一致,则检测到所述堆栈帧被破坏。
进一步,上述方法还可具有以下特点:步骤(2)中,若堆栈帧中有当前堆栈帧指针PFP,函数入桩时,若堆栈无溢出,直接将返回地址LR和当前堆栈帧指针PFP备份到反向跟踪栈指针RTSP指向的内存区域,并将反向跟踪栈指针RTSP更新为反向跟踪栈指针RTSP加上备份的返回地址LR和当前堆栈帧指针PFP所占空间的大小;函数出桩时,将更新的反向跟踪栈指针RTSP减去返回地址LR和当前堆栈帧指针PFP所占空间的大小,访问该内存区域,从中读取对应函数入桩中备份的返回地址LR和当前堆栈帧指针PFP,并将减去返回地址LR和当前堆栈帧指针PFP的反向跟踪栈指针RTSP写入到地址内存中,如果备份的返回地址LR或当前堆栈帧指针PFP与当前堆栈帧中返回地址LR或当前堆栈帧指针PFP不一致,则检测到所述堆栈帧被破坏。
进一步,上述方法还可具有以下特点:步骤(2)中还包括,函数入桩时,如果当前栈指针SP-RTSP小于安全距离,则堆栈即将溢出或已经溢出,跳转到堆栈溢出异常处理;当只备份返回地址LR时,所述安全距离至少为返回地址LR占用的空间,当备份返回地址LR和当前堆栈帧指针PFP时,所述安全距离至少为返回地址LR+当前堆栈帧指针PFP占用的空间。
进一步,上述方法还可具有以下特点:当检测到堆栈帧破坏或者堆栈即将溢出或已经溢出时,进行异常处理,在异常处理中报告问题并重启系统。
进一步,上述方法还可具有以下特点:步骤(1)中,增加反向跟踪栈指针RTSP字段时,若对任务的堆栈进行检测,则借用任务控制块中的预留字段,如果所述预留字段已被占用殆尽,则借助任务变量功能来增加字段;若系统中存在单独的中断栈,则为中断栈单独增加一个反向跟踪栈指针RTSP指针变量,并初始化为指向中断栈顶端;若是对用户自定的具有栈空间的进程进行检测,直接在其控制块中增加反向跟踪栈指针RTSP字段,赋值为自定义进程的栈顶地址。
与现有技术相比,本发明方法通过判断备份的返回地址与当前堆栈帧中的返回地址是否一致,如果不一致,则检测到堆栈帧被破坏,并进行相应的异常处理。异常处理防止了程序错误的进一步蔓延,因而阻止了缓冲区溢出的恶意攻击,同时异常处理中所收集的故障第一现场信息,对于定位程序故障有极其重要的价值;本发明在检测堆栈帧破坏的同时还顺带提供了堆栈溢出的检测功能;本发明完全采用软件手段实现,在各种CPU体系结构上均可应用,无需增添额外的硬件;用汇编代码实现的函数入桩、函数出桩可以让本发明以程序执行效率开销的较小增加为代价,换取程序BUG定位的巨大便利和阻止缓冲区溢出攻击的高安全性。
具体实施方式
本发明提供一种利用编译器自动插桩,在函数入桩中备份返回地址到堆栈顶端,在函数出桩中根据备份的返回地址判断堆栈帧是否被破坏的纯软件方法,利用现有的栈空间顶端少量内存,以程序运行速率的微小下降为代价来换取程序BUG定位的及时性和更高的安全性。
以下结合附图,在IA-32体系结构、GNU C编译器、VxWorks操作系统上对本发明的具体实施例进行详细说明。包括如下步骤:
步骤110,在操作系统的任务控制块中增加一个反向跟踪栈指针(RTSP)字段,该RTSP指向存放返回地址(LR)和当前堆栈帧指针(PFP)的安全备份的内存区域;
所述任务控制块用来管理任务相关的各种资源,例如管理它的栈空间等。不同任务(或中断)的堆栈帧中,LR/PFP备份在各自的堆栈空间内,因此,借用堆栈内部靠近栈顶的内存空间,备份堆栈帧的生长方向由低地址(栈顶)到高地址(栈底),与正常的函数调用堆栈生长方向(由高地址到低地址)相对立,因此,称做“反向跟踪栈指针”。
若是对VxWorks任务的堆栈进行检测,由于不能修改内核本身的任务控制块,需要借用任务控制块中为用户预留的字段,例如VxWorks任务控制块结构WIND TCB中的spare1~spare4字段,假如为用户预留的字段已被占用殆尽后,也可以借助VxWorks的任务变量功能来增加字段;若是对用户自定的具有栈空间的进程进行检测,可以直接在其控制块中增加RTSP字段;若是系统中有单独的中断栈,需要为中断栈定义一个RTSP指针变量。
步骤120,在创建任务时初始化该RTSP,使其指向任务栈顶部;
若是初始化VxWorks任务的RTSP字段,可以通过挂接任务创建钩子的方式,在钩子函数中将任务控制块中RTSP赋值为任务的pStackLimit(栈顶)字段,并且不允许系统中调用从栈顶部分分配空间的函数taskStackAllot;若是初始化用户自定的具有栈空间进程的RTSP字段,则在初始化自定义进程的控制块时增加对RTSP的初始化,赋值为自定义进程的栈顶地址;若是初始化中断栈的RTSP指针变量,需要在中断使能前初始化中断栈的RTSP指针变量,使其指向中断栈的顶部。
步骤130,利用编译器的函数插桩选项,在函数进入后以及退出前分别插入函数入桩和函数出桩,在函数入桩时,如果当前栈指针SP-RTSP大于安全距离,则备份返回地址LR和当前堆栈帧指针PFP,更新任务控制块中的RTSP字段,否则,检测到堆栈溢出,执行步骤140;在函数出桩时,如果备份的LR和/或FPF与当前堆栈帧中LR和/或FPF一致,更新任务控制块中的RTSP字段,否则,检测到所述堆栈帧被破坏,执行步骤140;
堆栈帧中是否包含PFP由CPU架构、编译器等决定,而返回地址在函数调用时肯定会保存在堆栈帧中。因此,若堆栈帧中没有PFP,则只需要判断返回地址即可;若堆栈帧中有PFP,则PFP和返回地址均需要判断,任一个非法都可认为堆栈帧被破坏了。
函数入桩和函数出桩分别是在函数执行进入后和退出前,通过某些手段(例如通过编译器自动插桩选项、通过手动修改源程序等)插入调用的一段程序。在函数入桩中,需要编写函数入桩_cyg_profile_func_enter代码,如图3所示为函数入桩的处理流程图。使用GNU C编译的-finstrument-functions选项,通过编译的方式在函数进入后自动调用函数入桩:void_cyg_profile_func_enter(void*this_fn,void*call_site),进行堆栈溢出检测和堆栈帧备份;在函数出桩中,需要编写函数出桩_cyg_profile_func_exit代码,如附图4所示为函数出桩的处理流程图;在函数退出前自动调用函数出桩:void_cyg_profile_func_exit(void*this_fn,void*call_site),进行堆栈帧破坏检测。
如附图2所示为函数入桩/出桩示意图。这里需要注意对函数入桩、函数出桩代码的编译要剔除-finstrument-functions选项,否则引起桩函数调用的无穷递归。
步骤140,当检测到堆栈帧破坏或者堆栈溢出时,进行异常处理,在异常处理中报告问题并重启系统。
需要收集更多的出错现场信息,例如收集当前任务信息、上下文相关信息、分别从正常函数栈和反向跟踪栈中进行函数调用链回溯等等,并通过一定手段将收集的信息进行保存和上报,例如写入保留内存、写入磁盘介质、通过网络发送到其他设备等等,最后,需要保证出现问题的任务或用户进程不能继续返回运行,例如将任务挂起、将任务杀死、重启系统等等。
附图3所示为步骤130中的函数入桩的处理流程图,包括以下步骤:
步骤310,根据当前的运行环境(例如处在任务态或者用户自定义进程态或者中断态等)计算RTSP的地址,并从该地址中读取RTSP;
所述RTSP属于各具有栈空间的实体(例如任务或者中断等),在任务创建时初始化为其栈空间的顶部,各实体的RTSP初始值是不同的。
步骤320,判断SP-RTSP是否大于安全距离L,如果是,执行步骤330,否则,执行步骤360;
由于备份堆栈帧的生长方向与正常函数调用栈的生长方向相对,必须保证两个区域内存不会冲突,因此,需要保证SP与RTSP间保持一定的安全距离L,否则,可以判定堆栈即将溢出或已经溢出。
所述安全距离L定义为足够存储一个备份的堆栈帧的空间,因此,安全距离至少定义为LR+PFP占用空间的。例如,在IA-32中需要大于等于8。
步骤330,将当前堆栈帧中PFP(即EBP指向的内存中4字节内容)备份到RTSP指向的内存中;
由于在IA-32结构、采用GCC编译器编译时堆栈帧中存在PFP,因此,需要备份PFP。
步骤340,将当前堆栈帧中LR(即EBP+4指向的内存中4字节内容)备份到RTSP+4指向的内存中;
LR/PFP所占空间不限于本实施例所述的大小。LR与PFP的字节数由具体的CPU架构、编译器等因素决定。
步骤350,更新RTSP,即将RTSP+8写入到RTSP的地址内存中;
假设备份的堆栈帧长度为L字节(在IA-32架构中,L应为8,包括4字节的PFP和4字节的返回地址),则在函数入桩时,备份堆栈帧到RTSP指向内存空间中,会占用L个字节,因此,备份完后,RTSP需要加上L,用于更新原来的RTSP。
步骤360,堆栈即将溢出或已经溢出,跳转到堆栈溢出异常处理。
完成函数入桩后的堆栈内容排布如附图1所示。
附图4所示为步骤130中的函数出桩的处理流程图,包括以下步骤:
步骤410,根据当前的运行环境(例如处在任务态或者用户自定义进程态或者中断态等)计算RTSP的地址,并从该地址中读取RTSP;
在函数出桩时,备份的堆栈帧已经被读取用于比较,需要对应的将RTSP减去L,得到RTSP的地址。
步骤420,判断当前堆栈帧中PFP(即EBP指向的内存中4字节内容)与对应的备份PFP(即RTSP-8指向的内存中4字节内容)是否一致,如果是,执行步骤430,否则,执行步骤450;
当堆栈帧中不存在PFP且没有备份PFP时,不需要判断当前堆栈帧中PFP与备份PFP是否一致。
步骤430,判断当前堆栈帧中LR(即EBP+4指向的内存中4字节内容)与对应的备份LR(即RTSP-4指向的内存中4字节内容)是否一致,如果是,执行步骤440,否则,执行步骤450;
步骤440,更新RTSP,即将RTSP-L写入到RTSP的地址内存中;
步骤450,堆栈帧被破坏,跳转到堆栈帧破坏异常处理。
本发明方法在不同的CPU体系结构、不同的编译器、不同操作系统内核上的实现可能有些细节差异,但基本原理、基本思想是一致的。本发明可以检测多次嵌套调用时的堆栈帧,对于单次可以认为嵌套次数为1;对于单次调用时,也可以不设置指针。
应当理解的是,本发明的上述针对具体实施例的描述较为具体,并不能因此而理解为对本发明的专利保护范围的限制,本发明的专利保护范围应以所附权利要求为准。