Windows主机入侵检测与防御内核技术深入解析
第五章 方案漏洞分析与利用(1)
5.1 漏洞分析的基本原则
5.1.1 尽量明确需求
在软件开发完成之后,理应进行测试和评估以证明其质量。但安全方面的测试和一般软件的测试不同。
一般的软件通常有一个功能列表,测试的目的是为了确保所有的功能均可达到需求预期的目的,这种预期一般是实在且有限的。但安全方面的需求的预期则和实在和有限截然相反,往往是“虚无且无限”的。
这里所谓的“虚无”是说其预期很难用一个实在的结果来展示。比如我预期的目的是“该软件无法被攻破”,那么我要如何才能展示某个软件无法被攻破呢?
即便我把它挂在网上邀请全世界所有的黑客来攻击,十年内无人攻破也无法证明它是无法攻破的。因为十年内无人攻破并不说明它永远不会有人攻破。更夸张地说,即便地球人无法攻陷它,也无法证明宇宙内没有外星人能攻破它。
所谓“无限”是指我们无法穷举攻击的手段。实际上,因为攻击的手段是有无限多的,安全功能的预期其实是“无穷大”的。实际上,任何不带明确范围限制的安全功能的描述都是不可靠的。如一些安全软件的广告中会描述如下一组功能:
(1)能有效防范rootkit病毒。
(2)保护用户密码不会泄漏。
(3)能具有保护用户机密文件的功能。
以上三条没有任何一条是实在且有限的描述。比如第(1)条,要证明该软件能有效防范rootkit病毒,就必须测试世界上任何可能存在的rootkit病毒。但这是不可能的。rootkit病毒如同生物界不断排列组合DNA进化的真实病毒,是无法穷举的。同样,所谓保护用户密码不会泄漏有如何证实呢?有些泄漏方式(比如严刑拷问)甚至是安全软件根本无法防御的。
因此,尝试对安全功能进行评估时,我们不得不尽力去把“虚无且无限”的需求,变成“实在且有限”的需求。如我们在第3、4章所设计的内核安全模块,如果我们的预期是“任何恶意代码都无法在用户电脑上执行。”这个描述显然是虚无且无限的。我们应该如何明确需求呢?方法不外乎如下几种:
(1)限定明确的目标对象实体
(2)限定明确的执行环境。
(3)将否定性的描述改为肯定性的。
(4)避开任何无法明确的预期。
“恶意代码”本身就是很难界定的实体。我将这个实体改为“PE文件”将会变得明确。因为PE文件的格式是清楚的。当然,因为这个限制的存在,我们的“防御能力”也大大降低了。
Windows上虽然主要的可执行文件是PE文件,但这并不是全部。而且即便Windows只有PE文件能执行,能阻止用户运行任何恶意的PE文件,也无法保证“恶意代码”就无法执行了。
比如有人打开了一个pdf文件,而pdf文件中含有的恶意代码利用pdf阅读器的漏洞成功得到执行权。这个过程用户并没有执行过任何恶意的PE文件(有善意但有漏洞的PE文件被执行),但恶意代码依然能执行(非模块的原生执行被本书归为壳代码执行或利用执行)。
用户不会关心这些细节,但我们自己一定要明确某个安全组件的明确的预期。否则项目的结果将完全无法评估。
在执行环境方面,我们可以限定操作系统的版本和执行权限等级。考虑到Windows内核环境的高权限和复杂的对抗,限定在用户态环境,但有管理员权限是个合理的选择。也就是说,我们防范的是来自有管理员权限的用户态应用程序,而不是从操作系统内核发起的攻击。
环境限定必须是用户可接受且可行的。如我限定了Windows11版本,而用户接受所有的Windows版本都使用Windows11,那么这条限定是用户接受且可行的。同理,如果将来出现Windows12,而用户需要升级,我们再投入人力升级到支持Windows12也无不可。
但有些限制看似合理但实际上用户却不可接受。比如把环境限制为“没有管理员权限的用户态环境”。实际上,非管理员权限的Windows环境在国内用得不多,尤其是进行软件开发工作得时候,非管理员权限会导致很多工作无法进行。因此这样的限定大概率会导致用户无法接受。
另外一些环境条件则是受限于技术而无法去除,比如“用户态环境”的限制。声称能防范“即便是来自内核的攻击”是非常酷的。实际上Windows内核已经沦陷的情况下,模块执行的防御没有太大意义。内核无需绕过安全系统去执行模块,本身就已经可以实现任何恶意操作了,甚至可以将我们的内核安全组件破坏掉。
同时,“恶意模块……无法执行”是一个否定性的、无法明确的预期。要证明“无法”就远比证明“能”更难。同事,要界定“恶意模块”又是难上加难。
因此我们把“界定”任务实际留给了用户或者是其他的机制,由用户的安全管理部门或者是其他的机制去界定何为恶意模块。我们提供的功能是一个“能够拦截”并提供“根据界定选择允许或禁止”的能力。这样我们就避开了一堆无法明确的预期。
所以我们的明确需求为:“Windows11用户态包括管理员权限的任何权限环境下任何PE文件的加载执行都能被拦截,并可根据用户的判断选择放过执行或阻止执行。”
要注意这样的预期是仅用于内部项目测试和漏洞分析的,和用于宣传推广的PPT、发给目标用户厂商的宣传单是两回事。用户直接看到这样的内部预期必然会拒绝接受。
但对我们的这个安全组件的测试和漏洞分析来说,这样的预期是是更明确的。只有各个安全组件的预期明确并做出各个安全组件的评估和测试,我们才可能将这些结果的拼图综合到一起,为整个系统的风险做出正确的评估。
在5.1.3节我们会发现,即便是如此明确的预期,在漏洞分析中出现的不可控因素依然是非常多的。也就是说,获得完美的结果依然是不可能的。我们将不得不做出各种妥协,并评估许多残余风险。
5.1.2 持续进行漏洞分析
当我们有了明确的预期,那么接下来的测试就是要证明能否达成我们的预期。在安全系统测试中,渗透测试是非常有价值和重要的。但一个常见的错误认识是,安全系统开发完毕之后就万事大吉,剩下的交给渗透测试来评估就可以了。
实际上,仅仅进行渗透测试是无法保证安全系统的安全性的。渗透测试的目标是“找出漏洞”,而绝非是“评估所有可能的漏洞”。只需要一个漏洞就可以让渗透测试成功,然后实际的工作流程变成这样:
渗透测试发现漏洞1->修改代码弥补漏洞1->渗透测试发现漏洞2->修改代码弥补漏洞2……如此循环。
因为漏洞无限多,所以该循环可以无休无止。而且聘请渗透测试人员非常昂贵,发现了漏洞就算成功结束。等到打上补丁后下次再进行渗透测试又不知道是猴年马月。因此每次循环的时间也是极长的。
后果就是安全项目长期处于只知道已经修补了少数漏洞,但既不知道还有究竟有多少漏洞、也不知道还需要多久才能达到可用的安全的状况。
问题的根本在渗透测试一般是黑盒攻击,渗透测试人员并不掌握代码。即便提供代码给渗透测试人员,渗透测试人员也未必像开发者一样熟悉这些代码。渗透测试人员的目标是渗透成功,而不是评估代码中所有漏洞。
事实上,安全组件的安全性应该首先由最熟悉代码的开发人员做出评估。在开发过程中,他们最清楚自己留下了什么漏洞。剩下的,他们自己无法想到的,才有理由由专门的渗透测试人员来挖掘。没有由开发者进行过漏洞分析的项目,必定会留下难以评估的风险,做渗透测试完全是浪费成本。
很多项目会在开发结束之后进行起码的漏洞分析。但只是由开发人员简单地编写一份文档就完事了。
在项目中我常常见到各种文档。比如安全模块的说明、安全模块的设计、安全模块的错误码列表等等。有意思的是,大部分文档不是永远被置之高阁无人使用,就是用的时候发现问题百出,和实际情况完全不同。所以如果我作为开发人员,专门为某个我开发的安全模块写了一份名为“漏洞分析”的doc文档,那么其下场也必然如此。
为什么文档永远无法和实际匹配?关键在于文档永远不会运行起来。和实际匹配的只有在运行着的代码和配置[1]。
文档都是各种人员为了应付差事而编写的。他们要么代码水平很好但文字水平很差,要么完全反过来文字水平很好但对代码一窍不通。文档写好整理完毕发布之后极少会再修改,即便修改也往往难以快速更新到所有的阅读者。而代码和配置的修改往往是十万火急而且频繁进行的。因此文档不可能跟得上实际运行的代码和配置。
漏洞分析也是一样。单独撰写的漏洞分析文档没有意义。也许最开始它是符合实际情况的,但数十次修改代码之后,它和实际情况已经不再有任何相似之处,除了误导人之外再没有其他的作用。
我们往往会以为安全项目代码的修改主要是修改漏洞。但实际又是恰恰相反,安全项目的不断修改主要是不断增加漏洞。
安全项目投入使用之后必定会给用户的使用带来各种困难(想象一下原本直接就能使用的项目现在需要输入用户名密码了,甚至可能还要增加验证码)。在用户的不断抱怨和投诉之下,“烦人”的功能可能会被关闭,各种白名单、特殊操作的允许都会被加入到系统中,漏洞逐步增加将会是常态。
所以漏洞分析和写代码一样,是应该随着代码的编写、修改而不断进行的。否则这种分析就毫无意义。因此我更主张将代码相关的漏洞分析用某种合理的格式嵌入在代码注释中。如果一定需要生成单独的文档,就在每次编译项目时工具从代码中提取生成。同样,如果项目有脚本进行的配置,那么配置中也必须有相关漏洞分析的说明。
有些漏洞和代码关系不大,而和设计有关。但在我看来,一个安全组件的设计,大部分可以在一个或者几个接口相关的源代码文件中体现出来(如重要接口的头文件)。设计相关的可能漏洞分析亦可在这些关键的源代码文件中进行注释说明,并最终编译时生成完整的漏洞分析文档。
一般而言,安全组件最终的运行的版本为某次编译结果+某版配置之和。因此配置的任何修改也同样要有相关漏洞的注释说明。我们可以提取给某用户的编译版本漏洞分析加上配置中提取的漏洞分析,得到该用户运行此安全组件时的真实漏洞分析情况,作为风险评估的基础之一。
否则,绝大部分日常工作环境下,根本无人也无法能全面了解当前的安全系统到底有多少漏洞,更别提具体是哪些漏洞。
5.1.3 漏洞的分而治之
任何时候,分而治之是永远的工程之道。在做漏洞分析的时候,我们首先把漏洞分成显而易见的两类:外部漏洞和内部漏洞。所谓内部漏洞,是指产品内部的设计、技术、实现导致的漏洞。
假设安全组件中的代码中有一段逻辑,判断文件路径超过N字节之后直接跳过不处理(结果为允许)。攻击者只要故意构造一个长度足够的路径就可以绕过检查,那么这是一个内部漏洞。
但同一个问题,如果发生的问题根源在外部,则不再认为是内部漏洞。因此外部漏洞是指根源在产品外部的漏洞。
比如假定(注意这是一个假定而不是事实)微软在Windows相关文档中已经明确声明,Windows文件系统中任何文件路径长度不可能超过N字节。但黑客用某种方法绕过了Windows的限制使得超过N字节的路径长度进入了微过滤驱动中,那么该问题就变成了一个外部漏洞(实际上是Windows的漏洞)。
要注意的是外部漏洞和内部漏洞是相对而言的。我们开发我们的安全组件,那么当漏洞的根源在Windows内核时,对我们而言这是外部漏洞,而对Windows而言就成了内部漏洞。
外部漏洞范围是极广的。除了我们所开发的产品自身,几乎一切东西都可能带有外部漏洞。操作系统、其他软件、机器硬件、人、公司、社会、国家都能带有漏洞。社会工程学就可以看成对“人”进行漏洞挖掘的学说。
对某个安全产品进行漏洞分析的时候,找出所有外部漏洞是绝不可能的,也不是本次分析的目标。但并不是于任何外部漏洞,我们什么都不用做。
比如针对前面的Windows对路径有最长限制的假设:如果微过滤驱动确实收到了路径长度超过最长限制的问题,我们应该如何处理?
显而易见,在异常情况下总是阻止操作进行能带来最大的安全收益。这样即便Windows真的存在这样的漏洞,也会被我们的微过滤驱动所阻止,符合“层层防御”的要求。
但这只是我们付出成本很小的一种情况。如果我们要防范Windows的某个“未来可能出现的漏洞”而付出巨大的成本,做起来就得不偿失了。
所以,对可能的外部漏洞的分析我们需要做一些事,但这必须小心地权衡性价比。对明显的、可以顺势而为解决掉的外部漏洞应该从内部堵上。但对复杂的、弥补起来很麻烦的情况,就不要去处理它。
接下来我们深入讨论内部漏洞。对内部漏洞而言,可以分成设计漏洞、技术漏洞和实现漏洞。
所谓设计漏洞,是与技术环境和平台无关,仅仅从产品、功能设计的层次上进行讨论就能发现,会产生不符合安全预期的结果的缺陷。
要注意的是讨论设计漏洞的阶段,一定要撇开所有的技术和具体的代码实现才能避免牵扯不清。一般而言,如果一个想法和操作系统、硬件平台没有绑定的关系,那么我称之为“设计”。一旦有关,就下沉为“技术”。
比如通过监控文件操作来实现模块执行的防御这是一个设计,因为无论Linux、Android还是Windows都可以通过监控文件操作来实现模块执行的防御。但用何种技术来实现对文件的监控?这在Linux、Android和Windows上是不同的。因此这与设计无关,是一个技术层面的选择。
所谓技术漏洞是指和实现(包括编码和配置)无关,也和设计无关的,仅仅和所选技术方案相关的漏洞。
比如有人用NTFS日志来监控曾经发生过的文件操作来实现前面的设计,这就是一个技术层面的选择。假定他的代码完美地实现了符合所选的技术,没有任何缺陷。但黑客通过迅速删除NTFS日志的方式绕过了这项防御。这个结果产生的根本原因,是NTFS日志本身无法实时地监控文件操作,而且是可被修改的。因此,这是一个技术漏洞,而非设计或者实现漏洞。
所谓实现漏洞是指和设计与技术无关的、因为编码、配置不当而出现的导致安全问题的缺陷。因此又可以分成编码漏洞和配置漏洞。
总而言之,我将所有漏洞分类如下:
n 外部漏洞
n 内部漏洞
u 设计漏洞
u 技术漏洞
u 实现漏洞
l 编码漏洞
l 配置漏洞
在漏洞分析中,外部漏洞的分析往往不是重点。有时在内部漏洞分析中遇到某种“外部影响的可能”会附带讨论一下。重点是内部漏洞的分析。而内部漏洞分析中,从设计漏洞到技术漏洞到实现漏洞,工作量是依次增加的。但风险反而是逐步递减的。
此外,根据5.1.2节讲述过的内容,漏洞分析必须持续进行,因此只有开始的时间点,没有“完成的时间”一说。
设计漏洞的分析应在项目最早开始,在进行技术方案选择前尽量解决所有漏洞。下层技术的选择和实现都是根据设计来实施的。一旦在设计上存在漏洞,往往意味着下层无论如何弥补都无济于事,大概率必须推翻重做,这也意味着项目彻底失败。我认为解决所有设计漏洞是可能的。
但这里要注意的是,解决漏洞并不一定意味着弥补漏洞。转移、接受都是解决漏洞带来的风险的办法之一。
技术漏洞的分析应在实现前开始,并在实现开始之前基本解决我们所知范围内所有漏洞。你会注意到我的措辞:“所知范围内”、“基本”。这是因为对任何技术本身,我们的所知永远是有限的。而且任何技术本身都不可避免将存在未知漏洞。除了极其简单特殊的情况外,实际项目中解决所有技术漏洞是不太可能的。
实现漏洞分析应该在实现中持续进行,并在版本迭代和运营中持续,尽量解决发现的漏洞并明确残余风险。但和技术漏洞一样,除了极其简单特殊的情况外,实际项目中解决所有的实现漏洞是几乎不可能的。
在后面的内容中,我将按照以上的分类,依次展示设计漏洞、技术漏洞、实现漏洞的分析过程。
但要注意的是,本书第2章、第3章、第4章首先展示的是需求和实现,其中没有进行漏洞分析。本章的漏洞分析实是在实现之后才延期进行。这有利于读者体会漏洞分析的重要性。但在实际项目中,等完成之后再发现大量的漏洞,将是项目的重大失败。
[1]这里的配置是指软件在实际运行时,由开发者默认设定或者运营管理者设定的各种选项和参数。
看雪ID:星星人
https://bbs.kanxue.com/user-home-143652.htm
# 往期推荐
2、记由长城杯初赛Time_Machine掌握父子进程并出题
球分享
球点赞
球在看
点击阅读原文查看更多