Несмотря на уже довольно давно пройденный рубеж одной операционной системы, PowerShell (далее просто pwsh) по-прежнему не занимает топовых позиций среди прочих языков, что, впрочем, нисколько не смущает киберпреступность. Первый зловред был написан ещё во времена первого поколения могучего шелла, затем были различного рода постэксплуатационные "фреймворки" для скомпрометированных систем, ну а там уж и анекдотов насочиняли. В смысле, была понаписана масса ненужного в информационном плане барахла вроде tips of day или how to do. Иными словами, информационная безопасность в контексте pwsh грозила перерасти в некий stand up, если бы не внезапно для многих случившаяся кроссплатформенность, на которой-то и споткнулись ряд проектов, не говоря уже о матёрых скриптописателях. Как ни странно, но в большинстве статей посвящённых расследованию инцидентов компрометации систем посредством pwsh используется пятая версия последнего. Между тем малварь, написанная на pwsh, существует и под Linux, и под MacOS, что великодушно игнорируется некоторыми ИБ экспертами, - винить во всём Microsoft уже не столько необходимо, сколько вошло в привычку.

В основе pwsh лежит вполне благая идея: дать программистам и системным администраторам удобный расширяемый инструмент управления инфраструктурой различного масштаба (нечто подобное было и с Tcl). Но, как оно часто бывает, хорошее начинание стало заложником не самой лучшей реализации, и то состояние, в котором ныне пребывает pwsh, более напоминает тупик, нежели потенциал. Развиваться проекту уже некуда, разве что обрастать синтаксическим сахаром или довести до ума систему классов, ввести таки структуры и интерфейсы, да кое-что по мелочи. Или же сделать разворот в сторону функционального стиля программирования, как это ныне возможно в C++. О взаимодействии с системным API речь не затрагивается принципиально по нескольким причинам. Во-первых, введение в pwsh атрибута DllImport превратит pwsh в некое подобие C# интерпретатора, во-вторых, в самом pwsh можно вполне отказаться от архаичного и тормознутого Add-Type в пользу, скажем, делегатов. При этом адреса интересуемых функций можно вычислять "налету", чай ведь Get-Process любезно предоставляет информацию по базовым адресам имиджей, а там, вооружившись знаниями о структуре PE или ELF (относительно MacOS ничего не могу сказать, ибо чтобы разжиться им придётся продать почку), глянуть таблицу экспорта проблем не составит. Концептуально (для Windows) это можно представить так:

$GetNtdllExports = {
  end {
    $jmp = ($mov = [Marshal]::ReadInt32(( # IMAGE_NT_HEADERS
      $mod = ($ps = Get-Process -Id $PID).Modules.Where{
        $_.ModuleName -match 'ntdll'}.BaseAddress), 0x3C
      )
    ) + [Marshal]::SizeOf([UInt32]0)
    $ps.Dispose()

    $j = switch ([BitConverter]::ToUInt16( # IMAGE_FILE_HEADER->Machine
      [BitConverter]::GetBytes([Marshal]::ReadInt16($mod, $jmp)), 0)
    ) { # битность, смещения на VA и размер директории экспорта
      0x0014C { 0x20, 0x78, 0x7C }
      0x08664 { 0x40, 0x88, 0x8C }
      default { throw [SystemException]::new() }
    }

    $tmp, $fun = $mod."ToInt$($j[0])"(), @{}
    $va, $sz = $j[1..2].ForEach{[Marshal]::ReadInt32($mod, $mov + $_)}
    ($e=@{bs=0x10;nf=0x14;nn=0x18;af=0x1C;an=0x20;ao=0x24}).Keys.ForEach{
      $$ = [Marshal]::ReadInt32($mod, $va + $e.$_)
      Set-Variable -Name $_ -Value ($_.StartsWith('a') ? $tmp + $$ : $$) -Scope Script
    }

    function Assert-Forwarder([UInt32]$fa) {end{($va -le $fa) -and ($fa -lt ($va + $sz))}}
    (0..($nf - 1)).ForEach{
      $fun[$bs + $_] = (
        Assert-Forwarder ($fa = [Marshal]::ReadInt32([IntPtr]($af + $_ * 4)))
      ) ? @{Address = ''; Forward = [Marshal]::PtrToStringAnsi([IntPtr]($tmp + $fa))}
        : @{ Address = [IntPtr]($tmp + $fa); Forward = '' }
    }
    (0..($nn - 1)).ForEach{
      [PSCustomObject]@{
        Ordinal = ($ord = $bs + [Marshal]::ReadInt16([IntPtr]($ao + $_ * 2)))
        Address = $fun[$ord].Address
        Name = [Marshal]::PtrToStringAnsi(
          [IntPtr]($tmp + [Marshal]::ReadInt32([IntPtr]($an + $_ * 4)))
        )
        ForwarderTo = $fun[$ord].Forward
      }
    }
  }
}

Воспользоваться полученной таблицей на практике можно "связав" адрес интересуемой нас функции с делегатом, например:

$EstablishConnection = {
  param(
    [IntPtr]$Address,
    [Type]$Prototype,
    [CallingConvention]$CallingConvention = 'StdCall'
  )

  end {
    $method = $Prototype.GetMethod('Invoke')
    $returntype, $paramtypes = $method.ReturnType, ($method.GetParameters().ParameterType ?? $null)
    $il, $to_i = ($holder = [DynamicMethod]::new('Invoke', $returntype, $paramtypes, $Prototype)
    ).GetILGenerator(), "ToInt$(($sz = [IntPtr]::Size) * 8)"
    if ($paramtypes) { (0..($paramtypes.Length - 1)).ForEach{$il.Emit([OpCodes]::ldarg, $_)} }
    $il.Emit([OpCodes]::"ldc_i$sz", $Address.$to_i())
    $il.EmitCalli([OpCodes]::calli, $CallingConvention, $returntype, $paramtypes)
    $il.Emit([OpCodes]::ret)
    $holder.CreateDelegate($Prototype)
  }
}

Таким образом, чтобы узнать (снова лишь для примера) PEB хоста pwsh:

$signature = (& $GetNtdllExports).Where{$_.Name -ceq 'NtQueryInformationProcess'}
Set-Variable -Name NtQueryInformationProcess -Value (
  $EstablishConnection.Invoke($signature.Address, [Func[IntPtr, UInt32, [Byte[]], UInt32, [Byte[]], Int32]])
) -Scope Script -Option Const

$buffer = [Byte[]]::new((($sz = [IntPtr]::Size) -eq 8 ? 0x30 : 0x18))
if ($NtQueryInformationProcess.Invoke([IntPtr]-1, 0, $buffer, $buffer.Length, $null) -ne 0) {
  throw [InvalidOperationException]::new()
}
"PEB: 0x{0:X$($sz * 2)}" -f [BitConverter]::"ToInt$($sz * 8)"(($sz -eq 8 ? $buffer[8..15] : $buffer[4..7]), 0)

Так как "связывание" реализуется посредством обобщённого делегата, на вызов функции, казалось бы, накладываются ограничения связанные с таким типом делегатов (отсутствие ref и out), поэтому пришлось пойти на хитрость и указать в качестве параметров буферы. А что делать с теми из системных функций, параметры которых не могут быть представлены буферами в виду их специфики (например, HANDLE требующий последующего освобождения)? Придётся задействовать рефлексию, конкретнее - метод MakeNewCustomDelegate типа DelegateHelpers. Подобного рода техники дают довольно внушительное пространство для манёвра. Но и на них свет клином не сошёлся, так как в виду либерализации некоторых краеугольных концепций платформы .NET (имеется в виду Core), можно прибегнуть к более изощрённым и эффективным в плане производительности методам вызова системных API. Но давайте-ка ближе к теме.

На данный момент pwsh пребывает в состоянии стагнации. Казалось бы: что-то правится, доводится до ума, но ничего принципиально нового не предлагается. Более того, никаких шагов не предпринимается в сторону безопасности (хотя этим грешат большинство интерпретаторов, в том числе многими любимый Python); экосистема pwsh архаична и не имеет интеграции с SCM (к примеру, тот же CSV, Git и т.д.) и отладчиками вроде gdb и WinDbg (последний, правда, хоть можно автоматизировать с помощью JavaScript), - словом, в нём нет многого из того, что по-настоящему необходимо и что в других сообществах реализуется весьма оперативно (тот же Python). Иначе говоря, pwsh пытается штурмовать амбициями, а не делом. А это заведомо проигрышная стратегия.

Комментарии (1)


  1. ApeCoder
    06.10.2021 18:42
    +1

    JFYI Для гит есть posh-git