Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- Added DNS C2 listener (`empire/server/listeners/dns.py`) enabling full agent communication over DNS A/TXT records on UDP/53
- Added PowerShell DNS stager and comms templates (`dns/dns.ps1`, `dns/comms.ps1`) with chunked upload/download, JOB-based large payload delivery, and Ed25519/ChaCha20/AES crypto stack
- Added Python DNS launcher with native UDP socket implementation (no external dependencies)
- Added Go agent DNS support via `comms.MessageSender` interface, native DNS client (`dns.go`), and DH key exchange over DNS (`dh.go`)
- Added DNS listener documentation (`docs/listeners/dns.md`)
- Added DNS listener API and launcher generation tests
- Added a runtime `Background` option to C# modules, allowing operators to override background/foreground execution at task time

### Fixed
Expand Down
1 change: 1 addition & 0 deletions docs/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
* [OneDrive](listeners/onedrive.md)
* [HTTP](listeners/http.md)
* [Malleable C2](listeners/malleable-c2.md)
* [DNS](listeners/dns.md)
* [Stagers](stagers/README.md)
* [multi\_generate\_agent](stagers/multi_generate_agent.md)
* [Plugins](plugins/README.md)
Expand Down
1 change: 1 addition & 0 deletions docs/listeners/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Empire offers several listener types designed for different network conditions a

* **HTTP/HTTPS**: A standard HTTP listener for internet-facing operations supports both standard HTTP and encrypted HTTPS.
* **HTTP Malleable**: A customizable HTTP listener that allows beacons to match specific threat profiles.
* **DNS**: A listener that communicates entirely over DNS A/TXT records, ideal for restricted environments where only DNS egress is available.
* **SMB**: A peer-to-peer listener that works over SMB pipes (**currently only supports IronPython**).
* **HTTP Hop**: A listener that adds an intermediate hop or redirection server using PHP.
* **Port Forward**: Enables chaining agents through port forwarding.
Expand Down
98 changes: 98 additions & 0 deletions docs/listeners/dns.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# DNS

The DNS listener in Empire enables agent communication **entirely over DNS queries**, using **A** and **TXT** records. This is particularly useful in restricted environments where only DNS traffic is allowed to leave the network.

By default, the DNS listener runs on **port 53 (UDP)**. It implements a custom UDP DNS server that handles agent staging, tasking, and result collection without relying on any external DNS infrastructure.

## How It Works

The DNS listener uses a chunked protocol to transmit data within DNS query names:

- **Upload (agent → server)**: Data is split into 60-byte Base64url chunks sent as **A record** queries. The last chunk is sent as a **TXT** query to receive the server's response.
- **Download (server → agent)**: For small responses (≤200 bytes), the data is returned directly in a single **TXT** record. For larger payloads, the server returns a `JOB:<id>` reference, and the agent downloads the full response in chunks via sequential **TXT** queries.

### Query Format

| Direction | Format | Record Type |
| --------------------- | ------------------------------------- | ----------- |
| Upload (intermediate) | `r[TID]c[chunk]t[total].[base64].xyz` | A |
| Upload (last chunk) | `r[TID]c[chunk]t[total].[base64].xyz` | TXT |
| Download chunk | `s[JobID]c[index].xyz` | TXT |

Where `TID` is a random transaction ID, `chunk` is the chunk index, and `total` is the total number of chunks.

## Staging Process

The DNS listener follows the same multi-stage negotiation as the HTTP listener:

1. **STAGE0** – The launcher sends a routing packet to announce itself. The server generates the stager (stage 1) and buffers it for chunked download.
2. **STAGE1** – The agent downloads the stager via TXT chunks, executes it, then performs a Diffie-Hellman key exchange with Ed25519 certificate validation. A shared session key is derived.
3. **STAGE2** – The agent sends encrypted sysinfo. The server responds with the full agent code (`agent.ps1`), encrypted with the session key and delivered via JOB-based chunked download.

After staging, the agent uses **TASKING_REQUEST** (Meta 4) and **RESULT_POST** (Meta 5) for runtime communication.

## Key Configuration Options

### **Host**

The IP address or hostname that the agent will use as the DNS server for all queries. This must point to the Empire server (e.g., `127.0.0.1` for local testing, or a public IP / NS delegation in production).

### **BindIP**

The local IP address the DNS server binds to. Defaults to `0.0.0.0` (all interfaces).

### **Port**

The UDP port for the DNS server. Defaults to `53`.

### **Staging Key**

The staging key used to negotiate the session key between the agent and the server during the STAGE0–STAGE2 handshake.

### **Delay & Jitter**

- **DefaultDelay** – The interval (in seconds) at which the agent checks back with the server for new tasks.
- **DefaultJitter** – A randomness factor (between **0** and **1**) that modifies the delay to avoid detection through predictable timing patterns.

### **DefaultLostLimit**

The number of missed check-ins before the agent assumes it has lost communication and exits.

### **DefaultProfile**

The default communication profile for the agent, structured the same way as the HTTP listener profile.

### Optional Fields

- **KillDate** – The expiration date when the agent will automatically exit (MM/DD/YYYY). Leave empty for no expiration.
- **WorkingHours** – Defines when the agent will operate (e.g., `09:00-17:00`). Leave empty for 24/7 operation.

## Cryptographic Stack

The DNS listener uses the same cryptographic stack as the HTTP listener:

- **ChaCha20-Poly1305** – Used for routing packet encryption/authentication between the agent and server.
- **AES-256-CBC + HMAC-SHA256** – Used for encrypting the payload body (encrypt-then-MAC).
- **Ed25519** – Used for certificate-based identity validation of the server.
- **Diffie-Hellman** – Used during STAGE1 to derive a shared session key.

## Deployment Considerations

### Local Testing

For local testing, set `Host` to `127.0.0.1` and ensure nothing else is bound to port 53.

### Production Deployment

In a real engagement, the DNS listener requires that target machines send their DNS queries to the Empire server. This is typically achieved through:

- **NS delegation** – Register a domain and point the NS records to the Empire server's IP.
- **Direct specification** – If you control the target's DNS configuration, point it at the Empire server directly.

### Performance

DNS has inherent bandwidth limitations compared to HTTP. Each query carries ~60 bytes of payload data, and each TXT response carries ~200 bytes. Large payloads (like the full agent code at ~50KB) require hundreds of DNS round-trips. As a result:

- Staging takes longer than HTTP (~10–20 seconds depending on network latency).
- Task results are slower to return for large outputs.
- The listener is best suited for **low-bandwidth, high-stealth** scenarios where DNS is the only available egress channel.
1 change: 1 addition & 0 deletions empire/server/core/stager_generation_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -782,6 +782,7 @@ def generate_go_stageless(self, options, listener_name=None):
"DELAY": delay,
"JITTER": jitter,
"LOST_LIMIT": lost_limit,
"LISTENER_TYPE": active_listener.info["Name"].lower(),
"STAGING_KEY": base64.b64encode(staging_key.encode("UTF-8")).decode(
"UTF-8"
),
Expand Down
2 changes: 1 addition & 1 deletion empire/server/data/agent/gopire/agent/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ type MainAgent struct {
encryptionKey []byte
}

func NewMainAgent(packetHandler comms.PacketHandler, messagesender *comms.HttpMessageSender, sessionID, killDate, workingHours string, delay int, jitter float64, lostLimit int, aeskey []byte, defaultResponse string) *MainAgent {
func NewMainAgent(packetHandler comms.PacketHandler, messagesender MessageSender, sessionID, killDate, workingHours string, delay int, jitter float64, lostLimit int, aeskey []byte, defaultResponse string) *MainAgent {
packetHandler.SessionID = sessionID

return &MainAgent{
Expand Down
52 changes: 52 additions & 0 deletions empire/server/data/agent/gopire/comms/dh.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,3 +154,55 @@ func PerformDHKeyExchange(server string, sessionID string, stagingKey []byte, ag

return sessionKey, newSessionID, nonce, nil
}

func PerformDHKeyExchangeDns(server string, sessionID string, stagingKey []byte, agent_private_cert_key []byte, agent_public_cert_key []byte, server_public_cert_key []byte) ([]byte, string, []byte, error) {
privateKey, publicKey, err := GenerateDHKeyPair()
if err != nil {
return nil, "", nil, fmt.Errorf("error generating DH keys: %v", err)
}

nbytes := floorDiv((publicKey.BitLen() + 7), 8)
clientPubBytes := publicKey.FillBytes(make([]byte, nbytes))
agentCert := sign_with_hash([]byte("SIGNATURE"), agent_private_cert_key)
message := append(clientPubBytes, agentCert...)
packetHandler := PacketHandler{}
encData := common.AesEncryptThenHMAC(stagingKey, message)
routingPacket := packetHandler.BuildRoutingPacket(stagingKey, sessionID, 2, encData)

sender := NewDnsMessageSender(server)
responseData, err := sender.SendMessage(routingPacket)
if err != nil {
return nil, "", nil, fmt.Errorf("error sending DH via DNS: %v", err)
}

parsedPackets, err := packetHandler.ParseRoutingPacket(stagingKey, responseData)
if err != nil {
return nil, "", nil, fmt.Errorf("error parsing routing packet: %v", err)
}

var newSessionID string
for sid, packet := range parsedPackets {
newSessionID = sid
encData = packet[3].([]byte)
break
}

if len(encData) < 32 {
return nil, "", nil, fmt.Errorf("invalid server response length in DH exchange")
}

serverNonce := encData[:16]
serverPubKeyBytes := encData[16:]
serverPubKey := new(big.Int).SetBytes(serverPubKeyBytes)

if !CheckPublicKey(serverPubKey) {
return nil, "", nil, fmt.Errorf("invalid server public key")
}

sessionKey, err := ComputeDHSharedSecret(privateKey, serverPubKey)
if err != nil {
return nil, "", nil, fmt.Errorf("error computing shared secret: %v", err)
}

return sessionKey, newSessionID, serverNonce, nil
}
Loading