Рефлексия в PowerShell

от автора

Немодные вещи куда интересней нежели то, что у всех на слуху и на виду. В мире .NET, например, немодной является рефлексия, о которой знают, но не пользуются в виду преклонения перед мантрами Рихтера. Несомненно, монография «CLR via C#» — лучшее из книг о .NET, однако сам ее автор далеко не везде следует своим же рекомендациям, а потому принимающим на веру абсолютно все написанное в ней, стоит перестать выдавать чужие мысли за свои.

Рефлексия типов действительно достаточно медленная вещь, но не настолько, чтобы отказаться от ее использования вовсе. В случае PowerShell издержки на упаковку и распаковку практически незаметны глазу, поэтому за производительность шибко опасаться не приходится. При этом в некоторых случаях использование рефлексии способно существенно сократить количество кода, что в свою очередь упрощает сопровождение последнего (с чем модники категорически не согласны) и открывает доступ к интимным местам операционной системы в обход оснастке управления (WMI). С точки зрения безопасности это не очень-то и хорошо, но вот в плане системного администрирования — недурственно. Хотя у этого мнения также найдутся свои противники.
Допустим, мы все же решились на использование рефлексии: как наиболее эффективно ее применять в PowerShell? Во-первых, готовых рецептов ни у кого нет, да и вряд ли когда-то будут, ибо самая суть уже описана все в той же «CLR via C#», во-вторых, само по себе понятие «эффективность» относительно, следовательно, рефлексию можно рассматривать лишь как альтернативный вариант решения некоторых задач. В качестве примера — пусть и весьма натянутого, — рассмотрим получение сборок установленных в GAC.

#requires -version 2  $al = New-Object Collections.ArrayList [Object].Assembly.GetType(   'Microsoft.Win32.Fusion' ).GetMethod('ReadCache').Invoke($null, @(   [Collections.ArrayList]$al, $null, [UInt32]2 )) $al 

В отсутствии gacutil пример вполне может заменить собой первый, запускаемый с ключом /l. Впрочем, интереснее методов-оберток могут быть только WinAPI сигнатуры, однако перебирать типы в которых они имеются ILDASM’ом или просто ковыряться в исходных кода .NET платформы не шибко заманчиво. Почему бы не доверить эту работу самому PowerShell?!

 function Find-Pinvoke {   <#     .EXAMPLE         PS C:\> Find-Pinvoke Regex   #>   param(     [Parameter(Mandatory=$true)]     [ValidateNotNullOrEmpty()]     [String]$TypeName   )      begin {     if (($base = $TypeName -as [Type]) -eq $null) {       Write-Warning "В текущем домене приложений указанный тип не нйден."       break     }   }   process {     foreach ($type in $base.Assembly.GetTypes()) {       $type.GetMethods([Reflection.BindingFlags]60) | % {         if (($_.Attributes -band 0x2000) -eq 0x2000) {           $sig = [Reflection.CustomAttributeData]::GetCustomAttributes(             $_ # данные о pinvoke методе           ) | ? {$_.ToString() -cmatch 'DllImportAttribute'}           New-Object PSObject -Property @{             Module     = if (![IO.Path]::HasExtension(               ($$ = $sig.ConstructorArguments[0].Value)             )) { "$($$).dll" } else { $$ }             EntryPoint = ($sig.NamedArguments | ? {               $_.MemberInfo.Name -eq 'EntryPoint'             }).TypedValue.Value             MethodName = $_.Name             Attributes = $_.Attributes             TypeName   = $type.FullName             Signature  = $_.ToString() -replace '(\S+)\s+(.*)', '$2 as $1'             DllImport  = $sig           } | Select-Object Module, EntryPoint, TypeName, MethodName, `           Attributes, Signature, DllImport         }       }     } #foreach   }   end {} } 


Как оно работает? Мы передаем название некоторого публичного типа, скажем Regex, функции выше, далее тип проверяется на доступность в текущем домене приложений и извлекаются данные о сигнатурах сборки, в которой этот тип определен. Можно, конечно, вывод перенаправить в XML или любой другой формат, дабы не тратиться на повторное сканирование, но это кому как нравится, да и идея здесь главным образом в том, чтобы не размениваться на поиски сигнатур вручную. А сигнатур, между тем, не просто много, а очень много; особый интерес могут вызвать DeviceIoControl (Systemd.Data.dll), а также NtQueryInformationProcess и NtQuerySystemInformation (System.dll), — и вот здесь мы вплотную подобрались к вещам совершенно немодным: чтению данным по смещениям посредством рефлекторно вызываемых методов. В процессе подбора примера ничего оригинального, кроме как вывести список модулей, загруженных системой, не надумалось, так что будем рассматривать его.

 PS C:\> Invoke-Debugger ... 0.000> dt ole32!_rtl_process_modules /r    +0x000 NumberOfModules  : Uint4B    +0x004 Modules          : [1] _RTL_PROCESS_MODULE_INFORMATION       +0x000 Section          : Ptr32 Void       +0x004 MappedBase       : Ptr32 Void       +0x008 ImageBase        : Ptr32 Void       +0x00c ImageSize        : Uint4B       +0x010 Flags            : Uint4B       +0x014 LoadOrderIndex   : Uint2B       +0x016 InitOrderIndex   : Uint2B       +0x018 LoadCount        : Uint2B       +0x01a OffsetToFileName : Uint2B       +0x01c FullPathName     : [256] UChar 0.000> ?? sizeof(ole32!_rtl_process_modules) unsigned int 0x120 


То есть, размеры структур RTL_PROCESS_MODULES и RTL_PROCESS_MODULE_INFORMATION равны 288 и 284 байт соответственно.

 0.000> dt ole32!_system_information_class ...    SystemModuleInformation = 0n11 ... 


Здорово! Дело за малым.

 #function Get-LoadedModules {   begin {     # акселератор типа Marshal     if (($$ = [PSObject].Assembly.GetType(       'System.Management.Automation.TypeAccelerators'     ))::Get.Keys -notcontains 'Marshal') {       [void]$$::Add('Marshal', [Runtime.InteropServices.Marshal])     }      $NtQuerySystemInformation = [Regex].Assembly.GetType(       'Microsoft.Win32.NativeMethods'     ).GetMethod('NtQuerySystemInformation')     $ret = 0   }   process {     try { # устанавливаем истинный размер буфера       $ptr = [Marshal]::AllocHGlobal(1024)       if ($NtQuerySystemInformation.Invoke($null, (         $par = [Object[]]@(11, $ptr, 1024, $ret)       )) -ne 0) {         $ptr = [Marshal]::ReAllocHGlobal($ptr, [IntPtr]$par[3])         if ($NtQuerySystemInformation.Invoke($null, @(11, $ptr, $par[3], 0)) -ne 0) {           throw New-Object InvalidOperationException('Что-то пошло не так...')         }       }        # считываем интересующую нас информацию относительно смещений       0..([Marshal]::ReadInt32($ptr) - 1) | % {$i = 12}{         New-Object PSObject -Property @{           Address = '0x{0:X}' -f [Marshal]::ReadInt32($ptr, $i)           Size = [Marshal]::ReadInt32($ptr, $i + 4)           Name = [IO.Path]::GetFileName(([Marshal]::PtrToStringAnsi(             [IntPtr]($ptr.ToInt64() + $i + 20), 256           )).Split("`0")[0])         }         $i += 284 # переходим к следующей структуре       } | Select-Object Name, Address, Size | Format-Table -AutoSize     }     catch {       $_.Exception     }     finally {       if ($ptr) { [Marshal]::FreeHGlobal($ptr) }     }   }   end {     [void]$$::Remove('Marshal') # удаляем акселератор   } #} 


Вот таким вот незатейливым способом мы извлекли интересующие нас данные, — ничего сложного. При использовании Add-Type то же заняло примерно одинаковое количество кода, разница лишь в том, что в домене приложений не было создано вспомогательной сборки. Можно ли это считать приятным бонусом или это все же пространство для злокодинг-маневра, вопрос риторический.

ссылка на оригинал статьи https://habrahabr.ru/post/282842/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *