pstools.psm1

using namespace System.IO
using namespace System.Linq
using namespace System.Reflection
using namespace System.ComponentModel
using namespace System.Linq.Expressions
using namespace System.Management.Automation
using namespace System.Runtime.InteropServices
using namespace System.Collections.ObjectModel

$keys, $types = ($x = [PSObject].Assembly.GetType(
  'System.Management.Automation.TypeAccelerators'
))::Get.Keys, @{
  buf = [Byte[]]
  ptr = [IntPtr]
  sfh = [Microsoft.Win32.SafeHandles.SafeFileHandle]
}
$types.Keys.ForEach{ if ($_ -notin $keys) { $x::Add($_, $types.$_) } }

Add-Member -InputObject ([buf]) -Name Uni -MemberType ScriptMethod -Value {
  param([String]$str) [Text.Encoding]::Unicode.GetBytes($str)
} -Force

Add-Member -InputObject ([enum]) -Name All -MemberType ScriptMethod -Value {
  param([Hashtable]$flags, [Int32]$val) ($flags.Keys.Where{
    ($flags[$_] -band $val) -eq $flags[$_]
  }, 'n/a')[!$val] -join ' + '
} -Force

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

    [Parameter(Mandatory, Position=1)]
    [ValidateScript({![String]::IsNullOrEmpty($_)})]
    [ScriptBlock]$Signature
  )

  begin {
    $kernel32 = @{}
    [Array]::Find((
      Add-Type -AssemblyName Microsoft.Win32.SystemEvents -PassThru
    ), [Predicate[Type]]{$args[0].Name -eq 'kernel32'}
    ).GetMethods([BindingFlags]'NonPublic, Static, Public').Where{
      $_.Name -cmatch '\AGet(P|M)'
    }.ForEach{ $kernel32[$_.Name] = $_ }

    if ((
      $mod = $kernel32.GetModuleHandle.Invoke($null, @($Module))
    ) -eq [IntPtr]::Zero) {
      throw [DllNotFoundException]::new("Can not find $Module library.")
    }
  }
  process {}
  end {
    $funcs = @{}
    for ($i, $m, $fn, $p = 0, ([Expression].Assembly.GetType(
        'System.Linq.Expressions.Compiler.DelegateHelpers'
      ).GetMethod('MakeNewCustomDelegate', [BindingFlags]'NonPublic, Static')
      ), [Marshal].GetMethod('GetDelegateForFunctionPointer', ([IntPtr])),
      $Signature.Ast.FindAll({$args[0].CommandElements}, $true).ToArray();
      $i -lt $p.Length; $i++
    ) {
      $fnret, $fname = ($def = $p[$i].CommandElements).Value

      if ((
        $fnsig = $kernel32.GetProcAddress.Invoke($null, @($mod, $fname))
      ) -eq [IntPtr]::Zero) {
        throw [InvalidOperationException]::new("Can not find $fname signature.")
      }

      $fnargs = $def.Pipeline.Extent.Text
      [Object[]]$fnargs = ((
        ($fnargs -replace '\[|\]' -split ',\s+?') + $fnret
      ), $fnret)[[String]::IsNullOrEmpty($fnargs)]

      $funcs[$fname] = $fn.MakeGenericMethod(
        [Delegate]::CreateDelegate([Func[[Type[]], Type]], $m).Invoke($fnargs)
      ).Invoke([Marshal], $fnsig)
    }
    Set-Variable $Module -Value $funcs -Scope Script -Force
  }
}

function Read-SourceData {
  [CmdletBinding(DefaultParameterSetName='Buffer')]
  param(
    [Parameter(Mandatory, ParameterSetName='Buffer', Position=0)]
    [ValidateNotNull()]
    [Byte[]]$Buffer,

    [Parameter(Mandatory, ParameterSetName='Handle', Position=0)]
    [ValidateScript({$_ -ne [IntPtr]::Zero})]
    [IntPtr]$Handle,

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

  process {
    $pos, $set = 0, @{b='Byte';s='Int16';i='Int32';l='Int64';p='IntPtr'}

    try {
      $ptr = ($Handle, ($gch = [GCHandle]::Alloc(
        $Buffer, [GCHandleType]::Pinned
      )).AddrOfPinnedObject())[$PSCmdlet.ParameterSetName -eq 'Buffer']

      ($Map -split '(\S(?:\d+)?)').Where{!!$_}.ForEach{
        if ($_ -notmatch '([bsilp])(\d+)?') {
          throw [InvalidOperationException]::new('Invalid map value.')
        }

        switch ($matches.Count) {
          2 {$matches[0]}
          3 {[Char[]]($matches[1] * $matches[2])}
        } # switch
      }.ForEach{
        $tmp = [Marshal]::"Read$($set["$_"])"($ptr, $pos)
        if ([Char]::IsUpper([Char]$_) -and $_ -ne 'p') {
          [BitConverter]::"To$('SU'[$_ -ne 'B'])$($set["$_"])"(
            [BitConverter]::GetBytes($tmp), 0
          )
        }
        else {$tmp}
        Write-Verbose "+0x$($pos.ToString('X3')) $($set["$_"])"
        $pos += [Marshal]::SizeOf(0 -as ($set["$_"] -as [Type]))
      }
    }
    catch { Write-Verbose $_ }
    finally {
      if ($gch) { $gch.Free() }
    }
  }
}

function New-PsProxy {
  [CmdletBinding(DefaultParameterSetName='Name')]
  param(
    [Parameter(Mandatory, ParameterSetName='Name', Position=0)]
    [ValidateScript({!!($script:ps = Get-Process $_ -ErrorAction 0)})]
    [ValidateNotNullOrEmpty()]
    [String]$Name,

    [Parameter(Mandatory, ParameterSetName='Id', Position=0)]
    [ValidateScript({!!($script:ps = Get-Process -Id $_ -ErrorAction 0)})]
    [Int32]$Id,

    [Parameter(Mandatory, Position=1)]
    [ValidateScript({![String]::IsNullOrEmpty($_)})]
    [ScriptBlock]$Callback
  )

  process {
    $ps.ForEach{
      .$Callback $_
      $_.Dispose()
    }
  }
}

Set-Alias -Name pswsc -Value Clear-PsWorkingSet
function Clear-PsWorkingSet {
  [CmdletBinding()]param($PSBoundParameters)

  process {
    New-Delegate kernel32 {
      bool SetProcessWorkingSetSize([ptr, int, int])
    }

    New-PsProxy $PSBoundParameters -Callback {
      .({'PID {0}: {1}' -f $_.Id, $kernel32.SetProcessWorkingSetSize.Invoke(
        $_.Handle, -1, -1
      )},{
        Write-Verbose "PID $($_.Id): could not clear working set."
      })[!$_.Handle]
    }
  }
}

Set-Alias -Name psdump -Value Get-PsDump
function Get-PsDump {
  [CmdletBinding()]param($PSBoundParameters)
  DynamicParam {
    $dict = [RuntimeDefinedParameterDictionary]::new()

    $attr = [Collection[Attribute]]::new()
    $attr.Add((New-Object ParameterAttribute -Property @{
      ParameterSetName = '__AllParameterSets'
    }))
    $attr.Add([ValidateNotNullOrEmptyAttribute]::new())
    $attr.Add((New-Object ValidateSetAttribute(@('MiniDump', 'FullDump'))))
    $paramDumpType = [RuntimeDefinedParameter]::new('DumpType', [String], $attr)
    $paramDumpType.Value = 'MiniDump'
    $dict.Add('DumpType', $paramDumpType)

    $attr = [Collection[Attribute]]::new()
    $attr.Add((New-Object ParameterAttribute -Property @{
      ParameterSetName = '__AllParameterSets'
    }))
    $attr.Add([ValidateNotNullOrEmptyAttribute]::new())
    $attr.Add((New-Object ValidateScriptAttribute({Test-Path $_})))
    $paramSavePath = [RuntimeDefinedParameter]::new('SavePath', [String], $attr)
    $paramSavePath.Value = $pwd.Path
    $dict.Add('SavePath', $paramSavePath)

    return $dict
  }

  process {
    New-Delegate kernel32 {
      ptr LoadLibraryW([buf])
      bool FreeLibrary([ptr])
    }

    if (($dll = $kernel32.LoadLibraryW.Invoke(
      [buf].Uni('dbghelp.dll')
    )) -eq [IntPtr]::Zero) {
      Write-Verbose 'can not load dbghelp.dll library.'
      return
    }

    New-Delegate dbghelp {
      bool MiniDumpWriteDump([ptr, uint, sfh, uint, ptr, ptr, ptr])
    }

    $numeric = (6, 261)[$paramDumpType.Value -eq 'MiniDump']
    New-PsProxy $PSBoundParameters -Callback {
      $dmp = "$(Resolve-Path $paramSavePath.Value)\$($_.Name)_$($_.Id).dmp"
      try {
        $fs = [File]::Create($dmp)
        if (!$dbghelp.MiniDumpWriteDump.Invoke(
          $_.Handle, $_.Id, $fs.SafeFileHandle, $numeric,
          [IntPtr]::Zero, [IntPtr]::Zero, [IntPtr]::Zero
        )) {
          $err = $true
          throw [InvalidOperationException]::new("Dumping failure PID: $($_.Id)")
        }
      }
      catch { Write-Verbose $_ }
      finally {
        if ($fs) { $fs.Dispose() }
        if ($err) { Remove-Item $dmp -Force }
      }
    }

    if (!$kernel32.FreeLibrary.Invoke($dll)) {
      Write-Verbose 'can not release dbghelp.dll library.'
    }
  }
}

Set-Alias -Name psvprot -Value Get-PsVMInfo
function Get-PsVMInfo {
  [CmdletBinding()]param($PSBoundParameters)
  DynamicParam {
    $dict = [RuntimeDefinedParameterDictionary]::new()

    $attr = [Collection[Attribute]]::new()
    $attr.Add((New-Object ParameterAttribute -Property @{
      ParameterSetName = '__AllParameterSets'
    }))
    $paramAddress = [RuntimeDefinedParameter]::new('Address', [IntPtr], $attr)
    $paramAddress.Value = [IntPtr]::Zero
    $dict.Add('Address', $paramAddress)

    return $dict
  }

  process {
    New-Delegate kernel32 {
      int VirtualQueryEx([ptr, ptr, ptr, uint])
    }

    $MEM_PROTECT, $MEM_STATE, $MEM_TYPE, $sz = @{
      PAGE_NOACCESS          = 0x00000001
      PAGE_READONLY          = 0x00000002
      PAGE_READWRITE         = 0x00000004
      PAGE_WRITECOPY         = 0x00000008
      PAGE_EXECUTE           = 0x00000010
      PAGE_EXECUTE_READ      = 0x00000020
      PAGE_EXECUTE_READWRITE = 0x00000040
      PAGE_EXECUTE_WRITECOPY = 0x00000080
      PAGE_GUARD             = 0x00000100
      PAGE_NOCACHE           = 0x00000200
      PAGE_WRITECOMBINE      = 0x00000400
    }, @{
      MEM_COMMIT  = 0x00001000
      MEM_RESERVE = 0x00002000
      MEM_FREE    = 0x00010000
    }, @{
      MEM_PRIVATE = 0x00020000
      MEM_MAPPED  = 0x00040000
      MEM_IMAGE   = 0x00100000
    }, [IntPtr]::Size

    $fmt, $to_i = "{0:x$($sz * 2)}", "ToInt$($sz * 8)"

    New-PsProxy $PSBoundParameters -Callback {
      try {
        $ptr = [Marshal]::AllocHGlobal((0x1C, 0x30)[$sz / 4 - 1]) # sizeof(MEMORY_BASIC_INFORMATION)
        if ($kernel32.VirtualQueryEx.Invoke($_.Handle, $paramAddress.Value, $ptr, 0x1000) -ne 0) {
          $mbi = Read-SourceData -Handle $ptr -Map p4I3
          [PSCustomObject]@{
            BaseAddress       = $fmt -f $mbi[0].$to_i()
            AllocationBase    = $fmt -f $mbi[1].$to_i()
            AllocationProtect = ($x = [BitConverter]::ToUInt32(
              [BitConverter]::GetBytes($mbi[2].$to_i()), 0
            )).ToString('x8'), [Enum].All($MEM_PROTECT, $x)
            RegionSize        = $fmt -f $mbi[3].$to_i()
            State             = $mbi[4].ToString('x8'), [Enum].All($MEM_STATE, $mbi[4])
            Protect           = $mbi[5].ToString('x8'), [Enum].All($MEM_PROTECT, $mbi[5])
            Type              = $mbi[6].ToString('x8'), [Enum].All($MEM_TYPE, $mbi[6])
          }
        }
        else { Write-Verbose 'Invalid address has been specified.' }
      }
      catch { Write-Verbose $_ }
      finally { if ($ptr) { [Marshal]::FreeHGlobal($ptr) } }
    }
  }
}

Set-Alias -Name psresume -Value Resume-PsProcess
function Resume-PsProcess {
  [CmdletBinding()]param($PSBoundParameters)

  process {
    New-Delegate ntdll {
      int NtResumeProcess([ptr])
      int RtlNtStatusToDosError([int])
    }

    New-PsProxy $PSBoundParameters -Callback {
      if ([Enumerable]::Sum([Int32[]](
        Select-Object -InputObject $_.Threads[0] -Property ThreadState, WaitReason
      ).PSObject.Properties.Value.ForEach{$_ -eq 5}) -eq 2) {
        if (($nts = $ntdll.NtResumeProcess.Invoke($_.Handle)) -ne 0) {
          Write-Verbose "$([Win32Exception]::new(
            $ntdll.RtlNtStatusToDosError.Invoke($nts)
          ).Message)"

        }
        else { Write-Verbose "Process $($_.Id) is resumed." }
      }
      else { Write-Verbose "Process $($_.Id) is already active." }
    }
  }
}

Set-Alias -Name psuspend -Value Suspend-PsProcess
function Suspend-PsProcess {
  [CmdletBinding()]param($PSBoundParameters)

  process {
    New-Delegate ntdll {
      int NtSuspendProcess([ptr])
      int RtlNtStatusToDosError([int])
    }

    New-PsProxy $PSBoundParameters -Callback {
      if ([Enumerable]::Sum([Int32[]](
        Select-Object -InputObject $_.Threads[0] -Property ThreadState, WaitReason
      ).PSObject.Properties.Value.ForEach{$_ -eq 5}) -ne 2) {
        if (($nts = $ntdll.NtSuspendProcess.Invoke($_.Handle)) -ne 0) {
          Write-Verbose "$([Win32Exception]::new(
            $ntdll.RtlNtStatusToDosError.Invoke($nts)
          ).Message)"

        }
        else { Write-Verbose "Process $($_.Id) is suspended." }
      }
      else { Write-Verbose "Process $($_.Id) is already suspended." }
    }
  }
}

Export-ModuleMember -Alias * -Function (
  'Clear-PsWorkingSet',
  'Get-PsDump',
  'Get-PsVMInfo',
  'Resume-PsProcess',
  'Suspend-PsProcess'
)