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

这是关于 PowerShell 脚本块日志的迷你系列的第 6 部分。该是时候介绍最后的部分了:当您执行一段非常大的 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
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
function Get-LoggedCode
{
# to speed up SID-to-user translation,
# we use a hash table with already translated SIDs
# it is empty at first
$translateTable = @{}

# read all raw events
$logInfo = @{ ProviderName="Microsoft-Windows-PowerShell"; Id = 4104 }
Get-WinEvent -FilterHashtable $logInfo |
# take each raw set of data...
ForEach-Object {
# store the code in this entry

# if this is the first part, take it
if ($_.Properties[0].Value -eq 1)
{
$code = $_.Properties[2].Value
}
# else, add it to the $code variable
else
{
$code += $_.Properties[2].Value
}

# return the object when all parts have been processed
if ($_.Properties[0].Value -eq $_.Properties[1].Value)
{
# turn SID into user
$userSID = $_.UserId

# if the cache does not contain the user SID yet...
if (!$translateTable.ContainsKey($userSid))
{
try
{
# ...try and turn it into a real name, and add it
# to the cache
$identifier = New-Object System.Security.Principal.SecurityIdentifier($userSID)
$result = $identifier.Translate( [System.Security.Principal.NTAccount]).Value
$translateTable[$userSid] = $result
}
catch
{
# if this fails, use the SID instead of a real name
$translateTable[$userSid] = $userSID
}
}
else
{

}

# 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 = $code
# 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
# take the real user name from the cache of translated
# user names
User = $translateTable[$userSID]
}
}
}
}

本质上,这个函数检查该脚本是否是由多段组成。如果是,它将代码添加到 $code 中,直到当前块等于 最后一块。就是这么简单。

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

这是关于 PowerShell 脚本块日志的迷你系列的第 5 部分。我们已经几乎达到目标了,缺少的是一个更好的方式来读取记录的代码。在我们之前的方法中,执行代码的用户收到一个晦涩的 SID 而不是一个清晰的名称。以下是一个将用户 SID 转为真实名称的函数,并且使用智能缓存来加速 SID 的查询过程:

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
function Get-LoggedCode
{
# to speed up SID-to-user translation,
# we use a hash table with already translated SIDs
# it is empty at first
$translateTable = @{}

# read all raw events
$logInfo = @{ ProviderName="Microsoft-Windows-PowerShell"; Id = 4104 }
Get-WinEvent -FilterHashtable $logInfo |
# take each raw set of data...
ForEach-Object {
# turn SID into user
$userSID = $_.UserId

# if the cache does not contain the user SID yet...
if (!$translateTable.ContainsKey($userSid))
{
try
{
# ...try and turn it into a real name, and add it
# to the cache
$identifier = New-Object System.Security.Principal.SecurityIdentifier($userSID)
$result = $identifier.Translate( [System.Security.Principal.NTAccount]).Value
$translateTable[$userSid] = $result
}
catch
{
# if this fails, use the SID instead of a real name
$translateTable[$userSid] = $userSID
}
}
else
{

}

# 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
# take the real user name from the cache of translated
# user names
User = $translateTable[$userSID]
}
}
}

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

这是关于 PowerShell 脚本块日志的迷你系列的第 4 部分。到目前为止,您已经了解了如何读取记录的 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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
function Set-SBLLogSize
{
<#
.SYNOPSIS
Sets a new size for the script block logging log.
Administrator privileges required.

.DESCRIPTION
By default, the script block log has a maximum size of 15MB
which may be too small to capture and log PowerShell activity
over a given period of time. With this command,
you can assign more memory to the log.

.PARAMETER MaxSizeMB
New log size in Megabyte

.EXAMPLE
Set-SBLLogSize -MaxSizeMB 100
Sets the maximum log size to 100MB.
Administrator privileges required.
#>


param
(
[Parameter(Mandatory)]
[ValidateRange(15,3000)]
[int]
$MaxSizeMB
)

$Path = "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\WINEVT\Channels\Microsoft-Windows-PowerShell/Operational"
try
{
$ErrorActionPreference = 'Stop'
Set-ItemProperty -Path $Path -Name MaxSize -Value ($MaxSizeMB * 1MB)
}
catch
{
Write-Warning "Administrator privileges required. Run this command from an elevated PowerShell."
}
}

要将日志文件的尺寸从缺省的 15MB 增加到 100MB,请运行以下代码(需要管理员特权):

1
PS> Set-SBLLogSize -MaxSizeMB 100

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

这是关于 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
34
35
function Enable-VerboseLogging
{
<#
.SYNOPSIS
Enables verbose script block logging.
Requires Administrator privileges.

.DESCRIPTION
Turns script block logging on. Any code that is sent to
PowerShell will be logged.

.EXAMPLE
Enable-VerboseLogging
Enables script block logging.
Administrator privileges required.
#>


$path = "Registry::HKLM\Software\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging"
$exists = Test-Path -Path $path
try
{

$ErrorActionPreference = 'Stop'
if (!$exists) { $null = New-Item -Path $path -Force }

Set-ItemProperty -Path $path -Name EnableScriptBlockLogging -Type DWord -Value 1
Set-ItemProperty -Path $path -Name EnableScriptBlockInvocationLogging -Type DWord -Value 1

}
catch
{
Write-Warning "Administrator privileges required. Run this command from an elevated PowerShell."
}
}

当您运行 Enable-VerboseLogging 之后,所有 PowerShell 代码将会记录到日志中。您可以使用我们之前介绍的方式之一来读取记录的代码,例如我们的 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
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
}
}
}

请注意只有改变日志设置需要管理员特权。而所有用户都可以读取记录的数据。

如果您希望禁止详细模式并且返回到缺省的设置,请使用这个函数:

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
}
}
}

请注意即便详细脚本日志被关闭,PowerShell 将仍会记录和安全相关的代码。

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)
}