1

I have a PowerShell script that listens for a key to be pressed before exiting. I accomplish this using a custom TypeDefinition; here's the code:

param(
  [string]$quitKey = "F16"
)

$keyLogger = @"
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);
    }
  }
}
"@

Add-Type -TypeDefinition $keyLogger -ReferencedAssemblies system.Windows.Forms

while ($keyPress -ne $quitKey) {
  $keyPress = [System.Windows.Forms.Keys][KeyLogger.Program]::WaitForKey()
}

Write-Host "`nCaught $quitKey, ending recording..." -ForegroundColor Green

exit

The above executes flawlessly in PowerShell 5.1, the version pre-installed on Windows 11. However, I'm unable to get this script to run in PowerShell 7 (7.3.4 to be specific); I get this error:

InvalidOperation: 
Line |
  62 |    $keyPress = [System.Windows.Forms.Keys][KeyLogger.Program]::WaitFor …
     |                                           ~~~~~~~~~~~~~~~~~~~
     | Unable to find type [KeyLogger.Program].

Searched everywhere online for a similar issue and couldn't find anything. Add-Type seems to work in my other scripts, even in PowerShell 7. But for some reason this specific custom TypeDefiniton fails.

ninbura
  • 450
  • 6
  • 14

1 Answers1

1

There is a long standing bug in newer versions of PowerShell where when -ReferenceAssemblies is used then the standard assemblies are not included by default, see GitHub issue #9599.

You can use the same workaround I used in this answer, for your case, the code would look like this to make it compatible with both versions:

# Top of your code goes this:
Add-Type -AssemblyName System.Windows.Forms

$refAssemblies = @(
    [System.Windows.Forms.Form].Assembly.Location

    if ($IsCoreCLR) {
        $pwshLocation = Split-Path -Path ([psobject].Assembly.Location) -Parent
        $pwshRefAssemblyPattern = [IO.Path]::Combine($pwshLocation, 'ref', '*.dll')
        (Get-Item -Path $pwshRefAssemblyPattern).FullName
    }
)

# Definition stays the same:
$keyLogger = @'
...
...
'@ 

# Here you include the assembly you want plus the default ones:
Add-Type -TypeDefinition $keyLogger -ReferencedAssemblies $refAssemblies
Santiago Squarzon
  • 41,465
  • 5
  • 14
  • 37