从PowerShell内存中提取取证脚本内容

阅读量    107977 |   稿费 120

分享到: QQ空间 新浪微博 微信 QQ facebook twitter

 

在发布了《从PowerShell进程转储中提取活动历史记录》这篇文章后,我收到了一个有趣的问题:“如果没有捕获到原始文件,是否可以提取到已执行的脚本内容呢(从磁盘里)?”

答案是“可以”,但是操作起来很复杂。 我们从头开始一步一步看看是如何进行取证的,第一步先安装PowerShell的WinDbg模块,我们需要它来做一些自动化的工作。

首先搭建我们的取证实验环境,在C:temp之类的目录下创建如下简单的脚本:

打开PowerShell会话,运行这个脚本。然后在任务管理器中Dump Powershell 内存:

现在,使用WinDbg模块连接到Dump下来的文件:

Connect-DbgSession -ArgumentList '-z "C:UsersleeAppDataLocalTemppowershell.DMP"'

 

开始取证

在开始我们的取证之前,让我们脑暴一下。我们的目的是提取Powershell会话中运行的脚本对象内容(如果存在的话),那么问题就是,如何找到这个脚本对象呢?

首先,我们使用SOS的Dump Object命令来转储进程内存中的所有对象内容。然后,我们用!DumpHeap命令开始查找所有对象实例(eg:这里我们甚至没有用-Type过滤指令)。这一步和接下来的操作将花费很长时间,所以建议在睡觉之前跑这个命令 : D

$allReferences = dbg !dumpheap -short

一旦我们导出了所有对象引用,便可使用!do(Dump Object)命令将它们全部可视化。转储对象的输出中不包括被转储对象的地址,因此我们用Add-Member来跟踪它。

$allObjects = $allReferences | Foreach-Object { $object = dbg "!do $_"; Add-Member -InputObject $object Address $_ -PassThru -Force }

(第二天)确实是一大堆数据呢!这个进程大概有100万个被SOS识别出来的对象实例。他们中有SOS可以可视化的GUID吗? 我们来看看:

看起来我们运气还不错。在上百万个对象中,我们设法将范围缩小到PowerShell内存中的7个System.String对象,这些对象以某种方式引用了GUID。如果我们认为信息可能一直在System.String中,我们可以使用$allReferences = dbg !dumpheap –type System.String –short代替一开始的$allObjects来加快查询。 问题又来了,我们怎么知道谁包含了这些GUID呢?

为了找到答案,我们使用SOS的!gcroot命令。 这个命令通常用于诊断托管内存的泄漏问题。例如:“为什么CLR保留了字符串的一千万个实例?”。对于任何给定对象,!gcroot命令告诉您哪个对象引用了它,这样递归一直到对象树的根节点。 让我们探讨一下这些根节点。

好了,所以最后一个元素( 数组中的第6个 )并不是最终的根节点。它不会再被引用,很快就会被垃圾收集器清理干净。

第5个是以一个对象数组为根节点的,其中一个元素是ConcurrentDictionary,它包含一个ScriptBlockScriptBlock中包含CompiledScriptBlockDataCompiledScriptBlockData包含了PowerShell AST中的节点,最后PowerShell AST引用了CommandAst AST,最终引用了这个GUID。

看起来还不错,那其他的呢,我们看看实例中的第4项:

有意思! 这个是以相同的根对象数组(0000026e101e9a40),相同的ConcurrentDictionary(0000026e003bc440)开头的,但这次最后是一个简单配对的元组(包含我们要找的String和另一个String)。 让我们深入了解那个元组及其包含的字符串:

这个元组有两个元素。 第一个元素看起来是执行脚本的路径,第二个元素看起来是该脚本中的内容。 让我们看看PowerShell Source怎么定义这些数据结构的。 搜索一下ConcurrentDictionary, 在第三页我们可以看到:

有一个名为CompiledScriptBlock的类。 它包含一个名为s_cachedScripts的静态(进程范围)缓存。 这是一个将一对字符串映射到ScriptBlock实例的字典。 如果您阅读了源代码,您可以确切地看到Tuple的内容(一个脚本路径到ScriptBlock缓存内容的映射):

这个数据结构就是我们最终要关心的。出于性能原因,PowerShell维护一个内部脚本块缓存,这样每次看到脚本时都不需要重新编译脚本块。 该缓存是路径和脚本内容的关键。 存储在缓存中的东西是ScriptBlock类的一个实例,它包含了已编译脚本的AST。

所以现在我们知道了它的存在,就可以自动化地并有目的性地去提取这些东西! 现在我们正式开始写提取脚本,我们要做的就是:

  1. 使用!dumpheap查找此元组类的实例。dumpheap命令会进行字符串搜索,我们可以使用正则表达式进行一些后期过滤
  2. 我们拿到了实际想要研究的元组类的MT
  3. 使用该MT作为过滤器再次运行!dumpheap

现在我们可以深入其中的一个节点。 它有一个m_key,我们可以研究一下。

差不多了!让我们从结果中提取出两个东西,然后获得一个漂亮的PowerShell对象:

xx

​ 这是一个漫长的过程,但是我们从一开始的假设到最后提取出脚本内容,都完整的过了一遍。现在,即使原始脚本文件已经被删除,你也能够从Powershell内存中恢复有所的脚本内容了。

​ 以下是将所有这些都打包成了一个函数的最终脚本:

function Get-ScriptBlockCache 
{ 
    $nodeType = dbg !dumpheap -type ConcurrentDictionary | 
        Select-String 'ConcurrentDictionary.*Node.*Tuple.*String.*String.*]]$' 
    $nodeMT = $nodeType | ConvertFrom-String | Foreach-Object P1 
    $nodeAddresses = dbg !dumpheap -mt $nodeMT -short 
    $keys = $nodeAddresses | % { dbg !do $_ } | Select-String m_key 
    $keyAddresses = $keys | ConvertFrom-String | Foreach-Object P7
    foreach($keyAddress in $keyAddresses) { 
        $keyObject = dbg !do $keyAddress

        $item1 = $keyObject | Select-String m_Item1 | ConvertFrom-String | % P7 
        $string1 = dbg !do $item1 | Select-String 'String:s+(.*)' | % { $_.Matches.Groups[1].Value }

        $item2 = $keyObject | Select-String m_Item2 | ConvertFrom-String | % P7 
        $string2 = dbg !do $item2 | Select-String 'String:s+(.*)' | % { $_.Matches.Groups[1].Value }

        [PSCustomObject] @{ Path = $string1; Content = $string2 } 
    } 
}
分享到: QQ空间 新浪微博 微信 QQ facebook twitter
|推荐阅读
|发表评论
|评论列表
加载更多