Обобщенные делегаты в PowerShell

Идея появилась давно, но вот на ее практическую реализацию, а тем более описание, времени не хватало. Были лишь какие-то заметки на полях и черновики, упорядочить которые удалось лишь несколько дней тому назад. Возможно потому, что это были действительно выходные, а не их иллюзорное представление. И вот тут-то появляется риск впасть в рассуждения на тему рабочей загруженности и прочего в этом духе, так что перейдем, пожалуй, непосредственно к делу.
Обобщенные делегаты в PowerShell — явление редкое, можно даже сказать экзотическое. А ежели говорить относительно собственного опыта, они и вовсе ни разу не встречались, что в свою очередь может создать ложное впечатление, мол, ими никто не пользуется. Можно, конечно, заложиться на почку, дескать, так оно и есть, токмо перспектива остаться без этой самой почки не очень-то радужная. Забегая наперед, отметим, что рефлексия в купе с обобщенными делегатами позволяет вызывать некоторые WinAPI функции без создания динамических сборок, тем самым, казалось бы, увеличивая уровень возможности компрометации системы за счет вызовов «нужных» функций и отсутствия «левых» сборок в текущем домене приложений, но ключевым здесь является слово «некоторые», так что дух злокодинга здесь и близко не стоял, а вот для каких-то бытовых целей описанный ниже прием вполне может сгодиться.

Создание базовой библиотеки


Как уже было сказано выше, для успешного вызова WinAPI функций наряду с обобщенными делегатами используется рефлексия — главным образом для того, чтобы, во-первых, получить указатель экспортируемой функции из указанной DLL, во-вторых, для объявления динамического метода. Для получения указателя, наша функция будет выглядеть так:

function Get-ProcAddress {
  param(
    [Parameter(Mandatory=$true, Position=0)]
    [ValidateNotNullOrEmpty()]
    [String]$Module,
    
    [Parameter(Mandatory=$true, Position=1)]
    [ValidateNotNullOrEmpty()]
    [String]$Function
  )
  
  [Data.Rule].Assembly.GetType(
    'System.Data.Common.SafeNativeMethods'
  ).GetMethods(
    [Reflection.BindingFlags]40
  ) | Where-Object {
    $_.Name -cmatch '\AGet(ProcA|ModuleH)'
  } | ForEach-Object {
    Set-Variable $_.Name $_
  }
  
  if (($ptr = $GetModuleHandle.Invoke(
    $null, @($Module)
  )) -eq [IntPtr]::Zero) {
    if (($mod = [Regex].Assembly.GetType(
      'Microsoft.Win32.SafeNativeMethods'
    ).GetMethod('LoadLibrary').Invoke(
      $null, @($Module)
    )) -eq [IntPtr]::Zero) {
      Write-Warning "$([PSObject].Assembly.GetType(
        'Microsoft.PowerShell.Commands.Internal.Win32Native'
      ).GetMethod(
        'GetMessage', [Reflection.BindingFlags]40
      ).Invoke($null, @(
        [Runtime.InteropServices.Marshal]::GetLastWin32Error()
      )))"
      break
    }
    $ptr = $GetModuleHandle.Invoke($null, @($Module))
  }
  
  $GetProcAddress.Invoke($null, @($ptr, $Function)), $mod
}


Функция ищет указатель на DLL, отображенный в адресном пространстве PowerShell и если таковой найден, возвращает его, в противном случае пытается загрузить указанную DLL и повторно найти указатель, после — извлекается адрес экспортируемой функции. Возвратом Get-ProcAddress будет массив из адреса экспортируемой функции и указателя загруженной DLL. Иными словами:

$ptr, $null = Get-ProcAddress ntdll NtQuerySystemInformation



Зная, что ntdll.dll уже имеется, а адресном пространстве PowerShell, заносим данные возвращаемого массива в переменные $ptr и $null. В случае же, когда DLL погружается, например:

$ptr, $mod = Get-ProcAddress msi MsiEnumProductsA



было бы идеологически правильным освобождать эту DLL, поэтому наряду с функцией Get-ProcAddress наша базовая библиотека будет содержать еще и функцию Invoke-FreeLibrary:

function Invoke-FreeLibrary {
  param(
    [Parameter(Mandatory=$true)]
    [IntPtr]$ModuleHandle
  )
  
  [Linq.Enumerable].Assembly.GetType(
    'Microsoft.Win32.UnsafeNativeMethods'
  ).GetMethod(
    'FreeLibrary', [Reflection.BindingFlags]40
  ).Invoke($null, @($ModuleHandle)) | Out-Null
}


Все, что остается сделать — «преобразовать» указатель функции в делегат.

function Set-Delegate {
  param(
    [Parameter(Mandatory=$true, Position=0)]
    [ValidateScript({$_ -ne [IntPtr]::Zero})]
    [IntPtr]$ProcAddress,
    
    [Parameter(Mandatory=$true, Position=1)]
    [ValidateNotNullOrEmpty()]
    [String]$Delegate
  )
  
  $proto = Invoke-Expression $Delegate
  $method = $proto.GetMethod('Invoke')
  
  $returntype = $method.ReturnType
  $paramtypes = $method.GetParameters() |
                           Select-Object -ExpandProperty ParameterType
  
  $holder = New-Object Reflection.Emit.DynamicMethod(
    'Invoke', $returntype, $paramtypes, $proto
  )
  $il = $holder.GetILGenerator()
  0..($paramtypes.Length - 1) | ForEach-Object {
    $il.Emit([Reflection.Emit.OpCodes]::Ldarg, $_)
  }
  
  switch ([IntPtr]::Size) {
    4 { $il.Emit([Reflection.Emit.OpCodes]::Ldc_I4, $ProcAddress.ToInt32()) }
    8 { $il.Emit([Reflection.Emit.OpCodes]::Ldc_I8, $ProcAddress.ToInt64()) }
  }
  $il.EmitCalli(
    [Reflection.Emit.OpCodes]::Calli,
    [Runtime.InteropServices.CallingConvention]::StdCall,
    $returntype, $paramtypes
  )
  $il.Emit([Reflection.Emit.OpCodes]::Ret)
  
  $holder.CreateDelegate($proto)
}


Здесь также все предельно просто: получаем возвращаемый тип, типы параметров и объявляем динамический метод, из которого и создается делегат.

Собираем в единое целое


Оформление базовой библиотеки может выглядеть по-разному, например, в виде модуля (man about_Modules) — кто как пожелает. Мы же сфокусируемся главным образом на возможности запуска WinAPI функций как таковых.
Давайте посмотрим на следующий пример:

function New-HardLink {
  param(
    [Parameter(Mandatory=$true, Position=0)]
    [ValidateScript({Test-Path $_})]
    [ValidateNotNullOrEmpty()]
    [String]$Source,
    
    [Parameter(Mandatory=$true, Position=1)]
    [ValidateNotNullOrEmpty()]
    [String]$Destination
  )
  
  begin {
    $ptr, $null = Get-ProcAddress kernel32 CreateHardLinkA
    $CreateHardLink = Set-Delegate $ptr '[Func[String, String, IntPtr, Boolean]]'
    
    $Source = Resolve-Path $Source
  }
  process {
    $CreateHardLink.Invoke($Destination, $Source, [IntPtr]::Zero)
  }
  end {}
}


Импортированная в текущий сеанс, эта функция позволяет создавать жесткие ссылки:

PS E:\Users\greg\Documents> New-HardLink .\habrapost.txt E:\temp\post.txt



Другой пример — дать некоторую привилегию хосту PowerShell (пусть это будет SeShutdownPrivilege):

function Set-Privilege {
  param(
    [Parameter(Position=0)]
    [ValidateRange(2, 35)]
    [UInt32]$Privilege = 19, #SeShutdownPrivilege
    
    [Parameter(Position=1)]
    [Switch]$Enable = $true
  )
  
  begin {
    $ptr, $null = Get-ProcAddress ntdll RtlAdjustPrivilege
    $RtlAdjustPrivilege = Set-Delegate $ptr `
          '[Action[UInt32, Boolean, Boolean, Text.StringBuilder]]'
    $ret = New-Object Text.StringBuilder
  }
  process {
    $RtlAdjustPrivilege.Invoke($Privilege, $Enable, $false, $ret)
  }
  end {}
}


После импорта:

#дали привилегию
PS E:\> Set-Privilege
#забрали
PS E:\> Set-Privilege 19 $false



Подводя итоги


Если вы заметили, в примерах выше ни разу не была использована функция Invoke-FreeLibrary. Это объясняется тем, что в адресном пространстве PowerShell и kernel32, и ntdll уже имеются. Если же нам потребовалась некая функция, скажем, из msi.dll, то код бы пришлось дополнить так:

$ptr, $mod = Get-ProcAddress msi MsiEnumProductsA
$MsiEnumProducts = Set-Delegate $ptr '[Func[Int32, Text.StringBuilder, Int32]]'

#некий полезный код
...

Invoke-FreeLibrary $mod


Таким образом — с помощью рефлексии и обобщенных делегатов — можно вызывать некоторые WinAPI функции.

© Habrahabr.ru