PowerShell 技能连载 - 导出和导入代码签名证书

在前面的技能里我们解释了如何在 Windows 10 和 Server 2016(以及更高的版本)中创建自签名的代码签名证书。今天,我们来看看如何导出这些证书,创建一个密码保护的文件,然后在不同的机器上再次使用这些证书。

假设您已经在个人证书存储中创建了一个新的代码签名证书,或者在您的证书存储中有一个来自其它来源的代码签名证书。这段代码会将证书导出为一个 PFX 文件放在桌面上:

1
2
3
4
5
6
7
8
9
# this password is required to be able to load and use the certificate later
$Password = Read-Host -Prompt 'Enter Password' -AsSecureString
# certificate will be exported to this file
$Path = "$Home\Desktop\myCert.pfx"

# certificate must be in your personal certificate store
$cert = Get-ChildItem -Path Cert:\CurrentUser\My -CodeSigningCert |
Out-GridView -Title 'Select Certificate' -OutputMode Single
$cert | Export-PfxCertificate -Password $Password -FilePath $Path

导出的过程中将会让您输入密码。由于代码签名证书是安全相关的,所以将使用密码来加密存储在 PFX 文件中的证书,并且等等加载证书的时候将需要您输入这个密码。

下一步,将在一个网格视图窗口中显示您个人证书存储中所有的代码签名证书。请选择一个您想导出的证书。

当创建了一个 PFX 文件,您可以用这行命令加载:

1
2
$cert = Get-PfxCertificate -FilePath $Path
$cert | Select-Object -Property *

Get-PfxCertificate 将会让您输入创建 PFX 文件时所输入的密码。当证书加载完,您可以执行 Set-AuthenticodeSignature 用它来签名文件。

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

Windows 10 和 Serve 2016(以及更高版本)带有一个高级的 New-SelfSignedCert cmdlet,它可以用来创建代码签名证书。通过代码签名证书,您可以对 PowerShell 脚本进行数字签名并使用这个签名来检测用户是否改篡改过脚本内容。

以下是一个用来创建代码签名证书的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function New-CodeSigningCert
{
param
(
[Parameter(Mandatory, Position=0)]
[System.String]
$FriendlyName,

[Parameter(Mandatory, Position=1)]
[System.String]
$Name
)

# Create a certificate:
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')
}

要创建一个新的证书,请运行这行代码:

1
2
3
4
5
6
7
8
PS> New-CodeSigningCert -FriendlyName TobiasWeltner -Name TWeltner


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

Thumbprint Subject
---------- -------
2350D77A4CACAF17136B94D297DEB1A5E413655D CN=TWeltner

使用新的代码签名证书,您可以对脚本进行数字签名。代码签名证书位于个人证书存储中。要使用它,需要先从存储中读取它:

1
2
$cert = Get-ChildItem -Path Cert:\CurrentUser\My -CodeSigningCert |
Out-GridView -Title 'Select Certificate' -OutputMode Single

要签名一个单独的脚本,请使用这行代码:

1
2
3
$Path = "C:\path\to\your\script.ps1"

Set-AuthenticodeSignature -Certificate $cert -FilePath $Path

如果您希望通过时间戳签名,请使用这行代码:

1
Set-AuthenticodeSignature -Certificate $cert -TimestampServer http://timestamp.digicert.com -FilePath $Path

当签名使用的证书过期以后,时间戳签名仍然有效。

要批量签名多个脚本,请使用 Get-ChildItem 并用管道将文件传送到 Set-AuthenticodeSignature。这行代码将对用户配置文件中的所有 PowerShell 脚本签名:

1
2
Get-ChildItem -Path "$home\Documents" -Filter *.ps1 -Include *.ps1 -Recurse |
Set-AuthenticodeSignature -Certificate $cert

当您得到了签名后的脚本,随时可以使用 Get-AuthenticodeSignature 来检查签名的完整性:

1
2
Get-ChildItem -Path "$home\Documents" -Filter *.ps1 -Include *.ps1 -Recurse |
Get-AuthenticodeSignature

PowerShell 技能连载 - 使用目录文件来维护文件夹完整性

如果您希望确保一个文件夹的内容保持不变,那么可以使用目录文件。目录文件可以列出所有文件夹内容并为文件夹中的每个文件创建哈希。以下是一个例子:

1
2
3
4
5
6
7
# path to folder to create a catalog file for
# (make sure it exists and isn't too large)
$path = "$Home\Desktop"
# path to catalog file to be created
$catPath = "$env:temp\myDesktop.cat"
# create catalog
New-FileCatalog -Path $path -CatalogVersion 2.0 -CatalogFilePath $catPath

根据文件夹的大小,可能需要一些时间来创建目录文件。您无法创建被锁定和在使用中的文件的目录。生成的目录文件是一个二进制文件并且包含目录中所有文件的哈希。

要检查文件夹是否未被该国,您可以使用 Test-FileCatalog 命令:

1
2
3
4
5
6
7
8
PS> Test-FileCatalog -Detailed -Path $path -CatalogFilePath $catPath


Status : Valid
HashAlgorithm : SHA256
CatalogItems : {...}
PathItems : {...}
Signature : System.Management.Automation.Signature

如果文件夹内容和目录相匹配,那么结果状态为 “Valid”。否则,CatalogItems 属性将包含一个文件夹中所有内容的详细列表,以及它们是否变更过的标志。

PowerShell 技能连载 - 查找 PowerShell 命名管道

每个运行 PowerShell 5 以及以上版本的 PowerShell 宿主都会打开一个能被检测到的“命名管道”。以下代码检测这些命名管道并返回暴露这些管道的进程:

1
2
3
4
Get-ChildItem -Path "\\.\pipe\" -Filter '*pshost*' |
ForEach-Object {
Get-Process -Id $_.Name.Split('.')[2]
}

结果看起来类似这样:

Handles  NPM(K)    PM(K)      WS(K)     CPU(s)     Id  SI ProcessName
-------  ------    -----      -----     ------     --  -- -----------
   1204      98   306220      66620      63,30  28644   1 powershell_ise
    525      29    72604      12708       5,64  12188   1 powershell
    741      41   125728     142656      11,52  27144   1 powershell
    835      61    40836      82624       1,44  22412   1 pwsh
    820      49   199680     230632       2,86  26500   1 powershell_ise

这里列出的每个进程都启动了一个 PowerShell 运行空间。您可以使用 Enter-PSHostProcess -Id XXX 来连接到 PowerShell 进程(假设您有本地管理员特权)。

PowerShell 技能连载 - 查找最新的 PowerShell 6 发布

PowerShell 6 是开源的,所以经常发布新的更新。您随时可以访问 https://github.com/PowerShell/PowerShell/releases 来查看这些更新。

对于 PowerShell 来说,可以使这步操作自动化。以下是一小段读取 GitHub 发布的 RSS 供稿的代码,它能够正确地转换数据,然后取出这些更新以及下载信息,并按降序排列:

1
2
3
4
5
$AllProtocols = [Net.SecurityProtocolType]'Ssl3,Tls,Tls11,Tls12'[Net.ServicePointManager]::SecurityProtocol = $AllProtocols $Updated = @{
Name = 'Updated' Expression = { $_.Updated -as [DateTime] }
}$Link = @{
Name = 'URL' Expression = { $_.Link.href }
}Invoke-RestMethod -Uri https://github.com/PowerShell/PowerShell/releases.atom -UseBasicParsing | Sort-Object -Property Updated -Descending | Select-Object -Property Title, $Updated, $Link

结果类似这样:

title                                       Updated             URL
-----                                       -------             ---
v6.2.0 Release of PowerShell Core           28.03.2019 19:52:27 https://github.com/PowerShell/PowerShell/releases/tag/v6.2.0
v6.2.0-rc.1 Release of PowerShell Core      05.03.2019 23:47:46 https://github.com/PowerShell/PowerShell/releases/tag/v6.2.0-rc.1
v6.1.3 Release of PowerShell Core           19.02.2019 19:32:01 https://github.com/PowerShell/PowerShell/releases/tag/v6.1.3
v6.2.0-preview.4 Release of PowerShell Core 28.01.2019 22:28:01 https://github.com/PowerShell/PowerShell/releases/tag/v6.2.0-preview.4
v6.1.2 Release of PowerShell Core           15.01.2019 21:02:39 https://github.com/PowerShell/PowerShell/releases/tag/v6.1.2
v6.2.0-preview.3 Release of PowerShell Core 11.12.2018 01:29:33 https://github.com/PowerShell/PowerShell/releases/tag/v6.2.0-preview.3
v6.2.0-preview.2 Release of PowerShell Core 16.11.2018 02:52:53 https://github.com/PowerShell/PowerShell/releases/tag/v6.2.0-preview.2
v6.1.1 Release of PowerShell Core           13.11.2018 20:55:45 https://github.com/PowerShell/PowerShell/releases/tag/v6.1.1
v6.0.5 Release of PowerShell Core           13.11.2018 19:00:56 https://github.com/PowerShell/PowerShell/releases/tag/v6.0.5
v6.2.0-preview.1 Release of PowerShell Core 18.10.2018 02:07:32 https://github.com/PowerShell/PowerShell/releases/tag/v6.2.0-preview.1

请注意,只有在 Windows 10 1803 之前才需要显式启用 SSL。

PowerShell 技能连载 - 查找最新的 PowerShell 6 下载地址

PowerShell 6 是开源的,所以经常发布新的更新。以下是如何查找最新的 PowerShell 6 发布地址的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$AllProtocols = [Net.SecurityProtocolType]'Ssl3,Tls,Tls11,Tls12'
[Net.ServicePointManager]::SecurityProtocol = $AllProtocols


# get the URL for the latest PowerShell 6 release
$url = "https://github.com/PowerShell/PowerShell/releases/latest?dummy=$(Get-Random)"
$request = [System.Net.WebRequest]::Create($url)
$request.AllowAutoRedirect=$false
$response = $request.GetResponse()
$realURL = $response.GetResponseHeader("Location")
$response.Close()
$response.Dispose()

# get the current version from that URL
$v = ($realURL -split '/v')[-1]

# create the download URL for the release of choice
# (adjust the end part to target the desired platform, architecture, and package format)
$platform = "win-x64.zip"
$static = "https://github.com/PowerShell/PowerShell/releases/download"
$url = "$static/v$version/PowerShell-$version-$platform"

这段代码生成 ZIP 格式的 64 位 Windows 版下载链接。如果您需要不同的发行版,只需要调整 $platform 中定义的平台部分。

当获得了下载链接,您可以通过它自动完成剩下的步骤:下载 ZIP 文件,取消禁用并解压,然后执行 PowerShell 6:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# define the place to download to
$destinationFile = "$env:temp\PS6\powershell6.zip"
$destinationFolder = Split-Path -Path $destinationFile

# create destination folder if it is not present
$existsDestination = Test-Path -Path $destinationFolder
if ($existsDestination -eq $false)
{
$null = New-Item -Path $destinationFolder -Force -ItemType Directory
}

# download file
Invoke-WebRequest -Uri $url -OutFile $destinationFile
# unblock downloaded file
Unblock-File -Path $destinationFile
# extract file
Expand-Archive -Path $destinationFile -DestinationPath $destinationFolder -Force

最终,在桌面上创建一个快捷方式,指向 PowerShell 6 这样可以快捷地启动 shell:

1
2
3
4
5
6
7
8
9
10
11
# place a shortcut on your desktop
$path = "$Home\Desktop\powershell6.lnk"
$obj = New-Object -ComObject WScript.Shell
$scut = $obj.CreateShortcut($path)
$scut.TargetPath = "$destinationFolder\pwsh.exe"
$scut.IconLocation = "$destinationFolder\pwsh.exe,0"
$scut.WorkingDirectory = "$home\Documents"
$scut.Save()

# run PowerShell 6
Invoke-Item -Path $path

PowerShell 技能连载 - 查找最新的 PowerShell 6 发行信息(以及下载地址)

PowerShell 6 是开源的并且在 GitHub 上维护了一个公共的仓库。在仓库中频繁发行新版本。

如果您不希望深入到 GitHub 的前端来获取最新版 PowerShell 6 发行的下载地址,那么可以采用这个 PowerShell 的方法:

1
2
3
4
5
6
7
8
9
10
11
$AllProtocols = [Net.SecurityProtocolType]'Ssl3,Tls,Tls11,Tls12'
[Net.ServicePointManager]::SecurityProtocol = $AllProtocols

# get all releases
Invoke-RestMethod -Uri https://github.com/PowerShell/PowerShell/releases.atom -UseBasicParsing |
# sort in descending order
Sort-Object -Property Updated -Descending |
# pick the first (newest) release and get a link
Select-Object -ExpandProperty Link -First 1 |
# pick a URL
Select-Object -ExpandProperty HRef

(请注意只有在 Windows 10 1803 之前才需要显式启用 SSL。)

这将货渠道最新的 PowerShell 发行页面的 URL。在页面中,您可以获取到不同平台的下载地址。

不过,还有一个更简单的方法:访问 https://github.com/PowerShell/PowerShell/releases/latest

然而,这并不能提供 URL 和标签信息。而只是被跳转到一个对应的 URL。

以下是两者的混合:使用最新发行版的快捷方式,但是不允许跳转。通过这种方式,PowerShell 将返回完整的 URL:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$AllProtocols = [Net.SecurityProtocolType]'Ssl3,Tls,Tls11,Tls12'
[Net.ServicePointManager]::SecurityProtocol = $AllProtocols


# add a random number to the URL to trick proxies
$url = "https://github.com/PowerShell/PowerShell/releases/latest?dummy=$(Get-Random)"

$request = [System.Net.WebRequest]::Create($url)
# do not allow to redirect. The result is a "MovedPermanently"
$request.AllowAutoRedirect=$false
# send the request
$response = $request.GetResponse()
# get back the URL of the true destination page, and split off the version
$realURL = $response.GetResponseHeader("Location")
# make sure to clean up
$response.Close()
$response.Dispose()

$realURL

PowerShell 技能连载 - PowerShell ISE 模块浏览器

如果您在使用内置的 PowerShell ISE,您也许发现 “Module Browser Add-on” 很有用。它十分古老了,是 2015 年发布的,然而您可以方便滴从 PowerShell Gallery 中下载安装它:

1
PS C:\> Install-Module ISEModuleBrowserAddOn -Repository PSGallery -Scope CurrentUser

当这个模块安装好,您就可以这样将它加载到 PowerShell ISE 中:

1
PS C:\> Import-Module -Name ISEModuleBrowserAddon -Verbose

这将在 PowerShell 的右侧打开一个新的 Add-on 面板,其中有三个类别:Gallery,Favorites,以及 My Collection。

“Gallery” 将您连接到在线的 PowerShell Gallery,最初创建该库是为了帮助更容易地发现 PowerShell Gallery 中的在线内容。不过这部分看上去不能工作了。

不过当您点击 “My Collection” 时,将会获取到所有模块,并且当您双击列表中的一个模块时,可以获取模块中的内容,例如包含的命令。您也可以讲一个模块标注为 “Favorite”(它就会出现在 “Favorites” 列表中),卸载一个模块,或通过模块列表底部的按钮打开它。

通过顶部的 “New Module” 按钮,您可以创建一个新的空白 PowerShell 模块:一个向导将引导您完成采集元数据并将建有关文件的所有步骤。

PowerShell 技能连载 - 将 SecureString 转换为字符串

有些时候需要将 SecureString 转换为普通字符串,例如因为您使用了由 Read-Host 提供的安全输入:

1
$secret = Read-Host -Prompt 'Enter Keypass' -AsSecureString

这将提示用户输入密码,并且输入的内容将变为一个 SecureString:

1
2
PS> $secret
System.Security.SecureString

要将它还原为纯文本,请使用 SecureString 来创建一个 PSCredential 对象,它包含了一个解密密码的方法:

1
2
$secret = Read-Host -Prompt 'Enter Keypass' -AsSecureString
[System.Management.Automation.PSCredential]::new('hehe',$secret).GetNetworkCredential().Password

PowerShell 技能连载 - Real-Time Processing for Language Structures

In the previous tip we looked at queues and how they can search the entire file system:

# create a new queue
$dirs = [System.Collections.Queue]::new()

# add an initial path to the queue
# any folder path in the queue will later be processed
$dirs.Enqueue('c:\windows')

# process all elements on the queue until all are taken
While ($current = $dirs.Dequeue())
{
    # find subfolders of current folder, and if present,
    # add them all to the queue
    try
    {
        foreach ($_ in [IO.Directory]::GetDirectories($current))
        {
                $dirs.Enqueue($_)
        }
    } catch {}

    try
    {
        # find all files in the folder currently processed
        [IO.Directory]::GetFiles($current, "*.exe")
        [IO.Directory]::GetFiles($current, "*.ps1")
    } catch { }
}

How would you process the data created by the loop though, i.e. to display it in a grid view window? You cannot pipe it in real-time, so this fails:

$dirs = [System.Collections.Queue]::new()
$dirs.Enqueue('c:\windows')

While ($current = $dirs.Dequeue())
{
    try
    {
        foreach ($_ in [IO.Directory]::GetDirectories($current))
        {
                $dirs.Enqueue($_)
        }
    } catch {}

    try
    {
        [IO.Directory]::GetFiles($current, "*.exe")
        [IO.Directory]::GetFiles($current, "*.ps1")
    } catch { }
# this fails
} | Out-GridView

You can save the results produced by do-while to a variable. That works but takes forever because you’d have to wait for the loop to complete until you can do something with the variable:

$dirs = [System.Collections.Queue]::new()
$dirs.Enqueue('c:\windows')

# save results to variable...
$all = while ($current = $dirs.Dequeue())
{
    try
    {
        foreach ($_ in [IO.Directory]::GetDirectories($current))
        {
                $dirs.Enqueue($_)
        }
    } catch {}

    try
    {
        [IO.Directory]::GetFiles($current, "*.exe")
        [IO.Directory]::GetFiles($current, "*.ps1")
    } catch { }
}

# then process or output
$all | Out-GridView

The same limitation applies when you use $() or other constructs. To process the results emitted by do-while in true real-time, use a script block instead:

$dirs = [System.Collections.Queue]::new()
$dirs.Enqueue('c:\windows')

# run the code in a script block
& { while ($current = $dirs.Dequeue())
    {
        try
        {
            foreach ($_ in [IO.Directory]::GetDirectories($current))
            {
                    $dirs.Enqueue($_)
            }
        } catch {}

        try
        {
            [IO.Directory]::GetFiles($current, "*.exe")
            [IO.Directory]::GetFiles($current, "*.ps1")
        } catch { }
    }
} | Out-GridView

With this approach, results start to show in the grid view window almost momentarily, and you don’t have to wait for the loop to complete.


psconf.eu – PowerShell Conference EU 2019 – June 4-7, Hannover Germany – visit www.psconf.eu There aren’t too many trainings around for experienced PowerShell scripters where you really still learn something new. But there’s one place you don’t want to miss: PowerShell Conference EU - with 40 renown international speakers including PowerShell team members and MVPs, plus 350 professional and creative PowerShell scripters. Registration is open at www.psconf.eu, and the full 3-track 4-days agenda becomes available soon. Once a year it’s just a smart move to come together, update know-how, learn about security and mitigations, and bring home fresh ideas and authoritative guidance. We’d sure love to see and hear from you!

Twitter This Tip!ReTweet this Tip!