Сегодня ночью мне не давала покоя мысль возможно ли как-то заставить PowerShell исполнять ассемблерные инструкции или это нечто из области фантастики. Концепт был готов спустя двадцать минут.

Те, кто не первый год на «ты» с C#, знают о чем пойдет далее речь: все верно — представление ассемблерных инструкций в виде массива байтов. Единственное, что может показаться невероятным во всей это истории, как этот массив удалось преобразовать в функцию. Полагаю, кто уже догадался, будет ворчать, мол, снова обобщенные делегаты. А ведь когда я о них только упомянал, многие скептически отнеслись ко всему этому, ибо самую соль, а именно отсутсвие необходимости генерирования динамической сборки при использовании таковых делегатов, мало кто уловил.

Итак, концепт представляет собой получение вендора CPU.

Набор байтов для x86 архитектуры будет выглядеть так:

[Byte[]]$x86 = @(
  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
)

Для x64:

[Byte[]]$x64 = @(
  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
)

Далее нам нужно выделить участок памяти в виртуальном адресном пространстве и запихать в полученный указатель один из вышеобозначенных массивов.

#в этой сборке можно разжиться VirtualAlloc и VirtualFree
Add-Type -AssemblyName System.ServiceModel
#извлекаем нужные нам функции и заносим их в одноименные переменные
([AppDomain]::CurrentDomain.GetAssemblies() |
Where-Object {
  $_.ManifestModule.ScopeName.Equals(
    'System.ServiceModel.dll'
  )
}).GetType(
  'System.ServiceModel.Channels.UnsafeNativeMethods'
).GetMethods([Reflection.BindingFlags]40) |
Where-Object {
  $_.Name -cmatch '\AVirtual(Alloc|Free)'
} | ForEach-Object { Set-Variable $_.Name $_ }
#какой блок нужно запихнуть
[Byte[]]$bytes = switch ([IntPtr]::Size) {
  4 { $x86 }
  8 { $x64 }
}

try {
  #выделяем память равной размеру массива байтов
  $ptr = $VirtualAlloc.Invoke($null, @(
    [IntPtr]::Zero, [UIntPtr](New-Object UIntPtr($bytes.Length)),
    [UInt32](0x1000 -bor 0x2000), [UInt32]0x40
  ))
  #запихиваем данные в указатель
  [Marshal]::Copy($bytes, 0, $ptr, $bytes.Length)
  ...

Далее используем уже известную нам технику создания функции через обобщенный делегат.

  ...
  $proto  = [Action[Int32, [Byte[]]]]
  $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([OpCodes]::Ldarg, $_)
  }
  
  switch ([IntPtr]::Size) {
    4 { $il.Emit([OpCodes]::Ldc_I4, $ptr.ToInt32()) }
    8 { $il.Emit([OpCodes]::Ldc_I8, $ptr.ToInt64()) }
  }
  $il.EmitCalli(
    [OpCodes]::Calli, [CallingConvention]::Cdecl, $returntype, $paramtypes
  )
  $il.Emit([OpCodes]::Ret)
  
  $cpuid = $holder.CreateDelegate($proto)
  ...

Остается только прочитать вендора и освободить указатель.

  ...
  [Byte[]]$buf = New-Object Byte[] 16
  $gch = [GCHandle]::Alloc($buf, 'Pinned')
  $cpuid.Invoke(0, $buf)
  $gch.Free()
  
  "$(-join [Char[]]$buf[4..7])$(
        -join [Char[]]$buf[12..15]
  )$(-join [Char[]]$buf[8..11])"
}
catch { $_.Exception }
finally {
  if ($ptr) {
    [void]$VirtualFree.Invoke($null, @($ptr, [UIntPtr]::Zero, [UInt32]0x8000))
  }
}

В моем случае будет возвращена строка AuthenticAMD.

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

Код полностью
function Get-CPUVendor {
  begin {
    @(
      [Runtime.InteropServices.CallingConvention],
      [Runtime.InteropServices.GCHandle],
      [Runtime.InteropServices.Marshal],
      [Reflection.Emit.OpCodes]
    ) | ForEach-Object {
      $keys = ($ta = [PSObject].Assembly.GetType(
        'System.Management.Automation.TypeAccelerators'
      ))::Get.Keys
      $collect = @()
    }{
      if ($keys -notcontains $_.Name) {
        $ta::Add($_.Name, $_)
        $collect += $_.Name
      }
    }
    
    Add-Type -AssemblyName System.ServiceModel
    
    ([AppDomain]::CurrentDomain.GetAssemblies() |
    Where-Object {
      $_.ManifestModule.ScopeName.Equals(
        'System.ServiceModel.dll'
      )
    }).GetType(
      'System.ServiceModel.Channels.UnsafeNativeMethods'
    ).GetMethods([Reflection.BindingFlags]40) |
    Where-Object {
      $_.Name -cmatch '\AVirtual(Alloc|Free)'
    } | ForEach-Object { Set-Variable $_.Name $_ }
    
    [Byte[]]$x86 = @(
      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
    )
    
    [Byte[]]$x64 = @(
      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
    )
    
    [Byte[]]$bytes = switch ([IntPtr]::Size) {
      4 { $x86 }
      8 { $x64 }
    }
  }
  process {
    try {
      $ptr = $VirtualAlloc.Invoke($null, @(
        [IntPtr]::Zero, [UIntPtr](New-Object UIntPtr($bytes.Length)),
        [UInt32](0x1000 -bor 0x2000), [UInt32]0x40
      ))
      
      [Marshal]::Copy($bytes, 0, $ptr, $bytes.Length)
      
      $proto  = [Action[Int32, [Byte[]]]]
      $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([OpCodes]::Ldarg, $_)
      }
      
      switch ([IntPtr]::Size) {
        4 { $il.Emit([OpCodes]::Ldc_I4, $ptr.ToInt32()) }
        8 { $il.Emit([OpCodes]::Ldc_I8, $ptr.ToInt64()) }
      }
      $il.EmitCalli(
        [OpCodes]::Calli, [CallingConvention]::Cdecl, $returntype, $paramtypes
      )
      $il.Emit([OpCodes]::Ret)
      
      $cpuid = $holder.CreateDelegate($proto)
      
      [Byte[]]$buf = New-Object Byte[] 16
      $gch = [GCHandle]::Alloc($buf, 'Pinned')
      $cpuid.Invoke(0, $buf)
      $gch.Free()
      
      "$(-join [Char[]]$buf[4..7])$(
            -join [Char[]]$buf[12..15]
      )$(-join [Char[]]$buf[8..11])"
    }
    catch { $_.Exception }
    finally {
      if ($ptr) {
        [void]$VirtualFree.Invoke($null, @($ptr, [UIntPtr]::Zero, [UInt32]0x8000))
      }
    }
  }
  end {
    $collect | ForEach-Object { [void]$ta::Remove($_) }
  }
}

Поделиться с друзьями
-->

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


  1. ForNeVeR
    24.06.2016 12:29
    +1

    Наваять поверх этого DSL с нормальными мнемониками команд — и можно, э, удобно программировать.


  1. Shaz
    24.06.2016 12:59
    +1

    Выглядит конечно круто. А вот применение кроме как написание малвари не смогли придумать?)


    1. ForNeVeR
      24.06.2016 13:35

      Применение мне лично видится простое. Хочется что-то написать на ассемблере, а непосредственно программы-ассемблера под рукой нету. Зато PowerShell везде есть (да-да, у меня везде, и на Linux тоже). Это было бы весьма кстати для быстрого написания например, идентификации процессора или проверки каких-нибудь процессорных флагов. Ну и просто for fun, разумеется.


  1. dmitryredkin
    24.06.2016 16:14
    +2

    Вот только не надо путать ассемблер и машинный код!


    1. dmitryredkin
      24.06.2016 16:22
      +1

      А то зашел увидеть компилятор ассеблера, а тут делегаты…


  1. PahanMenski
    27.06.2016 11:44

    Насколько я помню, можно вместо создания динамического метода просто использовать метод Marshal.GetDelegateForFunctionPointer. Когда-то делал такое в C#, и оно вполне себе работало.