PowerShell 技能连载 - 控制音量和静音状态

要调节音量以及静音、取消扬声器的静音,PowerShell 可以像这样用 C# 代码来操作 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
41
42
43
44
45
46
47
48
49
50
51
52
53
Add-Type -TypeDefinition @'
using System.Runtime.InteropServices;
[Guid("5CDF2C82-841E-4546-9722-0CF74078229A"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IAudioEndpointVolume {
// f(), g(), ... are unused COM method slots. Define these if you care
int f(); int g(); int h(); int i();
int SetMasterVolumeLevelScalar(float fLevel, System.Guid pguidEventContext);
int j();
int GetMasterVolumeLevelScalar(out float pfLevel);
int k(); int l(); int m(); int n();
int SetMute([MarshalAs(UnmanagedType.Bool)] bool bMute, System.Guid pguidEventContext);
int GetMute(out bool pbMute);
}
[Guid("D666063F-1587-4E43-81F1-B948E807363F"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IMMDevice {
int Activate(ref System.Guid id, int clsCtx, int activationParams, out IAudioEndpointVolume aev);
}
[Guid("A95664D2-9614-4F35-A746-DE8DB63617E6"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IMMDeviceEnumerator {
int f(); // Unused
int GetDefaultAudioEndpoint(int dataFlow, int role, out IMMDevice endpoint);
}
[ComImport, Guid("BCDE0395-E52F-467C-8E3D-C4579291692E")] class MMDeviceEnumeratorComObject { }
public class Audio {
static IAudioEndpointVolume Vol() {
var enumerator = new MMDeviceEnumeratorComObject() as IMMDeviceEnumerator;
IMMDevice dev = null;
Marshal.ThrowExceptionForHR(enumerator.GetDefaultAudioEndpoint(/*eRender*/ 0, /*eMultimedia*/ 1, out dev));
IAudioEndpointVolume epv = null;
var epvid = typeof(IAudioEndpointVolume).GUID;
Marshal.ThrowExceptionForHR(dev.Activate(ref epvid, /*CLSCTX_ALL*/ 23, 0, out epv));
return epv;
}
public static float Volume {
get {float v = -1; Marshal.ThrowExceptionForHR(Vol().GetMasterVolumeLevelScalar(out v)); return v;}
set {Marshal.ThrowExceptionForHR(Vol().SetMasterVolumeLevelScalar(value, System.Guid.Empty));}
}
public static bool Mute {
get { bool mute; Marshal.ThrowExceptionForHR(Vol().GetMute(out mute)); return mute; }
set { Marshal.ThrowExceptionForHR(Vol().SetMute(value, System.Guid.Empty)); }
}
}
'@

# query audio volume and mute status
[Audio]::Volume
[Audio]::Mute

# unmute
[Audio]::Mute = $false

# set volume to 50% (range 0.0 - 1.0)
[Audio]::Volume = 0.5]

PowerShell 技能连载 - 检查坏(不安全的)密码(第 2 部分)

在前一个技能中我们解释了如何用 Web Service 来安全地检测密码并且查明它们是否已被泄漏。

信息安全有关的代码有时经过压缩后看起来是否“有趣”,所以在第一步分钟我们分享了优美的而且可读的代码。而以下是考虑“信息安全”的变体,它展示 PowerShell 代码可以被压缩到什么程度并且可以自动混淆。这段代码返回一个指定的密码被暴露了多少次(如果未曾发现被暴露过,返回 null)。

1
$p = 'P@ssw0rd'[Net.ServicePointManager]::SecurityProtocol = 'Tls12'$a,$b = (Get-FileHash -A 'SHA1' -I ([IO.MemoryStream]::new([Text.Encoding]::UTF8.GetBytes($p)))).Hash -split '(?<=^.{5})'(((irm "https://api.pwnedpasswords.com/range/$a" -UseB) -split '\r\n' -like "$b*") -split ':')[-1

PowerShell 技能连载 - 检查坏(不安全的)密码(第 1 部分)

复杂的密码不一定安全。例如,”P@ssw0rd” 是一个非常复杂的密码,但是非常不安全。这是为什么安全社区开始建议用更相关的测试取代复杂性标准,并防止使用以前黑客入侵中使用过的密码。这些密码——虽然它们可能很复杂——是字典攻击的一个常规部分并且非常不安全。

如何知道某个密码是否已被泄露?您可以使用类似 haveibeenpwnd.com 的网站或者它们的 API。这是它的工作原理:

  1. 您根据密码创建哈希值,这样就不会泄漏您的密码。
  2. 将哈希的头五个字节发送到 API,这样就不会泄漏哈希值。
  3. 您可以获得所有以这五个字节开头的哈希值。
  4. 检查返回的哈希中是否有您密码的哈希值。

以下是用 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
# enable all SSL protocols
[Net.ServicePointManager]::SecurityProtocol = 'Ssl3,Tls, Tls11, Tls12'

# get password hash
$stream = [IO.MemoryStream]::new([Text.Encoding]::UTF8.GetBytes($Password))
$hash = Get-FileHash -InputStream $stream -Algorithm SHA1
$stream.Close()
$stream.Dispose()

# find first five and subsequent hash characters
$prefix, $suffix = $hash.Hash -split '(?<=^.{5})'

# ask for matching passwords with the same first 5 hash digits
$url = "https://api.pwnedpasswords.com/range/$prefix"
$response = Invoke-RestMethod -Uri $url -UseBasicParsing

# find the exact match
$lines = $response -split '\r\n'
$seen = foreach ($line in $lines)
{
if ($line.StartsWith($suffix)) {
[int]($line -split ':')[-1]
break
}
}

"$Password has been seen {0:n0} times." -f $seen

试着改变 $Password 中的密码来测试这段代码。您会很惊讶地发现许多密码已经泄漏:

Sunshine has been seen 13.524 times.

PowerShell 技能连载 - 用聪明的方法指定位标志

在前一个技能中我们学习了如何在 PowerShell 中启用所有的 SSL 安全协议来连接到 Web Service 和 网站:

1
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Ssl3 -bor [Net.SecurityProtocolType]::Tls -bor [Net.SecurityProtocolType]::Tls11 -bor [Net.SecurityProtocolType]::Tls12

有趣的是,可以用一行更短的代码来代替:

1
[Net.ServicePointManager]::SecurityProtocol = 'Ssl3, Tls, Tls11, Tls12'

以下是它的原因:

由于 SecurityProtocolNet.SecurityProtocolType 类型的,所以当您传入字符串数据,它可以自动转换:

1
2
PS> [Net.ServicePointManager]::SecurityProtocol.GetType().FullName
System.Net.SecurityProtocolType

与其用 SecurityProtocolType 枚举并且用 -bor 操作符来连接,您还可以用比特标志位的名称组成的逗号分隔的字符串。两者是相同的:

1
2
3
4
5
6
7
$a = [Net.SecurityProtocolType]::Ssl3 -bor [Net.SecurityProtocolType]::Tls -bor [Net.SecurityProtocolType]::Tls11 -bor [Net.SecurityProtocolType]::Tls12
$b = [Net.SecurityProtocolType]'Ssl3, Tls, Tls11, Tls12'



PS> $a -eq $b
True

PowerShell 技能连载 - 在 PowerShell 中使用 SSL/HTTPS

根据您的 PowerShell、.NET Framework 的版本和升级,WEB 连接的缺省安全协议可能仍然是 SSL3。您可以方便地查明它:

1
[Net.ServicePointManager]::SecurityProtocol

如果返回的协议不包含 Tls12,那么您可能无法用 PowerShell 连接到安全的 Web Service 和网站。我们只需要这样操作就可以启用更多的服务:

1
2
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Ssl3 -bor [Net.SecurityProtocolType]::Tls -bor [Net.SecurityProtocolType]::Tls11 -bor [Net.SecurityProtocolType]::Tls12
[Net.ServicePointManager]::SecurityProtocol

PowerShell 技能连载 - 以固定宽度分割文本

假设您需要以固定宽度分割一段文本。例如,如果您需要一段文本的前 5 个字符,以及剩余的部分,如何实现它?

大多数 PowerShell 用户可能会用类似这样的方法:

1
$text = 'ID12:Here is the text'$prefix = $text.Substring(0,5)$suffix = $text.Substring(5)$prefix$suffix

当然,如果用分割字符,例如 “:”,可以这样操作:

1
$prefix, $suffix = 'ID12:Here is the text' -split ':'$prefix$suffix

然而,这将会吃掉分割字符,并且它会导致超过两个部分。这不是我们要的目标:用固定宽度分割一段文本。而您仍然可以使用 -split 操作符:

1
$prefix, $suffix = 'ID12:Here is the text' -split '(?<=^.{5})'$prefix$suffix

正则表达式结构 “(?<=XXX)“ 称为“向后引用”。”^“ 代表文本的开始,而 “.“ 代表任何字符。如您猜测的那样,”{5}“ 限定该占位符出现的次数,所以基本上这个正则表达式从剩下的文本中分割出前 5 个字符并且返回两部分(假设文本至少 6 个以上字符长度)。

PowerShell 技能连载 - 获取文本的哈希值

在 PowerShell 5(以及 Get-FileHash 之前),要计算字符串和文件的哈希值,您需要借助原生的 .NET 方法。以下是一段为一个字符串创建 MD5 哈希的示例代码:

1
2
3
4
5
6
7
8
9
10
11
$Text = 'this is the text that you want to convert into a hash'

$Provider = New-Object -TypeName Security.Cryptography.MD5CryptoServiceProvider
$Encodiner = New-Object -TypeName Text.UTF8Encoding

$Bytes = $Encodiner.GetBytes($Text)
$hashBytes = $Provider.ComputeHash($Bytes)
$hash = [System.BitConverter]::ToString($hashBytes)

# remove dashes if needed
$hash -replace '-'

如果您需要计算一个文件内容的哈希值,要么使用 Get-Content 来读取文件,或者使用以下代码:

1
2
3
4
5
6
7
8
9
10
11
$Path = "C:\somefile.txt"

# use your current PowerShell host as a sample
$Path = Get-Process -Id $Pid | Select-Object -ExpandProperty Path

$Provider = New-Object -TypeName Security.Cryptography.MD5CryptoServiceProvider
$FileContent = [System.IO.File]::ReadAllBytes($Path)
$hashBytes = $Provider.ComputeHash($FileContent)
$hash = [System.BitConverter]::ToString($HashBytes)

$hash -replace '-'

PowerShell 技能连载 - 从文本创建哈希

哈希是一种唯一确定一段文本而不用暴露原始文本的棒法。哈希被用来确定文本、查找重复的文件内容,以及验证密码。PowerShell 5 以及更高版本甚至提供了一个 cmdlet 来计算文件的哈希值:Get-FileHash

然而,Get-FileHash 不能计算字符串的哈希。没有必要只是为了计算哈希值而将字符串保存到文件。您可以使用所谓的内存流来代替。以下是一段从任何字符串计算哈希值的代码片段:

1
2
3
4
5
6
7
8
$Text = 'this is the text that you want to convert into a hash'

$stream = [IO.MemoryStream]::new([Text.Encoding]::UTF8.GetBytes($Text))
$hash = Get-FileHash -InputStream $stream -Algorithm SHA1
$stream.Close()
$stream.Dispose()

$hash

使用完成后别忘了关闭并释放内存流,防止内存泄漏并释放所有资源。

PowerShell 技能连载 - 美化 Out-GridView 对话框

当您用管道将对象输出到 Out-GridView,该 cmdlet 显示缺省的属性,所以当您用一个网格视图窗口当作选择框时,您可以控制用户可见的内容。以下代码将读取前 10 个 AD 用户输出到网格视图窗口,并且用户可以选择要返回的项。然而,网格视图窗口中显示的数据看起来很丑:

1
2
3
Get-ADUser -ResultSetSize 10 -Filter * |
Out-GridView -Title 'Select-User' -OutputMode Single |
Select-Object -Property *

如果您没有使用 AD 或没有安装 RSAT 工具,以下是使用进程的类似的例子:

1
2
3
4
Get-Process |
Where-Object MainWindowTitle |
Out-GridView -Title 'Which process do you want to kill?' -OutputMode Single |
Stop-Process -WhatIf

如果您使用 Select-Object 来限制显示的属性,这将改变对象的类型,所以当您继续用管道将改变过的对象传给下一级 cmdlet,它们将无法处理返回的对象。

解决方法是保持对象类型不变,而是改变缺省属性。以下是 AD 用户对象的解决方案,在选择对话框中只显示 Name 和 SID:

1
2
3
4
5
6
7
8
9
[string[]]$visible = 'Name', 'SID'
$type = 'DefaultDisplayPropertySet'
[Management.Automation.PSMemberInfo[]]$i =
New-Object System.Management.Automation.PSPropertySet($type,$visible)

Get-ADUser -LDAPFilter '(samaccountname=schul*)' |
Add-Member -MemberType MemberSet -Name PSStandardMembers -Value $i -Force -PassThru |
Out-GridView -Title 'Select-User' -OutputMode Single |
Select-Object -Property *

这是进程选择框的解决方案,显示进程的名称、公司、起始时间,和窗体标题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[string[]]$visible = 'Name', 'Company','StartTime','MainWindowTitle'
$type = 'DefaultDisplayPropertySet'
[Management.Automation.PSMemberInfo[]]$i =
New-Object System.Management.Automation.PSPropertySet($type,$visible)



Get-Process |
Where-Object MainWindowTitle |
Sort-Object -Property Name |
# important: object clone required
Select-Object -Property * |
Add-Member -MemberType MemberSet -Name PSStandardMembers -Value $i -Force -PassThru |
Out-GridView -Title 'Which process do you want to kill?' -OutputMode Single |
Stop-Process -WhatIf

结果发现,进程对象不接受新的 DefaultDisplayPropertySet,所以在这个例子中需要一个完整的克隆,这样您可以用 Select-Object -Property * 将对象输出到管道。由于这不会改变对象类型,所以所有原始属性都被保留下来,下游管道命令能继续起作用,因为管道绑定仍然有效。

PowerShell 技能连载 - 将 PowerShell 输出重定向到 GridView

当在 PowerShell 中输出数据时,它会静默地通过管道输出到 Out-Default 并且最终以文本的方式输出到控制台。如果我们覆盖 Out-Default,就可以改变它的行为,例如将所有 PowerShell 的输出改到一个网格视图窗口。实际中,您甚至可以区别对待正常的输出和错误信息,并且将两者显示在不同的窗口里。

以下是两个函数:Enable-GridOutputDisable-GridOutput。当您运行 Enable-GridOutput 时,它会覆盖 Out-Default 并将常规的输出显示在 “Output” 网格视图窗口,并且将错误信息转换为有用的文本,并将它输出到一个独立的 “Error” 网格视图窗口。

当运行 Disable-GridOutput 后,会去掉覆盖的效果,并且回到缺省的行为:

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
55
56
57
58
function Enable-GridOutput
{
function global:Out-Default
{
param
(
[Parameter(ValueFromPipeline=$true)][Object]
$InputObject
)

begin
{
$cmd = $ExecutionContext.InvokeCommand.
GetCommand('Microsoft.PowerShell.Utility\Out-GridView',
[Management.Automation.CommandTypes]::Cmdlet)

$p1 = {& $cmd -Title 'Output' }.
GetSteppablePipeline($myInvocation.CommandOrigin)
$p2 = {& $cmd -Title 'Error' }.
GetSteppablePipeline($myInvocation.CommandOrigin)

$p1.Begin($PSCmdlet)
$p2.Begin($PSCmdlet)
}

process
{
if ($_ -is [Management.Automation.ErrorRecord])
{
$info = $_ | ForEach-Object { [PSCustomObject]@{
Exception = $_.Exception.Message
Reason = $_.CategoryInfo.Reason
Target = $_.CategoryInfo.TargetName
Script = $_.InvocationInfo.ScriptName
Line = $_.InvocationInfo.ScriptLineNumber
Column = $_.InvocationInfo.OffsetInLine
}
}
$p2.Process($info)
}
else
{
$p1.Process($_)
}
}

end
{
$p1.End()
$p2.End()
}
}
}

function Disable-GridOutput
{
Remove-Item -Path function:Out-Default -ErrorAction SilentlyContinue
}