PowerShell 技能连载 - 读取 Windows 10 产品序列号

有很多脚本可以通过转换一系列二进制值来读取注册表的原始 Windows 10 产品序列号。

不幸的是,这些脚本中找到的大多数算法都是弃用的,并且计算出的产品密钥是错误的。如果您计划重新安装操作系统,则有可能在删除了原来的系统后才发现。

这行代码可能是一种更好,更简单的方法:

1
PS> (Get-CimInstance -ClassName SoftwareLicensingService).OA3xOriginalProductKey

如果它返回 “nothing”,则机器中没有安装产品序列号。否则,它将原样返回。

PowerShell 技能连载 - 为 PowerShell 创建 sudo(第 2 部分)

当我们尝试为 PowerShell 创建一个 sudo 命令——来提升单个命令的权限——在第一部分中我们创建了 sudo 函数体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function sudo
{
param
(
[Parameter(Mandatory)]
[string]
$FilePath,

[Parameter(ValueFromRemainingArguments)]
[string[]]
$ArgumentList

)

$PSBoundParameters
}

现在,让我们用实际的逻辑替换 $PSBoundParameters,以运行提升权限的命令。Start-Process 可以解决这个问题。例如,这段代码将在用户 “Tobias” 下启动提升权限的记事本:

1
PS> Start-Process -FilePath notepad -ArgumentList $env:windir\system32\drivers\etc\hosts -Verb runas

巧合的是,我们的 sudo 函数体的参数名称与 Start-Process 所需的参数名称匹配,因此实现很简单:使用 splatting,并且将用户在自动定义的 $PSBoundParameters 哈希表中传入的参数传递给 Start-Process——这就是所有步骤:

并传递在自动定义的哈希表$ psboundparameters中找到的用户供给参数,以启动过程 - 全部:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function sudo
{
param
(
[Parameter(Mandatory)]
[string]
$FilePath,

[Parameter(ValueFromRemainingArguments)]
[string[]]
$ArgumentList

)

Start-Process @PSBoundParameters -Verb Runas
}

运行代码,然后测试新的 sudo 命令。事实证明,您现在可以在脚本中运行提升权限的单个命令。同时,您将体会到 Windows 中的主要设计差异:所有命令都在自己的窗口中运行,并且无法从提升权限的命令中将结果重定向到您的 PowerShell。

虽然我们的 sudo 命令可能对提升权限很有用,但是当您想从提升权限的命令中获取信息时会受到限制。Windows 架构禁止该操作。

您在过程中学到了如何创建函数参数,例如接受可变数量参数的 “ArgumentList”。

PowerShell 技能连载 - 为 PowerShell 创建 sudo(第 1 部分)

在 Linux Shells 中,有一个名为 “sudo“ 的命令,可以通过它运行具有提升特权的命令。在 Powershell 中,您必须打开一个具有更高特权的全新 shell。

让我们尝试将 sudo 命令添加到 PowerShell。 我们想要一个名为 “sudo” 的新命令,它至少需要一个命令名称,但随后还需要一个可变的空格分隔的参数。这是函数的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function sudo
{
param
(
[Parameter(Mandatory)]
[string]
$FilePath,

[Parameter(ValueFromRemainingArguments)]
[string[]]
$ArgumentList
)

$PSBoundParameters
}

param() 块定义输入参数。$FilePath 是强制性的(必须)。$Arguments 是可选的,但通过属性 ValueFromRemainingArguments 装饰,因此它是一个所谓的“参数数组”,它能接受剩下的所有未绑定到其它形参的输入参数。

运行这段代码,然后尝试一些用例。$PSBoundParameters 显示该函数如何接收您的输入参数:

这是我测试的内容,它似乎按预期工作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
PS> sudo notepad c:\test

Key Value
--- -----
FilePath notepad
ArgumentList {c:\test}



PS> sudo ping 127.0.0.1 -n 1

Key Value
--- -----
FilePath ping
ArgumentList {127.0.0.1, -n, 1}

Now that the sudo function body works, in part 2 we look at the actual implantation of running commands elevated.
现在,sudo 函数体能正常工作,在第二部分中,我们将学习实际注入一个函数并提升权限。

PowerShell 技能连载 - 签名 PowerShell 脚本(第 3 部分)

在上一部分中,我们创建了一个代码签名证书,并将其用于将数字签名添加到 PowerShell 脚本文件中。 然而,将数字签名添加到 PowerShell 脚本文件中可以做什么?

使用 Get-AuthenticodeSignature 可以查看数字签名中的秘密。只需确保您调整 $Path 以指向具有数字签名的文件:

1
2
3
4
# path to a digitally signed file (adjust path to an existing signed ps1 file):
$Path = "$env:temp\test.ps1"
$Status = Get-AuthenticodeSignature -FilePath $Path | Select-Object -Property *
$Status

结果类似这样:

SignerCertificate      : [Subject]
                           CN=MyPowerShellCode

                         [Issuer]
                           CN=MyPowerShellCode

                         [Serial Number]
                           1B98E986A1D7BCB245034A0225381CA4

                         [Not Before]
                           01.05.2022 17:39:56

                         [Not After]
                           01.05.2027 17:49:56

                         [Thumbprint]
                           F4C1F9978D564E143D554F3679746B3A79E1FF87

TimeStamperCertificate :
Status                 : UnknownError
StatusMessage          : A certificate chain processed, but terminated in a root certificate which is not trusted by the trust provider
Path                   : C:\Users\tobia\AppData\Local\Temp\test.ps1
SignatureType          : Authenticode
IsOSBinary             : False

最重要的返回属性是 “Status”(及其在 “StatusMessage” 中找到的友好信息)。该属性告诉您签名的文件外的印章是否值得信赖和未被篡改:

| Status | Description |
|— |— |
| UnknownError | 文件未被篡改,但是签名用的数字证书可能不受信任 |
| HashMismatch | 自上次签名依赖,文件内容已改变 |
| Valid | 文件未被篡改,且数字证书受信任 |
| NotSigned | 文件未携带数字证书 |

最有可能的是,您会看到状态 “UnknownError”(如果您添加签名以来文件内容没有更改)或 “HashMismatch”(您或其他人确实更改了文件)。

您可能看不到 “Valid” 的原因是我们在此迷你系列中使用的证书类型:任何人都可以创建一个自签名的证书,以便攻击者可以更改脚本文件,然后使用他拥有自签名的证书以重新签名文件。

由于每个证书——公司或自签名——始终都有其独特的指纹,因此即使是对于自签名证书,您也可以通过同时检查 “Status” 和 “SignerCertificate.Thumbprint”,来确认脚本的完整性。当您还检查证书指纹时,邪恶的人就无法在不更改指纹的情况下重新签名脚本:

1
2
3
4
5
6
7
8
# thumbprint of your certificate (adjust to match yours)
$thumbprint = 'F4C1F9978D564E143D554F3679746B3A79E1FF87'

# path to a digitally signed file (adjust path to an existing signed ps1 file):
$Path = "$env:temp\test.ps1"
$Status = Get-AuthenticodeSignature -FilePath $Path | Select-Object -Property *
$ok = $Status.Status -eq 'Valid' -or ($status.Status -eq 'UnknownError' -and $status.SignerCertificate.Thumbprint -eq $thumbprint)
"Script ok: $ok"

PowerShell 技能连载 - 签名 PowerShell 脚本(第 2 部分)

在上一个技能中,我们解释了如何使用 New-SeftSignedCert 创建自签名的代码签名证书。今天,我们将使用自签名或公司代码签名证书真实地对 PowerShell 脚本进行数字签名。

为此,请使用您想要的任何 PowerShell 脚本文件。您所需要的只是它的路径。另外,您需要一个在 Windows 证书存储中存储的有效代码签名证书的路径。这是第一部分的快速回顾,以创建自签名证书,以防您没有公司证书:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$Subject = 'MyPowerShellCode'
$FriendlyName = 'My Valid PowerShell Code'

# expires 5 years from now:
$ExpirationDate = (Get-Date).AddYears(5)

# store in user personal store:
$certStore = 'Cert:\CurrentUser\my'

# create certificate:
$cert = New-SelfSignedCertificate -Subject $Subject -Type CodeSigningCert -CertStoreLocation $certStore -FriendlyName $FriendlyName -NotAfter $ExpirationDate

$thumbprint = $cert.Thumbprint

$Path = Join-Path -Path $certStore -ChildPath $thumbprint
Write-Warning "Certificate Path: $Path"

运行此代码时,您将获得一个自签名的代码签名证书,并且该代码将返回生成证书的路径,即:

Cert:\CurrentUser\my\F4C1F9978D564E143D554F3679746B3A79E1FF87

要使用您的证书,请像这样通过 Get-Item 读取它(确保修改匹配证书的路径 - 每个证书都有唯一的指纹):

1
PS> $myCert = Get-Item -Path Cert:\CurrentUser\my\F4C1F9978D564E143D554F3679746B3A79E1FF87

要将数字签名添加到 PowerShell 脚本文件(或其他能够为此问题携带数字签名的文件),请使用 Set-AuthenticodeSignature。运行以下演示代码(根据需要调整文件和证书的路径):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# digitally sign this file (adjust path to an existing ps1 file):
$Path = "$env:temp\test.ps1"

# adjust this path to point to a valid code signing certificate:
$CertPath = 'Cert:\CurrentUser\my\F4C1F9978D564E143D554F3679746B3A79E1FF87'

# if it does not exist, create a dummy file
$exists = Test-Path -Path $Path
if ($exists -eq $false)
{
'Hello World!' | Set-Content -Path $Path -Encoding UTF8
}

# read a code signing certificate to use for signing:
$myCert = Get-Item -Path $CertPath

# add a digital signature to a PS script file:
Set-AuthenticodeSignature -FilePath $Path -Certificate $myCert

# show changes inside script file:
notepad $Path

运行此代码时,在 $Path 中指定的脚本文件将打开并显示添加到脚本底部的数字签名:

Hello World!

# SIG # Begin signature block
# MIIFcAYJKoZIhvcNAQcCoIIFYTCCBV0CAQExCzAJBgUrDgMCGgUAMGkGCisGAQQB
# gjcCAQSgWzBZMDQGCisGAQQBgjcCAR4wJgIDAQAABBAfzDtgWUsITrck0sYpfvNR
# AgEAAgEAAgEAAgEAAgEAMCEwCQYFKw4DAhoFAAQU4QK+x7NLicgrIdzN+Nvxqbuq
# Qv2gggMKMIIDBjCCAe6gAwIBAgIQG5jphqHXvLJFA0oCJTgcpDANBgkqhkiG9w0B
...

恭喜,您刚刚已经对一个 PowerShell 脚本进行了数字签名!我们将在第三部分中探讨这种签名的好处。

PowerShell 技能连载 - 签名 PowerShell 脚本(第 1 部分)

如今,将数字签名添加到 PowerShell 脚本不再是黑魔法,尽管理想情况下需要从公司或可信赖的权威中获得正式的“信任”代码签名证书,但现在不再是强制性的。

在进行代码签名之前,首先了解为什么要对 PowerShell 脚本进行签名?

数字签名是脚本代码的哈希,并使用数字证书的私钥进行加密。将其视为针对脚本的“包装器”或“密封”。有了它,您现在可以随时分辨出某人是否篡改了您的脚本。没有它,您将无法实现这个目的。

如果您尚未拥有适合代码签名的数字证书,那么在 Windows 机器上,您可以使用 New-SelfSignedCertificate cmdlet 快速创建一个。

运行此行代码以创建自己的全新代码签名证书,并将其存储在您的个人证书存储中(除非您使用 -NotAfter 指定不同的到期日期,否则有效期为一年):

1
2
3
4
5
6
7
8
PS> New-SelfSignedCertificate -Subject MyPowerShellCode -Type CodeSigningCert -CertStoreLocation Cert:\CurrentUser\my -FriendlyName 'My Valid PowerShell Code'


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

Thumbprint Subject
---------- -------
57402F9D82231CABA4586127C99819F055AA2AF2 CN=MyPowerShellCode

要稍后在任何时候检索它,请记住它的指纹并像这样访问它(修改指纹以匹配您的其中一个证书):

1
2
3
4
5
6
7
8
9
10
11
PS> $cert = Get-Item -Path Cert:\CurrentUser\My\57402F9D82231CABA4586127C99819F055AA2AF2

PS> $cert.Subject
CN=MyPowerShellCode

PS> $cert.FriendlyName
My Valid PowerShell Code

PS> $cert.NotAfter

Monday, May 1, 2023 17:47:43

或者,如果您只记得主题或友好名称,则可以使用过滤器:

1
2
3
4
5
6
7
8
PS> Get-ChildItem -Path Cert:\CurrentUser\My -CodeSigningCert | Where-Object Subject -like *MyPowerShell*


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

Thumbprint Subject
---------- -------
57402F9D82231CABA4586127C99819F055AA2AF2 CN=MyPowerShellCode

在下一个技能中,我们将开始使用此证书签署 PowerShell 脚本。

PowerShell 技能连载 - 专业地处理错误

通常,PowerShell 脚本使用这样简单的错误报告形式,该报告的结构是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
# clearing global error list:
$error.Clear()
# hiding errors:
$ErrorActionPreference = 'SilentlyContinue'

# do stuff:
Stop-Service -Name Spooler
dir c:\gibtsnichtabc


# check errors at end:
$error.Count
$error | Out-GridView

尽管这没有错,但您应该了解 $error 是一个全局变量,因此,如果您在脚本中使用外部代码(即其他人写的功能或模块),这些作者可能已经使用了相同的技术,并且如果他们产生了错误并清除了全局错误列表,那么您将失去以前记录的错误。

一种更好,更健壮的方式是使用私有变量进行记录。 实际上并没有重写太多的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# hiding errors:
$ErrorActionPreference = 'SilentlyContinue'
# telling all cmdlets to use a private variable for error logging:
$PSDefaultParameterValues.Add('*:ErrorVariable', '+myErrors')
# initializing the variable:
$myErrors = $null

# do stuff:
Stop-Service -Name Spooler
dir c:\gibtsnichtabc


# check errors at end USING PRIVATE VARIABLE:
$myErrors.Count
$myErrors | Out-GridView

基本技巧是定义 -ErrorVariable 的默认参数值,并为其分配私有变量的名称。确保在名称之前添加一个 “+”,以便附加新错误,而不是覆盖现有错误。

PowerShell 技能连载 - 清理硬盘(第 2 部分)

在上一篇文章中,我们介绍了 Windows 工具 cleanmgr 及其参数 /sageset/sagerun,您可以用它们来定义和运行自动硬盘清理。

今天,我们将研究如何自定义 cleanmgr.exe 执行的实际清理任务。

此工具将所有配置存储在 Windows 注册表的这个位置:

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VolumeCaches

在下面,您可以找到 cleanmgr 可以执行的每项清理任务的关键。在上一个技能中,我们定义了这样的自定义清理任务(请记住,使用提升的管理员权限执行它):

1
PS> cleanmgr.exe /sageset:5388

这行命令打开一个对话框窗口,您可以在其中检查应绑定到已提交 ID 5388 的清理任务。

关闭对话框后,可以在 Windows 注册表中找到这些设置:

1
2
3
4
5
6
7
8
9
10
11
12
13
# the ID you picked when saving the options:
$id = 5388

# the name of the reg value that stores your choices:
$flag = "StateFlags$id"

# the location where user choices are stored:
$path = "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VolumeCaches\*"

# get all subkeys where your user choice was enabled:
Get-Item -Path $path |
Where-Object { $_.Property -contains $flag } # contains your registry value (StateFlags5388)
Where-Object { $_.GetValue($flag) -gt 0 } # your registry value contains a number greater than 0

执行结果准确列出了您先前在选择对话框中检查的清理器模块:

Name                           Property
----                           --------
Active Setup Temp Folders      (default)      : {C0E13E61-0CC6-11d1-BBB6-0060978B2AE6}
                               Autorun        : 1
                               Description    : These files should no longer be needed. They were originally
                                                created by a setup program that is no longer running.
                               FileList       : *.tmp
                               Flags          : {124, 0, 0, 0}
                               Folder         : C:\Windows\msdownld.tmp|?:\msdownld.tmp
                               LastAccess     : {2, 0, 0, 0}
                               Priority       : 50
                               StateFlags0001 : 2
                               StateFlags0003 : 2
                               StateFlags0033 : 0
                               StateFlags6254 : 0
                               StateFlags5388 : 2
BranchCache                    (default)      : {DE661907-527D-4d6a-B6A6-EBC7F88D9B95}
                               StateFlags0001 : 0
                               StateFlags0003 : 0
                               StateFlags0033 : 0
                               StateFlags6254 : 0
                               StateFlags5388 : 2
D3D Shader Cache               (default)      : {D8D133CD-3F26-402F-86DA-90B710751C2C}
                               Autorun        : 1
                               ReserveIDHint  : 2
                               StateFlags0001 : 0
                               StateFlags0003 : 0
                               StateFlags0033 : 0
                               StateFlags6254 : 0
                               StateFlags5388 : 2
Delivery Optimization Files    (default)      : {4057C1AD-A51F-40BB-B960-22888CEB9812}
                               Autorun        : 0
                               Description    : @C:\WINDOWS\system32\domgmt.dll,-104
                               Display        : @C:\WINDOWS\system32\domgmt.dll,-103
                               Flags          : 128
                               ReserveIDHint  : 2
                               StateFlags0001 : 0
                               StateFlags0003 : 0
                               StateFlags0033 : 0
                               StateFlags6254 : 0
                               StateFlags5388 : 2
Diagnostic Data Viewer         (default)        : {C0E13E61-0CC6-11d1-BBB6-0060978B2AE6}
database files                 Autorun          : 0
                               CleanupString    : rundll32.exe utcutil.dll,DiskCleanupEnd
                               Description      : @C:\WINDOWS\system32\utcutil.dll,-302
                               Display          : @C:\WINDOWS\system32\utcutil.dll,-301
                               FileList         : *.*
                               Flags            : 573
                               Folder           : C:\ProgramData\Microsoft\Diagnosis\EventTranscript\
                               IconPath         : C:\WINDOWS\system32\utcutil.dll,0
                               PreCleanupString : rundll32.exe utcutil.dll,DiskCleanupStart
                               Priority         : 100
                               StateFlags0001   : 0
                               StateFlags0003   : 0
                               StateFlags0033   : 0
                               StateFlags6254   : 0
                               StateFlags5388   : 2
Downloaded Program Files       (default)          : {8369AB20-56C9-11D0-94E8-00AA0059CE02}
                               AdvancedButtonText : @C:\Windows\System32\occache.dll,-1072
                               Autorun            : 1
                               Description        : @C:\Windows\System32\occache.dll,-1071
                               Display            : @C:\Windows\System32\occache.dll,-1070
                               Priority           : {100, 0, 0, 0}
                               StateFlags0001     : 2
                               StateFlags0003     : 2
                               StateFlags0033     : 0
                               StateFlags6254     : 2
                               StateFlags5388     : 2
Internet Cache Files           (default)          : {9B0EFD60-F7B0-11D0-BAEF-00C04FC308C9}
                               AdvancedButtonText : &View Files
                               Autorun            : 1
                               Description        : The Temporary Internet Files folder contains Web pages stored
                                                    on your hard disk for quick viewing.
                                                    Your personalized settings for Web pages will be left intact.
                               Display            : Temporary Internet Files
                               Priority           : 100
                               StateFlags0001     : 2
                               StateFlags0003     : 2
                               StateFlags0033     : 0
                               StateFlags6254     : 2
                               StateFlags5388     : 2
Language Pack                  (default)      : {191D5A6B-43B9-477A-BB22-656BF91228AB}
                               Autorun        : 1
                               StateFlags0001 : 0
                               StateFlags0003 : 0
                               StateFlags0033 : 0
                               StateFlags6254 : 0
                               StateFlags5388 : 2
Old ChkDsk Files               (default)      : {C0E13E61-0CC6-11d1-BBB6-0060978B2AE6}
                               Autorun        : 1
                               FileList       : *.CHK
                               Flags          : 288
                               Folder         : ?:\FOUND.000|?:\FOUND.001|?:\FOUND.002|?:\FOUND.003|?:\FOUND.004|
                                                ?:\FOUND.005|?:\FOUND.006|?:\FOUND.007|?:\FOUND.008|?:\FOUND.009
                               IconPath       : C:\WINDOWS\System32\DATACLEN.DLL,3
                               Priority       : 50
                               PropertyBag    : {60F6E464-4DEF-11d2-B2D9-00C04F8EEC8C}
                               StateFlags0001 : 2
                               StateFlags0003 : 2
                               StateFlags0033 : 0
                               StateFlags6254 : 2
                               StateFlags5388 : 2
Recycle Bin                    (default)      : {5ef4af3a-f726-11d0-b8a2-00c04fc309a4}
                               PluginType     : 2
                               StateFlags0001 : 2
                               StateFlags0003 : 0
                               StateFlags0033 : 0
                               StateFlags6254 : 0
                               StateFlags5388 : 2

每个清理器模块在 “(default)” 中具有唯一的GUID。 如您所见,GUID “{C0E13E61-0CC6-11d1-BBB6-0060978B2AE6}” 用于许多清理模块。这是一个通用的文件删除模块,您可以轻松地在自己的文件清理器模块中使用。只需在注册表键 HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VolumeCaches 中添加新的子键,使用上面提到的 GUID,然后配置该清理器应找到并删除的文件。您可能需要查看现有的清理器,例如 regedit.exe 中的 “Old ChkDsk Files”,以找出定义要删除文件的注册表值的名称。

PowerShell 技能连载 - 清理硬盘(第 1 部分)

cleanmgr.exe 是 Windows 自带的一个古老的工具,可以清除您的硬盘驱动器。

该工具可以删除各种垃圾数据,有时会删除许多 GB 的空间。对于 PowerShell 来说,它更有趣的地方在于支持自动化。

为了自动化清理磁盘,首先您需要启动具有管理员特权的 PowerShell(如果没有,不会提出错误,但是您的清理选择无法正确保存)。接下来,选择一个随机数,例如 5388,然后执行以下代码:

1
PS> cleanmgr.exe /sageset:5388

这将打开一个对话框窗口,您可以在其中选择下一步要执行的清理任务。检查所有适用的项目,然后单击“确定”。

现在,这些选择存储在您的自定义 ID 5388(或您选择的其他位置)下的 Windows 注册表中。 通过再次运行命令检查:对话框应再次打开并记住您的选择。如果对话框不记得您的选择,则可能没有使用管理员特权运行它。

要自动化磁盘清理,请使用参数 /sagerun 而不是 /sageset 运行命令,然后使用相同的 ID 号:

1
PS> cleanmgr /sagerun:5388

现在,清理程序无人值守地执行清理操作,因此这可能是某些情况下清理磁盘的最佳方法。

请注意,对话框显示清理进度,当前用户可以通过单击“取消”来中止清理。不过,无法隐藏此对话框。

PowerShell 技能连载 - 管理蓝牙设备(第 2 部分)

如果您想通过编程方式删除配对的蓝牙设备,则没有内置的 cmdlet。 PowerShell 仍然可以解决问题,甚至通常可以解除配对无法通过 UI 删除或不断重复出现的蓝牙设备。

您需要首先删除蓝牙设备的硬件地址。 这是有关如何列出所有蓝牙设备并返回其硬件地址的示例:

1
2
3
4
5
6
7
8
9
10
11
$Address =     @{
Name='Address'
Expression={$_.HardwareID |
ForEach-Object { [uInt64]('0x' + $_.Substring(12))}}
}

Get-PnpDevice -Class Bluetooth |
Where-Object HardwareID -match 'DEV_' |
Select-Object FriendlyName, $Address |
Where-Object Address |
Out-GridView -Title 'Select Bluetooth Device to Remove' -OutputMode Single

结果看起来类似这样,并在网格视图窗口中显示,您可以在其中选择一个蓝牙设备:

FriendlyName                               Address
------------                               -------
Bamboo Ink Plus                       480816531482
SMA001d SN: 2110109033 SN2110109033   550378395892
MX Master 3                            20919489792
MX Keys                              1089715743697
Bose QC35 II                        44056255752152

附带说明,该代码说明了一个简单的技巧,可以以编程方式将十六进制数字转换为十进制:

1
2
3
4
PS> $hex = 'A0FD'

PS> [int]"0x$hex"
41213

一旦知道要解除配对的蓝牙设备的硬件地址,接下来,您必须将其传给一个内部的 Windows API。内部方法 Bluetoothremavyevice() 将其删除。 下面的代码灵感来自 Keith A. Miller 在 Microsoft 论坛中提供的建议。

以下是一个包装内部 Windows API 签名的函数,它输入一个硬件地址,然后解绑设备:

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
function Unpair-Bluetooth
{
# take a UInt64 either directly or as part of an object with a property
# named "DeviceAddress" or "Address"
param
(
[Parameter(Mandatory,ValueFromPipeline,ValueFromPipelineByPropertyName)]
[Alias('Address')]
[UInt64]
$DeviceAddress
)

# tell PowerShell the location of the internal Windows API
# and define a static helper function named "Unpair" that takes care
# of creating the needed arguments:
begin
{
Add-Type -Namespace "Devices" -Name 'Bluetooth' -MemberDefinition '
[DllImport("BluetoothAPIs.dll", SetLastError = true, CallingConvention = CallingConvention.StdCall)]
[return: MarshalAs(UnmanagedType.U4)]
static extern UInt32 BluetoothRemoveDevice(IntPtr pAddress);
public static UInt32 Unpair(UInt64 BTAddress) {
GCHandle pinnedAddr = GCHandle.Alloc(BTAddress, GCHandleType.Pinned);
IntPtr pAddress = pinnedAddr.AddrOfPinnedObject();
UInt32 result = BluetoothRemoveDevice(pAddress);
pinnedAddr.Free();
return result;
}'
}

# do this for every object that was piped into this function:
process
{
$result = [Devices.Bluetooth]::Unpair($DeviceAddress)
[PSCustomObject]@{
Success = $result -eq 0
ReturnValue = $result
}
}
}

由于新函数 Unpair-Bluetooth 是支持管道的,因此您可以将其附加到以前的代码之后即可解除蓝牙配对:

1
2
3
4
5
6
7
8
9
10
11
12
$Address =     @{
Name='Address'
Expression={$_.HardwareID |
ForEach-Object { [uInt64]('0x' + $_.Substring(12))}}
}

Get-PnpDevice -Class Bluetooth |
Where-Object HardwareID -match 'DEV_' |
Select-Object FriendlyName, $Address |
Where-Object Address |
Out-GridView -Title 'Select Bluetooth Device to Unpair' -OutputMode Single |
Unpair-Bluetooth

运行代码时,它再次显示所有蓝牙设备。选择一个要解除配对的设备,然后点击网格视图窗口右下角的确定。请注意,如果设备不是“记住的设备”,则解除配对会失败。当解除配对成功时,返回值为 0,并将设备从蓝牙设备列表中删除。