导语:在最近发布的PSAmsi v1.1中的一大亮点是新增加了基于抽象语法树(AbstractSyntaxTree)的PowerShell“混淆”功能。我在这里用引号括起了“混淆”二字,希望你能在读到这篇文章的末尾后能明白我为什么会这样做。
在最近发布的PSAmsi v1.1中的一大亮点是新增加了基于抽象语法树(AbstractSyntaxTree)的PowerShell“混淆”功能。我在这里用引号括起了“混淆”二字,希望你能在读到这篇文章的末尾后能明白我为什么会这样做。
抽象语法树?
那么,什么是AbstractSyntaxTree?AbstractSyntaxTree(“AST”)是一种来表示和分析编译和代码语言的源代码的常用结构。自PowerShell v3之后,PowerShell包含了一个内置的AST。PowerShell的独特之处在于它以一种对开发者友好的方式公开了AST结构,并且有内容丰富的文档。
以下是一个PowerShell脚本的完整抽象语法树(AbstractSyntaxTree)示例:
示例
我们将在本文的其余部分中,使用以下示例脚本:
这只是一个愚蠢的PowerShell函数,它没有任何用处,但会帮助我们演示一些基于AST的混淆技术。
基于PSToken的混淆方法
为了有助于读者理解基于AbstractSyntaxTree的混淆技术的好处,我认为我们首先需要了解一下基于PSToken的混淆原理。Tokens或“PSTokens”是解析和表示PowerShell代码的另一种语法结构,并且从PowerShell v2开始,就已经被使用。一个PowerShell脚本本质上是由大量的PSTokens组成,通常用空格分隔。AST用一个更加复杂的结构表示了脚本代码,并将函数组件进行模糊地分组,PSTokens是一个更为简单的列表。
在PSAmsi v1.0中,唯一使用的模糊处理技术是利用了Invoke-Obfuscation的“Token”混淆处理技术,它是一种基于PSToken的混淆处理技术。Invoke-Obfuscation具有一个用于混淆各种类型的PSTokens的选项库。在一个高层次上来看,它基本上遍历了脚本中的所有PSTokens,然后将每个Token单独进行混淆,并在末尾将混淆的碎片合并在一起。
例如,我们的示例脚本的PSToken混淆过程有点像下面这样:
我们开始迭代遍历PSTokens,第一个token是CommandArgument。我们知道我们可以将刻度字符(也就是`字符)插入到CommandArgument 这个Token中,所以我们可以这样做:
Type Content ObfuscatedContent ---- ------- ----------------- CommandArgument Test-AstObfuscation -> TE`s`t-AS`TOBFus`CAtIon
接下来我们有一个ParameterSetName成员Token,我们不能插入刻度字符。所以我们只是生成了随机的字符:
Type Content ObfuscatedContent ---- ------- ----------------- CommandArgument Test-AstObfuscation -> TE`s`t-AS`TOBFus`CAtIon Member ParameterSetName -> ParamEterseTNAME
接下来我们有一个Token 叫String,我们有好几种做法,但是我们只在这里添加刻度字符:
Type Content ObfuscatedContent ---- ------- ----------------- CommandArgument Test-AstObfuscation -> TE`s`t-AS`TOBFus`CAtIon Member ParameterSetName -> ParamEterseTNAME String Set1 -> “S`et1”
这个迭代过程贯穿了整个脚本
Type Content ObfuscatedContent ---- ------- ----------------- CommandArgument Test-AstObfuscation -> TE`s`t-AS`TOBFus`CAtIon Member ParameterSetName -> ParamEterseTNAME String Set1 -> “S`et1” Member Position -> PositiOn Member Mandatory -> MAnDatoRY Member ValueFromPipelineByPropertyName -> ValUefroMPipELiNebyProPeRTyname Variable True -> ${t`RuE} String Parameter1 -> {“{0}{1}{2}” –f’Parame’,’te’,’r1’} String Param1 -> {“{1}{0}”-f’1’,’Param’} String ParamOne -> {”{0}{2}{1}” –f ‘Para’,’e’,’mOn’} Variable ParameterOne -> ${p`Ara`m`et`EROne} Member ParameterSetName -> PaRamEtERsEtNaME String Set2 -> “SE`T2” Member Position -> POSitiON ...
并最终生成了下面这样的结果:
构建在Invoke-Obfuscation中的这种PSToken混淆方式可以绕过AMSI签名校验(事实上,我还没有遇到过一个基于PSToken混淆的实例没有突破AMSI签名的)。PSToken混淆的缺点与我在上一篇文章中提到的模糊检测有关。基于PSToken的模糊处理增加了大量的特殊字符,并采用了奇怪的PowerShell语法。
PSAmsi v1.0的主要混淆方式是基于PSToken的混淆处理技术,而不是在整个脚本中使用混淆,从而弥补这一缺陷。这种方法运作良好,但我们可以做得更好吗?
基于AST抽象语法树的混淆技术
在PSAmsi v1.1中,我添加了一个名为Out-ObfuscatedAst的函数,它利用抽象语法树的强大功能来执行隐藏的混淆处理过程。基于AST的混淆技术的关键点在于,它使用了AST的类型,并将其放在附加的混淆选项的上下文位置。至少到目前为止,很多这些附加的混淆选项都与脚本中AST的顺序有关。举个例子吧,这样会更有意义。
整个语法树很大,不能很好地显示出来,所以我们先看一个子树,它只是代表了我们示例脚本的整颗大树的一小部分:
这只是AttributeAst这一行代码,它将大量的属性应用于ParamBlockAst中的参数中。这四个Ast子节点都是应用于参数的所有属性。其中一个属性是“Mandatory”属性,它表示使用该函数需要一个参数。Mandatory属性是布尔值类型,可以是True或False。事实证明,我们可以仅通过名称(即“强制性”)或者通过实际分配True值(即“Mandatory = $True”)来指定True布尔属性。目前,我们只是通过名称来指定它,所以我们变换一下语法树:
我们可以用“ValueFromPipelineByPropertyName”属性来做一些非常类似的事情。这次我们去掉“= $True”这一部分,只留下属性名称:
最后,所有这些子节点都是不相关的属性并且被应用到了同一个变量,所以我们可以以任何我们想要的顺序分配它们,我们可以像下面这样重新对它们进行排列:
现在你可以开始了解语法树AST位置的上下文是如何为我们提供额外的混淆选项的。这些属性都是一个单一的AttributeAst对象的子节点,并且这些属性都应用于一个相同的参数,这允许我们在AttributeAst的内部重新对它们进行排序。
现在,让我们稍微向外放大一下,来看看一颗更大的语法树:
这是原来的语法树,其中包括了上一个我们讨论的那个将AttributeAst作为一个子节点的语法树。所以我们首先将混淆应用到我们已经完成的AttributeAst上面吧:
现在让我们继续向下看看指定了几个“别名”的AttributeAst。这AttributeAst指定了替代名称,或“别名”,可被用于替换默认的“ParameterOne”参数名称。这些别名当然可以以任何顺序列出,所以我们可以变换这些别名:
我们将语法树再次向外缩小到更大的语法树:
我们现在看到,有两个参数在更大的语法树ParamBlockAst中。到目前为止,我们已经分析了第一个ParameterOne参数。所以我们再次应用我们已经为该参数找到的混淆方式:
当然,我们可以对第二个参数ParameterTwo做类似的混淆。我们可以重新排列AttributeAst的P arameterTwo中的属性,就像我们操作第一个参数那样:
但参数本身就是可以重新排序的!所以我们在ParamBlockAst中变换整个ParameterAst 的顺序:
现在我们继续缩小到更大的语法树,这是最后一次了:
ScriptBlockAst包含了整个ParamBlockAst的语法树,也就是我们迄今为止所看到的全部以及其他三个NamedBockAst子节点。我们首先对ParamBlockAst应用我们所发现的所有混淆:
现在我们把重点放在第一个NamedBlockAst子节点,也就是那个包含“Begin”块的子节点。我们所要做的事情就是将一个名为“Start”的变量赋值为最小值和最大值之间的一个随机值。事实证明,除了使用标准的“=”运算符之外,还有其他方法可以为变量赋值。我们可以改为使用Set-Variable这个cmdlet。另外,可以按照我们喜欢的任何顺序来指定命名参数(比如Get-Random的-Minimum和-Maximum参数)。因此,我们现在将同时应用这些混淆技术,使用Set-Variable和指定-Minimum与-Maximum参数:
继续操作第二个NamedBlockAst子节点,“Process”块,我们可以使用Set-Variable将表达式的结果赋给“Result”变量来做一些非常相似的事情。此外,数值表达式作为表达式的一部分,分配给“Result”变量。事实证明,一些数字表达式(例如加法运算符)是“可交换的数学表达式”,这意味着它们也可以被重新排序!所以我们同样应用这两种混淆技术:
最后,Begin,Process和End这三个 NamedBlockAst子节点可以在ScriptBlockAst的函数内重新排序:
Out-ObfuscatedAst为我们生成了最后的结果:
到此为止,希望你可以明白为什么我在文章开头对 “混淆”二字加了双引号。这种基于抽象语法树AST的模糊处理技术并不能真正隐藏脚本所做的事情,它只是通过重新排序语法树中的子节点,找到功能上相同的代码,让代码看起来与原来的有点不同而已。虽然它可能不会彻底地隐藏代码正在做的事情,但它足以绕过签名验证。
基于AST混淆的一件很酷的事情是,它并不没有真正使用许多特殊字符或奇怪的语法来使代码看起来不同。它看起来像正常的PowerShell,所以这使得那些基于任何形式上的检测的混淆检测技术变得无效。我也可以将一些特殊字符和奇怪的语法加入到AST混淆处理中,但是对于我来说,在PSToken混淆处理中保留这些做法会更有意义,所以如果你真的想要把这两种混淆技术都加上,那么你可以这样做:
Out-ObfuscatedAst仍然有很大的提升空间。它已经可以绕过很多签名校验了,但是AST是一个强大的结构,我相信还有其他很好的机会将新的基于AST的混淆形式引入到函数中。我会继续尝试添加新的类型,因为我发现了一些的类型。
关于PSAmsi
在此之前,PSAmsi仅针对基于PSToken的混淆,并且仅在需要混淆的签名上尝试使用最小化的混淆数量,并绕过混淆检测技术。现在,我们不仅可以只在需要混淆的签名处进行混淆处理,而且还可以使用隐藏的AST形式的混淆处理方式。
所以,PSAmsi在Get-MinimallyObfuscated函数上做的事情就是:
1. 首先枚举脚本中的所有签名。
2. 其次,我们迭代遍历每个签名。对于每一个签名,我们都尝试使用基于AST的混淆处理方式,这通常会成功的破坏签名,但也并不总是如此。
3. 第三,如果基于AST的混淆失败了,我们可以回退到使用基于PSToken的混淆方法。
这种方法允许我们限制我们所使用的混淆的总数量,同时仍然能够破坏给定脚本中存在的所有签名。