How to capture global keystrokes with PowerShell?
Asked Answered
P

2

8

Can Powershell listen for and capture key presses?

Is it possible to write a PowerShell script that, like AutoHotkey, sits in tray and waits until you press a predefined keyboard key to start execution? And possibly not return but fire every time you press said key?

What I would like to achieve is - perform a predefined scripted action at the press of a button only AFTER starting the script, so putting it on the desktop and defining a shortcut key doesn't work.

For example:
I'd like the text "TEST" typed 3 times every time I press the "x" key but I would like this to happen only if the script that does this is running. So when the script is not running - pressing "x" would do nothing.

Basically, AutoHotkey can do exactly that but I'd like to do it in PowerShell, if possible, without writing huge amounts of C# code, because then I'd just write a tiny C# tray application for that.

Peculation answered 17/1, 2019 at 13:9 Comment(4)
This is possible in C#, thus in Powershell too. As this would require a lot of effort, I can't but wonder if there would be easier soltuions. What would you like to achieve anyway?Becki
Possible duplicate of Best way to detect key combination in windows using built in scripting languagesChiropteran
@vonPryz, updated.Peculation
Guess you're gonna write a tiny C# try application then.Chiropteran
C
8

Not in PowerShell directly maybe, but as you can run pretty much any C# code, here is a basic working example based on an excellent solution by Peter Hinchley:

Add-Type -TypeDefinition '
using System;
using System.IO;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Windows.Forms;

namespace KeyLogger {
  public static class Program {
    private const int WH_KEYBOARD_LL = 13;
    private const int WM_KEYDOWN = 0x0100;

    private static HookProc hookProc = HookCallback;
    private static IntPtr hookId = IntPtr.Zero;
    private static int keyCode = 0;

    [DllImport("user32.dll")]
    private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);

    [DllImport("user32.dll")]
    private static extern bool UnhookWindowsHookEx(IntPtr hhk);

    [DllImport("user32.dll")]
    private static extern IntPtr SetWindowsHookEx(int idHook, HookProc lpfn, IntPtr hMod, uint dwThreadId);

    [DllImport("kernel32.dll")]
    private static extern IntPtr GetModuleHandle(string lpModuleName);

    public static int WaitForKey() {
      hookId = SetHook(hookProc);
      Application.Run();
      UnhookWindowsHookEx(hookId);
      return keyCode;
    }

    private static IntPtr SetHook(HookProc hookProc) {
      IntPtr moduleHandle = GetModuleHandle(Process.GetCurrentProcess().MainModule.ModuleName);
      return SetWindowsHookEx(WH_KEYBOARD_LL, hookProc, moduleHandle, 0);
    }

    private delegate IntPtr HookProc(int nCode, IntPtr wParam, IntPtr lParam);

    private static IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam) {
      if (nCode >= 0 && wParam == (IntPtr)WM_KEYDOWN) {
        keyCode = Marshal.ReadInt32(lParam);
        Application.Exit();
      }
      return CallNextHookEx(hookId, nCode, wParam, lParam);
    }
  }
}
' -ReferencedAssemblies System.Windows.Forms

while ($true) {
    $key = [System.Windows.Forms.Keys][KeyLogger.Program]::WaitForKey()
    if ($key -eq "X") {
        Write-Host "Do something now."
    }
}

Version 2

(using a callback):

Add-Type -TypeDefinition '
  using System;
  using System.IO;
  using System.Diagnostics;
  using System.Runtime.InteropServices;
  using System.Windows.Forms;

  namespace PowerShell {
    public static class KeyLogger {
      private const int WH_KEYBOARD_LL = 13;
      private const int WM_KEYDOWN = 0x0100;

      private delegate IntPtr HookProc(int nCode, IntPtr wParam, IntPtr lParam);

      private static Action<Keys> keyCallback;
      private static IntPtr hookId = IntPtr.Zero;

      [DllImport("user32.dll")]
      private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);

      [DllImport("user32.dll")]
      private static extern bool UnhookWindowsHookEx(IntPtr hhk);

      [DllImport("user32.dll")]
      private static extern IntPtr SetWindowsHookEx(int idHook, HookProc lpfn, IntPtr hMod, uint dwThreadId);

      [DllImport("kernel32.dll")]
      private static extern IntPtr GetModuleHandle(string lpModuleName);

      public static void Run(Action<Keys> callback) {
        keyCallback = callback;
        hookId = SetHook();
        Application.Run();
        UnhookWindowsHookEx(hookId);
      }

      private static IntPtr SetHook() {
        IntPtr moduleHandle = GetModuleHandle(Process.GetCurrentProcess().MainModule.ModuleName);
        return SetWindowsHookEx(WH_KEYBOARD_LL, HookCallback, moduleHandle, 0);
      }

      private static IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam) {
        if (nCode >= 0 && wParam == (IntPtr)WM_KEYDOWN) {
          var key = (Keys)Marshal.ReadInt32(lParam);
          keyCallback(key);
        }
        return CallNextHookEx(hookId, nCode, wParam, lParam);
      }
    }
  }
' -ReferencedAssemblies System.Windows.Forms

[PowerShell.KeyLogger]::Run({
  param($key)
  if ($key -eq "X") {
    Write-Host "Do something now."
  }
})
Chiropteran answered 17/1, 2019 at 13:39 Comment(9)
Thanks, marsze. J. Doe, you could use this code in combination with a script I wrote that puts a bunch of code snippets in your system tray to have an easy way to enable or disable it.Casavant
It is actually possible to do this completely in Powershell without a single line of C#. You'll have to use the ModuleBuilder though because of the PInvoke calls (and it involves a lot of extra code compared to C#)Always
@Always Possible. Would love to see. But in the end it's all just .NET, so doesn't really matter if it's C#, VB or PS code in your script. Right?Chiropteran
@Chiropteran only for malware builders it makes a real difference : C# code embedded in Powershell is compiled (it writes a file to disk and removes it afterwards). Using the modulebuilder does everything in memory (this is how a lot of fileless malware avoids detection)Always
@J.Doe Since setting up the message pipe every time is expensive, I modified the code to use a callback instead. Look at the code now. Hope this works for you.Chiropteran
V2 seems to crash when attempting to exit, whether I put exit in the if statement or if I just send ctrl+c with my keyboard.Indocile
Is there also a way to modify this to get key combinations, e.g Windows Key + U or CTRL + O etc?Muriel
@Muriel I am pretty sure. But you would have to research that yourself. If you find anything, you can add it to the answer.Chiropteran
Version 2 is unstable and gives unpredictable results.Tiffanytiffi
L
2

You can Capture Keystrokes with Powershell with the help of a Third Part coding Script (Autohotkey)

You only need in Powershell to read this Windows registry Key.

$val = (Get-ItemProperty -path 'HKCU:\Software\GetKeypressValue').KeypressValue

And if you then run, together these Two AHk Scripts (KeypressValueToREG + ShowKeypressValue) then it is indirect Possible.

Note - the ShowKeypressValue.ahk is only to show visually, all your mouse button clicks and all your Keyboard keystrokes (it is not nessesary to use this Script)

You can run only the KeypressValueToREG.Ahk in the Background and then you are ready to go. (you can Capture all your keystrokes values into only one Variable $val)

monitor keystrokes

KeypressValueToREG.ahk

;KeypressValueToREG.ahk comes from KeypressOSD.ahk that was Created by Author RaptorX
; Open this Script in Wordpad and For Changelog look to the Bottom of the script. 
;This code works with a getkeyname from a Dllcall (See Bottom Script- by Lexikos)
;you can press the esc key to exit.

#SingleInstance force
#NoEnv
SetBatchLines, -1
ListLines, Off

; Settings
    global TransN                := 200      ; 0~255
    global ShowSingleKey         := True
    global ShowMouseButton       := True
    global ShowSingleModifierKey := True
    global ShowModifierKeyCount  := true
    global ShowStickyModKeyCount := false
    global DisplayTime           := 2000     ; In milliseconds
    global GuiPosition           := "Bottom" ; Top or Bottom
    global FontSize              := 50
    global GuiHeight             := 115

CreateGUI()
CreateHotkey()
return

OnKeyPressed:
    try {
        key := GetKeyStr()
        ShowHotkey(key)
        SetTimer, HideGUI, % -1 * DisplayTime
    }
return

OnKeyUp:
return

_OnKeyUp:
    tickcount_start := A_TickCount
return


CreateGUI() {
    global

    Gui, +AlwaysOnTop -Caption +Owner +LastFound +E0x20
    Gui, Margin, 0, 0
    Gui, Color, Black
    Gui, Font, cWhite s%FontSize% bold, Arial
    Gui, Add, Text, vHotkeyText Center y20

    WinSet, Transparent, %TransN%
}

CreateHotkey() {
    Loop, 95
    {
        k := Chr(A_Index + 31)
        k := (k = " ") ? "Space" : k

        Hotkey, % "~*" k, OnKeyPressed
        Hotkey, % "~*" k " Up", _OnKeyUp
    }

    Loop, 24 ; F1-F24
    {
        Hotkey, % "~*F" A_Index, OnKeyPressed
        Hotkey, % "~*F" A_Index " Up", _OnKeyUp
    }

    Loop, 10 ; Numpad0 - Numpad9
    {
        Hotkey, % "~*Numpad" A_Index - 1, OnKeyPressed
        Hotkey, % "~*Numpad" A_Index - 1 " Up", _OnKeyUp
    }

    Otherkeys := "WheelDown|WheelUp|WheelLeft|WheelRight|XButton1|XButton2|Browser_Forward|Browser_Back|Browser_Refresh|Browser_Stop|Browser_Search|Browser_Favorites|Browser_Home|Volume_Mute|Volume_Down|Volume_Up|Media_Next|Media_Prev|Media_Stop|Media_Play_Pause|Launch_Mail|Launch_Media|Launch_App1|Launch_App2|Help|Sleep|PrintScreen|CtrlBreak|Break|AppsKey|NumpadDot|NumpadDiv|NumpadMult|NumpadAdd|NumpadSub|NumpadEnter|Tab|Enter|Esc|BackSpace"
               . "|Del|Insert|Home|End|PgUp|PgDn|Up|Down|Left|Right|ScrollLock|CapsLock|NumLock|Pause|sc145|sc146|sc046|sc123"
    Loop, parse, Otherkeys, |
    {
        Hotkey, % "~*" A_LoopField, OnKeyPressed
        Hotkey, % "~*" A_LoopField " Up", _OnKeyUp
    }

    If ShowMouseButton {
        Loop, Parse, % "LButton|MButton|RButton", |
            Hotkey, % "~*" A_LoopField, OnKeyPressed
    }

    for i, mod in ["Ctrl", "Shift", "Alt"] {
        Hotkey, % "~*" mod, OnKeyPressed
        Hotkey, % "~*" mod " Up", OnKeyUp
    }
    for i, mod in ["LWin", "RWin"]
        Hotkey, % "~*" mod, OnKeyPressed
}

ShowHotkey(HotkeyStr) {
    WinGetPos, ActWin_X, ActWin_Y, ActWin_W, ActWin_H, A
    if !ActWin_W
        throw

    text_w := (ActWin_W > A_ScreenWidth) ? A_ScreenWidth : ActWin_W

    ;remove this gui codeline if you want only to Write the Value to Windows registry
    ;GuiControl,     , HotkeyText, %HotkeyStr%
    ;GuiControl,     , HotkeyText, %HotkeyStr%

    RegWrite, REG_SZ, HKEY_CURRENT_USER,software\GetKeypressValue,KeypressValue,%HotkeyStr%

    ;remove this gui codeline if you want only to Write the Value to Windows registry
    ;GuiControl, Move, HotkeyText, w%text_w% Center
    ;GuiControl, Move, HotkeyText, w%text_w% Center

    if (GuiPosition = "Top")
        gui_y := ActWin_Y
    else
        gui_y := (ActWin_Y+ActWin_H) - 115 - 50

    ;remove this gui codeline if you want only to Write the Value to Windows registry
    ;Gui, Show, NoActivate x%ActWin_X% y%gui_y% h%GuiHeight% w%text_w%
    ;Gui, Show, NoActivate x%ActWin_X% y%gui_y% h%GuiHeight% w%text_w%
}

GetKeyStr() {
    static modifiers := ["Ctrl", "Shift", "Alt", "LWin", "RWin"]
    static repeatCount := 1

    for i, mod in modifiers {
        if GetKeyState(mod)
            prefix .= mod " + "
    }

    if (!prefix && !ShowSingleKey)
        throw

    key := SubStr(A_ThisHotkey, 3)

    if (key ~= "i)^(Ctrl|Shift|Alt|LWin|RWin)$") {
        if !ShowSingleModifierKey {
            throw
        }
        key := ""
        prefix := RTrim(prefix, "+ ")

        if ShowModifierKeyCount {
            if !InStr(prefix, "+") && IsDoubleClickEx() {
                if (A_ThisHotKey != A_PriorHotKey) || ShowStickyModKeyCount {
                    if (++repeatCount > 1) {
                        prefix .= " ( * " repeatCount " )"
                    }
                } else {
                    repeatCount := 0
                }
            } else {
                repeatCount := 1
            }
        }
    } else {
        if ( StrLen(key) = 1 ) {
            key := GetKeyChar(key, "A")
        } else if ( SubStr(key, 1, 2) = "sc" ) {
            key := SpecialSC(key)
        } else if (key = "LButton") && IsDoubleClick() {
            key := "Double-Click"
        }
        _key := (key = "Double-Click") ? "LButton" : key

        static pre_prefix, pre_key, keyCount := 1
        global tickcount_start
        if (prefix && pre_prefix) && (A_TickCount-tickcount_start < 300) {
            if (prefix != pre_prefix) {
                result := pre_prefix pre_key ", " prefix key
            } else {
                keyCount := (key=pre_key) ? (keyCount+1) : 1
                key := (keyCount>2) ? (key " (" keyCount ")") : (pre_key ", " key)
            }
        } else {
            keyCount := 1
        }

        pre_prefix := prefix
        pre_key := _key

        repeatCount := 1
    }
    return result ? result : prefix . key
}

SpecialSC(sc) {
    static k := {sc046: "ScrollLock", sc145: "NumLock", sc146: "Pause", sc123: "Genius LuxeMate Scroll"}
    return k[sc]
}

; by Lexikos - https://autohotkey.com/board/topic/110808-getkeyname-for-other-languages/#entry682236
GetKeyChar(Key, WinTitle:=0) {
    thread := WinTitle=0 ? 0
        : DllCall("GetWindowThreadProcessId", "ptr", WinExist(WinTitle), "ptr", 0)
    hkl := DllCall("GetKeyboardLayout", "uint", thread, "ptr")
    vk := GetKeyVK(Key), sc := GetKeySC(Key)
    VarSetCapacity(state, 256, 0)
    VarSetCapacity(char, 4, 0)
    n := DllCall("ToUnicodeEx", "uint", vk, "uint", sc
        , "ptr", &state, "ptr", &char, "int", 2, "uint", 0, "ptr", hkl)
    return StrGet(&char, n, "utf-16")
}

IsDoubleClick(MSec = 300) {
    Return (A_ThisHotKey = A_PriorHotKey) && (A_TimeSincePriorHotkey < MSec)
}

IsDoubleClickEx(MSec = 300) {
    preHotkey := RegExReplace(A_PriorHotkey, "i) Up$")
    Return (A_ThisHotKey = preHotkey) && (A_TimeSincePriorHotkey < MSec)
}

HideGUI() {
    Gui, Hide
}

~esc::exitapp    
;---------------------------------------------
; ChangeLog : v2.22 (2017-02-25) - Now pressing the same combination keys continuously more than 2 times,
;                                  for example press Ctrl+V 3 times, will displayed as "Ctrl + v (3)"
;             v2.21 (2017-02-24) - Fixed LWin/RWin not poping up start menu
;             v2.20 (2017-02-24) - Added displaying continuous-pressed combination keys.
;                                  e.g.: With CTRL key held down, pressing K and U continuously will shown as "Ctrl + k, u"
;             v2.10 (2017-01-22) - Added ShowStickyModKeyCount option
;             v2.09 (2017-01-22) - Added ShowModifierKeyCount option
;             v2.08 (2017-01-19) - Fixed a bug
;             v2.07 (2017-01-19) - Added ShowSingleModifierKey option (default is True)
;             v2.06 (2016-11-23) - Added more keys. Thanks to SashaChernykh.
;             v2.05 (2016-10-01) - Fixed not detecting "Ctrl + ScrollLock/NumLock/Pause". Thanks to lexikos.
;             v2.04 (2016-10-01) - Added NumpadDot and AppsKey
;             v2.03 (2016-09-17) - Added displaying "Double-Click" of the left mouse button.
;             v2.02 (2016-09-16) - Added displaying mouse button, and 3 settings (ShowMouseButton, FontSize, GuiHeight)
;             v2.01 (2016-09-11) - Display non english keyboard layout characters when combine with modifer keys.
;             v2.00 (2016-09-01) - Removed the "Fade out" effect because of its buggy.
;                                - Added support for non english keyboard layout.
;                                - Added GuiPosition setting.
;             v1.00 (2013-10-11) - First release.
;--------------------------------------------

ShowKeypressValue.ahk

#SingleInstance force
Gui, +AlwaysOnTop -MaximizeBox ; -Caption +Resize -MinimizeBox +Disabled -SysMenu -Owner +OwnDialogs
Gui, Add, Text, center y10 h50 w300 vVar,  %KeypressValue%
Gui, Color, White
Gui, show
size=20
Gui, Font, s%size%
GuiControl, Font, var

;run KeypressValueToREG.ahk - together with ShowKeypressValue.ahk
;The Features Are:
; - It will Show On your Screen, [All your Mouse Movements] and [All Keyboard Shortcuts Movement]
; - You can Make Scripts, that can do actions with MultiClicks on All Keyboard Shortcuts Clicks, How Cool Is that. 

loop
{
RegRead, KeypressValue, HKEY_CURRENT_USER,software\GetKeypressValue,KeypressValue ; read KeypressValue
sleep 50
GuiControl,, var, %KeypressValue%



if (KeypressValue="Alt ( * 2 )") ;use this for [1x=Alt][2x=Alt ( * 2 )][3x=Alt ( * 3 )] [and many more]
{
;Here you can put any AHK CODE 
msgbox you did click Alt 2x Times
}

if (KeypressValue="Alt ( * 3 )") ;use this for [1x=Alt][2x=Alt ( * 2 )][3x=Alt ( * 3 )] [and many more]
{
;Here you can put any AHK CODE 
msgbox you did click Alt 3x Times
}


} ;End Loop

~esc::exitapp

monitor keyboard shortcuts

Lector answered 18/1, 2019 at 11:35 Comment(7)
Wasn't the original question to do it only in PowerShell, without AutoHotKey?Chiropteran
thanks, but as @Chiropteran points out, if I have to use two AHK scripts along with PowerShell, I'd just use a single AHK script to do what I want. :) The essence of the question was if PowerShell can listen and capture keystrokes on his own to make a script that triggers on keypress.Peculation
Hi @Chiropteran - you can not in PowerShell directly With a Script Capture all Keystrokes Movements, with my example you are also able to do (indirect), with only one AHK script (running in the background) - after that you need only to read one Registry Key and then put it into a one variable and then with that variable you can do anything true the PowerShell Languages.Lector
@Lector you are wrong, this is possible to do it directly in Powershell. It's even possible without using a single line of C# code in your Powershell script (by using reflection to build the Pinvoke methods)Always
@Always - i am not an expert in the Pinvoke Coding Methode, but i did give only an eligant solution that can solved to write a simple Powershell script with only to use a single line of code $val = (Get-ItemProperty -path 'HKCU:\Software\GetKeypressValue').KeypressValue put this in a loop and you can Capture all your Keyboard movements and Mouse Movements and if for example, you did pressed a count of keystrokes or a key you can then simple execute any part of PowerShell Coding.Lector
@Lector elegant and a single line of code aren't really relevant when you have to resort to 3rd party tools, especially when this is possible without using 3rd party tools (the solution provided by Marsze is what I call an elegant solution)Always
@Always - is the solution provided by Marsze, also working indirect with a 3rd party tools (c#)+(import dll's) and can you then in a single codeline (simultane) direct from out Powershell Languages capture all the keystroke movements. if it can then i am agreed with you. - ps: i do not say my solution is better but it give you also a elegant alternative to capture keystrokes from out Powershell Languages.Lector

© 2022 - 2024 — McMap. All rights reserved.