PowerShell 技能连载 - 对 PowerShell 脚本进行数字签名

在前一个技能中您学习了如何创建一个自签名的代码签名证书、将证书保存为一个 PFX 文件,并且将它加载进内存。

今天,假设您已经有了一个包含代码签名证书的 PFX 文件,我们将看看如何对 PowerShell 脚本进行数字签名。

以下代码在您的用户配置文件中查找所有 PowerShell 脚本,如果脚本还未经过数字签名,将会从 PFX 文件中读取一个证书对它进行签名:

1
2
3
4
5
6
7
8
9
10
11
12
13
# read in the certificate from a pre-existing PFX file
$cert = Get-PfxCertificate -FilePath "$env:temp\codeSignCert.pfx"

# find all scripts in your user profile...
Get-ChildItem -Path $home -Filter *.ps1 -Include *.ps1 -Recurse -ErrorAction SilentlyContinue |
# ...that do not have a signature yet...
Where-Object {
($_ | Get-AuthenticodeSignature).Status -eq 'NotSigned'
} |
# and apply one
# (note that we added -WhatIf so no signing occurs. Remove this only if you
# really want to add digital signatures!)
Set-AuthenticodeSignature -Certificate $cert -WhatIf

当您在编辑器中查看这些脚本时,您将在脚本的底部看到一个新的注释段。它包含了使用证书加密的脚本的哈希值。他也包含了证书的公开信息。

当您右键点击一个签名的脚本并且选择“属性”,可以看到谁对脚本做了签名。如果您确实信任这个人,您可以将它们的证书安装到受信任的根证书中。

一旦脚本经过数字签名,就可以很方便地审查脚本状态。签名可以告诉您谁签名了一个脚本,以及脚本的内容是否被纂改过。以下代码检查用户配置文件中所有的 PowerShell 脚本并显示签名状态:

1
2
3
4
# find all scripts in your user profile...
Get-ChildItem -Path $home -Filter *.ps1 -Include *.ps1 -Recurse -ErrorAction SilentlyContinue |
# ...and check signature status
Get-AuthenticodeSignature

如果您收到“UnknownError”消息,这并不代表是一个未知错误,而是代表脚本未受纂改但签名在系统中是未知的(或非受信的)。

PowerShell 技能连载 - 从 PFX 文件加载证书

在前一个技能中我们演示了如何使用 New-SelfSignedCertificate 来创建新的代码签名证书,并且将它们存储为一个 PFX 文件。今天让我们来看看如何加载一个 PFX 文件。

假设您的 PFX 文件存放在 $env:temp\codeSignCert.pfx。以下是读取该文件的代码:

1
$cert = Get-PfxCertificate -FilePath "$env:temp\codeSignCert.pfx"

这段代码执行时,将会提示输入密码。这个密码是您创建证书时输入的密码,并且它保护这个文件不被滥用。

当命令成功执行以后,可以从 $cert 变量获取证书详细信息:

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
PS C:\> $cert

Thumbprint Subject
---------- -------
5D8A325641CC583F882B439833961AE9BCDEC946 CN=SecurityDepartment



PS C:\> $cert | Select-Object -Property *


EnhancedKeyUsageList : {Code Signing (1.3.6.1.5.5.7.3.3)}
DnsNameList : {SecurityDepartment}
SendAsTrustedIssuer : False
EnrollmentPolicyEndPoint : Microsoft.CertificateServices.Commands.EnrollmentEndPointProperty
EnrollmentServerEndPoint : Microsoft.CertificateServices.Commands.EnrollmentEndPointProperty
PolicyId :
Archived : False
Extensions : {System.Security.Cryptography.Oid, System.Security.Cryptography.Oid, System.Security.Cryptography.Oid}
FriendlyName : IT Sec Department
IssuerName : System.Security.Cryptography.X509Certificates.X500DistinguishedName
NotAfter : 9/29/2022 12:57:28 AM
NotBefore : 9/29/2017 12:47:28 AM
HasPrivateKey : True
PrivateKey : System.Security.Cryptography.RSACryptoServiceProvider
PublicKey : System.Security.Cryptography.X509Certificates.PublicKey
RawData : {48, 130, 3, 10...}
SerialNumber : 45C8C7871DC392A44AD1ADD28FFDFAC7
SubjectName : System.Security.Cryptography.X509Certificates.X500DistinguishedName
SignatureAlgorithm : System.Security.Cryptography.Oid
Thumbprint : 5D8A325641CC583F882B439833961AE9BCDEC946
Version : 3
Handle : 2832940980736
Issuer : CN=SecurityDepartment
Subject : CN=SecurityDepartment

证书对象包含了一系列方法:

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
PS C:\> $cert | Get-Member -MemberType *Method


TypeName: System.Security.Cryptography.X509Certificates.X509Certificate2

Name MemberType Definition
---- ---------- ----------
Dispose Method void Dispose(), void IDisposable.Dispose()
Equals Method bool Equals(System.Object obj), bool Equals(X509Certificate other)
Export Method byte[] Export(System.Security.Cryptography.X509Certificates.X509ContentType contentType), byte[] Export(System.Sec...
GetCertHash Method byte[] GetCertHash()
GetCertHashString Method string GetCertHashString()
GetEffectiveDateString Method string GetEffectiveDateString()
GetExpirationDateString Method string GetExpirationDateString()
GetFormat Method string GetFormat()
GetHashCode Method int GetHashCode()
GetIssuerName Method string GetIssuerName()
GetKeyAlgorithm Method string GetKeyAlgorithm()
GetKeyAlgorithmParameters Method byte[] GetKeyAlgorithmParameters()
GetKeyAlgorithmParametersString Method string GetKeyAlgorithmParametersString()
GetName Method string GetName()
GetNameInfo Method string GetNameInfo(System.Security.Cryptography.X509Certificates.X509NameType nameType, bool forIssuer)
GetObjectData Method void ISerializable.GetObjectData(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization...
GetPublicKey Method byte[] GetPublicKey()
GetPublicKeyString Method string GetPublicKeyString()
GetRawCertData Method byte[] GetRawCertData()
GetRawCertDataString Method string GetRawCertDataString()
GetSerialNumber Method byte[] GetSerialNumber()
GetSerialNumberString Method string GetSerialNumberString()
GetType Method type GetType()
Import Method void Import(byte[] rawData), void Import(byte[] rawData, string password, System.Security.Cryptography.X509Certifi...
OnDeserialization Method void IDeserializationCallback.OnDeserialization(System.Object sender)
Reset Method void Reset()
ToString Method string ToString(), string ToString(bool verbose)
Verify Method bool Verify()

例如,如果您想验证证书是否合法,只需要调用 Verify() 方法。结果是一个布尔值,$false 代表证书不被 Windows 信任。

明天,我们将会使用证书对 PowerShell 脚本进行数字签名。

PowerShell 技能连载 - 创建自签名的代码签名证书

如果您想对您的脚本进行数字签名,首先您需要一个包含“代码签名”功能的数字证书。如果只是测试,您可以方便地创建免费的个人自签名证书。不要期望其他人信任这些证书,因为任何人都可以创建它们。这是一种很好的测试驱动代码签名的方法。

从 PowerShell 4 开始,New-SelfSignedCertificate cmdlet 可以创建签名证书。以下代码创建一个包含私钥和公钥的 PFX 文件:

1
2
3
4
5
6
7
8
9
10
11
12
#requires -Version 5

# this is where the cert file will be saved
$Path = "$env:temp\codeSignCert.pfx"

# you'll need this password to load the PFX file later
$Password = Read-Host -Prompt 'Enter new password to protect certificate' -AsSecureString

# create cert, export to file, then delete again
$cert = New-SelfSignedCertificate -KeyUsage DigitalSignature -KeySpec Signature -FriendlyName 'IT Sec Department' -Subject CN=SecurityDepartment -KeyExportPolicy ExportableEncrypted -CertStoreLocation Cert:\CurrentUser\My -NotAfter (Get-Date).AddYears(5) -TextExtension @('2.5.29.37={text}1.3.6.1.5.5.7.3.3')
$cert | Export-PfxCertificate -Password $Password -FilePath $Path
$cert | Remove-Item

在接下来的技能里,我们将看一看可以用新创建的证书来做什么。

PowerShell 技能连载 - 在 Active Directory 中使用 LDAP 过滤器

LDAP 过滤器类似 Active Directory 中使用的查询语言。并且如果您安装了 Microsoft 的 RSAT 工具,您可以很方便地用 ActiveDirectory 模块中的 cmdlet 来用 LDAP 过滤器搜索用户、计算机,或其它资源。

以下代码将查找所有无邮箱地址的用户:

1
2
$filter = '(&(objectCategory=person)(objectClass=user)(!mail=*))'
Get-ADUser -LDAPFilter $filter -Prop *

即便您没有 RSAT 工具和指定的 ActiveDirectory cmdlet 的权限,LDAP 过滤器也十分有用:

1
2
3
4
5
6
$filter = '(&(objectCategory=person)(objectClass=user)(!mail=*))'
$searcher = [ADSISearcher]$filter
# search results only
$searcher.FindAll()
# access to directory entry objects with more details
$searcher.FindAll().GetDirectoryEntry() | Select-Object -Property *

PowerShell 技能连载 - 对比从 PowerShell 远程处理中受到的计算机数据

PowerShell 远程处理是一个查询多台计算机的快速方法,因为 PowerShell 远程处理是并行工作的。以下是一个演示一系列有趣技术的真实案例。

目标是从两台计算机中获取正在运行的进程的列表,然后查找区别。

为了速度最快,进程列表是通过 PowerShell 远程处理和 Invoke-Command,并且结果是从两台计算机获得的。

要区分输入的数据,我们使用了 Group-Object。它通过计算机名对数据集分组。结果是一个哈希表,而计算机名是哈希表的键。

下一步,用 Compare-Object 来快速比较两个列表并查找区别:

1
2
3
4
5
6
7
8
9
10
11
12
13
# get data in parallel via PowerShell remoting
# make sure you adjust the computer names
$computer1 = 'server1'
$computer2 = 'server2'
$data = Invoke-Command { Get-Process } -ComputerName $computer1, $computer2

# separate the data per computer
$infos = $data | Group-Object -Property PSComputerName -AsHashTable -AsString

# find differences in running processes
Compare-Object -ReferenceObject $infos.$computer1 -DifferenceObject $infos.$computer2 -Property ProcessName -PassThru |
Sort-Object -Property ProcessName |
Select-Object -Property ProcessName, Id, PSComputerName, SideIndicator

PowerShell 技能连载 - 从文本生成 MD5 哈希

Get-FileHash cmdlet 可以从文件内容生成哈希值。它无法从任意文本生成哈希值。并且只适用于 PowerShell 5 及更高的版本。

以下是一个小的函数,用 .NET Framework 从任意文本生成 MD5 哈希值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Function Get-StringHash
{
param
(
[String] $String,
$HashName = "MD5"
)
$bytes = [System.Text.Encoding]::UTF8.GetBytes($String)
$algorithm = [System.Security.Cryptography.HashAlgorithm]::Create('MD5')
$StringBuilder = New-Object System.Text.StringBuilder

$algorithm.ComputeHash($bytes) |
ForEach-Object {
$null = $StringBuilder.Append($_.ToString("x2"))
}

$StringBuilder.ToString()
}

每段文本都会生成一个唯一(且短小)的哈希值,所以它可以快速地判断文本是否唯一。它也可以用来检查一大段文本是否有变更过。

1
2
3
4
PS C:\> Get-StringHash "Hello World!"
ed076287532e86365e841e92bfc50d8c

PS C:\>

PowerShell 技能连载 - 查找重复的文件

在前一个技能中我们介绍了如何用 Get-FileHash cmdlet(PowerShell 5 新增的功能)来生成脚本文件的 MD5 哈希。

哈希可以用来查找重复的文件。大体上,哈希表可以用来检查一个文件哈希是否已经发现过。以下代码检查您的用户配置文件中的所有脚本并且报告重复的文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$dict = @{}

Get-ChildItem -Path $home -Filter *.ps1 -Recurse |
ForEach-Object {
$hash = ($_ | Get-FileHash -Algorithm MD5).Hash
if ($dict.ContainsKey($hash))
{
[PSCustomObject]@{
Original = $dict[$hash]
Duplicate = $_.FullName
}
}
else
{
$dict[$hash]=$_.FullName
}
} |
Out-GridView

PowerShell 技能连载 - 创建 MD5 文件哈希

MD5 文件哈希可以唯一确定文件内容,并且可以用来检测文件内容是否唯一。在 PowerShell 5 中,有一个新的 cmdlet 可以创建文件哈希。以下代码将在您的用户配置文件中查找所有 PowerShell 脚本,并且为每个文件生成 MD5 哈希:

1
2
3
Get-ChildItem -Path $home -Filter *.ps1 -Recurse |
Get-FileHash -Algorithm MD5 |
Select-Object -ExpandProperty Hash

一个更好的方法是将哈希值关联到原始路径上:

1
2
3
4
5
6
7
Get-ChildItem -Path $home -Filter *.ps1 -Recurse |
ForEach-Object {
[PSCustomObject]@{
Hash = ($_ | Get-FileHash -Algorithm MD5).Hash
Path = $_.FullName
}
}

输出结果类似如下:

1
2
3
4
5
6
7
8
9
10
11
Hash                             Path
---- ----
2AE5CA30DCF6550903B994E61A714AC0 C:\Users\tobwe\.nuget\packages\Costura.Fody...
46CB505EECEC72AA8D9104A6263D2A76 C:\Users\tobwe\.nuget\packages\Costura.Fody...
2AE5CA30DCF6550903B994E61A714AC0 C:\Users\tobwe\.nuget\packages\Costura.Fody...
46CB505EECEC72AA8D9104A6263D2A76 C:\Users\tobwe\.nuget\packages\Costura.Fody...
930621EE040F82392017D240CAE13A97 C:\Users\tobwe\.nuget\packages\Fody\2.1.2\T...
39466FE42CE01CC7786D8B446C4C11C2 C:\Users\tobwe\.nuget\packages\MahApps.Metr...
2FF7910807634C984FC704E52ABCDD36 C:\Users\tobwe\.nuget\packages\microsoft.co...
C7E3AAD4816FD98443A7F1C94155954D C:\Users\tobwe\.nuget\packages\microsoft.co...
...

PowerShell 技能连载 - Creating Balloon Tips Safely

受到 MVP 同行 Boe Prox 一篇文章的启发,编写了一个精致的创建气球状提示对话框的函数。您可以在 Boe 的原始文章中找到背景信息:https://mcpmag.com/articles/2017/09/07/creating-a-balloon-tip-notification-using-powershell.aspx

您可以找到许多关于如何显示气球状提示的使用技巧,但大多数都只是一个不能操作的任务栏图标。

以下函数是基于 Boe 的点子,但可以确保不需要全局变量或任何其它会污染 PowerShell 环境的东西。当您调用 Show-BalloonTip 时,一个气球状提示将从桌面的右下角滑入。您可以点击打开这个工具提示并再次关闭它,或者取消它。当您取消它时,它的图标会留在任务栏的托盘区域。当您点击托盘图标,该气球状图标会再次显示。

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
function Show-BalloonTip
{
param
(
[Parameter(Mandatory=$true)][string]$Text,
[string]$Title = "Message from PowerShell",
[ValidateSet('Info','Warning','Error','None')][string]$Icon = 'Info'
)

Add-Type -AssemblyName System.Windows.Forms

# we use private variables only. No need for global scope
$balloon = New-Object System.Windows.Forms.NotifyIcon
$cleanup =
{
# this gets executed when the user clicks the balloon tip dialog

# take the balloon from the event arguments, and dispose it
$event.Sender.Dispose()
# take the event handler names also from the event arguments,
# and clean up
Unregister-Event -SourceIdentifier $event.SourceIdentifier
Remove-Job -Name $event.SourceIdentifier
$name2 = "M" + $event.SourceIdentifier
Unregister-Event -SourceIdentifier $name2
Remove-Job -Name $name2
}
$showBalloon =
{
# this gets executed when the user clicks the tray icon
$event.Sender.ShowBalloonTip(5000)
}

# use unique names for event handlers so you can open multiple balloon tips
$name = [Guid]::NewGuid().Guid

# subscribe to the balloon events
$null = Register-ObjectEvent -InputObject $balloon -EventName BalloonTipClicked -Source $name -Action $cleanup
$null = Register-ObjectEvent -InputObject $balloon -EventName MouseClick -Source "M$name" -Action $showBalloon

# use the current application icon as tray icon
$path = (Get-Process -id $pid).Path
$balloon.Icon = [System.Drawing.Icon]::ExtractAssociatedIcon($path)

# configure the balloon tip
$balloon.BalloonTipIcon = $Icon
$balloon.BalloonTipText = $Text
$balloon.BalloonTipTitle = $Title

# make the tray icon visible
$balloon.Visible = $true
# show the balloon tip
$balloon.ShowBalloonTip(5000)
}

PowerShell 技能连载 - 补零

您是否曾需要将数字转换为以零开头的字符串,例如生成服务器名?只需要使用 PowerShell 的 -f 操作符:

1
2
$id = 12
'server{0:d4}' -f $id

以下是输出结果:

1
server0012

-f 操作符左边是文本模板,右边是数值。在文本模板中,用 {x} 作为右侧数值的占位符。占位符的下标从 0 开始。

要在左侧补零,使用 d(digit 的缩写)加上您需要的数字位数即可。