diff --git a/Loader/DNS.go b/Loader/DNS.go new file mode 100644 index 0000000..bf06580 --- /dev/null +++ b/Loader/DNS.go @@ -0,0 +1,151 @@ +package Loader + +import ( + crand "crypto/rand" + "encoding/binary" + "log" + "math/rand" + "net" + "strings" + "time" +) + +const ( + dnsLabelFirstChars = "abcdefghijklmnopqrstuvwxyz" + dnsLabelChars = "abcdefghijklmnopqrstuvwxyz0123456789" + + // dnsPadWidth aligns the generated directives the way the block was + // originally written out by hand. + dnsPadWidth = 21 +) + +// dnsRNG is seeded once from crypto/rand so that labels do not repeat between +// profiles generated in quick succession. +var dnsRNG = rand.New(rand.NewSource(dnsSeed())) + +func dnsSeed() int64 { + var b [8]byte + if _, err := crand.Read(b[:]); err != nil { + return time.Now().UnixNano() + } + return int64(binary.LittleEndian.Uint64(b[:])) +} + +// dnsLabel returns a lowercase DNS label of length n. The first character is +// always a letter: RFC 1123 permits a leading digit, but a leading letter is +// handled more predictably across resolvers. +func dnsLabel(n int) string { + if n < 1 { + n = 1 + } + b := make([]byte, n) + b[0] = dnsLabelFirstChars[dnsRNG.Intn(len(dnsLabelFirstChars))] + for i := 1; i < n; i++ { + b[i] = dnsLabelChars[dnsRNG.Intn(len(dnsLabelChars))] + } + return string(b) +} + +// dnsDistinctLabels returns count distinct labels, every one of length n. +// +// The get_*/put_* prefixes are how the teamserver tells one request type from +// another, so a collision between any two would break the channel rather than +// merely look wrong. Colliding candidates are redrawn at the same length: an +// earlier version widened n on a collision, which quietly made every label +// after the first collision one character longer and pushed the resulting +// prefix past the eight character maximum c2lint recommends. +// +// There are 26 * 36^(n-1) possible labels, which for the values used here +// exceeds count by orders of magnitude, so the redraw terminates. +func dnsDistinctLabels(count, n int) []string { + out := make([]string, 0, count) + seen := make(map[string]bool, count) + for len(out) < count { + label := dnsLabel(n) + if seen[label] { + continue + } + seen[label] = true + out = append(out, label) + } + return out +} + +func dnsPad(key string) string { + if len(key) >= dnsPadWidth { + return " " + } + return strings.Repeat(" ", dnsPadWidth-len(key)) +} + +// GenerateDNSBeacon renders the dns-beacon block for the generated profile, or +// an empty string when DNS was not requested. +// +// The block previously 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 exact labels are the most heavily signatured DNS C2 +// indicators there are, so pasting them in unchanged is the worst possible +// starting point. Every label is now generated per profile. +// +// The numeric tuning values are deliberately left as they were written. +// dns_max_txt, dns_sleep, dns_ttl and maxdns affect throughput and reliability +// rather than signature, and choosing them is an operator decision. +func GenerateDNSBeacon(enabled bool, dnsIdle string) string { + if !enabled { + return "" + } + if dnsIdle == "" { + // Cobalt Strike's own default. dns_idle is the "no tasks" sentinel, so + // it must not collide with an address the operator's domain genuinely + // serves, which makes it an operator choice rather than something to + // randomize. + dnsIdle = "0.0.0.0" + } + if ip := net.ParseIP(dnsIdle); ip == nil || ip.To4() == nil { + log.Fatalf("Error: -DNSIdle must be an IPv4 address, got %q", dnsIdle) + } + + // The documentation values share a first label and vary the second + // ("doc.bc.", "doc.tx."). Keeping that shape keeps the queries short. + // + // The base is capped at 4 characters so the full prefix lands at 7 or 8, + // within the 8 character maximum c2lint recommends. Every indicator + // character is data space lost from each query, so overshooting costs DNS + // throughput on every request the beacon makes. + base := dnsLabel(3 + dnsRNG.Intn(2)) + labels := dnsDistinctLabels(7, 2) + + directives := []struct{ key, value string }{ + {"dns_idle", dnsIdle}, + // Cobalt Strike requires dns_max_txt to be divisible by four and + // rejects the profile otherwise. The commented-out block carried 199, + // which is not, so this uses the documented default of 252. + {"dns_max_txt", "252"}, + {"dns_sleep", "1"}, + {"dns_ttl", "5"}, + {"maxdns", "200"}, + {"dns_stager_prepend", dnsLabel(6 + dnsRNG.Intn(5))}, + {"dns_stager_subhost", base + "." + labels[6] + "."}, + {"", ""}, + {"beacon", base + "." + labels[0] + "."}, + {"get_A", base + "." + labels[1] + "."}, + {"get_AAAA", base + "." + labels[2] + "."}, + {"get_TXT", base + "." + labels[3] + "."}, + {"put_metadata", base + "." + labels[4] + "."}, + {"put_output", base + "." + labels[5] + "."}, + {"ns_response", "zero"}, + } + + var b strings.Builder + b.WriteString("\ndns-beacon {\n") + for _, d := range directives { + if d.key == "" { + b.WriteString("\n") + continue + } + b.WriteString(" set " + d.key + dnsPad(d.key) + "\"" + d.value + "\";\n") + } + b.WriteString("}\n") + return b.String() +} diff --git a/Loader/DNS_test.go b/Loader/DNS_test.go new file mode 100644 index 0000000..86eb712 --- /dev/null +++ b/Loader/DNS_test.go @@ -0,0 +1,153 @@ +package Loader + +import ( + "regexp" + "strconv" + "strings" + "testing" +) + +var dnsDirective = regexp.MustCompile(`set\s+(\S+)\s+"([^"]*)";`) + +func dnsValues(t *testing.T, block string) map[string]string { + t.Helper() + out := make(map[string]string) + for _, m := range dnsDirective.FindAllStringSubmatch(block, -1) { + out[m[1]] = m[2] + } + return out +} + +// DNS is opt-in, so a profile generated without it must be unchanged. +func TestDNSBeaconOmittedWhenDisabled(t *testing.T) { + if got := GenerateDNSBeacon(false, ""); got != "" { + t.Errorf("expected no dns-beacon block when DNS is off, got %q", got) + } +} + +// The block shipped carrying Cobalt Strike's documentation example values, +// which are the most heavily signatured DNS C2 labels in existence. None of +// them may survive into a generated profile. +func TestDNSBeaconDropsDocumentationValues(t *testing.T) { + block := GenerateDNSBeacon(true, "") + docValues := []string{ + "doc.bc.", "doc.1a.", "doc.4a.", "doc.tx.", "doc.md.", "doc.po.", + "doc-stg-prepend", "doc-stg-sh.", + } + for _, v := range docValues { + if strings.Contains(block, v) { + t.Errorf("block still carries the documentation value %q", v) + } + } +} + +// The get_*/put_* prefixes are how the teamserver distinguishes request types. +// Two sharing a value would break the channel, not just look wrong. +// All seven prefixes have to share a length. Redrawing a collision at a wider +// length made every label after the first collision one character longer. +func TestDNSBeaconLabelsShareALength(t *testing.T) { + keys := []string{ + "beacon", "get_A", "get_AAAA", "get_TXT", + "put_metadata", "put_output", "dns_stager_subhost", + } + for i := 0; i < 200; i++ { + v := dnsValues(t, GenerateDNSBeacon(true, "")) + want := len(v["beacon"]) + for _, k := range keys { + if len(v[k]) != want { + t.Fatalf("%s = %q is %d characters, but beacon is %d", k, v[k], len(v[k]), want) + } + } + } +} + +func TestDNSBeaconLabelsAreDistinct(t *testing.T) { + keys := []string{ + "beacon", "get_A", "get_AAAA", "get_TXT", + "put_metadata", "put_output", "dns_stager_subhost", + } + for i := 0; i < 50; i++ { + v := dnsValues(t, GenerateDNSBeacon(true, "")) + owner := make(map[string]string, len(keys)) + for _, k := range keys { + got := v[k] + if got == "" { + t.Fatalf("%s was not set", k) + } + if prev, dup := owner[got]; dup { + t.Fatalf("%s and %s share the prefix %q", prev, k, got) + } + owner[got] = k + } + } +} + +func TestDNSBeaconVariesBetweenProfiles(t *testing.T) { + seen := make(map[string]bool) + for i := 0; i < 25; i++ { + v := dnsValues(t, GenerateDNSBeacon(true, "")) + if seen[v["beacon"]] { + t.Fatalf("beacon prefix repeated across profiles: %q", v["beacon"]) + } + seen[v["beacon"]] = true + } +} + +// dns_idle is the "no tasks" sentinel, so it stays an operator decision rather +// than something the generator randomizes. +func TestDNSIdleDefaultsAndHonoursOperatorValue(t *testing.T) { + if got := dnsValues(t, GenerateDNSBeacon(true, ""))["dns_idle"]; got != "0.0.0.0" { + t.Errorf("dns_idle default = %q, want 0.0.0.0", got) + } + if got := dnsValues(t, GenerateDNSBeacon(true, "8.8.4.4"))["dns_idle"]; got != "8.8.4.4" { + t.Errorf("dns_idle = %q, want 8.8.4.4", got) + } +} + +// Cobalt Strike rejects the profile outright unless dns_max_txt is divisible +// by four. The commented-out block this feature replaced carried 199, which is +// not, so the value has to be checked rather than inherited. +func TestDNSMaxTXTIsDivisibleByFour(t *testing.T) { + got := dnsValues(t, GenerateDNSBeacon(true, ""))["dns_max_txt"] + n, err := strconv.Atoi(got) + if err != nil { + t.Fatalf("dns_max_txt = %q, which is not a number", got) + } + if n%4 != 0 { + t.Errorf("dns_max_txt = %d, which Cobalt Strike rejects because it is not divisible by four", n) + } +} + +// c2lint warns when a prefix runs past eight characters, because every +// indicator character is data space lost from each query. +func TestDNSLabelsStayWithinTheRecommendedLength(t *testing.T) { + keys := []string{ + "beacon", "get_A", "get_AAAA", "get_TXT", + "put_metadata", "put_output", "dns_stager_subhost", + } + for i := 0; i < 100; i++ { + v := dnsValues(t, GenerateDNSBeacon(true, "")) + for _, k := range keys { + if len(v[k]) > 8 { + t.Fatalf("%s = %q is %d characters, over the 8 character maximum", k, v[k], len(v[k])) + } + } + } +} + +// Every label must be a syntactically valid lowercase DNS label sequence. +func TestDNSBeaconLabelsAreValid(t *testing.T) { + valid := regexp.MustCompile(`^([a-z][a-z0-9]*\.)+$`) + keys := []string{ + "beacon", "get_A", "get_AAAA", "get_TXT", + "put_metadata", "put_output", "dns_stager_subhost", + } + for i := 0; i < 50; i++ { + v := dnsValues(t, GenerateDNSBeacon(true, "")) + for _, k := range keys { + if !valid.MatchString(v[k]) { + t.Errorf("%s = %q is not a valid dotted label sequence", k, v[k]) + } + } + } +} diff --git a/Loader/Loader.go b/Loader/Loader.go index 7c1579a..5b68f05 100644 --- a/Loader/Loader.go +++ b/Loader/Loader.go @@ -75,7 +75,7 @@ type Beacon_SSL struct { var num_Profile int var Post bool -func GenerateOptions(stage, sleeptime, jitter, useragent, uri, customuri, customuriGET, customuriPOST, beacon_PE, processinject_min_alloc, Post_EX_Process_Name, metadata, injector, Host, Profile, ProfilePath, outFile, custom_cert, cert_password, CDN, CDN_Value, datajitter, Keylogger string, Forwarder bool, tasks_max_size string, tasks_proxy_max_size string, tasks_dns_proxy_max_size string, syscall_method string, httplib string, ThreadSpoof bool, beacongate string, eaf_bypass bool, rdll_use_syscalls bool, copy_pe_header bool, rdll_loader string, transform_obfuscate string, smartinject bool, sleep_mask bool) { +func GenerateOptions(stage, sleeptime, jitter, useragent, uri, customuri, customuriGET, customuriPOST, beacon_PE, processinject_min_alloc, Post_EX_Process_Name, metadata, injector, Host, Profile, ProfilePath, outFile, custom_cert, cert_password, CDN, CDN_Value, datajitter, Keylogger string, Forwarder bool, tasks_max_size string, tasks_proxy_max_size string, tasks_dns_proxy_max_size string, syscall_method string, httplib string, ThreadSpoof bool, beacongate string, eaf_bypass bool, rdll_use_syscalls bool, copy_pe_header bool, rdll_loader string, transform_obfuscate string, smartinject bool, sleep_mask bool, dns bool, dns_idle string) { Beacon_Com := &Beacon_Com{} Beacon_Stage_p1 := &Beacon_Stage_p1{} Beacon_Stage_p2 := &Beacon_Stage_p2{} @@ -89,6 +89,7 @@ func GenerateOptions(stage, sleeptime, jitter, useragent, uri, customuri, custom fmt.Println("[*] Preparing Varibles...") HostStageMessage, Beacon_Com.Variables = GenerateComunication(stage, sleeptime, jitter, useragent, datajitter, tasks_max_size, tasks_proxy_max_size, tasks_dns_proxy_max_size, httplib) + Beacon_Com.Variables["dns_beacon"] = GenerateDNSBeacon(dns, dns_idle) Beacon_PostEX.Variables = GeneratePostProcessName(Post_EX_Process_Name, Keylogger, ThreadSpoof) Beacon_GETPOST.Variables = GenerateHTTPVaribles(Host, metadata, uri, customuri, customuriGET, customuriPOST, CDN, CDN_Value, Profile, Forwarder) Beacon_Stage_p1.Variables, Beacon_Stage_p2.Variables, syscall_method = GeneratePE(beacon_PE, syscall_method, beacongate, eaf_bypass, rdll_use_syscalls, copy_pe_header, rdll_loader, transform_obfuscate, smartinject, sleep_mask) @@ -112,6 +113,9 @@ func GenerateOptions(stage, sleeptime, jitter, useragent, uri, customuri, custom } else { fmt.Println("[!] " + syscall_method + " syscall method selected") } + if dns { + fmt.Println("[!] DNS Beacon Block Added With Randomized Labels") + } Name, _ := strconv.Atoi(Profile) fmt.Println("[*] Seleted Profile: " + Struct.Profile_Names[Name]) fmt.Println("[+] Profile Generated: " + outFile) diff --git a/Sample.yaml b/Sample.yaml index 8ce7fc3..082648e 100644 --- a/Sample.yaml +++ b/Sample.yaml @@ -38,4 +38,6 @@ TransformObfuscate: "lznt1,xor \"32\"" SmartInject: False BeaconGate: "All" SleepMask: False +DNS: False +DNSIdle: "0.0.0.0" diff --git a/SourcePoint.go b/SourcePoint.go index 591407a..3b0769c 100644 --- a/SourcePoint.go +++ b/SourcePoint.go @@ -51,6 +51,8 @@ type FlagOptions struct { transform_obfuscate string smartinject bool sleep_mask bool + dns bool + dns_idle string } type conf struct { @@ -93,6 +95,8 @@ type conf struct { TransformObfuscate string `yaml:"TransformObfuscate"` SmartInject bool `yaml:"SmartInject"` SleepMask bool `yaml:"SleepMask"` + DNS bool `yaml:"DNS"` + DNSIdle string `yaml:"DNSIdle"` } func (c *conf) getConf(yamlfile string) *conf { @@ -227,8 +231,10 @@ func options() *FlagOptions { Example: "lznt1,rc4 \"64\",xor \"32\",base64"`) smartinject := flag.Bool("SmartInject", false, "Enable Smart Inject") sleep_mask := flag.Bool("SleepMask", true, "Enable Sleep Mask") + dns := flag.Bool("DNS", false, "Add a dns-beacon block for C2 over DNS, with all staging and request labels randomly generated") + dns_idle := flag.String("DNSIdle", "0.0.0.0", "IPv4 address returned when the beacon has no tasks (only used with -DNS)") flag.Parse() - return &FlagOptions{stage: *stage, sleeptime: *sleeptime, jitter: *jitter, useragent: *useragent, uri: *uri, customuri: *customuri, customuriGET: *customuriGET, customuriPOST: *customuriPOST, beacon_PE: *beacon_PE, processinject_min_alloc: *processinject_min_alloc, Post_EX_Process_Name: *Post_EX_Process_Name, metadata: *metadata, injector: *injector, Host: *Host, Profile: *Profile, ProfilePath: *ProfilePath, outFile: *outFile, custom_cert: *custom_cert, cert_password: *cert_password, CDN: *CDN, CDN_Value: *CDN_Value, Yaml: *Yaml, Datajitter: *Datajitter, Keylogger: *Keylogger, Forwarder: *Forwarder, tasks_max_size: *tasks_max_size, tasks_proxy_max_size: *tasks_proxy_max_size, tasks_dns_proxy_max_size: *tasks_dns_proxy_max_size, syscall_method: *syscall_method, httplib: *httplib, threadspoof: *threadspoof, beacongate: *beacongate, eaf_bypass: *eaf_bypass, rdll_use_syscalls: *rdll_use_syscalls, copy_pe_header: *copy_pe_header, rdll_loader: *rdll_loader, transform_obfuscate: *transform_obfuscate, smartinject: *smartinject, sleep_mask: *sleep_mask} + return &FlagOptions{stage: *stage, sleeptime: *sleeptime, jitter: *jitter, useragent: *useragent, uri: *uri, customuri: *customuri, customuriGET: *customuriGET, customuriPOST: *customuriPOST, beacon_PE: *beacon_PE, processinject_min_alloc: *processinject_min_alloc, Post_EX_Process_Name: *Post_EX_Process_Name, metadata: *metadata, injector: *injector, Host: *Host, Profile: *Profile, ProfilePath: *ProfilePath, outFile: *outFile, custom_cert: *custom_cert, cert_password: *cert_password, CDN: *CDN, CDN_Value: *CDN_Value, Yaml: *Yaml, Datajitter: *Datajitter, Keylogger: *Keylogger, Forwarder: *Forwarder, tasks_max_size: *tasks_max_size, tasks_proxy_max_size: *tasks_proxy_max_size, tasks_dns_proxy_max_size: *tasks_dns_proxy_max_size, syscall_method: *syscall_method, httplib: *httplib, threadspoof: *threadspoof, beacongate: *beacongate, eaf_bypass: *eaf_bypass, rdll_use_syscalls: *rdll_use_syscalls, copy_pe_header: *copy_pe_header, rdll_loader: *rdll_loader, transform_obfuscate: *transform_obfuscate, smartinject: *smartinject, sleep_mask: *sleep_mask, dns: *dns, dns_idle: *dns_idle} } @@ -283,6 +289,10 @@ func main() { opt.transform_obfuscate = c.TransformObfuscate opt.smartinject = c.SmartInject opt.sleep_mask = c.SleepMask + opt.dns = c.DNS + if c.DNSIdle != "" { + opt.dns_idle = c.DNSIdle + } } if opt.outFile == "" { @@ -298,5 +308,5 @@ func main() { log.Fatal("Error: When using CustomuriGET/CustomuriPOST, both must be sepecified") } fmt.Println(c.TasksMaxSize) - Loader.GenerateOptions(opt.stage, opt.sleeptime, opt.jitter, opt.useragent, opt.uri, opt.customuri, opt.customuriGET, opt.customuriPOST, opt.beacon_PE, opt.processinject_min_alloc, opt.Post_EX_Process_Name, opt.metadata, opt.injector, opt.Host, opt.Profile, opt.ProfilePath, opt.outFile, opt.custom_cert, opt.cert_password, opt.CDN, opt.CDN_Value, opt.Datajitter, opt.Keylogger, opt.Forwarder, opt.tasks_max_size, opt.tasks_proxy_max_size, opt.tasks_dns_proxy_max_size, opt.syscall_method, opt.httplib, opt.threadspoof, opt.beacongate, opt.eaf_bypass, opt.rdll_use_syscalls, opt.copy_pe_header, opt.rdll_loader, opt.transform_obfuscate, opt.smartinject, opt.sleep_mask) + Loader.GenerateOptions(opt.stage, opt.sleeptime, opt.jitter, opt.useragent, opt.uri, opt.customuri, opt.customuriGET, opt.customuriPOST, opt.beacon_PE, opt.processinject_min_alloc, opt.Post_EX_Process_Name, opt.metadata, opt.injector, opt.Host, opt.Profile, opt.ProfilePath, opt.outFile, opt.custom_cert, opt.cert_password, opt.CDN, opt.CDN_Value, opt.Datajitter, opt.Keylogger, opt.Forwarder, opt.tasks_max_size, opt.tasks_proxy_max_size, opt.tasks_dns_proxy_max_size, opt.syscall_method, opt.httplib, opt.threadspoof, opt.beacongate, opt.eaf_bypass, opt.rdll_use_syscalls, opt.copy_pe_header, opt.rdll_loader, opt.transform_obfuscate, opt.smartinject, opt.sleep_mask, opt.dns, opt.dns_idle) } diff --git a/Struct/Struct.go b/Struct/Struct.go index aedafd8..a5eadb5 100644 --- a/Struct/Struct.go +++ b/Struct/Struct.go @@ -1304,25 +1304,7 @@ set tcp_frame_header ""; set ssh_banner "{{.Variables.SSH_Banner}}"; set ssh_pipename "{{.Variables.SSH_pipename}}##"; -####Manaully add these if your doing C2 over DNS (Future Release)#### -##dns-beacon { -# set dns_idle "1.2.3.4"; -# set dns_max_txt "199"; -# set dns_sleep "1"; -# set dns_ttl "5"; -# set maxdns "200"; -# set dns_stager_prepend "doc-stg-prepend"; -# set dns_stager_subhost "doc-stg-sh."; - -# set beacon "doc.bc."; -# set get_A "doc.1a."; -# set get_AAAA "doc.4a."; -# set get_TXT "doc.tx."; -# set put_metadata "doc.md."; -# set put_output "doc.po."; -# set ns_response "zero"; - -#} +{{.Variables.dns_beacon}} ` }