Pipelist в PowerShell

в 10:33, , рубрики: C#, powershell, Программирование, разработка под windows

Наиболее распространенным методом получения списка именованных каналов среди буржуинов является незатейливая команда:

[IO.Directory]::GetFiles(($$='\.pipe'))|%{$_.Replace($$, '')}

Однако тем, кому ранее доводилось использовать pipelist из набора Sysinternals Suite, вывод данной команды явно покажется малоинформативным.

Доступ к пайпам через метод GetFiles возможен благодаря двум WinAPI функциям, оберткой для которых он является: FindFirstFile и FindNextFile. Те в свою очередь используют ряд NativeAPI функций, одна из которых — NtQueryDirectoryFile (ее описание вполне можно найти в MSDN) — согласно дизассемблеру задействуется в утилите pipelist.exe напрямую. Давайте посмотрим как это все работает в приближении.

Find[First|Next]File

Здесь изобретать ничего не будем, а воспользуемся рефлексией для демонстрации механизма заложенного в методе GetFiles.

#извлекаем FindFirstFile и FindNextFile
[Object].Assembly.GetType(
  'Microsoft.Win32.Win32Native'
).GetMethods(
  [Reflection.BindingFlags]40
) | Where-Object {
  $_.Name -cmatch 'AFind(First|Next)FileZ'
} | ForEach-Object {
  Set-Variable $_.Name $_
}

#обе функции требуют структуру WIN32_FIND_DATA
$WIN32_FIND_DATA = [Object].Assembly.GetType(
  'Microsoft.Win32.Win32Native+WIN32_FIND_DATA'
).GetConstructor(
  [Reflection.BindingFlags]20, $null, [Type[]]@(), $null
).Invoke($null)

#если вдруг не удалось получить доступ к пайпам
if (($fff = $FindFirstFile.Invoke($null, @(
  '\.pipe*', $WIN32_FIND_DATA
))).IsInvalid) {
  (New-Object ComponentModel.Win32Exception(
    [Runtime.InteropServices.Marshal]::GetLastWin32Error()
  )).Message
  break
}

#лямбда-функция, выводящая данные из структуры WIN32_FIND_DATA
&($lambda = {
  $WIN32_FIND_DATA.GetType().GetFields(
    [Reflection.BindingFlags]36
  ) | ForEach-Object {$data = @{}}{
    $data[$_.Name] = $_.GetValue($WIN32_FIND_DATA)
  }{
    $data | Select-Object @{N='PipeName';E={$_.cFileName}}, @{
      N='Instances';E={$_.nFileSizeLow}
    }
  }
})

#вызываем FindNextFile до тех пор, пока она не вернет $false
while ($FindNextFile.Invoke($null, @($fff, $WIN32_FIND_DATA))) {
  &$lambda
}

#освобождаем ресурсы
if ($fff) {
  $fff.Dispose()
  $fff.Close()
}

Вывод становится более приближенным к pipelist.exe, не хватает разве что графы «Max Instances», но это максимум, который можно выжать в данном случае.

NtQueryDirectoryFile

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

Код командлета
using System;
using System.IO;
using System.ComponentModel;
using Microsoft.Win32.SafeHandles;
using System.Management.Automation;
using System.Runtime.InteropServices;
using System.Diagnostics.CodeAnalysis;

namespace Pipelist {
  [StructLayout(LayoutKind.Explicit, Size = 8)]
  internal struct LARGE_INTEGER {
    [SuppressMessage("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
    [FieldOffset(0)]internal Int64 QuadPart;
    [FieldOffset(0)]internal UInt32 LowPart;
    [SuppressMessage("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
    [FieldOffset(4)]internal Int32 HighPart;
  }
  
  [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
  internal struct FILE_DIRECTORY_INFORMATION {
    internal UInt32        NextEntryOffset;
    internal UInt32        FileIndex;
    internal LARGE_INTEGER CreationTime;
    internal LARGE_INTEGER LastAccessTime;
    internal LARGE_INTEGER LastWriteTime;
    internal LARGE_INTEGER ChangeTime;
    internal LARGE_INTEGER EndOfFile;
    internal LARGE_INTEGER AllocationSize;
    internal UInt32        FileAttributes;
    internal UInt32        FileNameLength;
    internal UInt16        FileName;
    
    [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic")]
    internal Int32 FileNameOffset { //дабы не вызывать непосредственно в цикле
      get {
        return Marshal.OffsetOf(
          typeof(FILE_DIRECTORY_INFORMATION), "FileName"
        ).ToInt32();
      }
    }
  }
  
  internal static class NativeMethods {
    [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
    internal static extern SafeFileHandle CreateFile(
        String    lpFileName,
        UInt32    dwDesiredAccess,
        FileShare dwShareMode,
        IntPtr    lpSecuriryAttributes,
        FileMode  dwCreationDisposition,
        UInt32    dwFlagsAndAttributes,
        IntPtr    hTemplateFile
    );
    
    [DllImport("ntdll.dll")]
    internal static extern Int32 NtQueryDirectoryFile(
        SafeFileHandle FileHandle,
        IntPtr         Event,
        IntPtr         ApcRoutine,
        IntPtr         ApcContext,
        out IntPtr     IoStatusBlock,
        [Out] IntPtr   FileInformation,
        UInt32         Length,
        UInt32         FileInformationClass,
        [MarshalAs(UnmanagedType.Bool)]
        Boolean        ReturnSingleEntry,
        IntPtr         FileName,
        [MarshalAs(UnmanagedType.Bool)]
        Boolean        RestartScan
    );
  }
  
  [Cmdlet(VerbsCommon.Get, "PipeList")]
  public sealed class GetPipeListCommand : PSCmdlet {
    const UInt32 FileDirectoryInformation = 1;
    const UInt32 GENERIC_READ             = 0x80000000;
    const Int32  STATUS_SUCCESS           = 0x00000000;
    const Int32  BufferLength             = 0x00001000;
    
    private static void GetLastError() {
      Console.WriteLine(
        new Win32Exception(Marshal.GetLastWin32Error()).Message
      );
    }
    
    private static T PtrToStruct<T>(IntPtr p) {
      return (T)Marshal.PtrToStructure(p, typeof(T));
    }
    
    protected override void ProcessRecord() {
      SafeFileHandle pipes;
      IntPtr dir, tmp, isb;
      Boolean query = true;
      
      pipes = NativeMethods.CreateFile( //цепляемся к пайпам
        @"\.\pipe", GENERIC_READ, FileShare.Read, IntPtr.Zero, FileMode.Open, 0, IntPtr.Zero
      );
      
      if (pipes.IsInvalid) {
        GetLastError();
        return;
      }
      
      Console.WriteLine("{0,-40}{1,14}{2,20}", "Pipe Name", "Instances", "Max Instances");
      Console.WriteLine("{0,-40}{1,14}{2,20}", "---------", "---------", "-------------");
      
      dir = Marshal.AllocHGlobal(BufferLength);
      try {
        while (true) { //"опрашиваем" до тех пор, пока ничего не останется
          if (NativeMethods.NtQueryDirectoryFile(
            pipes, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, out isb, dir,
            BufferLength, FileDirectoryInformation, false, IntPtr.Zero, query
          ) != STATUS_SUCCESS) break;
          
          tmp = dir;
          while (true) {
            FILE_DIRECTORY_INFORMATION fdi = PtrToStruct<FILE_DIRECTORY_INFORMATION>(tmp);
            IntPtr name = (IntPtr)(fdi.FileNameOffset + tmp.ToInt32());
            
            Console.WriteLine("{0,-40}{1,14}{2,20}",
              Marshal.PtrToStringUni(name, (Int32)(fdi.FileNameLength / 2)),
              fdi.EndOfFile.LowPart,
              (Int32)fdi.AllocationSize.LowPart
            );
            
            if (fdi.NextEntryOffset == 0) break;
            tmp = (IntPtr)(tmp.ToInt32() + fdi.NextEntryOffset);
          }
          
          query = false;
        }
      }
      catch (Exception e) {
        Console.WriteLine(e);
      }
      finally {
        Marshal.FreeHGlobal(dir);
        pipes.Dispose();
      }
      
      pipes.Close();
    }
  }
}

Идеологически правильным было бы использовать что-то вроде WriteObject вместо Console.WriteLine при создании командлета, код же выше — всего лишь пример, в котором можно поступиться идеологией.

Сборка командлета из хоста PowerShell такова: прописываем в переменной PATH путь до компилятора C# и переходим в каталог с исходником.

PS E:> $env:path += ';E:WindowsMicrosoft.NETvXXXX' #XXXX -версия фреймворка
PS E:> pushd proj
PS E:proj>

Непосредственно сборка:

PS E:proj> copy $([PSObject].Assembly.Location)
PS E:proj> csc /nologo /t:library /out:PipeList.dll /optimize+ /debug:pdbonly /r:System.Management.Automation.dll source.cs

Сборку импортируем как модуль:

PS E:proj> Import-Module .PipeList.dll

Вызываем наш командлет:

PS E:proj> Get-PipeList

Вот теперь у нас полноценный pipelist.exe. При желании можно дополнить код «расшифровкой» отдельно взятого пайпа, скажем, keysvc относится к одному из криптографических сервисов IKeySvc или ICertProtect, а ntsvcs — к сервисам plug and play, но это нечто из разряда свистоперделок.

Автор: GrigoriZakharov

Источник

Поделиться новостью

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