Обобщенные делегаты в 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 функции.