handle.psm1

#requires -version 6.1
using namespace System.Reflection
using namespace System.Reflection.Emit
using namespace System.Linq.Expressions
using namespace System.Runtime.InteropServices

function Get-ProcAddress {
  [OutputType([Hashtable])]
  [CmdletBinding()]
  param(
    [Parameter(Mandatory, Position=0)]
    [ValidateNotNullOrEmpty()]
    [String]$Module,

    [Parameter(Mandatory, Position=1)]
    [ValidateNotNullOrEmpty()]
    [String[]]$Function
  )

  process {
    $kernel32 = @{}

    [Assembly]::LoadFile("$(
      [RuntimeEnvironment]::GetRuntimeDirectory()
    )Microsoft.Win32.SystemEvents.dll"

    ).GetType('Interop+Kernel32').GetMethods(
      [BindingFlags]'NonPublic, Static, Public'
    ).Where{$_.Name -cmatch '\AGet(Proc|Mod)'}.ForEach{
      $kernel32[$_.Name] = $_
    }

    if ((
      $mod = $kernel32.GetModuleHandle.Invoke($null, @($Module))
    ) -eq [IntPtr]::Zero) {
      throw [DllNotFoundException]::new()
    }

    $funcs = @{}
    $Function.ForEach{
      if ((
        $$ = $kernel32.GetProcAddress.Invoke($null, @($mod, $_))
      ) -ne [IntPtr]::Zero) { $funcs.$_ = $$ }
    }
    $funcs
  }
}

function Set-Delegate {
  [OutputType([Type])]
  [CmdletBinding(DefaultParameterSetName='Prototype')]
  param(
    [Parameter(Mandatory, ParameterSetName='Prototype', Position=0)]
    [ValidateNotNull()]
    [Alias('p')]
    [Type]$Prototype,

    [Parameter(Mandatory, ParameterSetName='PrototypeAsTypesArray', Position=0)]
    [ValidateNotNullOrEmpty()]
    [Alias('pa')]
    [Type[]]$PrototypeAsTypesArray,

    [Parameter(Mandatory, Position=1)]
    [ValidateScript({$_ -ne [IntPtr]::Zero})]
    [IntPtr]$ProcAddress,

    [Parameter(Position=2)]
    [ValidateNotNullOrEmpty()]
    [CallingConvention]$CallingConvention = 'StdCall'
  )

  process {
    switch ($PSCmdlet.ParameterSetName) {
      'Prototype' {
        $method = $Prototype.GetMethod('Invoke')
        $returntype, $paramtypes = $method.ReturnType, $method.GetParameters().ParameterType
        $paramtypes = ($paramtypes, $null)[!$paramtypes]
        $il, $sz = ($holder = [DynamicMethod]::new(
          'Invoke', $returntype, $paramtypes, $Prototype
        )).GetILGenerator(), [IntPtr]::Size

        if ($paramtypes) {
          (0..($paramtypes.Length - 1)).ForEach{$il.Emit([OpCodes]::ldarg, $_)}
        }

        $il.Emit([OpCodes]::"ldc_i$sz", $ProcAddress."ToInt$((32, 64)[$sz / 4 - 1])"())
        $il.EmitCalli([OpCodes]::calli, $CallingConvention, $returntype, $paramtypes)
        $il.Emit([OpCodes]::ret)

        $holder.CreateDelegate($Prototype)
      }
      'PrototypeAsTypesArray' {
        [Marshal]::GetDelegateForFunctionPointer(
          $ProcAddress, [Delegate]::CreateDelegate(
            [Func[[Type[]], Type]],
            [Expression].Assembly.GetType(
              'System.Linq.Expressions.Compiler.DelegateHelpers'
            ).GetMethod(
              'MakeNewCustomDelegate', [BindingFlags]'NonPublic, Static'
            )
          ).Invoke($PrototypeAsTypesArray)
        )
      }
    } # switch
  }
}

function New-Delegate {
  [OutputType([Hashtable])]
  [CmdletBinding()]
  param(
    [Parameter(Mandatory, Position=0)]
    [ValidateNotNullOrEmpty()]
    [String]$Module,

    [Parameter(Mandatory, Position=1)]
    [ValidateNotNull()]
    [Hashtable]$Signature
  )

  process {
    $funcs, $addr = @{}, (Get-ProcAddress -Module $Module -Function $Signature.Keys)
    $addr.Keys.ForEach{
      $ptr, $sig = $addr.$_, $Signature.$_
      if (!$sig) { throw [InvalidOperationException]::new() }
      $funcs.$_ = switch -Regex ($sig.Name) {
        '\A(Action|Func)' { Set-Delegate -ProcAddress $ptr -Prototype $sig }
        default { Set-Delegate -ProcAddress $ptr -PrototypeAsTypesArray $sig }
      }
    }
    $funcs
  }
}

Set-Alias -Name handle -Value Get-PsHandles
function Get-PsHandles {
  [CmdletBinding()]
  param(
    [Parameter(Mandatory, Position=0)]
    [ValidateScript({!!($script:ps = Get-Process -Id $_ -ErrorAction 0) -and $_ -ne $PID})]
    [Int32]$Id,

    [Parameter()][Switch]$ShowEmpty
  )

  begin {
    $kernel32, $ntdll, $page, $to_i = (New-Delegate kernel32 -Signature @{
      CloseHandle = [Func[IntPtr, Boolean]]
      OpenProcess = [Func[UInt32, Boolean, Int32, IntPtr]]
    }), (New-Delegate ntdll -Signature @{
      NtDuplicateObject = (
        [IntPtr], [IntPtr], [IntPtr], [IntPtr].MakeByRefType(),
        [UInt32], [UInt32], [UInt32], [Int32]
      )
      NtQueryInformationProcess = [Func[IntPtr, Int32, IntPtr, Int32, [Byte[]], Int32]]
      NtQueryObject = [Func[IntPtr, UInt32, [Byte[]], UInt32, [Byte[]], Int32]]
    }), [Byte[]]::new(0x1000), "ToInt$((32, 64)[($x = ($sz = [IntPtr]::Size) / 4 - 1)])"

    function Expand-UnicodeString([IntPtr]$h, [UInt32]$o) {
      if (!$ntdll.NtQueryObject.Invoke($h, $o, $page, $page.Length, $null)) {
        try {
          $uni = [GCHandle]::Alloc($page, [GCHandleType]::Pinned)
          $str = $uni.AddrOfPinnedObject()
          [Marshal]::PtrToStringUni([Marshal]::ReadIntPtr([IntPtr]($str.$to_i() + $sz)))
        }
        catch { Write-Verbose $_ }
        finally {
          if ($uni) { $uni.Free() }
        }
      }
    }
  }
  process {
    try {
      if (($hndl = $kernel32.OpenProcess.Invoke(0x440, $false, $Id)) -eq [IntPtr]::Zero) {
        throw [InvalidOperationException]::new()
      }

      $ptr = [Marshal]::AllocHGlobal(($bufsz = 0x1000))
      while ($ntdll.NtQueryInformationProcess.Invoke($hndl, 51, $ptr, $bufsz, $null)) {
        $ptr = [Marshal]::ReAllocHGlobal($ptr, [IntPtr]($bufsz *= 2))
      }

      $tmp = $ptr
      $NumberOfHandles = [Marshal]::ReadIntPtr($tmp).$to_i()
      $handles = (0..($NumberOfHandles - 1)).ForEach{
        $HandleValue = [Marshal]::ReadIntPtr([IntPtr]($tmp.$to_i() + (0x08, 0x10)[$x]))
        [IntPtr]$duple = [IntPtr]::Zero
        if (!$ntdll.NtDuplicateObject.Invoke(
          $hndl, $HandleValue, [IntPtr]-1, [ref]$duple, 0, 0, 0x02
        )) {
          $tmp = [IntPtr]($tmp.$to_i() + (0x1C, 0x28)[$x])
          continue
        }
        $page.Clear()
        $type = Expand-UnicodeString $duple 2
        $page.Clear()
        $name = Expand-UnicodeString $duple 1

        if ($duple -ne [IntPtr]::Zero) {
          if (!$kernel32.CloseHandle.Invoke($duple)) {
            Write-Verbose "Could not close duple $($HandleValue.$to_i()) handle."
          }
        }

        [PSCustomObject]@{
          Value = '0x{0:X}' -f $HandleValue.$to_i()
          Type  = $type
          Name  = $name
        }

        $tmp = [IntPtr]($tmp.$to_i() + (0x1C, 0x28)[$x])
      }
    }
    catch { Write-Verbose $_ }
    finally {
      if ($ptr) {[Marshal]::FreeHGlobal($ptr)}
      if ($hndl -and $hndl -ne [IntPtr]::Zero) {
        if (!$kernel32.CloseHandle.Invoke($hndl)) {
          Write-Verbose "Could not close process handle."
        }
      }
    }
  }
  end {
    $ps.Dispose()
    if ($handles) {($handles, $handles.Where{$_.Name})[!$ShowEmpty]}
    [GC]::Collect()
  }
}

Export-ModuleMember -Alias handle -Function Get-PsHandles