本系列文章为看雪星星人为看雪安全爱好者创作的原创免费作品。
欢迎评论、交流、转载,转载请保留看雪星星人署名并勿用于商业盈利。
本人水平有限,错漏在所难免,欢迎批评指正。
理论上,项目中所有的代码都应该进行代码漏洞分析并逐一标注。但实际上只有实际会被执行的代码才需要进行分析。有些项目中的有些开发人员会引入巨大的开源库或者自己积累的库,而实际库中只有少量函数被调用,那么就没有必要浪费时间去给永远不会跑到的代码做漏洞分析。
在这里你会注意到进行漏洞分析非常合适的单位是函数。如果是C语言这类结构化编程语言的项目,那么逐个函数分析是很好的选择。如果是C++或者Java这类面向对象的编程语言,可以以类或者是类的每个成员函数为单位进行分析。
接下来需要确认的是分析的顺序。原则依然是“不会被执行的代码就不用分析”,因此分析的起点应该是毫无疑问会被执行的函数。
作为一个Windows内核驱动,自己并不会主动执行任何代码,所有的代码都是由Windows内核调用而执行的。如下的函数会直接被外界调用:
(1)入口函数DriverEntry。
(2)向系统注册的各类回调函数(如进程线程回调、对象回调、文件过滤驱动的各类回调、网络过滤驱动的各类回调等)。
(3)创建的线程的执行体函数。
(4)设备对象的分发函数(Dispatch函数)。
(5)注册的DPC、APC等各类处理函数、系统工作线程任务(WorkItem)等。
(6)可能被外界调用的导出函数。
应该首先将这些可能的“入口”函数列出,然后对它们进行依次的分析。在分析过程中,应该局限于该函数内部的代码,而不包括这个函数所调用的其他函数的代码。对于被调用的函数可暂时作为黑盒,根据黑盒可能存在的问题而标注潜在风险,并标注该风险和黑盒内部风险之间的关联。
将当前分析的函数完全分析完毕之后,再顺次展开该函数内部调用的函数,对被调用的函数进行逐个分析,如此递归直到所有的入口函数以及入口函数中直接或者间接调用的所有函数分析完毕,任务即告完成。
当然这是说事后分析的方式。实际上更好的顺序是在开发的过程中,写代码的同时就标注好潜在风险的。
另外,以函数或者类为单位进行分析的时候,要特别注意一些“隐式”的代码。比如全局变量的默认赋值。C语言中的全局变量初始化的代码并不写在任何函数中,然而这个赋值过程确实会执行。如果是使用的面向对象的语言,还会执行构造函数之类更复杂的代码。如果在分析中忽视,就可能导致潜在漏洞被漏过了。
我这里在代码注释中用[]把风险标注和正常的注释隔离开,一个具体的例子如下代码5-1所示。
代码5-1 一个简单的风险标注示例
代码5-1中的风险标注实际上分成了两个部分。首先是“风险”(见(1)处),然后是“评估”(见(2)处)。“风险”是对这个潜在风险的问题的说明。而“评估”则是开发者初步做出的评估结论。结论当然必须是“无需处理”或“暂不处理”或“等待反馈”等。如果评估结论是必须处理,那么就应该已经处理掉了。评估之后冒号后的“*”代表这处风险严重程度的评级。如果评估为“*”即为1星,表示风险暂时可以忽略不计。如果评价为“*****”即五星最高,表示应该尽快处理这个风险。
(1)处的“风险”的冒号之后标注的“绕过”为风险的名字。在分析中我将风险分为多种,比如“绕过”,是指能绕过安全机制的风险。“崩溃”是指能导致系统崩溃的风险。“外泄”是指能导致机密信息泄漏的风险。“误判”是将正常行为误判为恶意行为的风险。你也可以根据你自身项目的需要而分出多种需要关注的风险并未它们命名。
接下来是一行语言描述“中断级高于APC_LEVEL时,可以绕过”。这说明了风险存在的原因。真正的问题显然来源于(3)处的代码。这里判断了当前中断级(用函数KeGetCurrentIrql()获得),如果高于APC_LEVEL,就会用break语句跳出do循环块。实际上也使得这次执行流程得不到后面的安全策略的处理而直接执行了。
那么如果攻击者有办法让当前中断级提升到APC_LEVEL以上,即可绕过这个系统的防护。但攻击者有这样的办法吗?从微软的文档上看,这种情况是不存在的。上述代码出自本例的微过滤驱动一个操作前回调中,而操作前回调的中断级,微软文档说明如图5-1所示。
图5-1 关于操作前回调的中断级的微软文档说明
从微软文档来看,微过滤驱动中的操作前回调的中断级要么是PASSIVE_LEVEL,要么是APC_LEVEL,这二者都不高于APC_LEVEL,因此代码中的跳转条件是不存在的。
从上面的简单的例子可以看出,从用户发起操作,到安全组件截获操作,对操作做出处理是一个长链。在这个链条上任何一个节点如果判断条件存在“跳出”并返回正常执行的过程,那就存在一个潜在的风险点。
彻底解决这些风险点是不可能的。一律阻止这些情况会给系统带来未知的问题。用更多代码去解决它们又不一定有必要,因为很多情况其实并不会真实发生。
但如果完全无视它们的存在,那么风险就被彻底隐藏。黑客只要能挖掘到任何一个能成构造条件成为现实的漏洞,就足以攻破整个系统。而我们甚至无法评估在数十万行代码所组成的系统中,究竟有多少这样的潜在漏洞。
因此我们有必要在代码中做出标注,并用工具生成文档来评估现有的所有的潜在的漏洞,来评估整个系统的安全性。如果安全性不足,我们还需要将部分高危潜在漏洞按紧迫度排名来逐个修复。
在做标注时,开发者不得不去调查微软文档,否则无法结束这个风险的处理流程。当文档调查完毕,做出评估:“这种情况不符合微软文档,无需处理”这是合理的。
当然,有人会提问:“既然这种情况是不符合微软文档的,那也就根本不是一个漏洞,或者即便漏洞存在也是一个外部漏洞,为何一定要注明呢?直接不管它不就可以了吗?
是的,理论上这段代码不做任何标注也不会有问题。但这是建立在编码正确的情况下。假定上述(3)处的代码并非是“BreakIf(irql > APC_LEVEL)”,而是“BreakIf(irql >= APC_LEVEL)”,那么漏洞就可能变成了现实。
而面对这个“BreakIf”,开发者完全可以自信满满地不留下任何标注。因为在他的意识中,认为所有操作前回调中断级都是PASSIVE_LEVEL的(事实上,绝大多数测试都符合PASSIVE_LEVEL),因此他也不会去查阅微软的文档。
更危险的是,即便代码曾经是正确的,也可能后续被修改导致错误。假定原版的代码是“BreakIf(irql > APC_LEVEL)”,有人为了后面编程的方便(这是很有可能的,因为有些函数必须PASSIVE_LEVEL才能调用),改成了“BreakIf(irql >= APC_LEVEL)”,就变成了和标注不符的代码,在代码提交评审的时候就会被发现。若没有标注,没有人会意识到这个问题。
如果没有风险标注的要求,这些简单的跳转代码会淹没在无数和它们看起来一样的代码中,无论它们是否被修改,其中潜藏的风险永远也不会有人去调查,直到它们被恶意攻击者或者渗透测试人员利用。
它们被利用之后自然会被修复,但没有人会知道无数缺乏风险标注的代码中知道究竟还有多少这样的漏洞没有被修复。
因此上风险标注的作用有三个:
(1)提示出代码实现中潜在的风险。
(2)迫使开发者去查证这些风险实际发生的可能性。
(3)一旦代码发生更改,这些标注也会提示更改带来的新的威胁。
本节只写出了一个风险标注。5.3.4节将展示对一个完整的函数进行风险标注的例子。
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
最后于 2024-8-1 09:17
被星星人编辑
,原因: