70

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

 5 years ago
source link: https://www.anquanke.com/post/id/170497?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

iu6fInj.png!web

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

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

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

zeYVV3r.png!web

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

3maaQrY.png!web

现在,使用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吗? 我们来看看:

zU7NRjv.png!web

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

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

aA7zuma.png!web

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

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

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

NBZjmmE.png!web

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

fQBRZnI.png!web

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

YbMJFzA.png!web

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

RV3ERbN.png!web

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

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

!dumpheap
!dumpheap

VNrARfZ.png!web

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

3A77RvA.png!web

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

ymU7Fzi.png!web

​ 这是一个漫长的过程,但是我们从一开始的假设到最后提取出脚本内容,都完整的过了一遍。现在,即使原始脚本文件已经被删除,你也能够从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 } 
    } 
}

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK