PowerShell 技能连载 - 理解脚本块日志(第 2 部分)

这是关于 PowerShell 脚本块日志的迷你系列的第 2 部分。今天,我们将会再次读取脚本块日志的记录。但是这次我们将以更加面向对象的方式读取日志数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
function Get-LoggedCode
{
# read all raw events
$logInfo = @{ ProviderName="Microsoft-Windows-PowerShell"; Id = 4104 }
Get-WinEvent -FilterHashtable $logInfo |
# take each raw set of data...
ForEach-Object {
# create a new object and extract the interesting
# parts from the raw data to compose a "cooked"
# object with useful data
[PSCustomObject]@{
# when this was logged
Time = $_.TimeCreated
# script code that was logged
Code = $_.Properties[2].Value
# if code was split into multiple log entries,
# determine current and total part
PartCurrent = $_.Properties[0].Value
PartTotal = $_.Properties[1].Value

# if total part is 1, code is not fragmented
IsMultiPart = $_.Properties[1].Value -ne 1
# path of script file (this is empty for interactive
# commands)
Path = $_.Properties[4].Value
# log level
# by default, only level "Warning" will be logged
Level = $_.LevelDisplayName
# user who executed the code (SID)
User = $_.UserId
}
}
}

当您运行这段代码时,您将得到一个新的名为 Get-LoggedCode 的命令。当您执行它时,它将返回类似这样的对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
Time        : 25.05.2018 10:57:36
Code : function Get-LoggedCode
{
# read all raw events
$logInfo = @{ ProviderName="Microsoft-Windows-PowerShell"; Id = 4104 }
Get-WinEvent -FilterHashtable $logInfo |
# take each raw set of data...
ForEach-Object {
# create a new object and extract the interesting
# parts from the raw data to compose a "cooked"
# object with useful data:
[PSCustomObject]@{
# when this was logged:
Time = $_.TimeCreated
# script code that was logged:
Code = $_.Properties[2].Value
# if code was split into multiple log entries,
# determine current and total part:
PartCurrent = $_.Properties[0].Value
PartTotal = $_.Properties[1].Value

# if total part is 1, code is not fragmented:
IsMultiPart = $_.Properties[1].Value -ne 1
# path of script file (this is empty for interactive
# commands)
Path = $_.Properties[4].Value
# log level
# by default, only level "Warning" will be logged:
Level = $_.LevelDisplayName
# user who executed the code (SID)
User = $_.UserId
}
}
}



PartCurrent : 1
PartTotal : 1
IsMultiPart : False
Path : D:\sample.ps1
Level : Warning
User : S-1-5-21-2012478179-265285931-690539891-1001

在我们的代码中,我们添加了 Select-Object 来读取整个日志,而不是最后一条日志。这里,我们得到我们刚刚执行的代码。您机器上的执行情况可能有所不同,原因如下:

缺省情况下,脚本快日志只记录“安全相关”(在返回的数据中,级别为 “Warning”)的代码。PowerShell 内部判断哪些代码是和安全相关的。在明天的技能中,我们将会介绍如何启用 “Verbose” 模式。当该模式打开时,所有代码都会被记录,所以日志文件的体积会增长得很快。以下是缺省设置 (“Warning”) 日志和详细日志的数量对比(从测试机器中提取):

1
2
3
4
5
6
PS> Get-LoggedCode | Group-Object Level

Count Name Group
----- ---- -----
549 Verbose {@{Time=25.05.2018 10:57:52; Code=prompt;..
36 Warning {@{Time=25.05.2018 10:57:52; Code={...

请注意:由于日志的体积非常大,所以长的代码被分成多块。”IsMultiPart“、”PartCurrent“ 和 “PartTotal“ 属性可以提供这方面的有用信息。

PowerShell 技能连载 - 理解脚本块日志(第 1 部分)

从 PowerShell 5 开始,PowerShell 引擎开始记录执行的命令和脚本块。缺省情况下,只有被认为有潜在威胁的命令会记录日志。当启用了详细日志后,由所有用户执行的所有执行代码都会被记录。

这是一个介绍脚本块日志的迷你系列的第一部分。今天,我们只是学习以最基础的方式使用脚本块日志。一行代码就够了:

1
2
3
4
$logInfo = @{ ProviderName="Microsoft-Windows-PowerShell"; Id = 4104 }

Get-WinEvent -FilterHashtable $logInfo |
Select-Object -ExpandProperty Message

这将从 “Microsoft-Windows-PowerShell” 的日志(它包含代码日志)中读取所有 ID 为 4104 的事件。请注意 PowerShell Core 也记录日志,但是使用的是一个不同的日志文件。

您现在可以类似这样获取大量的数据:

Creating Scriptblock text (1 of 1):
Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy Bypass

ScriptBlock ID: aeb85bcb-98be-42d0-b695-fbbb975ec5d2
Path:

如果 Path 为空,则说明该命令以交互的方式执行。否则,在这里可以查看到脚本的路径。

如果您没有获取到任何信息,请考虑以下可能性:

  • 您是否使用的是 Windows PowerShell?如果您使用的是 PowerShell Core,您需要调整日志文件名。
  • 缺省情况下,脚本块日志仅记录和安全有关的代码。如果您没有涉及到这方面的代码,可能不会收到任何记录。

以下代码是和安全相关的,当您执行它时,您将会从后续的日志中读取到这行代码:

1
PS> Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass -Force

PowerShell 技能连载 - 添加前导零

如果您需要数字前面添加前导零,例如对于服务器名,以下是两种实现方式。第一,您可以将数字转换为字符串,然后用 PadLeft() 函数将字符串填充到指定的长度:

1
2
3
4
5
6
7
8
9
10
11
12
$number = 76
$leadingZeroes = 8

$number.Tostring().PadLeft($leadingZeroes, '0')

Or, you can use the -f operator:


$number = 76
$leadingZeroes = 8

"{0:d$leadingZeroes}" -f $number

PowerShell 技能连载 - 显示消息框

如果您希望显示一个带有按钮,可供用户点击的消息框,请试试这个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
function Show-MessageBox
{
[CmdletBinding()]
param
(
[Parameter(Mandatory=$true,ValueFromPipeline=$false)]
[String]
$Text,

[Parameter(Mandatory=$true,ValueFromPipeline=$false)]
[String]
$Caption,

[Parameter(Mandatory=$true,ValueFromPipeline=$false)]
[Windows.MessageBoxButton]
$Button,

[Parameter(Mandatory=$true,ValueFromPipeline=$false)]
[Windows.MessageBoxImage]
$Icon

)

process
{
try
{
[System.Windows.MessageBox]::Show($Text, $Caption, $Button, $Icon)
}
catch
{
Write-Warning "Error occured: $_"
}
}
}

以下是它的使用方法:

1
PS> Show-MessageBox -Text 'Do you want to reboot now?' -Caption Reboot -Button YesNoCancel -Icon Exclamatio

PowerShell 技能连载 - 显示输入框

如果您想弹出一个快速而粗糙的输入框,提示用户输入数据,您可以通过 Microsoft Visual Basic 并且“借用”它的 InputBox 控件:

1
2
3
Add-Type -AssemblyName Microsoft.VisualBasic
$result = [Microsoft.VisualBasic.Interaction]::InputBox("Enter your Name", "Name", $env:username)
$result

但是请注意,这种方法有一些局限性:该输入框可能在您的 PowerShell 窗口之下打开,而且在高分辨率屏中,它的缩放可能不正确。

PowerShell 技能连载 - 快速读取文本文件

PowerShell 有许多读取文本文件的方法,它们的性能差异很大。请自己确认一下。以下例子演示了不同的实践,并测量执行的时间。请确保例子中的路径实际存在。如果文件不存在,请选择一个大文件来测试。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# make sure this file exists, or else
# pick a different text file that is
# very large
$path = 'C:\Windows\Logs\DISM\dism.log'

# slow reading line-by-line
Measure-Command {
$text = Get-Content -Path $Path
}

# fast reading entire text as one large string
Measure-Command {
$text = Get-Content -Path $Path -Raw
}

# fast reading text as string array with one
# array element per line
Measure-Command {
$text = Get-Content -Path $Path -ReadCount 0
}

# reading entire text with .NET
# no advantage over -Raw
Measure-Command {
$text = [System.IO.File]::ReadAllText($path)
}

PowerShell 技能连载 - 从本地时间以 ISO 格式创建 UTC 时间

如果您的工作是跨国跨时区的,您可能会需要使用 UTC 时间来代替本地时间。要确保时间格式是语言中性的,我们推荐使用 ISO 格式。以下是使用方法:

1
2
$date = Get-Date
$date.ToUniversalTime().ToString('yyyy-MM-dd HH:mm:ss')

PowerShell 技能连载 - 巧妙地读取事件日志(第 2 部分)

在前一个技能中我们演示了如何使用 ReplacementStrings 读取从 Get-EventLog 中收到的详细的事件日志信息。它工作得很完美,但是 Get-EventLog 职能读取“传统的”Windows 日志。在现代的 Windows 版本中还有许多额外的日志。

这些日志可以通过 Get-WinEvent 读取,而且有许多信息可以发掘。例如,要获取已安装的更新列表,请试试这段代码:

1
2
3
$filter = @{ ProviderName="Microsoft-Windows-WindowsUpdateClient"; Id=19 }

Get-WinEvent -FilterHashtable $filter | Select-Object -ExpandProperty Message -First 4

请注意这只是一个例子。通过以上代码,您可以查询您关心的任意事件 ID 的日志。例如以上代码,可以获取最新安装的 4 条更新:

1
2
3
4
5
6
7
8
9
PS> . 'C:\Users\tobwe\Documents\PowerShell\Untitled5.ps1' <# script is not saved yet #>
Installation Successful: Windows successfully installed the following update: Definitionsupdate für
Windows Defender Antivirus – KB2267602 (Definition 1.269.69.0)
Installation Successful: Windows successfully installed the following update: 9WZDNCRFJ1XX-FITBIT.F
ITBIT
Installation Successful: Windows successfully installed the following update: Definitionsupdate für
Windows Defender Antivirus – KB2267602 (Definition 1.269.28.0)
Installation Successful: Windows successfully installed the following update: 9WZDNCRFHVQM-MICROSOF
T.WINDOWSCOMMUNICATIONSAPPS

然而,这只是文本,而且要将它转换为一份已安装的更新的漂亮的报告并不容易。通过 Get-EventLog,类似我们之前的技能介绍的,您可以使用 ReplacementStrings 来方便地存取纯净的信息。但是 Get-WinEvent 没有 ReplacementStrings

然而,有一个名为 Properties 的属性。以下是如何将属性转换为类似 ReplacementStrings 的数组的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$filter = @{ ProviderName="Microsoft-Windows-WindowsUpdateClient"; Id=19 }

Get-WinEvent -FilterHashtable $filter |
ForEach-Object {
# create a ReplacementStrings array
# this array holds the information that is inserted
# into the event message template text
$ReplacementStrings = $_.Properties | ForEach-Object { $_.Value }

# return a new object with the required information
[PSCustomObject]@{
Time = $_.TimeCreated
# index 0 contains the name of the update
Name = $ReplacementStrings[0]
User = $_.UserId.Value
}
}

这段代码返回以安装更新的美观的列表:

Time                Name
----                ----
25.05.2018 09:00:20 Definitionsupdate für Windows Defender Antivirus – KB2267602 (Definition 1....
25.05.2018 07:59:44 9WZDNCRFJ1XX-FITBIT.FITBIT
24.05.2018 11:04:15 Definitionsupdate für Windows Defender Antivirus – KB2267602 (Definition 1....
24.05.2018 08:36:26 9WZDNCRFHVQM-MICROSOFT.WINDOWSCOMMUNICATIONSAPPS
24.05.2018 08:34:30 9N4WGH0Z6VHQ-Microsoft.HEVCVideoExtension
24.05.2018 08:34:24 9WZDNCRFJ2QK-ZDFGemeinntzigeAnstaltdes.ZDFmediathek
23.05.2018 11:57:42 Definitionsupdate für Windows Defender Antivirus – KB2267602 (Definition 1....
23.05.2018 07:37:11 9WZDNCRFHVQM-MICROSOFT.WINDOWSCOMMUNICATIONSAPPS
23.05.2018 07:36:57 9WZDNCRFJ3PT-MICROSOFT.ZUNEMUSIC
23.05.2018 04:01:11 Definitionsupdate für Windows Defender Antivirus – KB2267602 (Definition 1....
22.05.2018 12:26:55 Definitionsupdate für Windows Defender Antivirus – KB2267602 (Definition 1....
22.05.2018 08:34:28 9NBLGGH5FV99-Microsoft.MSPaint
22.05.2018 08:33:25 9WZDNCRFJ364-MICROSOFT.SKYPEAPP

PowerShell 技能连载 - 巧妙地读取事件日志(第 1 部分)

当您使用 PowerShell 来查询事件,缺省情况下获取到的是日志信息的文本消息。例如,如果您想知道谁登录了您的计算机,您可以使用类似这样的代码(需要管理员权限):

1
2
Get-EventLog -LogName Security -InstanceId 4624 |
Select-Object -Property TimeGenerated, Message

结果大概类似这样:

25.04.2018 07:48:41 An account was successfully logged on....
25.04.2018 07:48:40 An account was successfully logged on....
24.04.2018 18:18:17 An account was successfully logged on....
...

这并不是很直观,因为 PowerShell 缩短了输出内容。在类似这样的情况下,您可能需要将结果用管道传给 Format-List

1
2
3
Get-EventLog -LogName Security -InstanceId 4624 |
Select-Object -Property TimeGenerated, Message |
Format-List

它现在可以生成详细的数据了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
PS> Get-EventLog -LogName Security -InstanceId 4624 |
Select-Object -Property TimeGenerated, Message -first 1 |
Format-List




TimeGenerated : 25.05.2018 11:39:29
Message : An account was successfully logged on.

Subject:
Security ID: S-1-5-18
Account Name: DESKTOP-7AAMJLF$
Account Domain: WORKGROUP
Logon ID: 0x3e7

Logon Information:
Logon Type: 5
Restricted Admin Mode: -
Virtual Account: %%1843
Elevated Token: %%1842

Impersonation Level: %%1833

New Logon:
Security ID: S-1-5-18
Account Name: SYSTEM
Account Domain: NT-AUTORITÄT
Logon ID: 0x3e7
Linked Logon ID: 0x0
Network Account Name: -
Network Account Domain: -
Logon GUID: {00000000-0000-0000-0000-000000000000}

Process Information:
Process ID: 0x328
Process Name: C:\Windows\System32\services.exe

Network Information:
Workstation Name: -
Source Network Address: -
Source Port: -

Detailed Authentication Information:
Logon Process: Advapi
Authentication Package: Negotiate
Transited Services: -
Package Name (NTLM only): -
Key Length: 0

This event is generated when a logon session is created. It is
generated on the computer that was accessed.

The subject fields indicate the account on the local system
which requested the logon. This is most commonly a service
such as the Server service, or a local process such as
Winlogon.exe or Services.exe.

The logon type field indicates the kind of logon that
occurred. The most common types are 2 (interactive) and 3
(network).

The New Logon fields indicate the account for whom the new
logon was created, i.e. the account that was logged on.

The network fields indicate where a remote logon request
originated. Workstation name is not always available and may
be left blank in some cases.

The impersonation level field indicates the extent to which a
process in the logon session can impersonate.

The authentication information fields provide detailed
information about this specific logon request.
- Logon GUID is a unique identifier that can be used to
correlate this event with a KDC event.
- Transited services indicate which intermediate services
have participated in this logon request.
- Package name indicates which sub-protocol was used among
the NTLM protocols.
- Key length indicates the length of the generated session
key. This will be 0 if no session key was requested.

这个结果很难处理。如果您希望基于这段文本做一些自动化处理,您需要解析这段文本。

有一个简单得多的方法:您见到的消息只是一个文本模板,Windows 以“替换字符串”的方式插入相关的信息。他们是从 Get-0EventLog 接收到的事件数据的一部分。该数据存在一个数组中,整个数组对应一个事件 ID 的信息。

当您确定了哪个信息存放在哪个数组元素中,要解析出您关心的信息十分容易:

1
2
3
4
5
6
7
8
9
10
Get-EventLog -LogName Security -InstanceId 4624 |
ForEach-Object {
# translate the raw data into a new object
[PSCustomObject]@{
Time = $_.TimeGenerated
User = "{0}\{1}" -f $_.ReplacementStrings[5], $_.ReplacementStrings[6]
Type = $_.ReplacementStrings[10]
Path = $_.ReplacementStrings[17]
}
}

当您运行这一小段代码时,它返回只包含您需要的、美观的验证信息:

12.05.2018 17:38:58 SYSTEM\NT-AUTORITÄT                     Negotiate C:\Windows\System32\services.exe
12.05.2018 17:38:58 tobweltner@zumsel.local\InternalAccount Negotiate C:\Windows\System32\svchost.exe
12.05.2018 17:38:58 SYSTEM\NT-AUTORITÄT                     Negotiate C:\Windows\System32\services.exe
12.05.2018 17:38:58 SYSTEM\NT-AUTORITÄT                     Negotiate C:\Windows\System32\services.exe
12.05.2018 17:38:53 SYSTEM\NT-AUTORITÄT                     Negotiate C:\Windows\System32\services.exe

PowerShell 技能连载 - 理解类型加速器(第 2 部分)

PowerShell 带来了一系列硬编码的类型加速器,它们的效果就像通常使用的 .NET 类型,而且由于它们比原始数据类型名称短很多,所以它们“加快了打字速度”。

一个很少人知道的事实是类型加速器列表是可以扩展的。以下代码添加一个新的名为 “SuperArray” 的类型加速器,它指向 “System.Collections.ArrayList”。

您现在可以创建一个新的“超级数组”(它用起来像普通的数组,但是拥有一系列额外的方法来向指定的位置增删元素,而且附加数组元素也比普通的数组快得多):

1
2
3
[PSObject].Assembly.GetType('System.Management.Automation.TypeAccelerators')::Add('SuperArray', [System.Collections.ArrayList])

$a = [superarray]::new()

您还可以将一个普通数组转换成“超级数组”:

1
2
3
4
5
6
7
PS> $a = [superarray](1,2,3)

PS> $a.RemoveAt(1)

PS> $a
1
3

请注意虽然不用类型加速器也可以完成这项任务。但是得敲这么长的代码:

1
2
3
4
5
6
7
PS> $a = [System.Collections.ArrayList](1,2,3)

PS> $a.RemoveAt(1)

PS> $a
1
3