1

Тема: PowerShell: детектор Hyper-V

Нестандартный во всех смыслах подход к детектированию Hyper-V: адреса экспортируемых kernelbase.dll забираем вручную, оборачиваем их обобщёнными (generic) делегатами, с помощью шелл-кода извлекаем данные cpuid. Писалось с учётом синтаксиса pwsh v7.

using namespace System.Reflection.Emit
using namespace System.Runtime.InteropServices

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

    $j = switch ([BitConverter]::ToUInt16( # IMAGE_FILE_HEADER->Machine
      [BitConverter]::GetBytes([Marshal]::ReadInt16($mod, $jmp)), 0
    )) { # ToIntX, смещения полей VA и Size
      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([UInt32]$fa) {end{($va -le $fa) -and ($fa -lt ($va + $sz))}}
    (0..($nf - 1)).ForEach{
      $fun[$bs + $_] = (assert ($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)))
        )
        ForwardedTo = $fun[$ord].Forward
      }
    }
  }
}

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

  end {
    $method = $Prototype.GetMethod('Invoke') # $null в следующей инструкции отнюдь не блажь
    $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)
  }
}

$signatures, $kernelbase = @{
  VirtualAlloc = [Func[IntPtr, UIntPtr, UInt32, UInt32, IntPtr]]
  VirtualFree  = [Func[IntPtr, UIntPtr, UInt32, Boolean]]
}, @{}

(& $GetKernelbaseExports).Where{$_.Name -in $signatures.Keys}.ForEach{
  $kernelbase[$_.Name] = $AddressToDelegate.Invoke($_.Address, $signatures[$_.Name])
}

$bytes = [Byte[]](([IntPtr]::Size -eq 4 ? (
   0x55,                   # push  ebp
   0x8B, 0xEC,             # mov   ebp,  esp
   0x53,                   # push  ebx
   0x57,                   # push  edi
   0x8B, 0x45, 0x08,       # mov   eax,  dword ptr[ebp+8]
   0x0F, 0xA2,             # cpuid
   0x8B, 0x7D, 0x0C,       # mov   edi,  dword ptr[ebp+12]
   0x89, 0x07,             # mov   dword ptr[edi+0],  eax
   0x89, 0x5F, 0x04,       # mov   dword ptr[edi+4],  ebx
   0x89, 0x4F, 0x08,       # mov   dword ptr[edi+8],  ecx
   0x89, 0x57, 0x0C,       # mov   dword ptr[edi+12], edx
   0x5F,                   # pop   edi
   0x5B,                   # pop   ebx
   0x8B, 0xE5,             # mov   esp,  ebp
   0x5D,                   # pop   ebp
   0xC3                    # ret
) : (
   0x53,                   # push  rbx
   0x49, 0x89, 0xD0,       # mov   r8,  rdx
   0x89, 0xC8,             # mov   eax, ecx
   0x0F, 0xA2,             # cpuid
   0x41, 0x89, 0x40, 0x00, # mov   dword ptr[r8+0],  eax
   0x41, 0x89, 0x58, 0x04, # mov   dword ptr[r8+4],  ebx
   0x41, 0x89, 0x48, 0x08, # mov   dword ptr[r8+8],  ecx
   0x41, 0x89, 0x50, 0x0C, # mov   dword ptr[r8+12], edx
   0x5B,                   # pop   rbx
   0xC3                    # ret
)))

$BreakOnChunks = {
  param([Int32]$point, [Byte[]]$buf)

  end {
    $cpuid.Invoke($point, $buf)
    ($buf | Group-Object {[Math]::Floor($script:i++ / 4)}).ForEach{
      [BitConverter]::ToInt32($_.Group, 0)
    }
    $buf.Clear()
  }
}

try {
  if (($ptr = $kernelbase.VirtualAlloc.Invoke(
    [IntPtr]::Zero, [UIntPtr]::new($bytes.Length), 0x3000, 0x40
  )) -eq [IntPtr]::Zero) {
    throw [InvalidOperationException]::new('VirtualAlloc пал в грязь лицом.')
  }

  $cpuid = $AddressToDelegate.Invoke($ptr, ([Action[Int32, [Byte[]]]]))
  [Marshal]::Copy($bytes, 0, $ptr, $bytes.Length)

  $buf, $invalid, $correct = [Byte[]]::new(0x10), 0x13371337, 0x40000000
  $invalid = $BreakOnChunks.Invoke($invalid, $buf)
  $correct = $BreakOnChunks.Invoke($correct, $buf)
  (Compare-Object -ReferenceObject $invalid -DifferenceObject $correct
  ) -eq $null ? 'Hyper-V в отключке' : 'Hyper-V активен'
}
catch { Write-Warning $_ }
finally {
  if ($ptr) {
    if (!$kernelbase.VirtualFree.Invoke($ptr, [UIntPtr]::Zero, 0x8000)) {
      Write-Warning 'VirtualFree пал в грязь лицом.'
    }
  }
}