Сегодня ночью мне не давала покоя мысль возможно ли как-то заставить PowerShell исполнять ассемблерные инструкции или это нечто из области фантастики. Концепт был готов спустя двадцать минут.
Те, кто не первый год на «ты» с C#, знают о чем пойдет далее речь: все верно — представление ассемблерных инструкций в виде массива байтов. Единственное, что может показаться невероятным во всей это истории, как этот массив удалось преобразовать в функцию. Полагаю, кто уже догадался, будет ворчать, мол, снова обобщенные делегаты. А ведь когда я о них только упомянал, многие скептически отнеслись ко всему этому, ибо самую соль, а именно отсутсвие необходимости генерирования динамической сборки при использовании таковых делегатов, мало кто уловил.
Итак, концепт представляет собой получение вендора CPU.
Набор байтов для x86 архитектуры будет выглядеть так:
Для x64:
Далее нам нужно выделить участок памяти в виртуальном адресном пространстве и запихать в полученный указатель один из вышеобозначенных массивов.
Далее используем уже известную нам технику создания функции через обобщенный делегат.
Остается только прочитать вендора и освободить указатель.
В моем случае будет возвращена строка AuthenticAMD.
Пример предельно прост, чтобы разобраться что к чему. Если пофантазировать, можно вполне себе писать таким образом на 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)
Shaz
24.06.2016 12:59+1Выглядит конечно круто. А вот применение кроме как написание малвари не смогли придумать?)
ForNeVeR
24.06.2016 13:35Применение мне лично видится простое. Хочется что-то написать на ассемблере, а непосредственно программы-ассемблера под рукой нету. Зато PowerShell везде есть (да-да, у меня везде, и на Linux тоже). Это было бы весьма кстати для быстрого написания например, идентификации процессора или проверки каких-нибудь процессорных флагов. Ну и просто for fun, разумеется.
PahanMenski
27.06.2016 11:44Насколько я помню, можно вместо создания динамического метода просто использовать метод Marshal.GetDelegateForFunctionPointer. Когда-то делал такое в C#, и оно вполне себе работало.
ForNeVeR
Наваять поверх этого DSL с нормальными мнемониками команд — и можно, э, удобно программировать.