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

在之前的技能中,我们报告了 ReleaseId 已弃用,无法再用于正确识别当前的 Windows 10 版本。相反,应该使用 DisplayVersion

1
2
PS> (Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion').DisplayVersion
20H2

但是,DisplayVersion 也不是确定当前 Windows 10 版本的可靠方法,因为其最初的目的是使外壳能够向用户报告当前版本。它也可能在未来发生变化。

识别当前 Windows 10 版本的唯一受支持的安全方法是使用名为 AnalyticsInfo 的操作系统类。不过用起来比较复杂,因为该类在 WinRT 中异步运行。PowerShell 7 (pwsh.exe) 无法访问此类。但是,内置的 Windows PowerShell (powershell.exe) 可以创建包装器并返回信息:

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
# load WinRT and runtime types
[System.Void][Windows.System.Profile.AnalyticsInfo,Windows.System.Profile,ContentType=WindowsRuntime]
Add-Type -AssemblyName 'System.Runtime.WindowsRuntime'

# define call and information to query
[Collections.Generic.List[System.String]]$names = 'DeviceFamily',
'OSVersionFull',
'FlightRing',
'App',
'AppVer'

$task = [Windows.System.Profile.AnalyticsInfo]::GetSystemPropertiesAsync($names)

# use reflection to find method definition
$definition = [System.WindowsRuntimeSystemExtensions].GetMethods().Where{
$_.Name -eq 'AsTask' -and
$_.GetParameters().Count -eq 1 -and
$_.GetParameters()[0].ParameterType.Name -eq 'IAsyncOperation`1'
}

# create generic method
$Method = $definition.MakeGenericMethod( [System.Collections.Generic.IReadOnlyDictionary[System.String,System.String]] )

# call async method and wait for completion
$task = $Method.Invoke.Invoke($null, $task)
$null = $task.Wait(-1)

# emit output
$task.Result

结果类似于:

Key           Value
---           -----
OSVersionFull 10.0.19042.985.amd64fre.vb_release.191206-1406
FlightRing    Retail
App           powershell_ise.exe
AppVer        10.0.19041.1
DeviceFamily  Windows.Desktop

OSVersionFull 返回有关当前 Windows 版本的完整详细信息。

请注意,上面示例中调用的方法可以检索更多详细信息。$names 列出要查询的属性名称。不幸的是,没有办法发现可用的属性名称,因为第三方方可能会添加无穷无尽的附加信息。本示例中使用的五个属性是唯一保证的属性。

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

可以从 Windows 注册表轻松读取当前的 Windows 版本:

1
2
3
4
5
PS> Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' | Select-Object -Property ReleaseId, DisplayVersion

ReleaseId DisplayVersion
--------- --------------
2009 20H2

请注意,不推荐使用 ReleaseId 中显示的信息。新的 Windows 10 版本改为使用 DisplayVersion 属性。要正确识别 Windows 10 版本,请确保使用 DisplayVersion 而不是 ReleaseId

在上面的示例中,您可以看到 Windows 10 报告了许多不同版本的相同 ReleaseId (2009)。例如,DisplayVersion 20H1 也使用 ReleaseId 2009。

微软宣布 ReleaseId 已弃用,不能再用于正确识别 Windows 10 版本。这对于脚本以及 WSUS、WAC 等工具可能很重要。例如,像 Get-WindowsImage 这样的 cmdlet 现在很难识别正确的 Windows 10 版本。

PowerShell 技能连载 - 原版 Windows 10 产品密钥

有大量 PowerShell 脚本到处流传,它们生成可以解码原始的 Windows 10 产品密钥。大多数这些脚本使用过时的算法,这些算法不再适用于 Windows 10。

这是检索原始 Windows 10 产品密钥的更简单的方法:

1
2
Get-CimInstance -ClassName SoftwareLicensingService |
Select-Object -ExpandProperty OA3xOriginalProductKey

如果该命令不产生任何返回信息,则操作系统安装中没有存储单独的产品密钥。

PowerShell 技能连载 - 评估事件日志数据(第 3 部分)

在上一个技能中,我们了解了 Get-WinEvent 以及如何使用计算属性直接访问附加到每个事件的“属性”,而不必对事件消息进行文本解析。

例如,下面的代码通过从“属性”中找到的数组中提取已安装更新的名称来生成已安装更新列表:

1
2
3
4
$software = @{
Name = 'Software' Expression = { $_.Properties[0].Value }
}Get-WinEvent -FilterHashTable @{
Logname='System' ID=19 ProviderName='Microsoft-Windows-WindowsUpdateClient'} | Select-Object -Property TimeCreated, $software

这个概念一般适用于所有事件类型,您唯一的工作就是找出哪些信息包含在哪个数组索引中。让我们来看一个更复杂的事件类型,它包含的不仅仅是一条信息:

1
2
3
4
5
6
7
8
9
10
11
12
$LogonType = @{
Name = 'LogonType' Expression = { $_.Properties[8].Value }
}$Process = @{
Name = 'Process' Expression = { $_.Properties[9].Value }
}$Domain = @{
Name = 'Domain' Expression = { $_.Properties[5].Value }
}$User = @{
Name = 'User' Expression = { $_.Properties[6].Value }
}$Method = @{
Name = 'Method' Expression = { $_.Properties[10].Value }
}Get-WinEvent -FilterHashtable @{
LogName = 'Security' Id = 4624 } | Select-Object -Property TimeCreated, $LogonType, $Process, $Domain, $User, $Method

在这里,Get-WinEvent 从安全日志中读取 ID 为 4624 的所有事件。这些事件代表登录。由于事件位于安全日志中,因此您需要本地管理员权限才能运行代码。

Select-Object 仅返回 TimeCreated 属性。所有剩余的属性都被计算出来,基本上都是一样的:它们从所有事件日志条目对象中找到的“属性”数组中提取一些信息。

事实证明,登录的用户名可以在该数组的索引 6 中找到,登录类型可以在数组索引 8 中找到。

将代码包装到一个函数中后,现在可以很容易地对记录的登录事件进行复杂的查询:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function Get-LogonInfo{
$LogonType = @{
Name = 'LogonType' Expression = { $_.Properties[8].Value }
}

$Process = @{
Name = 'Process' Expression = { $_.Properties[9].Value }
}

$Domain = @{
Name = 'Domain' Expression = { $_.Properties[5].Value }
}

$User = @{
Name = 'User' Expression = { $_.Properties[6].Value }
}

$Method = @{
Name = 'Method' Expression = { $_.Properties[10].Value }
}

Get-WinEvent -FilterHashtable @{
LogName = 'Security' Id = 4624 } | Select-Object -Property TimeCreated, $LogonType, $Process, $Domain, $User, $Method}Get-LogonInfo | Where-Object Domain -ne System | Where-Object User -ne 'Window Manager' | Select-Object -Property TimeCreated, Domain, User, Method

结果类似于:

TimeCreated         Domain                  User             Method
-----------         ------                  ----             ------
06.05.2021 11:46:04 RemotingUser2           DELL7390         Negotiate
05.05.2021 19:20:16 tobi.weltner@-------.de MicrosoftAccount Negotiate
05.05.2021 19:20:06 UMFD-1                  Font Driver Host Negotiate
05.05.2021 19:20:05 UMFD-0                  Font Driver Host Negotiate

PowerShell 技能连载 - 评估事件日志数据(第 2 部分)

在上一个技能中,我们查看了 Get-WinEvent 以及如何使用哈希表来指定查询。上一个提示使用以下代码列出了 Windows 更新客户端使用事件 ID 19 写入的所有事件日志文件中的所有事件:

1
2
3
4
Get-WinEvent -FilterHashTable @{
ID=19
ProviderName='Microsoft-Windows-WindowsUpdateClient'
} | Select-Object -Property TimeCreated, Message

结果是已安装更新的列表:

TimeCreated         Message
-----------         -------
05.05.2021 18:13:34 Installation erfolgreich: Das folgende Update wurde installiert. Security Intelligence-Update für
                    Microsoft Defender Antivirus - KB2267602 (Version 1.337.679.0)
05.05.2021 00:11:33 Installation erfolgreich: Das folgende Update wurde installiert. Security Intelligence-Update für
                    Microsoft Defender Antivirus - KB2267602 (Version 1.337.615.0)
04.05.2021 12:07:03 Installation erfolgreich: Das folgende Update wurde installiert. Security Intelligence-Update für
                    Microsoft Defender Antivirus - KB2267602 (Version 1.337.572.0)
03.05.2021 23:54:58 Installation erfolgreich: Das folgende Update wurde installiert. Security Intelligence-Update für
                    Microsoft Defender Antivirus - KB2267602 (Version 1.337.528.0)
...

通常,您只需要一个实际安装软件的列表,当您查看 “Message” 列时,需要删除大量无用的文本。

分析:事件日志消息由带有占位符的静态文本模板和插入到模板中的实际数据组成。实际数据可以在名为 “Properties” 的属性中找到,您需要做的就是找出这些属性中的哪些是您需要的信息。

这是上述代码的改进版本,它使用一个名为 “Software” 的计算属性读取属性(索引为 0)中的第一个数组元素,它恰好是已安装软件的实际名称:

1
2
3
4
5
6
7
8
9
10
11
$software = @{
Name = 'Software'
Expression = { $_.Properties[0].Value }
}


Get-WinEvent -FilterHashTable @{
Logname='System'
ID=19
ProviderName='Microsoft-Windows-WindowsUpdateClient'
} | Select-Object -Property TimeCreated, $software

所以现在代码返回一个更新列表以及它们的安装时间——不需要文本解析:

TimeCreated         Software
-----------         --------
05.05.2021 18:13:34 Security Intelligence-Update für Microsoft Defender Antivirus - KB2267602 (Version 1.337.679.0)
05.05.2021 00:11:33 Security Intelligence-Update für Microsoft Defender Antivirus - KB2267602 (Version 1.337.615.0)
04.05.2021 12:07:03 Security Intelligence-Update für Microsoft Defender Antivirus - KB2267602 (Version 1.337.572.0)
03.05.2021 23:54:58 Security Intelligence-Update für Microsoft Defender Antivirus - KB2267602 (Version 1.337.528.0)
03.05.2021 00:57:52 9WZDNCRFJ3Q2-Microsoft.BingWeather
03.05.2021 00:57:25 9NCBCSZSJRSB-SpotifyAB.SpotifyMusic
03.05.2021 00:57:06 9PG2DK419DRG-Microsoft.WebpImageExtension

PowerShell 技能连载 - 评估事件日志数据(第 1 部分)

事件日志包含有关 Windows 系统几乎所有方面的非常有用的信息。但是,在使用已弃用的 Get-EventLog cmdlet 时,只能访问此信息的一小部分,因为此 cmdlet 只能访问较旧的经典日志。这就是该 cmdlet 从 PowerShell 7 中完全删除的原因。

在 PowerShell 3 中,添加了一个更快、更强大的替代 cmdlet:et-WinEvent。此 cmdlet 可以根据哈希表中提供的查询项过滤任何日志文件。

例如,此行代码转储所有事件日志文件中由 Windows Update Client 使用事件 ID 19 写入的所有事件:

1
2
3
4
Get-WinEvent -FilterHashTable @{
ID=19
ProviderName='Microsoft-Windows-WindowsUpdateClient'
} | Select-Object -Property TimeCreated, Message

结果是已安装更新的列表:

TimeCreated         Message
-----------         -------
05.05.2021 18:13:34 Installation erfolgreich: Das folgende Update wurde installiert. Security Intelligence-Update für
                    Microsoft Defender Antivirus - KB2267602 (Version 1.337.679.0)
05.05.2021 00:11:33 Installation erfolgreich: Das folgende Update wurde installiert. Security Intelligence-Update für
                    Microsoft Defender Antivirus - KB2267602 (Version 1.337.615.0)
04.05.2021 12:07:03 Installation erfolgreich: Das folgende Update wurde installiert. Security Intelligence-Update für
                    Microsoft Defender Antivirus - KB2267602 (Version 1.337.572.0)
03.05.2021 23:54:58 Installation erfolgreich: Das folgende Update wurde installiert. Security Intelligence-Update für
                    Microsoft Defender Antivirus - KB2267602 (Version 1.337.528.0)
...

PowerShell 技能连载 - 解析原始数据和日志文件(第 2 部分)

在上一个技能中,我们解释了大多数日志文件可以被视为 CSV 文件并由 Import-Csv 读取。您需要做的就是告诉 Import-Csv 您的日志文件与标准 CSV 的不同之处,例如定义不同的分隔符或提供缺少的标题。

然而,一种日志文件格式很难解析:固定宽度的列。在这种情况下,没有可使用的单个分隔符。相反,数据使用固定宽度的字符串。

为了说明这种类型的数据,在 Windows 上运行实用程序 qprocess.exe。它返回固定宽度的数据(列出正在运行的进程、它们的所有者和它们的连接会话)。下面的示例取自德语操作系统,但本地化的列标题在这里并不重要。更重要的是每列使用固定的字符串宽度而不是单个分隔符,因此 ConvertFrom-Csv 无法读取数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
PS> qprocess
BENUTZERNAME SITZUNGSNAME ID PID ABBILD
>tobia console 1 9332 dptf_helper.exe
>tobia console 1 9352 mbamtray.exe
>tobia console 1 9440 sihost.exe
>tobia console 1 9472 svchost.exe
...

PS> qprocess | ConvertTo-Csv
#TYPE System.String
"Length"
"60"
...

不过,对于固定宽度数据,您可以使用简单的正则表达式将可变空白替换为固定宽度分隔符:

1
2
3
4
5
6
PS> (qprocess) -replace '\s{1,}',','
,BENUTZERNAME,SITZUNGSNAME,ID,PID,ABBILD
>tobia,console,1,9332,dptf_helper.exe
>tobia,console,1,9352,mbamtray.exe
>tobia,console,1,9440,sihost.exe
...

现在您获得了有效的 CSV。由于 qprocess 返回一个字符串数组,您可以稍微微调数据,例如从每一行中删除不需要的字符:

1
2
3
4
5
6
PS> (qprocess).TrimStart(' >') -replace '\s{1,}',','
BENUTZERNAME,SITZUNGSNAME,ID,PID,ABBILD
tobia,console,1,9332,dptf_helper.exe
tobia,console,1,9352,mbamtray.exe
tobia,console,1,9440,sihost.exe
...

PowerShell 技能连载 - 解析原始数据和日志文件(第 1 部分)

大多数原始日志文件以表格形式出现:尽管它们可能不是功能齐全的 CSV 格式,但它们通常具有列和某种分隔符,有时甚至包含标题。

这是从 IIS 日志中获取的示例。当您查看它时时,会发现许多日志文件从根本上以表格方式组织它们的数据,如下所示:

#Software: Microsoft Internet Information Services 10.0
#Version: 1.0
#Date: 2018-02-02 00:03:04
#Fields: date time s-ip cs-method cs-uri-stem cs-uri-query s-port cs-username c-ip cs(User-Agent) cs(Referer) sc-status sc-substatus sc-win32-status time-taken
2021-05-02 00:00:04 10.10.12.5 GET /Content/anonymousCheckFile.txt - 8530 - 10.22.121.248 - - 200 0 0 0
2021-05-02 00:00:04 10.10.12.5 GET /Content/anonymousCheckFile.txt - 8531 - 10.22.121.248 - - 200 0 0 2

与编写复杂的代码来读取和解析日志文件的数据相比,将日志文件与标准 CSV 格式进行比较并查看是否可以这样处理会非常有价值。

在上面的 IIS 日志示例中,结果将是这样:

  • 分隔符是一个空格(不是逗号)
  • 字段(列名)记录在注释行(而不是标题行)中

知道这一点后,请使用 Import-Csv(用于 CSV 的快速内置 PowerShell 解析器)来快速解析日志文件并将其转换为对象。您需要做的就是告诉 Import-Csv 您的日志文件与标准 CSV 格式功能的不同之处:

1
2
3
$Path = "c:\logs\l190202.log"

Import-Csv -Path $Path -Delimiter ' ' -Header date, time, s-ip, cs-method, cs-uri-stem, cs-uri-query, s-port, cs-username, c-ip, csUser-Agent, csReferer, sc-status ,sc-substatus, sc-win32-status, time-taken

在此示例中,使用 -Delimiter 告诉 Import-Csv 分隔符是一个空格,并且由于没有定义标题,请使用 -Header 并粘贴在开头的日志文件注释中找到的标题名称。

如果您不知道日志的标题名称,只需提供一个字符串数组,或使用此参数:

1
Import-Csv -Header (1..50)

这将为日志文件的列分配数字。

PowerShell 技能连载 - 修复 CSV 导出(第 2 部分)

在上一个技能中,我们指出了将对象转换为 CSV 时的一个普遍问题:任何包含数组的属性都将显示数组数据类型而不是数组内容。下面是一个例子:

1
2
3
4
5
6
PS> Get-Service | Select-Object -Property Name, DependentServices, RequiredServices | ConvertTo-Csv

#TYPE Selected.System.ServiceProcess.ServiceController
"Name","DependentServices","RequiredServices"
"AarSvc_e1277","System.ServiceProcess.ServiceController[]","System.ServiceProcess.ServiceController[]"
"AdobeARMservice","System.ServiceProcess.ServiceController[]","System.ServiceProcess.ServiceController[]"

在上一个技能中,我们还展示了该问题的手动解决方案:您总是可以使用 -join 运算符手动将任何数组属性的内容转换为字符串:

1
2
3
4
5
6
7
8
9
10
Get-Service |
Select-Object -Property Name, DependentServices, RequiredServices |

ForEach-Object {
$_.DependentServices = $_.DependentServices -join ','
$_.RequiredServices = $_.RequiredServices -join ','
return $_
} |

ConvertTo-Csv

数据现已“修复”,数组属性内容显示正确:

"Name","DependentServices","RequiredServices"
"AppIDSvc","applockerfltr","RpcSs,CryptSvc,AppID"
"Appinfo","","RpcSs,ProfSvc"
"AppVClient","","AppvVfs,RpcSS,AppvStrm,netprofm"
"AppXSvc","","rpcss,staterepository"
"AssignedAccessManagerSvc","",""
...

但是,将数组属性转换为扁平字符串可能需要大量手动工作,因此这里有一个名为 Convert-ArrayPropertyToString 的新函数,它会自动完成所有转换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Convert-ArrayPropertyToString
{
process
{
$original = $_
Foreach ($prop in $_.PSObject.Properties)
{
if ($Prop.Value -is [Array] -and $prop.MemberType -ne 'AliasProperty')
{
Add-Member -InputObject $original -MemberType NoteProperty -Name $prop.Name -Value ($prop.Value -join ',') -Force
}
}
$original
}
}

要将对象转换为 CSV 而不丢失数组信息,只需将它通过管道传给新函数:

1
2
3
4
5
6
7
8
9
10
PS> Get-Service | Select-Object -Property Name, DependentServices, RequiredServices | Convert-ArrayPropertyToString | ConvertTo-Csv

#TYPE Selected.System.ServiceProcess.ServiceController
"Name","DependentServices","RequiredServices"
"AarSvc_e1277","",""
"AppIDSvc","applockerfltr","RpcSs,CryptSvc,AppID"
"Appinfo","","RpcSs,ProfSvc"
"AppMgmt","",""
"AppReadiness","",""
"AppVClient","","AppvVfs,RpcSS,AppvStrm,netprofm"

厉害吧?新函数能为您完成所有工作,它适用于任何对象:

1
2
3
4
5
6
7
8
9
PS> [PSCustomObject]@{
Name = 'Tobias'
Array = 1,2,3,4
Date = Get-Date
} | Convert-ArrayPropertyToString

Name Date Array
---- ---- -----
Tobias 06.05.2021 11:30:58 1,2,3,4

Convert-ArrayPropertyToString 使用在任何 PowerShell 对象都包含的 PSObject 隐藏属性来获取属性名称。接下来,它检查数组内容的所有属性。如果找到,它会自动将数组转换为逗号分隔的字符串。

为了能够使用新的扁平字符串内容覆盖现有属性——即使属性被写保护——它使用 Add-Member 并使用 -Force 隐藏属性。新的扁平字符串内容并没有真正覆盖属性。相反,它们被添加并优先使用。实际上,任何对象——即使其属性被写保护——都可以调整。

现在,每当您需要创建 Excel 报告或将数据导出到 CSV 时,您都可以轻松保留数组内容。

PowerShell 技能连载 - 修复 CSV 导出(第 1 部分)

当您将数据转换为 CSV 时,您可能会遇到一个很烦恼的情况:某些属性不是显示原始数据。下面是一个例子:

1
2
3
4
5
6
PS> Get-Service | Select-Object -Property Name, DependentServices, RequiredServices | ConvertTo-Csv

#TYPE Selected.System.ServiceProcess.ServiceController
"Name","DependentServices","RequiredServices"
"AarSvc_e1277","System.ServiceProcess.ServiceController[]","System.ServiceProcess.ServiceController[]"
"AdobeARMservice","System.ServiceProcess.ServiceController[]","System.ServiceProcess.ServiceController[]"

如您所见,DependentServicesRequiredServices 属性和所有服务显示的内容是一样的。

当属性包含数组时会发生这种情况。扁平化二维导出格式(例如 CSV)无法显示数组,因此将显示数组数据类型。这对用户来说当然根本没有帮助。

在我们解决它之前先总结一下:您在这里看到的是许多场景中的严重问题。它不仅影响CSV导出,还影响导出为 Excel 或其他二维表格格式。

要解决这个问题,您必须将所有数组转换为字符串。您可以手动或自动执行此操作。在这个技能中,我们首先展示手动方法来关注效果。在未来的技能中,我们会自动执行相同的操作。

这是从上方正确导出所选数据的手动方法:

1
2
3
4
5
6
7
8
9
10
Get-Service |
Select-Object -Property Name, DependentServices, RequiredServices |

ForEach-Object {
$_.DependentServices = $_.DependentServices -join ','
$_.RequiredServices = $_.RequiredServices -join ','
return $_
} |

ConvertTo-Csv

数据现在显示所有数组内容,因为 ForEach-Object 循环已使用 -join 运算符将数组内容转换为逗号分隔的字符串。

"Name","DependentServices","RequiredServices"
"AppIDSvc","applockerfltr","RpcSs,CryptSvc,AppID"
"Appinfo","","RpcSs,ProfSvc"
"AppVClient","","AppvVfs,RpcSS,AppvStrm,netprofm"
"AppXSvc","","rpcss,staterepository"
"AssignedAccessManagerSvc","",""
...

只要您通过 Select-Object 操作原始数据,就可以进行这种“调整”:Select-Object 始终复制(克隆)信息,因此一旦 Select-Object 处理了数据,您就拥有这些对象并可以以任何方式更改其属性。