首页
社区
课程
招聘
[原创]Windows主机入侵检测与防御内核技术深入解析(2)
2024-4-17 09:01 2207

[原创]Windows主机入侵检测与防御内核技术深入解析(2)

2024-4-17 09:01
2207

本系列文章为看雪星星人为看雪安全爱好者创作的原创免费作品。

欢迎评论、交流、转载,转载请保留看雪星星人署名并勿用于商业盈利。

本人水平有限,错漏在所难免,欢迎批评指正。

2 模块防御的设计思想  

2.1 执行与模块执行

         本章内容为介绍模块执行防御。在此我将先介绍“执行”分类,以及“模块执行”在“执行”中的位置和重要性。

2.1.1 初次执行

         恶意代码(或者行为)要在被攻击的机器上执行起来,看起来似乎一定要让被攻击的处理器执行一些指令。但这种想法并不正确。

         在《渗透测试高手:打造固若金汤的安全网络》(有意思的是这本书的英文书名意义和中文翻译版完全相反,意义为破解最坚固的网络)一书中,作者Wil Allsopp举出了一些例子,比如一个如同转接头一样的东西。他趁别人去吃午饭的时候爬到别人办公桌下面,把键盘线拔了,插上他的“转接头”,然后再把原本直接插在电脑上的键盘线插在转接头上。

         这样对方所有的按键都被这个“转接头”记录。而且转接头内置手机卡和具有手机功能的芯片,能源源不断将记录的按键通过移动网络发出,信息随之泄漏。被攻击主机上没有执行恶意任何代码,而且这种硬件很长时间也不会有人发现。

         我举这个例子仅为说明情况并不总是如我们想象的那么简单。在我们构筑安全的防线的时候,千万不要有“我这种技术能百分之百防住所有攻击”的想法。在对可能的攻击形式进行分类的时候要尤为小心。就像我打算将恶意代码的执行方式进行分类,也一定会有我未能想到的地方。

         现在假定排除所有的恶意硬件以及不需要执行任何恶意代码的社会工程学攻击,仅仅考虑有恶意代码在主机上运行的形式。那么无论如何,恶意代码从并未执行到开始一系列的操作,一定存在一次“初始执行”的过程。所谓的“初始执行”是指恶意代码在被攻击机器上的第一次执行

         恶意代码的初始执行有很多种可能。在我们的现实生活中可以分成如下几种途径:

         1)用户操作直接运行了来自攻击者的恶意的、或者被恶意代码感染的(后续统一称为恶意的)可执行模块。

         2)用户操作同(1),但用户点击的是某种可解释执行的命令或脚本,如.bat,或者Linuxshell脚本,被浏览器解释执行的JavaScript等。

         3)用户操作打开了来自攻击者的某种非可执行模块的东西(如网页、pdfdoc文档、图片等),导致了主机上某个软件(如浏览器)启动,然后发生了恶意代码的执行或者是合法的程序被利用执行了恶意的操作。

         4)用户什么也没有做。但是主机受到来自网络的攻击,系统或某软件自身漏洞或设计缺陷或策略上的漏洞导致了同(3)的后果。

2.1.2 原生执行与解释执行

         不管是哪种初始途径,我们总是可以把所有的恶意代码的执行归结为两种:

         1)原生执行。

         2)解释执行。

         所谓原生执行是指被攻击的电脑的处理器支持的指令直接在被攻击的处理器上运行。Biru 现在的内网电脑绝大部分使用x64的处理器,那么我们可以认为恶意代码本身就是x64指令的情况就称为恶意代码的原生执行。原生执行必须拥有如下两个要素之一:

         1)这些原生指令不属于任何合法的软件,是出于任何合法的软件设计之外的目的编写的。用户直接下载一个恶意的可执行模块就是这种情况。

         2)这些原生指令虽然属于合法的软件,但其执行过程不符合该软件的任何设计。比如一个程序因为缓冲溢出而返回到了错误的地址,然后执行了一串本不该被执行的指令。

         如果某服务程序收到了来自远程的命令然后执行了恶意的操作(比如RPC的方式),我并不认为这是一种原生执行。因为该操作虽然恶意,但是服务程序本身就具备提供该操作的功能,因此是符合设计的。

         符合设计并不等于没有恶意。此类恶意执行我将归于上述的(2)解释执行。因为它实际上是解释执行了恶意的命令。解释执行将会放在后面讨论,在这里我们只讨论符合上面提到的二个要素之一的原生执行。

         原生执行本身的形式是多种多样的。其中最为“规范”的方式是模块执行

         所谓模块是操作系统所规定的一个可执行单元。它保存在硬盘上的时候表现为一个可执行文件(如Windows上的.exe.dll或者.sys文件,Linux上的.so文件),加载进入内存之后则成为一个可执行映像。

         模块并不一定要经过文件-加载-映像-执行的过程。不生成任何文件,直接把模块写入内存生成映像是完全可以做到的。很多恶意代码用这个方式避免扫描硬盘文件方式的检测。

         除了模块执行之外,壳代码(shellcode)执行也是原生指令执行方式的一种。

         这个地方很容易混淆。所谓壳代码中的“壳”(shell)本意是指Linux中的控制台。因为最早隐藏的这些恶意代码目的是为了能偷偷连接一个控制台来控制这台机器。通过远程控制台来恶意地控制一台机器,这在本书的分类中属于解释执行而非原生执行。因为恶意的是命令而不是原生指令。

         但现在这个词语的意义已经发生了很大的转变。目前国内安全行业中所谓“壳代码”特指并非以规范的完整模块形式存在,而是嵌入在模块中、或者是游离于所有模块外的代码。壳代码并非一定是恶意的,但有很多恶意代码是壳代码的形式。

         加壳”也成为了国内安全行业的一个特有名词。所谓的“加壳”的意思是“在原本干净的模块中加入壳代码”。这个操作并不一定是恶意的。很多对模块进行安全加固的操作,就是以壳代码的形式加入安全组件,使得模块更难被破解。

         无论是单纯的模块,还是不属于任何模块的壳代码,它们都需要生成新的指令。但实际上还存在一种不需要生成任何指令就能原生执行恶意代码的形式,那就是不写入任何恶意指令,但利用主机上原有指令进行执行。

         内存中已经存在的合法模块中已经有很多的指令。如果用一种巧妙的方法,在不改写这些指令的情况下构造一个新的指令路径来执行某些指令,就能实现想要的完成的恶意操作。有兴趣的读者可以自己了解一下ROP执行。

         此外在利用漏洞进行壳代码执行之前,也常会利用已有模块中的指令作为跳板来跳转到壳代码。本书中我可以把此类执行统称为“利用执行”,意思是利用原有合法执行实现新的操作的原生执行。

         因此,原生执行可以分为模块执行、壳代码执行、利用执行这三种。如果读者认为还有更多的形式,欢迎来信补充。

         解释执行是和原生执行相对的,并非直接执行处理器所执行的原生指令,而是由合法的服务器或解释器(比如RPC服务、Python.exeWindows PowerShell、浏览器、OfficeJava虚拟机)去执行命令、解释脚本或者中间码来实现执行的。其执行过程本身是符合设计的,但执行的命令或脚本中含有恶意的内容。这一部分将会在本书后面的章节中再详细分析。本章不会涉及。

         总而言之,本书对恶意代码的执行进行了如下的分类:

n   原生执行

u  模块执行

u  壳代码执行

u  利用执行

n   解释执行

         本书的第一部分将重点解决模块执行的防御问题。

2.1.3 模块执行

         在各种执行方式的分类中,如果从攻击者的角度出发,我们会发现,壳代码执行和利用执行的能力虽然巧妙而且隐蔽,但在使用上是极为不便和受限的。而可执行模块和脚本则几乎具有“无限”的能力。

         比如壳代码,它没有模块结构,因此也不存在导入表,如果需要调用系统中其他模块的函数则相当麻烦。这种麻烦并非不可以克服,但很少有人那么去做,因为没有必要。既然壳代码已经执行起来,那从下载一个可执行模块或者恶意脚本来运行很难吗?

         我见过很多恶意程序,但完全由壳代码构成、无任何模块组成和脚本成分的恶意程序少之又少。一般攻击者都会将壳代码执行和利用执行当作下载并执行真正的恶意模块或者恶意脚本的跳板。

         总而言之,模块执行非常重要。监控住所有的模块执行,或者拦截住非法模块的执行,对内网安全有巨大的意义,是基于主机的入侵检测和防御系统的重要组成部分。

         在我个人看来,成功的模块执行防御(完美的成功是不存在的)的技术难度最小,但足以在内网安全中顶小半边天,同时另外小半边天则是范围极广、难度极大的解释执行的防御。

2.2 模块的公开检验措施

         在当前现实的世界中,对模块的执行已经存在诸如签名校验、特征扫描等措施。因为这些措施的共同特点都是公开的,所以我将它们称之为“公开检验”措施。本节将会详述这些公开检验措施的效果和局限性。

2.2.1 Windows的可执行文件格式

         在模块被加载进入系统之前,一般的载体为可执行文件,在Windows上为PE文件(Portable Executable,这是一种文件格式规范,意为可移植的可执行文件)。Windows中常见扩展名为exedllsys的文件就是PE文件。

         理论上初始执行时如果要在用户态环境下加载一个模块到Windows中,模块必须首先作为磁盘文件的形式而存在。

         有些恶意代码在执行之后模块文件会消失,但那只不过是自己删除自己而已,并非从未作为文件存在过。有些恶意代码可以直接把模块写入内存而绕过文件加载,但既然恶意代码已经能执行,就不再是初始执行了。

         有些带有漏洞的驱动程序提供了接口,调用这些接口可以绕过文件形式,直接往内核中写入模块映像并执行。但这也需要执行调用这些接口的恶意代码先被执行,并非初始执行。

         是否存在不需要通过文件的形式存在,就获得初始执行的可执行模块呢?理论上可以为Windows或者某个应用软件设计一个非常离谱的后门或漏洞来做到这一点。

         但这并不关键。若是真存在通过漏洞直接执行,而不通过Windows的文件加载方式的模块,我们可以无视它的模块结构,将它视同壳代码处理。因为它不是通过Windows加载,而是通过壳的跳转方式开始执行的。

         本书后面的章节会专门讨论壳代码执行的防御。本章中,我们假定任何模块执行的初始执行,恶意模块都必须首先以文件的形式存在

         PE文件的文件名并无明确规定,因此一般认为“.bmp”一定不是可执行文件而“.exe”一定是可执行文件的想法是不对的。

         一个随意指定扩展名的PE文件,使用Windows的“LoadLibrary”之类的接口去加载也可以成功。但是PE文件格式的规范不可违反。比如PE文件开头前两个字节必为‘MZ’,倘若不正确,Windows就会拒绝加载这个文件。

         任何时候我们可以把系统中所有存在的文件分为两种:

         1)正确格式的PE文件,以Windows可以成功加载并运行它为标准可执行模块。

         2)非PE文件。Windows无法正常加载运行它(解释方式执行除外)。

         这时候我们就可以有一个想法:是否如果任何时候我们都确保主机系统中所有的可执行文件都不是恶意的,那么就可以确保主机系统的安全?

         这其实就是本书1.1.1节中山羊胡子同事的想法。在不考虑恶意硬件、纯社工攻击、壳代码执行、利用执行、解释执行等一大堆特殊情况的基础上,这个说法是正确的。那么如何确保这些文件都不是恶意的呢?

         只要确保两点就可以“认为”这些文件不是恶意的:

         1)这些文件是被信任的,即文件的可信任性。

         2)这些文件没有被修改过,即文件的完整性。

         下面我们将专门探讨用来解决这两个问题的公开检验措施。

2.2.2 可执行模块的签名

         在我们日常的、对计算机安全的想法中,最常见的一个错觉,就是认为可执行文件的签名能有效确保可执行文件是可信而且完整的。

         如今大部分Windows上的可执行模块都有签名,包括Windows系统自带的可执行模块和安装其他软件而带入的可执行模块。Windows上对任何一个可执行文件用鼠标右键点击,弹出菜单中选择“属性”、“数字签名”则可以看到该文件的数字签名。

         Windows上可执行文件的数字签名如图2-1所示。

 

                                               

2-1 Windows上可执行模块的数字签名

        

         有些模块没有签名,那么熟悉中也就不存在“数字签名”这个标签页。没有签名的可执行模型有这么几种情况:

         1Windows的一些早期模块遗留到了最新系统中,没有签名。

         2Windows中一些可执行模块是用cab方式打包安装的。cab包有签名,安装之后存在于系统中的可执行模块没有签名。但如果这些模块被修改,会无法通过校验,导致不能加载。

         3)一些不太正规,或者以不太商业的方式发布的软件,比如开源软件,其中可执行模块没有签名。

         没有签名的模块虽然存在,但不是主流。后续可以当作特例处理。下面假定我们碰到的正常的模块都是由数字签名的。数字签名是一种算法,分为以下两步:

         1)首先通过摘要算法对文件原有数据进行一轮计算,得到一个摘要值。之后如果文件内容被修改,那么摘要值也得修改,否则就会出现摘要无法匹配内容的情况。

         从图2-1看,这个文件实际有两个签名,分别使用了sha1sha256两种摘要算法。

         当然,我们都会想到,既然摘要算法是公开的,那么攻击者修改了可执行模块之后,再计算一次摘要值填回去不就可以了?因此数字签名还有下面的第二步。

         2)签名机构用私钥对算出的摘要进行加密。这样攻击者虽然可以修改文件内容计算出新的摘要,但是因为没有签名机构的私钥而无法对摘要进行加密。

         私钥加密的数据用公钥是可以解密的。校验者只要有签名机构的公钥即可解密摘要,验证可执行文件的内容的可靠性。

         为了演示效果,我用二进制编辑工具对WeChat.exe这个文件做无关紧要的修改,如图2-2所示。我将PE文件头部常见的字符串“This program cannot be run in DOS mode”中的一个字母“r”改成了“s”。

         实际上,修改之后我尝试运行它,发现运行依然是正常的。系统并没有什么异常提示。说明Windows本身并不在执行时校验用户态可执行模块的签名的有效性(但Windows会严格校验内核可执行模块的签名)。

 

2-2 二进制编辑工具对WeChat.exe这个文件做无关紧要的修改

 

         这时用同前面的方法打开文件属性页面查看签名,似乎也没有什么异常。但如果双击两个签名的其中之一,我们就会发现Windows判定这个签名是无效的,如图2-3所示。

         注意这并非是签名的公司有问题,实际上也不是签名有问题,而是文件的内容被修改了,导致内容和签名不匹配。

         虽然Windows并不阻止这个模块的执行,但对这种签名和内容不匹配,几乎可以肯定被病毒感染或者因为某种意外被写坏的文件,我们完全可以使用自己的技术手段来阻止执行。

         签名似乎同时解决了文件的可信性和完整性问题。

         首先文件经过一个机构签名,比如微软,又或者比如英特尔。即便是一个我不认识的公司的签名,那也是一个公司。如果他签名病毒给我,那是跑得了和尚跑不了庙的。然后签名的有效性确保了文件并没有被修改过。

         我们并不需要自己来计算哈希,简单校验可执行模块是否有签名,签名是否有效就可以了?这几步都是有Windows提供的API支持的。



2-3 Windows判定这个签名是无效的

        

         这时候反直觉的知识就来了:就本书写作的时期而言,绝大部分病毒、木马自带的恶意模块都是有签名的!

         设计之初,可执行模块的签名的确被设计来解决可信性和完整性问题。其中的理念之一是:虽然我无法知道这个可执行模块是否是恶意的,但至少知道恶意的事是谁做(谁签)的!

         如果一个签名签了恶意的模块,那么让更权威的机构吊销签名不就可以了?签名机构为了确保自己的签名不被吊销,也会认真确保不给恶意模块签名吧?

         但实际上,最终各种现实的妥协导致了签名的恶意模块满天飞,而签名机构对此能做的极为有限。为什么会导致这种情况呢?

         本书写作时,微软还在不断提高Windows内核驱动的认证门槛。EV签名、WHQL认证等措施均在不断强制实施中。等读者读到此书,微软或许已经有了更多更强硬的措施。但该机制的本质缺陷始终没有解决。

         有证书就可以对代码进行签名,但绝非是证书持有人只能对“自己”的代码进行签名。证书的颁发机构也无法判断证书持有者到底是在给自己的代码签名,还是在给他人提供“签名服务”。

         因为没有签名的代码可能无法在Windows上运行,又或许会被各种安全软件报为非法软件,所以无论是合法软件还是非法软件都会对签名产生强烈的需求。有需求就有市场,有市场就有价格。网络上提供的代码签名服务报价如图2-4所示。

         该图是本书写作时在网络上截取的、提供代码签名服务的报价页面。有着强大的经济利益推动,对恶意代码来说,获得签名只是一个价格问题。相比网络黑产可能获得的庞大利益,一年不到一万元的开销只是小菜一碟。

        


2-4:网络上提供的代码签名服务报价

 

         既然签名服务总是可以买到的。那么对恶意软件被成功签名就只能寄希望于两条:

         1、签名机构对模块的黑白进行准确的判定,不给恶意代码签名。

         2、如果某个机构签名的代码最终被公认是恶意的,那么对该机构进行处罚,永久吊销其证书作为震慑。

         但偏偏以上两点都不可能实施,或者实施中会出现巨大的变形而失去意义。

         我曾见过签名服务提供者要求客户提供自己的全部源码用来检验其中是否存在恶意行为。这种情况很罕见,绝大部分用户不会配合,会选择其他的服务商。但即便客户提供了源码,签名机构就真能发现其中的恶意行为?

         假定一份十万行的代码,其中九万九千九百行都实现了非常合理、合法的功能。只是其处在做完某个复杂的行为后,“忘记”了清理缓存并关闭某个端口。此时如果外部黑客连接进来,无需认证就能最终实现远程控制。

         那么签名机构仅仅通过阅读代码就能发现这一点?如果客户没有提供源码呢?签名机构通过逆向二进制代码就能发现一个复杂模块中存在的安全漏洞?这到底是安全漏洞还是恶意后门?眼前的黑不是黑,你说的白是什么白?

         何况签名机构很可能要对大量的模块以及同个模块的大量版本进行签名,根本没有人力物力去对每个模块进行分析检测。从收益成本比上来说,最优的选择是客户提交模块并付费——机构直接签名并开发票。

         让签名机构对模块的黑白进行准确的判定,不给恶意代码签名,无论从技术上说还是成本上考虑都是不现实的。

         推而广之,不但任何认证机构,包括微软,或是安全厂商,都无法对一份静态的代码的黑白做出可靠的认证。因为后门、缺陷本质上无法区分。没有缺陷的代码是不存在的,所以绝对安全、不可被恶意利用的代码也不存在。

         反而所有的认证机制最终都会变形为某种利益交换。Windows上总有些软件开发者会碰到过开发的软件发布之后,被某些病毒扫描软件报毒导致用户无法使用的情况,最后向相关安全厂商缴纳一笔“服务费”来解决。

         然后是第二个问题,证书的吊销。吊销证书对持有证书的签名机构来说,确实是一个很大的震慑。因为毫无疑问,申请新的证书又面临流程上的麻烦和金钱上的消耗。但实际上,吊销证书不是一件简单的事。

         我见过大量已泄漏而被吊销的证书,但签名恶意代码之后生成的Windows驱动程序还可以在目前最新版本的Windows11上加载。已吊销签名的证书用来签名恶意代码如图2-5所示。某某科技有限公司的签名私钥泄漏后,被广泛用于黑产签名。

         该证书已经被吊销,但签名的驱动竟然还可以加载。是不是又是一件很反常识的事?

 


2-5:已吊销签名的证书用来签名恶意代码

        

         微软不能直接禁止一个被吊销的证书签名的恶意模块加载到内核中,这是因为该证书以前签名过很多合法的模块。一旦全部禁止,许多合法的软件将无法正常工作。所谓投鼠忌器就是这个意思。

         那微软能不能允许这个证书以前签名的模块加载,但禁止这个证书签名任何新的模块呢?事实上Windows也是这么做的。如果我在今天用这个证书签名一个驱动,那么这个驱动将会被阻止无法加载。

         但微软是根据签名时间戳来判断签名时间的。如果签名时间在某个遥远的过去,那么Windows依然认为这个模块是合法的,可以加载。反常识的问题又来了:系统的时间可以设置。只要临时设置系统时间为1999年,完成签名,产生的模块就又可以加载了!

         目前流行的黑产签名工具中有些就集成了自动设置系统时间的功能。还有一些是签名后没有时间戳。这种签名的程序将被Windows合法加载。但Windows的内置杀软已经开始拦截无时间戳的黑签名驱动。

         此外,想要让签名的时间变得不可伪造,唯一的办法是让微软而不是软件厂商来签名。微软的EV签名强制要求就是将文件发送到微软进行签名,因此EV签名的时间不可伪造。

         但即便吊销签名的问题能得到彻底解决,我也不认为证书签名恶意模块的问题能够得到解决。事实上恶意模块并不在意证书是否被吊销。这家被吊销了,再换另一家就行了。

         总结这些原因对我们设计未来方案有很大的借鉴意义。

         首先签名应该继续存在。它确实杜绝了用感染其他模块方式来隐藏自己的病毒。说明它的意义是存在的。其次恶意模块的签名已经转变为付费行为,门槛提高,完全无成本开发恶意代码的时代开始落幕。

         其次,证书的吊销机制有很大的改进空间。现在微软早期签名时间控制机制因为很容易伪造实际失去了意义,EV签名将会一定程度解决这个问题。

         但无论如何设计,我们都不能指望签名机制能杜绝恶意可执行模块。它只能提升制作恶意可执行模块的门槛。

2.2.3 恶意代码的特征扫描

         现在市面上有许多安全软件,一般都有带有“病毒扫描引擎”。病毒扫描引擎的工作原理是在已知的恶意代码(这里包括了病毒、木马、蠕虫等各类)中截取部分特征码,在系统中的文件或内存中进行检索。如果发现这些特征存在则报告、拦截或清除它们。

         这是对文件可信性的另一种认证,和2.2.2节中所述的签名的作用刚好形成互补的关系。签名的作用是认证一个模块是白的(非恶意的、合法的软件)。而特征扫描确认的则是这个模块是黑的。

         在这里读者不必深究特征扫描的原理。读者需要明白的是,安装杀毒软件的确能有效地防止恶意模块的加载,但绝没有我们期望的那么完美。我可以举一个简单的例子来说明。

         有一个著名的网站名为VirusTotal。请使用搜索引擎搜索VirusTotal并进入官网。该网站提供简单的页面,让用户可以上传任何一个可执行模块的文件。它会使用行业中几乎所有的病毒扫描引擎对该文件进行扫描。任何个人用户都可以免费使用这个网站。

 

2-6:一个可执行模块在VirusTotal的扫描结果

 

         一个可执行模块在VirusTotal的扫描结果图2-6所示。我们可以看到,一共有71家病毒扫描引擎对它进行了扫描,只有4家认为是恶意代码。反过来说,有67家病毒扫描引擎认为它并不是恶意代码。

         事实上该程序是彻头彻尾的恶意代码。如果只是加载它,它几乎不做任何事情。但它提供了功能强大的接口。调用这些接口,黑产作者可以悄无声息地把任何代码塞进Windows内核隐藏起来,绕过签名、绕过杀软的扫描,实现任何非法目的。

         此类产品早已产业化。总地来说,工具厂商提供工具,如具有合法签名、不做任何非法的事,但是具有强大功能的工具。而真正的恶意厂商则可以选择购买或者租用这些工具打包安装到受害者机器上,并顺利将自己的恶意代码注入并隐藏起来,轻松绕过签名和扫描的措施。

 


2-7 网上各种公开售卖出租各种“合法”程序的广告

 

         本书写作时,网上已存在各种公开售卖或出租的“合法”驱动程序,为“租客”提供驱动隐藏、(任意进程的)内存读写、(任意进程的)隐藏式DLL注入等功能。网上各种公开售卖出租各种“合法”程序的广告如图2-7所示。

         它以这种方式提供公开的服务售卖,并在网上声明“严禁用户购买用于非法目的,若有,本站不承担任何责任”,就和毒贩声称“严禁购买用于吸食,一切后果与本贩无关”如出一辙。更讽刺的是该模块还获得了微软的签名。

         我在现实对抗中领教过它的威力,我知道这些。而病毒扫描引擎厂商们和微软并不知道。

         在这里你也会发现,VirusTotal这个网站其实是黑产代码开发的好助手。如果有人编写了一个恶意的模块,他一定会第一时间把“作品”拖到VirusTotal中进行一轮又一轮的扫描。如果被扫出恶意特征,没有关系,他可以立刻修改代码、加入保护等等不断进行调整。直到所有的扫毒引擎都提示没有恶意特征了,他就可以将恶意代码对外发布了。因此对外发布的恶意模块一定是扫毒引擎扫不到的

         即便是发布出去之后,恶意模块的作者依然可以定时用VirusTotal进行扫描。一旦发现有个别的扫毒引擎开始对该模块报毒,他就可以立刻调整代码,更新版本(大部分恶意软件都具有远程更新能力),将问题扼杀于萌芽之中。

         2-6所示的模块仅仅出现4家引擎报毒,大概率是经过类似方式处理的。之所以还剩下4家引擎报毒,是因为该模块仅在国内提供服务,并不在国外泛滥。剩余4家国外引擎报毒并不影响其“销量”,也就无需继续处理了。

         扫毒引擎并不是完全没有用,但它的作用是很有限的。对专业的、被强大经济利益所推动的、有针对性的攻击来说,它的作用更是趋近于零。

 

2.3 模块的执行防御方案设计

         外部通用的如签名、病毒扫描等机制虽然并不完全可靠,对个人用户而言已可以阻挡大部分低成本的一般攻击。但对办公环境而言,一套由内部自行管理的、可执行模块的可信性与完整性认证机制是有必要的。

         很多公司的开发办公室里充斥的源代码、原画、3D设计模型等在黑市上都是价值不菲的商品。有时甚至无需黑客动手,内部人士就会主动窃取售卖。我在现实中也接触过中知名游戏公司的办公网络被黑客在长期远程控制多年,持续不断窃取源代码并提供给私服商的案例。

         我并不了解所有的行业。但我可以肯定,不同行业会面临不同性质和不同程度的办公室信息安全问题。办公环境面临的是特别针对性的攻击。常见的如1.2.2小节和1.23小节所述的签名、扫毒等措施必然会被无情绕过。任何侥幸心理只会最终让公司蒙受重大损失。

         本书不推荐任何商业软件、产品或服务。需要的读者可以自行评估购置。本书仅用示例代码的方式介绍如何进行内网安全软件的开发。请尤其注意的是,本书的方案设计以及展示的代码仅用于教学,未经过详尽的测试和外网实际对抗的考验,仅供学习参考,不可直接商用。

 

2.3.1 模块执行防御的功能设计

         2.2.1节中的有一个假定。我们将这个假定记录并描述如下。

         假定2.1:任何模块执行的初始执行,恶意模块都必须首先以文件的形式存在。

         注意这个假定之所以能够成立,是建立在我们将所有非文件形式存在的“模块”都视同壳代码处理的基础上的。壳代码执行的防御将是本书中的另一个主题。而本章只讨论以文件形式存在并被Windows以正常方式加载的模块的防御。

         基于假定2.1,我们能够设计一个简单有效的模块执行防御系统。其作用是阻止或审计一切以文件形式存在的可疑的可执行模块的执行。

         1.1.1节的对话中,山羊胡子同事实际上已经做出了一个设计。他的设计是在每个可执行模块执行之前,计算该模块文件的散列值,并与已有的文件哈希值白名单库进行比对。如果该散列值在白名单中,则可以正常执行。反之,则拒绝执行。

         该方案对于工作环境单一且较少变更(比如工作线或者服务窗口上的电脑)是适用的。但对需要访问网络、查阅很多网络资料、使用各种大型工具软件(如游戏美工和程序员),且工具经常要更新的用户来说就比较难了。可能存在问题。

         对任何可执行模块的每次加载重复计算散列值,是一件相当耗费性能的工作。一般软件加载的可执行模块都可能超过数百个,大型软件更不用说。假定每个文件耗费0.1秒时间计算散列值,也能将加载时间拖慢数十秒,这将引起用户反感并最终被抛弃。

         这其中存在显而易见的优化空间:如果模块A在第一次运行的时候已经经过了计算确认其散列值在白名单库中,我们又能确认在第二次执行之前它并没有被修改,那么第二次执行时为何还要再次计算散列值呢?

         为此我提出一个性能更为优化的设计。其主要思想是:在大部分情况下,使用文件系统路径来代替散列值,只有必要时才使用散列值。在一般的执行过中,只要该文件的路径(实际上包括路径和文件名)不可疑,那么就放它继续执行,不用去计算散列值。

         该如何判断一个文件的路径是否可疑?这里我设计一个可疑路径库(后文简称可疑库)。如果一个模块执行时,它的路径位于可疑库中,那么它需要计算散列值才能执行。反之,则可以直接执行。

         换句话说,只要不在可疑库中,用户的系统中所有软件都可疑直接执行,不受影响。同时,设计如下规则:

         规则2.1:在安全系统初次安装之后,系统中的可疑库为空,不存在任何可疑路径。此时用户系统中所有软件均可正常执行。

         这主要是考虑到安全系统的部署时用户工作的连续性。在绝大部分情况下,安全系统并不会在目标环境创建之初就开始部署。

         安全系统开始部署时,目标环境(假定是一个办公室)往往已经存在并正常运行了多年。系统上已经安装了工作人员“趁手”的大量软件。贸然阻止用户已有环境中任何软件模块的运行,可能导致工作流程中断,连带导致安全系统的部署实际无法推进。

         方便的做法是,在安全系统初次安装时,进行恶意软件扫描清除和和操作系统及漏洞修补升级,然后将所有现存模块均认定安全的。这存在一定的风险,但在普通的工作环境下是可以接受的。

         对于更标准的流程,所有的新接入设备都应该使用公司IT部门统一配备的干净软件环境。这种情况下,依然不存在任何可疑模块,所以上述策略同样兼容。

         那么何时应产生可疑文件路径呢?设想一下,如果用户自觉或者不自觉从网上下载了一个exe文件,然后双击执行;又或者某个dll文件被下载了并覆盖了系统中一个原有的dll。此时问题产生了:该exedll文件是可疑的,不应被简单执行,因此设计如下两条规则。

         规则2.2:新的可执行文件创建时,将其路径加入可疑库中。

         规则2.3:原有的可执行文件的内容被覆盖或被修改时,若修改后的内容是一个可执行文件,将其路径加入可疑库中。

         以上两条就是整个设计的基础。其中有一些隐含的细节。比如一个新的可执行文件的产生不一定是从无到有创建一个新的文件。也有可能是一个原本就存在但并不是可执行文件的文件经过改写之后变成了可执行文件。

         一个原有的可执行文件被修改也存在两种情况:第一种是经过修改之后,它依然是可执行文件。这种情况是需要添加可疑路径的。另一种情况是经过修改之后,它不再是可执行文件,这就无需关注了。

         但被修改的也可能是可疑路径中的可执行文件(后面简称可疑文件)。可疑文件被修改之后不再是可执行文件,则应该从可疑库中删除。反之则保留。

         除了文件内容被修改之外,还有一种情况可能会影响我们的可疑库,那就是文件的改名(Rename)。

         要注意的是文件系统概念中的文件改名本质上与文件的移动(Move)等同。无论是路径的修改还是文件名字的修改,都认为是改名。同理,把一个文件从卷[1]一个位置移动到另一个位置也认为是改名。文件的改名无法跨卷。从C盘“移动”一个文件到“D”盘是不可能的,会自动转换成从C盘复制到D盘,然后删除C盘中原文件的操作。

         规则2.4:当某可疑文件被改名,那么对应可疑库中的可疑路径应该随之更新。但非可疑文件被改名并不会有任何影响。

         完善了关于可疑库的细节,这个设计就完成了大半。接下来是,如果系统中要加载并执行的文件的路径刚好命中了可疑库中的某个路径,那么应该如何处理?

         规则2.5:当某可疑文件被执行时,对文件内容计算散列值,如果散列值在散列值白库中,则执行并从可疑库中删除,否则禁止执行并将该散列值上报到后台。有必要的话,同时上报完整样本。

         所以除了可疑文件路径库之外,我们还需要一个散列值白库(后面简称白库)。白库中保存着经过公司自身认证过的所有合法、安全、必要的可执行库的散列值

         散列值位于白库中,则这个文件可以直接执行,该路径也应立刻从可疑路径库中删除。这样下次再执行这个文件的时候就不会遇到又要重复计算一次散列值的损耗了。因为所有的可疑文件执行仅计算一次散列值,性能将大幅度提升。

         那么如果将来这个文件被又被修改了之后怎么办呢?这种情况下该文件落入规则2.3的管辖范围,被重新加入到可疑列表中。

         白库是如何产生的?白库应从该公司认为合法、安全、必要的软件集合中所有的可执行模块库中产生。注意,合法、安全是相对模糊的概念,尤其安全的认证有一定的难度。对公司或组织内部的安全认定来所,最重要的是“必要”。

         绝对安全的软件是不存在的,我们需要做的是减少攻击面。如果一个组织体系内固定统一使用某个品牌的浏览器、办公套件和开发工具,那么这些软件就是“必要”的。而除此之外的软件皆为“不必要的”。

         我们无需去确认不必要的软件是否安全,只需要一概拒绝即可。这样我们可以把对安全的关注集中在公司必要的少数几款软件上,从而大大提升内网环境的安全性。

         这一点非常重要,但很多内网环境对此没有较为严格的认证。有时我们会吐槽柜台办公人员在闲暇时玩扑克游戏,或者管理人员在办公室打开了炒股软件(注意本章仅限模块执行的情况,使用浏览器浏览网页属于解释或者说脚本执行的范畴,其相关问题在本书后文解决),就是缺乏此类限制的典型案例。

         但完全严格地执行“非必要则拒绝”又可能导致作茧自缚的后果。比如某部门需要使用某个不常用的小众工具来解决一个独特问题,无法运行可能导致项目无法推进。所以我们需要规则2.5中的上报机制。

         当被拒绝执行的模块提交到后台,相关使用人员应有提起申请使用的机制。后台收到这些模块的样本和使用人员提交的申请,应组织管理者和安全人员处理这些申请,确认这些模块是有必要而且安全的,则可加入到白库中。

         客户端的机器可以从服务器上下载得到更新的白库,这样原来不可执行的模块现在经过审核之后就可以执行了。

         从缩小攻击面的角度出发,白库不应是全公司统一的。如果只有某部门或者某台机器需要某一个特殊的工具,在下发给该部门或者该机器的白库中添加其散列值即可。这样使用该工具的权限仅限于某个范围。

         到此为止我们基本完成了模块执行防御的方案设计。注意本方案的设计目标在于监控或阻止恶意模块的初始执行。换句话说,它是在当前系统并未沦陷情况下运行的方案。所以我们暂不探讨它本身被破坏(比如遭遇恶意停止、删除规则等情况)下的安全性。

         内网安全方案应该是强制执行,无法被用户手动关闭的(如果用户能手动关闭,那么大概率用户会因为五花八门的理由和借口关闭它)。防止关闭属于方案自保护的范围。本书后面会有专门的章节讨论安全方案的自保护。

2.3.2 模块执行防御的技术选择

         在任何环境下开发确保内网安全的软件,应尽量将技术实现做在底层。这是因为所有软件系统中,上层的应用依赖操作系统内核,而操作系统内核依赖硬件的实现。下层的实现总是可以干扰上层获得的结果。而反过来则不行。因此上层与下层的对抗是不平等的。

         我们很难预测复杂的系统在哪一层会有漏洞、哪一层可能被恶意代码感染,不断往底层做总是没错的。但同时我们必须考虑技术上的可行性与成本。

         比如苹果限制了任何第三方开发者染指IOS的内核(然而非法的黑产作者并不受此限),对此我们只能说一句由他去吧。我不相信任何安全厂商声称的IOS上开发的安全软件,那只是印刷着“保险柜”三个字的瓦楞纸箱。

         市面上大部分Android手机也类似。手机厂商限制了安全厂商接触系统内核。手机上几乎没有什么安全系统存在。那是因为应用市场限制带来的虚假和平给了人们很强的安全感,从此再也不需要安全防护软件了。

         好在目前手机并不是办公网内最常用的开发工具。否则如果有人专门针对布满了Android或者IOS系统的企业内网开发APT,将如入无人之境。实际上如果一个机构需要使用自己能掌控安全的移动设备,那将不得不进行设备定制,而不是直接购买普通用户使用的产品。

         出于对企业内网安全的考虑起见,我对安全系统的客户端部分的开发的技术选择建议如下:

         1)如果我们参与硬件的设计,那么将一些基础的安全功能做在硬件里是最好的选择。

         2)如果我们参与操作系统的设计,那么就应该将一些安全功能实现做在操作系统内核中。

         3)如果我们不能参与操作系统的设计,但是可以利用操作系统的内核提供的接口来实现内核模块,那么就把安全功能做在内核模块里。

         4)如果我们不能触及操作系统内核,但操作系统留有若干底层接口让我们实现部分安全功能,那么就尽量利用这些接口。

         5)如果我们既无法触及操作系统的内核、操作系统也未给安全系统留下足够底层的接口,那么我不推荐将该系统用于作为企业的主要生产力工具,因为其安全性很难保证。

         假定有一天我们必须使用某种平板电脑作为主要的设计和开发工具,那么我可以肯定,自定制的Linux平板只要正确地配置了安全特性、加入了基础的内部安全防御系统,会比某些消费级的通用平板产品安全性要好很多。

         幸运的是,Windows依然允许安全厂商开发内核驱动模块加载到Windows内核中。因此本书的设计依然可以在Windows上得以实现。

         我们在2.3.1节中设计的功能所需的核心技术实现有:

         1)捕获并可拦截任何文件的内容写入,以便以确认该文件经过写入之后是否变成了一个可执行模块,或者可执行文件被修改了而且修改后还是一个可执行文件。

         2)捕获任何文件的改名(本质是移动),以便监控可疑的可执行文件路径的变化,并据此更新可疑库。

         3)捕获并可阻止任何可执行模块被加载执行的事件,以便验证可疑库命中的可执行文件,计算散列值并在需要时上报相关信息。

         “捕获事件”是基于主机的入侵防御系统的技术实现中的一个常见要求。我们需要能及时捕获系统中的各种事件,进行安全与否的判断之后再予以放行。如果没有这个能力,即便能通过事后的扫描发现恶意代码的存在,安全事件也已经发生,则只能称为“检测”而不能称之为“防御”了。

         以上需求可以使用Windows的文件系统微过滤驱动(Minifilter Driver,后文均称为Minifilter Driver)来方便的实现。微过滤驱动运行在Windows的内核中,已经足够底层,能够满足我们的需求。

         在第3章中,我将展示具体的代码实现。



[1] 本书中的卷(Volume)是文件系统中的概念,对应磁盘中的一个逻辑分区,是文件系统能处理的最大存储单位。日常操作中可以将存有文件的C盘、D盘等各看成一个卷。



[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

最后于 2024-4-17 09:07 被星星人编辑 ,原因:
收藏
点赞0
打赏
分享
最新回复 (3)
雪    币: 19431
活跃值: (29097)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
秋狝 2024-4-17 09:17
2
1
感谢分享
雪    币: 4857
活跃值: (2619)
能力值: ( LV15,RANK:550 )
在线值:
发帖
回帖
粉丝
diycode 5 2024-4-17 15:24
3
0

挺扎实的内容,期待后续更新

最后于 2024-4-17 15:33 被diycode编辑 ,原因:
雪    币: 124
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
A1v1n 2024-4-17 16:45
4
0
感谢分享,很有收获可否给一个目录列表呀?谢谢~
游客
登录 | 注册 方可回帖
返回