Дерево процессов

в 19:40, , рубрики: powershell

Вывод комадлета Get-Process для восприятия не слишком удобен — просто список, в котором данные ко всему прочему отражены в стиле стран ближнего востока, то бишь справа налево, подается Microsoft как идеолгоически верный путь к пониманию сути того, что творится в процессах. Речь не о том, как с помощью PowerShell палить вирусы, а об отсутствии визуального представления какой процесс какого Ерофеича родил, чай, ведь пятая версия PowerShell'а, а древовидного представления по-прежнему нема, да и в ближайшей перспективе, должно быть, не предвидется.

И здесь из зрительного зала раздается: «А нафига нам древовидное представление процессов в PowerShell, когда есть ProcessExplorer, на худой конец — pslist?» Во-первых, GUI для консольщика как серпом по яйцам, во-вторых, какой резон разводить зоопарк из набора сторонних утилит, когда наличие PowerShell по сути является синонимом «уже все есть»? — остается лишь творить под цвет своих фломастеров. Преамбула приобретает некий сюрреалистический оттенок, да и рискует затянуться, если продолжать в том же роде, так что готовим фломастеры…

Сюрприз!

Если кто-то из читающих раскатал губу на WMI, то может смело закатывать ее фломастером обратно, ибо речь опять-таки пойдет о рефлексии, точнее не столько о ней самой, сколько о достижении цели через нее. Кудряво сказано, но да ладно. Как подсказывает Кэп, для построения дерева процессов нужно знать такие параметры, как имя процесса, его PID, а также PPID. Последний служит отправной точной при определении отпрыска от родителя, причем получить оный в Windows можно как миниму тремя приемами дзюдо: счетчики производительности, WMI и NtQuerySystemInformation. Первый (ровно как и второй) идет лесом, так как в грубом приближении является оберткой над NtQuerySystemInformation, токмо с тормозами в придачу — показатель варьируется от начинки ПК, но это тема отдельного разговора.
Открываем Vim (или что у кого там любимое) и пишем:

Set-Variable ($$ = [Regex].Assembly.GetType(
  'Microsoft.Win32.NativeMethods'
).GetMethod('NtQuerySystemInformation')).Name $$

Итак, мы определили переменую $NtQuerySystemInformation. Теперь нужно получить указатель на структуру SYSTEM_PROCESS_INFORMATION, на Win7 x86 выглядящую так:

   +0x000 NextEntryOffset  : Uint4B
   +0x004 NumberOfThreads  : Uint4B
   +0x008 WorkingSetPrivateSize : _LARGE_INTEGER
   +0x010 HardFaultCount   : Uint4B
   +0x014 NumberOfThreadsHighWatermark : Uint4B
   +0x018 CycleTime        : Uint8B
   +0x020 CreateTime       : _LARGE_INTEGER
   +0x028 UserTime         : _LARGE_INTEGER
   +0x030 KernelTime       : _LARGE_INTEGER
   +0x038 ImageName        : _UNICODE_STRING
   +0x040 BasePriority     : Int4B
   +0x044 UniqueProcessId  : Ptr32 Void
   +0x048 InheritedFromUniqueProcessId : Ptr32 Void
   +0x04c HandleCount      : Uint4B
   +0x050 SessionId        : Uint4B
   +0x054 UniqueProcessKey : Uint4B
   +0x058 PeakVirtualSize  : Uint4B
   +0x05c VirtualSize      : Uint4B
   +0x060 PageFaultCount   : Uint4B
   +0x064 PeakWorkingSetSize : Uint4B
   +0x068 WorkingSetSize   : Uint4B
   +0x06c QuotaPeakPagedPoolUsage : Uint4B
   +0x070 QuotaPagedPoolUsage : Uint4B
   +0x074 QuotaPeakNonPagedPoolUsage : Uint4B
   +0x078 QuotaNonPagedPoolUsage : Uint4B
   +0x07c PagefileUsage    : Uint4B
   +0x080 PeakPagefileUsage : Uint4B
   +0x084 PrivatePageCount : Uint4B
   +0x088 ReadOperationCount : _LARGE_INTEGER
   +0x090 WriteOperationCount : _LARGE_INTEGER
   +0x098 OtherOperationCount : _LARGE_INTEGER
   +0x0a0 ReadTransferCount : _LARGE_INTEGER
   +0x0a8 WriteTransferCount : _LARGE_INTEGER
   +0x0b0 OtherTransferCount : _LARGE_INTEGER


Причем из всей структуры нас интересуют такие поля как NextEntryOffset, ImageName, UniqueProcessId и InheritedFromUniqueProcessId, так что для получения только этих четырех полей определять структуру в домене приложений слишком жирно — воспользуемся методами типа Marshal.
Получаем указатель:

if (($ta = [PSObject].Assembly.GetType(
  'System.Management.Automation.TypeAccelerators'
))::Get.Keys -notcontains 'Marshal') {
  $ta::Add('Marshal', [Runtime.InteropServices.Marshal])
}

$ret = 0
try {
  #задаем размер буфера минимальным значением
  $ptr = [Marshal]::AllocHGlobal(1024)
  if ($NtQuerySystemInformation.Invoke($null, (
    $par = [Object[]]@(5, $ptr, 1024, $ret)
  )) -eq 0xC0000004) { #STATUS_INFO_LENGTH_MISMATCH
    $ptr = [Marshal]::ReAllocHGlobal($ptr, [IntPtr]$par[3])
    if ($NtQuerySystemInformation.Invoke($null, (
      $par = [Object[]]@(5, $ptr, $par[3], 0)
    )) -ne 0) {
      throw New-Object InvalidOperationException('Что-то пошло не так...')
    }
  }
}
catch { $_.Exception }
finally {
  if ($ptr -ne $null) {
    [Marshal]::FreeHGlobal($ptr)
  }
}

Указатель получили, читаем данные. Стоп! А ведь ImageName — это структура UNICODE_STRING, как быть? Делаем ход конем:

$UNICODE_STRING = [Activator]::CreateInstance(
  [Object].Assembly.GetType(
    'Microsoft.Win32.Win32Native+UNICODE_STRING'
  )
)

Вот теперь мы во всеоружии и готовы «читать» указатель.

$len = [Marshal]::SizeOf($UNICODE_STRING) - 1
$tmp = $ptr
$Processes = while (($$ = [Marshal]::ReadInt32($tmp))) { #NextEntryOffset
  [Byte[]]$bytes = 0..$len | ForEach-Object {$ofb = 0x38}{
    [Marshal]::ReadByte($tmp, $ofb)
    $ofb++
  }
  #конвертируем байты в UNICODE_STRING
  $gch = [Runtime.InteropServices.GCHandle]::Alloc($bytes, 'Pinned')
  $uni = [Marshal]::PtrToStructure(
    $gch.AddrOfPinnedObject(), [Type]$UNICODE_STRING.GetType()
  )
  $gch.Free()
  
  New-Object PSObject -Property @{
    ProcessName = if ([String]::IsNullOrEmpty((
      $proc = $uni.GetType().GetField(
        'Buffer', [Reflection.BindingFlags]36
      ).GetValue($uni))
    )) { 'Idle' } else { $proc }
    PID = [Marshal]::ReadInt32($tmp, 0x44)
    PPID = [Marshal]::ReadInt32($tmp, 0x48)
  }
  $tmp = [IntPtr]($tmp.ToInt32() + $$)
}

Переменная $Processes отныне хранит массив объектов PSObject, эдакие контейнеры для нужных нам данных. Теперь, согласно женевской конвенции, остается построить само дерево.

function Get-ProcessChild {
  param(
    [Parameter(Mandatory=$true, Position=0)]
    [PSObject]$Process,
    
    [Parameter(Position=1)]
    [Int32]$Depth = 1
  )
  
  $Processes | Where-Object {
    $_.PPID -eq $Process.PID -and $_.PPID -ne 0
  } | ForEach-Object {
    "$("$([Char]32)" * 2 * $Depth)$($_.ProcessName) ($($_.PID))"
    Get-ProcessChild $_ (++$Depth)
    $Depth--
  }
}

$Processes | Where-Object {
  -not (Get-Process -Id $_.PPID -ea 0) -or $_.PPID -eq 0
} | ForEach-Object {
  "$($_.ProcessName) ($($_.PID))"
  Get-ProcessChild $_
}

После запуска получим в хосте древовидное представление процессов. Собственно, на этом вечерний эротический сеанс показ окончен, можно расходиться.

Автор: GrigoriZakharov

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js