Од­ним из нем­ногих минусов C# счи­тает­ся некото­рая слож­ность при вызове методов WinAPI. Мно­гие воз­можнос­ти уже переко­чева­ли в сбор­ки, но до сих пор при­ходит­ся час­то стал­кивать­ся с задачей вызова фун­кций Win32 нап­рямую. В таком слу­чае исполь­зуют­ся PInvoke, DInvoke и их про­изводные. В этой статье я покажу, как работа­ют эти методы, и мы научим­ся вызывать фун­кции WinAPI из управля­емо­го кода.

Раз­работ­чики инс­тру­мен­тов из катего­рии offensive пос­тепен­но отка­зыва­ются от зубод­робитель­ных «плю­сов» и герой­ско­го ассем­бле­ра и перехо­дят к более при­ятным, доб­рым, отзывчи­вым и миловид­ным язы­кам. Конеч­но, опро­мет­чиво называть C# лег­ким. В нем есть и слож­ные кон­цепции — поп­робуй челове­ку с ули­цы понят­ным язы­ком объ­яснить отли­чия IEnumerable от IEnumerator, IComparer от IComparable, рас­ска­зать о плю­сах TPL и PLINQ, рас­толко­вать реф­лексию, а уж про мар­шалинг или внут­реннее устрой­ство JIT-ком­пилято­ра я даже не заикаюсь (впро­чем, пос­ледний отно­сит­ся к плат­форме CLR).

При раз­работ­ке прог­рамм для Windows слож­но не исполь­зовать WinAPI. Конеч­но, мно­гие методы уже успешно пор­тирова­ны в отдель­ные сбор­ки .NET. Нап­ример, вмес­то GetUserName() дос­таточ­но обра­щать­ся вот к такой фун­кции:

System.Security.Principal.WindowsIdentity.GetCurrent().Name

Впро­чем, если ты захочешь коп­нуть чуть глуб­же, то при­дешь к выводу, что нам­ного луч­ше (и ста­биль­нее) будет обра­щать­ся нап­рямую к методу WinAPI, чем доверять свой код непонят­ным, дав­но не под­держи­ваемым опен­сор­сным сбор­кам от сим­патяги инду­са с GitHub.

Прос­то так WinAPI не выз­вать. И при­ходит­ся искать ухищ­рения, что­бы научить управля­емый код обра­щать­ся к натив­ным методам. Давай пос­мотрим, что это за методы.

 

Platform Invoke

Platform Invoke (он же Static Invoke) — единс­твен­ный «легаль­ный» вызов методов WinAPI из C#. Он отлично опи­сан в офи­циаль­ной докумен­тации Microsoft.

Ис­поль­зовать его прос­то — все завяза­но на дирек­тиве DllImport. Сна­чала в нашем коде на C# идет объ­явле­ние целевой фун­кции для вызова, ука­зание DLL, из которой эту фун­кцию брать, а пос­ле мож­но сме­ло обра­щать­ся к ней из управля­емо­го кода.

Нап­ример, так может выг­лядеть стан­дар­тный шелл‑код‑ран­нер через VirtualAlloc(), memcpy(), CreateThread() и WaitForSingleObject():

[DllImport("kernel32.dll")]
public static extern IntPtr VirtualAlloc(IntPtr lpAddress, int dwSize, uint flAllocationType, uint flProtect);
[DllImport("kernel32.dll")]
public static extern IntPtr CreateThread(IntPtr lpThreadAttributes, uint dwStackSize, IntPtr lpStartAddress, IntPtr lpParameter, uint dwCreationFlags, out uint lpThreadId);
[DllImport("kernel32.dll")]
public static extern UInt32 WaitForSingleObject(IntPtr hHandle, UInt32 dwMilliseconds);
public static void StartShellcode(byte[] shellcode)
{
uint threadId;
IntPtr alloc = VirtualAlloc(IntPtr.Zero, shellcode.Length, (uint)(AllocationType.Commit | AllocationType.Reserve), (uint)MemoryProtection.ExecuteReadWrite);
if (alloc == IntPtr.Zero) {
return;
}
Marshal.Copy(shellcode, 0, alloc, shellcode.Length);
IntPtr threadHandle = CreateThread(IntPtr.Zero, 0, alloc, IntPtr.Zero, 0, out threadId);
WaitForSingleObject(threadHandle, 0xFFFFFFFF);
}

В дан­ном слу­чае соз­дают­ся пол­ноцен­ные про­тоти­пы исполь­зуемых натив­ных фун­кций. Отдель­но обра­щу вни­мание на ука­зание целевой биб­лиоте­ки, из которой они вызыва­ются. Такой механизм очень похож на вызов этих же самых фун­кций из кода на C++.

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

Не сто­ит путать этот раз­дел со стан­дар­тным раз­делом импортов. Натив­ные фун­кции, объ­явленные через Platform Invoke, ока­зыва­ются в таб­лице ImplMap, в раз­деле с метадан­ными CLR-сбор­ки. Этот самый раз­дел авто­мати­чес­ки добав­ляет­ся в PE-файл.

Как выглядит таблица ImplMap
Как выг­лядит таб­лица ImplMap

К счастью, сиг­натуры (это стро­ка DllImport плюс про­тотип фун­кции) самос­тоятель­но писать не нуж­но. Дос­таточ­но заг­лянуть на один из сай­тов с готовы­ми вари­анта­ми:

  • pinvoke.net — самый популяр­ный вари­ант, который, увы, иног­да не откры­вает­ся;
  • pinvoke.dev — тьфу‑тьфу, пока работа­ет;
  • p-invoke.net — тоже вык­лючил­ся.

Как ты видишь, в наше суровое вре­мя полагать­ся толь­ко на веб‑стра­ницы может быть оши­боч­но, поэто­му качай ге­нера­тор P/Invoke-сиг­натур, написан­ный в Microsoft. Он уж никуда не про­падет... ско­рее все­го.

Что ж, любая прос­тота и абс­трак­ция при раз­работ­ке инс­тру­мен­тов ата­ки работа­ет как в плюс (упро­щает жизнь ред­тимерам), так и в минус (усложня­ет жизнь ред­тимерам). Ведь рас­парсить таб­лицу ImplMap и понять, какие методы WinAPI исполь­зуют­ся, не сос­тавит тру­да. Это мож­но сде­лать даже на PowerShell.

$assembly = "C:\File.exe"
$stream = [System.IO.File]::OpenRead($assembly)
$peReader = [System.Reflection.PortableExecutable.PEReader]::new($stream, [System.Reflection.PortableExecutable.PEStreamOptions]::LeaveOpen -bor [System.Reflection.PortableExecutable.PEStreamOptions]::PrefetchMetadata)
$metadataReader = [System.Reflection.Metadata.PEReaderExtensions]::GetMetadataReader($peReader)
$assemblyDefinition = $metadataReader.GetAssemblyDefinition()
foreach($typeHandler in $metadataReader.TypeDefinitions) {
$typeDef = $metadataReader.GetTypeDefinition($typeHandler)
foreach($methodHandler in $typeDef.GetMethods()) {
$methodDef = $metadataReader.GetMethodDefinition($methodHandler)
$import = $methodDef.GetImport()
if ($import.Module.IsNil) {
continue
}
$dllImportFuncName = $metadataReader.GetString($import.Name)
$dllImportParameters = $import.Attributes.ToString()
$dllImportPath = $metadataReader.GetString($metadataReader.GetModuleReference($import.Module).Name)
Write-Host "$dllImportPath, $dllImportParameters`n$dllImportFuncName`n"
}
}
Пример вывода
При­мер вывода

Им­порты мож­но гля­нуть и через более спе­циали­зиро­ван­ное ПО, такое как monodis или извес­тный мно­гим dnSpy.

По­это­му люди ста­ли при­думы­вать иные методы вызова неуп­равля­емо­го кода из C#, что­бы как минимум скрыть импорты.

 

Dynamic Invoke

Этот механизм исполь­зует делега­ты, что­бы получить дос­туп к методам из неуп­равля­емо­го кода. Делегат, если упростить, мож­но счи­тать ука­зате­лем на фун­кцию. В C# не как в C++ — здесь имя фун­кции не рав­но адре­су фун­кции. Что­бы, нап­ример, передать фун­кцию как кол­бэк, пот­ребу­ется исполь­зовать делегат. Ниже при­мер прос­тей­шего делега­та.

using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
namespace Template
{
public delegate int Operation(int x, int y);
class Program
{
static void Main()
{
var a = 2;
var b = 3;
Operation op = Add;
Console.WriteLine(op(a, b)); // Вызывается Add => 5
op = Multiply;
Console.WriteLine(op(a, b)); // Вызывается Multiply => 6
}
static int Add(int x, int y) => x + y;
static int Multiply(int x, int y) => x * y;
}
}

Как видишь, идет объ­явле­ние делега­та: воз­вра­щаемое зна­чение, при­нима­емые аргу­мен­ты, все‑все дан­ные. Затем мож­но ини­циали­зиро­вать этот делегат кон­крет­ным методом и, обра­тив­шись к делега­ту, выз­вать фун­кцию.

Продолжение доступно только участникам

Материалы из последних выпусков становятся доступны по отдельности только через два месяца после публикации. Чтобы продолжить чтение, необходимо стать участником сообщества «Xakep.ru».

Присоединяйся к сообществу «Xakep.ru»!

Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее

  • Подпишись на наc в Telegram!

    Только важные новости и лучшие статьи

    Подписаться

  • Подписаться
    Уведомить о
    1 Комментарий
    Старые
    Новые Популярные
    Межтекстовые Отзывы
    Посмотреть все комментарии