本系列文章为看雪星星人为看雪安全爱好者创作的原创免费作品。
欢迎评论、交流、转载,转载请保留看雪星星人署名并勿用于商业盈利。
本人水平有限,错漏在所难免,欢迎批评指正。
本章内容为介绍模块执行防御。在此我将先介绍“执行”分类,以及“模块执行”在“执行”中的位置和重要性。
恶意代码(或者行为)要在被攻击的机器上执行起来,看起来似乎一定要让被攻击的处理器执行一些指令。但这种想法并不正确。
在《渗透测试高手:打造固若金汤的安全网络》(有意思的是这本书的英文书名意义和中文翻译版完全相反,意义为破解最坚固的网络)一书中,作者Wil Allsopp举出了一些例子,比如一个如同转接头一样的东西。他趁别人去吃午饭的时候爬到别人办公桌下面,把键盘线拔了,插上他的“转接头”,然后再把原本直接插在电脑上的键盘线插在转接头上。
这样对方所有的按键都被这个“转接头”记录。而且转接头内置手机卡和具有手机功能的芯片,能源源不断将记录的按键通过移动网络发出,信息随之泄漏。被攻击主机上没有执行恶意任何代码,而且这种硬件很长时间也不会有人发现。
我举这个例子仅为说明情况并不总是如我们想象的那么简单。在我们构筑安全的防线的时候,千万不要有“我这种技术能百分之百防住所有攻击”的想法。在对可能的攻击形式进行分类的时候要尤为小心。就像我打算将恶意代码的执行方式进行分类,也一定会有我未能想到的地方。
现在假定排除所有的恶意硬件以及不需要执行任何恶意代码的社会工程学攻击,仅仅考虑有恶意代码在主机上运行的形式。那么无论如何,恶意代码从并未执行到开始一系列的操作,一定存在一次“初始执行”的过程。所谓的“初始执行”是指恶意代码在被攻击机器上的第一次执行。
恶意代码的初始执行有很多种可能。在我们的现实生活中可以分成如下几种途径:
(1)用户操作直接运行了来自攻击者的恶意的、或者被恶意代码感染的(后续统一称为恶意的)可执行模块。
(2)用户操作同(1),但用户点击的是某种可解释执行的命令或脚本,如.bat,或者Linux的shell脚本,被浏览器解释执行的JavaScript等。
(3)用户操作打开了来自攻击者的某种非可执行模块的东西(如网页、pdf、doc文档、图片等),导致了主机上某个软件(如浏览器)启动,然后发生了恶意代码的执行或者是合法的程序被利用执行了恶意的操作。
(4)用户什么也没有做。但是主机受到来自网络的攻击,系统或某软件自身漏洞或设计缺陷或策略上的漏洞导致了同(3)的后果。
不管是哪种初始途径,我们总是可以把所有的恶意代码的执行归结为两种:
(1)原生执行。
(2)解释执行。
所谓原生执行是指被攻击的电脑的处理器支持的指令直接在被攻击的处理器上运行。Biru 现在的内网电脑绝大部分使用x64的处理器,那么我们可以认为恶意代码本身就是x64指令的情况就称为恶意代码的原生执行。原生执行必须拥有如下两个要素之一:
(1)这些原生指令不属于任何合法的软件,是出于任何合法的软件设计之外的目的编写的。用户直接下载一个恶意的可执行模块就是这种情况。
(2)这些原生指令虽然属于合法的软件,但其执行过程不符合该软件的任何设计。比如一个程序因为缓冲溢出而返回到了错误的地址,然后执行了一串本不该被执行的指令。
如果某服务程序收到了来自远程的命令然后执行了恶意的操作(比如RPC的方式),我并不认为这是一种原生执行。因为该操作虽然恶意,但是服务程序本身就具备提供该操作的功能,因此是符合设计的。
符合设计并不等于没有恶意。此类恶意执行我将归于上述的(2)解释执行。因为它实际上是解释执行了恶意的命令。解释执行将会放在后面讨论,在这里我们只讨论符合上面提到的二个要素之一的原生执行。
原生执行本身的形式是多种多样的。其中最为“规范”的方式是模块执行。
所谓模块是操作系统所规定的一个可执行单元。它保存在硬盘上的时候表现为一个可执行文件(如Windows上的.exe、.dll或者.sys文件,Linux上的.so文件),加载进入内存之后则成为一个可执行映像。
模块并不一定要经过文件-加载-映像-执行的过程。不生成任何文件,直接把模块写入内存生成映像是完全可以做到的。很多恶意代码用这个方式避免扫描硬盘文件方式的检测。
除了模块执行之外,壳代码(shellcode)执行也是原生指令执行方式的一种。
这个地方很容易混淆。所谓壳代码中的“壳”(shell)本意是指Linux中的控制台。因为最早隐藏的这些恶意代码目的是为了能偷偷连接一个控制台来控制这台机器。通过远程控制台来恶意地控制一台机器,这在本书的分类中属于解释执行而非原生执行。因为恶意的是命令而不是原生指令。
但现在这个词语的意义已经发生了很大的转变。目前国内安全行业中所谓“壳代码”特指并非以规范的完整模块形式存在,而是嵌入在模块中、或者是游离于所有模块外的代码。壳代码并非一定是恶意的,但有很多恶意代码是壳代码的形式。
“加壳”也成为了国内安全行业的一个特有名词。所谓的“加壳”的意思是“在原本干净的模块中加入壳代码”。这个操作并不一定是恶意的。很多对模块进行安全加固的操作,就是以壳代码的形式加入安全组件,使得模块更难被破解。
无论是单纯的模块,还是不属于任何模块的壳代码,它们都需要生成新的指令。但实际上还存在一种不需要生成任何指令就能原生执行恶意代码的形式,那就是不写入任何恶意指令,但利用主机上原有指令进行执行。
内存中已经存在的合法模块中已经有很多的指令。如果用一种巧妙的方法,在不改写这些指令的情况下构造一个新的指令路径来执行某些指令,就能实现想要的完成的恶意操作。有兴趣的读者可以自己了解一下ROP执行。
此外在利用漏洞进行壳代码执行之前,也常会利用已有模块中的指令作为跳板来跳转到壳代码。本书中我可以把此类执行统称为“利用执行”,意思是利用原有合法执行实现新的操作的原生执行。
因此,原生执行可以分为模块执行、壳代码执行、利用执行这三种。如果读者认为还有更多的形式,欢迎来信补充。
解释执行是和原生执行相对的,并非直接执行处理器所执行的原生指令,而是由合法的服务器或解释器(比如RPC服务、Python.exe、Windows PowerShell、浏览器、Office、Java虚拟机)去执行命令、解释脚本或者中间码来实现执行的。其执行过程本身是符合设计的,但执行的命令或脚本中含有恶意的内容。这一部分将会在本书后面的章节中再详细分析。本章不会涉及。
总而言之,本书对恶意代码的执行进行了如下的分类:
n 原生执行
u 模块执行
u 壳代码执行
u 利用执行
n 解释执行
本书的第一部分将重点解决模块执行的防御问题。
在各种执行方式的分类中,如果从攻击者的角度出发,我们会发现,壳代码执行和利用执行的能力虽然巧妙而且隐蔽,但在使用上是极为不便和受限的。而可执行模块和脚本则几乎具有“无限”的能力。
比如壳代码,它没有模块结构,因此也不存在导入表,如果需要调用系统中其他模块的函数则相当麻烦。这种麻烦并非不可以克服,但很少有人那么去做,因为没有必要。既然壳代码已经执行起来,那从下载一个可执行模块或者恶意脚本来运行很难吗?
我见过很多恶意程序,但完全由壳代码构成、无任何模块组成和脚本成分的恶意程序少之又少。一般攻击者都会将壳代码执行和利用执行当作下载并执行真正的恶意模块或者恶意脚本的跳板。
总而言之,模块执行非常重要。监控住所有的模块执行,或者拦截住非法模块的执行,对内网安全有巨大的意义,是基于主机的入侵检测和防御系统的重要组成部分。
在我个人看来,成功的模块执行防御(完美的成功是不存在的)的技术难度最小,但足以在内网安全中顶小半边天,同时另外小半边天则是范围极广、难度极大的解释执行的防御。
在当前现实的世界中,对模块的执行已经存在诸如签名校验、特征扫描等措施。因为这些措施的共同特点都是公开的,所以我将它们称之为“公开检验”措施。本节将会详述这些公开检验措施的效果和局限性。
在模块被加载进入系统之前,一般的载体为可执行文件,在Windows上为PE文件(Portable Executable,这是一种文件格式规范,意为可移植的可执行文件)。Windows中常见扩展名为exe、dll、sys的文件就是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)这些文件没有被修改过,即文件的完整性。
下面我们将专门探讨用来解决这两个问题的公开检验措施。
在我们日常的、对计算机安全的想法中,最常见的一个错觉,就是认为可执行文件的签名能有效确保可执行文件是可信而且完整的。
如今大部分Windows上的可执行模块都有签名,包括Windows系统自带的可执行模块和安装其他软件而带入的可执行模块。Windows上对任何一个可执行文件用鼠标右键点击,弹出菜单中选择“属性”、“数字签名”则可以看到该文件的数字签名。
Windows上可执行文件的数字签名如图2-1所示。
图2-1 Windows上可执行模块的数字签名
有些模块没有签名,那么熟悉中也就不存在“数字签名”这个标签页。没有签名的可执行模型有这么几种情况:
(1)Windows的一些早期模块遗留到了最新系统中,没有签名。
(2)Windows中一些可执行模块是用cab方式打包安装的。cab包有签名,安装之后存在于系统中的可执行模块没有签名。但如果这些模块被修改,会无法通过校验,导致不能加载。
(3)一些不太正规,或者以不太商业的方式发布的软件,比如开源软件,其中可执行模块没有签名。
没有签名的模块虽然存在,但不是主流。后续可以当作特例处理。下面假定我们碰到的正常的模块都是由数字签名的。数字签名是一种算法,分为以下两步:
(1)首先通过摘要算法对文件原有数据进行一轮计算,得到一个摘要值。之后如果文件内容被修改,那么摘要值也得修改,否则就会出现摘要无法匹配内容的情况。
从图2-1看,这个文件实际有两个签名,分别使用了sha1和sha256两种摘要算法。
当然,我们都会想到,既然摘要算法是公开的,那么攻击者修改了可执行模块之后,再计算一次摘要值填回去不就可以了?因此数字签名还有下面的第二步。
(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、如果某个机构签名的代码最终被公认是恶意的,那么对该机构进行处罚,永久吊销其证书作为震慑。
但偏偏以上两点都不可能实施,或者实施中会出现巨大的变形而失去意义。
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
最后于 2024-5-7 07:16
被星星人编辑
,原因: