前言

在我们分析了如何从PowerShell进程转储中提取活动历史记录后,又产生了一个有意思的后续问题:“是否可以提取已经执行的脚本内容(来自磁盘),即使这些文件未被捕获?”

答案是肯定的,但这一过程也非常复杂。为了详细讲解这一方法,我们将从侦查开始起步。在我们的研究过程中,很多地方都需要WinDbg协助完成自动化操作,因此我们的第一步就是安装WinDbg模块

要部署我们的实验环境,我们编写了这个简单的脚本,并将其保存在类似于C:\temp的位置:

1.png

打开PowerShell会话,运行脚本,然后创建转储文件。

2.png

现在,使用WinDbg模块连接到转储文件:

Connect-DbgSession -ArgumentList '-z "C:\Users\lee\AppData\Local\Temp\powershell.DMP"'

开始调查

在调查刚刚开始的阶段,我们需要投身于一个非常广泛的网络之中。我们目前知道,需要提取的是代表在该会话中运行的脚本的对象(如果存在)。但是,我们如何能找到呢?

首先,我们使用SOS的“Dump Object”(转储对象)命令,来转储进程中每个对象的所有内容。因此,我们将从!DumpHeap命令开始查找所有对象实例(我们甚至没有使用Type过滤器)。实际上,我们有更优的方法可以实现,但这一步骤和下一步骤将需要很长的时间,所以我们可以在睡觉之前进行这项工作。

$allReferences = dbg !dumpheap –short

一旦我们获得了全部对象引用,就可以使用!do(转储对象)命令,让SOS将它们全部可视化。转储对象的输出不包括被转储对象的地址,因此我们也将使用Add-Member来跟踪它。

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

(转天)我们发现,这确实是一个强大的工具。在我的系统上,SOS获取了该进程例程的大约1000000个对象。那么,其中的任何一个对象的GUID都能被SOS以可视化的方式展现出来吗?我们来具体看看。

3.png

看起来,我们很幸运,在这百万级的对象中,我们设法将其缩小到PowerShell内存中的7个System.String对象,这些对象以某种方式引用了GUID。如果我们认为信息一直在System.String中,我们可以使用$allReferences = dbg !dumpheap –type System.String –short,以使我们的初始“$allObjects”查询更快。但是,我们如何弄清楚这些GUID对应的是什么呢?

为了找到答案,我们要使用SOS的!gcroot命令。该命令通常用于诊断托管内存泄漏,例如:“我做了什么最终导致CLR保留此字符串的1000万个实例?”对于任何指定的对象,!gcroot命令会给出哪个对象引用了它,以此类推,直至对象树的根。

4.png

如上图所示,最后一个(数组中的第6项)实际上并没有走到根。它不再被引用,因此很快就会被垃圾收集器清理干净。

第5项以一个对象数组(System.Object[])作为根,其中一个元素是ConcurrentDictionary,其中有一个ScriptBlock,包含CompiledScriptBlockData,负责在PowerShell AST中保存节点,并在CommandAst AST中到达末端,引用了这一GUID。

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

5.png

这非常有趣!它的开头是相同的根对象数组(0000026e101e9a40)、相同的ConcurrentDictionary(0000026e003bc440),但最后是一个包含我们的字符串和另一个字符串的元组(Tuple,两项的简单配对)。接下来,我们深入了解一下这个元组,以及其包含的字符串。

6.png

分析PowerShell中相应数据结构

因此,这个元组共包含两个元素。第一个元素看起来像是执行脚本的路径,第二个元素看起来像是该脚本中的内容。接下来,我们看看PowerShell源代码中的相应数据结构。我们搜索ConcurrentDictionary,以查看我能找到的内容。在第三页,找到如下内容:

7.png

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

8.png

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

因此,我们知道它的存在,我们可以优化自动化程序,并专门提取这些内容。我们现在编写的是真正有效的脚本,具体步骤如下:

1、使用!dumpheap查找此元组类的实例。Dumpheap命令会执行子字符串搜索,因此我们会使用正则表达式进行一些后续处理。

2、我们获得实际想要研究的元组类MT。

3、再次使用该MT作为过滤器运行!dumpheap。

9.png

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

10.png

我们从结果中提取出两项,然后就可以得到一个完美的PowerShell对象:

11.png

这是一段漫长的旅程。我们进行一下回顾,首先我们分析了一个假设,然后从这个假设开始,层层递进,最终能够从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 }
    }
}
本文翻译自:http://www.leeholmes.com/blog/2019/01/17/extracting-forensic-script-content-from-powershell-process-dumps/如若转载,请注明原文地址: http://www.4hou.com/technology/15890.html
源链接

Hacking more

...