What Is a Domain Generation Algorithm (DGA)?

If you’re running a botnet, you have a problem: your infected hosts need a way to phone home, but the moment defenders find your command and control (C2) domain, they blocklist it, sinkhole it, or hand it to a takedown request, and your whole fleet goes dark at once. A single hardcoded IP or domain baked into your malware is a single point of failure, and it’s the first thing an incident responder goes looking for when they pull apart a sample in a sandbox.

A Domain Generation Algorithm sidesteps this by not committing to any one domain at all. Instead, both the malware and the operator’s infrastructure independently compute a rotating list of candidate domain names using the same algorithm and the same inputs, typically some kind of seed value, the current date, and maybe a suffix or TLD list. The infected host walks down its generated list trying to resolve each domain until one of them actually answers, at which point it’s found its C2 server for that period. The operator only has to register a handful of the domains that will be generated on any given day, usually just one, and let the rest sit as noise.

This is the clever part: because both sides can compute the same list without ever exchanging information, a defender who blocks today’s active domain accomplishes almost nothing. Tomorrow the whole list rotates to a fresh, unregistered, unblocked batch of domains, and the operator just needs to register whichever ones they intend to use next. Conficker is the textbook example here, its later variants generated tens of thousands of candidate domains a day across multiple TLDs, which made it functionally impossible for defenders to preemptively register or block them all, even when security researchers had fully reverse engineered the algorithm.

At a mechanical level, a typical DGA workflow looks like this:

  1. Input parameters. A seed (often hardcoded into the binary, sometimes derived from something else like a Twitter trending topic or a stock price in more creative malware families), the current date (usually truncated to just the day, so the domain list changes daily rather than every second), and a suffix or TLD.
  2. Random-looking generation. The inputs get fed through a pseudo-random number generator or, more commonly in modern malware, a cryptographic hash function, to produce something that looks like noise but is fully reproducible from the same inputs.
  3. Domain construction. The hashed or generated output gets truncated and formatted into valid DNS labels, then glued onto a base domain and suffix to produce something that’s syntactically a real domain name, even though semantically it’s gibberish.

The reason step two usually leans on a hash function rather than a “true” random number generator is worth sitting with for a second, because it’s the entire point of the technique. A random number generator that isn’t seeded identically on both sides produces different output every time you run it, which is exactly what you don’t want. A hash function, on the other hand, is a pure function: the same input always produces the same output, on any machine, at any time, with no shared state required beyond the algorithm itself. That determinism is what lets a piece of malware sitting on a victim’s laptop and a command and control server sitting in a bulletproof hosting provider on the other side of the world arrive at the exact same list of thirty domain names without ever talking to each other first.

Implementation of a DGA in Go

Let’s look at a simplified DGA written in Go. It won’t be anywhere near as evasive as what you’d find in an actual malware family (real ones layer in things like XOR obfuscation of the seed, dictionary-based word combination to defeat entropy detectors, and per-campaign suffix rotation), but it captures the shape of the technique well enough to reason about.

Main Application

GO
package main

import (
	"dga-project/pkg/dga"
	"fmt"
	"os"
	"time"
)

func main() {
	if len(os.Args) < 4 {
		fmt.Println("Usage: go run ./cmd/dga/ <domain.com> <seed> <suffix>")
		os.Exit(1)
	}

	baseDomain := os.Args[1]
	seed := os.Args[2]
	suffix := os.Args[3]

	currentDate := time.Now()
	domains := dga.GenerateDomains(baseDomain, seed, suffix, currentDate, 20)

	for _, domain := range domains {
		fmt.Println("Generated Domain:", domain)
	}
}
Click to expand and view more

This is just wiring, nothing exotic. It reads three command-line arguments and hands them off to the actual generation logic along with the current timestamp and a fixed count of twenty domains. The interesting stuff happens in the dga package.

Domain Generation Logic (dga package)

GO
package dga

import (
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"math/rand"
	"time"
)

func GenerateDomains(baseDomain, seed, suffix string, date time.Time, count int) []string {
	var domains []string
	dateString := date.Format("20060102")
	rand.Seed(time.Now().UnixNano())

	for i := 0; i < count; i++ {
		randomComponent := rand.Intn(1000000)
		hashInput := fmt.Sprintf("%s%s%d%d", seed, dateString, i, randomComponent)
		hashedValue := sha256.Sum256([]byte(hashInput))
		hashString := hex.EncodeToString(hashedValue[:])

		prefix := hashString[0:6]
		middle := hashString[6:12]

		domainName := fmt.Sprintf("%s.%s.%s.%s", prefix, middle, suffix, baseDomain)
		domains = append(domains, domainName)
	}

	return domains
}
Click to expand and view more

Walking through this: for each of the twenty domains we want, we build a string out of the seed, the date, the loop index, and a random number, then run that whole string through SHA-256. SHA-256 gives us a 256-bit digest, which we hex-encode into a 64-character string, and then we slice out the first six characters as a “prefix” label and the next six as a “middle” label. Those get stitched together with the suffix and base domain into something like a3f9c1.7b2e04.suffix.baseDomain.com, which is a syntactically valid, if slightly weird-looking, fully qualified domain name.

Why hash at all instead of just generating random hex directly? Partly because it’s convenient (SHA-256 gives you a fixed-length, well-distributed output no matter what garbage you feed it as input), and partly because in real malware the hash also acts as a lightweight obfuscation layer: an analyst looking at the binary can see the seed and the algorithm, but can’t shortcut their way to “guessing” domains without actually running the hash function themselves. Six hex characters per label keeps every label comfortably under the DNS limit of 63 characters per label and 253 characters for the whole name, so there’s no risk of generating an invalid domain that some resolver rejects outright.

A Note on the Randomness in This Sample

Here’s a subtlety worth calling out explicitly, because it’s the kind of thing that separates a demo from something that would actually work as a DGA in the wild: this code calls rand.Seed(time.Now().UnixNano()) before generating anything. That means every time you run this program, even with identical seed, date, and suffix arguments, you’ll get a completely different set of twenty domains, because the randomComponent value baked into the hash input changes on every execution.

That’s fine for demonstrating the mechanics, but it quietly breaks the one property that makes a DGA useful to an attacker in the first place: reproducibility without communication. If the malware on a victim’s machine and the operator’s own domain-registration tooling can’t independently compute the same list from the same known inputs, then the “generation” part isn’t actually saving you from having to hardcode something, you’ve just moved the coordination problem from “hardcode a domain” to “hardcode a random seed that doesn’t actually determine anything.” A real DGA would drop the math/rand call entirely and derive everything purely from the seed, the date, and the loop index, so that anyone (attacker, malware, or a defender doing reverse engineering) who has those three inputs and the algorithm can compute the exact same domain list every time. That reproducibility cuts both ways, by the way, which is exactly why security researchers put so much effort into reverse engineering these algorithms: once you have it, you can precompute months of future domains and get ahead of the botnet instead of chasing it.

Automatic Domain Registration with Namecheap API

Generating a list of candidate domains is only half the problem. Somebody still has to actually register one of them before an infected host can resolve it and find a live C2 server. Doing this by hand, logging into a registrar’s dashboard and typing in a domain name, doesn’t scale if you’re running a campaign that rotates domains daily or even hourly, so operators lean on registrar APIs to automate the whole thing. The sample below uses Namecheap’s API as an example of what that plumbing looks like.

GO
package autoregister

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"net/http"
)

const (
	apiUrl   = "https://api.namecheap.com/v1/domains/create"
	apiUser  = "YOUR_API_USER"
	apiKey   = "YOUR_API_KEY"
	userName = "YOUR_USER_NAME"
	clientIp = "YOUR_CLIENT_IP"
)

type DomainRegistrationRequest struct {
	ApiUser    string `json:"ApiUser"`
	ApiKey     string `json:"ApiKey"`
	UserName   string `json:"UserName"`
	ClientIp   string `json:"ClientIp"`
	DomainName string `json:"DomainName"`
	Years      int    `json:"Years"`
	AddOns     string `json:"AddOns"`
}

type DomainRegistrationResponse struct {
	Command string `json:"Command"`
	Status  string `json:"Status"`
}

func RegisterDomain(domain string, years int) {
	request := DomainRegistrationRequest{
		ApiUser:    apiUser,
		ApiKey:     apiKey,
		UserName:   userName,
		ClientIp:   clientIp,
		DomainName: domain,
		Years:      years,
		AddOns:     "",
	}

	jsonData, err := json.Marshal(request)
	if err != nil {
		fmt.Println("Error marshalling request:", err)
		return
	}

	resp, err := http.Post(apiUrl, "application/json", bytes.NewBuffer(jsonData))
	if err != nil {
		fmt.Println("Error sending request:", err)
		return
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		fmt.Println("Error: Received status code", resp.StatusCode)
		return
	}

	body, _ := ioutil.ReadAll(resp.Body)

	var response DomainRegistrationResponse
	if err := json.Unmarshal(body, &response); err != nil {
		fmt.Println("Error unmarshalling response:", err)
		return
	}

	fmt.Printf("Domain Registration Status for %s: %s\n", domain, response.Status)
}
Click to expand and view more

There’s nothing mysterious happening here structurally: it’s a plain REST client. We build a DomainRegistrationRequest struct with the API credentials and the target domain, marshal it to JSON, POST it to Namecheap’s registration endpoint, and then unmarshal whatever comes back into a DomainRegistrationResponse so we can log the status. What’s interesting isn’t the HTTP plumbing, it’s what this code implies operationally.

Notice that the API credentials (apiUser, apiKey, userName, clientIp) are hardcoded constants. In a real malicious toolkit that would be a significant weakness, and not just from a “best practices” standpoint. If this registration binary or its credentials ever end up in the hands of a researcher, whether through a compromised C2 panel, a leaked build, or just careless operational security, those API keys let anyone impersonate the operator’s registrar account, register domains under their name, or worse, get the account’s registrations mapped and correlated across a whole campaign. This is actually a fairly common way researchers cluster together domains belonging to the same threat actor: registrar API abuse and WHOIS/registration metadata patterns leave fingerprints that are far more durable than any single domain name.

There’s also a practical constraint worth noting: Namecheap, like most registrars, requires the account to already hold a funded balance or a valid payment method on file before domains/create will succeed, and most registrars apply rate limits and fraud-detection heuristics on top of that. A script blasting out twenty registration requests in a tight loop, all from the same account, all resolving to infrastructure with a similar hosting fingerprint, is exactly the kind of pattern that trips a registrar’s own abuse detection long before any security researcher gets involved. This is part of why real-world DGA-driven campaigns tend to spread registrations across multiple registrars, multiple accounts, and sometimes multiple TLDs entirely (some ccTLDs have much laxer verification than gTLDs like .com), rather than hammering one API endpoint with a predictable naming pattern.

Integrating Domain Generation and Registration

Putting the two pieces together is straightforward: generate the day’s list of candidate domains, then loop through it and attempt to register each one.

GO
package main

import (
	"dga-project/pkg/autoregister"
	"dga-project/pkg/dga"
	"fmt"
	"os"
	"time"
)

func main() {
	if len(os.Args) < 4 {
		fmt.Println("Usage: go run ./cmd/dga/ <domain.com> <seed> <suffix>")
		os.Exit(1)
	}

	baseDomain := os.Args[1]
	seed := os.Args[2]
	suffix := os.Args[3]
	currentDate := time.Now()

	domains := dga.GenerateDomains(baseDomain, seed, suffix, currentDate, 20)
	for _, domain := range domains {
		fmt.Println("Generated Domain:", domain)
		autoregister.RegisterDomain(domain, 1)
	}
}
Click to expand and view more

Worth pointing out: this example registers every single generated domain, all twenty of them, in the loop. In practice that would be unusual and, frankly, wasteful for an operator. Remember the whole premise of a DGA is that the infected host and the operator can independently compute the same candidate list; the operator doesn’t need to register all twenty to have a working C2 channel, they just need to register the one or two they actually intend to use, then let the infected hosts burn through DNS lookups on the other eighteen or nineteen until they hit the one that resolves. Registering all of them costs real money (domain registration isn’t free, and at scale, across daily rotations, it adds up fast) and it also hands defenders a much bigger, more conspicuous footprint to correlate, since every domain registered under the same account or with similar WHOIS metadata is another data point tying the infrastructure together.

The realistic pattern looks more like: generate the full candidate list locally (or precompute it for the next several days), pick a small subset to actually register right before they’re needed, and let them expire or get abandoned once the campaign moves on to the next day’s batch. This is sometimes called a “single-domain” or “sparse” registration strategy, and it’s a direct response to defenders’ own DGA-hunting techniques, which we should talk about, because understanding the offense here really only makes sense in light of the defense.

How Defenders Fight Back

None of this is a one-sided game. DGA-based C2 has been common enough for long enough that defenders have built a whole toolkit specifically aimed at it, and understanding that toolkit is arguably more useful than the offensive code above, since it’s what tells you whether any of this would actually work against a modern network.

Detecting DGA traffic. The domains a DGA produces tend to look statistically different from domains a human registers for a real website. Human-chosen domains cluster around pronounceable syllables and dictionary words; hash-derived domains like the ones our sample generates are close to uniformly random over the hex alphabet, which shows up clearly in entropy measurements and n-gram language models trained on legitimate DNS traffic. Security vendors run exactly this kind of classifier over passive DNS logs, flagging query patterns that look statistically “off” even before anyone knows what the specific malware family is. On top of that, infected hosts running through a DGA’s candidate list generate a very distinctive traffic signature: a burst of NXDOMAIN (non-existent domain) responses as the host tries domain after domain that isn’t registered yet, followed by one successful resolution. A single host doing that occasionally isn’t remarkable, but a fleet of hosts across an organization all producing the same NXDOMAIN burst pattern on the same day is a strong signal that something is coordinating them.

Reverse engineering the algorithm. Because a DGA has to be deterministic to be useful to the attacker, that same determinism is a gift to anyone who gets their hands on a malware sample and pulls the algorithm out of it. Once a researcher has the seed, the date-formatting logic, and the hashing scheme, they can run the exact same computation the malware runs and get the exact same domain list, for today, and for every future day the algorithm will ever produce. That’s an enormously powerful capability: it lets defenders and researchers preregister or “sinkhole” upcoming domains before the botnet operator ever gets to them, effectively hijacking the C2 channel by winning the registration race. This is precisely what happened repeatedly during the Conficker takedown effort, where a coalition of registrars and security researchers raced to register or block Conficker’s daily domain lists faster than its operators could, and it’s a large part of why later Conficker variants moved to generating tens of thousands of domains a day across a wide spread of TLDs: it made the “register everything preemptively” defense too expensive to sustain, even for a large coalition.

This cat-and-mouse dynamic is really the whole story of DGA-based C2. Every improvement on the attacker’s side (bigger candidate lists, dictionary-based domains that defeat entropy scoring, per-campaign seed rotation, spreading registrations across registrars) exists as a direct response to something defenders got good at, and vice versa. Neither side gets to sit still.

Closing Thoughts

The code in this post is a toy, deliberately so, but it captures the two structural ideas that make DGA-driven C2 effective: determinism without communication, so both attacker and malware can independently arrive at the same rotating list of domains, and automated, sparse registration, so the operator only pays for and exposes the handful of domains they’re actually going to use at any given moment. The interesting engineering, on both the offense and defense side, lives in the details we glossed over here: how you keep a generated domain from looking statistically generated, how you keep registration patterns from correlating back to a single actor, and how, as a defender, you get from “here’s a weird DNS pattern” to “here’s the exact algorithm and here’s next week’s domain list” fast enough for it to matter.

Copyright Notice

Author: Kernelstub

Link: https://blog.kernelstub.dev/posts/domain-generation-algorithms-and-automatic-domain-registration-in-c2/

License: CC BY-NC-SA 4.0

This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License. Please attribute the source, use non-commercially, and maintain the same license.

© 2026 Prepakis Georgios | Kernelstub | Security Researcher. All rights reserved.

Start searching

Enter keywords to search articles

↑↓
ESC
⌘K Shortcut