要对一个设计进行漏洞分析,首先必须用完整的逻辑描述出整个设计。很明显,这是在设计阶段就应该开始做的事。本书将这部分内容放在这里,仅仅是由于行文的顺序,而不意味着项目真正实施的顺序。
设计来源于需求。在5.1.1节中,我们明确了需求的预期。这里再重复如下:
“Windows11用户态包括管理员权限的任何权限环境下任何PE文件的加载执行都能被拦截,并可根据用户的判断选择放过执行或阻止执行。”
一个完整的软件产品设计往往由一组功能点的设计组成。本书的执行模块防御模块的设计出现在本书第二章的2.3.1节中,整个系统的设计由如下的5条规则组成。其中每个规则可以看出一个功能点。
规则2.1:在安全系统初次安装之后,系统中的可疑库为空,不存在任何可疑路径。
规则2.2:新的可执行文件创建时,将其路径加入可疑库中。
规则2.3:原有的可执行文件的内容被覆盖或被修改时,若修改后的内容是一个可执行文件,将其路径加入可疑库中。
规则2.4:当某可疑文件被改名,那么对应可疑库中的可疑路径应该随之更新。但非可疑文件被改名并不会有任何影响。
规则2.5:当某可疑文件被执行时,对文件内容计算散列值,如果散列值在散列值白库中,则执行并从可疑库中删除,否则禁止执行并将该散列值上报到后台。有必要的话,同时上报完整样本。
当然以上只是本书举出的小规模的范例。真正的产品设计规模会远比这个大,可能需要很多篇幅才能列出。但无论如何,完整的需求和设计都必须明确地列出来。需求和设计明确了之后才有可能进行漏洞分析。那么如何进行漏洞分析呢?
首先要明确的是,造成设计漏洞的原因永远都在于用户需求和技术实现之间的巨大鸿沟。
一般而言产品由产品经理设计。产品经理会比较了解用户需求,然而对技术不甚清楚。改为由精通技术的开发人员来设计也会面临类似的问题:开发人员精通技术然而不懂甚至不关心用户的需求。
用户需求和技术一样,都是五花八门博大精深的。试图去寻找一个“全能”的人材来同时兼顾需求和技术是不可能的。因此,在设计的漏洞分析中,最关键的是弥合针对需求的设计与技术实现之间的距离。
因此,对每个功能点进行漏洞分析的步骤为:
(1)与开发人员沟通,该功能点在技术上是否可行。如果不可行,则将功能点修改直至可行为止。
(2)对于技术可行的功能点,判断其逻辑上是否完备符合需求。如果不符合则存在漏洞。对于漏洞,要么修改逻辑直至漏洞消失,要么接受漏洞的存在,并将风险转移到产品之外或者接受风险。
(3)如果(2)中为了解决漏洞已经修改了功能点,那么必须回到(1),循环至(2)的结果为不再修改为止。
举例来说,功能点规则2.1表示,在安全系统安装之初,可疑库为空,也就是说任何文件都不是可疑文件。这在技术实现上没什么问题,但逻辑上显而易见存在一个漏洞:如果在安全系统安装之前,恶意文件已经存在了怎么办?
对于这个漏洞,我们选择接受。因为在安全系统安装之前,可以使用全盘扫描,或是标准化的环境安装(全面格式化并统一安装全公司标准化的初始环境)来解决这个问题。那么漏洞即便还存在,也属于全盘扫描或者是标准化安装流程了,和主机防御不再有任何关系。因此,风险被成功转移。
再看功能点规则2.2:“新的可执行文件创建时,将其路径加入可疑库中。”这其实潜在意味着,系统中只要有新的可执行文件创建,就必须被安全系统捕获到。这是一个技术问题,必须与开发人员进行沟通。
最初开发人员断然拒绝了这一要求。他们认为在一个系统中捕获所有的可执行文件的创建是不可能的。因为在系统中创建文件有太多的方法,不可能被完全捕获。
但是当他们注意到需求规定的环境:“Windows11用户态包括管理员权限的任何权限环境下”,这已经明确了是用户态程序发起的文件创建操作。考虑到用户态创建文件必须通过Windows内核,利用内核驱动来完全捕获所有的用户态的文件创建是可能的,开发人员最终接受了这一设计。
当然,上述论断的前提是Windows内核没有相关漏洞,导致用户态程序能在创建文件时能绕过文件系统的过滤而创建出一个文件。这个前提大概率是不正确的。但对于无法从内部弥补的外部漏洞,我们选择不承担责任。
规则2.3、2.4和2.5与规则2.2是类似的,其逻辑不存在什么问题,关键是技术能否实现。当开发人员确认可以实现,那么这个设计的功能点就通过了漏洞分析,可以继续下一步操作了。
当每个功能点分析完毕,接下来我们必须综合所有功能点,来看总体而言它们是否满足了需求。
实际的安全项目中常见的问题是,各个功能点已是技术可行且逻辑完备的,它们综合起来似乎也符合需求,但最终使用才发现实际效果和需求并不真正吻合。
原因在于设计者认为设计已符合需求,往往是建立在一系列看似显而易见的前提上(正如论文中常见的“显而易见”“众所周知”等)。然而这些假设没有经过专业技术人员的认真审视,又或者刚好超出了所有评审人员的所知范围,导致留下巨大的漏洞。
在审视设计的时候,一定要将所有这些显而易见的前提列出,逐个审核这些前提,再综合考虑最终的结果。因此设计的综合漏洞分析步骤如下:
(1)如前文完成每个功能点的漏洞分析。
(2)综合所有功能点的效果,查看效果与最终需求是否等同。如果不等同,要么接受漏洞,要么修改功能点并回到(1)。如果接受漏洞后等同了,必须找出它们“等同”所必须的所有前提。
(3)与技术人员充分沟通,并集思广益地评审(2)中发现的所有前提的技术可行性。如果存在问题,往往能发现漏洞。要么接受漏洞,要么修改功能点并回到(1)。
最终的效果是所有的漏洞都已明确解决(修复或接受了),所有前提都是技术可行的。除了已接受的风险外,功能点综合效果和最终需求等同。
请注意我用了“等同”,而不是“吻合”或者“符合”,原因就是后两者很容易让人掉以轻心。当我们认为设计A的效果“符合”需求B的时,其中往往暗含着大量的坑。如果我们的追求是设计A的效果“等同”需求B,结局会稍微好上一点点。
回到本书的例子。规则2.1至规则2.5总体而言,其意义是对原有的文件置之不理,而对任何新建的PE文件、被修改过的PE文件均根据路径标记可疑,然后再在执行时对可疑文件进行检查。而需求则是去除接受的原有文件的风险之外,是“除了机器上原有的文件之外,任何PE文件的加载执行都能被拦截,并可根据用户的判断选择放过执行或阻止执行”。
这设计效果符合需求吗?乍一看非常符合。新建的、新被修改的PE文件,不正符合“除了机器上原有文件之外任何PE文件”的需求吗?
所以我才强调我们的追求一定必须是效果“等同”需求。“新建的、被修改的PE文件”等同“除了机器上原有的文件之外任何PE”文件吗?至少字面上它们是不等同的。要等同就暗含了一个前提:
“机器上除了原有文件之外,如果出现任何新的PE文件,要么是创建出来的,要么是修改了原有文件形成的。”
这在一般人看来再合理不过了。一个机器上除了原来就存在过的文件之外,那不就是新建的、或者老文件修改内容变成的新文件吗?这有什么可疑?
但我们也可以反问一下:“不新建文件、也不修改任何文件,能不能让机器上出现一个前所未有的新文件?”
实际上,安全系统的漏洞分析的过程,就是不断找出安全系统的真实限制条件,并反问在这些限制条件下,能否实现绕过安全系统的阻碍的过程。
有些情况技术人员也可能一时想不到,但对另一些人(不一定需要是技术人员)来说,这可能是常识。因此在评审这些看似显而易见的前提时一定要集思广益,充分沟通。
文件并不一定要通过“新建”才能出现。磁盘的挂载也会导致大量新文件的出现。装满了PE文件的U盘插到电脑上,电脑上并未新建任何文件,也没有任何老文件被修改,然而大量的新的PE文件就这样凭空出现了。
这变成了一个巨大的漏洞,而且可以让整个设计的主机防御系统完全失去作用。因为用户运行U盘或者移动硬盘中的带毒程序是引入威胁的常见情形。而我们的系统不要说抵挡渗透测试了,就是最常见的用户行为都没有防范住。
以上只是漏洞分析的简单示例。完整的漏洞分析必须遵循步骤,列出所有的假定前提,逐条与开发人员进行充分的讨论,以便挖掘出所有的漏洞。
技术方案的漏洞由所选择的技术方案带来。因此,仅仅有产品设计而没有选定技术方案时时无法进行漏洞分析的。选择了技术方案那么技术漏洞也随之确定,可以开始分析了。
技术方案存在如下几个要素:
1、该方案的执行环境。技术方案的选择首先基于需求中所限定的执行环境。在5.5.1节中,本例的执行环境已经限于Windows11,因此技术方案只能选在在Windows上能运行的方案。
2、该方案在已经确定的环境平台上所使用的具体技术。但即便已经限定了操作系统平安,技术方案依然有很多选项。本书的2.3.2节已经选定微过滤驱动作为本方案的具体技术。
3、在该平台上使用该技术的实现方法。但这个实现方法只是一个相关专业技术人员设想的、理论可行的实现,而不是具体的代码。在设想的实现中,代码总是完美无缺的,
具体到本书的例子,使用微过滤驱动实现的技术方案已经在第3、4章中具体完成了编码,但其设想实现可以概述如下:
(1)通过微过滤驱动注册文件过滤回调。
(2)通过写操作过滤获得文件修改事件,对新生成和被修改的PE文件加入可疑库。
(3)通过设置请求过滤发现文件改名,同步修改可疑库中的文件名。
(4)通过IRP_MJ_ACQUIRE_FOR_SECTION_SYNCHRONIZATION请求过滤获得模块加载事件,获取模块全路径和可疑库中的路径比较来发现可疑模块的加载,并进行允许和禁止。
以上只是该设想实现的一个简单描述。在真正的项目设计中,对技术实现的设计会更详细。但关键在这个设想中,我们不考虑任何代码实现带来的漏洞,而只考虑这个设想本身是否存在漏洞。
这个设想中的(1)-(4)都可以看成一个一个的技术点。对技术漏洞的分析其实就是对这些技术点的分析。
这是一个几乎专门考验知识与经验的工作,仅有长期从事该技术的专家才能完成。而且很多情况之下,任何专家的经验与眼界甚至思路也是不够的。尽可能阻止更多的相关从业者进行讨论,集思广益可能会有更好的结果。但无论如何,结果不会是完美的。
举例上面的(1)这个点。对这个技术点进行漏洞分析,等同于反问:“在Windows11上,能否存在从用户态发起的文件操作,能绕过微过滤驱动的过滤回调并操作成功?”
这个问题看似简单,回答就是简单的“能”或者“不能”。理论上无论事实如何,我们只要看微软的文档就行了。如果微软声称“不能”,那么我们就认为不能。但实际上,微软并没有这方面的声称。
微软在微过滤驱动的文档中详细介绍了微过滤驱动的实现原理、使用方法,但从未承诺没有任何方法能从用户态发起一个文件操作绕过微过滤驱动的过滤。
事实上要绕过微过滤驱动的方法有很多。有些读者可能会想到使用内核驱动来绕过过滤。但本书的例子是在首先阻止任何恶意模块的加载的情况下,这也包括了阻止内核模块的加载。但即便如此,我们也很难确保Windows已有的众多内核模块中,其中是否有一些带有漏洞的接口暴露给用户态,而用户态利用这些接口可实现创建或修改文件,并绕过微过滤驱动的情况。
所以这个问题的关键不在究竟“能”还是“不能”,而在于用微过滤驱动来实现这个功能是否是最优解。
对有经验的专家来说,他可能知道一些已知外部漏洞,那么他可以在做这个项目的同时附加列出这些漏洞,以便开发过程中弥补。另外,他根据他的知识和经验,判断使用微过滤驱动确实是最优解,因为根本没有其他更好的选择。那么这个技术点的漏洞分析就宣告完成。
上面设想中的(2)可以同样展开反问:“是否存在修改了文件,但微过滤驱动的写操作过滤拦截不到,导致恶意模块被执行的可能?”
具体到微过滤驱动写操作拦截的实现,只有真正写过相关代码的开发人员、或者干脆就是设计微过滤架构的微软的内核工程师才能做出正确的分析。
回顾本书的3.1.2节,你会发现微过滤驱动对写操作的拦截往往并不是完全的。注意代码3-1中的1处,分页读写操作的请求被跳过,只有普通写操作的请求才被处理。那么问题来了,能否仅仅通过分页读写操作请求修改一个文件呢?
答案是完全可以!这就等于找到了一个漏洞。任何情况下如果我们确认找到了漏洞,就应该写出该漏洞的利用(PoC)。原因是既然发现了漏洞就应该弥补漏洞。如果没有利用原型,我们就无法验证漏洞已经被弥补了,甚至无法验证漏洞是否真实存在。
部分开发人员只会使用fopen、fwrite或者是CreateFile、WriteFile来进行文件的创建和修改。在这种情况下,无论如何编码都是无法写出正确的利用原型,甚至无法察觉这个漏洞的存在。
实际上正确的方式是使用CreateFileMapping来实现文件的内存映射。在内存映射中操作文件的内容时并不会有任何文件请求发生,因此也不可能被微过滤驱动过滤到,而此时文件内容已经被修改了。那么这个被修改的文件能否直接(避免所有文件操作)执行?
如果可以,我们会惊觉原来文件内容的修改可以这样绕过微过滤驱动的过滤,那岂不是成了一个无解的问题?好在咨询了某资深专家的意见之后,该专家声称:
“由于Windows在加载任何模块之前会强制将内存映像刷入磁盘,因此被内存映射修改的模块执行之前一定会有分页写操作发生,因此会被微过滤驱动拦截到。”
这时我们才松了一口气,这避免了整个项目的彻底失败。虽然发现了漏洞,但显然该漏洞还是可以弥补的,只要过滤分页操作就可以了。那么至少微过滤驱动依然基本满足我们的需求。
通过这个例子我们可以看到,技术漏洞的分析强烈依赖于分析者在相关技术方面的知识和经验。这方面的经验往往不是简单地通过查阅文档、临时的学习、逻辑推理就能获得的。对相关技术的经验不足是技术上留下漏洞的最主要的原因。
这个世界上并没有全知的人类存在,因此技术上留下漏洞是很难避免的。正确的方法是找到真正的有经验的人群,并将技术设想进行广泛、充分的思考和讨论,而不是在封闭的小圈子内自信满满地完成。
同样对(3)、(4)进行漏洞分析,也可能会发现其他的技术漏洞。我在这里不做进一步的展开。但是在5.3节的漏洞利用与测试中,会给出更多的例子。
在所有的漏洞分析中,实现漏洞的分析将会占据最大的工作量,但也是最常被忽视的。原因是不阅读代码就无法去分析代码实现所带来的漏洞。而代码是最没有人愿意去读的。即便是项目负责人要求相关人员去阅读代码,这个工作又如何验收呢?我完全可以摸鱼一下午然后说阅读完毕。至于漏洞没有找出,那不是我摸鱼的后果,而是凡人难以避免的疏忽所致。
让开发者给出相关实现所可能带来的漏洞的说明,并留下文档这也是毫无意义的。因为此类文档无法验收质量。
想要检验一份漏洞说明文档是否可靠,唯一的办法是和原始代码一一进行对比。但做这个对比的工作量和重新分析漏洞差不多。更何况这种对比注定会失败。因为代码会不断修改更新,而相关的漏洞文档不会。没有人会在每次代码修改之后去找到原来的漏洞分析文档,然后检查其中是否应该做相应更新。这根本就是不可能完成的操作。
合理的办法是在写代码的同时,就在代码中留下这些代码可能带来的漏洞的标注。
提交新代码的时候,标注在代码注释中一起提交。如果代码被修改,相关标注也必须随之修改。检查可以在日常代码评审时进行。代码评审本身就要审视可能潜在存在的漏洞,对比和代码一同提交的标注就可以明确这些标注的质量如何。如果任何代码修改后没有相关标注或者标注质量明显有问题,评审不予通过即可。
这当然不能确保所有漏洞都被精准地标注出来,但无论如何,这和修改代码之后去翻阅文档更新相应部分的工作量比起来是天壤之别。
如果需要集合了所有可能的实现漏洞的说明文档,使用自动化的文本处理脚本从代码中提取即可。
本书第3章、第4章中所列出的代码并没有标注漏洞的存在。因此这些就成为了现成的例子。我将在本节中演示如何为部分代码进行漏洞分析,并标注漏洞。
要注意的是,我们要标注的并非是“缓冲溢出”之类的漏洞——此类漏洞并不是说不要去找,而是说不用“标注”。如果发现缓冲溢出,毫无疑问我们要做的是立刻修复,而不是将它标注起来。
我们在代码漏洞分析中,需要标注的是那些怀疑有风险残留,但不一定确定存在、也不一定要去修复的潜在的漏洞。因此在标注中我并不直接将它们注明为“漏洞”,而是“风险”,因为它们大多是暂时无法证实,或者暂时无需处理的潜在问题。
此类标注可以使用任何格式,只要适合脚本自动化识别即可。本书举出的例子不一定是最好用的,你应该自己选择合适自己项目的方式。
看雪ID:星星人
https://bbs.kanxue.com/user-home-143652.htm
# 往期推荐
点击阅读原文查看更多