PowerShell 技能连载 - 取证事件日志分析(第 2 部分)

在上一个技能中,我们查看了 Get-EventLog 以进行取证分析并在应用程序日志中查找与搜索相关的错误。Get-EventLog 使用简单,但速度慢且已弃用。虽然在 Windows PowerShell 上使用 Get-EventLog 是完全可以的,但您可能希望改用 Get-WinEvent。它速度更快,也可以在 PowerShell 7 上运行。

让我们快速将 Get-EventLog 转换为 Get-WinEvent,以进行上一技巧中介绍的取证分析。下面的代码在应用程序事件日志中查找与“搜索”源相关的所有错误(您的系统上可能没有):

1
2
3
4
5
6
7
8
9
# old
Get-EventLog -LogName Application -Source *search* -EntryType error -Newest 10 | Select-Object TimeGenerated, Message

# new
Get-WinEvent -FilterHashtable @{
LogName = 'Application'
ProviderName = '*search*'
Level = 1,2
} -ErrorAction Ignore | Select-Object TimeCreated, Message

要将每天的事件分组,请使用 Group-Object 和日期作为分组标准:

1
2
3
4
5
6
7
8
9
10
# old
Get-EventLog -LogName Application -Source *search* -EntryType error | Group-Object { Get-Date $_.timegenerated -format yyyy-MM-dd } -NoElement


# new
Get-WinEvent -FilterHashtable @{
LogName = 'Application'
ProviderName = '*search*'
Level = 1,2
} -ErrorAction Ignore | Group-Object { Get-Date $_.TimeCreated -format yyyy-MM-dd } -NoElement

同样,您的日志中可能没有任何与搜索相关的错误条目,但是当您调整条件并搜索不同的事件日志条目时,您会很快意识到 Get-WinEvent 的速度有多快。在上面的示例中,Get-WinEventGet-EventLog 快大约 10 倍。

PowerShell 技能连载 - 取证事件日志分析(第 1 部分)

事件日志记录 Windows 几乎所有方面的信息,因此如果出现问题或停止按预期工作,最好将事件日志取证策略包含在故障排除中。

例如,一些用户报告说他们的 Windows“即时搜索”停止查找更新的电子邮件项目。为什么索引服务不再随 Outlook 更新?

那时阅读事件日志会变得非常重要(并且很有帮助)。下面这行代码能快速找出您是否有系统索引问题。它在“应用程序”日志中搜索与“搜索”相关的任何错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
PS> Get-EventLog -LogName Application -Source *search* -EntryType error -Newest 10 |
Select-Object TimeGenerated, Message

TimeGenerated Message
------------- -------
21.05.2021 09:55:48 The protocol handler Mapi16 cannot be loaded. Error description: (HRES...
21.05.2021 09:48:03 The protocol handler Mapi16 cannot be loaded. Error description: (HRES...
21.05.2021 08:55:14 The protocol handler Mapi16 cannot be loaded. Error description: (HRES...
21.05.2021 08:47:53 The protocol handler Mapi16 cannot be loaded. Error description: (HRES...
21.05.2021 08:32:15 The protocol handler Mapi16 cannot be loaded. Error description: (HRES...
21.05.2021 08:28:41 The protocol handler Mapi16 cannot be loaded. Error description: (HRES...
21.05.2021 08:26:18 The protocol handler Mapi16 cannot be loaded. Error description: (HRES...
20.05.2021 18:14:48 The protocol handler Mapi16 cannot be loaded. Error description: (HRES...
20.05.2021 12:55:06 The protocol handler Mapi16 cannot be loaded. Error description: (HRES...
20.05.2021 11:41:06 The protocol handler Mapi16 cannot be loaded. Error description: (HRES...

最明显的是,在这个例子中,Mapi16 协议处理程序似乎存在重复的系统问题,阻止索引服务读取 Outlook 电子邮件。

要了解问题何时发生以及它是否仍然令人担忧,您可以将事件日志条目分组并显示它们的频率:

1
2
PS> Get-EventLog -LogName Application -Source *search* -EntryType error |
Group-Object { Get-Date $_.timegenerated -format yyyy-MM-dd } -NoElement

本示例中的 Group-Object 使用脚本块来计算分组标准:在 同一天 发生的任何错误事件都被放入同一组中,该组返回一个时间顺序协议。这是示例输出:

Count Name
----- ----
    7 2021-05-21
    6 2021-05-20
   29 2021-05-19
   29 2021-05-18
   16 2021-05-17
    5 2021-05-16
    2 2021-05-15
    8 2021-05-14
    2 2021-05-13
    3 2021-05-12
    9 2021-05-11
   13 2021-05-10
    1 2021-05-09
    3 2021-05-08
    7 2021-05-07
   10 2021-05-06
   15 2021-05-05
    8 2021-05-04
   24 2021-05-03
   22 2021-05-02
   10 2021-05-01
    2 2021-04-30

输出清楚地表明该问题始于 4 月 30 日,一直持续到 5 月 21 日,当时它显然已得到修复。

显然,这些示例不会在您的机器上产生相同的结果(除非您遇到相同的问题)。它们确实展示了事件日志信息的价值以及 PowerShell 可以多么轻松地帮助对数据进行取证检查。

PowerShell 技能连载 - 生日派对的琐事

假设您受邀参加一位朋友的 37 岁生日。你可以在生日贺卡上放什么?试试这个:

1
2
PS> Invoke-RestMethod -Uri http://numbersapi.com/37 -UseBasicParsing
37 is the number of plays William Shakespeare is thought to have written (counting Henry IV as three parts).

只需将 URL 中的数字替换为您需要的任何实际数字。这是一个很好的例子,说明使用 PowerShell 使用 REST Web 服务是多么容易。

Invoke-RestMethod 始终是更聪明的选择。有时示例使用 Invoke-WebRequest 代替。对于后者,您需要从接收到的值中手动提取信息并将其转换为正确的格式。这就是 Invoke-RestMethod 自动做的:

1
2
PS> (Invoke-WebRequest -Uri http://numbersapi.com/37 -UseBasicParsing).Content
37 is the cost in cents of the Whopper Sandwich when Burger King first introduced it in 1957.

PowerShell 技能连载 - 拆分而不丢失字符

拆分文本时,通常会丢失拆分字符。这就是为什么这个例子中的反斜杠丢失的原因:

1
2
3
4
PS> 'c:\test\file.txt' -split '\\'
c:
test
file.txt

重要提示:请注意 -split 运算符需要一个正则表达式。如果你想在反斜杠处拆分,因为反斜杠是正则表达式中的一个特殊字符,你需要对它进行转义。以下调用告诉您需要转义的内容:提交要使用的文字文本。结果是转义的正则表达式文本:

1
2
PS> [regex]::Escape('\')
\\

如果你想拆分而不丢失任何字符,你可以使用所谓的先行断言和后行断言。以下代码在一个反斜杠分割(不删除它):

1
2
3
4
PS> 'c:\test\file.txt' -split '(?<=\\)'
c:\
test\
file.txt

以下代码在每个反斜杠之前拆分:

1
2
3
4
PS> 'c:\test\file.txt' -split '(?=\\)'
c:
\test
\file.txt

PowerShell 技能连载 - 排序技巧(第 4 部分)

在上一个技能中,我们展示了 Sort-Object 通过属性名称、哈希表或普通脚本块来排序,并且我们使用脚本块来控制排序算法,并按日期和时间而不是字母数字对字符串信息进行排序。

在这最后一个示例中,让我们使用它来对 IPv4 地址进行排序。默认情况下,Sort-Object 将它们视为纯文本并使用字母排序:

1
2
3
4
PS> '10.12.11.1', '298.12.11.112', '8.8.8.8' | Sort-Object
10.12.11.1
298.12.11.112
8.8.8.8

要正确排序 IPv4 地址,您可以将它们转换为 [version] 类型,该类型也包含四个数字:

1
2
3
4
5
PS> '10.12.11.1', '298.12.11.112', '8.8.8.8' | Sort-Object -Property { $_ -as [version] }

8.8.8.8
10.12.11.1
298.12.11.112

PowerShell 技能连载 - 排序技巧(第 3 部分)

在上一个技能中,我们展示了 Sort-Object 接受属性名称、哈希表或普通脚本块来对事物进行排序。让我们看看为什么将脚本块提交给 Sort-Object 是个好主意。

假设您有一堆表示日期的字符串数据,并且您想对它们进行排序:

1
2
3
4
5
PS> 'May 12, 2020', 'Feb 1, 1999', 'June 12, 2021' | Sort-Object

Feb 1, 1999
June 12, 2021
May 12, 2020

结果已排序,但不按日期排序。由于输入是字符串,Sort-Object 使用字母数字排序算法。您现在可以将原始数据转换为另一种格式并对其进行排序。

但是,您也可以要求 Sort-Object 进行转换。不同之处在于原始数据格式保持不变:一系列字符串将按日期排序,但仍然是字符串:

1
2
3
4
5
6
PS> 'May 12, 2020', 'Feb 1, 1999', 'June 12, 2021' |
Sort-Object -Property { [DateTime]$_ }

Feb 1, 1999
May 12, 2020
June 12, 2021

当然,您的工作是想出一个脚本块,将 $_ 中传入的原始数据正确转换为您想要用于排序的所需类型。对于日期,您可能还想查看使用本地日期和时间格式的 -as 运算符,而直接转换始终使用美国格式:

1
2
3
4
5
PS> 'May 12, 2020', 'Feb 1, 1999', 'June 12, 2021' | Sort-Object -Property { $_ -as [DateTime] }

Feb 1, 1999
May 12, 2020
June 12, 2021

PowerShell 技能连载 - 排序技巧(第 2 部分)

在上一个技能中,我们展示了 Sort-Object 如何对多个属性进行排序,以及如何使用哈希表分别控制每个属性的排序方向。不过,哈希表还可以做更多的事情。

例如,哈希表键 “Expression“ 可以是一个脚本块,然后针对您要排序的每个项目执行该脚本块。脚本块的结果决定了排序顺序。

这就是为什么这行代码每次都会以不同的方式重新排列数字列表:

1
1..10 | Sort-Object -Property @{Expression={ Get-Random }}

本质上,本示例使用 Get-Random 的随机结果对数字列表进行随机排序。这可能很有用,即当您使用密码生成器并希望随机分配脚本计算的字符时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# compose password out of these
$Capitals = 2
$Numbers = 1
$lowerCase = 3
$Special = 1

# collect random chars from different lists in $chars
$chars = & {
'ABCDEFGHKLMNPRSTUVWXYZ'.ToCharArray() | Get-Random -Count $Capitals
'23456789'.ToCharArray() | Get-Random -Count $Numbers
'abcdefghkmnprstuvwxyz'.ToCharArray() | Get-Random -Count $lowerCase
'!§$%&?=#*+-'.ToCharArray() | Get-Random -Count $Special
} | # <- don't forget pipeline symbol!
# sort them randomly
Sort-Object -Property { Get-Random }

# convert chars to one string
$password = -join $chars
$password

正如您在实际示例中看到的,Sort-Object 还接受一个简单的脚本块,它表示哈希表的 “Expression“ 键。两行的工作方式相同(但当然会产生不同的随机结果):

1
1..10 | Sort-Object -Property @{Expression={ Get-Random }}1..10 | Sort-Object -Property { Get-Random }

PowerShell 技能连载 - 排序技巧(第 1 部分)

Sort-Object 是用于排序的 cmdlet:只需指定要排序的属性,Sort-Object 涵盖其余部分,包括根据属性数据类型选择正确的排序算法:

1
Get-Service | Sort-Object -Property DisplayName -Descending

一个鲜为人知的事实是 Sort-Object 也接受哈希表,这给了你更多的控制权。例如,您可以像这样轻松地对多个属性进行排序:

1
Get-Service | Sort-Object -Property Status, DisplayName -Descending

但是,如果您想控制每个属性的排序方向,则需要一个哈希表。此示例按降序对状态进行排序,但按升序对显示名称进行排序:

1
2
3
4
5
6
$displayName = @{
Expression = "DisplayName"
Descending = $false
}

Get-Service | Sort-Object -Property Status, $displayName -Descending

PowerShell 技能连载 - 对文本做哈希

PowerShell 附带了 Get-FileHash 命令,它读取文件并计算唯一的哈希值。这非常适合测试文件是否具有相同的内容。但是,无法对纯文本进行哈希处理。

当然,您可以将要哈希的文本写入文件,然后使用 Get-FileHash

更好的方法是 Get-FileHash 的一个很少人知道的功能。除了传入文件路径之外,您还可以提交所谓的 “MemoryStream“,当您将文本加载内存流,再调用命令,就能得到哈希值:

1
2
3
4
5
6
7
8
9
10
11
$text = 'this is a test'

$memoryStream = [System.IO.MemoryStream]::new()
$streamWriter = [System.IO.StreamWriter]::new($MemoryStream)
$streamWriter.Write($text)
$streamWriter.Flush()
$memoryStream.Position = 0
$hash = Get-FileHash -InputStream $MemoryStream -Algorithm 'SHA1'
$memoryStream.Dispose()
$streamWriter.Dispose()
$hash.Hash

只是不要忘记在使用后处理 MemoryStreamStreamWriter 对象以释放内存。

PowerShell 技能连载 - 检测 Windows 版本(第 3 部分)

在上一个技巧中,我们说明访问 WinRT 类 AnalyticsInfo 似乎是读取当前 Windows 10 版本的唯一受支持方式。与使用上一个示例中的异步方法不同,为了仅获取当前的 Windows 10 版本,这里有一个更简单的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# get raw Windows version
[int64]$rawVersion =
[Windows.System.Profile.AnalyticsInfo,Windows.System.Profile,ContentType=WindowsRuntime].
GetMember('get_VersionInfo').Invoke( $Null, $Null ).DeviceFamilyVersion

# decode bits to version bytes
$major = ( $rawVersion -band 0xFFFF000000000000l ) -shr 48
$minor = ( $rawVersion -band 0x0000FFFF00000000l ) -shr 32
$build = ( $rawVersion -band 0x00000000FFFF0000l ) -shr 16
$revision = $rawVersion -band 0x000000000000FFFFl

# compose version
$winver = [System.Version]::new($major, $minor, $build, $revision)
$winver

请注意,PowerShell 7 (pwsh.exe) 无法访问此 API。该代码需要 Windows PowerShell (powershell.exe)。