赞
踩
在最近发布的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 的混淆方法。
这种方法允许我们限制我们所使用的混淆的总数量,同时仍然能够破坏给定脚本中存在的所有签名。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。