PowerShell: за гранью. Часть четвертая

в 18:02, , рубрики: powershell, powershell2, Программирование

Рейтинги — показатель пристрастий, а не объективности
или проницательности ума голосующего.

Г.Гаретт

Наверняка среди администраторов Windows найдутся те, кого озадачивали критерии, согласно которым разработчики PowerShell определяют какие командлеты непременно должны быть включены в основную поставку, а какие — нет. И нужно отметить, что без бутылки, а то и двух, здесь явно не разобраться. Дело, правда, не в количестве спиртного, прямо пропорционального достижению трезвости ума, главную роль здесь скорее играет все же попытка разработчиков мыслить глобальными категориями. Одни говорят, дескать, попытка эта обречена в самом зародыше, другие видят во всем этом некие перспективы, третьи, будучи приверженцами идеи «без разницы какая ОСь, лишь бы консоль», просто продолжают работать, тешась перепалками первых двух.

One vision, one purpose!
Для чего был создан PowerShell догадаться несложно, если поработать поочередно с ним и голой консолью этак минут десять. Необходимость инструмента, способного решать задачи далеко за пределами проблем насущных, среди администраторов сетей Windows зрела давно, а когда они ее получили, как-то странно стали ею распоряжаться: кто-то стал размениваться на написание блогов, в которых главным образом были пространные рассуждения о нюансах, становящиеся очевидными на практике; более предприимчивые строчат книги, являющиеся в подавляющем большинстве вольным пересказом справочного руководства и лишь немногие увидели в PowerShell наряду со средством автоматизации полноценный интерпретируемый язык программирования. В некоторых случаях выбор консоли в пользу PowerShell, ровно как и наоборот, особой погоды не делает. Например, администратору без разницы как создать конрольную точку восстановления.

wmic /namespace:\rootdefault path SystemRestore call CreateRestorePoint "Системная контрольная точка", 100, 7

Или то же, но в PowerShell:

[void]([wmiclass]'\.rootdefault:SystemRestore').CreateRestorePoint('Системная контрольная точка', 7, 100)

В том же PowerShell'е:

man Checkpoint-Computer

И все же то, что в сущности служит одной цели, расходится во многом благодаря возможностям.

Shell… PowerShell!
С момента появления голубого хоста на свет кода для последнего написано немало. Главным козырем PowerShell, пожалуй, стали модули, позволившие расширять потенциал последнего до пределов, ограниченных разве что фантазией скриптописателя. Наряду со сценариями полезными есть вещи довольно бесполезные, которые иначе как демонстрацией возможностей PowerShell и не назовешь. Если брать личный опыт, одной из таких бестолковых вещей были аналоговые часы.
image

function Show-AnalogClock {
  Add-Type -AssemblyName System.Windows.Forms
  
  function private:radpnt([Int32]$radius, [Int32]$seconds) {
    $c = New-Object Drawing.Point(($this.ClientRectangle.Width / 2), ($this.ClientRectangle.Height / 2))
    [Double]$angle =- (($seconds - 15) % 60) * [Math]::PI / 30
    New-Object Drawing.Point(
      ($c.X + [Int32]($radius * [Math]::Cos($angle))),
      ($c.Y - [Int32]($radius * [Math]::Sin($angle)))
    )
  }
  
  $frmMain = New-Object Windows.Forms.Form
  $tmrTick = New-Object Windows.Forms.Timer
  #
  #tmrTick
  #
  $tmrTick.Enabled = $true
  $tmrTick.Interval = 1000
  $tmrTick.Add_Tick({$frmMain.Invalidate()})
  #
  #frmMain
  #
  $frmMain.ClientSize = New-Object Drawing.Size(150, 150)
  $frmMain.FormBorderStyle = [Windows.Forms.FormBorderStyle]::FixedSingle
  $frmMain.Icon = [Drawing.Icon]::ExtractAssociatedIcon("$PSHomepowershell.exe")
  'MaximizeBox', 'MinimizeBox' | % {$frmMain.$_ = $false}
  $frmMain.StartPosition = [Windows.Forms.FormStartPosition]::CenterScreen
  $frmMain.Text = 'Analog Clock'
  $frmMain.Add_Paint({
    $now = [DateTime]::Now
    
    $g = $this.CreateGraphics()
    $c = New-Object Drawing.Point(($this.ClientRectangle.Width / 2), ($this.ClientRectangle.Height / 2))
    $r = [Math]::Min($this.ClientRectangle.Width, $this.ClientRectangle.Height) / 2
    #фон циферблата
    $b = New-Object Drawing.Drawing2D.LinearGradientBrush(
      $this.ClientRectangle, [Drawing.Color]::Linen, [Drawing.Color]::DarkGreen,
      [Drawing.Drawing2D.LinearGradientMode]::BackwardDiagonal
    )
    
    $g.FillEllipse($b, $c.X - $r, $c.Y - $r, $r * 2, $r * 2)
    #минутные засечки
    for ($min = 0; $min -lt 60; $min++) {
      [Drawing.Point]$pnt = radpnt ($r - 10) $min
      $sb = New-Object Drawing.SolidBrush([Drawing.Color]::Black)
      
      if (($min % 5) -eq 0) {
        $g.FillRectangle($sb, $pnt.X - 3, $pnt.Y - 3, 6, 6)
      }
      else {
        $g.FillRectangle($sb, $pnt.X - 1, $pnt.Y - 1, 2, 2)
      }
    }
    #стрелки
    $hp = New-Object Drawing.Pen([Drawing.Color]::Black, 8)
    $mp = New-Object Drawing.Pen([Drawing.Color]::Black, 6)
    $sp = New-Object Drawing.Pen([Drawing.Color]::Red, 1)
    #отрисовка стрелок
    $hp, $mp | % {
      $_.StartCap = [Drawing.Drawing2D.LineCap]::Round
      $_.EndCap = [Drawing.Drawing2D.LineCap]::Round
    }
    $sp.CustomEndCap = New-Object Drawing.Drawing2D.AdjustableArrowCap(2, 3, $true)
    $pin = New-Object Drawing.SolidBrush([Drawing.Color]::Red)
    $g.DrawLine(
      $hp,
      (radpnt 15 (30 + $now.Hour * 5 + $now.Minute / 12)),
      (radpnt ([Int32]($r * 0.55)) ($now.Hour * 5 + $now.Minute / 12))
    )
    $g.DrawLine($mp, (radpnt 15 (30 + $now.Minute)), (radpnt ([Int32]($r * 0.8)) $now.Minute))
    $g.DrawLine($sp, (radpnt 20 ($now.Second + 30)), (radpnt ($r - 2) $now.Second))
    $g.FillEllipse($pin, $c.X - 5, $c.Y - 5, 10, 10)
  })
  
  [void]$frmMain.ShowDialog()
}

Есть поработать напильником, можно довести пример до ума. Но это все, как было сказано ранее, из разряда баловства. Лучше давайте обратимся к задачам, которые вполне могут возникнуть в повседневной практике. Например, как дать процессу привилегию? Можно наколбасить код на C# и использовать Add-Type, но если поковыряться ILDASM'ом в недрах сборок .NET Framework, можно задействовать рефлексию.

function Set-Privilege {
  param(
    [Parameter(Mandatory=$true, Position=0)]
    [ValidateSet(
      'SeAssignPrimaryTokenPrivilege', 'SeAuditPrivilege', 'SeBackupPrivilege', 'SeChangeNotifyPrivilege',
      'SeCreateGlobalPrivilege', 'SeCreatePagefilePrivilege', 'SeCreatePermanentPrivilege',
      'SeCreateSymbolicLinkPrivilege', 'SeCreateTokenPrivilege', 'SeDebugPrivilege', 'SeEnableDelegationPrivilege',
      'SeImpersonatePrivilege', 'SeIncreaseBasePriorityPrivilege', 'SeIncreaseQuotaPrivilege',
      'SeIncreaseWorkingSetPrivilege', 'SeLoadDriverPrivilege', 'SeLockMemoryPrivilege', 'SeMachineAccountPrivilege',
      'SeManageVolumePrivilege', 'SeProfileSingleProcessPrivilege', 'SeRelabelPrivilege', 'SeRemoteShutdownPrivilege',
      'SeRestorePrivilege', 'SeSecurityPrivilege', 'SeShutdownPrivilege', 'SeSyncAgentPrivilege',
      'SeSystemEnvironmentPrivilege', 'SeSystemProfilePrivilege', 'SeSystemtimePrivilege', 'SeTakeOwnershipPrivilege',
      'SeTcbPrivilege', 'SeTimeZonePrivilege', 'SeTrustedCredManAccessPrivilege', 'SeUndockPrivilege',
      'SeUnsolicitedInputPrivilege'
    )]
    [String]$Privilege,
    
    [Parameter(Position=1)]
    [Switch]$Disable,
    
    [Parameter(Position=2)]
    [Diagnostics.Process]$Process = (Get-Process -Id $PID)
  )
  
  begin {
    #SE_PRIVILEGE_[DIS|EN]ABLED
    ($Win32Native = ($mscorlib = [Object].Assembly).GetType(
      'Microsoft.Win32.Win32Native'
    )).GetFields(($bfs = [Reflection.BindingFlags]40)) | ? {
      $_.Name -match 'Ase_p.*dZ'
    } | % {
      Set-Variable $_.Name ([UInt32]$_.GetValue($null))
    }
    #AdjustTokenPrivileges, LookupPrivilegeValue и OpenProcessToken
    $Win32Native.GetMethods($bfs) | ? {
      $_.Name -match 'A(Adjust|LookupP|OpenP).*Z'
    } | % {
      Set-Variable $_.Name $_
    }
    #LUID, LUID_AND_ATTRIBUTES и TOKEN_PRIVILEGES
    $Win32Native.GetNestedTypes(($bfi = [Reflection.BindingFlags]36)) | ? {
      $_.Name -match 'A(LUID|TOKEN_P).*Z'
    } | % {
      Set-Variable $_.Name $_
    }
  }
  process {
    try {
      $SafeTokenHandle = $mscorlib.GetType(
        'Microsoft.Win32.SafeHandles.SafeTokenHandle'
      ).GetConstructor(
        $bfi, $null, [Type[]]@([IntPtr]), $null
      ).Invoke([IntPtr]::Zero)
      
      if (!$OpenProcessToken.Invoke($null, (
        $sth = [Object[]]@($Process.Handle, [Security.Principal.TokenAccessLevels]40, $SafeTokenHandle)
      ))) {
        throw (New-Object Exception('Не могу найти указанный процесс.'))
      }
      
      $LUID = [Activator]::CreateInstance($LUID)
      $LUID.GetType().GetFields($bfi) | % { $_.SetValue($LUID, [UInt32]0) }
      
      if (!$LookupPrivilegeValue.Invoke($null, (
        $LUID = [Object[]]@($null, $Privilege, $LUID)
      ))) {
        throw (New-Object Exception('Не могу проверить LUID.'))
      }
      
      $State = switch ($Disable) {
        $true  { $SE_PRIVILEGE_DISABLED }
        $false { $SE_PRIVILEGE_ENABLED }
      }
      
      $LUID_AND_ATTRIBUTES = [Activator]::CreateInstance($LUID_AND_ATTRIBUTES)
      $LUID_AND_ATTRIBUTES.GetType().GetField('Luid', $bfi).SetValue($LUID_AND_ATTRIBUTES, $LUID[2])
      $LUID_AND_ATTRIBUTES.GetType().GetField('Attributes', $bfi).SetValue($LUID_AND_ATTRIBUTES, $State)
      
      $TOKEN_PRIVILEGE = [Activator]::CreateInstance($TOKEN_PRIVILEGE)
      $TOKEN_PRIVILEGE.GetType().GetField('Privilege', $bfi).SetValue($TOKEN_PRIVILEGE, $LUID_AND_ATTRIBUTES)
      $TOKEN_PRIVILEGE.GetType().GetField('PrivilegeCount', $bfi).SetValue($TOKEN_PRIVILEGE, [UInt32]1)
      
      [UInt32]$sz = [Runtime.InteropServices.Marshal]::SizeOf($TOKEN_PRIVILEGE)
      if (!$AdjustTokenPrivileges.Invoke($null, @(
        $sth[2], $false, $TOKEN_PRIVILEGE, $sz, $null, $null
      ))) {
        throw (New-Object Exception('Не могу дать привилегию.'))
      }
    }
    catch { $_.Exception }
    finally {
      if ($sth -is [Array] -and $sth[2] -ne $null) { $sth[2].Close() }
      if ($SafeTokenHandle -ne $null) { $SafeTokenHandle.Close() }
    }
  }
  end {}
}

Собственно, даем привилегию:

PS C:> Set-Privilege SeShutdownPrivilege

Привилегию забираем:

PS C:> Set-Privilege SeShutdownPrivilege -Disable

Peace through Power!
Напоследок стоит заметить, что какая-то одна задача может быть решена в PowerShell несколькими способами. Не будем выдумывать что-то сверъестественное, а возьмем заезженный пример: дата последнего входа пользователя в систему. Итак, вариант первый:

$Marshal = [Runtime.InteropServices.Marshal]
$ft = New-Object Runtime.InteropServices.ComTypes.FILETIME

$raw = (gp "HKLM:SOFTWAREMicrosoftWindows NTCurrentVersionProfileList$(
  [Security.Principal.WindowsIdentity]::GetCurrent().User.Value
)").PSObject.Properties | ? {$_.Name -match 'time'}
$ft.dwLowDateTime = $raw[0].Value
$ft.dwHighDateTime = $raw[1].Value

try {
  $ptr = $Marshal::AllocHGlobal($Marshal::SizeOf($ft))
  $Marshal::StructureToPtr($ft, $ptr, $false)
  [DateTime]::FromFileTime($Marshal::ReadInt64($ptr))
}
catch { $_.Exception }
finally {
  if ($ptr -ne [IntPtr]::Zero) { $Marshal::FreeHGlobal($ptr) }
}

Вариант второй:

(gp ('HKLM:SOFTWAREMicrosoftWindows NTCurrentVersionProfilelist' +
[Security.Principal.WindowsIdentity]::GetCurrent().User.Value) |
select @{N='LoggedTime';E={
  ([DateTime]'1.1.1601').AddDays(
    ($_.ProfileLoadTimeHigh * [Math]::Pow(2, 32) + $_.ProfileLoadTimeLow) / (
      6 * [Math]::Pow(10, 8)
    ) / 1440
  ).ToLocalTime()
}}).LoggedTime

А если поэксперименировать, найдется еще и третий, и прочие варианты.

Автор: gregzakharov

Источник

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


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