Sunday, June 15, 2008

PowerShell Low-level keyboard hook

This post provides an example PowerShell script to create a global system low-level keyboard hook, detecting and discarding the first Alt+Tab combination after the hook is installed. This was developed and tested on Windows Vista.

This is run out of a PowerShell script using runtime compiled VB.Net code, with a form-less Application.Run() call to wait for the message. You could also hook WH_MOUSE_LL using similar code.

I was originally trying to hook WH_GETMESSAGE, but it seems this is not possible without a DLL which is loaded and injected into running processes to provide the shared address space, something which negated what I was trying to achieve (a simple demo with PowerShell).

Note that the MSDN SetWindowsHookEx Function description (http://msdn.microsoft.com/en-us/library/ms644990.aspx) defines WH_KEYBOARD_LL as a global only hook, which to my understanding means it needs to be referenced in a DLL, which is most definitely not the case below as hMod is intptr.zero and it's all running out of script/dynamic code. I also tested using the hinstance of the compiled module, which didn't work, and using loadlibrary user32 which did work (I don't understand why I just copied from some other example).
 

## KeyboardHookExample.ps1 ##

#
# Description:
#  Example global WH_KEYBOARD_LL hook in PowerShell using VB.Net runtime compiled code.
#  Adds a hook in the LIFO chain for all keyboard activity to CallbackFunction(), which checks for and discards the first Alt+Tab combination
#
# Author: 
#  Wayne Martin, 15/06/2008, http://waynes-world-it.blogspot.com/
#
# References:
#  CLRInsideOut\WinSigGen.exe
#  http://msdn.microsoft.com/en-us/library/ms644959(VS.85).aspx
#  http://msdn.microsoft.com/en-us/library/ms644960(VS.85).aspx
#  http://support.microsoft.com/kb/319524
#  http://blogs.msdn.com/toub/archive/2006/05/03/589468.aspx


$provider = new-object Microsoft.VisualBasic.VBCodeProvider
$params = new-object System.CodeDom.Compiler.CompilerParameters
$params.GenerateInMemory = $True
$refs = "System.dll","Microsoft.VisualBasic.dll","System.Windows.Forms.dll"
$params.ReferencedAssemblies.AddRange($refs)

# VB.NET EXAMPLE
$txtCode = @'

Imports Microsoft.VisualBasic
Imports System
Imports System.Runtime.InteropServices
Imports System.Windows.Forms

Public Class hookExample
    Public hookHandle As Integer
    Public hWndExplorer as Integer

     Private CallbackDelegate As HookProc

      _
    Public Structure KBDLLHOOKSTRUCT
        '''DWORD->unsigned int
        Public vkCode As UInteger
        '''DWORD->unsigned int
        Public scanCode As UInteger
        '''DWORD->unsigned int
        Public flags As UInteger
        '''DWORD->unsigned int
        Public time As UInteger
        '''ULONG_PTR->unsigned int
        Public dwExtraInfo As UInteger
    End Structure

    '''Return Type: LRESULT->LONG_PTR->int
    '''code: int
    '''wParam: WPARAM->UINT_PTR->unsigned int
    '''lParam: LPARAM->LONG_PTR->int
      _
    Public Delegate Function HOOKPROC(ByVal code As Integer, ByVal wParam As System.IntPtr, ByVal lParam As System.IntPtr) As Integer

      _
    Public Structure HHOOK__
        '''int
        Public unused As Integer
    End Structure

      _
    Public Structure HINSTANCE__
        '''int
        Public unused As Integer
    End Structure

    '''Return Type: HHOOK->HHOOK__*
    '''idHook: int
    '''lpfn: HOOKPROC
    '''hmod: HINSTANCE->HINSTANCE__*
    '''dwThreadId: DWORD->unsigned int
      _
    Public Shared Function SetWindowsHookExW(ByVal idHook As Integer, ByVal lpfn As HOOKPROC,  ByVal hmod As System.IntPtr, ByVal dwThreadId As UInteger) As System.IntPtr
    End Function

    '''Return Type: LRESULT->LONG_PTR->int
    '''hhk: HHOOK->HHOOK__*
    '''nCode: int
    '''wParam: WPARAM->UINT_PTR->unsigned int
    '''lParam: LPARAM->LONG_PTR->int
      _
    Public Shared Function CallNextHookEx( ByVal hhk As System.IntPtr, ByVal nCode As Integer, ByVal wParam As System.IntPtr, ByVal lParam As System.IntPtr) As Integer
    End Function

    '''Return Type: BOOL->int
    '''hhk: HHOOK->HHOOK__*
      _
    Public Shared Function UnhookWindowsHookEx( ByVal hhk As System.IntPtr) As  Boolean
    End Function

    '''HC_ACTION -> 0
    Public Const HC_ACTION As Integer = 0

    '''LLKHF_ALTDOWN -> (KF_ALTDOWN >> 8)
    Public Const LLKHF_ALTDOWN As Integer = (KF_ALTDOWN) >> (8)
    
    '''KF_ALTDOWN -> 0x2000
    Public Const KF_ALTDOWN As Integer = 8192

    '''VK_TAB -> 0x09
    Public Const VK_TAB As Integer = 9

    '''WH_KEYBOARD_LL -> 13
    Public Const WH_KEYBOARD_LL As Integer = 13

    '''Return Type: HMODULE->HINSTANCE->HINSTANCE__*
    '''lpLibFileName: LPCWSTR->WCHAR*
      _
    Public Shared Function LoadLibraryW( ByVal lpLibFileName As String) As System.IntPtr
    End Function


    Public Sub New()
        CallbackDelegate = New HookProc(AddressOf CallbackFunction)

        'Dim hInstance As IntPtr
        ' This doesn't work:
        'hInstance = Marshal.GetHINSTANCE(Reflection.Assembly.GetExecutingAssembly().GetModules()(0))     
        'hookHandle = SetWindowsHookExW(WH_KEYBOARD_LL, CallbackDelegate, hInstance, 0)

        ' This does work, but is unnecessary and intptr.zero also works:
        'hInstance = LoadLibraryW("User32")
        'hookHandle = SetWindowsHookExW(WH_KEYBOARD_LL, CallbackDelegate, hInstance, 0)

       hookHandle = SetWindowsHookExW(WH_KEYBOARD_LL, CallbackDelegate, IntPtr.Zero, 0)
        If (hookHandle > 0) Then
            console.writeline("Keyboard hooked, press Alt+Tab when any window has focus to test")
        End If
        Application.Run()  
    End Sub


    Public Function CallbackFunction(ByVal code As Integer, ByVal wParam As IntPtr, ByVal lParam As IntPtr) As Integer
        Dim HookStruct As KBDLLHOOKSTRUCT
        If (code < 0) Then
            Return CallNextHookEx(hookHandle, code, wParam, lParam)
        End If

        hookStruct = CType(Marshal.PtrToStructure(lParam, GetType(KBDLLHOOKSTRUCT)),KBDLLHOOKSTRUCT)

        If (Hookstruct.vkCode = VK_TAB) And (Hookstruct.flags And LLKHF_ALTDOWN) Then
            console.writeline("Alt+Tab detected, discarding and removing hook")
            Call UnhookWindowsHookEx(hookHandle)
            Application.Exit()
            Return 1
        Else
            Return CallNextHookEx(hookHandle, code, wParam, lParam)
        End If
    End Function

End Class

'@

$results = $provider.CompileAssemblyFromSource($params, $txtCode)

if ($results.Errors.Count) {
    $codeLines = $txtCode.Split("`n");
    foreach ($ce in $results.Errors)
    {
        write-host "Error: $($codeLines[$($ce.Line - 1)])"
        write-host $ce
        #$ce out-default
    }
    Throw "INVALID DATA: Errors encountered while compiling code"
 }

$mAssembly = $results.CompiledAssembly
$i = $mAssembly.CreateInstance("hookExample")
#$r = $i.New()


Wayne's World of IT (WWoIT), Copyright 2008 Wayne Martin.

No comments:

Post a Comment