PowerShell 技能连载 - 使用一个停表

在 PowerShell 中,要测量时间,您可以简单将一个 datetime 值减去另一个 datetime 值:

1
2
3
4
5
6
7
$Start = Get-Date

$null = Read-Host -Prompt "Press ENTER as fast as you can!"

$Stop = Get-Date
$TimeSpan = $Stop - $Start
$TimeSpan.TotalMilliseconds

一个优雅的实现是用停表:

1
2
3
4
5
6
$StopWatch = [Diagnostics.Stopwatch]::StartNew()

$null = Read-Host -Prompt "Press ENTER as fast as you can!"

$StopWatch.Stop()
$StopWatch.ElapsedMilliseconds

使用停表的好处是可以暂停和继续。

PowerShell 技能连载 - 使用 $MyInvocation 的固定替代方式

类似 $MyInvocation.MyCommand.Definition 的代码对于确定当前脚本存储的位置十分有用,例如需要存取同一个文件夹下其它资源的时候。

然而,从 PowerShell 3 开始,有一个更简单的替代方式可以查找当前脚本的名称和/或当前脚本文件夹的路径。请自己运行以下代码测试:

1
2
3
4
5
$MyInvocation.MyCommand.Definition
$PSCommandPath

Split-Path -Path $MyInvocation.MyCommand.Definition
$PSScriptRoot

如果您交互式运行这段代码(或在一个“无标题”脚本中),它们都不会返回任何内容。但是当您将脚本保存后执行,这两行代码将返回脚本的路径,并且后两行代码将返回脚本所在文件夹的路径。

$PSCommandPath$PSScriptRoot 的好处在于它们总是包含相同的信息。相比之下,$MyInvocation 可能会改变,而且当从一个函数中读取这个变量时,它就会发生改变。

1
2
3
4
5
6
7
8
9
10
function test
{
$MyInvocation.MyCommand.Definition
$PSCommandPath

Split-Path -Path $MyInvocation.MyCommand.Definition
$PSScriptRoot
}

test

现在,$MyInvocation 变得没有价值,因为它重视返回调用本脚本块的调用者信息。

PowerShell 技能连载 - 查找打开的防火墙端口

以下是一段连接到本地防火墙并转储所有打开的防火墙端口的 PowerShell 代码:

1
2
3
$firewall = New-object -ComObject HNetCfg.FwPolicy2
$firewall.Rules | Where-Object {$_.Action -eq 0} |
Select-Object Name, ApplicationName,LocalPorts

结果看起来类似这样:

Name           ApplicationName                                         LocalPorts
----           ---------------                                         ----------
pluginhost.exe C:\users\tobwe\appdata\local\skypeplugin\pluginhost.exe *
pluginhost.exe C:\users\tobwe\appdata\local\skypeplugin\pluginhost.exe *
spotify.exe    C:\users\tobwe\appdata\roaming\spotify\spotify.exe      *
spotify.exe    C:\users\tobwe\appdata\roaming\spotify\spotify.exe      *

在 Windows 10 和 Server 2016 中,有一系列现成的跟防火墙有关的 cmdlet:

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
PS> Get-Command -Noun *Firewall*

CommandType Name Version Source
----------- ---- ------- ------
Function Copy-NetFirewallRule 2.0.0.0 NetSecurity
Function Disable-NetFirewallRule 2.0.0.0 NetSecurity
Function Enable-NetFirewallRule 2.0.0.0 NetSecurity
Function Get-NetFirewallAddressFilter 2.0.0.0 NetSecurity
Function Get-NetFirewallApplicationFilter 2.0.0.0 NetSecurity
Function Get-NetFirewallInterfaceFilter 2.0.0.0 NetSecurity
Function Get-NetFirewallInterfaceTypeFilter 2.0.0.0 NetSecurity
Function Get-NetFirewallPortFilter 2.0.0.0 NetSecurity
Function Get-NetFirewallProfile 2.0.0.0 NetSecurity
Function Get-NetFirewallRule 2.0.0.0 NetSecurity
Function Get-NetFirewallSecurityFilter 2.0.0.0 NetSecurity
Function Get-NetFirewallServiceFilter 2.0.0.0 NetSecurity
Function Get-NetFirewallSetting 2.0.0.0 NetSecurity
Function New-NetFirewallRule 2.0.0.0 NetSecurity
Function Remove-NetFirewallRule 2.0.0.0 NetSecurity
Function Rename-NetFirewallRule 2.0.0.0 NetSecurity
Function Set-NetFirewallAddressFilter 2.0.0.0 NetSecurity
Function Set-NetFirewallApplicationFilter 2.0.0.0 NetSecurity
Function Set-NetFirewallInterfaceFilter 2.0.0.0 NetSecurity
Function Set-NetFirewallInterfaceTypeFilter 2.0.0.0 NetSecurity
Function Set-NetFirewallPortFilter 2.0.0.0 NetSecurity
Function Set-NetFirewallProfile 2.0.0.0 NetSecurity
Function Set-NetFirewallRule 2.0.0.0 NetSecurity
Function Set-NetFirewallSecurityFilter 2.0.0.0 NetSecurity
Function Set-NetFirewallServiceFilter 2.0.0.0 NetSecurity
Function Set-NetFirewallSetting 2.0.0.0 NetSecurity
Function Show-NetFirewallRule 2.0.0.0 NetSecurity

PowerShell 技能连载 - 为代码执行添加超时(第 2 部分)

在前一个技能中我们通过 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
function Invoke-CodeWithTimeout
{
param
(
[Parameter(Mandatory)]
[ScriptBlock]
$Code,

[int]
$Timeout = 5

)

$ps = [PowerShell]::Create()
$null = $ps.AddScript($Code)
$handle = $ps.BeginInvoke()
$start = Get-Date
do
{
$timeConsumed = (Get-Date) - $start
if ($timeConsumed.TotalSeconds -ge $Timeout) {
$ps.Stop()
$ps.Dispose()
throw "Job timed out."
}
Start-Sleep -Milliseconds 300
} until ($handle.isCompleted)

$ps.EndInvoke($handle)
$ps.Runspace.Close()
$ps.Dispose()
}

以下是使用该新超时机制的方法:

1
2
3
4
5
6
7
8
9
10
11
12
PS> Invoke-CodeWithTimeout -Code { Start-Sleep -Seconds 6; Get-Date } -Timeout 5
Job timed out.
At line:24 char:13
+ throw "Job timed out."
+ ~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : OperationStopped: (Job timed out.:String) [], RuntimeException
+ FullyQualifiedErrorId : Job timed out.


PS> Invoke-CodeWithTimeout -Code { Start-Sleep -Seconds 3; Get-Date } -Timeout 5

Thursday November 1, 2018 14:53:26

PowerShell 技能连载 - 为代码执行添加超时(第 1 部分)

如果希望某些代码不会无限执行下去,您可以使用后台任务来实现超时机制。以下是一个示例函数:

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
function Invoke-CodeWithTimeout
{
param
(
[Parameter(Mandatory)]
[ScriptBlock]
$Code,

[int]
$Timeout = 5

)

$j = Start-Job -ScriptBlock $Code
$completed = Wait-Job $j -Timeout $Timeout
if ($completed -eq $null)
{
throw "Job timed out."
Stop-Job -Job $j
}
else
{
Receive-Job -Job $j
}
Remove-Job -Job $j
}

所以基本上说,要让代码执行的时间不超过 5 秒,请试试以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
PS> Invoke-CodeWithTimeout -Code { Start-Sleep -Seconds 6; Get-Date } -Timeout 5
Job timed out.
At line:18 char:7
+ throw "Job timed out."
+ ~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : OperationStopped: (Job timed out.:String) [], RuntimeException
+ FullyQualifiedErrorId : Job timed out.


PS> Invoke-CodeWithTimeout -Code { Start-Sleep -Seconds 3; Get-Date } -Timeout 5

Thursday November 1, 2018 14:53:26

该方法有效。但是,它所使用的作业相关的开销相当大。创建后台作业并将数据返回到前台任务的开销可能增加了额外的时间。所以我们将在明天的技能中寻求一个更好的方法。

PowerShell 技能连载 - 代码签名迷你系列(第 5 部分:审计签名)

当 Powershell 脚本携带一个数字签名后,您可以快速地找出是谁对这个脚本签的名,更重要的是,这个脚本是否未被篡改。在本系列之前的部分中,您学习了如何创建数字签名,以及如何对 PowerShell 文件签名。现在我们来看看如何验证脚本的合法性。

1
2
3
4
5
# this is the path to the scripts you'd like to examine
$Path = "$home\Documents"

Get-ChildItem -Path $Path -Filter *.ps1 -Recurse |
Get-AuthenticodeSignature

只需要调整路径,该脚本讲查找该路径下的所有 PowerShell 脚本,然后检查它们的签名。结果有如下可能性:

NotSigned:	没有签名
UnknownError:	使用非受信的证书签名
HashMismatch:	签名之后修改过
Valid:		采用受信任的证书签名,并且没有改动过

PowerShell 技能连载 - 代码签名迷你系列(第 4 部分:签名 PowerShell 文件)

在将 PowerShell 脚本发送给别人之前,最好对它进行数字签名。签名的角色类似脚本的“包装器”,可以帮助别人确认是谁编写了这个脚本以及这个脚本是否仍是原始本版本,或是已被篡改过。

要对 PowerShell 脚本签名,您需要一个数字代码签名证书。在前一个技能中我们解释了如何创建一个该证书,并且/或者从 pfx 或证书存储中加载该证书。以下代码假设在 $cert 中已经有了一个合法的代码签名证书。如果还没有,请先阅读之前的技能文章!

1
2
3
4
5
6
7
8
9
10
11
12
13
# make sure this PFX file exists or create one
# or load a code-signing cert from other sources
# (review the previous tips for hints)
$pfxFile = "$home\desktop\tobias.pfx"
$cert = Get-PfxCertificate -FilePath $pfxFile

# make sure this folder exists and contains
# PowerShell script that you'd like to sign
$PathWithScripts = 'c:\myScripts'

# apply signatures to all scripts in the folder
Get-ChildItem -Path $PathWithScripts -Filter *.ps1 -Recurse |
Set-AuthenticodeSignature -Certificate $cert

运行这段代码后,指定目录中的所有脚本都会添加上数字签名。如果您连接到了 Internet,应该考虑签名时使用时间戳服务器,并且将最后一行代码替换成这行:

1
2
3
# apply signatures to all scripts in the folder
Get-ChildItem -Path $PathWithScripts -Filter *.ps1 -Recurse |
Set-AuthenticodeSignature -Certificate $cert -TimestampServer http://timestamp.digicert.com

使用时间戳服务器会减慢签名的速度但是确保不会用过期的证书签名:例如当某天一本证书过期了,但是签名仍然有效。因为官方的时间戳服务器,签名仍然有效,因为官方的时间戳服务器证明该签名是在证书过期之前应用的。

PowerShell 技能连载 - 代码签名迷你系列(第 3 部分:从个人存储中读取证书)

可以通过加载至 Windows 证书存储的方式永久地安装证书。PowerShell 可以通过 cert: 驱动器存取这个存储。以下这行代码将显示所有个人证书:

1
2
3
4
5
6
7
8
9
10
PS C:\> Get-ChildItem -Path Cert:\CurrentUser\my


PSParentPath: Microsoft.PowerShell.Security\Certificate::CurrentUser\my

Thumbprint Subject
---------- -------
9F2F02100F6AE1DA83628906D60267F89377A6B2 CN=König von Timbuktu (Ost)
65C5ED677C9EEE9AB8D8F55354E920313FE427C2 CN=UniYork IT Security
322CA0B1F37F43B26D4D8DE17DCBF3E2C17CE111 CN=Tobias

请注意:如果您的个人证书存储是空的,您可能需要查看这个系列之前的文章来创建一些测试证书。

要只查看代码签名证书,请添加 -CodeSigningCert 动态参数。这将排除所有其它用途的证书以及没有私钥的证书:

1
PS C:\> Get-ChildItem -Path Cert:\CurrentUser\my -CodeSigningCert

证书是以它们唯一的指纹 ID 标识,它就像文件的名字一样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
PS C:\> Get-ChildItem -Path Cert:\CurrentUser\my


PSParentPath: Microsoft.PowerShell.Security\Certificate::CurrentUser\my

Thumbprint Subject
---------- -------
9F2F02100F6AE1DA83628906D60267F89377A6B2 CN=King of Timbuktu (Eastside)
65C5ED677C9EEE9AB8D8F55354E920313FE427C2 CN=UniYork IT Security
322CA0B1F37F43B26D4D8DE17DCBF3E2C17CE111 CN=Tobias


PS C:\> $cert = Get-Item -Path Cert:\CurrentUser\My\9F2F02100F6AE1DA83628906D60267F89377A6B2

PS C:\> $cert


PSParentPath: Microsoft.PowerShell.Security\Certificate::CurrentUser\My

Thumbprint Subject
---------- -------
9F2F02100F6AE1DA83628906D60267F89377A6B2 CN=King of Timbuktu (Eastside)

如果还不知道唯一的指纹 ID,您需要先找出它,因为这个 ID 能够唯一确认证书。一个查找的方法是按其它属性过滤,例如 subject:

1
2
3
4
5
6
7
8
PS C:\> dir Cert:\CurrentUser\my | where subject -like *tobias*


PSParentPath: Microsoft.PowerShell.Security\Certificate::CurrentUser\my

Thumbprint Subject
---------- -------
322CA0B1F37F43B26D4D8DE17DCBF3E2C17CE111 CN=Tobias

在本迷你系列的第一部分,已经介绍了如何用 PowerShell 创建新的证书。

现在您已经学习了如何从 pfx 文件和个人证书存储中读取已有的证书。

请关注下一个技能来学习如何使用证书来实际签名 PowerShell 代码!

PowerShell 技能连载 - 代码签名迷你系列(第 2 部分:从 PFX 文件读取证书)

在前一个技能中我们创建了新的代码签名测试证书,它既是一个 pfx 文件,同时也位于您的证书存储中。今天,我们将看看如何在 PowerShell 中加载这些(或来自其它来源的任意其它证书)。

要从 pfx 文件加载证书,请使用 Get-PfxCertificate

1
2
3
4
$Path = "$home\desktop\tobias.pfx"
$cert = Get-PfxCertificate -FilePath $Path

$cert | Select-Object -Property *

Get-PfxCertificate 将提醒输入 pfx 文件创建时输入的密码。有一些 pfx 文件并没有使用密码保护或者是通过您的用户账户来保护证书,这些情况下不会显示提示。

如果您需要自动读取 pfx 证书,以下是一个通过参数输入密码,并且可以无人值守地从 pfx 文件读取证书:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function Load-PfxCertificate
{
param
(
[String]
[Parameter(Mandatory)]
$FilePath,

[SecureString]
[Parameter(Mandatory)]
$Password
)

# get clear text password
$plaintextPassword = [PSCredential]::new("X", $Password).GetNetworkCredential().Password


[void][System.Reflection.Assembly]::LoadWithPartialName("System.Security")
$container = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2Collection
$container.Import($FilePath, $plaintextPassword, 'PersistKeySet')
$container[0]
}

以下是这个函数工作的方式:

1
2
3
4
5
6
7
8
9
PS C:\> $pwd = 'secret' | ConvertTo-SecureString -AsPlainText -Force
PS C:\> $path = "$home\desktop\tobias.pfx"
PS C:\> $cert = Load-PfxCertificate -FilePath $path -Password $pwd

PS C:\> $cert

Thumbprint Subject
---------- -------
322CA0B1F37F43B26D4D8DE17DCBF3E2C17CE111 CN=Tobias

修改 Load-PfxCertificate 的最后一行,可以支持多于一个证书。改函数永远返回第一个证书 ($container[0]),但是可以选择任意另一个下标。

请关注下一个技能,学习如何存取您个人证书存储中的证书。

PowerShell 技能连载 - 代码签名迷你系列(第 1 部分:创建证书)

要使用数字签名,以及探索如何对脚本和模块签名,您首先需要代码签名证书。如果您无法从公司的 IT 部门获取到代码签名证书,PowerShell 可以为您创建一个(假设您使用的是 Windows 10 或者 Server 2016)。

我们将这些细节封装为一个名为 New-CodeSigningCert 的易用的函数,它可以在个人证书存储中创建新的代码签名证书,并且以 pfx 文件的形式返回新创建的证书。

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
function New-CodeSigningCert
{
[CmdletBinding(DefaultParametersetName="__AllParameterSets")]
param
(
[Parameter(Mandatory)]
[String]
$FriendlyName,

[Parameter(Mandatory)]
[String]
$Name,

[Parameter(Mandatory,ParameterSetName="Export")]
[SecureString]
$Password,

[Parameter(Mandatory,ParameterSetName="Export")]
[String]
$FilePath,

[Switch]
$Trusted
)

$cert = New-SelfSignedCertificate -KeyUsage DigitalSignature -KeySpec Signature -FriendlyName $FriendlyName -Subject "CN=$Name" -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')


if ($Trusted)
{
$Store = New-Object system.security.cryptography.X509Certificates.x509Store("Root", "CurrentUser")
$Store.Open("ReadWrite")
$Store.Add($cert)
$Store.Close()
}


$parameterSet = $PSCmdlet.ParameterSetName.ToLower()

if ($parameterSet -eq "export")
{
$cert | Export-PfxCertificate -Password $Password -FilePath $FilePath
$cert | Remove-Item
explorer.exe /select,$FilePath
}
else { $cert }
}

以下是如何以 pfx 文件的形式创建代码签名证书的方法:

1
PS> New-CodeSigningCert -FriendlyName 'Tobias Code-Signing Test Cert' -Name TobiasCS -FilePath "$home\desktop\myCert.pfx"

您将会收到提示,要求输入用来保护 pfx 文件的密码。请记住该密码,一会儿导入 pfx 文件的时候需要该密码。

以下是如何在个人证书存储中创建代码签名证书的方法:

1
PS> New-CodeSigningCert -FriendlyName 'Tobias Code-Signing Test Cert' -Name TobiasCS -Trusted

调用这个函数之后,您的证书现在位于 cert: 驱动器中,您现在可以像这样查看它:

1
PS C:\> dir Cert:\CurrentUser\my

同样地,您可以打开个人证书存储来管理它:

1
PS C:\> certmgr.msc

请继续关注后续的技能,来学习现在能对代码签名证书做哪些事!

请注意自签名证书仅在复制到受信任的根证书发布者容器之后才能被信任。当使用 -Trusted 开关参数之后,会自动应用以上操作。