Skip to content
Open
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
151 changes: 151 additions & 0 deletions Loader/DNS.go
Original file line number Diff line number Diff line change
@@ -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()
}
153 changes: 153 additions & 0 deletions Loader/DNS_test.go
Original file line number Diff line number Diff line change
@@ -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])
}
}
}
}
6 changes: 5 additions & 1 deletion Loader/Loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}
Expand All @@ -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)
Expand All @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions Sample.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,6 @@ TransformObfuscate: "lznt1,xor \"32\""
SmartInject: False
BeaconGate: "All"
SleepMask: False
DNS: False
DNSIdle: "0.0.0.0"

14 changes: 12 additions & 2 deletions SourcePoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ type FlagOptions struct {
transform_obfuscate string
smartinject bool
sleep_mask bool
dns bool
dns_idle string
}

type conf struct {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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}

}

Expand Down Expand Up @@ -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 == "" {
Expand All @@ -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)
}
Loading