Add DNS staging with generated labels (closes the DNS Staging TODO) - #31
Open
warpedatom wants to merge 2 commits into
Open
Add DNS staging with generated labels (closes the DNS Staging TODO)#31warpedatom wants to merge 2 commits into
warpedatom wants to merge 2 commits into
Conversation
The dns-beacon block shipped commented out in Struct.go carrying Cobalt Strike's documentation example values (doc.bc., doc.1a., doc.tx., doc.md., doc-stg-sh. and the rest) under a note telling operators to add them manually. Those labels are among the most heavily signatured DNS C2 indicators there are, so following that instruction produces the most detectable configuration available. The block is now generated behind an opt-in -DNS flag, with all eight staging and request labels randomized per profile and the get_/put_ prefixes guaranteed distinct. dns_idle is exposed as -DNSIdle instead of being randomized, since it is the no-tasks sentinel and must not collide with a real answer. The numeric tuning values are unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
c2lint rejects a profile whose dns_max_txt is not divisible by four, and the commented-out block this feature replaced carried 199, so it now uses the documented default of 252. c2lint also warns when a dns-beacon prefix runs past eight characters, since every indicator character is data space lost from each query. Two things caused that: the base label could reach five characters, and dnsDistinctLabels widened the label length on a collision, which made every label after the first collision one character longer. The base is now capped at four and collisions are redrawn at the same length. Tests cover the divisibility rule, the length limit, and all seven prefixes sharing a length. None of this surfaced earlier because c2lint stops at the stage block errors from Tylous#32 before reaching dns-beacon. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Add DNS staging with generated labels (closes the "DNS Staging" TODO)
This implements the DNS Staging item from the README roadmap.
The
dns-beaconblock already existed inStruct.go, commented out, with anote reading
####Manaully add these if your doing C2 over DNS (Future Release)####. This wires it up so it can be generated rather than pasted in byhand.
Why this is worth generating rather than documenting
The commented block carries Cobalt Strike's documentation example values
verbatim:
doc.bc.,doc.tx.,doc.md.and the rest are among the most heavilysignatured DNS C2 indicators in existence. Any operator who follows the
instruction above and pastes the block in as-is ends up with the single most
detectable DNS configuration available, which is the opposite of what the rest
of the tool is for. Generating the labels solves that in the same way the HTTP
side already randomizes its URIs.
What it does
Adds
-DNS(defaultfalse). When it is off, generated profiles arefunctionally unchanged. The one difference is that the commented-out
placeholder block is no longer written into every profile, since the template
now carries a variable in its place, so a non-DNS profile comes out about 18
lines shorter.
When it is on, all eight label strings are generated per profile:
dns_stager_prepend,dns_stager_subhost,beacon,get_A,get_AAAA,get_TXT,put_metadataandput_output.The shape of the documentation values is preserved. They share a first label
and vary the second, so the generator produces one random base label plus seven
distinct two-character suffixes. Distinctness is enforced rather than assumed:
the
get_*andput_*prefixes are how the teamserver tells request typesapart, so a collision would break the channel rather than merely look wrong.
Sample output:
What I deliberately did not randomize
dns_idleis exposed as-DNSIdleinstead, defaulting to0.0.0.0(Cobalt Strike's own default). It is the "no tasks" sentinel, so it must not
collide with an address the operator's domain genuinely serves. That is an
operational decision, not a fingerprint. It is validated as IPv4.
The numeric tuning values (
dns_max_txt,dns_sleep,dns_ttl,maxdns)are left exactly as you wrote them. They affect throughput and reliability
rather than signature, so randomizing them would add risk for no detection
benefit.
ns_response "zero"is behavior rather than a signature, so it stays.Linted against Cobalt Strike 4.13
c2lintfrom a licensed Cobalt Strike 4.13 team server accepts the generateddns-beaconblock, with no errors and no warnings against any of itsdirectives, labels or values.
Getting there took two fixes worth recording, because both came from carrying
values over from the commented-out block unexamined:
dns_max_txtmust be divisible by four. The commented block carried199, which is not, and Cobalt Strike rejects the profile outright over it.This uses the documented default of
252.directive that does, since each indicator character is data space lost from
every query the beacon sends. The generated base label is capped so the full
prefix lands at seven or eight.
Both are now covered by tests, so a future change cannot quietly reintroduce
either.
One caveat on scope: this validates that the block compiles and that its values
are legal, not that a beacon stages over DNS end to end. That needs a live
listener rather than a linter.
Tests
Loader/DNS_test.gocovers: the block being omitted entirely when-DNSisoff, none of the documentation values surviving into output, all seven prefixes
being distinct within a profile, labels varying between profiles, every label
being a syntactically valid lowercase dotted sequence, and
dns_idledefaulting correctly while honouring an operator value.
Staging has to be enabled for the stager labels to be used
Worth stating plainly:
dns_stager_prependanddns_stager_subhostonly comeinto play when host staging is on, and SourcePoint defaults
-Stageto false.c2lintsays so directly:So with default flags the two staging labels are inert, while the rest of the
block (
beacon,get_A,get_AAAA,get_TXT,put_metadata,put_output)governs DNS beacon comms and applies either way. Operators who want DNS staging
need
-Stage trueas well. The same is true of the existing HTTPuri_x86anduri_x64stager URIs, so this is not new behaviour, just something that wasnot written down anywhere.
Note on overlap with #30
This branches from
mainand is independently mergeable in either order, butthe merge is not quite trivial if #30 goes in first, so it is worth flagging
what to watch for. I hit this merging both into a local branch.
SourcePoint.goconflicts in three places: theFlagOptionsstruct, the flagdefinitions, and the YAML overlay. The first two are simple unions. The third
needs care: this branch predates #30, so it still carries the old unconditional
opt.X = c.Xoverlay and thefmt.Println(c.TasksMaxSize)debug print that #30removes. Taking this branch's side of that hunk would silently revert several
of #30's config fixes and reinstate the debug print. The correct resolution is
to keep #30's
setString/setBooloverlay and routeDNSandDNSIdlethrough it, making
conf.DNSa*boolfor the same reason the other booleansbecame pointers there.
Happy to rebase this branch onto #30 and resolve it myself if you would rather
merge a clean diff.
Worth flagging separately:
go vet ./...currently fails onmainwithUtils/Utils.go:164:4: self-assignment of ii. That is pre-existing and notintroduced here, and it is one of the bugs #30 fixes (the
ii = iiline meant-Uri Ncould return fewer than N URIs). Mentioning it so a vet failure onthis branch is not mistaken for something this PR caused.
One small duplication if both land:
Loader/DNS.goseeds its ownmath/randsource, because onmainthe shared generators reseed fromtime.Now()on every call and can repeat within a clock tick. #30 fixes thatcentrally, so once it merges this file can drop its local source and use the
shared helper instead. Happy to do that as a follow-up.