mirror of
https://github.com/BishopFox/sliver.git
synced 2026-11-09 01:49:40 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d95da61556 | ||
|
|
5d5661947c | ||
|
|
aad8ca5d4e | ||
|
|
55b0f2bce3 | ||
|
|
73a567550d | ||
|
|
9be2cd8b34 |
No files matched your search
@@ -35,6 +35,7 @@ ARMORY_REPO_URL ?= https://api.github.com/repos/sliverarmory/armory/releases
|
||||
CLIENT_ASSETS_PKG = github.com/bishopfox/sliver/client/assets
|
||||
SLIVER_UPDATE_PKG = github.com/bishopfox/sliver/client/command/update
|
||||
PB_COMPILERS = protoc protoc-gen-go protoc-gen-go-grpc
|
||||
ASSET_SOURCES = $(filter-out %_test.go,$(wildcard util/assets/*.go util/cmd/assets/*.go))
|
||||
|
||||
.PHONY: lint
|
||||
lint:
|
||||
@@ -170,8 +171,8 @@ pb:
|
||||
protoc -I protobuf/ protobuf/dnspb/dns.proto --go_out=paths=source_relative:protobuf/
|
||||
protoc -I protobuf/ protobuf/rpcpb/services.proto --go_out=paths=source_relative:protobuf/ --go-grpc_out=protobuf/ --go-grpc_opt=paths=source_relative
|
||||
|
||||
.PHONY: debug
|
||||
debug: clean
|
||||
.PHONY: debug
|
||||
debug: clean .downloaded_assets validate-go-version
|
||||
$(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor $(TAGS),server $(LDFLAGS_DEBUG) -o sliver-server$(ARTIFACT_SUFFIX) ./server
|
||||
$(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor $(TAGS),client $(LDFLAGS_DEBUG) -o sliver-client$(ARTIFACT_SUFFIX) ./client
|
||||
|
||||
@@ -199,9 +200,9 @@ clean-all: clean
|
||||
clean:
|
||||
rm -f sliver-client sliver-client_* sliver-server sliver-server_* sliver-*.exe
|
||||
|
||||
.downloaded_assets:
|
||||
$(ENV) $(GO) run -mod=vendor ./util/cmd/assets
|
||||
touch ./.downloaded_assets
|
||||
.downloaded_assets: $(ASSET_SOURCES)
|
||||
$(ENV) $(GO) run -mod=vendor ./util/cmd/assets
|
||||
touch ./.downloaded_assets
|
||||
|
||||
|
||||
#
|
||||
@@ -221,6 +222,13 @@ LDFLAGS = -ldflags "-s -w -X $(SLIVER_UPDATE_PKG).SliverPublicKey=$(SLIVER_PUBLI
|
||||
LDFLAGS_DEBUG = -ldflags "-X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) -X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
|
||||
|
||||
COMMA := ,
|
||||
|
||||
DEFAULT_EXECUTABLE_EXT := .exe
|
||||
ifneq ($(strip $(GOOS)),)
|
||||
ifneq ($(strip $(GOOS)),windows)
|
||||
DEFAULT_EXECUTABLE_EXT :=
|
||||
endif
|
||||
endif
|
||||
|
||||
ifeq ($(MAKECMDGOALS), linux)
|
||||
LDFLAGS = -ldflags "-s -w -extldflags '-static' -X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) -X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
|
||||
@@ -235,10 +243,10 @@ $(call windows_exec,$(ENV) GOOS=$(1) GOARCH=$(2) CGO_ENABLED=$(3),"$(GO)" build
|
||||
endef
|
||||
|
||||
.PHONY: default
|
||||
default: clean validate-go-version
|
||||
$(call windows_exec,$(ENV) GOOS= GOARCH=,"$(MAKE)" GOOS= GOARCH= .downloaded_assets)
|
||||
$(call windows_go_build,$(GOOS),$(GOARCH),$(CGO_ENABLED),-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server$(ARTIFACT_SUFFIX))
|
||||
$(call windows_go_build,$(GOOS),$(GOARCH),0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client$(ARTIFACT_SUFFIX))
|
||||
default: clean validate-go-version
|
||||
$(call windows_exec,$(ENV) GOOS= GOARCH=,"$(MAKE)" GOOS= GOARCH= .downloaded_assets)
|
||||
$(call windows_go_build,$(GOOS),$(GOARCH),$(CGO_ENABLED),-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server$(ARTIFACT_SUFFIX)$(DEFAULT_EXECUTABLE_EXT))
|
||||
$(call windows_go_build,$(GOOS),$(GOARCH),0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client$(ARTIFACT_SUFFIX)$(DEFAULT_EXECUTABLE_EXT))
|
||||
|
||||
.PHONY: client
|
||||
client: clean .downloaded_assets validate-go-version
|
||||
@@ -299,8 +307,8 @@ pb: validate-pb-compilers
|
||||
protoc -I protobuf/ protobuf/dnspb/dns.proto --go_out=paths=source_relative:protobuf/
|
||||
protoc -I protobuf/ protobuf/rpcpb/services.proto --go_out=paths=source_relative:protobuf/ --go-grpc_out=protobuf/ --go-grpc_opt=paths=source_relative
|
||||
|
||||
.PHONY: debug
|
||||
debug: clean
|
||||
.PHONY: debug
|
||||
debug: clean .downloaded_assets validate-go-version
|
||||
$(call windows_go_build,$(GOOS),$(GOARCH),$(CGO_ENABLED),-mod=vendor,server,$(LDFLAGS_DEBUG),sliver-server$(ARTIFACT_SUFFIX))
|
||||
$(call windows_go_build,$(GOOS),$(GOARCH),0,-mod=vendor,client,$(LDFLAGS_DEBUG),sliver-client$(ARTIFACT_SUFFIX))
|
||||
|
||||
@@ -324,8 +332,8 @@ clean-all: clean
|
||||
clean:
|
||||
-del /Q /F sliver-client sliver-client_* sliver-server sliver-server_* sliver-*.exe 2>NUL
|
||||
|
||||
.downloaded_assets:
|
||||
$(call windows_exec,$(ENV),"$(GO)" run -mod=vendor ./util/cmd/assets)
|
||||
@type NUL > .downloaded_assets
|
||||
.downloaded_assets: $(ASSET_SOURCES)
|
||||
$(call windows_exec,$(ENV),"$(GO)" run -mod=vendor ./util/cmd/assets)
|
||||
@type NUL > .downloaded_assets
|
||||
|
||||
endif
|
||||
@@ -49,7 +49,7 @@ func PrintBuilders(externalBuilders []*clientpb.Builder, con *console.SliverClie
|
||||
tw := table.NewWriter()
|
||||
tw.SetStyle(settings.GetTableStyle(con))
|
||||
tw.AppendHeader(table.Row{
|
||||
"Name", "Operator", "Templates", "Platform", "Compiler Targets",
|
||||
"Name", "Operator", "Templates", "Platform", "Capabilities", "Compiler Targets",
|
||||
})
|
||||
for _, builder := range externalBuilders {
|
||||
|
||||
@@ -63,6 +63,7 @@ func PrintBuilders(externalBuilders []*clientpb.Builder, con *console.SliverClie
|
||||
builder.OperatorName,
|
||||
strings.Join(builder.Templates, ", "),
|
||||
fmt.Sprintf("%s/%s", builder.GOOS, builder.GOARCH),
|
||||
strings.Join(builder.Capabilities, ", "),
|
||||
strings.Join(targets, "\n"),
|
||||
}
|
||||
tw.AppendRow(table.Row(row))
|
||||
|
||||
@@ -59,6 +59,19 @@ func MigrateCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
config = con.GetActiveBeaconConfig()
|
||||
implantName = beacon.Name
|
||||
}
|
||||
if config == nil {
|
||||
con.PrintErrorf("Failed to derive active implant config.\n")
|
||||
return
|
||||
}
|
||||
|
||||
builds, err := con.Rpc.ImplantBuilds(context.Background(), &commonpb.Empty{})
|
||||
if err != nil {
|
||||
con.PrintErrorf("Failed to retrieve originating implant build %q: %v\n", implantName, err)
|
||||
return
|
||||
}
|
||||
if !applyMigrateObfuscationPolicy(config, implantName, builds) {
|
||||
con.PrintWarnf("Originating implant build %q was not found; continuing with reconstructed legacy obfuscation settings.\n", implantName)
|
||||
}
|
||||
|
||||
targetArch := config.GetGOARCH()
|
||||
if session != nil && session.Arch != "" {
|
||||
@@ -134,6 +147,21 @@ func MigrateCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
}
|
||||
}
|
||||
|
||||
func applyMigrateObfuscationPolicy(config *clientpb.ImplantConfig, implantName string, builds *clientpb.ImplantBuilds) bool {
|
||||
if config == nil || builds == nil {
|
||||
return false
|
||||
}
|
||||
originatingConfig, ok := builds.GetConfigs()[implantName]
|
||||
if !ok || originatingConfig == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
config.Debug = originatingConfig.Debug
|
||||
config.ObfuscateSymbols = originatingConfig.ObfuscateSymbols
|
||||
config.ControlFlow = originatingConfig.ControlFlow
|
||||
return true
|
||||
}
|
||||
|
||||
func normalizeShellcodeEncoderName(name string) string {
|
||||
normalized := strings.ToLower(strings.TrimSpace(name))
|
||||
normalized = strings.ReplaceAll(normalized, "-", "_")
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package exec
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
)
|
||||
|
||||
func TestApplyMigrateObfuscationPolicy(t *testing.T) {
|
||||
config := &clientpb.ImplantConfig{
|
||||
Debug: true,
|
||||
ObfuscateSymbols: false,
|
||||
ControlFlow: clientpb.ControlFlowPolicy_CONTROL_FLOW_DISABLED,
|
||||
}
|
||||
builds := &clientpb.ImplantBuilds{Configs: map[string]*clientpb.ImplantConfig{
|
||||
"source-build": {
|
||||
Debug: false,
|
||||
ObfuscateSymbols: true,
|
||||
ControlFlow: clientpb.ControlFlowPolicy_CONTROL_FLOW_BALANCED_V1,
|
||||
},
|
||||
}}
|
||||
|
||||
if !applyMigrateObfuscationPolicy(config, "source-build", builds) {
|
||||
t.Fatal("applyMigrateObfuscationPolicy() = false, want true")
|
||||
}
|
||||
if config.Debug {
|
||||
t.Fatal("Debug = true, want originating build value false")
|
||||
}
|
||||
if !config.ObfuscateSymbols {
|
||||
t.Fatal("ObfuscateSymbols = false, want originating build value true")
|
||||
}
|
||||
if config.ControlFlow != clientpb.ControlFlowPolicy_CONTROL_FLOW_BALANCED_V1 {
|
||||
t.Fatalf("ControlFlow = %s, want %s", config.ControlFlow, clientpb.ControlFlowPolicy_CONTROL_FLOW_BALANCED_V1)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyMigrateObfuscationPolicyMissingBuildPreservesLegacyConfig(t *testing.T) {
|
||||
config := &clientpb.ImplantConfig{
|
||||
Debug: true,
|
||||
ObfuscateSymbols: false,
|
||||
ControlFlow: clientpb.ControlFlowPolicy_CONTROL_FLOW_DISABLED,
|
||||
}
|
||||
if applyMigrateObfuscationPolicy(
|
||||
config,
|
||||
"missing-build",
|
||||
&clientpb.ImplantBuilds{Configs: map[string]*clientpb.ImplantConfig{}},
|
||||
) {
|
||||
t.Fatal("applyMigrateObfuscationPolicy() = true for missing build, want false")
|
||||
}
|
||||
if !config.Debug || config.ObfuscateSymbols || config.ControlFlow != clientpb.ControlFlowPolicy_CONTROL_FLOW_DISABLED {
|
||||
t.Fatalf("missing build changed legacy config: %+v", config)
|
||||
}
|
||||
}
|
||||
@@ -90,7 +90,11 @@ func PsExecCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
con.PrintErrorf("No profile found for name %s\n", profile)
|
||||
return
|
||||
}
|
||||
serviceBinary, _ = generate.GetSliverBinary(implantProfile, con)
|
||||
serviceBinary, err = generate.GetSliverBinary(implantProfile, con)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Failed to generate service binary: %s\n", err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// use a custom exe instead of generating a new Sliver
|
||||
fileBytes, err := os.ReadFile(customExe)
|
||||
|
||||
@@ -326,6 +326,7 @@ func coreImplantFlags(name string, cmd *cobra.Command) {
|
||||
f.StringP("debug-file", "O", "", "path to debug output")
|
||||
f.BoolP("evasion", "e", false, "enable evasion features (e.g. overwrite user space hooks)")
|
||||
f.BoolP("skip-symbols", "l", false, "skip symbol obfuscation")
|
||||
f.String("control-flow", "off", "control-flow obfuscation policy (off, balanced-v1)")
|
||||
f.String("shellcode-encoder", "", "shellcode encoder to apply (optional; see `shellcode-encoders`)")
|
||||
f.StringP("exports", "v", "StartW,VoidFunc,DllInstall,DllRegisterServer,DllUnregisterServer", "comma separated list of exports to include in the binary")
|
||||
f.StringP("canary", "c", "", "canary domain(s)")
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
package generate
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
consts "github.com/bishopfox/sliver/client/constants"
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
)
|
||||
|
||||
func TestParseControlFlowPolicy(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
value string
|
||||
want clientpb.ControlFlowPolicy
|
||||
wantErr bool
|
||||
}{
|
||||
{name: "off", value: "off", want: clientpb.ControlFlowPolicy_CONTROL_FLOW_DISABLED},
|
||||
{name: "balanced", value: "balanced-v1", want: clientpb.ControlFlowPolicy_CONTROL_FLOW_BALANCED_V1},
|
||||
{name: "case and whitespace", value: " BALANCED-V1 ", want: clientpb.ControlFlowPolicy_CONTROL_FLOW_BALANCED_V1},
|
||||
{name: "unknown", value: "aggressive", wantErr: true},
|
||||
{name: "empty", value: "", wantErr: true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := parseControlFlowPolicy(tt.value)
|
||||
if tt.wantErr {
|
||||
if err == nil {
|
||||
t.Fatal("expected an error")
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("parseControlFlowPolicy() error = %v", err)
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Fatalf("parseControlFlowPolicy() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateControlFlowFlags(t *testing.T) {
|
||||
disabled := clientpb.ControlFlowPolicy_CONTROL_FLOW_DISABLED
|
||||
balanced := clientpb.ControlFlowPolicy_CONTROL_FLOW_BALANCED_V1
|
||||
|
||||
if err := validateControlFlowFlags(disabled, true, true); err != nil {
|
||||
t.Fatalf("disabled control flow should not constrain other flags: %v", err)
|
||||
}
|
||||
if err := validateControlFlowFlags(balanced, false, false); err != nil {
|
||||
t.Fatalf("balanced control flow with symbol obfuscation should be valid: %v", err)
|
||||
}
|
||||
if err := validateControlFlowFlags(balanced, true, false); err == nil {
|
||||
t.Fatal("expected --debug conflict")
|
||||
}
|
||||
if err := validateControlFlowFlags(balanced, false, true); err == nil {
|
||||
t.Fatal("expected --skip-symbols conflict")
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyGenerateBeaconFormSetsControlFlow(t *testing.T) {
|
||||
generateCmd := commandByUse(Commands(&console.SliverClient{}), consts.GenerateStr)
|
||||
if generateCmd == nil {
|
||||
t.Fatalf("missing %q command", consts.GenerateStr)
|
||||
}
|
||||
beaconCmd := commandByUse(generateCmd.Commands(), consts.BeaconStr)
|
||||
if beaconCmd == nil {
|
||||
t.Fatalf("missing %q subcommand", consts.BeaconStr)
|
||||
}
|
||||
|
||||
err := applyGenerateBeaconForm(beaconCmd, &forms.GenerateBeaconFormResult{
|
||||
OS: "darwin",
|
||||
Arch: "arm64",
|
||||
Format: "exe",
|
||||
C2Type: "mtls",
|
||||
C2Value: "127.0.0.1:31337",
|
||||
ControlFlow: "balanced-v1",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("applyGenerateBeaconForm() error = %v", err)
|
||||
}
|
||||
if got, err := beaconCmd.Flags().GetString("control-flow"); err != nil || got != "balanced-v1" {
|
||||
t.Fatalf("control-flow flag = %q, %v; want balanced-v1", got, err)
|
||||
}
|
||||
}
|
||||
@@ -57,9 +57,11 @@ func GenerateBeaconCmd(cmd *cobra.Command, con *console.SliverClient, args []str
|
||||
save, _ = os.Getwd()
|
||||
}
|
||||
if external, _ := cmd.Flags().GetBool("external-builder"); !external {
|
||||
compile(name, config, spoofMetadata, save, con)
|
||||
_, _ = compile(name, config, spoofMetadata, save, con)
|
||||
} else {
|
||||
externalBuild(name, config, spoofMetadata, save, con)
|
||||
if _, err := externalBuild(name, config, spoofMetadata, save, con); err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,6 +115,9 @@ func applyGenerateBeaconForm(cmd *cobra.Command, result *forms.GenerateBeaconFor
|
||||
if err := cmd.Flags().Set("format", result.Format); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := cmd.Flags().Set("control-flow", result.ControlFlow); err != nil {
|
||||
return err
|
||||
}
|
||||
name := strings.TrimSpace(result.Name)
|
||||
if name != "" {
|
||||
if err := cmd.Flags().Set("name", name); err != nil {
|
||||
|
||||
@@ -74,7 +74,9 @@ const (
|
||||
)
|
||||
|
||||
const (
|
||||
crossCompilerInfoURL = "https://github.com/BishopFox/sliver/wiki/Cross-Compiling-Implants"
|
||||
crossCompilerInfoURL = "https://github.com/BishopFox/sliver/wiki/Cross-Compiling-Implants"
|
||||
controlFlowCapability = "garble.control-flow/balanced-v1"
|
||||
localControlFlowCapability = "garble.control-flow.local/balanced-v1"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -90,9 +92,14 @@ var (
|
||||
"windows/arm64": true,
|
||||
}
|
||||
|
||||
// ErrNoExternalBuilder indicates that no external builders are registered.
|
||||
ErrNoExternalBuilder = errors.New("no external builders are available")
|
||||
ErrNoValidBuilders = errors.New("no valid external builders for target")
|
||||
ErrNoSelection = errors.New("no selection")
|
||||
// ErrNoValidBuilders indicates that no external builder supports the target.
|
||||
ErrNoValidBuilders = errors.New("no valid external builders for target")
|
||||
// ErrNoControlFlowBuilder indicates that no target-compatible builder supports control-flow obfuscation.
|
||||
ErrNoControlFlowBuilder = errors.New("no target-compatible external builder supports control-flow obfuscation")
|
||||
// ErrNoSelection indicates that the user did not select an external builder.
|
||||
ErrNoSelection = errors.New("no selection")
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -100,6 +107,38 @@ const (
|
||||
imageFileDLLCharacteristic = 0x2000
|
||||
)
|
||||
|
||||
func parseControlFlowPolicy(value string) (clientpb.ControlFlowPolicy, error) {
|
||||
switch strings.ToLower(strings.TrimSpace(value)) {
|
||||
case "off":
|
||||
return clientpb.ControlFlowPolicy_CONTROL_FLOW_DISABLED, nil
|
||||
case "balanced-v1":
|
||||
return clientpb.ControlFlowPolicy_CONTROL_FLOW_BALANCED_V1, nil
|
||||
default:
|
||||
return clientpb.ControlFlowPolicy_CONTROL_FLOW_DISABLED, fmt.Errorf("invalid control-flow policy %q (expected off or balanced-v1)", value)
|
||||
}
|
||||
}
|
||||
|
||||
func validateControlFlowFlags(policy clientpb.ControlFlowPolicy, debug, skipSymbols bool) error {
|
||||
if policy == clientpb.ControlFlowPolicy_CONTROL_FLOW_DISABLED {
|
||||
return nil
|
||||
}
|
||||
if debug || skipSymbols {
|
||||
return errors.New("--control-flow=balanced-v1 requires symbol obfuscation and cannot be combined with --debug or --skip-symbols")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func controlFlowPolicyName(policy clientpb.ControlFlowPolicy) string {
|
||||
switch policy {
|
||||
case clientpb.ControlFlowPolicy_CONTROL_FLOW_DISABLED:
|
||||
return "disabled"
|
||||
case clientpb.ControlFlowPolicy_CONTROL_FLOW_BALANCED_V1:
|
||||
return "balanced-v1"
|
||||
default:
|
||||
return fmt.Sprintf("unknown (%d)", policy)
|
||||
}
|
||||
}
|
||||
|
||||
// GenerateCmd - The main command used to generate implant binaries
|
||||
func GenerateCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
if shouldRunGenerateForm(cmd, con, args) {
|
||||
@@ -172,6 +211,9 @@ func applyGenerateForm(cmd *cobra.Command, result *forms.GenerateFormResult) err
|
||||
if err := cmd.Flags().Set("format", result.Format); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := cmd.Flags().Set("control-flow", result.ControlFlow); err != nil {
|
||||
return err
|
||||
}
|
||||
name := strings.TrimSpace(result.Name)
|
||||
if name != "" {
|
||||
if err := cmd.Flags().Set("name", name); err != nil {
|
||||
@@ -329,12 +371,18 @@ func parseCompileFlags(cmd *cobra.Command, con *console.SliverClient) (string, *
|
||||
}
|
||||
c2s = append(c2s, tcpPivotC2...)
|
||||
|
||||
var symbolObfuscation bool
|
||||
if debug, _ := cmd.Flags().GetBool("debug"); debug {
|
||||
symbolObfuscation = false
|
||||
} else {
|
||||
symbolObfuscation, _ = cmd.Flags().GetBool("skip-symbols")
|
||||
symbolObfuscation = !symbolObfuscation
|
||||
debug, _ := cmd.Flags().GetBool("debug")
|
||||
skipSymbols, _ := cmd.Flags().GetBool("skip-symbols")
|
||||
symbolObfuscation := !debug && !skipSymbols
|
||||
controlFlowValue, _ := cmd.Flags().GetString("control-flow")
|
||||
controlFlow, err := parseControlFlowPolicy(controlFlowValue)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return "", nil
|
||||
}
|
||||
if err := validateControlFlowFlags(controlFlow, debug, skipSymbols); err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return "", nil
|
||||
}
|
||||
|
||||
if len(mtlsC2) == 0 && len(wgC2) == 0 && len(httpC2) == 0 && len(dnsC2) == 0 && len(namedPipeC2) == 0 && len(tcpPivotC2) == 0 {
|
||||
@@ -353,7 +401,6 @@ func parseCompileFlags(cmd *cobra.Command, con *console.SliverClient) (string, *
|
||||
}
|
||||
}
|
||||
|
||||
debug, _ := cmd.Flags().GetBool("debug")
|
||||
evasion, _ := cmd.Flags().GetBool("evasion")
|
||||
templateName, _ := cmd.Flags().GetString("template")
|
||||
|
||||
@@ -516,6 +563,7 @@ func parseCompileFlags(cmd *cobra.Command, con *console.SliverClient) (string, *
|
||||
SGNEnabled: sgnEnabled,
|
||||
ShellcodeEncoder: shellcodeEncoder,
|
||||
ObfuscateSymbols: symbolObfuscation,
|
||||
ControlFlow: controlFlow,
|
||||
C2: c2s,
|
||||
CanaryDomains: canaryDomains,
|
||||
TemplateName: templateName,
|
||||
@@ -1643,6 +1691,10 @@ func outputSavePath(save string, defaultName string, config *clientpb.ImplantCon
|
||||
}
|
||||
|
||||
func externalBuild(name string, config *clientpb.ImplantConfig, spoofMetadata *clientpb.SpoofMetadataConfig, save string, con *console.SliverClient) (*commonpb.File, error) {
|
||||
if err := ensureServerSupportsControlFlow(config, con); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
potentialBuilders, err := findExternalBuilders(config, con)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -1670,6 +1722,9 @@ func externalBuild(name string, config *clientpb.ImplantConfig, spoofMetadata *c
|
||||
} else if !config.Debug {
|
||||
con.PrintErrorf("Symbol obfuscation is %s\n", console.StyleBold.Render("disabled"))
|
||||
}
|
||||
if config.ControlFlow != clientpb.ControlFlowPolicy_CONTROL_FLOW_DISABLED {
|
||||
con.PrintInfof("Control-flow obfuscation policy: %s\n", console.StyleBold.Render(controlFlowPolicyName(config.ControlFlow)))
|
||||
}
|
||||
start := time.Now()
|
||||
|
||||
listenerID, listener := con.CreateEventListener()
|
||||
@@ -1690,6 +1745,14 @@ func externalBuild(name string, config *clientpb.ImplantConfig, spoofMetadata *c
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return nil, err
|
||||
}
|
||||
if config.ControlFlow != clientpb.ControlFlowPolicy_CONTROL_FLOW_DISABLED &&
|
||||
(externalImplantConfig == nil || externalImplantConfig.Config == nil || externalImplantConfig.Config.ControlFlow != config.ControlFlow) {
|
||||
return nil, fmt.Errorf(
|
||||
"server returned control-flow policy %s for requested policy %s",
|
||||
controlFlowPolicyName(externalImplantConfig.GetConfig().GetControlFlow()),
|
||||
controlFlowPolicyName(config.ControlFlow),
|
||||
)
|
||||
}
|
||||
con.Printf("done\n")
|
||||
|
||||
msgF := "Waiting for external builder to acknowledge build (template: %s) ... %s"
|
||||
@@ -1818,6 +1881,11 @@ func externalBuild(name string, config *clientpb.ImplantConfig, spoofMetadata *c
|
||||
}
|
||||
|
||||
func compile(name string, config *clientpb.ImplantConfig, spoofMetadata *clientpb.SpoofMetadataConfig, save string, con *console.SliverClient) (*commonpb.File, error) {
|
||||
if err := ensureServerCanBuildControlFlow(config, con); err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if config.IsBeacon {
|
||||
interval := time.Duration(config.BeaconInterval)
|
||||
con.PrintInfof("Generating new %s/%s beacon implant binary (%v)\n", config.GOOS, config.GOARCH, interval)
|
||||
@@ -1829,6 +1897,9 @@ func compile(name string, config *clientpb.ImplantConfig, spoofMetadata *clientp
|
||||
} else if !config.Debug {
|
||||
con.PrintErrorf("Symbol obfuscation is %s\n", console.StyleBold.Render("disabled"))
|
||||
}
|
||||
if config.ControlFlow != clientpb.ControlFlowPolicy_CONTROL_FLOW_DISABLED {
|
||||
con.PrintInfof("Control-flow obfuscation policy: %s\n", console.StyleBold.Render(controlFlowPolicyName(config.ControlFlow)))
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
ctrl := make(chan bool)
|
||||
@@ -2009,9 +2080,14 @@ func findExternalBuilders(config *clientpb.ImplantConfig, con *console.SliverCli
|
||||
}
|
||||
|
||||
validBuilders := []*clientpb.Builder{}
|
||||
targetCompatibleBuilder := false
|
||||
for _, builder := range builders.Builders {
|
||||
for _, target := range builder.Targets {
|
||||
if target.GOOS == config.GOOS && target.GOARCH == config.GOARCH && config.Format == target.Format {
|
||||
targetCompatibleBuilder = true
|
||||
if config.ControlFlow != clientpb.ControlFlowPolicy_CONTROL_FLOW_DISABLED && !hasCapability(builder.Capabilities, controlFlowCapability) {
|
||||
break
|
||||
}
|
||||
validBuilders = append(validBuilders, builder)
|
||||
break
|
||||
}
|
||||
@@ -2019,12 +2095,55 @@ func findExternalBuilders(config *clientpb.ImplantConfig, con *console.SliverCli
|
||||
}
|
||||
|
||||
if len(validBuilders) < 1 {
|
||||
if targetCompatibleBuilder && config.ControlFlow != clientpb.ControlFlowPolicy_CONTROL_FLOW_DISABLED {
|
||||
return []*clientpb.Builder{}, ErrNoControlFlowBuilder
|
||||
}
|
||||
return []*clientpb.Builder{}, ErrNoValidBuilders
|
||||
}
|
||||
|
||||
return validBuilders, nil
|
||||
}
|
||||
|
||||
func ensureServerSupportsControlFlow(config *clientpb.ImplantConfig, con *console.SliverClient) error {
|
||||
if config == nil || config.ControlFlow == clientpb.ControlFlowPolicy_CONTROL_FLOW_DISABLED {
|
||||
return nil
|
||||
}
|
||||
compiler, err := con.Rpc.GetCompiler(context.Background(), &commonpb.Empty{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to query server control-flow capability: %w", err)
|
||||
}
|
||||
if !hasCapability(compiler.Capabilities, controlFlowCapability) {
|
||||
return fmt.Errorf("server does not support %s", controlFlowCapability)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ensureServerCanBuildControlFlow(config *clientpb.ImplantConfig, con *console.SliverClient) error {
|
||||
if config == nil || config.ControlFlow == clientpb.ControlFlowPolicy_CONTROL_FLOW_DISABLED {
|
||||
return nil
|
||||
}
|
||||
compiler, err := con.Rpc.GetCompiler(context.Background(), &commonpb.Empty{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to query server control-flow capability: %w", err)
|
||||
}
|
||||
if !hasCapability(compiler.Capabilities, controlFlowCapability) {
|
||||
return fmt.Errorf("server does not support %s", controlFlowCapability)
|
||||
}
|
||||
if !hasCapability(compiler.Capabilities, localControlFlowCapability) {
|
||||
return fmt.Errorf("server local compiler does not provide %s", localControlFlowCapability)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func hasCapability(capabilities []string, required string) bool {
|
||||
for _, capability := range capabilities {
|
||||
if capability == required {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func selectExternalBuilder(builders []*clientpb.Builder, _ *console.SliverClient) (*clientpb.Builder, error) {
|
||||
choices := []string{}
|
||||
for _, builder := range builders {
|
||||
|
||||
@@ -58,7 +58,7 @@ func ProfilesNewCmd(cmd *cobra.Command, con *console.SliverClient, args []string
|
||||
Name: name,
|
||||
Config: config,
|
||||
}
|
||||
resp, err := con.Rpc.SaveImplantProfile(context.Background(), profile)
|
||||
resp, err := saveImplantProfileWithControlFlowCheck(con, profile)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
} else {
|
||||
@@ -106,7 +106,7 @@ func ProfilesNewBeaconCmd(cmd *cobra.Command, con *console.SliverClient, args []
|
||||
Name: name,
|
||||
Config: config,
|
||||
}
|
||||
resp, err := con.Rpc.SaveImplantProfile(context.Background(), profile)
|
||||
resp, err := saveImplantProfileWithControlFlowCheck(con, profile)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
} else {
|
||||
@@ -114,6 +114,25 @@ func ProfilesNewBeaconCmd(cmd *cobra.Command, con *console.SliverClient, args []
|
||||
}
|
||||
}
|
||||
|
||||
func saveImplantProfileWithControlFlowCheck(con *console.SliverClient, profile *clientpb.ImplantProfile) (*clientpb.ImplantProfile, error) {
|
||||
if profile == nil || profile.Config == nil {
|
||||
return nil, errors.New("implant profile config cannot be nil")
|
||||
}
|
||||
if err := ensureServerSupportsControlFlow(profile.Config, con); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := con.Rpc.SaveImplantProfile(context.Background(), profile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if profile.Config.ControlFlow != clientpb.ControlFlowPolicy_CONTROL_FLOW_DISABLED &&
|
||||
(resp == nil || resp.Config == nil || resp.Config.ControlFlow != profile.Config.ControlFlow) {
|
||||
return nil, errors.New("server did not preserve the requested control-flow policy")
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func shouldRunGenerateProfilesNewForm(cmd *cobra.Command, con *console.SliverClient, args []string) bool {
|
||||
if con == nil || con.IsCLI {
|
||||
return false
|
||||
@@ -134,6 +153,9 @@ func applyGenerateProfilesNewForm(cmd *cobra.Command, result *forms.GenerateProf
|
||||
if err := cmd.Flags().Set("format", result.Format); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := cmd.Flags().Set("control-flow", result.ControlFlow); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
profileName := strings.TrimSpace(result.ProfileName)
|
||||
if profileName == "" {
|
||||
@@ -200,6 +222,9 @@ func applyGenerateProfilesNewBeaconForm(cmd *cobra.Command, result *forms.Genera
|
||||
if err := cmd.Flags().Set("format", result.Format); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := cmd.Flags().Set("control-flow", result.ControlFlow); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
profileName := strings.TrimSpace(result.ProfileName)
|
||||
if profileName == "" {
|
||||
|
||||
@@ -60,6 +60,7 @@ func PrintProfiles(profiles []*clientpb.ImplantProfile, con *console.SliverClien
|
||||
"Debug",
|
||||
"Format",
|
||||
"Obfuscation",
|
||||
"Control Flow",
|
||||
"Limitations",
|
||||
"C2 Profile",
|
||||
// "Nonce",
|
||||
@@ -91,6 +92,7 @@ func PrintProfiles(profiles []*clientpb.ImplantProfile, con *console.SliverClien
|
||||
fmt.Sprintf("%v", config.Debug),
|
||||
fmt.Sprintf("%v", config.Format),
|
||||
obfuscation,
|
||||
controlFlowPolicyName(config.ControlFlow),
|
||||
getLimitsString(config),
|
||||
config.HTTPC2ConfigName,
|
||||
// profile.ImplantID,
|
||||
@@ -164,6 +166,7 @@ func populateProfileProperties(config *clientpb.ImplantConfig) map[string]string
|
||||
} else {
|
||||
properties["obsymbols"] = "disabled"
|
||||
}
|
||||
properties["obcontrolflow"] = controlFlowPolicyName(config.ControlFlow)
|
||||
|
||||
properties["shellcodeencoder"] = "n/a"
|
||||
if config.Format == clientpb.OutputFormat_SHELLCODE {
|
||||
@@ -323,6 +326,10 @@ func PrintProfileInfo(name string, con *console.SliverClient) {
|
||||
"Obfuscation of symbols is",
|
||||
properties["obsymbols"],
|
||||
})
|
||||
tw.AppendRow(table.Row{
|
||||
"Control-flow obfuscation is",
|
||||
properties["obcontrolflow"],
|
||||
})
|
||||
tw.AppendRow(table.Row{
|
||||
"Shellcode encoder is",
|
||||
properties["shellcodeencoder"],
|
||||
|
||||
@@ -28,6 +28,7 @@ import (
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
consts "github.com/bishopfox/sliver/client/constants"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
"github.com/bishopfox/sliver/protobuf/sliverpb"
|
||||
)
|
||||
|
||||
@@ -53,6 +54,14 @@ func GetSystemCmd(cmd *cobra.Command, con *console.SliverClient, args []string)
|
||||
con.PrintErrorf("Failed to derive active session config.\n")
|
||||
return
|
||||
}
|
||||
builds, err := con.Rpc.ImplantBuilds(context.Background(), &commonpb.Empty{})
|
||||
if err != nil {
|
||||
con.PrintErrorf("Failed to retrieve originating implant build %q: %v\n", session.Name, err)
|
||||
return
|
||||
}
|
||||
if !applyGetSystemObfuscationPolicy(config, session.Name, builds) {
|
||||
con.PrintWarnf("Originating implant build %q was not found; continuing with reconstructed legacy obfuscation settings.\n", session.Name)
|
||||
}
|
||||
|
||||
/* If the HTTP C2 Config name is not defined, then put in the default value
|
||||
This will have no effect on implants that do not use HTTP C2
|
||||
@@ -94,6 +103,21 @@ func GetSystemCmd(cmd *cobra.Command, con *console.SliverClient, args []string)
|
||||
}
|
||||
}
|
||||
|
||||
func applyGetSystemObfuscationPolicy(config *clientpb.ImplantConfig, implantName string, builds *clientpb.ImplantBuilds) bool {
|
||||
if config == nil || builds == nil {
|
||||
return false
|
||||
}
|
||||
originatingConfig, ok := builds.GetConfigs()[implantName]
|
||||
if !ok || originatingConfig == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
config.Debug = originatingConfig.Debug
|
||||
config.ObfuscateSymbols = originatingConfig.ObfuscateSymbols
|
||||
config.ControlFlow = originatingConfig.ControlFlow
|
||||
return true
|
||||
}
|
||||
|
||||
// PrintGetSystem - Print the results of get system
|
||||
func PrintGetSystem(getsystemResp *sliverpb.GetSystem, con *console.SliverClient) {
|
||||
if getsystemResp.Response != nil && getsystemResp.Response.GetErr() != "" {
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package privilege
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
)
|
||||
|
||||
func TestApplyGetSystemObfuscationPolicy(t *testing.T) {
|
||||
config := &clientpb.ImplantConfig{Debug: true}
|
||||
builds := &clientpb.ImplantBuilds{Configs: map[string]*clientpb.ImplantConfig{
|
||||
"source-build": {
|
||||
Debug: false,
|
||||
ObfuscateSymbols: true,
|
||||
ControlFlow: clientpb.ControlFlowPolicy_CONTROL_FLOW_BALANCED_V1,
|
||||
},
|
||||
}}
|
||||
|
||||
if !applyGetSystemObfuscationPolicy(config, "source-build", builds) {
|
||||
t.Fatal("applyGetSystemObfuscationPolicy() = false, want true")
|
||||
}
|
||||
if config.Debug || !config.ObfuscateSymbols || config.ControlFlow != clientpb.ControlFlowPolicy_CONTROL_FLOW_BALANCED_V1 {
|
||||
t.Fatalf("obfuscation policy = %+v, want originating build values", config)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyGetSystemObfuscationPolicyMissingBuildPreservesLegacyConfig(t *testing.T) {
|
||||
config := &clientpb.ImplantConfig{Debug: true}
|
||||
if applyGetSystemObfuscationPolicy(config, "missing-build", &clientpb.ImplantBuilds{}) {
|
||||
t.Fatal("applyGetSystemObfuscationPolicy() = true for missing build, want false")
|
||||
}
|
||||
if !config.Debug || config.ObfuscateSymbols || config.ControlFlow != clientpb.ControlFlowPolicy_CONTROL_FLOW_DISABLED {
|
||||
t.Fatalf("missing build changed legacy config: %+v", config)
|
||||
}
|
||||
}
|
||||
+20
-11
@@ -15,22 +15,24 @@ var ErrUserAborted = huh.ErrUserAborted
|
||||
|
||||
// GenerateFormResult captures the inputs needed to drive the generate command.
|
||||
type GenerateFormResult struct {
|
||||
OS string
|
||||
Arch string
|
||||
Format string
|
||||
Name string
|
||||
C2Type string
|
||||
C2Value string
|
||||
Save string
|
||||
OS string
|
||||
Arch string
|
||||
Format string
|
||||
Name string
|
||||
C2Type string
|
||||
C2Value string
|
||||
ControlFlow string
|
||||
Save string
|
||||
}
|
||||
|
||||
// GenerateForm prompts for core generate flags and returns the collected values.
|
||||
func GenerateForm(compiler *clientpb.Compiler) (*GenerateFormResult, error) {
|
||||
result := &GenerateFormResult{
|
||||
OS: "windows",
|
||||
Arch: "amd64",
|
||||
Format: "exe",
|
||||
C2Type: "mtls",
|
||||
OS: "windows",
|
||||
Arch: "amd64",
|
||||
Format: "exe",
|
||||
C2Type: "mtls",
|
||||
ControlFlow: "off",
|
||||
}
|
||||
commonPlatformsOnly := true
|
||||
lastCommonPlatformsOnly := commonPlatformsOnly
|
||||
@@ -78,6 +80,13 @@ func GenerateForm(compiler *clientpb.Compiler) (*GenerateFormResult, error) {
|
||||
}, targetBindings).
|
||||
Height(3).
|
||||
Value(&result.Format),
|
||||
huh.NewSelect[string]().
|
||||
Title("Control-flow obfuscation").
|
||||
Options(
|
||||
huh.NewOption("Off", "off"),
|
||||
huh.NewOption("Balanced (experimental)", "balanced-v1"),
|
||||
).
|
||||
Value(&result.ControlFlow),
|
||||
),
|
||||
huh.NewGroup(
|
||||
huh.NewInput().
|
||||
|
||||
@@ -13,27 +13,29 @@ import (
|
||||
|
||||
// GenerateBeaconFormResult captures the inputs needed to drive the generate beacon command.
|
||||
type GenerateBeaconFormResult struct {
|
||||
OS string
|
||||
Arch string
|
||||
Format string
|
||||
Name string
|
||||
C2Type string
|
||||
C2Value string
|
||||
Save string
|
||||
Days string
|
||||
Hours string
|
||||
Minutes string
|
||||
Seconds string
|
||||
Jitter string
|
||||
OS string
|
||||
Arch string
|
||||
Format string
|
||||
Name string
|
||||
C2Type string
|
||||
C2Value string
|
||||
ControlFlow string
|
||||
Save string
|
||||
Days string
|
||||
Hours string
|
||||
Minutes string
|
||||
Seconds string
|
||||
Jitter string
|
||||
}
|
||||
|
||||
// GenerateBeaconForm prompts for generate beacon flags and returns the collected values.
|
||||
func GenerateBeaconForm(compiler *clientpb.Compiler) (*GenerateBeaconFormResult, error) {
|
||||
result := &GenerateBeaconFormResult{
|
||||
OS: "windows",
|
||||
Arch: "amd64",
|
||||
Format: "exe",
|
||||
C2Type: "mtls",
|
||||
OS: "windows",
|
||||
Arch: "amd64",
|
||||
Format: "exe",
|
||||
C2Type: "mtls",
|
||||
ControlFlow: "off",
|
||||
}
|
||||
commonPlatformsOnly := true
|
||||
lastCommonPlatformsOnly := commonPlatformsOnly
|
||||
@@ -81,6 +83,14 @@ func GenerateBeaconForm(compiler *clientpb.Compiler) (*GenerateBeaconFormResult,
|
||||
}, targetBindings).
|
||||
Height(3).
|
||||
Value(&result.Format),
|
||||
huh.NewSelect[string]().
|
||||
Title("Control-flow obfuscation").
|
||||
Description("Experimental; balanced-v1 requires symbol obfuscation").
|
||||
Options(
|
||||
huh.NewOption("Off", "off"),
|
||||
huh.NewOption("Balanced (experimental)", "balanced-v1"),
|
||||
).
|
||||
Value(&result.ControlFlow),
|
||||
),
|
||||
huh.NewGroup(
|
||||
huh.NewInput().
|
||||
|
||||
@@ -18,15 +18,17 @@ type GenerateProfilesNewFormResult struct {
|
||||
Name string
|
||||
C2Type string
|
||||
C2Value string
|
||||
ControlFlow string
|
||||
}
|
||||
|
||||
// GenerateProfilesNewForm prompts for profiles new flags and returns the collected values.
|
||||
func GenerateProfilesNewForm() (*GenerateProfilesNewFormResult, error) {
|
||||
result := &GenerateProfilesNewFormResult{
|
||||
OS: "windows",
|
||||
Arch: "amd64",
|
||||
Format: "exe",
|
||||
C2Type: "mtls",
|
||||
OS: "windows",
|
||||
Arch: "amd64",
|
||||
Format: "exe",
|
||||
C2Type: "mtls",
|
||||
ControlFlow: "off",
|
||||
}
|
||||
|
||||
form := huh.NewForm(
|
||||
@@ -93,6 +95,13 @@ func GenerateProfilesNewForm() (*GenerateProfilesNewFormResult, error) {
|
||||
}, &result.OS).
|
||||
Height(3).
|
||||
Value(&result.Format),
|
||||
huh.NewSelect[string]().
|
||||
Title("Control-flow obfuscation").
|
||||
Options(
|
||||
huh.NewOption("Off", "off"),
|
||||
huh.NewOption("Balanced (experimental)", "balanced-v1"),
|
||||
).
|
||||
Value(&result.ControlFlow),
|
||||
),
|
||||
huh.NewGroup(
|
||||
huh.NewInput().
|
||||
|
||||
@@ -18,6 +18,7 @@ type GenerateProfilesNewBeaconFormResult struct {
|
||||
Name string
|
||||
C2Type string
|
||||
C2Value string
|
||||
ControlFlow string
|
||||
Days string
|
||||
Hours string
|
||||
Minutes string
|
||||
@@ -28,10 +29,11 @@ type GenerateProfilesNewBeaconFormResult struct {
|
||||
// GenerateProfilesNewBeaconForm prompts for profiles new beacon flags and returns the collected values.
|
||||
func GenerateProfilesNewBeaconForm() (*GenerateProfilesNewBeaconFormResult, error) {
|
||||
result := &GenerateProfilesNewBeaconFormResult{
|
||||
OS: "windows",
|
||||
Arch: "amd64",
|
||||
Format: "exe",
|
||||
C2Type: "mtls",
|
||||
OS: "windows",
|
||||
Arch: "amd64",
|
||||
Format: "exe",
|
||||
C2Type: "mtls",
|
||||
ControlFlow: "off",
|
||||
}
|
||||
|
||||
form := huh.NewForm(
|
||||
@@ -98,6 +100,13 @@ func GenerateProfilesNewBeaconForm() (*GenerateProfilesNewBeaconFormResult, erro
|
||||
}, &result.OS).
|
||||
Height(3).
|
||||
Value(&result.Format),
|
||||
huh.NewSelect[string]().
|
||||
Title("Control-flow obfuscation").
|
||||
Options(
|
||||
huh.NewOption("Off", "off"),
|
||||
huh.NewOption("Balanced (experimental)", "balanced-v1"),
|
||||
).
|
||||
Value(&result.ControlFlow),
|
||||
),
|
||||
huh.NewGroup(
|
||||
huh.NewInput().
|
||||
|
||||
@@ -42,6 +42,8 @@ Some commands such as `shell` and `portfwd` only work over interactive sessions.
|
||||
|
||||
Generating implants is done using the `generate` command, you must specify at least one C2 endpoint using `--mtls`, `--wg`, `--http`, or `--dns`. Note that when an implant attempts to connect to an endpoint specified using `--http` it will try both HTTPS and then HTTP (if HTTPS fails). We recommend using mTLS (`--mtls`) or WireGuard (`--wg`) whenever possible. You can also specify an output directory with `--save`, by default the implant will be saved to the current working directory.
|
||||
|
||||
Control-flow obfuscation is experimental and opt-in. Add `--control-flow=balanced-v1` to a session, beacon, or profile generation command to enable Sliver's versioned balanced policy. It requires symbol obfuscation and cannot be combined with `--debug` or `--skip-symbols`. The server admits one local control-flow build at a time because Garble's SSA transformation is memory intensive; external builds require a builder that advertises the matching capability. The default is `--control-flow=off`.
|
||||
|
||||
#### Session Mode
|
||||
|
||||
```asciinema
|
||||
|
||||
+31
-9
@@ -139,32 +139,54 @@ function Get-TestDirectories {
|
||||
}
|
||||
|
||||
function Get-ServerBinaryPath {
|
||||
$candidates = @(
|
||||
(Join-Path $PWD.Path "sliver-server.exe"),
|
||||
(Join-Path $PWD.Path "sliver-server")
|
||||
)
|
||||
$serverName = "sliver-server"
|
||||
if ([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform(
|
||||
[System.Runtime.InteropServices.OSPlatform]::Windows
|
||||
)) {
|
||||
$serverName = "sliver-server.exe"
|
||||
}
|
||||
|
||||
foreach ($candidate in $candidates) {
|
||||
if (Test-Path -LiteralPath $candidate) {
|
||||
return $candidate
|
||||
}
|
||||
$candidate = Join-Path $PWD.Path $serverName
|
||||
if (Test-Path -LiteralPath $candidate -PathType Leaf) {
|
||||
return $candidate
|
||||
}
|
||||
|
||||
return $null
|
||||
}
|
||||
|
||||
function Get-PinnedGarbleAssetPath {
|
||||
$garbleName = "garble"
|
||||
if ([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform(
|
||||
[System.Runtime.InteropServices.OSPlatform]::Windows
|
||||
)) {
|
||||
$garbleName = "garble.exe"
|
||||
}
|
||||
|
||||
$goBin = Join-Path (Join-Path $env:SLIVER_ROOT_DIR "go") "bin"
|
||||
return (Join-Path $goBin $garbleName)
|
||||
}
|
||||
|
||||
function Assert-PinnedGarbleAssetUnpacked {
|
||||
$garblePath = Get-PinnedGarbleAssetPath
|
||||
if (-not (Test-Path -LiteralPath $garblePath -PathType Leaf)) {
|
||||
throw "Server asset unpack did not produce pinned Garble at $garblePath"
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-UnpackServerAssets {
|
||||
$serverBinary = Get-ServerBinaryPath
|
||||
if ($serverBinary) {
|
||||
try {
|
||||
Invoke-ExternalCommand -FilePath $serverBinary -Arguments @("unpack", "--force")
|
||||
Assert-PinnedGarbleAssetUnpacked
|
||||
return
|
||||
} catch {
|
||||
Write-Host "sliver-server unpack failed, falling back to go run ./server unpack --force"
|
||||
Write-Host "sliver-server unpack failed ($($_.Exception.Message)), falling back to go run ./server unpack --force"
|
||||
}
|
||||
}
|
||||
|
||||
Invoke-ExternalCommand -FilePath "go" -Arguments @("run", "-tags=server,go_sqlite", "./server", "unpack", "--force")
|
||||
Assert-PinnedGarbleAssetUnpacked
|
||||
}
|
||||
|
||||
function Stop-TestProcesses {
|
||||
|
||||
@@ -84,6 +84,7 @@ func pingHandler(data []byte, resp RPCResponse) {
|
||||
resp(data, err)
|
||||
}
|
||||
|
||||
//garble:controlflow block_splits=2 junk_jumps=2 flatten_passes=1 flatten_hardening=xor trash_blocks=0
|
||||
func determineDirPathFilter(targetPath string) (string, string) {
|
||||
// The filter
|
||||
filter := ""
|
||||
|
||||
@@ -757,6 +757,8 @@ func wrapEnvelope(msgType uint32, message protoreflect.ProtoMessage) *sliverpb.E
|
||||
}
|
||||
|
||||
// registerSliver - Creates a registration protobuf message
|
||||
//
|
||||
//garble:controlflow block_splits=2 junk_jumps=2 flatten_passes=1 flatten_hardening=xor trash_blocks=0
|
||||
func registerSliver() *sliverpb.Register {
|
||||
hostname, err := os.Hostname()
|
||||
if err != nil {
|
||||
|
||||
@@ -91,7 +91,7 @@ func C2Generator(abort <-chan struct{}, temporaryC2 ...string) <-chan *url.URL {
|
||||
next = c2Servers[c2Counter%uint(len(c2Servers))]()
|
||||
}
|
||||
|
||||
// check if reconfig used to set a new C2-URI
|
||||
// check if reconfig used to set a new C2-URI
|
||||
if dynamic := GetC2URI(); dynamic != "" {
|
||||
next = dynamic
|
||||
}
|
||||
@@ -127,6 +127,8 @@ func C2Generator(abort <-chan struct{}, temporaryC2 ...string) <-chan *url.URL {
|
||||
}
|
||||
|
||||
// randomCCDomain - Random selection within a protocol
|
||||
//
|
||||
//garble:controlflow block_splits=2 junk_jumps=2 flatten_passes=1 flatten_hardening=xor trash_blocks=0
|
||||
func randomCCDomain(ccServers []func() string, next string) string {
|
||||
uri, err := url.Parse(next)
|
||||
if err != nil {
|
||||
|
||||
+425
-345
@@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc v7.34.1
|
||||
// protoc v7.35.1
|
||||
// source: clientpb/client.proto
|
||||
|
||||
package clientpb
|
||||
@@ -1331,6 +1331,54 @@ func (AIConversationEventType) EnumDescriptor() ([]byte, []int) {
|
||||
return file_clientpb_client_proto_rawDescGZIP(), []int{17}
|
||||
}
|
||||
|
||||
// ControlFlowPolicy selects a server-defined Garble control-flow obfuscation
|
||||
// policy. Policy parameters are versioned so existing profiles remain stable.
|
||||
type ControlFlowPolicy int32
|
||||
|
||||
const (
|
||||
ControlFlowPolicy_CONTROL_FLOW_DISABLED ControlFlowPolicy = 0
|
||||
ControlFlowPolicy_CONTROL_FLOW_BALANCED_V1 ControlFlowPolicy = 1
|
||||
)
|
||||
|
||||
// Enum value maps for ControlFlowPolicy.
|
||||
var (
|
||||
ControlFlowPolicy_name = map[int32]string{
|
||||
0: "CONTROL_FLOW_DISABLED",
|
||||
1: "CONTROL_FLOW_BALANCED_V1",
|
||||
}
|
||||
ControlFlowPolicy_value = map[string]int32{
|
||||
"CONTROL_FLOW_DISABLED": 0,
|
||||
"CONTROL_FLOW_BALANCED_V1": 1,
|
||||
}
|
||||
)
|
||||
|
||||
func (x ControlFlowPolicy) Enum() *ControlFlowPolicy {
|
||||
p := new(ControlFlowPolicy)
|
||||
*p = x
|
||||
return p
|
||||
}
|
||||
|
||||
func (x ControlFlowPolicy) String() string {
|
||||
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
|
||||
}
|
||||
|
||||
func (ControlFlowPolicy) Descriptor() protoreflect.EnumDescriptor {
|
||||
return file_clientpb_client_proto_enumTypes[18].Descriptor()
|
||||
}
|
||||
|
||||
func (ControlFlowPolicy) Type() protoreflect.EnumType {
|
||||
return &file_clientpb_client_proto_enumTypes[18]
|
||||
}
|
||||
|
||||
func (x ControlFlowPolicy) Number() protoreflect.EnumNumber {
|
||||
return protoreflect.EnumNumber(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use ControlFlowPolicy.Descriptor instead.
|
||||
func (ControlFlowPolicy) EnumDescriptor() ([]byte, []int) {
|
||||
return file_clientpb_client_proto_rawDescGZIP(), []int{18}
|
||||
}
|
||||
|
||||
type Version struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Major int32 `protobuf:"varint,1,opt,name=Major,proto3" json:"Major,omitempty"`
|
||||
@@ -2478,30 +2526,31 @@ type ImplantConfig struct {
|
||||
MaxConnectionErrors uint32 `protobuf:"varint,41,opt,name=MaxConnectionErrors,proto3" json:"MaxConnectionErrors,omitempty"`
|
||||
PollTimeout int64 `protobuf:"varint,42,opt,name=PollTimeout,proto3" json:"PollTimeout,omitempty"`
|
||||
// c2
|
||||
C2 []*ImplantC2 `protobuf:"bytes,50,rep,name=C2,proto3" json:"C2,omitempty"`
|
||||
CanaryDomains []string `protobuf:"bytes,51,rep,name=CanaryDomains,proto3" json:"CanaryDomains,omitempty"`
|
||||
ConnectionStrategy string `protobuf:"bytes,52,opt,name=ConnectionStrategy,proto3" json:"ConnectionStrategy,omitempty"`
|
||||
LimitDomainJoined bool `protobuf:"varint,60,opt,name=LimitDomainJoined,proto3" json:"LimitDomainJoined,omitempty"`
|
||||
LimitDatetime string `protobuf:"bytes,61,opt,name=LimitDatetime,proto3" json:"LimitDatetime,omitempty"`
|
||||
LimitHostname string `protobuf:"bytes,62,opt,name=LimitHostname,proto3" json:"LimitHostname,omitempty"`
|
||||
LimitUsername string `protobuf:"bytes,63,opt,name=LimitUsername,proto3" json:"LimitUsername,omitempty"`
|
||||
LimitFileExists string `protobuf:"bytes,64,opt,name=LimitFileExists,proto3" json:"LimitFileExists,omitempty"`
|
||||
LimitLocale string `protobuf:"bytes,65,opt,name=LimitLocale,proto3" json:"LimitLocale,omitempty"`
|
||||
Format OutputFormat `protobuf:"varint,100,opt,name=Format,proto3,enum=clientpb.OutputFormat" json:"Format,omitempty"`
|
||||
IsSharedLib bool `protobuf:"varint,101,opt,name=IsSharedLib,proto3" json:"IsSharedLib,omitempty"`
|
||||
IsService bool `protobuf:"varint,103,opt,name=IsService,proto3" json:"IsService,omitempty"`
|
||||
IsShellcode bool `protobuf:"varint,104,opt,name=IsShellcode,proto3" json:"IsShellcode,omitempty"`
|
||||
RunAtLoad bool `protobuf:"varint,105,opt,name=RunAtLoad,proto3" json:"RunAtLoad,omitempty"`
|
||||
DebugFile string `protobuf:"bytes,106,opt,name=DebugFile,proto3" json:"DebugFile,omitempty"`
|
||||
Exports []string `protobuf:"bytes,107,rep,name=exports,proto3" json:"exports,omitempty"`
|
||||
ShellcodeConfig *ShellcodeConfig `protobuf:"bytes,108,opt,name=ShellcodeConfig,proto3" json:"ShellcodeConfig,omitempty"`
|
||||
ShellcodeEncoder ShellcodeEncoder `protobuf:"varint,109,opt,name=ShellcodeEncoder,proto3,enum=clientpb.ShellcodeEncoder" json:"ShellcodeEncoder,omitempty"`
|
||||
HTTPC2ConfigName string `protobuf:"bytes,150,opt,name=HTTPC2ConfigName,proto3" json:"HTTPC2ConfigName,omitempty"`
|
||||
NetGoEnabled bool `protobuf:"varint,151,opt,name=NetGoEnabled,proto3" json:"NetGoEnabled,omitempty"`
|
||||
TrafficEncodersEnabled bool `protobuf:"varint,152,opt,name=TrafficEncodersEnabled,proto3" json:"TrafficEncodersEnabled,omitempty"`
|
||||
TrafficEncoders []string `protobuf:"bytes,153,rep,name=TrafficEncoders,proto3" json:"TrafficEncoders,omitempty"`
|
||||
Extension string `protobuf:"bytes,154,opt,name=Extension,proto3" json:"Extension,omitempty"`
|
||||
Assets []*commonpb.File `protobuf:"bytes,200,rep,name=Assets,proto3" json:"Assets,omitempty"`
|
||||
C2 []*ImplantC2 `protobuf:"bytes,50,rep,name=C2,proto3" json:"C2,omitempty"`
|
||||
CanaryDomains []string `protobuf:"bytes,51,rep,name=CanaryDomains,proto3" json:"CanaryDomains,omitempty"`
|
||||
ConnectionStrategy string `protobuf:"bytes,52,opt,name=ConnectionStrategy,proto3" json:"ConnectionStrategy,omitempty"`
|
||||
LimitDomainJoined bool `protobuf:"varint,60,opt,name=LimitDomainJoined,proto3" json:"LimitDomainJoined,omitempty"`
|
||||
LimitDatetime string `protobuf:"bytes,61,opt,name=LimitDatetime,proto3" json:"LimitDatetime,omitempty"`
|
||||
LimitHostname string `protobuf:"bytes,62,opt,name=LimitHostname,proto3" json:"LimitHostname,omitempty"`
|
||||
LimitUsername string `protobuf:"bytes,63,opt,name=LimitUsername,proto3" json:"LimitUsername,omitempty"`
|
||||
LimitFileExists string `protobuf:"bytes,64,opt,name=LimitFileExists,proto3" json:"LimitFileExists,omitempty"`
|
||||
LimitLocale string `protobuf:"bytes,65,opt,name=LimitLocale,proto3" json:"LimitLocale,omitempty"`
|
||||
Format OutputFormat `protobuf:"varint,100,opt,name=Format,proto3,enum=clientpb.OutputFormat" json:"Format,omitempty"`
|
||||
IsSharedLib bool `protobuf:"varint,101,opt,name=IsSharedLib,proto3" json:"IsSharedLib,omitempty"`
|
||||
IsService bool `protobuf:"varint,103,opt,name=IsService,proto3" json:"IsService,omitempty"`
|
||||
IsShellcode bool `protobuf:"varint,104,opt,name=IsShellcode,proto3" json:"IsShellcode,omitempty"`
|
||||
RunAtLoad bool `protobuf:"varint,105,opt,name=RunAtLoad,proto3" json:"RunAtLoad,omitempty"`
|
||||
DebugFile string `protobuf:"bytes,106,opt,name=DebugFile,proto3" json:"DebugFile,omitempty"`
|
||||
Exports []string `protobuf:"bytes,107,rep,name=exports,proto3" json:"exports,omitempty"`
|
||||
ShellcodeConfig *ShellcodeConfig `protobuf:"bytes,108,opt,name=ShellcodeConfig,proto3" json:"ShellcodeConfig,omitempty"`
|
||||
ShellcodeEncoder ShellcodeEncoder `protobuf:"varint,109,opt,name=ShellcodeEncoder,proto3,enum=clientpb.ShellcodeEncoder" json:"ShellcodeEncoder,omitempty"`
|
||||
HTTPC2ConfigName string `protobuf:"bytes,150,opt,name=HTTPC2ConfigName,proto3" json:"HTTPC2ConfigName,omitempty"`
|
||||
NetGoEnabled bool `protobuf:"varint,151,opt,name=NetGoEnabled,proto3" json:"NetGoEnabled,omitempty"`
|
||||
TrafficEncodersEnabled bool `protobuf:"varint,152,opt,name=TrafficEncodersEnabled,proto3" json:"TrafficEncodersEnabled,omitempty"`
|
||||
TrafficEncoders []string `protobuf:"bytes,153,rep,name=TrafficEncoders,proto3" json:"TrafficEncoders,omitempty"`
|
||||
Extension string `protobuf:"bytes,154,opt,name=Extension,proto3" json:"Extension,omitempty"`
|
||||
ControlFlow ControlFlowPolicy `protobuf:"varint,156,opt,name=ControlFlow,proto3,enum=clientpb.ControlFlowPolicy" json:"ControlFlow,omitempty"`
|
||||
Assets []*commonpb.File `protobuf:"bytes,200,rep,name=Assets,proto3" json:"Assets,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
@@ -2879,6 +2928,13 @@ func (x *ImplantConfig) GetExtension() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *ImplantConfig) GetControlFlow() ControlFlowPolicy {
|
||||
if x != nil {
|
||||
return x.ControlFlow
|
||||
}
|
||||
return ControlFlowPolicy_CONTROL_FLOW_DISABLED
|
||||
}
|
||||
|
||||
func (x *ImplantConfig) GetAssets() []*commonpb.File {
|
||||
if x != nil {
|
||||
return x.Assets
|
||||
@@ -4233,6 +4289,7 @@ type Compiler struct {
|
||||
Targets []*CompilerTarget `protobuf:"bytes,3,rep,name=Targets,proto3" json:"Targets,omitempty"`
|
||||
CrossCompilers []*CrossCompiler `protobuf:"bytes,4,rep,name=CrossCompilers,proto3" json:"CrossCompilers,omitempty"`
|
||||
UnsupportedTargets []*CompilerTarget `protobuf:"bytes,5,rep,name=UnsupportedTargets,proto3" json:"UnsupportedTargets,omitempty"`
|
||||
Capabilities []string `protobuf:"bytes,6,rep,name=Capabilities,proto3" json:"Capabilities,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
@@ -4302,6 +4359,13 @@ func (x *Compiler) GetUnsupportedTargets() []*CompilerTarget {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Compiler) GetCapabilities() []string {
|
||||
if x != nil {
|
||||
return x.Capabilities
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type DeleteReq struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Name string `protobuf:"bytes,1,opt,name=Name,proto3" json:"Name,omitempty"`
|
||||
@@ -8477,6 +8541,7 @@ type Builder struct {
|
||||
Templates []string `protobuf:"bytes,5,rep,name=Templates,proto3" json:"Templates,omitempty"`
|
||||
Targets []*CompilerTarget `protobuf:"bytes,6,rep,name=Targets,proto3" json:"Targets,omitempty"`
|
||||
CrossCompilers []*CrossCompiler `protobuf:"bytes,7,rep,name=CrossCompilers,proto3" json:"CrossCompilers,omitempty"`
|
||||
Capabilities []string `protobuf:"bytes,8,rep,name=Capabilities,proto3" json:"Capabilities,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
@@ -8560,6 +8625,13 @@ func (x *Builder) GetCrossCompilers() []*CrossCompiler {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Builder) GetCapabilities() []string {
|
||||
if x != nil {
|
||||
return x.Capabilities
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// [ HTTP C2 ] ----------------------------------------
|
||||
type HTTPC2Configs struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
@@ -13069,7 +13141,7 @@ const file_clientpb_client_proto_rawDesc = "" +
|
||||
"\aHeaders\x18\x06 \x01(\rR\aHeaders\x12\x16\n" +
|
||||
"\x06Thread\x18\a \x01(\bR\x06Thread\x12\x18\n" +
|
||||
"\aUnicode\x18\b \x01(\bR\aUnicode\x12\x10\n" +
|
||||
"\x03OEP\x18\t \x01(\rR\x03OEP\"\x8e\x0f\n" +
|
||||
"\x03OEP\x18\t \x01(\rR\x03OEP\"\xce\x0f\n" +
|
||||
"\rImplantConfig\x12\x0e\n" +
|
||||
"\x02ID\x18\x01 \x01(\tR\x02ID\x12<\n" +
|
||||
"\rImplantBuilds\x18\x02 \x03(\v2\x16.clientpb.ImplantBuildR\rImplantBuilds\x12*\n" +
|
||||
@@ -13126,7 +13198,8 @@ const file_clientpb_client_proto_rawDesc = "" +
|
||||
"\fNetGoEnabled\x18\x97\x01 \x01(\bR\fNetGoEnabled\x127\n" +
|
||||
"\x16TrafficEncodersEnabled\x18\x98\x01 \x01(\bR\x16TrafficEncodersEnabled\x12)\n" +
|
||||
"\x0fTrafficEncoders\x18\x99\x01 \x03(\tR\x0fTrafficEncoders\x12\x1d\n" +
|
||||
"\tExtension\x18\x9a\x01 \x01(\tR\tExtension\x12'\n" +
|
||||
"\tExtension\x18\x9a\x01 \x01(\tR\tExtension\x12>\n" +
|
||||
"\vControlFlow\x18\x9c\x01 \x01(\x0e2\x1b.clientpb.ControlFlowPolicyR\vControlFlow\x12'\n" +
|
||||
"\x06Assets\x18\xc8\x01 \x03(\v2\x0e.commonpb.FileR\x06AssetsJ\x06\b\x9b\x01\x10\x9c\x01R\tSpoofData\";\n" +
|
||||
"\x11SpoofMetadataFile\x12\x12\n" +
|
||||
"\x04Name\x18\x01 \x01(\tR\x04Name\x12\x12\n" +
|
||||
@@ -13255,13 +13328,14 @@ const file_clientpb_client_proto_rawDesc = "" +
|
||||
"TargetGOOS\x12\"\n" +
|
||||
"\fTargetGOARCH\x18\x02 \x01(\tR\fTargetGOARCH\x12\x16\n" +
|
||||
"\x06CCPath\x18\x03 \x01(\tR\x06CCPath\x12\x18\n" +
|
||||
"\aCXXPath\x18\x04 \x01(\tR\aCXXPath\"\xf5\x01\n" +
|
||||
"\aCXXPath\x18\x04 \x01(\tR\aCXXPath\"\x99\x02\n" +
|
||||
"\bCompiler\x12\x12\n" +
|
||||
"\x04GOOS\x18\x01 \x01(\tR\x04GOOS\x12\x16\n" +
|
||||
"\x06GOARCH\x18\x02 \x01(\tR\x06GOARCH\x122\n" +
|
||||
"\aTargets\x18\x03 \x03(\v2\x18.clientpb.CompilerTargetR\aTargets\x12?\n" +
|
||||
"\x0eCrossCompilers\x18\x04 \x03(\v2\x17.clientpb.CrossCompilerR\x0eCrossCompilers\x12H\n" +
|
||||
"\x12UnsupportedTargets\x18\x05 \x03(\v2\x18.clientpb.CompilerTargetR\x12UnsupportedTargets\"\x1f\n" +
|
||||
"\x12UnsupportedTargets\x18\x05 \x03(\v2\x18.clientpb.CompilerTargetR\x12UnsupportedTargets\x12\"\n" +
|
||||
"\fCapabilities\x18\x06 \x03(\tR\fCapabilities\"\x1f\n" +
|
||||
"\tDeleteReq\x12\x12\n" +
|
||||
"\x04Name\x18\x01 \x01(\tR\x04Name\"\xd7\x01\n" +
|
||||
"\tDNSCanary\x12\x0e\n" +
|
||||
@@ -13583,7 +13657,7 @@ const file_clientpb_client_proto_rawDesc = "" +
|
||||
"\vBuilderName\x18\x02 \x01(\tR\vBuilderName\x12\x12\n" +
|
||||
"\x04Name\x18\x03 \x01(\tR\x04Name\"9\n" +
|
||||
"\bBuilders\x12-\n" +
|
||||
"\bBuilders\x18\x01 \x03(\v2\x11.clientpb.BuilderR\bBuilders\"\x80\x02\n" +
|
||||
"\bBuilders\x18\x01 \x03(\v2\x11.clientpb.BuilderR\bBuilders\"\xa4\x02\n" +
|
||||
"\aBuilder\x12\x12\n" +
|
||||
"\x04Name\x18\x01 \x01(\tR\x04Name\x12\"\n" +
|
||||
"\fOperatorName\x18\x02 \x01(\tR\fOperatorName\x12\x12\n" +
|
||||
@@ -13591,7 +13665,8 @@ const file_clientpb_client_proto_rawDesc = "" +
|
||||
"\x06GOARCH\x18\x04 \x01(\tR\x06GOARCH\x12\x1c\n" +
|
||||
"\tTemplates\x18\x05 \x03(\tR\tTemplates\x122\n" +
|
||||
"\aTargets\x18\x06 \x03(\v2\x18.clientpb.CompilerTargetR\aTargets\x12?\n" +
|
||||
"\x0eCrossCompilers\x18\a \x03(\v2\x17.clientpb.CrossCompilerR\x0eCrossCompilers\"A\n" +
|
||||
"\x0eCrossCompilers\x18\a \x03(\v2\x17.clientpb.CrossCompilerR\x0eCrossCompilers\x12\"\n" +
|
||||
"\fCapabilities\x18\b \x03(\tR\fCapabilities\"A\n" +
|
||||
"\rHTTPC2Configs\x120\n" +
|
||||
"\aconfigs\x18\x01 \x03(\v2\x16.clientpb.HTTPC2ConfigR\aconfigs\"\"\n" +
|
||||
"\fC2ProfileReq\x12\x12\n" +
|
||||
@@ -14281,7 +14356,10 @@ const file_clientpb_client_proto_rawDesc = "" +
|
||||
"&AI_CONVERSATION_EVENT_TYPE_TURN_FAILED\x10\x05\x12.\n" +
|
||||
"*AI_CONVERSATION_EVENT_TYPE_MESSAGE_STARTED\x10\x06\x12.\n" +
|
||||
"*AI_CONVERSATION_EVENT_TYPE_MESSAGE_UPDATED\x10\a\x120\n" +
|
||||
",AI_CONVERSATION_EVENT_TYPE_MESSAGE_COMPLETED\x10\bB/Z-github.com/bishopfox/sliver/protobuf/clientpbb\x06proto3"
|
||||
",AI_CONVERSATION_EVENT_TYPE_MESSAGE_COMPLETED\x10\b*L\n" +
|
||||
"\x11ControlFlowPolicy\x12\x19\n" +
|
||||
"\x15CONTROL_FLOW_DISABLED\x10\x00\x12\x1c\n" +
|
||||
"\x18CONTROL_FLOW_BALANCED_V1\x10\x01B/Z-github.com/bishopfox/sliver/protobuf/clientpbb\x06proto3"
|
||||
|
||||
var (
|
||||
file_clientpb_client_proto_rawDescOnce sync.Once
|
||||
@@ -14295,7 +14373,7 @@ func file_clientpb_client_proto_rawDescGZIP() []byte {
|
||||
return file_clientpb_client_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_clientpb_client_proto_enumTypes = make([]protoimpl.EnumInfo, 18)
|
||||
var file_clientpb_client_proto_enumTypes = make([]protoimpl.EnumInfo, 19)
|
||||
var file_clientpb_client_proto_msgTypes = make([]protoimpl.MessageInfo, 158)
|
||||
var file_clientpb_client_proto_goTypes = []any{
|
||||
(OutputFormat)(0), // 0: clientpb.OutputFormat
|
||||
@@ -14316,321 +14394,323 @@ var file_clientpb_client_proto_goTypes = []any{
|
||||
(AIConversationMessageVisibility)(0), // 15: clientpb.AIConversationMessageVisibility
|
||||
(AIConversationMessageState)(0), // 16: clientpb.AIConversationMessageState
|
||||
(AIConversationEventType)(0), // 17: clientpb.AIConversationEventType
|
||||
(*Version)(nil), // 18: clientpb.Version
|
||||
(*ClientLogData)(nil), // 19: clientpb.ClientLogData
|
||||
(*Session)(nil), // 20: clientpb.Session
|
||||
(*Beacon)(nil), // 21: clientpb.Beacon
|
||||
(*Beacons)(nil), // 22: clientpb.Beacons
|
||||
(*BeaconTask)(nil), // 23: clientpb.BeaconTask
|
||||
(*BeaconTasks)(nil), // 24: clientpb.BeaconTasks
|
||||
(*BeaconIntegrity)(nil), // 25: clientpb.BeaconIntegrity
|
||||
(*ImplantC2)(nil), // 26: clientpb.ImplantC2
|
||||
(*ShellcodeConfig)(nil), // 27: clientpb.ShellcodeConfig
|
||||
(*ImplantConfig)(nil), // 28: clientpb.ImplantConfig
|
||||
(*SpoofMetadataFile)(nil), // 29: clientpb.SpoofMetadataFile
|
||||
(*IMAGE_RESOURCE_DIRECTORY)(nil), // 30: clientpb.IMAGE_RESOURCE_DIRECTORY
|
||||
(*IMAGE_RESOURCE_DIRECTORY_ENTRY)(nil), // 31: clientpb.IMAGE_RESOURCE_DIRECTORY_ENTRY
|
||||
(*IMAGE_RESOURCE_DATA_ENTRY)(nil), // 32: clientpb.IMAGE_RESOURCE_DATA_ENTRY
|
||||
(*IMAGE_EXPORT_DIRECTORY)(nil), // 33: clientpb.IMAGE_EXPORT_DIRECTORY
|
||||
(*PESpoofMetadataConfig)(nil), // 34: clientpb.PESpoofMetadataConfig
|
||||
(*SpoofMetadataConfig)(nil), // 35: clientpb.SpoofMetadataConfig
|
||||
(*TrafficEncoder)(nil), // 36: clientpb.TrafficEncoder
|
||||
(*TrafficEncoderMap)(nil), // 37: clientpb.TrafficEncoderMap
|
||||
(*TrafficEncoderTest)(nil), // 38: clientpb.TrafficEncoderTest
|
||||
(*TrafficEncoderTests)(nil), // 39: clientpb.TrafficEncoderTests
|
||||
(*ExternalImplantConfig)(nil), // 40: clientpb.ExternalImplantConfig
|
||||
(*ExternalImplantBinary)(nil), // 41: clientpb.ExternalImplantBinary
|
||||
(*ImplantBuilds)(nil), // 42: clientpb.ImplantBuilds
|
||||
(*ImplantStageReq)(nil), // 43: clientpb.ImplantStageReq
|
||||
(*ImplantBuild)(nil), // 44: clientpb.ImplantBuild
|
||||
(*CompilerTarget)(nil), // 45: clientpb.CompilerTarget
|
||||
(*CrossCompiler)(nil), // 46: clientpb.CrossCompiler
|
||||
(*Compiler)(nil), // 47: clientpb.Compiler
|
||||
(*DeleteReq)(nil), // 48: clientpb.DeleteReq
|
||||
(*DNSCanary)(nil), // 49: clientpb.DNSCanary
|
||||
(*Canaries)(nil), // 50: clientpb.Canaries
|
||||
(*UniqueWGIP)(nil), // 51: clientpb.UniqueWGIP
|
||||
(*ImplantProfile)(nil), // 52: clientpb.ImplantProfile
|
||||
(*ImplantProfiles)(nil), // 53: clientpb.ImplantProfiles
|
||||
(*RegenerateReq)(nil), // 54: clientpb.RegenerateReq
|
||||
(*Job)(nil), // 55: clientpb.Job
|
||||
(*Jobs)(nil), // 56: clientpb.Jobs
|
||||
(*KillJobReq)(nil), // 57: clientpb.KillJobReq
|
||||
(*RestartJobReq)(nil), // 58: clientpb.RestartJobReq
|
||||
(*KillJob)(nil), // 59: clientpb.KillJob
|
||||
(*ListenerJob)(nil), // 60: clientpb.ListenerJob
|
||||
(*MultiplayerListenerReq)(nil), // 61: clientpb.MultiplayerListenerReq
|
||||
(*MTLSListenerReq)(nil), // 62: clientpb.MTLSListenerReq
|
||||
(*WGListenerReq)(nil), // 63: clientpb.WGListenerReq
|
||||
(*DNSListenerReq)(nil), // 64: clientpb.DNSListenerReq
|
||||
(*HTTPListenerReq)(nil), // 65: clientpb.HTTPListenerReq
|
||||
(*NamedPipesReq)(nil), // 66: clientpb.NamedPipesReq
|
||||
(*NamedPipes)(nil), // 67: clientpb.NamedPipes
|
||||
(*TCPPivotReq)(nil), // 68: clientpb.TCPPivotReq
|
||||
(*TCPPivot)(nil), // 69: clientpb.TCPPivot
|
||||
(*Sessions)(nil), // 70: clientpb.Sessions
|
||||
(*RenameReq)(nil), // 71: clientpb.RenameReq
|
||||
(*GenerateReq)(nil), // 72: clientpb.GenerateReq
|
||||
(*GenerateStageReq)(nil), // 73: clientpb.GenerateStageReq
|
||||
(*Generate)(nil), // 74: clientpb.Generate
|
||||
(*GenerateSpoofMetadataReq)(nil), // 75: clientpb.GenerateSpoofMetadataReq
|
||||
(*MSFReq)(nil), // 76: clientpb.MSFReq
|
||||
(*MSFRemoteReq)(nil), // 77: clientpb.MSFRemoteReq
|
||||
(*StagerListenerReq)(nil), // 78: clientpb.StagerListenerReq
|
||||
(*StagerListener)(nil), // 79: clientpb.StagerListener
|
||||
(*ShellcodeRDIReq)(nil), // 80: clientpb.ShellcodeRDIReq
|
||||
(*ShellcodeRDI)(nil), // 81: clientpb.ShellcodeRDI
|
||||
(*GetSystemReq)(nil), // 82: clientpb.GetSystemReq
|
||||
(*MigrateReq)(nil), // 83: clientpb.MigrateReq
|
||||
(*CreateTunnelReq)(nil), // 84: clientpb.CreateTunnelReq
|
||||
(*CreateTunnel)(nil), // 85: clientpb.CreateTunnel
|
||||
(*CloseTunnelReq)(nil), // 86: clientpb.CloseTunnelReq
|
||||
(*PivotGraphEntry)(nil), // 87: clientpb.PivotGraphEntry
|
||||
(*PivotGraph)(nil), // 88: clientpb.PivotGraph
|
||||
(*Client)(nil), // 89: clientpb.Client
|
||||
(*Event)(nil), // 90: clientpb.Event
|
||||
(*Operators)(nil), // 91: clientpb.Operators
|
||||
(*Operator)(nil), // 92: clientpb.Operator
|
||||
(*WebContent)(nil), // 93: clientpb.WebContent
|
||||
(*WebsiteAddContent)(nil), // 94: clientpb.WebsiteAddContent
|
||||
(*WebsiteRemoveContent)(nil), // 95: clientpb.WebsiteRemoveContent
|
||||
(*Website)(nil), // 96: clientpb.Website
|
||||
(*Websites)(nil), // 97: clientpb.Websites
|
||||
(*WGClientConfig)(nil), // 98: clientpb.WGClientConfig
|
||||
(*Loot)(nil), // 99: clientpb.Loot
|
||||
(*AllLoot)(nil), // 100: clientpb.AllLoot
|
||||
(*IOC)(nil), // 101: clientpb.IOC
|
||||
(*ExtensionData)(nil), // 102: clientpb.ExtensionData
|
||||
(*Host)(nil), // 103: clientpb.Host
|
||||
(*AllHosts)(nil), // 104: clientpb.AllHosts
|
||||
(*DllHijackReq)(nil), // 105: clientpb.DllHijackReq
|
||||
(*DllHijack)(nil), // 106: clientpb.DllHijack
|
||||
(*BackdoorReq)(nil), // 107: clientpb.BackdoorReq
|
||||
(*Backdoor)(nil), // 108: clientpb.Backdoor
|
||||
(*ShellcodeEncodeReq)(nil), // 109: clientpb.ShellcodeEncodeReq
|
||||
(*ShellcodeEncode)(nil), // 110: clientpb.ShellcodeEncode
|
||||
(*ShellcodeEncoderArchMap)(nil), // 111: clientpb.ShellcodeEncoderArchMap
|
||||
(*ShellcodeEncoderMap)(nil), // 112: clientpb.ShellcodeEncoderMap
|
||||
(*ExternalGenerateReq)(nil), // 113: clientpb.ExternalGenerateReq
|
||||
(*Builders)(nil), // 114: clientpb.Builders
|
||||
(*Builder)(nil), // 115: clientpb.Builder
|
||||
(*HTTPC2Configs)(nil), // 116: clientpb.HTTPC2Configs
|
||||
(*C2ProfileReq)(nil), // 117: clientpb.C2ProfileReq
|
||||
(*HTTPC2ConfigReq)(nil), // 118: clientpb.HTTPC2ConfigReq
|
||||
(*HTTPC2Config)(nil), // 119: clientpb.HTTPC2Config
|
||||
(*HTTPC2ServerConfig)(nil), // 120: clientpb.HTTPC2ServerConfig
|
||||
(*HTTPC2ImplantConfig)(nil), // 121: clientpb.HTTPC2ImplantConfig
|
||||
(*HTTPC2Cookie)(nil), // 122: clientpb.HTTPC2Cookie
|
||||
(*HTTPC2Header)(nil), // 123: clientpb.HTTPC2Header
|
||||
(*HTTPC2URLParameter)(nil), // 124: clientpb.HTTPC2URLParameter
|
||||
(*HTTPC2PathSegment)(nil), // 125: clientpb.HTTPC2PathSegment
|
||||
(*Credential)(nil), // 126: clientpb.Credential
|
||||
(*Credentials)(nil), // 127: clientpb.Credentials
|
||||
(*Crackstations)(nil), // 128: clientpb.Crackstations
|
||||
(*CrackstationStatus)(nil), // 129: clientpb.CrackstationStatus
|
||||
(*CrackSyncStatus)(nil), // 130: clientpb.CrackSyncStatus
|
||||
(*CrackBenchmark)(nil), // 131: clientpb.CrackBenchmark
|
||||
(*CrackTask)(nil), // 132: clientpb.CrackTask
|
||||
(*Crackstation)(nil), // 133: clientpb.Crackstation
|
||||
(*CUDABackendInfo)(nil), // 134: clientpb.CUDABackendInfo
|
||||
(*OpenCLBackendInfo)(nil), // 135: clientpb.OpenCLBackendInfo
|
||||
(*MetalBackendInfo)(nil), // 136: clientpb.MetalBackendInfo
|
||||
(*CrackCommand)(nil), // 137: clientpb.CrackCommand
|
||||
(*CrackJob)(nil), // 138: clientpb.CrackJob
|
||||
(*CrackResponse)(nil), // 139: clientpb.CrackResponse
|
||||
(*CrackConfig)(nil), // 140: clientpb.CrackConfig
|
||||
(*CrackFiles)(nil), // 141: clientpb.CrackFiles
|
||||
(*CrackFile)(nil), // 142: clientpb.CrackFile
|
||||
(*CrackFileChunk)(nil), // 143: clientpb.CrackFileChunk
|
||||
(*AIProviderConfigs)(nil), // 144: clientpb.AIProviderConfigs
|
||||
(*AIProviderConfig)(nil), // 145: clientpb.AIProviderConfig
|
||||
(*AIConfigSummary)(nil), // 146: clientpb.AIConfigSummary
|
||||
(*AIConversationReq)(nil), // 147: clientpb.AIConversationReq
|
||||
(*AIConversations)(nil), // 148: clientpb.AIConversations
|
||||
(*AIContextWindowUsage)(nil), // 149: clientpb.AIContextWindowUsage
|
||||
(*AIConversation)(nil), // 150: clientpb.AIConversation
|
||||
(*AIConversationMessages)(nil), // 151: clientpb.AIConversationMessages
|
||||
(*AIConversationMessage)(nil), // 152: clientpb.AIConversationMessage
|
||||
(*AIConversationEvent)(nil), // 153: clientpb.AIConversationEvent
|
||||
(*MonitoringProviders)(nil), // 154: clientpb.MonitoringProviders
|
||||
(*MonitoringProvider)(nil), // 155: clientpb.MonitoringProvider
|
||||
(*ResourceID)(nil), // 156: clientpb.ResourceID
|
||||
(*CertificatesReq)(nil), // 157: clientpb.CertificatesReq
|
||||
(*CertificateData)(nil), // 158: clientpb.CertificateData
|
||||
(*CertificateInfo)(nil), // 159: clientpb.CertificateInfo
|
||||
(*CertificateAuthorityData)(nil), // 160: clientpb.CertificateAuthorityData
|
||||
(*CertificateAuthorityInfo)(nil), // 161: clientpb.CertificateAuthorityInfo
|
||||
nil, // 162: clientpb.TrafficEncoderMap.EncodersEntry
|
||||
nil, // 163: clientpb.ExternalImplantConfig.EncodersEntry
|
||||
nil, // 164: clientpb.ImplantBuilds.ConfigsEntry
|
||||
nil, // 165: clientpb.ImplantBuilds.ResourceIDsEntry
|
||||
nil, // 166: clientpb.ImplantBuilds.StagedEntry
|
||||
nil, // 167: clientpb.WebsiteAddContent.ContentsEntry
|
||||
nil, // 168: clientpb.Website.ContentsEntry
|
||||
nil, // 169: clientpb.Host.ExtensionDataEntry
|
||||
nil, // 170: clientpb.ShellcodeEncoderArchMap.EncodersEntry
|
||||
nil, // 171: clientpb.ShellcodeEncoderArchMap.DescriptionsEntry
|
||||
nil, // 172: clientpb.ShellcodeEncoderMap.EncodersEntry
|
||||
nil, // 173: clientpb.CrackSyncStatus.ProgressEntry
|
||||
nil, // 174: clientpb.CrackBenchmark.BenchmarksEntry
|
||||
nil, // 175: clientpb.Crackstation.BenchmarksEntry
|
||||
(*commonpb.File)(nil), // 176: commonpb.File
|
||||
(*commonpb.Request)(nil), // 177: commonpb.Request
|
||||
(*commonpb.Response)(nil), // 178: commonpb.Response
|
||||
(ControlFlowPolicy)(0), // 18: clientpb.ControlFlowPolicy
|
||||
(*Version)(nil), // 19: clientpb.Version
|
||||
(*ClientLogData)(nil), // 20: clientpb.ClientLogData
|
||||
(*Session)(nil), // 21: clientpb.Session
|
||||
(*Beacon)(nil), // 22: clientpb.Beacon
|
||||
(*Beacons)(nil), // 23: clientpb.Beacons
|
||||
(*BeaconTask)(nil), // 24: clientpb.BeaconTask
|
||||
(*BeaconTasks)(nil), // 25: clientpb.BeaconTasks
|
||||
(*BeaconIntegrity)(nil), // 26: clientpb.BeaconIntegrity
|
||||
(*ImplantC2)(nil), // 27: clientpb.ImplantC2
|
||||
(*ShellcodeConfig)(nil), // 28: clientpb.ShellcodeConfig
|
||||
(*ImplantConfig)(nil), // 29: clientpb.ImplantConfig
|
||||
(*SpoofMetadataFile)(nil), // 30: clientpb.SpoofMetadataFile
|
||||
(*IMAGE_RESOURCE_DIRECTORY)(nil), // 31: clientpb.IMAGE_RESOURCE_DIRECTORY
|
||||
(*IMAGE_RESOURCE_DIRECTORY_ENTRY)(nil), // 32: clientpb.IMAGE_RESOURCE_DIRECTORY_ENTRY
|
||||
(*IMAGE_RESOURCE_DATA_ENTRY)(nil), // 33: clientpb.IMAGE_RESOURCE_DATA_ENTRY
|
||||
(*IMAGE_EXPORT_DIRECTORY)(nil), // 34: clientpb.IMAGE_EXPORT_DIRECTORY
|
||||
(*PESpoofMetadataConfig)(nil), // 35: clientpb.PESpoofMetadataConfig
|
||||
(*SpoofMetadataConfig)(nil), // 36: clientpb.SpoofMetadataConfig
|
||||
(*TrafficEncoder)(nil), // 37: clientpb.TrafficEncoder
|
||||
(*TrafficEncoderMap)(nil), // 38: clientpb.TrafficEncoderMap
|
||||
(*TrafficEncoderTest)(nil), // 39: clientpb.TrafficEncoderTest
|
||||
(*TrafficEncoderTests)(nil), // 40: clientpb.TrafficEncoderTests
|
||||
(*ExternalImplantConfig)(nil), // 41: clientpb.ExternalImplantConfig
|
||||
(*ExternalImplantBinary)(nil), // 42: clientpb.ExternalImplantBinary
|
||||
(*ImplantBuilds)(nil), // 43: clientpb.ImplantBuilds
|
||||
(*ImplantStageReq)(nil), // 44: clientpb.ImplantStageReq
|
||||
(*ImplantBuild)(nil), // 45: clientpb.ImplantBuild
|
||||
(*CompilerTarget)(nil), // 46: clientpb.CompilerTarget
|
||||
(*CrossCompiler)(nil), // 47: clientpb.CrossCompiler
|
||||
(*Compiler)(nil), // 48: clientpb.Compiler
|
||||
(*DeleteReq)(nil), // 49: clientpb.DeleteReq
|
||||
(*DNSCanary)(nil), // 50: clientpb.DNSCanary
|
||||
(*Canaries)(nil), // 51: clientpb.Canaries
|
||||
(*UniqueWGIP)(nil), // 52: clientpb.UniqueWGIP
|
||||
(*ImplantProfile)(nil), // 53: clientpb.ImplantProfile
|
||||
(*ImplantProfiles)(nil), // 54: clientpb.ImplantProfiles
|
||||
(*RegenerateReq)(nil), // 55: clientpb.RegenerateReq
|
||||
(*Job)(nil), // 56: clientpb.Job
|
||||
(*Jobs)(nil), // 57: clientpb.Jobs
|
||||
(*KillJobReq)(nil), // 58: clientpb.KillJobReq
|
||||
(*RestartJobReq)(nil), // 59: clientpb.RestartJobReq
|
||||
(*KillJob)(nil), // 60: clientpb.KillJob
|
||||
(*ListenerJob)(nil), // 61: clientpb.ListenerJob
|
||||
(*MultiplayerListenerReq)(nil), // 62: clientpb.MultiplayerListenerReq
|
||||
(*MTLSListenerReq)(nil), // 63: clientpb.MTLSListenerReq
|
||||
(*WGListenerReq)(nil), // 64: clientpb.WGListenerReq
|
||||
(*DNSListenerReq)(nil), // 65: clientpb.DNSListenerReq
|
||||
(*HTTPListenerReq)(nil), // 66: clientpb.HTTPListenerReq
|
||||
(*NamedPipesReq)(nil), // 67: clientpb.NamedPipesReq
|
||||
(*NamedPipes)(nil), // 68: clientpb.NamedPipes
|
||||
(*TCPPivotReq)(nil), // 69: clientpb.TCPPivotReq
|
||||
(*TCPPivot)(nil), // 70: clientpb.TCPPivot
|
||||
(*Sessions)(nil), // 71: clientpb.Sessions
|
||||
(*RenameReq)(nil), // 72: clientpb.RenameReq
|
||||
(*GenerateReq)(nil), // 73: clientpb.GenerateReq
|
||||
(*GenerateStageReq)(nil), // 74: clientpb.GenerateStageReq
|
||||
(*Generate)(nil), // 75: clientpb.Generate
|
||||
(*GenerateSpoofMetadataReq)(nil), // 76: clientpb.GenerateSpoofMetadataReq
|
||||
(*MSFReq)(nil), // 77: clientpb.MSFReq
|
||||
(*MSFRemoteReq)(nil), // 78: clientpb.MSFRemoteReq
|
||||
(*StagerListenerReq)(nil), // 79: clientpb.StagerListenerReq
|
||||
(*StagerListener)(nil), // 80: clientpb.StagerListener
|
||||
(*ShellcodeRDIReq)(nil), // 81: clientpb.ShellcodeRDIReq
|
||||
(*ShellcodeRDI)(nil), // 82: clientpb.ShellcodeRDI
|
||||
(*GetSystemReq)(nil), // 83: clientpb.GetSystemReq
|
||||
(*MigrateReq)(nil), // 84: clientpb.MigrateReq
|
||||
(*CreateTunnelReq)(nil), // 85: clientpb.CreateTunnelReq
|
||||
(*CreateTunnel)(nil), // 86: clientpb.CreateTunnel
|
||||
(*CloseTunnelReq)(nil), // 87: clientpb.CloseTunnelReq
|
||||
(*PivotGraphEntry)(nil), // 88: clientpb.PivotGraphEntry
|
||||
(*PivotGraph)(nil), // 89: clientpb.PivotGraph
|
||||
(*Client)(nil), // 90: clientpb.Client
|
||||
(*Event)(nil), // 91: clientpb.Event
|
||||
(*Operators)(nil), // 92: clientpb.Operators
|
||||
(*Operator)(nil), // 93: clientpb.Operator
|
||||
(*WebContent)(nil), // 94: clientpb.WebContent
|
||||
(*WebsiteAddContent)(nil), // 95: clientpb.WebsiteAddContent
|
||||
(*WebsiteRemoveContent)(nil), // 96: clientpb.WebsiteRemoveContent
|
||||
(*Website)(nil), // 97: clientpb.Website
|
||||
(*Websites)(nil), // 98: clientpb.Websites
|
||||
(*WGClientConfig)(nil), // 99: clientpb.WGClientConfig
|
||||
(*Loot)(nil), // 100: clientpb.Loot
|
||||
(*AllLoot)(nil), // 101: clientpb.AllLoot
|
||||
(*IOC)(nil), // 102: clientpb.IOC
|
||||
(*ExtensionData)(nil), // 103: clientpb.ExtensionData
|
||||
(*Host)(nil), // 104: clientpb.Host
|
||||
(*AllHosts)(nil), // 105: clientpb.AllHosts
|
||||
(*DllHijackReq)(nil), // 106: clientpb.DllHijackReq
|
||||
(*DllHijack)(nil), // 107: clientpb.DllHijack
|
||||
(*BackdoorReq)(nil), // 108: clientpb.BackdoorReq
|
||||
(*Backdoor)(nil), // 109: clientpb.Backdoor
|
||||
(*ShellcodeEncodeReq)(nil), // 110: clientpb.ShellcodeEncodeReq
|
||||
(*ShellcodeEncode)(nil), // 111: clientpb.ShellcodeEncode
|
||||
(*ShellcodeEncoderArchMap)(nil), // 112: clientpb.ShellcodeEncoderArchMap
|
||||
(*ShellcodeEncoderMap)(nil), // 113: clientpb.ShellcodeEncoderMap
|
||||
(*ExternalGenerateReq)(nil), // 114: clientpb.ExternalGenerateReq
|
||||
(*Builders)(nil), // 115: clientpb.Builders
|
||||
(*Builder)(nil), // 116: clientpb.Builder
|
||||
(*HTTPC2Configs)(nil), // 117: clientpb.HTTPC2Configs
|
||||
(*C2ProfileReq)(nil), // 118: clientpb.C2ProfileReq
|
||||
(*HTTPC2ConfigReq)(nil), // 119: clientpb.HTTPC2ConfigReq
|
||||
(*HTTPC2Config)(nil), // 120: clientpb.HTTPC2Config
|
||||
(*HTTPC2ServerConfig)(nil), // 121: clientpb.HTTPC2ServerConfig
|
||||
(*HTTPC2ImplantConfig)(nil), // 122: clientpb.HTTPC2ImplantConfig
|
||||
(*HTTPC2Cookie)(nil), // 123: clientpb.HTTPC2Cookie
|
||||
(*HTTPC2Header)(nil), // 124: clientpb.HTTPC2Header
|
||||
(*HTTPC2URLParameter)(nil), // 125: clientpb.HTTPC2URLParameter
|
||||
(*HTTPC2PathSegment)(nil), // 126: clientpb.HTTPC2PathSegment
|
||||
(*Credential)(nil), // 127: clientpb.Credential
|
||||
(*Credentials)(nil), // 128: clientpb.Credentials
|
||||
(*Crackstations)(nil), // 129: clientpb.Crackstations
|
||||
(*CrackstationStatus)(nil), // 130: clientpb.CrackstationStatus
|
||||
(*CrackSyncStatus)(nil), // 131: clientpb.CrackSyncStatus
|
||||
(*CrackBenchmark)(nil), // 132: clientpb.CrackBenchmark
|
||||
(*CrackTask)(nil), // 133: clientpb.CrackTask
|
||||
(*Crackstation)(nil), // 134: clientpb.Crackstation
|
||||
(*CUDABackendInfo)(nil), // 135: clientpb.CUDABackendInfo
|
||||
(*OpenCLBackendInfo)(nil), // 136: clientpb.OpenCLBackendInfo
|
||||
(*MetalBackendInfo)(nil), // 137: clientpb.MetalBackendInfo
|
||||
(*CrackCommand)(nil), // 138: clientpb.CrackCommand
|
||||
(*CrackJob)(nil), // 139: clientpb.CrackJob
|
||||
(*CrackResponse)(nil), // 140: clientpb.CrackResponse
|
||||
(*CrackConfig)(nil), // 141: clientpb.CrackConfig
|
||||
(*CrackFiles)(nil), // 142: clientpb.CrackFiles
|
||||
(*CrackFile)(nil), // 143: clientpb.CrackFile
|
||||
(*CrackFileChunk)(nil), // 144: clientpb.CrackFileChunk
|
||||
(*AIProviderConfigs)(nil), // 145: clientpb.AIProviderConfigs
|
||||
(*AIProviderConfig)(nil), // 146: clientpb.AIProviderConfig
|
||||
(*AIConfigSummary)(nil), // 147: clientpb.AIConfigSummary
|
||||
(*AIConversationReq)(nil), // 148: clientpb.AIConversationReq
|
||||
(*AIConversations)(nil), // 149: clientpb.AIConversations
|
||||
(*AIContextWindowUsage)(nil), // 150: clientpb.AIContextWindowUsage
|
||||
(*AIConversation)(nil), // 151: clientpb.AIConversation
|
||||
(*AIConversationMessages)(nil), // 152: clientpb.AIConversationMessages
|
||||
(*AIConversationMessage)(nil), // 153: clientpb.AIConversationMessage
|
||||
(*AIConversationEvent)(nil), // 154: clientpb.AIConversationEvent
|
||||
(*MonitoringProviders)(nil), // 155: clientpb.MonitoringProviders
|
||||
(*MonitoringProvider)(nil), // 156: clientpb.MonitoringProvider
|
||||
(*ResourceID)(nil), // 157: clientpb.ResourceID
|
||||
(*CertificatesReq)(nil), // 158: clientpb.CertificatesReq
|
||||
(*CertificateData)(nil), // 159: clientpb.CertificateData
|
||||
(*CertificateInfo)(nil), // 160: clientpb.CertificateInfo
|
||||
(*CertificateAuthorityData)(nil), // 161: clientpb.CertificateAuthorityData
|
||||
(*CertificateAuthorityInfo)(nil), // 162: clientpb.CertificateAuthorityInfo
|
||||
nil, // 163: clientpb.TrafficEncoderMap.EncodersEntry
|
||||
nil, // 164: clientpb.ExternalImplantConfig.EncodersEntry
|
||||
nil, // 165: clientpb.ImplantBuilds.ConfigsEntry
|
||||
nil, // 166: clientpb.ImplantBuilds.ResourceIDsEntry
|
||||
nil, // 167: clientpb.ImplantBuilds.StagedEntry
|
||||
nil, // 168: clientpb.WebsiteAddContent.ContentsEntry
|
||||
nil, // 169: clientpb.Website.ContentsEntry
|
||||
nil, // 170: clientpb.Host.ExtensionDataEntry
|
||||
nil, // 171: clientpb.ShellcodeEncoderArchMap.EncodersEntry
|
||||
nil, // 172: clientpb.ShellcodeEncoderArchMap.DescriptionsEntry
|
||||
nil, // 173: clientpb.ShellcodeEncoderMap.EncodersEntry
|
||||
nil, // 174: clientpb.CrackSyncStatus.ProgressEntry
|
||||
nil, // 175: clientpb.CrackBenchmark.BenchmarksEntry
|
||||
nil, // 176: clientpb.Crackstation.BenchmarksEntry
|
||||
(*commonpb.File)(nil), // 177: commonpb.File
|
||||
(*commonpb.Request)(nil), // 178: commonpb.Request
|
||||
(*commonpb.Response)(nil), // 179: commonpb.Response
|
||||
}
|
||||
var file_clientpb_client_proto_depIdxs = []int32{
|
||||
21, // 0: clientpb.Beacons.Beacons:type_name -> clientpb.Beacon
|
||||
23, // 1: clientpb.BeaconTasks.Tasks:type_name -> clientpb.BeaconTask
|
||||
44, // 2: clientpb.ImplantConfig.ImplantBuilds:type_name -> clientpb.ImplantBuild
|
||||
26, // 3: clientpb.ImplantConfig.C2:type_name -> clientpb.ImplantC2
|
||||
22, // 0: clientpb.Beacons.Beacons:type_name -> clientpb.Beacon
|
||||
24, // 1: clientpb.BeaconTasks.Tasks:type_name -> clientpb.BeaconTask
|
||||
45, // 2: clientpb.ImplantConfig.ImplantBuilds:type_name -> clientpb.ImplantBuild
|
||||
27, // 3: clientpb.ImplantConfig.C2:type_name -> clientpb.ImplantC2
|
||||
0, // 4: clientpb.ImplantConfig.Format:type_name -> clientpb.OutputFormat
|
||||
27, // 5: clientpb.ImplantConfig.ShellcodeConfig:type_name -> clientpb.ShellcodeConfig
|
||||
28, // 5: clientpb.ImplantConfig.ShellcodeConfig:type_name -> clientpb.ShellcodeConfig
|
||||
3, // 6: clientpb.ImplantConfig.ShellcodeEncoder:type_name -> clientpb.ShellcodeEncoder
|
||||
176, // 7: clientpb.ImplantConfig.Assets:type_name -> commonpb.File
|
||||
29, // 8: clientpb.PESpoofMetadataConfig.Source:type_name -> clientpb.SpoofMetadataFile
|
||||
29, // 9: clientpb.PESpoofMetadataConfig.Icon:type_name -> clientpb.SpoofMetadataFile
|
||||
30, // 10: clientpb.PESpoofMetadataConfig.ResourceDirectory:type_name -> clientpb.IMAGE_RESOURCE_DIRECTORY
|
||||
31, // 11: clientpb.PESpoofMetadataConfig.ResourceDirectoryEntries:type_name -> clientpb.IMAGE_RESOURCE_DIRECTORY_ENTRY
|
||||
32, // 12: clientpb.PESpoofMetadataConfig.ResourceDataEntries:type_name -> clientpb.IMAGE_RESOURCE_DATA_ENTRY
|
||||
33, // 13: clientpb.PESpoofMetadataConfig.ExportDirectory:type_name -> clientpb.IMAGE_EXPORT_DIRECTORY
|
||||
34, // 14: clientpb.SpoofMetadataConfig.PE:type_name -> clientpb.PESpoofMetadataConfig
|
||||
176, // 15: clientpb.TrafficEncoder.Wasm:type_name -> commonpb.File
|
||||
162, // 16: clientpb.TrafficEncoderMap.Encoders:type_name -> clientpb.TrafficEncoderMap.EncodersEntry
|
||||
36, // 17: clientpb.TrafficEncoderTests.Encoder:type_name -> clientpb.TrafficEncoder
|
||||
38, // 18: clientpb.TrafficEncoderTests.Tests:type_name -> clientpb.TrafficEncoderTest
|
||||
28, // 19: clientpb.ExternalImplantConfig.Config:type_name -> clientpb.ImplantConfig
|
||||
44, // 20: clientpb.ExternalImplantConfig.Build:type_name -> clientpb.ImplantBuild
|
||||
119, // 21: clientpb.ExternalImplantConfig.HTTPC2:type_name -> clientpb.HTTPC2Config
|
||||
163, // 22: clientpb.ExternalImplantConfig.encoders:type_name -> clientpb.ExternalImplantConfig.EncodersEntry
|
||||
176, // 23: clientpb.ExternalImplantBinary.File:type_name -> commonpb.File
|
||||
164, // 24: clientpb.ImplantBuilds.Configs:type_name -> clientpb.ImplantBuilds.ConfigsEntry
|
||||
165, // 25: clientpb.ImplantBuilds.ResourceIDs:type_name -> clientpb.ImplantBuilds.ResourceIDsEntry
|
||||
166, // 26: clientpb.ImplantBuilds.staged:type_name -> clientpb.ImplantBuilds.StagedEntry
|
||||
0, // 27: clientpb.CompilerTarget.Format:type_name -> clientpb.OutputFormat
|
||||
45, // 28: clientpb.Compiler.Targets:type_name -> clientpb.CompilerTarget
|
||||
46, // 29: clientpb.Compiler.CrossCompilers:type_name -> clientpb.CrossCompiler
|
||||
45, // 30: clientpb.Compiler.UnsupportedTargets:type_name -> clientpb.CompilerTarget
|
||||
49, // 31: clientpb.Canaries.Canaries:type_name -> clientpb.DNSCanary
|
||||
28, // 32: clientpb.ImplantProfile.Config:type_name -> clientpb.ImplantConfig
|
||||
52, // 33: clientpb.ImplantProfiles.Profiles:type_name -> clientpb.ImplantProfile
|
||||
55, // 34: clientpb.Jobs.Active:type_name -> clientpb.Job
|
||||
62, // 35: clientpb.ListenerJob.MTLSConf:type_name -> clientpb.MTLSListenerReq
|
||||
63, // 36: clientpb.ListenerJob.WGConf:type_name -> clientpb.WGListenerReq
|
||||
64, // 37: clientpb.ListenerJob.DNSConf:type_name -> clientpb.DNSListenerReq
|
||||
65, // 38: clientpb.ListenerJob.HTTPConf:type_name -> clientpb.HTTPListenerReq
|
||||
61, // 39: clientpb.ListenerJob.MultiConf:type_name -> clientpb.MultiplayerListenerReq
|
||||
78, // 40: clientpb.ListenerJob.TCPConf:type_name -> clientpb.StagerListenerReq
|
||||
177, // 41: clientpb.NamedPipesReq.Request:type_name -> commonpb.Request
|
||||
178, // 42: clientpb.NamedPipes.Response:type_name -> commonpb.Response
|
||||
177, // 43: clientpb.TCPPivotReq.Request:type_name -> commonpb.Request
|
||||
178, // 44: clientpb.TCPPivot.Response:type_name -> commonpb.Response
|
||||
20, // 45: clientpb.Sessions.Sessions:type_name -> clientpb.Session
|
||||
28, // 46: clientpb.GenerateReq.Config:type_name -> clientpb.ImplantConfig
|
||||
176, // 47: clientpb.Generate.File:type_name -> commonpb.File
|
||||
35, // 48: clientpb.GenerateSpoofMetadataReq.SpoofMetadata:type_name -> clientpb.SpoofMetadataConfig
|
||||
177, // 49: clientpb.MSFReq.Request:type_name -> commonpb.Request
|
||||
177, // 50: clientpb.MSFRemoteReq.Request:type_name -> commonpb.Request
|
||||
1, // 51: clientpb.StagerListenerReq.Protocol:type_name -> clientpb.StageProtocol
|
||||
28, // 52: clientpb.GetSystemReq.Config:type_name -> clientpb.ImplantConfig
|
||||
177, // 53: clientpb.GetSystemReq.Request:type_name -> commonpb.Request
|
||||
28, // 54: clientpb.MigrateReq.Config:type_name -> clientpb.ImplantConfig
|
||||
3, // 55: clientpb.MigrateReq.Encoder:type_name -> clientpb.ShellcodeEncoder
|
||||
177, // 56: clientpb.MigrateReq.Request:type_name -> commonpb.Request
|
||||
177, // 57: clientpb.CreateTunnelReq.Request:type_name -> commonpb.Request
|
||||
177, // 58: clientpb.CloseTunnelReq.Request:type_name -> commonpb.Request
|
||||
20, // 59: clientpb.PivotGraphEntry.Session:type_name -> clientpb.Session
|
||||
87, // 60: clientpb.PivotGraphEntry.Children:type_name -> clientpb.PivotGraphEntry
|
||||
87, // 61: clientpb.PivotGraph.Children:type_name -> clientpb.PivotGraphEntry
|
||||
92, // 62: clientpb.Client.Operator:type_name -> clientpb.Operator
|
||||
20, // 63: clientpb.Event.Session:type_name -> clientpb.Session
|
||||
55, // 64: clientpb.Event.Job:type_name -> clientpb.Job
|
||||
89, // 65: clientpb.Event.Client:type_name -> clientpb.Client
|
||||
92, // 66: clientpb.Operators.Operators:type_name -> clientpb.Operator
|
||||
167, // 67: clientpb.WebsiteAddContent.Contents:type_name -> clientpb.WebsiteAddContent.ContentsEntry
|
||||
168, // 68: clientpb.Website.Contents:type_name -> clientpb.Website.ContentsEntry
|
||||
96, // 69: clientpb.Websites.Websites:type_name -> clientpb.Website
|
||||
2, // 70: clientpb.Loot.FileType:type_name -> clientpb.FileType
|
||||
176, // 71: clientpb.Loot.File:type_name -> commonpb.File
|
||||
99, // 72: clientpb.AllLoot.Loot:type_name -> clientpb.Loot
|
||||
101, // 73: clientpb.Host.IOCs:type_name -> clientpb.IOC
|
||||
169, // 74: clientpb.Host.ExtensionData:type_name -> clientpb.Host.ExtensionDataEntry
|
||||
103, // 75: clientpb.AllHosts.Hosts:type_name -> clientpb.Host
|
||||
177, // 76: clientpb.DllHijackReq.Request:type_name -> commonpb.Request
|
||||
178, // 77: clientpb.DllHijack.Response:type_name -> commonpb.Response
|
||||
177, // 78: clientpb.BackdoorReq.Request:type_name -> commonpb.Request
|
||||
178, // 79: clientpb.Backdoor.Response:type_name -> commonpb.Response
|
||||
3, // 80: clientpb.ShellcodeEncodeReq.Encoder:type_name -> clientpb.ShellcodeEncoder
|
||||
177, // 81: clientpb.ShellcodeEncodeReq.Request:type_name -> commonpb.Request
|
||||
178, // 82: clientpb.ShellcodeEncode.Response:type_name -> commonpb.Response
|
||||
170, // 83: clientpb.ShellcodeEncoderArchMap.Encoders:type_name -> clientpb.ShellcodeEncoderArchMap.EncodersEntry
|
||||
171, // 84: clientpb.ShellcodeEncoderArchMap.Descriptions:type_name -> clientpb.ShellcodeEncoderArchMap.DescriptionsEntry
|
||||
172, // 85: clientpb.ShellcodeEncoderMap.Encoders:type_name -> clientpb.ShellcodeEncoderMap.EncodersEntry
|
||||
28, // 86: clientpb.ExternalGenerateReq.Config:type_name -> clientpb.ImplantConfig
|
||||
115, // 87: clientpb.Builders.Builders:type_name -> clientpb.Builder
|
||||
45, // 88: clientpb.Builder.Targets:type_name -> clientpb.CompilerTarget
|
||||
46, // 89: clientpb.Builder.CrossCompilers:type_name -> clientpb.CrossCompiler
|
||||
119, // 90: clientpb.HTTPC2Configs.configs:type_name -> clientpb.HTTPC2Config
|
||||
119, // 91: clientpb.HTTPC2ConfigReq.C2Config:type_name -> clientpb.HTTPC2Config
|
||||
120, // 92: clientpb.HTTPC2Config.ServerConfig:type_name -> clientpb.HTTPC2ServerConfig
|
||||
121, // 93: clientpb.HTTPC2Config.ImplantConfig:type_name -> clientpb.HTTPC2ImplantConfig
|
||||
123, // 94: clientpb.HTTPC2ServerConfig.Headers:type_name -> clientpb.HTTPC2Header
|
||||
122, // 95: clientpb.HTTPC2ServerConfig.Cookies:type_name -> clientpb.HTTPC2Cookie
|
||||
124, // 96: clientpb.HTTPC2ImplantConfig.ExtraURLParameters:type_name -> clientpb.HTTPC2URLParameter
|
||||
123, // 97: clientpb.HTTPC2ImplantConfig.Headers:type_name -> clientpb.HTTPC2Header
|
||||
125, // 98: clientpb.HTTPC2ImplantConfig.PathSegments:type_name -> clientpb.HTTPC2PathSegment
|
||||
5, // 99: clientpb.Credential.HashType:type_name -> clientpb.HashType
|
||||
126, // 100: clientpb.Credentials.Credentials:type_name -> clientpb.Credential
|
||||
133, // 101: clientpb.Crackstations.Crackstations:type_name -> clientpb.Crackstation
|
||||
6, // 102: clientpb.CrackstationStatus.State:type_name -> clientpb.States
|
||||
130, // 103: clientpb.CrackstationStatus.Syncing:type_name -> clientpb.CrackSyncStatus
|
||||
173, // 104: clientpb.CrackSyncStatus.Progress:type_name -> clientpb.CrackSyncStatus.ProgressEntry
|
||||
174, // 105: clientpb.CrackBenchmark.Benchmarks:type_name -> clientpb.CrackBenchmark.BenchmarksEntry
|
||||
137, // 106: clientpb.CrackTask.Command:type_name -> clientpb.CrackCommand
|
||||
175, // 107: clientpb.Crackstation.Benchmarks:type_name -> clientpb.Crackstation.BenchmarksEntry
|
||||
134, // 108: clientpb.Crackstation.CUDA:type_name -> clientpb.CUDABackendInfo
|
||||
136, // 109: clientpb.Crackstation.Metal:type_name -> clientpb.MetalBackendInfo
|
||||
135, // 110: clientpb.Crackstation.OpenCL:type_name -> clientpb.OpenCLBackendInfo
|
||||
8, // 111: clientpb.CrackCommand.AttackMode:type_name -> clientpb.CrackAttackMode
|
||||
5, // 112: clientpb.CrackCommand.HashType:type_name -> clientpb.HashType
|
||||
10, // 113: clientpb.CrackCommand.OutfileFormat:type_name -> clientpb.CrackOutfileFormat
|
||||
9, // 114: clientpb.CrackCommand.EncodingFrom:type_name -> clientpb.CrackEncoding
|
||||
9, // 115: clientpb.CrackCommand.EncodingTo:type_name -> clientpb.CrackEncoding
|
||||
11, // 116: clientpb.CrackCommand.WorkloadProfile:type_name -> clientpb.CrackWorkloadProfile
|
||||
7, // 117: clientpb.CrackJob.Status:type_name -> clientpb.CrackJobStatus
|
||||
137, // 118: clientpb.CrackJob.Command:type_name -> clientpb.CrackCommand
|
||||
138, // 119: clientpb.CrackResponse.Job:type_name -> clientpb.CrackJob
|
||||
142, // 120: clientpb.CrackFiles.Files:type_name -> clientpb.CrackFile
|
||||
12, // 121: clientpb.CrackFile.Type:type_name -> clientpb.CrackFileType
|
||||
143, // 122: clientpb.CrackFile.Chunks:type_name -> clientpb.CrackFileChunk
|
||||
145, // 123: clientpb.AIProviderConfigs.Providers:type_name -> clientpb.AIProviderConfig
|
||||
146, // 124: clientpb.AIProviderConfigs.Config:type_name -> clientpb.AIConfigSummary
|
||||
150, // 125: clientpb.AIConversations.Conversations:type_name -> clientpb.AIConversation
|
||||
152, // 126: clientpb.AIConversation.Messages:type_name -> clientpb.AIConversationMessage
|
||||
13, // 127: clientpb.AIConversation.TurnState:type_name -> clientpb.AIConversationTurnState
|
||||
149, // 128: clientpb.AIConversation.ContextWindowUsage:type_name -> clientpb.AIContextWindowUsage
|
||||
152, // 129: clientpb.AIConversationMessages.Messages:type_name -> clientpb.AIConversationMessage
|
||||
14, // 130: clientpb.AIConversationMessage.Kind:type_name -> clientpb.AIConversationMessageKind
|
||||
15, // 131: clientpb.AIConversationMessage.Visibility:type_name -> clientpb.AIConversationMessageVisibility
|
||||
16, // 132: clientpb.AIConversationMessage.State:type_name -> clientpb.AIConversationMessageState
|
||||
17, // 133: clientpb.AIConversationEvent.EventType:type_name -> clientpb.AIConversationEventType
|
||||
150, // 134: clientpb.AIConversationEvent.Conversation:type_name -> clientpb.AIConversation
|
||||
152, // 135: clientpb.AIConversationEvent.Message:type_name -> clientpb.AIConversationMessage
|
||||
155, // 136: clientpb.MonitoringProviders.providers:type_name -> clientpb.MonitoringProvider
|
||||
158, // 137: clientpb.CertificateInfo.info:type_name -> clientpb.CertificateData
|
||||
160, // 138: clientpb.CertificateAuthorityInfo.info:type_name -> clientpb.CertificateAuthorityData
|
||||
36, // 139: clientpb.TrafficEncoderMap.EncodersEntry.value:type_name -> clientpb.TrafficEncoder
|
||||
28, // 140: clientpb.ImplantBuilds.ConfigsEntry.value:type_name -> clientpb.ImplantConfig
|
||||
156, // 141: clientpb.ImplantBuilds.ResourceIDsEntry.value:type_name -> clientpb.ResourceID
|
||||
93, // 142: clientpb.WebsiteAddContent.ContentsEntry.value:type_name -> clientpb.WebContent
|
||||
93, // 143: clientpb.Website.ContentsEntry.value:type_name -> clientpb.WebContent
|
||||
102, // 144: clientpb.Host.ExtensionDataEntry.value:type_name -> clientpb.ExtensionData
|
||||
3, // 145: clientpb.ShellcodeEncoderArchMap.EncodersEntry.value:type_name -> clientpb.ShellcodeEncoder
|
||||
111, // 146: clientpb.ShellcodeEncoderMap.EncodersEntry.value:type_name -> clientpb.ShellcodeEncoderArchMap
|
||||
147, // [147:147] is the sub-list for method output_type
|
||||
147, // [147:147] is the sub-list for method input_type
|
||||
147, // [147:147] is the sub-list for extension type_name
|
||||
147, // [147:147] is the sub-list for extension extendee
|
||||
0, // [0:147] is the sub-list for field type_name
|
||||
18, // 7: clientpb.ImplantConfig.ControlFlow:type_name -> clientpb.ControlFlowPolicy
|
||||
177, // 8: clientpb.ImplantConfig.Assets:type_name -> commonpb.File
|
||||
30, // 9: clientpb.PESpoofMetadataConfig.Source:type_name -> clientpb.SpoofMetadataFile
|
||||
30, // 10: clientpb.PESpoofMetadataConfig.Icon:type_name -> clientpb.SpoofMetadataFile
|
||||
31, // 11: clientpb.PESpoofMetadataConfig.ResourceDirectory:type_name -> clientpb.IMAGE_RESOURCE_DIRECTORY
|
||||
32, // 12: clientpb.PESpoofMetadataConfig.ResourceDirectoryEntries:type_name -> clientpb.IMAGE_RESOURCE_DIRECTORY_ENTRY
|
||||
33, // 13: clientpb.PESpoofMetadataConfig.ResourceDataEntries:type_name -> clientpb.IMAGE_RESOURCE_DATA_ENTRY
|
||||
34, // 14: clientpb.PESpoofMetadataConfig.ExportDirectory:type_name -> clientpb.IMAGE_EXPORT_DIRECTORY
|
||||
35, // 15: clientpb.SpoofMetadataConfig.PE:type_name -> clientpb.PESpoofMetadataConfig
|
||||
177, // 16: clientpb.TrafficEncoder.Wasm:type_name -> commonpb.File
|
||||
163, // 17: clientpb.TrafficEncoderMap.Encoders:type_name -> clientpb.TrafficEncoderMap.EncodersEntry
|
||||
37, // 18: clientpb.TrafficEncoderTests.Encoder:type_name -> clientpb.TrafficEncoder
|
||||
39, // 19: clientpb.TrafficEncoderTests.Tests:type_name -> clientpb.TrafficEncoderTest
|
||||
29, // 20: clientpb.ExternalImplantConfig.Config:type_name -> clientpb.ImplantConfig
|
||||
45, // 21: clientpb.ExternalImplantConfig.Build:type_name -> clientpb.ImplantBuild
|
||||
120, // 22: clientpb.ExternalImplantConfig.HTTPC2:type_name -> clientpb.HTTPC2Config
|
||||
164, // 23: clientpb.ExternalImplantConfig.encoders:type_name -> clientpb.ExternalImplantConfig.EncodersEntry
|
||||
177, // 24: clientpb.ExternalImplantBinary.File:type_name -> commonpb.File
|
||||
165, // 25: clientpb.ImplantBuilds.Configs:type_name -> clientpb.ImplantBuilds.ConfigsEntry
|
||||
166, // 26: clientpb.ImplantBuilds.ResourceIDs:type_name -> clientpb.ImplantBuilds.ResourceIDsEntry
|
||||
167, // 27: clientpb.ImplantBuilds.staged:type_name -> clientpb.ImplantBuilds.StagedEntry
|
||||
0, // 28: clientpb.CompilerTarget.Format:type_name -> clientpb.OutputFormat
|
||||
46, // 29: clientpb.Compiler.Targets:type_name -> clientpb.CompilerTarget
|
||||
47, // 30: clientpb.Compiler.CrossCompilers:type_name -> clientpb.CrossCompiler
|
||||
46, // 31: clientpb.Compiler.UnsupportedTargets:type_name -> clientpb.CompilerTarget
|
||||
50, // 32: clientpb.Canaries.Canaries:type_name -> clientpb.DNSCanary
|
||||
29, // 33: clientpb.ImplantProfile.Config:type_name -> clientpb.ImplantConfig
|
||||
53, // 34: clientpb.ImplantProfiles.Profiles:type_name -> clientpb.ImplantProfile
|
||||
56, // 35: clientpb.Jobs.Active:type_name -> clientpb.Job
|
||||
63, // 36: clientpb.ListenerJob.MTLSConf:type_name -> clientpb.MTLSListenerReq
|
||||
64, // 37: clientpb.ListenerJob.WGConf:type_name -> clientpb.WGListenerReq
|
||||
65, // 38: clientpb.ListenerJob.DNSConf:type_name -> clientpb.DNSListenerReq
|
||||
66, // 39: clientpb.ListenerJob.HTTPConf:type_name -> clientpb.HTTPListenerReq
|
||||
62, // 40: clientpb.ListenerJob.MultiConf:type_name -> clientpb.MultiplayerListenerReq
|
||||
79, // 41: clientpb.ListenerJob.TCPConf:type_name -> clientpb.StagerListenerReq
|
||||
178, // 42: clientpb.NamedPipesReq.Request:type_name -> commonpb.Request
|
||||
179, // 43: clientpb.NamedPipes.Response:type_name -> commonpb.Response
|
||||
178, // 44: clientpb.TCPPivotReq.Request:type_name -> commonpb.Request
|
||||
179, // 45: clientpb.TCPPivot.Response:type_name -> commonpb.Response
|
||||
21, // 46: clientpb.Sessions.Sessions:type_name -> clientpb.Session
|
||||
29, // 47: clientpb.GenerateReq.Config:type_name -> clientpb.ImplantConfig
|
||||
177, // 48: clientpb.Generate.File:type_name -> commonpb.File
|
||||
36, // 49: clientpb.GenerateSpoofMetadataReq.SpoofMetadata:type_name -> clientpb.SpoofMetadataConfig
|
||||
178, // 50: clientpb.MSFReq.Request:type_name -> commonpb.Request
|
||||
178, // 51: clientpb.MSFRemoteReq.Request:type_name -> commonpb.Request
|
||||
1, // 52: clientpb.StagerListenerReq.Protocol:type_name -> clientpb.StageProtocol
|
||||
29, // 53: clientpb.GetSystemReq.Config:type_name -> clientpb.ImplantConfig
|
||||
178, // 54: clientpb.GetSystemReq.Request:type_name -> commonpb.Request
|
||||
29, // 55: clientpb.MigrateReq.Config:type_name -> clientpb.ImplantConfig
|
||||
3, // 56: clientpb.MigrateReq.Encoder:type_name -> clientpb.ShellcodeEncoder
|
||||
178, // 57: clientpb.MigrateReq.Request:type_name -> commonpb.Request
|
||||
178, // 58: clientpb.CreateTunnelReq.Request:type_name -> commonpb.Request
|
||||
178, // 59: clientpb.CloseTunnelReq.Request:type_name -> commonpb.Request
|
||||
21, // 60: clientpb.PivotGraphEntry.Session:type_name -> clientpb.Session
|
||||
88, // 61: clientpb.PivotGraphEntry.Children:type_name -> clientpb.PivotGraphEntry
|
||||
88, // 62: clientpb.PivotGraph.Children:type_name -> clientpb.PivotGraphEntry
|
||||
93, // 63: clientpb.Client.Operator:type_name -> clientpb.Operator
|
||||
21, // 64: clientpb.Event.Session:type_name -> clientpb.Session
|
||||
56, // 65: clientpb.Event.Job:type_name -> clientpb.Job
|
||||
90, // 66: clientpb.Event.Client:type_name -> clientpb.Client
|
||||
93, // 67: clientpb.Operators.Operators:type_name -> clientpb.Operator
|
||||
168, // 68: clientpb.WebsiteAddContent.Contents:type_name -> clientpb.WebsiteAddContent.ContentsEntry
|
||||
169, // 69: clientpb.Website.Contents:type_name -> clientpb.Website.ContentsEntry
|
||||
97, // 70: clientpb.Websites.Websites:type_name -> clientpb.Website
|
||||
2, // 71: clientpb.Loot.FileType:type_name -> clientpb.FileType
|
||||
177, // 72: clientpb.Loot.File:type_name -> commonpb.File
|
||||
100, // 73: clientpb.AllLoot.Loot:type_name -> clientpb.Loot
|
||||
102, // 74: clientpb.Host.IOCs:type_name -> clientpb.IOC
|
||||
170, // 75: clientpb.Host.ExtensionData:type_name -> clientpb.Host.ExtensionDataEntry
|
||||
104, // 76: clientpb.AllHosts.Hosts:type_name -> clientpb.Host
|
||||
178, // 77: clientpb.DllHijackReq.Request:type_name -> commonpb.Request
|
||||
179, // 78: clientpb.DllHijack.Response:type_name -> commonpb.Response
|
||||
178, // 79: clientpb.BackdoorReq.Request:type_name -> commonpb.Request
|
||||
179, // 80: clientpb.Backdoor.Response:type_name -> commonpb.Response
|
||||
3, // 81: clientpb.ShellcodeEncodeReq.Encoder:type_name -> clientpb.ShellcodeEncoder
|
||||
178, // 82: clientpb.ShellcodeEncodeReq.Request:type_name -> commonpb.Request
|
||||
179, // 83: clientpb.ShellcodeEncode.Response:type_name -> commonpb.Response
|
||||
171, // 84: clientpb.ShellcodeEncoderArchMap.Encoders:type_name -> clientpb.ShellcodeEncoderArchMap.EncodersEntry
|
||||
172, // 85: clientpb.ShellcodeEncoderArchMap.Descriptions:type_name -> clientpb.ShellcodeEncoderArchMap.DescriptionsEntry
|
||||
173, // 86: clientpb.ShellcodeEncoderMap.Encoders:type_name -> clientpb.ShellcodeEncoderMap.EncodersEntry
|
||||
29, // 87: clientpb.ExternalGenerateReq.Config:type_name -> clientpb.ImplantConfig
|
||||
116, // 88: clientpb.Builders.Builders:type_name -> clientpb.Builder
|
||||
46, // 89: clientpb.Builder.Targets:type_name -> clientpb.CompilerTarget
|
||||
47, // 90: clientpb.Builder.CrossCompilers:type_name -> clientpb.CrossCompiler
|
||||
120, // 91: clientpb.HTTPC2Configs.configs:type_name -> clientpb.HTTPC2Config
|
||||
120, // 92: clientpb.HTTPC2ConfigReq.C2Config:type_name -> clientpb.HTTPC2Config
|
||||
121, // 93: clientpb.HTTPC2Config.ServerConfig:type_name -> clientpb.HTTPC2ServerConfig
|
||||
122, // 94: clientpb.HTTPC2Config.ImplantConfig:type_name -> clientpb.HTTPC2ImplantConfig
|
||||
124, // 95: clientpb.HTTPC2ServerConfig.Headers:type_name -> clientpb.HTTPC2Header
|
||||
123, // 96: clientpb.HTTPC2ServerConfig.Cookies:type_name -> clientpb.HTTPC2Cookie
|
||||
125, // 97: clientpb.HTTPC2ImplantConfig.ExtraURLParameters:type_name -> clientpb.HTTPC2URLParameter
|
||||
124, // 98: clientpb.HTTPC2ImplantConfig.Headers:type_name -> clientpb.HTTPC2Header
|
||||
126, // 99: clientpb.HTTPC2ImplantConfig.PathSegments:type_name -> clientpb.HTTPC2PathSegment
|
||||
5, // 100: clientpb.Credential.HashType:type_name -> clientpb.HashType
|
||||
127, // 101: clientpb.Credentials.Credentials:type_name -> clientpb.Credential
|
||||
134, // 102: clientpb.Crackstations.Crackstations:type_name -> clientpb.Crackstation
|
||||
6, // 103: clientpb.CrackstationStatus.State:type_name -> clientpb.States
|
||||
131, // 104: clientpb.CrackstationStatus.Syncing:type_name -> clientpb.CrackSyncStatus
|
||||
174, // 105: clientpb.CrackSyncStatus.Progress:type_name -> clientpb.CrackSyncStatus.ProgressEntry
|
||||
175, // 106: clientpb.CrackBenchmark.Benchmarks:type_name -> clientpb.CrackBenchmark.BenchmarksEntry
|
||||
138, // 107: clientpb.CrackTask.Command:type_name -> clientpb.CrackCommand
|
||||
176, // 108: clientpb.Crackstation.Benchmarks:type_name -> clientpb.Crackstation.BenchmarksEntry
|
||||
135, // 109: clientpb.Crackstation.CUDA:type_name -> clientpb.CUDABackendInfo
|
||||
137, // 110: clientpb.Crackstation.Metal:type_name -> clientpb.MetalBackendInfo
|
||||
136, // 111: clientpb.Crackstation.OpenCL:type_name -> clientpb.OpenCLBackendInfo
|
||||
8, // 112: clientpb.CrackCommand.AttackMode:type_name -> clientpb.CrackAttackMode
|
||||
5, // 113: clientpb.CrackCommand.HashType:type_name -> clientpb.HashType
|
||||
10, // 114: clientpb.CrackCommand.OutfileFormat:type_name -> clientpb.CrackOutfileFormat
|
||||
9, // 115: clientpb.CrackCommand.EncodingFrom:type_name -> clientpb.CrackEncoding
|
||||
9, // 116: clientpb.CrackCommand.EncodingTo:type_name -> clientpb.CrackEncoding
|
||||
11, // 117: clientpb.CrackCommand.WorkloadProfile:type_name -> clientpb.CrackWorkloadProfile
|
||||
7, // 118: clientpb.CrackJob.Status:type_name -> clientpb.CrackJobStatus
|
||||
138, // 119: clientpb.CrackJob.Command:type_name -> clientpb.CrackCommand
|
||||
139, // 120: clientpb.CrackResponse.Job:type_name -> clientpb.CrackJob
|
||||
143, // 121: clientpb.CrackFiles.Files:type_name -> clientpb.CrackFile
|
||||
12, // 122: clientpb.CrackFile.Type:type_name -> clientpb.CrackFileType
|
||||
144, // 123: clientpb.CrackFile.Chunks:type_name -> clientpb.CrackFileChunk
|
||||
146, // 124: clientpb.AIProviderConfigs.Providers:type_name -> clientpb.AIProviderConfig
|
||||
147, // 125: clientpb.AIProviderConfigs.Config:type_name -> clientpb.AIConfigSummary
|
||||
151, // 126: clientpb.AIConversations.Conversations:type_name -> clientpb.AIConversation
|
||||
153, // 127: clientpb.AIConversation.Messages:type_name -> clientpb.AIConversationMessage
|
||||
13, // 128: clientpb.AIConversation.TurnState:type_name -> clientpb.AIConversationTurnState
|
||||
150, // 129: clientpb.AIConversation.ContextWindowUsage:type_name -> clientpb.AIContextWindowUsage
|
||||
153, // 130: clientpb.AIConversationMessages.Messages:type_name -> clientpb.AIConversationMessage
|
||||
14, // 131: clientpb.AIConversationMessage.Kind:type_name -> clientpb.AIConversationMessageKind
|
||||
15, // 132: clientpb.AIConversationMessage.Visibility:type_name -> clientpb.AIConversationMessageVisibility
|
||||
16, // 133: clientpb.AIConversationMessage.State:type_name -> clientpb.AIConversationMessageState
|
||||
17, // 134: clientpb.AIConversationEvent.EventType:type_name -> clientpb.AIConversationEventType
|
||||
151, // 135: clientpb.AIConversationEvent.Conversation:type_name -> clientpb.AIConversation
|
||||
153, // 136: clientpb.AIConversationEvent.Message:type_name -> clientpb.AIConversationMessage
|
||||
156, // 137: clientpb.MonitoringProviders.providers:type_name -> clientpb.MonitoringProvider
|
||||
159, // 138: clientpb.CertificateInfo.info:type_name -> clientpb.CertificateData
|
||||
161, // 139: clientpb.CertificateAuthorityInfo.info:type_name -> clientpb.CertificateAuthorityData
|
||||
37, // 140: clientpb.TrafficEncoderMap.EncodersEntry.value:type_name -> clientpb.TrafficEncoder
|
||||
29, // 141: clientpb.ImplantBuilds.ConfigsEntry.value:type_name -> clientpb.ImplantConfig
|
||||
157, // 142: clientpb.ImplantBuilds.ResourceIDsEntry.value:type_name -> clientpb.ResourceID
|
||||
94, // 143: clientpb.WebsiteAddContent.ContentsEntry.value:type_name -> clientpb.WebContent
|
||||
94, // 144: clientpb.Website.ContentsEntry.value:type_name -> clientpb.WebContent
|
||||
103, // 145: clientpb.Host.ExtensionDataEntry.value:type_name -> clientpb.ExtensionData
|
||||
3, // 146: clientpb.ShellcodeEncoderArchMap.EncodersEntry.value:type_name -> clientpb.ShellcodeEncoder
|
||||
112, // 147: clientpb.ShellcodeEncoderMap.EncodersEntry.value:type_name -> clientpb.ShellcodeEncoderArchMap
|
||||
148, // [148:148] is the sub-list for method output_type
|
||||
148, // [148:148] is the sub-list for method input_type
|
||||
148, // [148:148] is the sub-list for extension type_name
|
||||
148, // [148:148] is the sub-list for extension extendee
|
||||
0, // [0:148] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_clientpb_client_proto_init() }
|
||||
@@ -14644,7 +14724,7 @@ func file_clientpb_client_proto_init() {
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_clientpb_client_proto_rawDesc), len(file_clientpb_client_proto_rawDesc)),
|
||||
NumEnums: 18,
|
||||
NumEnums: 19,
|
||||
NumMessages: 158,
|
||||
NumExtensions: 0,
|
||||
NumServices: 0,
|
||||
|
||||
@@ -220,6 +220,7 @@ message ImplantConfig {
|
||||
string Extension = 154;
|
||||
reserved 155;
|
||||
reserved "SpoofData";
|
||||
ControlFlowPolicy ControlFlow = 156;
|
||||
|
||||
repeated commonpb.File Assets = 200;
|
||||
}
|
||||
@@ -387,6 +388,7 @@ message Compiler {
|
||||
repeated CompilerTarget Targets = 3;
|
||||
repeated CrossCompiler CrossCompilers = 4;
|
||||
repeated CompilerTarget UnsupportedTargets = 5;
|
||||
repeated string Capabilities = 6;
|
||||
}
|
||||
|
||||
message DeleteReq { string Name = 1; }
|
||||
@@ -844,6 +846,7 @@ message Builder {
|
||||
repeated string Templates = 5;
|
||||
repeated CompilerTarget Targets = 6;
|
||||
repeated CrossCompiler CrossCompilers = 7;
|
||||
repeated string Capabilities = 8;
|
||||
|
||||
}
|
||||
|
||||
@@ -1476,6 +1479,13 @@ enum AIConversationEventType {
|
||||
AI_CONVERSATION_EVENT_TYPE_MESSAGE_COMPLETED = 8;
|
||||
}
|
||||
|
||||
// ControlFlowPolicy selects a server-defined Garble control-flow obfuscation
|
||||
// policy. Policy parameters are versioned so existing profiles remain stable.
|
||||
enum ControlFlowPolicy {
|
||||
CONTROL_FLOW_DISABLED = 0;
|
||||
CONTROL_FLOW_BALANCED_V1 = 1;
|
||||
}
|
||||
|
||||
message AIContextWindowUsage {
|
||||
int64 InputTokens = 1;
|
||||
int64 OutputTokens = 2;
|
||||
|
||||
+36
-8
@@ -24,10 +24,12 @@ import (
|
||||
"os/user"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/bishopfox/sliver/server/log"
|
||||
ver "github.com/bishopfox/sliver/server/version"
|
||||
utilAssets "github.com/bishopfox/sliver/util/assets"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -105,19 +107,26 @@ func assetVersion() string {
|
||||
return strings.TrimSpace(string(data))
|
||||
}
|
||||
|
||||
func saveAssetVersion(appDir string) {
|
||||
func expectedAssetVersion() string {
|
||||
garbleDigest, ok := utilAssets.ExpectedGarbleSHA256(runtime.GOOS, runtime.GOARCH)
|
||||
if !ok {
|
||||
return ver.GitCommit
|
||||
}
|
||||
return ver.GitCommit + ":garble-" + garbleDigest
|
||||
}
|
||||
|
||||
func saveAssetVersion(appDir, version string) error {
|
||||
versionFilePath := filepath.Join(appDir, versionFileName)
|
||||
fVer, _ := os.Create(versionFilePath)
|
||||
defer fVer.Close()
|
||||
fVer.Write([]byte(ver.GitCommit))
|
||||
return os.WriteFile(versionFilePath, []byte(version), 0600)
|
||||
}
|
||||
|
||||
// Setup - Extract or create local assets
|
||||
func Setup(force bool, echo bool) {
|
||||
appDir := GetRootAppDir()
|
||||
localVer := assetVersion()
|
||||
if force || localVer == "" || localVer != ver.GitCommit {
|
||||
setupLog.Infof("Version mismatch %v != %v", localVer, ver.GitCommit)
|
||||
expectedVer := expectedAssetVersion()
|
||||
if force || localVer == "" || localVer != expectedVer {
|
||||
setupLog.Infof("Version mismatch %v != %v", localVer, expectedVer)
|
||||
if echo {
|
||||
fmt.Printf(`
|
||||
Sliver Copyright (C) 2026 Bishop Fox
|
||||
@@ -126,13 +135,32 @@ This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type 'licenses' for details.`)
|
||||
fmt.Printf("\n\nUnpacking assets ...\n")
|
||||
}
|
||||
setupGo(appDir)
|
||||
setupSucceeded := true
|
||||
if err := setupGo(appDir); err != nil {
|
||||
setupLog.Errorf("Failed to setup Go: %s", err)
|
||||
setupSucceeded = false
|
||||
}
|
||||
err := setupZig(appDir)
|
||||
if err != nil {
|
||||
setupLog.Errorf("Failed to setup Zig: %s", err)
|
||||
}
|
||||
setupCodenames(appDir)
|
||||
saveAssetVersion(appDir)
|
||||
if setupSucceeded {
|
||||
garbleName := "garble"
|
||||
if runtime.GOOS == "windows" {
|
||||
garbleName += ".exe"
|
||||
}
|
||||
garblePath := filepath.Join(appDir, GoDirName, "bin", garbleName)
|
||||
if err := utilAssets.VerifyGarbleBinary(garblePath, runtime.GOOS, runtime.GOARCH); err != nil {
|
||||
setupLog.Errorf("Failed to verify extracted Garble: %s", err)
|
||||
setupSucceeded = false
|
||||
}
|
||||
}
|
||||
if setupSucceeded {
|
||||
if err := saveAssetVersion(appDir, expectedVer); err != nil {
|
||||
setupLog.Errorf("Failed to save asset version: %s", err)
|
||||
}
|
||||
}
|
||||
unpackDefaultTrafficEncoders(force)
|
||||
}
|
||||
setupLog.Infof("Initialized english encoder with %d words", len(English()))
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package assets
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
ver "github.com/bishopfox/sliver/server/version"
|
||||
utilAssets "github.com/bishopfox/sliver/util/assets"
|
||||
)
|
||||
|
||||
func TestExpectedAssetVersionIncludesGarbleManifest(t *testing.T) {
|
||||
digest, ok := utilAssets.ExpectedGarbleSHA256(runtime.GOOS, runtime.GOARCH)
|
||||
if !ok {
|
||||
t.Skipf("no pinned Garble artifact for %s/%s", runtime.GOOS, runtime.GOARCH)
|
||||
}
|
||||
|
||||
version := expectedAssetVersion()
|
||||
if !strings.HasPrefix(version, ver.GitCommit+":garble-") {
|
||||
t.Fatalf("expectedAssetVersion() = %q, want Git commit and Garble marker", version)
|
||||
}
|
||||
if !strings.HasSuffix(version, digest) {
|
||||
t.Fatalf("expectedAssetVersion() = %q, want pinned digest %q", version, digest)
|
||||
}
|
||||
}
|
||||
@@ -169,6 +169,17 @@ func (b *Builder) handleBuildEvent(event *clientpb.Event) {
|
||||
})
|
||||
return
|
||||
}
|
||||
if err := generate.ValidateControlFlowConfig(extConfig.Config); err != nil {
|
||||
builderLog.Warnf("Reject event, invalid control-flow config: %s", err)
|
||||
b.reportBuildFailure(implantBuildID, err)
|
||||
return
|
||||
}
|
||||
if generate.ControlFlowEnabled(extConfig.Config) && !generate.HasControlFlowCapability(b.externalBuilder.Capabilities) {
|
||||
err := fmt.Errorf("builder does not support %s", generate.ControlFlowCapability)
|
||||
builderLog.Warnf("Reject event: %s", err)
|
||||
b.reportBuildFailure(implantBuildID, err)
|
||||
return
|
||||
}
|
||||
|
||||
extModel := models.ImplantConfigFromProtobuf(extConfig.Config)
|
||||
|
||||
@@ -272,6 +283,16 @@ func (b *Builder) handleBuildEvent(event *clientpb.Event) {
|
||||
builderLog.Infof("All done, built and saved %s", fileName)
|
||||
}
|
||||
|
||||
func (b *Builder) reportBuildFailure(implantBuildID string, buildErr error) {
|
||||
_, err := b.rpc.BuilderTrigger(context.Background(), &clientpb.Event{
|
||||
EventType: consts.ExternalBuildFailedEvent,
|
||||
Data: []byte(fmt.Sprintf("%s:%s", implantBuildID, buildErr.Error())),
|
||||
})
|
||||
if err != nil {
|
||||
builderLog.Errorf("Failed to report build failure for %s: %s", implantBuildID, err)
|
||||
}
|
||||
}
|
||||
|
||||
func isSupportedTarget(targets []*clientpb.CompilerTarget, config *clientpb.ImplantConfig) bool {
|
||||
for _, target := range targets {
|
||||
if target.GOOS == config.GOOS && target.GOARCH == config.GOARCH {
|
||||
|
||||
@@ -266,7 +266,15 @@ func createBuilder(cmd *cobra.Command, configPath string, mutex *sync.Mutex) (*b
|
||||
}
|
||||
|
||||
func parseBuilderConfigFlags(cmd *cobra.Command) *clientpb.Builder {
|
||||
externalBuilder := &clientpb.Builder{GOOS: runtime.GOOS, GOARCH: runtime.GOARCH}
|
||||
externalBuilder := &clientpb.Builder{
|
||||
GOOS: runtime.GOOS,
|
||||
GOARCH: runtime.GOARCH,
|
||||
}
|
||||
if err := generate.ControlFlowRuntimeError(); err != nil {
|
||||
builderLog.Warnf("Not advertising %s: %s", generate.ControlFlowCapability, err)
|
||||
} else {
|
||||
externalBuilder.Capabilities = []string{generate.ControlFlowCapability}
|
||||
}
|
||||
|
||||
externalBuilder.CrossCompilers = generate.GetCrossCompilers()
|
||||
builderLog.Infof("Found %d cross-compilers", len(externalBuilder.CrossCompilers))
|
||||
|
||||
+114
-10
@@ -118,16 +118,19 @@ func ImplantBuilds() (*clientpb.ImplantBuilds, error) {
|
||||
Staged: map[string]bool{},
|
||||
}
|
||||
for _, dbBuild := range builds {
|
||||
resource, err := ResourceIDByName(dbBuild.Name)
|
||||
if errors.Is(err, ErrRecordNotFound) {
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
config, err := ImplantConfigByID(dbBuild.ImplantConfigID.String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pbBuilds.Configs[dbBuild.Name] = config
|
||||
|
||||
resource, err := ResourceIDByName(dbBuild.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pbBuilds.ResourceIDs[dbBuild.Name] = resource
|
||||
|
||||
pbBuilds.Staged[dbBuild.Name] = dbBuild.Stage
|
||||
@@ -138,20 +141,47 @@ func ImplantBuilds() (*clientpb.ImplantBuilds, error) {
|
||||
|
||||
// SaveImplantBuild
|
||||
func SaveImplantBuild(ib *clientpb.ImplantBuild) (*clientpb.ImplantBuild, error) {
|
||||
return saveImplantBuild(ib, nil)
|
||||
}
|
||||
|
||||
// SaveImplantBuildWithSourceProfile saves an implant build while retaining the
|
||||
// profile it was derived from. The build's ImplantConfigID still references an
|
||||
// immutable per-build config snapshot.
|
||||
func SaveImplantBuildWithSourceProfile(ib *clientpb.ImplantBuild, sourceProfileID string) (*clientpb.ImplantBuild, error) {
|
||||
profileID, err := models.ParseUUID(sourceProfileID)
|
||||
if err != nil || profileID == models.NilUUID() {
|
||||
return nil, fmt.Errorf("invalid source implant profile ID %q", sourceProfileID)
|
||||
}
|
||||
return saveImplantBuild(ib, &profileID)
|
||||
}
|
||||
|
||||
func saveImplantBuild(ib *clientpb.ImplantBuild, sourceProfileID *models.UUID) (*clientpb.ImplantBuild, error) {
|
||||
if ib == nil {
|
||||
return nil, errors.New("implant build cannot be nil")
|
||||
}
|
||||
dbSession := Session()
|
||||
var implantBuild *models.ImplantBuild
|
||||
if ib.ID != "" {
|
||||
_, err := ImplantBuildByID(ib.ID)
|
||||
if err != nil {
|
||||
existingBuild := &models.ImplantBuild{}
|
||||
buildID := models.ParseUUIDOrNil(ib.ID)
|
||||
if buildID == models.NilUUID() {
|
||||
return nil, ErrRecordNotFound
|
||||
}
|
||||
if err := dbSession.Where(&models.ImplantBuild{ID: buildID}).First(existingBuild).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
implantBuild = models.ImplantBuildFromProtobuf(ib)
|
||||
err = dbSession.Save(implantBuild).Error
|
||||
implantBuild.SourceImplantProfileID = existingBuild.SourceImplantProfileID
|
||||
if sourceProfileID != nil {
|
||||
implantBuild.SourceImplantProfileID = sourceProfileID
|
||||
}
|
||||
err := dbSession.Save(implantBuild).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
implantBuild = models.ImplantBuildFromProtobuf(ib)
|
||||
implantBuild.SourceImplantProfileID = sourceProfileID
|
||||
err := dbSession.Create(&implantBuild).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -185,6 +215,70 @@ func SaveImplantConfig(ic *clientpb.ImplantConfig) (*clientpb.ImplantConfig, err
|
||||
return implantConfig.ToProtobuf(), nil
|
||||
}
|
||||
|
||||
// CreateImplantConfigWithID creates a new implant config using a caller-chosen
|
||||
// UUID. This allows generated source to embed the final persistent config ID
|
||||
// before compilation without leaving an orphan record when compilation fails.
|
||||
func CreateImplantConfigWithID(ic *clientpb.ImplantConfig) (*clientpb.ImplantConfig, error) {
|
||||
if ic == nil {
|
||||
return nil, errors.New("implant config cannot be nil")
|
||||
}
|
||||
configID, err := models.ParseUUID(ic.ID)
|
||||
if err != nil || configID == models.NilUUID() {
|
||||
return nil, fmt.Errorf("invalid implant config ID %q", ic.ID)
|
||||
}
|
||||
|
||||
implantConfig := models.ImplantConfigFromProtobuf(ic)
|
||||
implantConfig.ID = configID
|
||||
if err := Session().Create(implantConfig).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return implantConfig.ToProtobuf(), nil
|
||||
}
|
||||
|
||||
// DeleteFailedImplantConfigSnapshot rolls back a newly-created, detached
|
||||
// config only when the immediately subsequent build save failed before any
|
||||
// ImplantBuild row could reference it. It must not be used for normal build or
|
||||
// profile deletion because active implants retain the config ID as origin
|
||||
// policy metadata.
|
||||
func DeleteFailedImplantConfigSnapshot(id string) error {
|
||||
configID := models.ParseUUIDOrNil(id)
|
||||
if configID == models.NilUUID() {
|
||||
return fmt.Errorf("invalid failed implant config snapshot ID %q", id)
|
||||
}
|
||||
|
||||
return Session().Transaction(func(tx *gorm.DB) error {
|
||||
config := &models.ImplantConfig{}
|
||||
if err := tx.Where(&models.ImplantConfig{ID: configID}).First(config).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
if config.ImplantProfileID != nil {
|
||||
return fmt.Errorf("refusing to delete profile implant config %q after build save failure", id)
|
||||
}
|
||||
|
||||
var buildCount int64
|
||||
if err := tx.Model(&models.ImplantBuild{}).Where("implant_config_id = ?", configID).Count(&buildCount).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if buildCount != 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, relation := range []interface{}{
|
||||
&models.ImplantC2{},
|
||||
&models.EncoderAsset{},
|
||||
&models.CanaryDomain{},
|
||||
} {
|
||||
if err := tx.Where("implant_config_id = ?", configID).Delete(relation).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return tx.Where("id = ? AND implant_profile_id IS NULL", configID).Delete(&models.ImplantConfig{}).Error
|
||||
})
|
||||
}
|
||||
|
||||
// ImplantBuildByName - Fetch implant build by name
|
||||
func ImplantBuildByName(name string) (*clientpb.ImplantBuild, error) {
|
||||
if len(name) < 1 {
|
||||
@@ -693,10 +787,20 @@ func ProfileByName(name string) (*clientpb.ImplantProfile, error) {
|
||||
return nil, ErrRecordNotFound
|
||||
}
|
||||
dbProfile := &models.ImplantProfile{}
|
||||
err := Session().Preload("ImplantConfig").Where(&models.ImplantProfile{Name: name}).Find(dbProfile).Error
|
||||
err := Session().Preload("ImplantConfig").Where(&models.ImplantProfile{Name: name}).First(dbProfile).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dbBuilds := []*models.ImplantBuild{}
|
||||
err = Session().Where(&models.ImplantBuild{ImplantConfigID: dbProfile.ImplantConfig.ID}).Find(&dbBuilds).Error
|
||||
err = Session().Where(
|
||||
"implant_config_id = ? OR source_implant_profile_id = ?",
|
||||
dbProfile.ImplantConfig.ID,
|
||||
dbProfile.ID,
|
||||
).Find(&dbBuilds).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
builds := []*clientpb.ImplantBuild{}
|
||||
for _, build := range dbBuilds {
|
||||
builds = append(builds, build.ToProtobuf())
|
||||
|
||||
@@ -55,7 +55,8 @@ type Beacon struct {
|
||||
Integrity string
|
||||
Capabilities uint64
|
||||
|
||||
ImplantBuildID UUID `gorm:"type:uuid;"`
|
||||
ImplantBuildID UUID `gorm:"type:uuid;"`
|
||||
ImplantConfigID UUID `gorm:"type:uuid;"`
|
||||
|
||||
Interval int64
|
||||
Jitter int64
|
||||
|
||||
@@ -52,7 +52,8 @@ type ImplantBuild struct {
|
||||
// Resource ID referencing build
|
||||
ImplantID uint64
|
||||
|
||||
ImplantConfigID UUID
|
||||
ImplantConfigID UUID
|
||||
SourceImplantProfileID *UUID
|
||||
|
||||
// ECC
|
||||
PeerPublicKey string
|
||||
@@ -159,6 +160,7 @@ type ImplantConfig struct {
|
||||
DebugFile string
|
||||
Evasion bool
|
||||
ObfuscateSymbols bool
|
||||
ControlFlow clientpb.ControlFlowPolicy
|
||||
ReconnectInterval int64
|
||||
PollTimeout int64
|
||||
MaxConnectionErrors uint32
|
||||
@@ -220,7 +222,9 @@ type ImplantConfig struct {
|
||||
|
||||
// BeforeCreate - GORM hook
|
||||
func (ic *ImplantConfig) BeforeCreate(tx *gorm.DB) (err error) {
|
||||
ic.ID = NewUUID()
|
||||
if ic.ID == NilUUID() {
|
||||
ic.ID = NewUUID()
|
||||
}
|
||||
ic.CreatedAt = time.Now()
|
||||
return nil
|
||||
}
|
||||
@@ -257,6 +261,7 @@ func (ic *ImplantConfig) ToProtobuf() *clientpb.ImplantConfig {
|
||||
DebugFile: ic.DebugFile,
|
||||
Evasion: ic.Evasion,
|
||||
ObfuscateSymbols: ic.ObfuscateSymbols,
|
||||
ControlFlow: ic.ControlFlow,
|
||||
TemplateName: ic.TemplateName,
|
||||
SGNEnabled: ic.SGNEnabled,
|
||||
ShellcodeEncoder: clientpb.ShellcodeEncoder(ic.ShellcodeEncoder),
|
||||
@@ -461,6 +466,7 @@ func ImplantConfigFromProtobuf(pbConfig *clientpb.ImplantConfig) *ImplantConfig
|
||||
cfg.Debug = pbConfig.Debug
|
||||
cfg.Evasion = pbConfig.Evasion
|
||||
cfg.ObfuscateSymbols = pbConfig.ObfuscateSymbols
|
||||
cfg.ControlFlow = pbConfig.ControlFlow
|
||||
cfg.TemplateName = pbConfig.TemplateName
|
||||
if cfg.TemplateName == "" {
|
||||
cfg.TemplateName = defaultTemplateName
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
)
|
||||
|
||||
func TestImplantConfigControlFlowRoundTrip(t *testing.T) {
|
||||
policies := []clientpb.ControlFlowPolicy{
|
||||
clientpb.ControlFlowPolicy_CONTROL_FLOW_DISABLED,
|
||||
clientpb.ControlFlowPolicy_CONTROL_FLOW_BALANCED_V1,
|
||||
}
|
||||
|
||||
for _, policy := range policies {
|
||||
t.Run(policy.String(), func(t *testing.T) {
|
||||
model := ImplantConfigFromProtobuf(&clientpb.ImplantConfig{ControlFlow: policy})
|
||||
if model.ControlFlow != policy {
|
||||
t.Fatalf("model ControlFlow = %v, want %v", model.ControlFlow, policy)
|
||||
}
|
||||
|
||||
got := model.ToProtobuf().ControlFlow
|
||||
if got != policy {
|
||||
t.Fatalf("protobuf ControlFlow = %v, want %v", got, policy)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestImplantConfigBeforeCreatePreservesAssignedID(t *testing.T) {
|
||||
assignedID := NewUUID()
|
||||
config := &ImplantConfig{ID: assignedID}
|
||||
if err := config.BeforeCreate(nil); err != nil {
|
||||
t.Fatalf("BeforeCreate() error = %v", err)
|
||||
}
|
||||
if config.ID != assignedID {
|
||||
t.Fatalf("BeforeCreate() ID = %s, want assigned ID %s", config.ID, assignedID)
|
||||
}
|
||||
}
|
||||
+75
-34
@@ -198,14 +198,29 @@ func appendToLDFlags(ldflags []string, extra string) []string {
|
||||
return []string{joined}
|
||||
}
|
||||
|
||||
const nonDebugImplantLDFlags = "-s -w -buildid="
|
||||
|
||||
func implantLDFlags(config *clientpb.ImplantConfig) []string {
|
||||
if config.Debug {
|
||||
return nil
|
||||
}
|
||||
|
||||
ldflags := []string{nonDebugImplantLDFlags}
|
||||
if config.GOOS == WINDOWS {
|
||||
ldflags = appendToLDFlags(ldflags, "-H=windowsgui")
|
||||
}
|
||||
return ldflags
|
||||
}
|
||||
|
||||
const linuxShellcodeBuildIDLDFlag = "-extldflags=-Wl,--build-id"
|
||||
|
||||
func applyLinuxShellcodeBuildID(ldflags []string) []string {
|
||||
// Garble removes Go's build ID. With Go 1.27, a native GNU c-shared link
|
||||
// then has neither PT_NOTE nor PT_PHDR, leaving Malasada one program header
|
||||
// short when it adds its entry stub, interpreter, and PT_PHDR. Ask the
|
||||
// external linker for its own build-id note so Malasada has a PT_NOTE to
|
||||
// repurpose without changing implant source or Malasada's conversion logic.
|
||||
// Release builds remove Go's build ID, and Garble does the same. With Go
|
||||
// 1.27, a native GNU c-shared link then has neither PT_NOTE nor PT_PHDR,
|
||||
// leaving Malasada one program header short when it adds its entry stub,
|
||||
// interpreter, and PT_PHDR. Ask the external linker for its own build-id note
|
||||
// so Malasada has a PT_NOTE to repurpose without changing implant source or
|
||||
// Malasada's conversion logic.
|
||||
return appendToLDFlags(ldflags, linuxShellcodeBuildIDLDFlag)
|
||||
}
|
||||
|
||||
@@ -374,8 +389,9 @@ func linuxShellcode(name string, build *clientpb.ImplantBuild, config *clientpb.
|
||||
HTTPPROXY: getGoHttpProxy(),
|
||||
HTTPSPROXY: getGoHttpsProxy(),
|
||||
|
||||
Obfuscation: config.ObfuscateSymbols,
|
||||
GOGARBLE: goGarble(config),
|
||||
Obfuscation: config.ObfuscateSymbols,
|
||||
ObfuscationControlFlow: controlFlowObfuscationEnabled(config),
|
||||
GOGARBLE: goGarble(config),
|
||||
}
|
||||
sanitizeZigForBuild(goConfig, "c-shared")
|
||||
buildLog.Infof(" CC: %s", goConfig.CC)
|
||||
@@ -399,7 +415,7 @@ func linuxShellcode(name string, build *clientpb.ImplantBuild, config *clientpb.
|
||||
if config.NetGoEnabled {
|
||||
tags = append(tags, "netgo")
|
||||
}
|
||||
ldflags := []string{""} // Garble will automatically add "-s -w -buildid="
|
||||
ldflags := implantLDFlags(config)
|
||||
ldflags = applyLinuxShellcodeBuildID(ldflags)
|
||||
ldflags, wantStaticSO := applyZigStaticLinking(goConfig, "c-shared", ldflags)
|
||||
gcFlags := ""
|
||||
@@ -464,8 +480,9 @@ func darwinShellcode(name string, build *clientpb.ImplantBuild, config *clientpb
|
||||
HTTPPROXY: getGoHttpProxy(),
|
||||
HTTPSPROXY: getGoHttpsProxy(),
|
||||
|
||||
Obfuscation: config.ObfuscateSymbols,
|
||||
GOGARBLE: goGarble(config),
|
||||
Obfuscation: config.ObfuscateSymbols,
|
||||
ObfuscationControlFlow: controlFlowObfuscationEnabled(config),
|
||||
GOGARBLE: goGarble(config),
|
||||
}
|
||||
|
||||
config.IsSharedLib = true
|
||||
@@ -486,7 +503,7 @@ func darwinShellcode(name string, build *clientpb.ImplantBuild, config *clientpb
|
||||
if config.NetGoEnabled {
|
||||
tags = append(tags, "netgo")
|
||||
}
|
||||
ldflags := []string{""} // Garble will automatically add "-s -w -buildid="
|
||||
ldflags := implantLDFlags(config)
|
||||
// Keep those for potential later use
|
||||
gcFlags := ""
|
||||
asmFlags := ""
|
||||
@@ -541,8 +558,9 @@ func windowsShellcode(name string, build *clientpb.ImplantBuild, config *clientp
|
||||
HTTPPROXY: getGoHttpProxy(),
|
||||
HTTPSPROXY: getGoHttpsProxy(),
|
||||
|
||||
Obfuscation: config.ObfuscateSymbols,
|
||||
GOGARBLE: goGarble(config),
|
||||
Obfuscation: config.ObfuscateSymbols,
|
||||
ObfuscationControlFlow: controlFlowObfuscationEnabled(config),
|
||||
GOGARBLE: goGarble(config),
|
||||
}
|
||||
pkgPath, err := renderSliverGoCode(name, build, config, goConfig, pbC2Implant)
|
||||
if err != nil {
|
||||
@@ -561,10 +579,7 @@ func windowsShellcode(name string, build *clientpb.ImplantBuild, config *clientp
|
||||
if config.NetGoEnabled {
|
||||
tags = append(tags, "netgo")
|
||||
}
|
||||
ldflags := []string{""} // Garble will automatically add "-s -w -buildid="
|
||||
if !config.Debug && goConfig.GOOS == WINDOWS {
|
||||
ldflags[0] += " -H=windowsgui"
|
||||
}
|
||||
ldflags := implantLDFlags(config)
|
||||
// Keep those for potential later use
|
||||
gcFlags := ""
|
||||
asmFlags := ""
|
||||
@@ -614,8 +629,9 @@ func SliverSharedLibrary(name string, build *clientpb.ImplantBuild, config *clie
|
||||
HTTPPROXY: getGoHttpProxy(),
|
||||
HTTPSPROXY: getGoHttpsProxy(),
|
||||
|
||||
Obfuscation: config.ObfuscateSymbols,
|
||||
GOGARBLE: goGarble(config),
|
||||
Obfuscation: config.ObfuscateSymbols,
|
||||
ObfuscationControlFlow: controlFlowObfuscationEnabled(config),
|
||||
GOGARBLE: goGarble(config),
|
||||
}
|
||||
sanitizeZigForBuild(goConfig, "c-shared")
|
||||
buildLog.Infof(" CC: %s", goConfig.CC)
|
||||
@@ -641,11 +657,8 @@ func SliverSharedLibrary(name string, build *clientpb.ImplantBuild, config *clie
|
||||
if config.NetGoEnabled {
|
||||
tags = append(tags, "netgo")
|
||||
}
|
||||
ldflags := []string{""} // Garble will automatically add "-s -w -buildid="
|
||||
ldflags := implantLDFlags(config)
|
||||
ldflags, wantStaticSO := applyZigStaticLinking(goConfig, "c-shared", ldflags)
|
||||
if !config.Debug && goConfig.GOOS == WINDOWS {
|
||||
ldflags[0] += " -H=windowsgui"
|
||||
}
|
||||
|
||||
// Keep those for potential later use
|
||||
gcFlags := ""
|
||||
@@ -722,8 +735,9 @@ func SliverArchive(name string, build *clientpb.ImplantBuild, config *clientpb.I
|
||||
HTTPPROXY: getGoHttpProxy(),
|
||||
HTTPSPROXY: getGoHttpsProxy(),
|
||||
|
||||
Obfuscation: config.ObfuscateSymbols,
|
||||
GOGARBLE: goGarble(config),
|
||||
Obfuscation: config.ObfuscateSymbols,
|
||||
ObfuscationControlFlow: controlFlowObfuscationEnabled(config),
|
||||
GOGARBLE: goGarble(config),
|
||||
}
|
||||
sanitizeZigForBuild(goConfig, "c-archive")
|
||||
buildLog.Infof(" CC: %s", goConfig.CC)
|
||||
@@ -745,7 +759,7 @@ func SliverArchive(name string, build *clientpb.ImplantBuild, config *clientpb.I
|
||||
if config.NetGoEnabled {
|
||||
tags = append(tags, "netgo")
|
||||
}
|
||||
ldflags := []string{""} // Garble will automatically add "-s -w -buildid="
|
||||
ldflags := implantLDFlags(config)
|
||||
|
||||
// Keep those for potential later use
|
||||
gcFlags := ""
|
||||
@@ -784,8 +798,9 @@ func SliverExecutable(name string, build *clientpb.ImplantBuild, config *clientp
|
||||
HTTPPROXY: getGoHttpProxy(),
|
||||
HTTPSPROXY: getGoHttpsProxy(),
|
||||
|
||||
Obfuscation: config.ObfuscateSymbols,
|
||||
GOGARBLE: goGarble(config),
|
||||
Obfuscation: config.ObfuscateSymbols,
|
||||
ObfuscationControlFlow: controlFlowObfuscationEnabled(config),
|
||||
GOGARBLE: goGarble(config),
|
||||
}
|
||||
|
||||
pkgPath, err := renderSliverGoCode(name, build, config, goConfig, pbC2Implant)
|
||||
@@ -801,15 +816,11 @@ func SliverExecutable(name string, build *clientpb.ImplantBuild, config *clientp
|
||||
if config.NetGoEnabled {
|
||||
tags = append(tags, "netgo")
|
||||
}
|
||||
ldflags := []string{""} // Garble will automatically add "-s -w -buildid="
|
||||
if !config.Debug && goConfig.GOOS == WINDOWS {
|
||||
ldflags[0] += " -H=windowsgui"
|
||||
}
|
||||
ldflags := implantLDFlags(config)
|
||||
gcFlags := ""
|
||||
asmFlags := ""
|
||||
if config.Debug {
|
||||
gcFlags = "all=-N -l"
|
||||
ldflags = []string{}
|
||||
}
|
||||
_, err = gogo.GoBuild(*goConfig, pkgPath, dest, "", tags, ldflags, gcFlags, asmFlags)
|
||||
if err != nil {
|
||||
@@ -821,6 +832,12 @@ func SliverExecutable(name string, build *clientpb.ImplantBuild, config *clientp
|
||||
|
||||
// This function is a little too long, we should probably refactor it as some point
|
||||
func renderSliverGoCode(name string, build *clientpb.ImplantBuild, config *clientpb.ImplantConfig, goConfig *gogo.GoConfig, pbC2Implant *clientpb.HTTPC2ImplantConfig) (string, error) {
|
||||
controlFlowPolicy, err := controlFlowPolicyForConfig(config)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
controlFlowFunctionsRendered := map[string]struct{}{}
|
||||
|
||||
target := fmt.Sprintf("%s/%s", config.GOOS, config.GOARCH)
|
||||
if _, ok := gogo.ValidCompilerTargets(*goConfig)[target]; !ok {
|
||||
return "", fmt.Errorf("invalid compiler target: %s", target)
|
||||
@@ -846,7 +863,7 @@ func renderSliverGoCode(name string, build *clientpb.ImplantBuild, config *clien
|
||||
// srcDir - ~/.sliver/slivers/<os>/<arch>/<name>/src
|
||||
srcDir := filepath.Join(projectGoPathDir, "src")
|
||||
assets.SetupGoPath(srcDir, config.IncludeDNS) // Extract GOPATH dependency files
|
||||
err := util.ChmodR(srcDir, 0600, 0700) // Ensures src code files are writable
|
||||
err = util.ChmodR(srcDir, 0600, 0700) // Ensures src code files are writable
|
||||
if err != nil {
|
||||
buildLog.Errorf("fs perms: %v", err)
|
||||
return "", err
|
||||
@@ -953,6 +970,18 @@ func renderSliverGoCode(name string, build *clientpb.ImplantBuild, config *clien
|
||||
buildLog.Errorf("Template execution error %s", err)
|
||||
return err
|
||||
}
|
||||
if controlFlowPolicy != nil && path.Ext(f.Name()) == ".go" {
|
||||
functionIDs, err := controlFlowFunctionsInSource(controlFlowPolicy, fsPath, buf.Bytes())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, functionID := range functionIDs {
|
||||
if _, duplicate := controlFlowFunctionsRendered[functionID]; duplicate {
|
||||
return fmt.Errorf("control-flow directive for %s rendered more than once", functionID)
|
||||
}
|
||||
controlFlowFunctionsRendered[functionID] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
// Render canaries
|
||||
if len(config.CanaryDomains) > 0 {
|
||||
@@ -979,6 +1008,14 @@ func renderSliverGoCode(name string, build *clientpb.ImplantBuild, config *clien
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if controlFlowPolicy != nil && len(controlFlowFunctionsRendered) != len(controlFlowPolicy.functions) {
|
||||
return "", fmt.Errorf(
|
||||
"control-flow policy %s rendered %d allowlisted functions, expected %d",
|
||||
config.ControlFlow,
|
||||
len(controlFlowFunctionsRendered),
|
||||
len(controlFlowPolicy.functions),
|
||||
)
|
||||
}
|
||||
|
||||
// Render encoder assets
|
||||
renderNativeEncoderAssets(build, config, sliverPkgDir)
|
||||
@@ -1142,6 +1179,10 @@ func renderImplantEnglish() []string {
|
||||
|
||||
// GenerateConfig - Generate the keys/etc for the implant
|
||||
func GenerateConfig(name string, implantConfig *clientpb.ImplantConfig) (*clientpb.ImplantBuild, error) {
|
||||
if err := ValidateControlFlowConfig(implantConfig); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var err error
|
||||
|
||||
// Cert PEM encoded certificates
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -225,6 +226,44 @@ func TestSymbolObfuscation(t *testing.T) {
|
||||
symbolObfuscation(t, "freebsd", "amd64")
|
||||
}
|
||||
|
||||
func TestControlFlowObfuscation(t *testing.T) {
|
||||
target := runtime.GOOS + "/" + runtime.GOARCH
|
||||
if _, supported := SupportedCompilerTargets[target]; !supported {
|
||||
t.Skipf("control-flow generator regression is unsupported on %s", target)
|
||||
}
|
||||
|
||||
name := fmt.Sprintf("control_flow_test%d", nonce)
|
||||
nonce++
|
||||
config := &clientpb.ImplantConfig{
|
||||
GOOS: runtime.GOOS,
|
||||
GOARCH: runtime.GOARCH,
|
||||
TemplateName: SliverTemplateName,
|
||||
ObfuscateSymbols: true,
|
||||
ControlFlow: clientpb.ControlFlowPolicy_CONTROL_FLOW_BALANCED_V1,
|
||||
Format: clientpb.OutputFormat_EXECUTABLE,
|
||||
IncludeMTLS: true,
|
||||
ConnectionStrategy: "s",
|
||||
C2: []*clientpb.ImplantC2{
|
||||
{URL: "mtls://127.0.0.1:31337"},
|
||||
},
|
||||
}
|
||||
build, err := GenerateConfig(name, config)
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateConfig() error = %v", err)
|
||||
}
|
||||
httpC2Config := configs.GenerateDefaultHTTPC2Config()
|
||||
binPath, err := SliverExecutable(name, build, config, httpC2Config.ImplantConfig)
|
||||
if err != nil {
|
||||
t.Fatalf("build control-flow Sliver for %s: %v", target, err)
|
||||
}
|
||||
defer cleanupGeneratedArtifacts(t, binPath)
|
||||
if stat, err := os.Stat(binPath); err != nil {
|
||||
t.Fatalf("stat generated control-flow Sliver: %v", err)
|
||||
} else if stat.Size() == 0 {
|
||||
t.Fatal("generated control-flow Sliver is empty")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTrafficEncoders(t *testing.T) {
|
||||
// Supported platforms
|
||||
trafficEncodersExecutable(t, "windows", "amd64")
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package generate
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/gofrs/uuid"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
// NewImplantConfigSnapshot creates the immutable configuration that a build
|
||||
// embeds and references. The source profile ID is returned separately so build
|
||||
// listing and profile deletion can retain their existing relationship without
|
||||
// making the snapshot itself part of the mutable profile.
|
||||
func NewImplantConfigSnapshot(config *clientpb.ImplantConfig) (*clientpb.ImplantConfig, string, error) {
|
||||
if config == nil {
|
||||
return nil, "", errors.New("implant config cannot be nil")
|
||||
}
|
||||
|
||||
snapshot, ok := proto.Clone(config).(*clientpb.ImplantConfig)
|
||||
if !ok {
|
||||
return nil, "", errors.New("failed to clone implant config")
|
||||
}
|
||||
configID, err := uuid.NewV4()
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
sourceProfileID := snapshot.ImplantProfileID
|
||||
if sourceProfileID != "" {
|
||||
profileID, err := uuid.FromString(sourceProfileID)
|
||||
if err != nil || profileID == uuid.Nil {
|
||||
return nil, "", errors.New("invalid source implant profile ID")
|
||||
}
|
||||
sourceProfileID = profileID.String()
|
||||
}
|
||||
snapshot.ID = configID.String()
|
||||
snapshot.ImplantProfileID = ""
|
||||
snapshot.ImplantBuilds = nil
|
||||
for _, c2 := range snapshot.C2 {
|
||||
if c2 != nil {
|
||||
c2.ID = ""
|
||||
}
|
||||
}
|
||||
return snapshot, sourceProfileID, nil
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
//go:build server
|
||||
|
||||
package generate
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/server/db"
|
||||
"github.com/bishopfox/sliver/server/db/models"
|
||||
"github.com/gofrs/uuid"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
type storedBuildSnapshotFixture struct {
|
||||
profileName string
|
||||
buildName string
|
||||
profile *clientpb.ImplantProfile
|
||||
snapshot *clientpb.ImplantConfig
|
||||
build *clientpb.ImplantBuild
|
||||
snapshotID string
|
||||
buildID string
|
||||
}
|
||||
|
||||
func newStoredBuildSnapshotFixture(t *testing.T) *storedBuildSnapshotFixture {
|
||||
t.Helper()
|
||||
|
||||
suffix := time.Now().UnixNano()
|
||||
profileName := fmt.Sprintf("snapshot-profile-%d", suffix)
|
||||
buildName := fmt.Sprintf("snapshot-build-%d", suffix)
|
||||
|
||||
profile, err := SaveImplantProfile(&clientpb.ImplantProfile{
|
||||
Name: profileName,
|
||||
Config: &clientpb.ImplantConfig{
|
||||
ObfuscateSymbols: true,
|
||||
ControlFlow: clientpb.ControlFlowPolicy_CONTROL_FLOW_BALANCED_V1,
|
||||
TemplateName: SliverTemplateName,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("SaveImplantProfile() error = %v", err)
|
||||
}
|
||||
|
||||
fixture := &storedBuildSnapshotFixture{
|
||||
profileName: profileName,
|
||||
buildName: buildName,
|
||||
profile: profile,
|
||||
}
|
||||
t.Cleanup(fixture.cleanup)
|
||||
|
||||
snapshot, sourceProfileID, err := NewImplantConfigSnapshot(profile.Config)
|
||||
if err != nil {
|
||||
t.Fatalf("NewImplantConfigSnapshot() error = %v", err)
|
||||
}
|
||||
fixture.snapshot = snapshot
|
||||
fixture.snapshotID = snapshot.ID
|
||||
if _, err := db.CreateImplantConfigWithID(snapshot); err != nil {
|
||||
t.Fatalf("CreateImplantConfigWithID() error = %v", err)
|
||||
}
|
||||
|
||||
build, err := db.SaveImplantBuildWithSourceProfile(&clientpb.ImplantBuild{
|
||||
Name: buildName,
|
||||
ImplantConfigID: snapshot.ID,
|
||||
}, sourceProfileID)
|
||||
if err != nil {
|
||||
t.Fatalf("SaveImplantBuildWithSourceProfile() error = %v", err)
|
||||
}
|
||||
fixture.build = build
|
||||
fixture.buildID = build.ID
|
||||
return fixture
|
||||
}
|
||||
|
||||
func (fixture *storedBuildSnapshotFixture) cleanup() {
|
||||
if fixture.buildID != "" {
|
||||
db.Session().Where("id = ?", uuid.FromStringOrNil(fixture.buildID)).Delete(&models.ImplantBuild{})
|
||||
}
|
||||
if fixture.snapshotID != "" {
|
||||
configID := uuid.FromStringOrNil(fixture.snapshotID)
|
||||
db.Session().Where("implant_config_id = ?", configID).Delete(&models.ImplantC2{})
|
||||
db.Session().Where("implant_config_id = ?", configID).Delete(&models.EncoderAsset{})
|
||||
db.Session().Where("implant_config_id = ?", configID).Delete(&models.CanaryDomain{})
|
||||
db.Session().Where("id = ?", configID).Delete(&models.ImplantConfig{})
|
||||
}
|
||||
_ = db.DeleteProfile(fixture.profileName)
|
||||
}
|
||||
|
||||
func (fixture *storedBuildSnapshotFixture) mutateProfilePolicy(t *testing.T) {
|
||||
t.Helper()
|
||||
|
||||
updatedProfile := proto.Clone(fixture.profile).(*clientpb.ImplantProfile)
|
||||
updatedProfile.Config.ControlFlow = clientpb.ControlFlowPolicy_CONTROL_FLOW_DISABLED
|
||||
if _, err := SaveImplantProfile(updatedProfile); err != nil {
|
||||
t.Fatalf("update profile policy: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (fixture *storedBuildSnapshotFixture) assertPersistedSnapshot(t *testing.T) *clientpb.ImplantBuild {
|
||||
t.Helper()
|
||||
|
||||
persistedBuild, err := db.ImplantBuildByName(fixture.buildName)
|
||||
if err != nil {
|
||||
t.Fatalf("ImplantBuildByName() error = %v", err)
|
||||
}
|
||||
persistedConfig, err := db.ImplantConfigByID(persistedBuild.ImplantConfigID)
|
||||
if err != nil {
|
||||
t.Fatalf("ImplantConfigByID() error = %v", err)
|
||||
}
|
||||
if persistedConfig.ID != fixture.snapshot.ID {
|
||||
t.Fatalf("build config ID = %q, want snapshot %q", persistedConfig.ID, fixture.snapshot.ID)
|
||||
}
|
||||
if persistedConfig.ImplantProfileID != "" {
|
||||
t.Fatalf("build snapshot profile ID = %q, want detached config", persistedConfig.ImplantProfileID)
|
||||
}
|
||||
if persistedConfig.ControlFlow != clientpb.ControlFlowPolicy_CONTROL_FLOW_BALANCED_V1 {
|
||||
t.Fatalf("stored build policy = %v after profile mutation, want balanced-v1", persistedConfig.ControlFlow)
|
||||
}
|
||||
return persistedBuild
|
||||
}
|
||||
|
||||
func (fixture *storedBuildSnapshotFixture) assertBuildUpdatePreservesSourceProfile(t *testing.T, persistedBuild *clientpb.ImplantBuild) {
|
||||
t.Helper()
|
||||
|
||||
if _, err := db.SaveImplantBuild(persistedBuild); err != nil {
|
||||
t.Fatalf("SaveImplantBuild() update error = %v", err)
|
||||
}
|
||||
storedBuild := &models.ImplantBuild{}
|
||||
if err := db.Session().Where("id = ?", uuid.FromStringOrNil(fixture.build.ID)).First(storedBuild).Error; err != nil {
|
||||
t.Fatalf("load build model: %v", err)
|
||||
}
|
||||
if storedBuild.SourceImplantProfileID == nil || storedBuild.SourceImplantProfileID.String() != fixture.profile.ID {
|
||||
t.Fatalf("source profile ID after build update = %v, want %q", storedBuild.SourceImplantProfileID, fixture.profile.ID)
|
||||
}
|
||||
}
|
||||
|
||||
func (fixture *storedBuildSnapshotFixture) assertProfileListsBuild(t *testing.T) {
|
||||
t.Helper()
|
||||
|
||||
listedProfile, err := db.ProfileByName(fixture.profileName)
|
||||
if err != nil {
|
||||
t.Fatalf("ProfileByName() error = %v", err)
|
||||
}
|
||||
if len(listedProfile.Config.ImplantBuilds) != 1 || listedProfile.Config.ImplantBuilds[0].Name != fixture.buildName {
|
||||
t.Fatalf("profile builds = %v, want detached snapshot build %q", listedProfile.Config.ImplantBuilds, fixture.buildName)
|
||||
}
|
||||
}
|
||||
|
||||
func (fixture *storedBuildSnapshotFixture) deleteBuildAndProfileRetainsAuditSnapshot(t *testing.T) {
|
||||
t.Helper()
|
||||
|
||||
if err := db.Session().Where("id = ?", uuid.FromStringOrNil(fixture.build.ID)).Delete(&models.ImplantBuild{}).Error; err != nil {
|
||||
t.Fatalf("delete build record: %v", err)
|
||||
}
|
||||
fixture.buildID = ""
|
||||
if err := db.DeleteProfile(fixture.profileName); err != nil {
|
||||
t.Fatalf("DeleteProfile() error = %v", err)
|
||||
}
|
||||
auditConfig, err := db.ImplantConfigByID(fixture.snapshot.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("load origin-policy audit config after build/profile deletion: %v", err)
|
||||
}
|
||||
if auditConfig.ControlFlow != clientpb.ControlFlowPolicy_CONTROL_FLOW_BALANCED_V1 {
|
||||
t.Fatalf("retained audit policy = %v, want balanced-v1", auditConfig.ControlFlow)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProfileMutationDoesNotChangeStoredBuildSnapshotPolicy(t *testing.T) {
|
||||
fixture := newStoredBuildSnapshotFixture(t)
|
||||
fixture.mutateProfilePolicy(t)
|
||||
persistedBuild := fixture.assertPersistedSnapshot(t)
|
||||
fixture.assertBuildUpdatePreservesSourceProfile(t, persistedBuild)
|
||||
fixture.assertProfileListsBuild(t)
|
||||
fixture.deleteBuildAndProfileRetainsAuditSnapshot(t)
|
||||
}
|
||||
|
||||
func TestDeleteFailedImplantConfigSnapshotOnlyDeletesUnreferencedSnapshot(t *testing.T) {
|
||||
newSnapshot := func(t *testing.T) *clientpb.ImplantConfig {
|
||||
t.Helper()
|
||||
config, err := db.CreateImplantConfigWithID(&clientpb.ImplantConfig{
|
||||
ID: uuid.Must(uuid.NewV4()).String(),
|
||||
C2: []*clientpb.ImplantC2{{URL: "mtls://127.0.0.1:8888"}},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("CreateImplantConfigWithID() error = %v", err)
|
||||
}
|
||||
return config
|
||||
}
|
||||
deleteConfig := func(configID string) {
|
||||
id := uuid.FromStringOrNil(configID)
|
||||
db.Session().Where("implant_config_id = ?", id).Delete(&models.ImplantC2{})
|
||||
db.Session().Where("id = ?", id).Delete(&models.ImplantConfig{})
|
||||
}
|
||||
|
||||
unreferenced := newSnapshot(t)
|
||||
t.Cleanup(func() { deleteConfig(unreferenced.ID) })
|
||||
if err := db.DeleteFailedImplantConfigSnapshot(unreferenced.ID); err != nil {
|
||||
t.Fatalf("DeleteFailedImplantConfigSnapshot() error = %v", err)
|
||||
}
|
||||
if _, err := db.ImplantConfigByID(unreferenced.ID); !errors.Is(err, db.ErrRecordNotFound) {
|
||||
t.Fatalf("unreferenced snapshot lookup error = %v, want record not found", err)
|
||||
}
|
||||
var c2Count int64
|
||||
if err := db.Session().Model(&models.ImplantC2{}).Where("implant_config_id = ?", uuid.FromStringOrNil(unreferenced.ID)).Count(&c2Count).Error; err != nil {
|
||||
t.Fatalf("count rolled-back C2 rows: %v", err)
|
||||
}
|
||||
if c2Count != 0 {
|
||||
t.Fatalf("rolled-back snapshot C2 rows = %d, want 0", c2Count)
|
||||
}
|
||||
|
||||
referenced := newSnapshot(t)
|
||||
buildName := fmt.Sprintf("snapshot-rollback-guard-%d", time.Now().UnixNano())
|
||||
build, err := db.SaveImplantBuild(&clientpb.ImplantBuild{
|
||||
Name: buildName,
|
||||
ImplantConfigID: referenced.ID,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("SaveImplantBuild() error = %v", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
db.Session().Where("id = ?", uuid.FromStringOrNil(build.ID)).Delete(&models.ImplantBuild{})
|
||||
deleteConfig(referenced.ID)
|
||||
})
|
||||
if err := db.DeleteFailedImplantConfigSnapshot(referenced.ID); err != nil {
|
||||
t.Fatalf("guard referenced snapshot: %v", err)
|
||||
}
|
||||
if _, err := db.ImplantConfigByID(referenced.ID); err != nil {
|
||||
t.Fatalf("referenced snapshot was deleted: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package generate
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/gofrs/uuid"
|
||||
)
|
||||
|
||||
func TestNewImplantConfigSnapshotDetachesAndPreservesPolicy(t *testing.T) {
|
||||
profileID := uuid.Must(uuid.NewV4()).String()
|
||||
configID := uuid.Must(uuid.NewV4()).String()
|
||||
c2ID := uuid.Must(uuid.NewV4()).String()
|
||||
source := &clientpb.ImplantConfig{
|
||||
ID: configID,
|
||||
ImplantProfileID: profileID,
|
||||
ImplantBuilds: []*clientpb.ImplantBuild{{Name: "old-build"}},
|
||||
ObfuscateSymbols: true,
|
||||
ControlFlow: clientpb.ControlFlowPolicy_CONTROL_FLOW_BALANCED_V1,
|
||||
C2: []*clientpb.ImplantC2{
|
||||
{ID: c2ID, URL: "mtls://127.0.0.1:8888"},
|
||||
nil,
|
||||
},
|
||||
}
|
||||
|
||||
snapshot, sourceProfileID, err := NewImplantConfigSnapshot(source)
|
||||
if err != nil {
|
||||
t.Fatalf("NewImplantConfigSnapshot() error = %v", err)
|
||||
}
|
||||
assertDetachedImplantConfigSnapshot(t, snapshot, sourceProfileID, profileID, configID)
|
||||
|
||||
source.ControlFlow = clientpb.ControlFlowPolicy_CONTROL_FLOW_DISABLED
|
||||
source.C2[0].URL = "mtls://127.0.0.1:9999"
|
||||
if snapshot.ControlFlow != clientpb.ControlFlowPolicy_CONTROL_FLOW_BALANCED_V1 {
|
||||
t.Fatal("mutating the source profile changed the build snapshot policy")
|
||||
}
|
||||
if snapshot.C2[0].URL != "mtls://127.0.0.1:8888" {
|
||||
t.Fatal("mutating the source profile changed the build snapshot C2")
|
||||
}
|
||||
if source.ID != configID || source.ImplantProfileID != profileID || source.C2[0].ID != c2ID {
|
||||
t.Fatal("snapshot creation mutated the source profile config")
|
||||
}
|
||||
|
||||
second, _, err := NewImplantConfigSnapshot(source)
|
||||
if err != nil {
|
||||
t.Fatalf("second NewImplantConfigSnapshot() error = %v", err)
|
||||
}
|
||||
if second.ID == snapshot.ID {
|
||||
t.Fatalf("two build snapshots reused config ID %q", snapshot.ID)
|
||||
}
|
||||
}
|
||||
|
||||
func assertDetachedImplantConfigSnapshot(t *testing.T, snapshot *clientpb.ImplantConfig, sourceProfileID, profileID, configID string) {
|
||||
t.Helper()
|
||||
|
||||
if sourceProfileID != profileID {
|
||||
t.Fatalf("source profile ID = %q, want %q", sourceProfileID, profileID)
|
||||
}
|
||||
if snapshot.ID == "" || snapshot.ID == configID {
|
||||
t.Fatalf("snapshot ID = %q, want a fresh UUID", snapshot.ID)
|
||||
}
|
||||
if uuid.FromStringOrNil(snapshot.ID) == uuid.Nil {
|
||||
t.Fatalf("snapshot ID = %q, want a valid UUID", snapshot.ID)
|
||||
}
|
||||
if snapshot.ImplantProfileID != "" {
|
||||
t.Fatalf("snapshot profile ID = %q, want detached config", snapshot.ImplantProfileID)
|
||||
}
|
||||
if snapshot.ImplantBuilds != nil {
|
||||
t.Fatalf("snapshot builds = %v, want nil", snapshot.ImplantBuilds)
|
||||
}
|
||||
if snapshot.C2[0].ID != "" {
|
||||
t.Fatalf("snapshot C2 ID = %q, want fresh persistence ID", snapshot.C2[0].ID)
|
||||
}
|
||||
if snapshot.ControlFlow != clientpb.ControlFlowPolicy_CONTROL_FLOW_BALANCED_V1 {
|
||||
t.Fatalf("snapshot control-flow policy = %v, want balanced-v1", snapshot.ControlFlow)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewImplantConfigSnapshotRejectsInvalidSourceProfileID(t *testing.T) {
|
||||
_, _, err := NewImplantConfigSnapshot(&clientpb.ImplantConfig{ImplantProfileID: "not-a-uuid"})
|
||||
if err == nil {
|
||||
t.Fatal("NewImplantConfigSnapshot() accepted an invalid source profile ID")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
package generate
|
||||
|
||||
/*
|
||||
Sliver Implant Framework
|
||||
Copyright (C) 2019 Bishop Fox
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
serverAssets "github.com/bishopfox/sliver/server/assets"
|
||||
"github.com/bishopfox/sliver/server/gogo"
|
||||
utilAssets "github.com/bishopfox/sliver/util/assets"
|
||||
)
|
||||
|
||||
const (
|
||||
// ControlFlowCapability is the protocol/policy contract understood by
|
||||
// coordinators and capable external builders.
|
||||
ControlFlowCapability = "garble.control-flow/balanced-v1"
|
||||
// LocalControlFlowCapability is advertised by a coordinator only when its
|
||||
// pinned local Garble runtime is present and verified.
|
||||
LocalControlFlowCapability = "garble.control-flow.local/balanced-v1"
|
||||
|
||||
controlFlowDirectivePrefix = "//garble:controlflow"
|
||||
balancedV1Directive = controlFlowDirectivePrefix + " block_splits=2 junk_jumps=2 flatten_passes=1 flatten_hardening=xor trash_blocks=0"
|
||||
)
|
||||
|
||||
type controlFlowPolicy struct {
|
||||
directive string
|
||||
functions map[string]controlFlowFunction
|
||||
}
|
||||
|
||||
type controlFlowFunction struct {
|
||||
sourcePath string
|
||||
name string
|
||||
}
|
||||
|
||||
var (
|
||||
// ErrControlFlowBuildBusy is returned instead of allowing an unbounded
|
||||
// queue of memory-intensive SSA builds to accumulate on the server.
|
||||
ErrControlFlowBuildBusy = errors.New("another control-flow build is already running")
|
||||
// ErrControlFlowUnavailable indicates that the extracted compiler asset is
|
||||
// absent or does not match Sliver's pinned control-flow-capable artifact.
|
||||
ErrControlFlowUnavailable = errors.New("control-flow obfuscation is unavailable")
|
||||
controlFlowBuildSlot = make(chan struct{}, 1)
|
||||
)
|
||||
|
||||
var balancedV1ControlFlowPolicy = &controlFlowPolicy{
|
||||
directive: balancedV1Directive,
|
||||
functions: map[string]controlFlowFunction{
|
||||
"handlers.determineDirPathFilter": {
|
||||
sourcePath: "sliver/handlers/handlers.go",
|
||||
name: "determineDirPathFilter",
|
||||
},
|
||||
"runner.registerSliver": {
|
||||
sourcePath: "sliver/runner/runner.go",
|
||||
name: "registerSliver",
|
||||
},
|
||||
"transports.randomCCDomain": {
|
||||
sourcePath: "sliver/transports/transports.go",
|
||||
name: "randomCCDomain",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// ValidateControlFlowConfig validates the public control-flow policy before
|
||||
// any source is rendered or cryptographic build material is created.
|
||||
func ValidateControlFlowConfig(config *clientpb.ImplantConfig) error {
|
||||
_, err := controlFlowPolicyForConfig(config)
|
||||
return err
|
||||
}
|
||||
|
||||
func controlFlowPolicyForConfig(config *clientpb.ImplantConfig) (*controlFlowPolicy, error) {
|
||||
if config == nil {
|
||||
return nil, fmt.Errorf("implant config cannot be nil")
|
||||
}
|
||||
|
||||
switch config.ControlFlow {
|
||||
case clientpb.ControlFlowPolicy_CONTROL_FLOW_DISABLED:
|
||||
return nil, nil
|
||||
case clientpb.ControlFlowPolicy_CONTROL_FLOW_BALANCED_V1:
|
||||
if !config.ObfuscateSymbols {
|
||||
return nil, fmt.Errorf("control-flow obfuscation requires symbol obfuscation")
|
||||
}
|
||||
if config.Debug {
|
||||
return nil, fmt.Errorf("control-flow obfuscation is not supported for debug builds")
|
||||
}
|
||||
if templateName := strings.TrimSpace(config.TemplateName); templateName != "" && templateName != SliverTemplateName {
|
||||
return nil, fmt.Errorf("control-flow obfuscation is not supported by implant template %q", templateName)
|
||||
}
|
||||
return balancedV1ControlFlowPolicy, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported control-flow obfuscation policy: %d", config.ControlFlow)
|
||||
}
|
||||
}
|
||||
|
||||
func controlFlowDirective(config *clientpb.ImplantConfig, functionID string) (string, bool, error) {
|
||||
policy, err := controlFlowPolicyForConfig(config)
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
if policy == nil {
|
||||
return "", false, nil
|
||||
}
|
||||
if _, ok := policy.functions[functionID]; !ok {
|
||||
return "", false, fmt.Errorf("function %q is not part of control-flow policy %s", functionID, config.ControlFlow)
|
||||
}
|
||||
return policy.directive, true, nil
|
||||
}
|
||||
|
||||
func controlFlowObfuscationEnabled(config *clientpb.ImplantConfig) bool {
|
||||
return config != nil && config.ControlFlow == clientpb.ControlFlowPolicy_CONTROL_FLOW_BALANCED_V1
|
||||
}
|
||||
|
||||
// ControlFlowEnabled reports whether an implant config requests a supported
|
||||
// non-disabled control-flow policy.
|
||||
func ControlFlowEnabled(config *clientpb.ImplantConfig) bool {
|
||||
return controlFlowObfuscationEnabled(config)
|
||||
}
|
||||
|
||||
// HasControlFlowCapability reports whether a server or external builder
|
||||
// advertises support for Sliver's current control-flow policy contract.
|
||||
func HasControlFlowCapability(capabilities []string) bool {
|
||||
for _, capability := range capabilities {
|
||||
if capability == ControlFlowCapability {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// HasLocalControlFlowCapability reports whether a coordinator can currently
|
||||
// execute the balanced-v1 policy with its local Garble runtime.
|
||||
func HasLocalControlFlowCapability(capabilities []string) bool {
|
||||
for _, capability := range capabilities {
|
||||
if capability == LocalControlFlowCapability {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ControlFlowRuntimeError verifies that the host Garble executable is exactly
|
||||
// the artifact pinned by Sliver's asset manifest. Builder capability
|
||||
// advertisement and local build admission fail closed when extraction is
|
||||
// stale or incomplete; the coordinator's protocol capability remains
|
||||
// independent so healthy external builders can still be used.
|
||||
func ControlFlowRuntimeError() error {
|
||||
garbleName := "garble"
|
||||
if runtime.GOOS == "windows" {
|
||||
garbleName += ".exe"
|
||||
}
|
||||
garblePath := filepath.Join(gogo.GetGoRootDir(serverAssets.GetRootAppDir()), "bin", garbleName)
|
||||
if err := utilAssets.VerifyGarbleBinary(garblePath, runtime.GOOS, runtime.GOARCH); err != nil {
|
||||
return fmt.Errorf("%w: %v", ErrControlFlowUnavailable, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AcquireControlFlowBuildSlot admits at most one local control-flow build. It
|
||||
// fails immediately under contention so queued RPCs cannot retain generated
|
||||
// key material or outlive a canceled client request while waiting for Garble.
|
||||
func AcquireControlFlowBuildSlot(config *clientpb.ImplantConfig) (func(), error) {
|
||||
policy, err := controlFlowPolicyForConfig(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if policy == nil {
|
||||
return func() {}, nil
|
||||
}
|
||||
if err := ControlFlowRuntimeError(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
select {
|
||||
case controlFlowBuildSlot <- struct{}{}:
|
||||
var releaseOnce sync.Once
|
||||
return func() {
|
||||
releaseOnce.Do(func() { <-controlFlowBuildSlot })
|
||||
}, nil
|
||||
default:
|
||||
return nil, ErrControlFlowBuildBusy
|
||||
}
|
||||
}
|
||||
|
||||
func controlFlowFunctionsInSource(policy *controlFlowPolicy, sourcePath string, source []byte) ([]string, error) {
|
||||
file, err := parser.ParseFile(token.NewFileSet(), sourcePath, source, parser.ParseComments)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse rendered control-flow source %q: %w", sourcePath, err)
|
||||
}
|
||||
|
||||
functionIDs := []string{}
|
||||
for _, declaration := range file.Decls {
|
||||
function, ok := declaration.(*ast.FuncDecl)
|
||||
if !ok || function.Doc == nil {
|
||||
continue
|
||||
}
|
||||
directiveCount := 0
|
||||
for _, comment := range function.Doc.List {
|
||||
if !strings.HasPrefix(comment.Text, controlFlowDirectivePrefix) {
|
||||
continue
|
||||
}
|
||||
if comment.Text != policy.directive {
|
||||
return nil, fmt.Errorf("control-flow directive on %s in %s does not match the selected policy", function.Name.Name, sourcePath)
|
||||
}
|
||||
directiveCount++
|
||||
}
|
||||
if directiveCount == 0 {
|
||||
continue
|
||||
}
|
||||
if directiveCount != 1 {
|
||||
return nil, fmt.Errorf("function %s in %s has %d control-flow directives, expected exactly one", function.Name.Name, sourcePath, directiveCount)
|
||||
}
|
||||
if function.Recv != nil {
|
||||
return nil, fmt.Errorf("unexpected control-flow directive on method %s in %s", function.Name.Name, sourcePath)
|
||||
}
|
||||
|
||||
functionID := ""
|
||||
for candidateID, candidate := range policy.functions {
|
||||
if candidate.sourcePath == sourcePath && candidate.name == function.Name.Name {
|
||||
functionID = candidateID
|
||||
break
|
||||
}
|
||||
}
|
||||
if functionID == "" {
|
||||
return nil, fmt.Errorf("unexpected control-flow directive on %s in %s", function.Name.Name, sourcePath)
|
||||
}
|
||||
functionIDs = append(functionIDs, functionID)
|
||||
}
|
||||
return functionIDs, nil
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
package generate
|
||||
|
||||
/*
|
||||
Sliver Implant Framework
|
||||
Copyright (C) 2019 Bishop Fox
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/bishopfox/sliver/implant"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
)
|
||||
|
||||
func TestValidateControlFlowConfig(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
config *clientpb.ImplantConfig
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "disabled is backward compatible",
|
||||
config: &clientpb.ImplantConfig{},
|
||||
},
|
||||
{
|
||||
name: "balanced v1",
|
||||
config: &clientpb.ImplantConfig{
|
||||
ObfuscateSymbols: true,
|
||||
ControlFlow: clientpb.ControlFlowPolicy_CONTROL_FLOW_BALANCED_V1,
|
||||
TemplateName: SliverTemplateName,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "requires symbol obfuscation",
|
||||
config: &clientpb.ImplantConfig{
|
||||
ControlFlow: clientpb.ControlFlowPolicy_CONTROL_FLOW_BALANCED_V1,
|
||||
},
|
||||
wantErr: "requires symbol obfuscation",
|
||||
},
|
||||
{
|
||||
name: "rejects debug builds",
|
||||
config: &clientpb.ImplantConfig{
|
||||
Debug: true,
|
||||
ObfuscateSymbols: true,
|
||||
ControlFlow: clientpb.ControlFlowPolicy_CONTROL_FLOW_BALANCED_V1,
|
||||
},
|
||||
wantErr: "not supported for debug builds",
|
||||
},
|
||||
{
|
||||
name: "rejects unsupported template",
|
||||
config: &clientpb.ImplantConfig{
|
||||
ObfuscateSymbols: true,
|
||||
ControlFlow: clientpb.ControlFlowPolicy_CONTROL_FLOW_BALANCED_V1,
|
||||
TemplateName: "custom",
|
||||
},
|
||||
wantErr: "not supported by implant template",
|
||||
},
|
||||
{
|
||||
name: "rejects unknown policy",
|
||||
config: &clientpb.ImplantConfig{
|
||||
ObfuscateSymbols: true,
|
||||
ControlFlow: clientpb.ControlFlowPolicy(99),
|
||||
},
|
||||
wantErr: "unsupported control-flow obfuscation policy",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
err := ValidateControlFlowConfig(test.config)
|
||||
if test.wantErr == "" {
|
||||
if err != nil {
|
||||
t.Fatalf("ValidateControlFlowConfig() error = %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err == nil || !strings.Contains(err.Error(), test.wantErr) {
|
||||
t.Fatalf("ValidateControlFlowConfig() error = %v, want substring %q", err, test.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBalancedV1ControlFlowPolicy(t *testing.T) {
|
||||
config := &clientpb.ImplantConfig{
|
||||
ObfuscateSymbols: true,
|
||||
ControlFlow: clientpb.ControlFlowPolicy_CONTROL_FLOW_BALANCED_V1,
|
||||
}
|
||||
|
||||
if len(balancedV1ControlFlowPolicy.functions) != 3 {
|
||||
t.Fatalf("balanced-v1 function count = %d, want 3", len(balancedV1ControlFlowPolicy.functions))
|
||||
}
|
||||
for functionID := range balancedV1ControlFlowPolicy.functions {
|
||||
directive, enabled, err := controlFlowDirective(config, functionID)
|
||||
if err != nil {
|
||||
t.Fatalf("controlFlowDirective(%q) error = %v", functionID, err)
|
||||
}
|
||||
if !enabled {
|
||||
t.Fatalf("controlFlowDirective(%q) unexpectedly disabled", functionID)
|
||||
}
|
||||
if directive != balancedV1Directive {
|
||||
t.Fatalf("controlFlowDirective(%q) = %q, want %q", functionID, directive, balancedV1Directive)
|
||||
}
|
||||
}
|
||||
|
||||
if _, _, err := controlFlowDirective(config, "runner.notAllowlisted"); err == nil {
|
||||
t.Fatal("controlFlowDirective accepted a non-allowlisted function")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBalancedV1SourceAnnotations(t *testing.T) {
|
||||
found := map[string]struct{}{}
|
||||
visitedPaths := map[string]struct{}{}
|
||||
for _, expected := range balancedV1ControlFlowPolicy.functions {
|
||||
if _, visited := visitedPaths[expected.sourcePath]; visited {
|
||||
continue
|
||||
}
|
||||
visitedPaths[expected.sourcePath] = struct{}{}
|
||||
|
||||
source, err := implant.FS.ReadFile(expected.sourcePath)
|
||||
if err != nil {
|
||||
t.Fatalf("read %s: %v", expected.sourcePath, err)
|
||||
}
|
||||
functionIDs, err := controlFlowFunctionsInSource(balancedV1ControlFlowPolicy, expected.sourcePath, source)
|
||||
if err != nil {
|
||||
t.Fatalf("validate %s: %v", expected.sourcePath, err)
|
||||
}
|
||||
for _, functionID := range functionIDs {
|
||||
if _, duplicate := found[functionID]; duplicate {
|
||||
t.Fatalf("control-flow directive for %s appears more than once", functionID)
|
||||
}
|
||||
found[functionID] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
if len(found) != len(balancedV1ControlFlowPolicy.functions) {
|
||||
t.Fatalf("found %d balanced-v1 source annotations, want %d", len(found), len(balancedV1ControlFlowPolicy.functions))
|
||||
}
|
||||
}
|
||||
|
||||
func TestControlFlowSourceRejectsUnapprovedDirective(t *testing.T) {
|
||||
source := []byte(`package handlers
|
||||
|
||||
//garble:controlflow flatten_passes=4
|
||||
func determineDirPathFilter() {}
|
||||
`)
|
||||
_, err := controlFlowFunctionsInSource(
|
||||
balancedV1ControlFlowPolicy,
|
||||
"sliver/handlers/handlers.go",
|
||||
source,
|
||||
)
|
||||
if err == nil || !strings.Contains(err.Error(), "does not match the selected policy") {
|
||||
t.Fatalf("controlFlowFunctionsInSource() error = %v, want policy mismatch", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestControlFlowSourceRejectsAllowlistedNameOnMethod(t *testing.T) {
|
||||
source := []byte(`package handlers
|
||||
|
||||
type receiver struct{}
|
||||
|
||||
//garble:controlflow block_splits=2 junk_jumps=2 flatten_passes=1 flatten_hardening=xor trash_blocks=0
|
||||
func (receiver) determineDirPathFilter() {}
|
||||
`)
|
||||
_, err := controlFlowFunctionsInSource(
|
||||
balancedV1ControlFlowPolicy,
|
||||
"sliver/handlers/handlers.go",
|
||||
source,
|
||||
)
|
||||
if err == nil || !strings.Contains(err.Error(), "on method") {
|
||||
t.Fatalf("controlFlowFunctionsInSource() error = %v, want method rejection", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestControlFlowSourceScansEveryDirectiveComment(t *testing.T) {
|
||||
source := []byte(`package handlers
|
||||
|
||||
//garble:controlflow block_splits=2 junk_jumps=2 flatten_passes=1 flatten_hardening=xor trash_blocks=0
|
||||
//garble:controlflow max
|
||||
func determineDirPathFilter() {}
|
||||
`)
|
||||
_, err := controlFlowFunctionsInSource(
|
||||
balancedV1ControlFlowPolicy,
|
||||
"sliver/handlers/handlers.go",
|
||||
source,
|
||||
)
|
||||
if err == nil || !strings.Contains(err.Error(), "does not match the selected policy") {
|
||||
t.Fatalf("controlFlowFunctionsInSource() error = %v, want second directive rejection", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestControlFlowCapabilities(t *testing.T) {
|
||||
if !HasControlFlowCapability([]string{"unrelated", ControlFlowCapability}) {
|
||||
t.Fatal("HasControlFlowCapability did not find advertised capability")
|
||||
}
|
||||
if HasControlFlowCapability([]string{"unrelated"}) {
|
||||
t.Fatal("HasControlFlowCapability accepted an unrelated capability")
|
||||
}
|
||||
}
|
||||
|
||||
func TestControlFlowBuildAdmission(t *testing.T) {
|
||||
disabledRelease, err := AcquireControlFlowBuildSlot(&clientpb.ImplantConfig{})
|
||||
if err != nil {
|
||||
t.Fatalf("AcquireControlFlowBuildSlot(disabled) error = %v", err)
|
||||
}
|
||||
disabledRelease()
|
||||
|
||||
config := &clientpb.ImplantConfig{
|
||||
ObfuscateSymbols: true,
|
||||
ControlFlow: clientpb.ControlFlowPolicy_CONTROL_FLOW_BALANCED_V1,
|
||||
TemplateName: SliverTemplateName,
|
||||
}
|
||||
firstRelease, err := AcquireControlFlowBuildSlot(config)
|
||||
if err != nil {
|
||||
t.Fatalf("first AcquireControlFlowBuildSlot() error = %v", err)
|
||||
}
|
||||
defer firstRelease()
|
||||
|
||||
if _, err := AcquireControlFlowBuildSlot(config); !errors.Is(err, ErrControlFlowBuildBusy) {
|
||||
t.Fatalf("contended AcquireControlFlowBuildSlot() error = %v, want ErrControlFlowBuildBusy", err)
|
||||
}
|
||||
firstRelease()
|
||||
|
||||
secondRelease, err := AcquireControlFlowBuildSlot(config)
|
||||
if err != nil {
|
||||
t.Fatalf("AcquireControlFlowBuildSlot() after release error = %v", err)
|
||||
}
|
||||
secondRelease()
|
||||
}
|
||||
@@ -19,6 +19,8 @@ package generate
|
||||
*/
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/server/db"
|
||||
"github.com/bishopfox/sliver/server/db/models"
|
||||
@@ -26,6 +28,11 @@ import (
|
||||
|
||||
// SliverExternal - Generates the cryptographic keys for the implant but compiles no code
|
||||
func SliverExternal(name string, config *clientpb.ImplantConfig) (*clientpb.ExternalImplantConfig, error) {
|
||||
config, sourceProfileID, err := NewImplantConfigSnapshot(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
config.IncludeMTLS = config.IncludeMTLS || models.IsC2Enabled([]string{"mtls"}, config.C2)
|
||||
config.IncludeWG = config.IncludeWG || models.IsC2Enabled([]string{"wg"}, config.C2)
|
||||
config.IncludeHTTP = config.IncludeHTTP || models.IsC2Enabled([]string{"http", "https"}, config.C2)
|
||||
@@ -55,14 +62,22 @@ func SliverExternal(name string, config *clientpb.ImplantConfig) (*clientpb.Exte
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config, err = db.SaveImplantConfig(config)
|
||||
config, err = db.CreateImplantConfigWithID(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
build.ImplantConfigID = config.ID
|
||||
implantBuild, err := db.SaveImplantBuild(build)
|
||||
var implantBuild *clientpb.ImplantBuild
|
||||
if sourceProfileID == "" {
|
||||
implantBuild, err = db.SaveImplantBuild(build)
|
||||
} else {
|
||||
implantBuild, err = db.SaveImplantBuildWithSourceProfile(build, sourceProfileID)
|
||||
}
|
||||
if err != nil {
|
||||
if cleanupErr := db.DeleteFailedImplantConfigSnapshot(config.ID); cleanupErr != nil {
|
||||
return nil, fmt.Errorf("save external implant build: %w (failed to roll back config snapshot: %v)", err, cleanupErr)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
+125
-2
@@ -38,6 +38,7 @@ import (
|
||||
"github.com/bishopfox/sliver/server/log"
|
||||
"github.com/bishopfox/sliver/server/watchtower"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
@@ -46,6 +47,9 @@ var (
|
||||
|
||||
// ErrImplantBuildFileNotFound - More descriptive 'key not found' error
|
||||
ErrImplantBuildFileNotFound = errors.New("implant build file not found")
|
||||
// ErrImplantBuildNotPending protects completed or partially completed
|
||||
// artifacts from pending external-build cleanup.
|
||||
ErrImplantBuildNotPending = errors.New("implant build is not pending")
|
||||
)
|
||||
|
||||
func getBuildsDir() (string, error) {
|
||||
@@ -95,6 +99,17 @@ func ImplantConfigSave(config *clientpb.ImplantConfig) (*clientpb.ImplantConfig,
|
||||
|
||||
// ImplantBuildSave - Saves a binary file into the database
|
||||
func ImplantBuildSave(build *clientpb.ImplantBuild, config *clientpb.ImplantConfig, fPath string) error {
|
||||
return implantBuildSave(build, config, fPath, "")
|
||||
}
|
||||
|
||||
// ImplantBuildSaveWithSourceProfile stores a build against its immutable
|
||||
// config snapshot while retaining its source profile for profile listing and
|
||||
// deletion behavior.
|
||||
func ImplantBuildSaveWithSourceProfile(build *clientpb.ImplantBuild, config *clientpb.ImplantConfig, fPath string, sourceProfileID string) error {
|
||||
return implantBuildSave(build, config, fPath, sourceProfileID)
|
||||
}
|
||||
|
||||
func implantBuildSave(build *clientpb.ImplantBuild, config *clientpb.ImplantConfig, fPath string, sourceProfileID string) error {
|
||||
rootAppDir, _ := filepath.Abs(assets.GetRootAppDir())
|
||||
fPath, _ = filepath.Abs(fPath)
|
||||
if !strings.HasPrefix(fPath, rootAppDir) {
|
||||
@@ -132,7 +147,12 @@ func ImplantBuildSave(build *clientpb.ImplantBuild, config *clientpb.ImplantConf
|
||||
}
|
||||
|
||||
build.ImplantConfigID = config.ID
|
||||
implantBuild, err := db.SaveImplantBuild(build)
|
||||
var implantBuild *clientpb.ImplantBuild
|
||||
if sourceProfileID == "" {
|
||||
implantBuild, err = db.SaveImplantBuild(build)
|
||||
} else {
|
||||
implantBuild, err = db.SaveImplantBuildWithSourceProfile(build, sourceProfileID)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -184,6 +204,16 @@ func overwriteImplantBuild(dst *clientpb.ImplantBuild, src *clientpb.ImplantBuil
|
||||
}
|
||||
|
||||
func SaveStage(build *clientpb.ImplantBuild, config *clientpb.ImplantConfig, stage2 []byte, stageType string) error {
|
||||
return saveStage(build, config, stage2, stageType, "")
|
||||
}
|
||||
|
||||
// SaveStageWithSourceProfile stores a staged build against its immutable
|
||||
// config snapshot while retaining its source profile association.
|
||||
func SaveStageWithSourceProfile(build *clientpb.ImplantBuild, config *clientpb.ImplantConfig, stage2 []byte, stageType string, sourceProfileID string) error {
|
||||
return saveStage(build, config, stage2, stageType, sourceProfileID)
|
||||
}
|
||||
|
||||
func saveStage(build *clientpb.ImplantBuild, config *clientpb.ImplantConfig, stage2 []byte, stageType string, sourceProfileID string) error {
|
||||
md5Hash, sha1Hash, sha256Hash := computeHashes(stage2)
|
||||
buildsDir, err := getBuildsDir()
|
||||
if err != nil {
|
||||
@@ -211,7 +241,12 @@ func SaveStage(build *clientpb.ImplantBuild, config *clientpb.ImplantConfig, sta
|
||||
}
|
||||
|
||||
build.ImplantConfigID = config.ID
|
||||
implantBuild, err := db.SaveImplantBuild(build)
|
||||
var implantBuild *clientpb.ImplantBuild
|
||||
if sourceProfileID == "" {
|
||||
implantBuild, err = db.SaveImplantBuild(build)
|
||||
} else {
|
||||
implantBuild, err = db.SaveImplantBuildWithSourceProfile(build, sourceProfileID)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -256,3 +291,91 @@ func ImplantFileDelete(build *clientpb.ImplantBuild) error {
|
||||
}
|
||||
return os.Remove(buildFilePath)
|
||||
}
|
||||
|
||||
// DeletePendingImplantBuild removes an external build that never produced an
|
||||
// artifact. Completion state is checked again inside the transaction so a
|
||||
// completed build cannot be mistaken for a pending one. Once the build is
|
||||
// removed, its immutable config snapshot and associations are removed only if
|
||||
// the snapshot is detached from a profile and no other build references it.
|
||||
func DeletePendingImplantBuild(buildID string) error {
|
||||
id := models.ParseUUIDOrNil(buildID)
|
||||
if id == models.NilUUID() {
|
||||
return db.ErrRecordNotFound
|
||||
}
|
||||
|
||||
buildsDir, err := getBuildsDir()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return db.Session().Transaction(func(tx *gorm.DB) error {
|
||||
build, err := loadPendingImplantBuild(tx, id, buildsDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
configID := build.ImplantConfigID
|
||||
if err := tx.Delete(build).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return deleteUnreferencedImplantConfig(tx, configID)
|
||||
})
|
||||
}
|
||||
|
||||
func loadPendingImplantBuild(tx *gorm.DB, id models.UUID, buildsDir string) (*models.ImplantBuild, error) {
|
||||
build := &models.ImplantBuild{}
|
||||
if err := tx.Where(&models.ImplantBuild{ID: id}).First(build).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if build.ImplantID != 0 || build.MD5 != "" || build.SHA1 != "" || build.SHA256 != "" {
|
||||
return nil, ErrImplantBuildNotPending
|
||||
}
|
||||
|
||||
var resourceCount int64
|
||||
if err := tx.Model(&models.ResourceID{}).Where("name = ?", build.Name).Count(&resourceCount).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resourceCount != 0 {
|
||||
return nil, ErrImplantBuildNotPending
|
||||
}
|
||||
|
||||
buildFilePath := filepath.Join(buildsDir, build.ID.String())
|
||||
if _, err := os.Stat(buildFilePath); err == nil {
|
||||
return nil, ErrImplantBuildNotPending
|
||||
} else if !os.IsNotExist(err) {
|
||||
return nil, err
|
||||
}
|
||||
return build, nil
|
||||
}
|
||||
|
||||
func deleteUnreferencedImplantConfig(tx *gorm.DB, configID models.UUID) error {
|
||||
var remainingBuilds int64
|
||||
if err := tx.Model(&models.ImplantBuild{}).Where("implant_config_id = ?", configID).Count(&remainingBuilds).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if remainingBuilds != 0 || configID == models.NilUUID() {
|
||||
return nil
|
||||
}
|
||||
|
||||
config := &models.ImplantConfig{}
|
||||
if err := tx.Where(&models.ImplantConfig{ID: configID}).First(config).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
if config.ImplantProfileID != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := tx.Where(&models.ImplantC2{ImplantConfigID: configID}).Delete(&models.ImplantC2{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Where(&models.EncoderAsset{ImplantConfigID: configID}).Delete(&models.EncoderAsset{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Where(&models.CanaryDomain{ImplantConfigID: configID}).Delete(&models.CanaryDomain{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Delete(config).Error
|
||||
}
|
||||
@@ -1,12 +1,60 @@
|
||||
package generate
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/server/gogo"
|
||||
)
|
||||
|
||||
func TestImplantLDFlags(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
config *clientpb.ImplantConfig
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "non-debug linux",
|
||||
config: &clientpb.ImplantConfig{GOOS: LINUX},
|
||||
want: []string{"-s -w -buildid="},
|
||||
},
|
||||
{
|
||||
name: "non-debug windows",
|
||||
config: &clientpb.ImplantConfig{GOOS: WINDOWS},
|
||||
want: []string{"-s -w -buildid= -H=windowsgui"},
|
||||
},
|
||||
{
|
||||
name: "debug linux",
|
||||
config: &clientpb.ImplantConfig{GOOS: LINUX, Debug: true},
|
||||
},
|
||||
{
|
||||
name: "debug windows",
|
||||
config: &clientpb.ImplantConfig{GOOS: WINDOWS, Debug: true},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
if got := implantLDFlags(test.config); !reflect.DeepEqual(got, test.want) {
|
||||
t.Fatalf("implantLDFlags() = %q, want %q", got, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestImplantLDFlagsReturnsIndependentSlices(t *testing.T) {
|
||||
config := &clientpb.ImplantConfig{GOOS: LINUX}
|
||||
first := implantLDFlags(config)
|
||||
first[0] = "modified"
|
||||
|
||||
want := []string{nonDebugImplantLDFlags}
|
||||
if second := implantLDFlags(config); !reflect.DeepEqual(second, want) {
|
||||
t.Fatalf("implantLDFlags() reused mutable state: got %q, want %q", second, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsZigCC(t *testing.T) {
|
||||
tests := []struct {
|
||||
cc string
|
||||
@@ -32,24 +80,24 @@ func TestApplyZigStaticLinking(t *testing.T) {
|
||||
CGO: "1",
|
||||
CC: "/tmp/zig cc -target x86_64-linux-musl",
|
||||
}
|
||||
ldflags, enabled := applyZigStaticLinking(cfg, "c-shared", []string{""})
|
||||
ldflags, enabled := applyZigStaticLinking(cfg, "c-shared", []string{nonDebugImplantLDFlags})
|
||||
if enabled {
|
||||
t.Fatalf("did not expect -static flags for c-shared (zig emits an ar archive instead of a .so)")
|
||||
}
|
||||
if len(ldflags) != 1 {
|
||||
t.Fatalf("expected a single -ldflags string, got %d (%v)", len(ldflags), ldflags)
|
||||
}
|
||||
if ldflags[0] != "" {
|
||||
if ldflags[0] != nonDebugImplantLDFlags {
|
||||
t.Fatalf("expected ldflags unchanged, got %q", ldflags[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyLinuxShellcodeBuildID(t *testing.T) {
|
||||
ldflags := applyLinuxShellcodeBuildID([]string{"-s -w"})
|
||||
ldflags := applyLinuxShellcodeBuildID([]string{nonDebugImplantLDFlags})
|
||||
if len(ldflags) != 1 {
|
||||
t.Fatalf("expected a single -ldflags string, got %d (%v)", len(ldflags), ldflags)
|
||||
}
|
||||
want := "-s -w " + linuxShellcodeBuildIDLDFlag
|
||||
want := nonDebugImplantLDFlags + " " + linuxShellcodeBuildIDLDFlag
|
||||
if ldflags[0] != want {
|
||||
t.Fatalf("applyLinuxShellcodeBuildID() = %q, want %q", ldflags[0], want)
|
||||
}
|
||||
@@ -61,7 +109,7 @@ func TestApplyZigStaticLinking_Executable(t *testing.T) {
|
||||
CGO: "1",
|
||||
CC: "/tmp/zig cc -target x86_64-linux-musl",
|
||||
}
|
||||
ldflags, enabled := applyZigStaticLinking(cfg, "", []string{""})
|
||||
ldflags, enabled := applyZigStaticLinking(cfg, "", []string{nonDebugImplantLDFlags})
|
||||
if !enabled {
|
||||
t.Fatalf("expected static linking to be enabled for linux zig cc executable build")
|
||||
}
|
||||
@@ -74,6 +122,9 @@ func TestApplyZigStaticLinking_Executable(t *testing.T) {
|
||||
if !strings.Contains(ldflags[0], "-extldflags=-static") {
|
||||
t.Fatalf("expected -extldflags=-static in ldflags, got %q", ldflags[0])
|
||||
}
|
||||
if !strings.Contains(ldflags[0], nonDebugImplantLDFlags) {
|
||||
t.Fatalf("expected production linker flags in ldflags, got %q", ldflags[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyZigStaticLinking_NotLinux(t *testing.T) {
|
||||
@@ -82,11 +133,11 @@ func TestApplyZigStaticLinking_NotLinux(t *testing.T) {
|
||||
CGO: "1",
|
||||
CC: "zig cc -target x86_64-windows-gnu",
|
||||
}
|
||||
ldflags, enabled := applyZigStaticLinking(cfg, "c-shared", []string{""})
|
||||
ldflags, enabled := applyZigStaticLinking(cfg, "c-shared", []string{nonDebugImplantLDFlags})
|
||||
if enabled {
|
||||
t.Fatalf("did not expect static linking to be enabled on %s", cfg.GOOS)
|
||||
}
|
||||
if len(ldflags) != 1 || ldflags[0] != "" {
|
||||
if len(ldflags) != 1 || ldflags[0] != nonDebugImplantLDFlags {
|
||||
t.Fatalf("expected ldflags unchanged, got %v", ldflags)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,13 @@ import (
|
||||
|
||||
// SaveImplantProfile - Save a sliver profile to disk
|
||||
func SaveImplantProfile(pbProfile *clientpb.ImplantProfile) (*clientpb.ImplantProfile, error) {
|
||||
if pbProfile == nil || pbProfile.Config == nil {
|
||||
return nil, errors.New("implant profile config cannot be nil")
|
||||
}
|
||||
if err := ValidateControlFlowConfig(pbProfile.Config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dbProfile, err := db.ImplantProfileByName(pbProfile.Name)
|
||||
if err != nil && !errors.Is(err, db.ErrRecordNotFound) {
|
||||
return nil, err
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
// Package gogo tests Sliver's Go and Garble compiler integration.
|
||||
package gogo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/bishopfox/sliver/server/assets"
|
||||
utilAssets "github.com/bishopfox/sliver/util/assets"
|
||||
)
|
||||
|
||||
func TestPinnedGarbleControlFlow(t *testing.T) {
|
||||
garblePath := pinnedGarblePath(t)
|
||||
moduleDir := controlFlowProbeModule(t)
|
||||
|
||||
transformedRuns := make([]map[string]string, 0, 2)
|
||||
for run := 1; run <= 2; run++ {
|
||||
runID := fmt.Sprintf("run-%d", run)
|
||||
transformedRuns = append(transformedRuns, runControlFlowProbe(t, moduleDir, garblePath, runID))
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(transformedRuns[0], transformedRuns[1]) {
|
||||
t.Fatal("fixed Garble seed produced different xor-hardened control-flow output across independent clean caches")
|
||||
}
|
||||
}
|
||||
|
||||
func pinnedGarblePath(t *testing.T) string {
|
||||
t.Helper()
|
||||
if _, ok := utilAssets.ExpectedGarbleSHA256(runtime.GOOS, runtime.GOARCH); !ok {
|
||||
t.Skipf("no pinned Garble asset for %s/%s", runtime.GOOS, runtime.GOARCH)
|
||||
}
|
||||
|
||||
garblePath := goToolPath(GetGoRootDir(assets.GetRootAppDir()), "garble")
|
||||
if _, err := os.Stat(garblePath); err != nil {
|
||||
t.Fatalf("pinned Garble asset is unavailable at %s: %v", garblePath, err)
|
||||
}
|
||||
if err := utilAssets.VerifyGarbleBinary(garblePath, runtime.GOOS, runtime.GOARCH); err != nil {
|
||||
t.Fatalf("verify pinned Garble asset at %s: %v", garblePath, err)
|
||||
}
|
||||
return garblePath
|
||||
}
|
||||
|
||||
func controlFlowProbeModule(t *testing.T) string {
|
||||
t.Helper()
|
||||
moduleDir := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(moduleDir, "go.mod"), []byte("module example.com/sliver-control-flow-probe\n\ngo 1.26.0\n"), 0o600); err != nil {
|
||||
t.Fatalf("write probe go.mod: %v", err)
|
||||
}
|
||||
probeSource := `package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
//garble:controlflow block_splits=2 junk_jumps=2 flatten_passes=1 flatten_hardening=xor trash_blocks=0
|
||||
func classify(value int) string {
|
||||
if value%2 == 0 {
|
||||
return "even"
|
||||
}
|
||||
return "odd"
|
||||
}
|
||||
|
||||
func main() {
|
||||
fmt.Print(classify(42))
|
||||
}
|
||||
`
|
||||
if err := os.WriteFile(filepath.Join(moduleDir, "main.go"), []byte(probeSource), 0o600); err != nil {
|
||||
t.Fatalf("write probe source: %v", err)
|
||||
}
|
||||
return moduleDir
|
||||
}
|
||||
|
||||
func runControlFlowProbe(t *testing.T, moduleDir string, garblePath string, runID string) map[string]string {
|
||||
t.Helper()
|
||||
debugDir := filepath.Join(moduleDir, "debug-"+runID)
|
||||
outputPath := filepath.Join(moduleDir, "probe-"+runID)
|
||||
if runtime.GOOS == "windows" {
|
||||
outputPath += ".exe"
|
||||
}
|
||||
config := GoConfig{
|
||||
ProjectDir: moduleDir,
|
||||
GOOS: runtime.GOOS,
|
||||
GOARCH: runtime.GOARCH,
|
||||
GOROOT: GetGoRootDir(assets.GetRootAppDir()),
|
||||
GOCACHE: filepath.Join(moduleDir, "go-cache-"+runID),
|
||||
GOMODCACHE: filepath.Join(moduleDir, "go-mod-cache-"+runID),
|
||||
GOPROXY: "off",
|
||||
CGO: "0",
|
||||
Obfuscation: true,
|
||||
ObfuscationControlFlow: true,
|
||||
GOGARBLE: "*",
|
||||
}
|
||||
cmd := exec.Command(
|
||||
garblePath,
|
||||
"-seed=AAAAAAAAAAA",
|
||||
"-literals",
|
||||
"-tiny",
|
||||
"-debugdir="+debugDir,
|
||||
"build",
|
||||
"-o", outputPath,
|
||||
".",
|
||||
)
|
||||
cmd.Dir = moduleDir
|
||||
cmd.Env = garbleCommandEnv(config)
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
t.Fatalf("build control-flow probe %s with pinned Garble: %v\n%s", runID, err, output)
|
||||
}
|
||||
|
||||
output, err := exec.Command(outputPath).CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("run control-flow probe %s: %v\n%s", runID, err, output)
|
||||
}
|
||||
if string(output) != "even" {
|
||||
t.Fatalf("control-flow probe %s output = %q, want %q", runID, output, "even")
|
||||
}
|
||||
return transformedControlFlowSources(t, debugDir, runID)
|
||||
}
|
||||
|
||||
func transformedControlFlowSources(t *testing.T, debugDir string, runID string) map[string]string {
|
||||
t.Helper()
|
||||
transformed := map[string]string{}
|
||||
err := filepath.WalkDir(debugDir, func(path string, entry fs.DirEntry, walkErr error) error {
|
||||
if walkErr != nil {
|
||||
return walkErr
|
||||
}
|
||||
if entry.IsDir() || entry.Name() != "GARBLE_controlflow.go" {
|
||||
return nil
|
||||
}
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !strings.Contains(string(data), "goto ") {
|
||||
return nil
|
||||
}
|
||||
relativePath, err := filepath.Rel(debugDir, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
transformed[relativePath] = string(data)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("inspect Garble debug output for %s: %v", runID, err)
|
||||
}
|
||||
if len(transformed) == 0 {
|
||||
t.Fatalf("Garble debug output for %s did not contain a transformed GARBLE_controlflow.go", runID)
|
||||
}
|
||||
return transformed
|
||||
}
|
||||
+162
-28
@@ -20,24 +20,34 @@ package gogo
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/bishopfox/sliver/server/assets"
|
||||
"github.com/bishopfox/sliver/server/log"
|
||||
utilAssets "github.com/bishopfox/sliver/util/assets"
|
||||
)
|
||||
|
||||
const (
|
||||
goDirName = "go"
|
||||
darwinNoCGOCheckLinknameLDFlag = "-checklinkname=0"
|
||||
goDirName = "go"
|
||||
garbleExperimentalControlFlowEnv = "GARBLE_EXPERIMENTAL_CONTROLFLOW"
|
||||
garbleDebugDirEnv = "SLIVER_GARBLE_DEBUG_DIR"
|
||||
garbleRandomSeedPrefix = "-seed chosen at random: "
|
||||
commandErrorOutputLimit = 8 * 1024
|
||||
darwinNoCGOCheckLinknameLDFlag = "-checklinkname=0"
|
||||
)
|
||||
|
||||
var (
|
||||
gogoLog = log.NamedLogger("gogo", "compiler")
|
||||
// ErrGarbleAssetVerification indicates that a control-flow build did not
|
||||
// use the exact host Garble artifact pinned by Sliver.
|
||||
ErrGarbleAssetVerification = errors.New("garble asset verification failed")
|
||||
gogoLog = log.NamedLogger("gogo", "compiler")
|
||||
)
|
||||
|
||||
// GoConfig - Env variables for Go compiler
|
||||
@@ -56,8 +66,9 @@ type GoConfig struct {
|
||||
HTTPPROXY string
|
||||
HTTPSPROXY string
|
||||
|
||||
Obfuscation bool
|
||||
GOGARBLE string
|
||||
Obfuscation bool
|
||||
ObfuscationControlFlow bool
|
||||
GOGARBLE string
|
||||
}
|
||||
|
||||
// GetGoRootDir - Get the path to GOROOT
|
||||
@@ -110,6 +121,42 @@ func goToolPath(goRoot string, toolName string) string {
|
||||
return filepath.Join(goRoot, "bin", goToolExecutableName(toolName, runtime.GOOS))
|
||||
}
|
||||
|
||||
func envKeyEqual(first, second string) bool {
|
||||
if runtime.GOOS == "windows" {
|
||||
return strings.EqualFold(first, second)
|
||||
}
|
||||
return first == second
|
||||
}
|
||||
|
||||
func setCommandEnv(env []string, name, value string) []string {
|
||||
updated := make([]string, 0, len(env)+1)
|
||||
for _, envVar := range env {
|
||||
key, _, _ := strings.Cut(envVar, "=")
|
||||
if envKeyEqual(key, name) {
|
||||
continue
|
||||
}
|
||||
updated = append(updated, envVar)
|
||||
}
|
||||
return append(updated, fmt.Sprintf("%s=%s", name, value))
|
||||
}
|
||||
|
||||
func setGoDebugSetting(settings, name, value string) string {
|
||||
updated := make([]string, 0, strings.Count(settings, ",")+1)
|
||||
for _, setting := range strings.Split(settings, ",") {
|
||||
setting = strings.TrimSpace(setting)
|
||||
if setting == "" {
|
||||
continue
|
||||
}
|
||||
key, _, _ := strings.Cut(setting, "=")
|
||||
if key == name {
|
||||
continue
|
||||
}
|
||||
updated = append(updated, setting)
|
||||
}
|
||||
updated = append(updated, name+"="+value)
|
||||
return strings.Join(updated, ",")
|
||||
}
|
||||
|
||||
func buildCommandEnv(config GoConfig, extra map[string]string) []string {
|
||||
goBinDir := ""
|
||||
if strings.TrimSpace(config.GOROOT) != "" {
|
||||
@@ -117,45 +164,125 @@ func buildCommandEnv(config GoConfig, extra map[string]string) []string {
|
||||
}
|
||||
|
||||
env := append([]string{}, os.Environ()...)
|
||||
overrides := []string{
|
||||
fmt.Sprintf("CC=%s", config.CC),
|
||||
fmt.Sprintf("CGO_ENABLED=%s", config.CGO),
|
||||
overrides := []struct {
|
||||
name string
|
||||
value string
|
||||
}{
|
||||
{name: "CC", value: config.CC},
|
||||
{name: "CGO_ENABLED", value: config.CGO},
|
||||
// GOROOT explicitly selects the compiler used for implant builds. Do not
|
||||
// let a surrounding host module make that compiler switch toolchains.
|
||||
"GOTOOLCHAIN=local",
|
||||
fmt.Sprintf("GOOS=%s", config.GOOS),
|
||||
fmt.Sprintf("GOARCH=%s", config.GOARCH),
|
||||
fmt.Sprintf("GOROOT=%s", config.GOROOT),
|
||||
fmt.Sprintf("GOPATH=%s", config.ProjectDir),
|
||||
fmt.Sprintf("GOCACHE=%s", config.GOCACHE),
|
||||
fmt.Sprintf("GOMODCACHE=%s", config.GOMODCACHE),
|
||||
fmt.Sprintf("GOPROXY=%s", config.GOPROXY),
|
||||
fmt.Sprintf("HTTP_PROXY=%s", config.HTTPPROXY),
|
||||
fmt.Sprintf("HTTPS_PROXY=%s", config.HTTPSPROXY),
|
||||
fmt.Sprintf("PATH=%s", joinPathList(goBinDir, assets.GetZigDir(), os.Getenv("PATH"))),
|
||||
fmt.Sprintf("HOME=%s", getHomeDir()),
|
||||
}
|
||||
for name, value := range extra {
|
||||
overrides = append(overrides, fmt.Sprintf("%s=%s", name, value))
|
||||
{name: "GOTOOLCHAIN", value: "local"},
|
||||
{name: "GOOS", value: config.GOOS},
|
||||
{name: "GOARCH", value: config.GOARCH},
|
||||
{name: "GOROOT", value: config.GOROOT},
|
||||
{name: "GOPATH", value: config.ProjectDir},
|
||||
{name: "GOCACHE", value: config.GOCACHE},
|
||||
{name: "GOMODCACHE", value: config.GOMODCACHE},
|
||||
{name: "GOPROXY", value: config.GOPROXY},
|
||||
{name: "HTTP_PROXY", value: config.HTTPPROXY},
|
||||
{name: "HTTPS_PROXY", value: config.HTTPSPROXY},
|
||||
{name: "PATH", value: joinPathList(goBinDir, assets.GetZigDir(), os.Getenv("PATH"))},
|
||||
{name: "HOME", value: getHomeDir()},
|
||||
}
|
||||
|
||||
return append(env, overrides...)
|
||||
extraNames := make([]string, 0, len(extra))
|
||||
for name := range extra {
|
||||
extraNames = append(extraNames, name)
|
||||
}
|
||||
sort.Strings(extraNames)
|
||||
for _, name := range extraNames {
|
||||
overrides = append(overrides, struct {
|
||||
name string
|
||||
value string
|
||||
}{name: name, value: extra[name]})
|
||||
}
|
||||
|
||||
for _, override := range overrides {
|
||||
env = setCommandEnv(env, override.name, override.value)
|
||||
}
|
||||
return env
|
||||
}
|
||||
|
||||
func garbleCommandEnv(config GoConfig) []string {
|
||||
controlFlow := "0"
|
||||
if config.ObfuscationControlFlow {
|
||||
controlFlow = "1"
|
||||
}
|
||||
return buildCommandEnv(config, map[string]string{
|
||||
garbleExperimentalControlFlowEnv: controlFlow,
|
||||
"GOGARBLE": config.GOGARBLE,
|
||||
"GODEBUG": setGoDebugSetting(os.Getenv("GODEBUG"), "randautoseed", "0"),
|
||||
})
|
||||
}
|
||||
|
||||
func validateObfuscationConfig(config GoConfig) error {
|
||||
if config.ObfuscationControlFlow && !config.Obfuscation {
|
||||
return fmt.Errorf("control-flow obfuscation requires symbol obfuscation")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func garbleRandomSeed(stderr string) string {
|
||||
for _, line := range strings.Split(stderr, "\n") {
|
||||
if seed, ok := strings.CutPrefix(strings.TrimSpace(line), garbleRandomSeedPrefix); ok {
|
||||
return strings.TrimSpace(seed)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func sanitizeCommandErrorOutput(output string) string {
|
||||
output = strings.Map(func(character rune) rune {
|
||||
switch character {
|
||||
case '\n', '\r', '\t':
|
||||
return character
|
||||
default:
|
||||
if character < 0x20 || character == 0x7f {
|
||||
return -1
|
||||
}
|
||||
return character
|
||||
}
|
||||
}, strings.TrimSpace(output))
|
||||
if len(output) <= commandErrorOutputLimit {
|
||||
return output
|
||||
}
|
||||
headLength := commandErrorOutputLimit / 4
|
||||
tailLength := commandErrorOutputLimit - headLength
|
||||
return output[:headLength] + "\n... output truncated ...\n" + output[len(output)-tailLength:]
|
||||
}
|
||||
|
||||
func garbleCommandError(err error, stderr string) error {
|
||||
details := sanitizeCommandErrorOutput(stderr)
|
||||
if details == "" {
|
||||
return fmt.Errorf("garble command failed: %w", err)
|
||||
}
|
||||
return fmt.Errorf("garble command failed: %w: %s", err, details)
|
||||
}
|
||||
|
||||
// GarbleCmd - Execute a go command
|
||||
func GarbleCmd(config GoConfig, cwd string, command []string) ([]byte, error) {
|
||||
if err := validateObfuscationConfig(config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
target := fmt.Sprintf("%s/%s", config.GOOS, config.GOARCH)
|
||||
if _, ok := ValidCompilerTargets(config)[target]; !ok {
|
||||
return nil, fmt.Errorf("%s", fmt.Sprintf("Invalid compiler target: %s", target))
|
||||
}
|
||||
garbleBinPath := goToolPath(config.GOROOT, "garble")
|
||||
if config.ObfuscationControlFlow {
|
||||
if err := utilAssets.VerifyGarbleBinary(garbleBinPath, runtime.GOOS, runtime.GOARCH); err != nil {
|
||||
return nil, fmt.Errorf("%w: %v", ErrGarbleAssetVerification, err)
|
||||
}
|
||||
}
|
||||
garbleFlags := []string{"-seed=random", "-literals", "-tiny"}
|
||||
if debugDir := strings.TrimSpace(os.Getenv(garbleDebugDirEnv)); debugDir != "" {
|
||||
garbleFlags = append(garbleFlags, "-debugdir="+debugDir)
|
||||
}
|
||||
command = append(garbleFlags, command...)
|
||||
cmd := exec.Command(garbleBinPath, command...)
|
||||
cmd.Dir = cwd
|
||||
cmd.Env = buildCommandEnv(config, map[string]string{
|
||||
"GOGARBLE": config.GOGARBLE,
|
||||
})
|
||||
cmd.Env = garbleCommandEnv(config)
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
@@ -166,6 +293,9 @@ func GarbleCmd(config GoConfig, cwd string, command []string) ([]byte, error) {
|
||||
}
|
||||
gogoLog.Infof("garble cmd: '%v'", cmd)
|
||||
err := cmd.Run()
|
||||
if seed := garbleRandomSeed(stderr.String()); seed != "" {
|
||||
gogoLog.Infof("garble random seed: %s", seed)
|
||||
}
|
||||
if err != nil {
|
||||
gogoLog.Debugf("--- env ---\n")
|
||||
for _, envVar := range cmd.Env {
|
||||
@@ -174,9 +304,10 @@ func GarbleCmd(config GoConfig, cwd string, command []string) ([]byte, error) {
|
||||
gogoLog.Errorf("--- stdout ---\n%s\n", stdout.String())
|
||||
gogoLog.Errorf("--- stderr ---\n%s\n", stderr.String())
|
||||
gogoLog.Error(err)
|
||||
return stdout.Bytes(), garbleCommandError(err, stderr.String())
|
||||
}
|
||||
|
||||
return stdout.Bytes(), err
|
||||
return stdout.Bytes(), nil
|
||||
}
|
||||
|
||||
// GoCmd - Execute a go command
|
||||
@@ -207,6 +338,9 @@ func GoCmd(config GoConfig, cwd string, command []string) ([]byte, error) {
|
||||
|
||||
// GoBuild - Execute a go build command, returns stdout/error
|
||||
func GoBuild(config GoConfig, src string, dest string, buildmode string, tags []string, ldflags []string, gcflags, asmflags string) ([]byte, error) {
|
||||
if err := validateObfuscationConfig(config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
target := fmt.Sprintf("%s/%s", config.GOOS, config.GOARCH)
|
||||
if _, ok := ValidCompilerTargets(config)[target]; !ok {
|
||||
return nil, fmt.Errorf("%s", fmt.Sprintf("Invalid compiler target: %s", target))
|
||||
|
||||
+114
-2
@@ -19,11 +19,23 @@ package gogo
|
||||
*/
|
||||
|
||||
import (
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func commandEnvValues(env []string, name string) []string {
|
||||
values := []string{}
|
||||
for _, envVar := range env {
|
||||
key, value, found := strings.Cut(envVar, "=")
|
||||
if found && envKeyEqual(key, name) {
|
||||
values = append(values, value)
|
||||
}
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
func TestGoToolExecutableName(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -75,8 +87,8 @@ func TestWithDarwinNoCGOLinknameCompatibility(t *testing.T) {
|
||||
{
|
||||
name: "darwin arm64 preserves existing flags",
|
||||
config: GoConfig{GOOS: "darwin", GOARCH: "arm64", CGO: "0"},
|
||||
ldflags: []string{"-s -w"},
|
||||
want: []string{"-s -w " + darwinNoCGOCheckLinknameLDFlag},
|
||||
ldflags: []string{"-s -w -buildid="},
|
||||
want: []string{"-s -w -buildid= " + darwinNoCGOCheckLinknameLDFlag},
|
||||
},
|
||||
{
|
||||
name: "coalesces multiple linker flag arguments",
|
||||
@@ -148,3 +160,103 @@ func TestGoVersionUsesHostToolchain(t *testing.T) {
|
||||
t.Fatalf("unexpected go version output: %q", string(output))
|
||||
}
|
||||
}
|
||||
|
||||
func TestGarbleCommandEnvOverridesAmbientControlFlow(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
ambient string
|
||||
controlFlow bool
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "enable overrides ambient disabled",
|
||||
ambient: "0",
|
||||
controlFlow: true,
|
||||
want: "1",
|
||||
},
|
||||
{
|
||||
name: "disable overrides ambient enabled",
|
||||
ambient: "1",
|
||||
controlFlow: false,
|
||||
want: "0",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
t.Setenv(garbleExperimentalControlFlowEnv, test.ambient)
|
||||
config := GoConfig{
|
||||
Obfuscation: true,
|
||||
ObfuscationControlFlow: test.controlFlow,
|
||||
}
|
||||
|
||||
values := commandEnvValues(garbleCommandEnv(config), garbleExperimentalControlFlowEnv)
|
||||
if len(values) != 1 {
|
||||
t.Fatalf("%s occurs %d times in Garble environment, want exactly once: %q", garbleExperimentalControlFlowEnv, len(values), values)
|
||||
}
|
||||
if values[0] != test.want {
|
||||
t.Fatalf("%s = %q, want %q", garbleExperimentalControlFlowEnv, values[0], test.want)
|
||||
}
|
||||
if ambient := os.Getenv(garbleExperimentalControlFlowEnv); ambient != test.ambient {
|
||||
t.Fatalf("building Garble environment changed process %s to %q, want ambient value %q", garbleExperimentalControlFlowEnv, ambient, test.ambient)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGarbleCommandEnvMakesGlobalRandDeterministic(t *testing.T) {
|
||||
ambient := "asyncpreemptoff=1,randautoseed=1,panicnil=1,randautoseed=2"
|
||||
t.Setenv("GODEBUG", ambient)
|
||||
|
||||
values := commandEnvValues(garbleCommandEnv(GoConfig{}), "GODEBUG")
|
||||
if len(values) != 1 {
|
||||
t.Fatalf("GODEBUG occurs %d times in Garble environment, want exactly once: %q", len(values), values)
|
||||
}
|
||||
want := "asyncpreemptoff=1,panicnil=1,randautoseed=0"
|
||||
if values[0] != want {
|
||||
t.Fatalf("GODEBUG = %q, want %q", values[0], want)
|
||||
}
|
||||
if got := os.Getenv("GODEBUG"); got != ambient {
|
||||
t.Fatalf("building Garble environment changed process GODEBUG to %q, want %q", got, ambient)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGoBuildRejectsControlFlowWithoutObfuscation(t *testing.T) {
|
||||
config := GoConfig{ObfuscationControlFlow: true}
|
||||
|
||||
_, err := GoBuild(config, "", "", "", nil, nil, "", "")
|
||||
if err == nil {
|
||||
t.Fatal("GoBuild accepted control-flow obfuscation without symbol obfuscation")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "control-flow obfuscation requires symbol obfuscation") {
|
||||
t.Fatalf("GoBuild returned unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGarbleRandomSeed(t *testing.T) {
|
||||
stderr := "-seed chosen at random: AAAAAAAAAAA\nsome other output\n"
|
||||
if seed := garbleRandomSeed(stderr); seed != "AAAAAAAAAAA" {
|
||||
t.Fatalf("garbleRandomSeed() = %q, want %q", seed, "AAAAAAAAAAA")
|
||||
}
|
||||
if seed := garbleRandomSeed("no seed here"); seed != "" {
|
||||
t.Fatalf("garbleRandomSeed() = %q, want empty", seed)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGarbleCommandErrorIsBoundedAndSanitized(t *testing.T) {
|
||||
stderr := garbleRandomSeedPrefix + "AAAAAAAAAAA\n" + strings.Repeat("x", commandErrorOutputLimit*2) + "\x00failure detail"
|
||||
err := garbleCommandError(os.ErrInvalid, stderr)
|
||||
if err == nil {
|
||||
t.Fatal("garbleCommandError() returned nil")
|
||||
}
|
||||
message := err.Error()
|
||||
if len(message) > commandErrorOutputLimit+512 {
|
||||
t.Fatalf("garbleCommandError() returned %d bytes, expected bounded output", len(message))
|
||||
}
|
||||
if strings.ContainsRune(message, '\x00') {
|
||||
t.Fatal("garbleCommandError() retained a NUL byte")
|
||||
}
|
||||
if !strings.Contains(message, "AAAAAAAAAAA") || !strings.Contains(message, "failure detail") {
|
||||
t.Fatalf("garbleCommandError() lost seed or tail detail: %q", message)
|
||||
}
|
||||
}
|
||||
@@ -80,7 +80,7 @@ func beaconRegisterHandler(implantConn *core.ImplantConnection, data []byte) *sl
|
||||
beacon.ReconnectInterval = beaconReg.Register.ReconnectInterval
|
||||
beacon.ActiveC2 = beaconReg.Register.ActiveC2
|
||||
beacon.ProxyURL = beaconReg.Register.ProxyURL
|
||||
// beacon.ConfigID = models.ParseUUIDOrNil(beaconReg.Register.ConfigID)
|
||||
beacon.ImplantConfigID = models.ParseUUIDOrNil(beaconReg.Register.ConfigID)
|
||||
beacon.Locale = beaconReg.Register.Locale
|
||||
beacon.Capabilities = beaconReg.Register.Capabilities
|
||||
|
||||
|
||||
@@ -24,9 +24,12 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/server/core"
|
||||
"github.com/bishopfox/sliver/server/db"
|
||||
"github.com/bishopfox/sliver/server/db/models"
|
||||
"github.com/bishopfox/sliver/server/generate"
|
||||
"github.com/bishopfox/sliver/server/gogo"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
@@ -81,6 +84,12 @@ func rpcError(err error) error {
|
||||
return status.Error(codes.AlreadyExists, err.Error())
|
||||
case errors.Is(err, core.ErrDuplicateExternalBuilderName):
|
||||
return status.Error(codes.AlreadyExists, err.Error())
|
||||
case errors.Is(err, generate.ErrControlFlowBuildBusy):
|
||||
return status.Error(codes.ResourceExhausted, err.Error())
|
||||
case errors.Is(err, generate.ErrControlFlowUnavailable):
|
||||
return status.Error(codes.FailedPrecondition, err.Error())
|
||||
case errors.Is(err, gogo.ErrGarbleAssetVerification):
|
||||
return status.Error(codes.FailedPrecondition, err.Error())
|
||||
case errors.Is(err, db.ErrRecordNotFound):
|
||||
return status.Error(codes.NotFound, err.Error())
|
||||
case os.IsNotExist(err):
|
||||
@@ -91,3 +100,14 @@ func rpcError(err error) error {
|
||||
return status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func acquireControlFlowBuild(config *clientpb.ImplantConfig) (func(), error) {
|
||||
if err := generate.ValidateControlFlowConfig(config); err != nil {
|
||||
return nil, status.Error(codes.InvalidArgument, err.Error())
|
||||
}
|
||||
release, err := generate.AcquireControlFlowBuildSlot(config)
|
||||
if err != nil {
|
||||
return nil, rpcError(err)
|
||||
}
|
||||
return release, nil
|
||||
}
|
||||
@@ -98,10 +98,18 @@ func (rpc *Server) Backdoor(ctx context.Context, req *clientpb.BackdoorReq) (*cl
|
||||
return nil, status.Error(codes.InvalidArgument, fmt.Sprintf("no profile found for name %s", req.ProfileName))
|
||||
}
|
||||
|
||||
if p.Config == nil {
|
||||
return nil, status.Error(codes.InvalidArgument, "profile is missing an implant config")
|
||||
}
|
||||
if p.Config.Format != clientpb.OutputFormat_SHELLCODE {
|
||||
return nil, status.Error(codes.InvalidArgument, "please select a profile targeting a shellcode format")
|
||||
}
|
||||
|
||||
releaseControlFlowBuild, err := acquireControlFlowBuild(p.Config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer releaseControlFlowBuild()
|
||||
build, err := generate.GenerateConfig(name, p.Config)
|
||||
if err != nil {
|
||||
return nil, rpcError(err)
|
||||
@@ -117,6 +125,7 @@ func (rpc *Server) Backdoor(ctx context.Context, req *clientpb.BackdoorReq) (*cl
|
||||
if err != nil {
|
||||
return nil, rpcError(err)
|
||||
}
|
||||
releaseControlFlowBuild()
|
||||
shellcode, err := os.ReadFile(fPath)
|
||||
if err != nil {
|
||||
return nil, rpcError(err)
|
||||
|
||||
+132
-25
@@ -59,10 +59,14 @@ var (
|
||||
// Generate - Generate a new implant
|
||||
func (rpc *Server) Generate(ctx context.Context, req *clientpb.GenerateReq) (*clientpb.Generate, error) {
|
||||
var (
|
||||
err error
|
||||
config *clientpb.ImplantConfig
|
||||
name string
|
||||
err error
|
||||
config *clientpb.ImplantConfig
|
||||
name string
|
||||
sourceProfileID string
|
||||
)
|
||||
if req == nil || req.Config == nil {
|
||||
return nil, status.Error(codes.InvalidArgument, "missing implant config")
|
||||
}
|
||||
|
||||
if req.Name == "" {
|
||||
name, err = codenames.GetCodename()
|
||||
@@ -76,14 +80,25 @@ func (rpc *Server) Generate(ctx context.Context, req *clientpb.GenerateReq) (*cl
|
||||
}
|
||||
|
||||
if req.Config.ID != "" {
|
||||
// if this is a profile reuse existing configuration
|
||||
// If this is a profile, load its configuration as the source for an
|
||||
// immutable per-build snapshot. Do not attach the build to the mutable
|
||||
// profile config row.
|
||||
config, err = db.ImplantConfigByID(req.Config.ID)
|
||||
if err != nil {
|
||||
return nil, rpcError(err)
|
||||
}
|
||||
} else {
|
||||
// configure c2 channels to enable
|
||||
if strings.TrimSpace(req.Config.ImplantProfileID) != "" {
|
||||
return nil, status.Error(codes.InvalidArgument, "implant profile ID requires a persisted implant config ID")
|
||||
}
|
||||
config = req.Config
|
||||
}
|
||||
config, sourceProfileID, err = generate.NewImplantConfigSnapshot(config)
|
||||
if err != nil {
|
||||
return nil, rpcError(err)
|
||||
}
|
||||
if req.Config.ID == "" {
|
||||
// configure c2 channels to enable
|
||||
config.IncludeMTLS = config.IncludeMTLS || models.IsC2Enabled([]string{"mtls"}, config.C2)
|
||||
config.IncludeWG = config.IncludeWG || models.IsC2Enabled([]string{"wg"}, config.C2)
|
||||
config.IncludeHTTP = config.IncludeHTTP || models.IsC2Enabled([]string{"http", "https"}, config.C2)
|
||||
@@ -91,10 +106,14 @@ func (rpc *Server) Generate(ctx context.Context, req *clientpb.GenerateReq) (*cl
|
||||
config.IncludeNamePipe = config.IncludeNamePipe || models.IsC2Enabled([]string{"namedpipe"}, config.C2)
|
||||
config.IncludeTCP = config.IncludeTCP || models.IsC2Enabled([]string{"tcppivot"}, config.C2)
|
||||
}
|
||||
|
||||
if len(config.Exports) == 0 {
|
||||
config.Exports = []string{"StartW"}
|
||||
}
|
||||
releaseControlFlowBuild, err := acquireControlFlowBuild(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer releaseControlFlowBuild()
|
||||
|
||||
// generate config
|
||||
build, err := generate.GenerateConfig(name, config)
|
||||
@@ -103,13 +122,13 @@ func (rpc *Server) Generate(ctx context.Context, req *clientpb.GenerateReq) (*cl
|
||||
}
|
||||
|
||||
// retrieve http c2 implant config
|
||||
httpC2Config, err := db.LoadHTTPC2ConfigByName(req.Config.HTTPC2ConfigName)
|
||||
httpC2Config, err := db.LoadHTTPC2ConfigByName(config.HTTPC2ConfigName)
|
||||
if err != nil {
|
||||
return nil, rpcError(err)
|
||||
}
|
||||
|
||||
var fPath string
|
||||
switch req.Config.Format {
|
||||
switch config.Format {
|
||||
case clientpb.OutputFormat_SERVICE:
|
||||
fallthrough
|
||||
case clientpb.OutputFormat_EXECUTABLE:
|
||||
@@ -121,7 +140,7 @@ func (rpc *Server) Generate(ctx context.Context, req *clientpb.GenerateReq) (*cl
|
||||
case clientpb.OutputFormat_SHELLCODE:
|
||||
fPath, err = generate.SliverShellcode(name, build, config, httpC2Config.ImplantConfig)
|
||||
default:
|
||||
return nil, status.Error(codes.InvalidArgument, fmt.Sprintf("invalid output format: %s", req.Config.Format))
|
||||
return nil, status.Error(codes.InvalidArgument, fmt.Sprintf("invalid output format: %s", config.Format))
|
||||
}
|
||||
if err != nil {
|
||||
return nil, rpcError(err)
|
||||
@@ -132,10 +151,21 @@ func (rpc *Server) Generate(ctx context.Context, req *clientpb.GenerateReq) (*cl
|
||||
if err != nil {
|
||||
return nil, rpcError(err)
|
||||
}
|
||||
config, err = db.CreateImplantConfigWithID(config)
|
||||
if err != nil {
|
||||
return nil, rpcError(err)
|
||||
}
|
||||
|
||||
err = generate.ImplantBuildSave(build, config, fPath)
|
||||
if sourceProfileID == "" {
|
||||
err = generate.ImplantBuildSave(build, config, fPath)
|
||||
} else {
|
||||
err = generate.ImplantBuildSaveWithSourceProfile(build, config, fPath, sourceProfileID)
|
||||
}
|
||||
if err != nil {
|
||||
rpcLog.Errorf("Failed to save external build: %s", err)
|
||||
if cleanupErr := db.DeleteFailedImplantConfigSnapshot(config.ID); cleanupErr != nil {
|
||||
rpcLog.Errorf("Failed to roll back config snapshot %s after build save failure: %s", config.ID, cleanupErr)
|
||||
}
|
||||
return nil, rpcError(err)
|
||||
}
|
||||
|
||||
@@ -487,6 +517,12 @@ func (rpc *Server) ImplantProfiles(ctx context.Context, _ *commonpb.Empty) (*cli
|
||||
|
||||
// SaveImplantProfile - Save a new profile
|
||||
func (rpc *Server) SaveImplantProfile(ctx context.Context, profile *clientpb.ImplantProfile) (*clientpb.ImplantProfile, error) {
|
||||
if profile == nil || profile.Config == nil {
|
||||
return nil, status.Error(codes.InvalidArgument, "missing implant profile config")
|
||||
}
|
||||
if err := generate.ValidateControlFlowConfig(profile.Config); err != nil {
|
||||
return nil, status.Error(codes.InvalidArgument, err.Error())
|
||||
}
|
||||
profile.Name = filepath.Base(profile.Name)
|
||||
if 0 < len(profile.Name) && profile.Name != "." {
|
||||
rpcLog.Infof("Saving new profile with name %#v", profile.Name)
|
||||
@@ -533,12 +569,27 @@ func (rpc *Server) DeleteImplantBuild(ctx context.Context, req *clientpb.DeleteR
|
||||
|
||||
// Remove Implant build given the build name
|
||||
func RemoveBuildByName(name string) error {
|
||||
resourceID, err := db.ResourceIDByName(name)
|
||||
build, err := db.ImplantBuildByName(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
build, err := db.ImplantBuildByName(name)
|
||||
resourceID, err := db.ResourceIDByName(name)
|
||||
if errors.Is(err, db.ErrRecordNotFound) {
|
||||
err = generate.DeletePendingImplantBuild(build.ID)
|
||||
if errors.Is(err, generate.ErrImplantBuildNotPending) {
|
||||
return fmt.Errorf("implant build %q has completion state but no resource ID", name)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
core.RemoveExternalBuildAssignment(build.ID)
|
||||
core.EventBroker.Publish(core.Event{
|
||||
EventType: consts.BuildEvent,
|
||||
Data: []byte(build.Name),
|
||||
})
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -581,6 +632,12 @@ func (rpc *Server) GetCompiler(ctx context.Context, _ *commonpb.Empty) (*clientp
|
||||
Targets: generate.GetCompilerTargets(),
|
||||
UnsupportedTargets: generate.GetUnsupportedTargets(),
|
||||
CrossCompilers: generate.GetCrossCompilers(),
|
||||
Capabilities: []string{generate.ControlFlowCapability},
|
||||
}
|
||||
if err := generate.ControlFlowRuntimeError(); err != nil {
|
||||
rcpGenLog.Warnf("Local builds using %s are unavailable: %s", generate.ControlFlowCapability, err)
|
||||
} else {
|
||||
compiler.Capabilities = append(compiler.Capabilities, generate.LocalControlFlowCapability)
|
||||
}
|
||||
rcpGenLog.Debugf("GetCompiler = %v", compiler)
|
||||
return compiler, nil
|
||||
@@ -616,6 +673,16 @@ func (rpc *Server) GenerateExternal(ctx context.Context, req *clientpb.ExternalG
|
||||
if config == nil {
|
||||
return nil, status.Error(codes.InvalidArgument, "invalid implant config")
|
||||
}
|
||||
config, err = resolveExternalBuildConfig(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := generate.ValidateControlFlowConfig(config); err != nil {
|
||||
return nil, status.Error(codes.InvalidArgument, err.Error())
|
||||
}
|
||||
if generate.ControlFlowEnabled(config) && !generate.HasControlFlowCapability(builder.Capabilities) {
|
||||
return nil, status.Errorf(codes.FailedPrecondition, "builder %q does not support %s", req.BuilderName, generate.ControlFlowCapability)
|
||||
}
|
||||
externalConfig, err := generate.SliverExternal(name, config)
|
||||
if err != nil {
|
||||
return nil, rpcError(err)
|
||||
@@ -630,6 +697,24 @@ func (rpc *Server) GenerateExternal(ctx context.Context, req *clientpb.ExternalG
|
||||
return externalConfig, err
|
||||
}
|
||||
|
||||
func resolveExternalBuildConfig(config *clientpb.ImplantConfig) (*clientpb.ImplantConfig, error) {
|
||||
if config == nil {
|
||||
return nil, status.Error(codes.InvalidArgument, "invalid implant config")
|
||||
}
|
||||
if config.ID == "" {
|
||||
if strings.TrimSpace(config.ImplantProfileID) != "" {
|
||||
return nil, status.Error(codes.InvalidArgument, "implant profile ID requires a persisted implant config ID")
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
|
||||
persistedConfig, err := db.ImplantConfigByID(config.ID)
|
||||
if err != nil {
|
||||
return nil, rpcError(err)
|
||||
}
|
||||
return persistedConfig, nil
|
||||
}
|
||||
|
||||
// GenerateExternalSaveBuild - Allows an external builder to save the build to the server
|
||||
func (rpc *Server) GenerateExternalSaveBuild(ctx context.Context, req *clientpb.ExternalImplantBinary) (*commonpb.Empty, error) {
|
||||
if req.GetFile() == nil {
|
||||
@@ -805,11 +890,16 @@ func (rpc *Server) BuilderTrigger(ctx context.Context, req *clientpb.Event) (*co
|
||||
if err = rpc.authorizeBuilderOperatorForExternalBuild(ctx, buildID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cleanupErr := generate.DeletePendingImplantBuild(buildID)
|
||||
core.RemoveExternalBuildAssignment(buildID)
|
||||
core.EventBroker.Publish(core.Event{
|
||||
EventType: req.EventType,
|
||||
Data: req.Data,
|
||||
})
|
||||
if cleanupErr != nil && !errors.Is(cleanupErr, generate.ErrImplantBuildNotPending) {
|
||||
rpcLog.Errorf("Failed to clean up pending external build %s: %s", buildID, cleanupErr)
|
||||
return nil, rpcError(cleanupErr)
|
||||
}
|
||||
case consts.AcknowledgeBuildEvent:
|
||||
buildID := strings.TrimSpace(string(req.Data))
|
||||
if buildID == "" {
|
||||
@@ -1092,37 +1182,46 @@ func (rpc *Server) GenerateStage(ctx context.Context, req *clientpb.GenerateStag
|
||||
} else {
|
||||
name = req.Name
|
||||
}
|
||||
|
||||
// retrieve http c2 implant config
|
||||
httpC2Config, err := db.LoadHTTPC2ConfigByName(profile.Config.HTTPC2ConfigName)
|
||||
config, sourceProfileID, err := generate.NewImplantConfigSnapshot(profile.Config)
|
||||
if err != nil {
|
||||
return nil, rpcError(err)
|
||||
}
|
||||
|
||||
if len(profile.Config.Exports) == 0 {
|
||||
profile.Config.Exports = []string{"StartW"}
|
||||
// retrieve http c2 implant config
|
||||
httpC2Config, err := db.LoadHTTPC2ConfigByName(config.HTTPC2ConfigName)
|
||||
if err != nil {
|
||||
return nil, rpcError(err)
|
||||
}
|
||||
|
||||
if len(config.Exports) == 0 {
|
||||
config.Exports = []string{"StartW"}
|
||||
}
|
||||
releaseControlFlowBuild, err := acquireControlFlowBuild(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer releaseControlFlowBuild()
|
||||
|
||||
// generate config
|
||||
build, err := generate.GenerateConfig(name, profile.Config)
|
||||
build, err := generate.GenerateConfig(name, config)
|
||||
if err != nil {
|
||||
return nil, rpcError(err)
|
||||
}
|
||||
|
||||
var fPath string
|
||||
switch profile.Config.Format {
|
||||
switch config.Format {
|
||||
case clientpb.OutputFormat_SERVICE:
|
||||
fallthrough
|
||||
case clientpb.OutputFormat_EXECUTABLE:
|
||||
fPath, err = generate.SliverExecutable(name, build, profile.Config, httpC2Config.ImplantConfig)
|
||||
fPath, err = generate.SliverExecutable(name, build, config, httpC2Config.ImplantConfig)
|
||||
case clientpb.OutputFormat_SHARED_LIB:
|
||||
fPath, err = generate.SliverSharedLibrary(name, build, profile.Config, httpC2Config.ImplantConfig)
|
||||
fPath, err = generate.SliverSharedLibrary(name, build, config, httpC2Config.ImplantConfig)
|
||||
case clientpb.OutputFormat_GO_ARCHIVE:
|
||||
fPath, err = generate.SliverArchive(name, build, profile.Config, httpC2Config.ImplantConfig)
|
||||
fPath, err = generate.SliverArchive(name, build, config, httpC2Config.ImplantConfig)
|
||||
case clientpb.OutputFormat_SHELLCODE:
|
||||
fPath, err = generate.SliverShellcode(name, build, profile.Config, httpC2Config.ImplantConfig)
|
||||
fPath, err = generate.SliverShellcode(name, build, config, httpC2Config.ImplantConfig)
|
||||
default:
|
||||
return nil, status.Error(codes.InvalidArgument, fmt.Sprintf("invalid output format: %s", profile.Config.Format))
|
||||
return nil, status.Error(codes.InvalidArgument, fmt.Sprintf("invalid output format: %s", config.Format))
|
||||
}
|
||||
if err != nil {
|
||||
return nil, rpcError(err)
|
||||
@@ -1170,9 +1269,17 @@ func (rpc *Server) GenerateStage(ctx context.Context, req *clientpb.GenerateStag
|
||||
}
|
||||
}
|
||||
|
||||
err = generate.SaveStage(build, profile.Config, stage2, stageType)
|
||||
config, err = db.CreateImplantConfigWithID(config)
|
||||
if err != nil {
|
||||
return nil, rpcError(err)
|
||||
}
|
||||
|
||||
err = generate.SaveStageWithSourceProfile(build, config, stage2, stageType, sourceProfileID)
|
||||
if err != nil {
|
||||
rpcLog.Errorf("Failed to save external build: %s", err)
|
||||
if cleanupErr := db.DeleteFailedImplantConfigSnapshot(config.ID); cleanupErr != nil {
|
||||
rpcLog.Errorf("Failed to roll back config snapshot %s after stage save failure: %s", config.ID, cleanupErr)
|
||||
}
|
||||
return nil, rpcError(err)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
package rpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
"github.com/bishopfox/sliver/server/core"
|
||||
"github.com/bishopfox/sliver/server/generate"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
func TestGenerateExternalRejectsBuilderWithoutControlFlowCapability(t *testing.T) {
|
||||
builderName := "control-flow-incompatible-builder"
|
||||
if err := core.AddBuilder(&clientpb.Builder{
|
||||
Name: builderName,
|
||||
OperatorName: "control-flow-test-operator",
|
||||
}); err != nil {
|
||||
t.Fatalf("register test builder: %v", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
core.RemoveBuilder(builderName)
|
||||
})
|
||||
|
||||
rpc := &Server{}
|
||||
_, err := rpc.GenerateExternal(context.Background(), &clientpb.ExternalGenerateReq{
|
||||
Name: "control-flow-capability-test",
|
||||
BuilderName: builderName,
|
||||
Config: &clientpb.ImplantConfig{
|
||||
ObfuscateSymbols: true,
|
||||
ControlFlow: clientpb.ControlFlowPolicy_CONTROL_FLOW_BALANCED_V1,
|
||||
TemplateName: "sliver",
|
||||
},
|
||||
})
|
||||
if status.Code(err) != codes.FailedPrecondition {
|
||||
t.Fatalf("GenerateExternal() error = %v, code = %s; want %s", err, status.Code(err), codes.FailedPrecondition)
|
||||
}
|
||||
if !strings.Contains(err.Error(), "does not support garble.control-flow/balanced-v1") {
|
||||
t.Fatalf("GenerateExternal() error = %v, want control-flow capability detail", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateRejectsInvalidControlFlowConfigAsInvalidArgument(t *testing.T) {
|
||||
_, err := (&Server{}).Generate(context.Background(), &clientpb.GenerateReq{
|
||||
Name: "control-flow-invalid-config",
|
||||
Config: &clientpb.ImplantConfig{
|
||||
Debug: true,
|
||||
ObfuscateSymbols: true,
|
||||
ControlFlow: clientpb.ControlFlowPolicy_CONTROL_FLOW_BALANCED_V1,
|
||||
},
|
||||
})
|
||||
if status.Code(err) != codes.InvalidArgument {
|
||||
t.Fatalf("Generate() error = %v, code = %s; want %s", err, status.Code(err), codes.InvalidArgument)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaveImplantProfileRejectsInvalidControlFlowConfigAsInvalidArgument(t *testing.T) {
|
||||
_, err := (&Server{}).SaveImplantProfile(context.Background(), &clientpb.ImplantProfile{
|
||||
Name: "control-flow-invalid-profile",
|
||||
Config: &clientpb.ImplantConfig{
|
||||
ObfuscateSymbols: true,
|
||||
ControlFlow: clientpb.ControlFlowPolicy(99),
|
||||
},
|
||||
})
|
||||
if status.Code(err) != codes.InvalidArgument {
|
||||
t.Fatalf("SaveImplantProfile() error = %v, code = %s; want %s", err, status.Code(err), codes.InvalidArgument)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateRejectsConcurrentControlFlowBuild(t *testing.T) {
|
||||
config := &clientpb.ImplantConfig{
|
||||
ObfuscateSymbols: true,
|
||||
ControlFlow: clientpb.ControlFlowPolicy_CONTROL_FLOW_BALANCED_V1,
|
||||
TemplateName: "sliver",
|
||||
}
|
||||
release, err := generate.AcquireControlFlowBuildSlot(config)
|
||||
if err != nil {
|
||||
t.Fatalf("AcquireControlFlowBuildSlot() error = %v", err)
|
||||
}
|
||||
defer release()
|
||||
|
||||
_, err = (&Server{}).Generate(context.Background(), &clientpb.GenerateReq{
|
||||
Name: "control-flow-contended-build",
|
||||
Config: config,
|
||||
})
|
||||
if status.Code(err) != codes.ResourceExhausted {
|
||||
t.Fatalf("Generate() error = %v, code = %s; want %s", err, status.Code(err), codes.ResourceExhausted)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetCompilerAdvertisesProtocolAndVerifiedLocalControlFlowCapabilities(t *testing.T) {
|
||||
compiler, err := (&Server{}).GetCompiler(context.Background(), &commonpb.Empty{})
|
||||
if err != nil {
|
||||
t.Fatalf("GetCompiler() error = %v", err)
|
||||
}
|
||||
if !generate.HasControlFlowCapability(compiler.Capabilities) {
|
||||
t.Fatalf("GetCompiler() capabilities = %q, want %q", compiler.Capabilities, generate.ControlFlowCapability)
|
||||
}
|
||||
if !generate.HasLocalControlFlowCapability(compiler.Capabilities) {
|
||||
t.Fatalf("GetCompiler() capabilities = %q, want %q", compiler.Capabilities, generate.LocalControlFlowCapability)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
package rpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
consts "github.com/bishopfox/sliver/client/constants"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
"github.com/bishopfox/sliver/server/core"
|
||||
"github.com/bishopfox/sliver/server/db"
|
||||
"github.com/bishopfox/sliver/server/db/models"
|
||||
"github.com/bishopfox/sliver/server/generate"
|
||||
"github.com/gofrs/uuid"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
func TestExternalBuildFailedDeletesPendingBuildAndDetachedSnapshot(t *testing.T) {
|
||||
fixture := newPendingExternalBuildFixture(t, "external-failed")
|
||||
|
||||
builds, err := db.ImplantBuilds()
|
||||
if err != nil {
|
||||
t.Fatalf("ImplantBuilds() with a pending build: %v", err)
|
||||
}
|
||||
if builds.Configs[fixture.name] != nil {
|
||||
t.Fatalf("ImplantBuilds() exposed pending build %q as completed", fixture.name)
|
||||
}
|
||||
if builds.ResourceIDs[fixture.name] != nil {
|
||||
t.Fatalf("pending build %q unexpectedly has a resource ID", fixture.name)
|
||||
}
|
||||
|
||||
const operatorName = "external-failed-operator"
|
||||
core.TrackExternalBuildAssignment(fixture.buildID, "external-failed-builder", operatorName)
|
||||
t.Cleanup(func() { core.RemoveExternalBuildAssignment(fixture.buildID) })
|
||||
|
||||
_, err = (&Server{}).BuilderTrigger(contextWithCommonName(operatorName), &clientpb.Event{
|
||||
EventType: consts.ExternalBuildFailedEvent,
|
||||
Data: []byte(fixture.buildID + ":compiler failed"),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("BuilderTrigger(ExternalBuildFailedEvent) error = %v", err)
|
||||
}
|
||||
if assignment := core.GetExternalBuildAssignment(fixture.buildID); assignment != nil {
|
||||
t.Fatalf("external build assignment still exists after failure: %+v", assignment)
|
||||
}
|
||||
|
||||
assertExternalBuildFixtureCounts(t, fixture, 0, 0, 0)
|
||||
}
|
||||
|
||||
func TestExternalBuildFailedProtectsCompletedBuild(t *testing.T) {
|
||||
fixture := newPendingExternalBuildFixture(t, "external-completed")
|
||||
resourceID := uint64(time.Now().UnixNano())
|
||||
if err := db.SaveResourceID(&clientpb.ResourceID{Name: fixture.name, Value: resourceID}); err != nil {
|
||||
t.Fatalf("SaveResourceID() error = %v", err)
|
||||
}
|
||||
build, err := db.ImplantBuildByName(fixture.name)
|
||||
if err != nil {
|
||||
t.Fatalf("ImplantBuildByName() error = %v", err)
|
||||
}
|
||||
build.ImplantID = resourceID
|
||||
build.MD5 = "completed-md5"
|
||||
build.SHA1 = "completed-sha1"
|
||||
build.SHA256 = "completed-sha256"
|
||||
if _, err := db.SaveImplantBuild(build); err != nil {
|
||||
t.Fatalf("SaveImplantBuild() completed update error = %v", err)
|
||||
}
|
||||
|
||||
const operatorName = "external-completed-operator"
|
||||
core.TrackExternalBuildAssignment(fixture.buildID, "external-completed-builder", operatorName)
|
||||
t.Cleanup(func() { core.RemoveExternalBuildAssignment(fixture.buildID) })
|
||||
|
||||
_, err = (&Server{}).BuilderTrigger(contextWithCommonName(operatorName), &clientpb.Event{
|
||||
EventType: consts.ExternalBuildFailedEvent,
|
||||
Data: []byte(fixture.buildID + ":late failure"),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("BuilderTrigger(ExternalBuildFailedEvent) error = %v", err)
|
||||
}
|
||||
if assignment := core.GetExternalBuildAssignment(fixture.buildID); assignment != nil {
|
||||
t.Fatalf("external build assignment still exists after terminal failure: %+v", assignment)
|
||||
}
|
||||
|
||||
assertExternalBuildFixtureCounts(t, fixture, 1, 1, 1)
|
||||
}
|
||||
|
||||
func TestRemoveBuildByNameDeletesPendingBuildAndDetachedSnapshot(t *testing.T) {
|
||||
fixture := newPendingExternalBuildFixture(t, "external-remove-pending")
|
||||
core.TrackExternalBuildAssignment(fixture.buildID, "external-remove-builder", "external-remove-operator")
|
||||
t.Cleanup(func() { core.RemoveExternalBuildAssignment(fixture.buildID) })
|
||||
if err := RemoveBuildByName(fixture.name); err != nil {
|
||||
t.Fatalf("RemoveBuildByName() pending build error = %v", err)
|
||||
}
|
||||
if assignment := core.GetExternalBuildAssignment(fixture.buildID); assignment != nil {
|
||||
t.Fatalf("external build assignment still exists after removal: %+v", assignment)
|
||||
}
|
||||
assertExternalBuildFixtureCounts(t, fixture, 0, 0, 0)
|
||||
}
|
||||
|
||||
func TestGenerateExternalRejectsUnpersistedProfileAssociation(t *testing.T) {
|
||||
builderName := fmt.Sprintf("external-profile-trust-builder-%d", time.Now().UnixNano())
|
||||
if err := core.AddBuilder(&clientpb.Builder{Name: builderName, OperatorName: "external-profile-trust-operator"}); err != nil {
|
||||
t.Fatalf("AddBuilder() error = %v", err)
|
||||
}
|
||||
t.Cleanup(func() { core.RemoveBuilder(builderName) })
|
||||
|
||||
_, err := (&Server{}).GenerateExternal(context.Background(), &clientpb.ExternalGenerateReq{
|
||||
Name: fmt.Sprintf("external-profile-trust-%d", time.Now().UnixNano()),
|
||||
BuilderName: builderName,
|
||||
Config: &clientpb.ImplantConfig{
|
||||
ImplantProfileID: uuid.Must(uuid.NewV4()).String(),
|
||||
},
|
||||
})
|
||||
if status.Code(err) != codes.InvalidArgument {
|
||||
t.Fatalf("GenerateExternal() error = %v, code = %s; want %s", err, status.Code(err), codes.InvalidArgument)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveExternalBuildConfigUsesPersistedProfileAssociation(t *testing.T) {
|
||||
profileName := fmt.Sprintf("external-profile-source-%d", time.Now().UnixNano())
|
||||
profile, err := generate.SaveImplantProfile(&clientpb.ImplantProfile{
|
||||
Name: profileName,
|
||||
Config: &clientpb.ImplantConfig{
|
||||
GOOS: "linux",
|
||||
GOARCH: "amd64",
|
||||
TemplateName: generate.SliverTemplateName,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("SaveImplantProfile() error = %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = db.DeleteProfile(profileName) })
|
||||
|
||||
resolved, err := resolveExternalBuildConfig(&clientpb.ImplantConfig{
|
||||
ID: profile.Config.ID,
|
||||
ImplantProfileID: uuid.Must(uuid.NewV4()).String(),
|
||||
GOOS: "forged",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("resolveExternalBuildConfig() error = %v", err)
|
||||
}
|
||||
if resolved.GOOS != "linux" {
|
||||
t.Fatalf("resolved GOOS = %q, want persisted value %q", resolved.GOOS, "linux")
|
||||
}
|
||||
if resolved.ImplantProfileID != profile.ID {
|
||||
t.Fatalf("resolved profile ID = %q, want persisted association %q", resolved.ImplantProfileID, profile.ID)
|
||||
}
|
||||
}
|
||||
|
||||
type pendingExternalBuildFixture struct {
|
||||
name string
|
||||
buildID string
|
||||
configID uuid.UUID
|
||||
}
|
||||
|
||||
func newPendingExternalBuildFixture(t *testing.T, prefix string) pendingExternalBuildFixture {
|
||||
t.Helper()
|
||||
configID := uuid.Must(uuid.NewV4())
|
||||
name := fmt.Sprintf("%s-%d", prefix, time.Now().UnixNano())
|
||||
config, err := db.CreateImplantConfigWithID(&clientpb.ImplantConfig{
|
||||
ID: configID.String(),
|
||||
GOOS: "linux",
|
||||
GOARCH: "amd64",
|
||||
TemplateName: generate.SliverTemplateName,
|
||||
C2: []*clientpb.ImplantC2{{URL: "mtls://127.0.0.1:8888"}},
|
||||
CanaryDomains: []string{"pending.example"},
|
||||
Assets: []*commonpb.File{{Name: "pending-asset"}},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("CreateImplantConfigWithID() error = %v", err)
|
||||
}
|
||||
build, err := db.SaveImplantBuild(&clientpb.ImplantBuild{
|
||||
Name: name,
|
||||
ImplantConfigID: config.ID,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("SaveImplantBuild() pending build error = %v", err)
|
||||
}
|
||||
|
||||
fixture := pendingExternalBuildFixture{name: name, buildID: build.ID, configID: configID}
|
||||
t.Cleanup(func() { cleanupExternalBuildFixture(fixture) })
|
||||
return fixture
|
||||
}
|
||||
|
||||
func cleanupExternalBuildFixture(fixture pendingExternalBuildFixture) {
|
||||
buildID := uuid.FromStringOrNil(fixture.buildID)
|
||||
db.Session().Where("id = ?", buildID).Delete(&models.ImplantBuild{})
|
||||
db.Session().Where("name = ?", fixture.name).Delete(&models.ResourceID{})
|
||||
db.Session().Where("implant_config_id = ?", fixture.configID).Delete(&models.ImplantC2{})
|
||||
db.Session().Where("implant_config_id = ?", fixture.configID).Delete(&models.EncoderAsset{})
|
||||
db.Session().Where("implant_config_id = ?", fixture.configID).Delete(&models.CanaryDomain{})
|
||||
db.Session().Where("id = ?", fixture.configID).Delete(&models.ImplantConfig{})
|
||||
}
|
||||
|
||||
func assertExternalBuildFixtureCounts(t *testing.T, fixture pendingExternalBuildFixture, wantBuilds int64, wantConfigs int64, wantResources int64) {
|
||||
t.Helper()
|
||||
var buildCount int64
|
||||
if err := db.Session().Model(&models.ImplantBuild{}).Where("id = ?", uuid.FromStringOrNil(fixture.buildID)).Count(&buildCount).Error; err != nil {
|
||||
t.Fatalf("count implant builds: %v", err)
|
||||
}
|
||||
var configCount int64
|
||||
if err := db.Session().Model(&models.ImplantConfig{}).Where("id = ?", fixture.configID).Count(&configCount).Error; err != nil {
|
||||
t.Fatalf("count implant configs: %v", err)
|
||||
}
|
||||
var resourceCount int64
|
||||
if err := db.Session().Model(&models.ResourceID{}).Where("name = ?", fixture.name).Count(&resourceCount).Error; err != nil {
|
||||
t.Fatalf("count resource IDs: %v", err)
|
||||
}
|
||||
if buildCount != wantBuilds || configCount != wantConfigs || resourceCount != wantResources {
|
||||
t.Fatalf(
|
||||
"fixture counts = builds:%d configs:%d resources:%d, want builds:%d configs:%d resources:%d",
|
||||
buildCount, configCount, resourceCount, wantBuilds, wantConfigs, wantResources,
|
||||
)
|
||||
}
|
||||
|
||||
if wantConfigs == 0 {
|
||||
for name, model := range map[string]interface{}{
|
||||
"C2": &models.ImplantC2{},
|
||||
"assets": &models.EncoderAsset{},
|
||||
"canaries": &models.CanaryDomain{},
|
||||
} {
|
||||
var count int64
|
||||
if err := db.Session().Model(model).Where("implant_config_id = ?", fixture.configID).Count(&count).Error; err != nil {
|
||||
t.Fatalf("count %s associations: %v", name, err)
|
||||
}
|
||||
if count != 0 {
|
||||
t.Fatalf("%s association count = %d, want 0", name, count)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -20,9 +21,12 @@ import (
|
||||
"github.com/bishopfox/sliver/server/certs"
|
||||
"github.com/bishopfox/sliver/server/configs"
|
||||
"github.com/bishopfox/sliver/server/db"
|
||||
"github.com/bishopfox/sliver/server/db/models"
|
||||
generatePkg "github.com/bishopfox/sliver/server/generate"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/grpc/test/bufconn"
|
||||
)
|
||||
|
||||
@@ -72,6 +76,23 @@ func TestGenerateSpoofMetadataAppliesPETimestampOverBufnet(t *testing.T) {
|
||||
if len(origData) == 0 {
|
||||
t.Fatal("Generate() returned empty file data")
|
||||
}
|
||||
persistedBuild, err := db.ImplantBuildByName(buildName)
|
||||
if err != nil {
|
||||
t.Fatalf("load generated implant build: %v", err)
|
||||
}
|
||||
if persistedBuild.ImplantConfigID == "" {
|
||||
t.Fatal("generated implant build has an empty config ID")
|
||||
}
|
||||
persistedConfig, err := db.ImplantConfigByID(persistedBuild.ImplantConfigID)
|
||||
if err != nil {
|
||||
t.Fatalf("load generated implant config: %v", err)
|
||||
}
|
||||
if persistedConfig.ID != persistedBuild.ImplantConfigID {
|
||||
t.Fatalf("persisted config ID = %q, build config ID = %q", persistedConfig.ID, persistedBuild.ImplantConfigID)
|
||||
}
|
||||
if !bytes.Contains(origData, []byte(persistedConfig.ID)) {
|
||||
t.Fatalf("generated artifact does not contain persisted config ID %q", persistedConfig.ID)
|
||||
}
|
||||
origTimestamp, err := peFileTimestamp(origData)
|
||||
if err != nil {
|
||||
t.Fatalf("parse generated PE timestamp: %v", err)
|
||||
@@ -131,6 +152,93 @@ func TestGenerateSpoofMetadataAppliesPETimestampOverBufnet(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateCompileFailureDoesNotPersistPreselectedConfig(t *testing.T) {
|
||||
setupGenerateSpoofTest(t)
|
||||
var configCountBefore int64
|
||||
if err := db.Session().Model(&models.ImplantConfig{}).Count(&configCountBefore).Error; err != nil {
|
||||
t.Fatalf("count implant configs before Generate(): %v", err)
|
||||
}
|
||||
|
||||
buildName := fmt.Sprintf("rpc-config-id-failure-%d", time.Now().UnixNano())
|
||||
config := &clientpb.ImplantConfig{
|
||||
GOOS: "unsupported",
|
||||
GOARCH: "amd64",
|
||||
Format: clientpb.OutputFormat_EXECUTABLE,
|
||||
Debug: true,
|
||||
ObfuscateSymbols: false,
|
||||
C2: []*clientpb.ImplantC2{
|
||||
{URL: "http://127.0.0.1"},
|
||||
},
|
||||
HTTPC2ConfigName: consts.DefaultC2Profile,
|
||||
}
|
||||
|
||||
_, err := (&Server{}).Generate(context.Background(), &clientpb.GenerateReq{
|
||||
Name: buildName,
|
||||
Config: config,
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("Generate() unexpectedly succeeded for an unsupported compiler target")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "invalid compiler target") {
|
||||
t.Fatalf("Generate() error = %v, want invalid compiler target", err)
|
||||
}
|
||||
var configCountAfter int64
|
||||
if err := db.Session().Model(&models.ImplantConfig{}).Count(&configCountAfter).Error; err != nil {
|
||||
t.Fatalf("count implant configs after Generate(): %v", err)
|
||||
}
|
||||
if configCountAfter != configCountBefore {
|
||||
t.Fatalf("implant config count after failed Generate() = %d, want %d", configCountAfter, configCountBefore)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateUsesPersistedProfileBuildSettings(t *testing.T) {
|
||||
setupGenerateSpoofTest(t)
|
||||
profileName := fmt.Sprintf("rpc-profile-authority-%d", time.Now().UnixNano())
|
||||
profile, err := generatePkg.SaveImplantProfile(&clientpb.ImplantProfile{
|
||||
Name: profileName,
|
||||
Config: &clientpb.ImplantConfig{
|
||||
GOOS: "linux",
|
||||
GOARCH: "amd64",
|
||||
Format: clientpb.OutputFormat(99),
|
||||
TemplateName: generatePkg.SliverTemplateName,
|
||||
HTTPC2ConfigName: consts.DefaultC2Profile,
|
||||
ObfuscateSymbols: true,
|
||||
ConnectionStrategy: "s",
|
||||
C2: []*clientpb.ImplantC2{
|
||||
{URL: "http://127.0.0.1"},
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("SaveImplantProfile() error = %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = db.DeleteProfile(profileName) })
|
||||
|
||||
_, err = (&Server{}).Generate(context.Background(), &clientpb.GenerateReq{
|
||||
Name: fmt.Sprintf("rpc-profile-authority-build-%d", time.Now().UnixNano()),
|
||||
Config: &clientpb.ImplantConfig{
|
||||
ID: profile.Config.ID,
|
||||
Format: clientpb.OutputFormat_EXECUTABLE,
|
||||
HTTPC2ConfigName: "does-not-exist",
|
||||
},
|
||||
})
|
||||
if status.Code(err) != codes.InvalidArgument || !strings.Contains(err.Error(), "invalid output format") {
|
||||
t.Fatalf("Generate() error = %v, code = %s; want persisted profile's invalid output format", err, status.Code(err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateRejectsUnpersistedProfileAssociation(t *testing.T) {
|
||||
_, err := (&Server{}).Generate(context.Background(), &clientpb.GenerateReq{
|
||||
Name: fmt.Sprintf("rpc-profile-trust-%d", time.Now().UnixNano()),
|
||||
Config: &clientpb.ImplantConfig{
|
||||
ImplantProfileID: "502f6ad5-1ff1-49cc-bbb5-8a6ea4533661",
|
||||
},
|
||||
})
|
||||
if status.Code(err) != codes.InvalidArgument {
|
||||
t.Fatalf("Generate() error = %v, code = %s; want %s", err, status.Code(err), codes.InvalidArgument)
|
||||
}
|
||||
}
|
||||
|
||||
func setupGenerateSpoofTest(t *testing.T) {
|
||||
t.Helper()
|
||||
spoofGenerateSetupOnce.Do(func() {
|
||||
|
||||
@@ -106,6 +106,9 @@ func (rpc *Server) HijackDLL(ctx context.Context, req *clientpb.DllHijackReq) (*
|
||||
))
|
||||
}
|
||||
|
||||
if p.Config == nil {
|
||||
return nil, status.Error(codes.InvalidArgument, "profile is missing an implant config")
|
||||
}
|
||||
if p.Config.Format != clientpb.OutputFormat_SHARED_LIB {
|
||||
return nil, status.Error(codes.InvalidArgument,
|
||||
"please select a profile targeting a shared library format",
|
||||
@@ -123,6 +126,11 @@ func (rpc *Server) HijackDLL(ctx context.Context, req *clientpb.DllHijackReq) (*
|
||||
} else {
|
||||
name = req.Name
|
||||
}
|
||||
releaseControlFlowBuild, err := acquireControlFlowBuild(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer releaseControlFlowBuild()
|
||||
build, err := generate.GenerateConfig(name, config)
|
||||
if err != nil {
|
||||
return nil, rpcError(err)
|
||||
@@ -143,6 +151,7 @@ func (rpc *Server) HijackDLL(ctx context.Context, req *clientpb.DllHijackReq) (*
|
||||
if err != nil {
|
||||
return nil, rpcError(err)
|
||||
}
|
||||
releaseControlFlowBuild()
|
||||
} else {
|
||||
if len(req.TargetDLL) == 0 {
|
||||
return nil, status.Error(codes.InvalidArgument, "missing target DLL")
|
||||
|
||||
+36
-8
@@ -86,11 +86,25 @@ func (rpc *Server) GetSystem(ctx context.Context, req *clientpb.GetSystemReq) (*
|
||||
if req == nil || req.Request == nil {
|
||||
return nil, ErrMissingRequestField
|
||||
}
|
||||
if req.Config == nil {
|
||||
return nil, status.Error(codes.InvalidArgument, "missing implant config")
|
||||
}
|
||||
|
||||
session := core.Sessions.Get(req.Request.SessionID)
|
||||
if session == nil {
|
||||
return nil, ErrInvalidSessionID
|
||||
}
|
||||
if session.ConfigID != "" {
|
||||
originatingConfig, err := db.ImplantConfigByID(session.ConfigID)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.FailedPrecondition, "cannot verify originating implant policy for session config %q: %v", session.ConfigID, err)
|
||||
} else if originatingConfig.ControlFlow != clientpb.ControlFlowPolicy_CONTROL_FLOW_DISABLED {
|
||||
return nil, status.Error(codes.FailedPrecondition, "getsystem does not support control-flow-obfuscated helper builds")
|
||||
}
|
||||
}
|
||||
if req.Config.ControlFlow != clientpb.ControlFlowPolicy_CONTROL_FLOW_DISABLED {
|
||||
return nil, status.Error(codes.FailedPrecondition, "getsystem does not support control-flow-obfuscated helper builds")
|
||||
}
|
||||
|
||||
// retrieve http c2 implant config
|
||||
httpC2Config, err := db.LoadHTTPC2ConfigByName(req.Config.HTTPC2ConfigName)
|
||||
@@ -109,15 +123,10 @@ func (rpc *Server) GetSystem(ctx context.Context, req *clientpb.GetSystemReq) (*
|
||||
name = req.Name
|
||||
}
|
||||
|
||||
shellcode, _, err = getSliverShellcode(name)
|
||||
shellcode, _, _, err = getSliverShellcode(name)
|
||||
if err != nil {
|
||||
req.Config.Format = clientpb.OutputFormat_SHELLCODE
|
||||
req.Config.ObfuscateSymbols = false
|
||||
req.Config.IsShellcode = true
|
||||
req.Config.IsSharedLib = false
|
||||
req.Config.TemplateName = "sliver"
|
||||
if len(req.Config.Exports) == 0 {
|
||||
req.Config.Exports = []string{"StartW"}
|
||||
if err := prepareGetSystemFallbackConfig(req.Config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
build, err := generate.GenerateConfig(name, req.Config)
|
||||
if err != nil {
|
||||
@@ -151,6 +160,25 @@ func (rpc *Server) GetSystem(ctx context.Context, req *clientpb.GetSystemReq) (*
|
||||
return getSystem, nil
|
||||
}
|
||||
|
||||
func prepareGetSystemFallbackConfig(config *clientpb.ImplantConfig) error {
|
||||
if config == nil {
|
||||
return status.Error(codes.InvalidArgument, "missing implant config")
|
||||
}
|
||||
if config.ControlFlow != clientpb.ControlFlowPolicy_CONTROL_FLOW_DISABLED {
|
||||
return status.Error(codes.FailedPrecondition, "getsystem does not support control-flow-obfuscated helper builds")
|
||||
}
|
||||
|
||||
config.Format = clientpb.OutputFormat_SHELLCODE
|
||||
config.ObfuscateSymbols = false
|
||||
config.IsShellcode = true
|
||||
config.IsSharedLib = false
|
||||
config.TemplateName = "sliver"
|
||||
if len(config.Exports) == 0 {
|
||||
config.Exports = []string{"StartW"}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MakeToken - Creates a new logon session to impersonate a user based on its credentials.
|
||||
func (rpc *Server) MakeToken(ctx context.Context, req *sliverpb.MakeTokenReq) (*sliverpb.MakeToken, error) {
|
||||
resp := &sliverpb.MakeToken{Response: &commonpb.Response{}}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
package rpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
func TestGetSystemRejectsMissingImplantConfig(t *testing.T) {
|
||||
_, err := (&Server{}).GetSystem(context.Background(), &clientpb.GetSystemReq{
|
||||
Request: &commonpb.Request{SessionID: "unused"},
|
||||
})
|
||||
if status.Code(err) != codes.InvalidArgument {
|
||||
t.Fatalf("GetSystem() error = %v, code = %s; want %s", err, status.Code(err), codes.InvalidArgument)
|
||||
}
|
||||
if !strings.Contains(err.Error(), "missing implant config") {
|
||||
t.Fatalf("GetSystem() error = %v, want missing implant config detail", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrepareGetSystemFallbackConfigRejectsControlFlowWithoutDowngrade(t *testing.T) {
|
||||
config := &clientpb.ImplantConfig{
|
||||
ObfuscateSymbols: true,
|
||||
ControlFlow: clientpb.ControlFlowPolicy_CONTROL_FLOW_BALANCED_V1,
|
||||
Format: clientpb.OutputFormat_EXECUTABLE,
|
||||
IsSharedLib: true,
|
||||
TemplateName: "custom",
|
||||
}
|
||||
|
||||
err := prepareGetSystemFallbackConfig(config)
|
||||
if status.Code(err) != codes.FailedPrecondition {
|
||||
t.Fatalf("prepareGetSystemFallbackConfig() error = %v, code = %s; want %s", err, status.Code(err), codes.FailedPrecondition)
|
||||
}
|
||||
if !strings.Contains(err.Error(), "does not support control-flow-obfuscated helper builds") {
|
||||
t.Fatalf("prepareGetSystemFallbackConfig() error = %v, want control-flow detail", err)
|
||||
}
|
||||
if config.ControlFlow != clientpb.ControlFlowPolicy_CONTROL_FLOW_BALANCED_V1 || !config.ObfuscateSymbols {
|
||||
t.Fatalf("control-flow policy was downgraded: %+v", config)
|
||||
}
|
||||
if config.Format != clientpb.OutputFormat_EXECUTABLE || !config.IsSharedLib || config.TemplateName != "custom" {
|
||||
t.Fatalf("config was mutated before fallback rejection: %+v", config)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrepareGetSystemFallbackConfigPreservesDisabledBehavior(t *testing.T) {
|
||||
config := &clientpb.ImplantConfig{
|
||||
ObfuscateSymbols: true,
|
||||
Format: clientpb.OutputFormat_EXECUTABLE,
|
||||
IsSharedLib: true,
|
||||
}
|
||||
|
||||
if err := prepareGetSystemFallbackConfig(config); err != nil {
|
||||
t.Fatalf("prepareGetSystemFallbackConfig() error = %v", err)
|
||||
}
|
||||
if config.Format != clientpb.OutputFormat_SHELLCODE || config.ObfuscateSymbols || !config.IsShellcode || config.IsSharedLib {
|
||||
t.Fatalf("fallback config = %+v, want legacy shellcode settings", config)
|
||||
}
|
||||
if config.TemplateName != "sliver" {
|
||||
t.Fatalf("TemplateName = %q, want sliver", config.TemplateName)
|
||||
}
|
||||
if len(config.Exports) != 1 || config.Exports[0] != "StartW" {
|
||||
t.Fatalf("Exports = %q, want [StartW]", config.Exports)
|
||||
}
|
||||
}
|
||||
+66
-13
@@ -40,6 +40,7 @@ import (
|
||||
"github.com/bishopfox/sliver/server/generate"
|
||||
"github.com/bishopfox/sliver/server/log"
|
||||
"github.com/bishopfox/sliver/util"
|
||||
"github.com/gofrs/uuid"
|
||||
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
@@ -88,10 +89,42 @@ func (rpc *Server) Migrate(ctx context.Context, req *clientpb.MigrateReq) (*sliv
|
||||
}
|
||||
}
|
||||
|
||||
var originatingConfig *clientpb.ImplantConfig
|
||||
if session != nil && session.ConfigID != "" {
|
||||
originatingConfig, err = db.ImplantConfigByID(session.ConfigID)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.FailedPrecondition, "cannot verify originating implant policy for session config %q: %v", session.ConfigID, err)
|
||||
}
|
||||
} else if dbBeacon != nil && dbBeacon.ImplantConfigID != models.NilUUID() {
|
||||
originatingConfig, err = db.ImplantConfigByID(dbBeacon.ImplantConfigID.String())
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.FailedPrecondition, "cannot verify originating implant policy for beacon config %q: %v", dbBeacon.ImplantConfigID, err)
|
||||
}
|
||||
} else if req.Config != nil {
|
||||
originatingConfig = req.Config
|
||||
}
|
||||
|
||||
name := filepath.Base(req.Name)
|
||||
sc, arch, err := getSliverShellcode(name)
|
||||
sc, arch, cachedConfig, err := getSliverShellcode(name)
|
||||
if err == nil && !migrateCachedPolicyCompatible(originatingConfig, cachedConfig) {
|
||||
return nil, status.Errorf(
|
||||
codes.FailedPrecondition,
|
||||
"cached build %q control-flow policy %s does not match originating policy %s",
|
||||
name,
|
||||
cachedConfig.ControlFlow,
|
||||
originatingConfig.ControlFlow,
|
||||
)
|
||||
}
|
||||
if err != nil {
|
||||
if req.Config == nil {
|
||||
return nil, status.Error(codes.InvalidArgument, "missing implant config")
|
||||
}
|
||||
config := req.Config
|
||||
if originatingConfig != nil {
|
||||
config.Debug = originatingConfig.Debug
|
||||
config.ObfuscateSymbols = originatingConfig.ObfuscateSymbols
|
||||
config.ControlFlow = originatingConfig.ControlFlow
|
||||
}
|
||||
if req.Name == "" {
|
||||
name, err = codenames.GetCodename()
|
||||
if err != nil {
|
||||
@@ -108,6 +141,16 @@ func (rpc *Server) Migrate(ctx context.Context, req *clientpb.MigrateReq) (*sliv
|
||||
config.IsSharedLib = false
|
||||
config.TemplateName = "sliver"
|
||||
config.ObfuscateSymbols = true
|
||||
configID, err := uuid.NewV4()
|
||||
if err != nil {
|
||||
return nil, rpcError(err)
|
||||
}
|
||||
config.ID = configID.String()
|
||||
releaseControlFlowBuild, err := acquireControlFlowBuild(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer releaseControlFlowBuild()
|
||||
build, err := generate.GenerateConfig(name, config)
|
||||
if err != nil {
|
||||
return nil, rpcError(err)
|
||||
@@ -123,10 +166,13 @@ func (rpc *Server) Migrate(ctx context.Context, req *clientpb.MigrateReq) (*sliv
|
||||
if err != nil {
|
||||
return nil, rpcError(err)
|
||||
}
|
||||
sc, _ = os.ReadFile(shellcodePath)
|
||||
sc, err = os.ReadFile(shellcodePath)
|
||||
if err != nil {
|
||||
return nil, rpcError(err)
|
||||
}
|
||||
releaseControlFlowBuild()
|
||||
// Save the implant config in the database so that the server recognizes it when it tries to connect
|
||||
config.ID = ""
|
||||
savedConfig, err := db.SaveImplantConfig(config)
|
||||
savedConfig, err := db.CreateImplantConfigWithID(config)
|
||||
if err != nil {
|
||||
return nil, rpcError(err)
|
||||
}
|
||||
@@ -383,17 +429,24 @@ func getOS(session *core.Session, beacon *clientpb.Beacon) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func migrateCachedPolicyCompatible(originatingConfig, cachedConfig *clientpb.ImplantConfig) bool {
|
||||
if originatingConfig == nil {
|
||||
return true
|
||||
}
|
||||
return cachedConfig != nil && cachedConfig.ControlFlow == originatingConfig.ControlFlow
|
||||
}
|
||||
|
||||
// Utility functions
|
||||
func getSliverShellcode(name string) ([]byte, string, error) {
|
||||
func getSliverShellcode(name string) ([]byte, string, *clientpb.ImplantConfig, error) {
|
||||
var data []byte
|
||||
build, err := db.ImplantBuildByName(name)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
return nil, "", nil, err
|
||||
}
|
||||
|
||||
config, err := db.ImplantConfigByID(build.ImplantConfigID)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
return nil, "", nil, err
|
||||
}
|
||||
|
||||
switch config.Format {
|
||||
@@ -401,7 +454,7 @@ func getSliverShellcode(name string) ([]byte, string, error) {
|
||||
case clientpb.OutputFormat_SHELLCODE:
|
||||
fileData, err := generate.ImplantFileFromBuild(build)
|
||||
if err != nil {
|
||||
return []byte{}, "", err
|
||||
return []byte{}, "", config, err
|
||||
}
|
||||
data = fileData
|
||||
|
||||
@@ -410,23 +463,23 @@ func getSliverShellcode(name string) ([]byte, string, error) {
|
||||
fileData, err := generate.ImplantFileFromBuild(build)
|
||||
rpcLog.Debugf("Found implant. Len: %d\n", len(fileData))
|
||||
if err != nil {
|
||||
return []byte{}, "", err
|
||||
return []byte{}, "", config, err
|
||||
}
|
||||
data, err = generate.DonutShellcodeFromPE(fileData, config.GOARCH, false, "", "", "", false, false, false, config.ShellcodeConfig)
|
||||
if err != nil {
|
||||
rpcLog.Errorf("DonutShellcodeFromPE error: %v\n", err)
|
||||
return []byte{}, "", err
|
||||
return []byte{}, "", config, err
|
||||
}
|
||||
|
||||
case clientpb.OutputFormat_SHARED_LIB:
|
||||
// retrieve DLL from db
|
||||
fileData, err := generate.ImplantFileFromBuild(build)
|
||||
if err != nil {
|
||||
return []byte{}, "", err
|
||||
return []byte{}, "", config, err
|
||||
}
|
||||
data, err = generate.ShellcodeRDIFromBytes(fileData, "StartW", "")
|
||||
if err != nil {
|
||||
return []byte{}, "", err
|
||||
return []byte{}, "", config, err
|
||||
}
|
||||
|
||||
case clientpb.OutputFormat_SERVICE:
|
||||
@@ -435,7 +488,7 @@ func getSliverShellcode(name string) ([]byte, string, error) {
|
||||
err = fmt.Errorf("no existing shellcode found")
|
||||
}
|
||||
|
||||
return data, config.GOARCH, err
|
||||
return data, config.GOARCH, config, err
|
||||
}
|
||||
|
||||
// ExportDirectory - stores the Export data
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package rpc
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
)
|
||||
|
||||
func TestMigrateCachedPolicyCompatible(t *testing.T) {
|
||||
disabled := &clientpb.ImplantConfig{ControlFlow: clientpb.ControlFlowPolicy_CONTROL_FLOW_DISABLED}
|
||||
balanced := &clientpb.ImplantConfig{ControlFlow: clientpb.ControlFlowPolicy_CONTROL_FLOW_BALANCED_V1}
|
||||
|
||||
if !migrateCachedPolicyCompatible(nil, disabled) {
|
||||
t.Fatal("unknown originating policy should preserve legacy cache behavior")
|
||||
}
|
||||
if !migrateCachedPolicyCompatible(balanced, balanced) {
|
||||
t.Fatal("matching control-flow policies should permit cache reuse")
|
||||
}
|
||||
if migrateCachedPolicyCompatible(balanced, disabled) {
|
||||
t.Fatal("balanced originating policy accepted a disabled cached build")
|
||||
}
|
||||
if migrateCachedPolicyCompatible(disabled, balanced) {
|
||||
t.Fatal("disabled originating policy accepted a balanced cached build")
|
||||
}
|
||||
if migrateCachedPolicyCompatible(balanced, nil) {
|
||||
t.Fatal("known originating policy accepted a cache entry without config metadata")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
# Control-flow obfuscation integration harness
|
||||
|
||||
This opt-in harness builds and runs native Darwin/arm64 session and beacon
|
||||
executables through the public multiplayer RPC API. It verifies:
|
||||
|
||||
- the server advertises `garble.control-flow/balanced-v1`;
|
||||
- both builds use symbol obfuscation and `CONTROL_FLOW_BALANCED_V1`;
|
||||
- the three balanced-v1 directives are attached to the expected rendered
|
||||
functions;
|
||||
- each build is captured with Garble's debug output and produces one parsed,
|
||||
flattened `GARBLE_controlflow.go` file for each of the three selected
|
||||
packages;
|
||||
- the session produces a matching `SessionOpened` event and answers a
|
||||
synchronous Ping with the requested nonce;
|
||||
- both implants use randomized C2 selection, exercising the transformed
|
||||
`randomCCDomain` before their single mTLS endpoint is selected;
|
||||
- the session successfully lists a harness-owned temporary proof file through
|
||||
a wildcard path, exercising the transformed `determineDirPathFilter`; and
|
||||
- the beacon produces a matching `BeaconRegistered` event and completes a
|
||||
nonempty asynchronous Ping task with the requested nonce, exercising the
|
||||
transformed `registerSliver` during registration.
|
||||
|
||||
The server, operator files, generated source, compiler cache, logs, and
|
||||
artifacts live under a unique temporary directory. Multiplayer and implant
|
||||
mTLS listeners bind dynamic loopback-only ports. Every long-lived child is put
|
||||
in its own process group, and cleanup signals only those owned groups.
|
||||
|
||||
## Run with a prebuilt server
|
||||
|
||||
Build the current branch using the repository's normal command, then pass the
|
||||
result to the harness:
|
||||
|
||||
```sh
|
||||
make
|
||||
go run -tags=sliver_controlflow_e2e ./test/controlflow \
|
||||
--server /absolute/path/to/sliver-server
|
||||
```
|
||||
|
||||
## Let the harness build the server
|
||||
|
||||
If the embedded assets are already populated (run `make` once if needed), omit
|
||||
`--server`. The harness performs an isolated `go build` of the current checkout
|
||||
and does not write its server binary or Go build cache into the repository:
|
||||
|
||||
```sh
|
||||
go run -tags=sliver_controlflow_e2e ./test/controlflow
|
||||
```
|
||||
|
||||
Use `--keep-work-dir` to retain successful artifacts and logs. Failed runs are
|
||||
always preserved and print their work-directory path. The harness refuses to
|
||||
run on a host other than native Darwin/arm64 because callback verification
|
||||
executes the generated binaries directly.
|
||||
@@ -0,0 +1,217 @@
|
||||
//go:build sliver_controlflow_e2e
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
consts "github.com/bishopfox/sliver/client/constants"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/rpcpb"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
type eventPump struct {
|
||||
events chan *clientpb.Event
|
||||
errors chan error
|
||||
}
|
||||
|
||||
func startEventPump(stream rpcpb.SliverRPC_EventsClient) *eventPump {
|
||||
pump := &eventPump{
|
||||
events: make(chan *clientpb.Event, 256),
|
||||
errors: make(chan error, 1),
|
||||
}
|
||||
go func() {
|
||||
defer close(pump.events)
|
||||
for {
|
||||
event, err := stream.Recv()
|
||||
if err != nil {
|
||||
pump.errors <- err
|
||||
return
|
||||
}
|
||||
select {
|
||||
case pump.events <- event:
|
||||
case <-stream.Context().Done():
|
||||
pump.errors <- stream.Context().Err()
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
return pump
|
||||
}
|
||||
|
||||
func waitForListenerReady(ctx context.Context, pump *eventPump, jobID uint32, address string) error {
|
||||
ticker := time.NewTicker(listenerPollInterval)
|
||||
defer ticker.Stop()
|
||||
started := false
|
||||
for {
|
||||
if started {
|
||||
connection, err := net.DialTimeout("tcp", address, listenerDialTimeout)
|
||||
if err == nil {
|
||||
_ = connection.Close()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case event, ok := <-pump.events:
|
||||
if !ok {
|
||||
return eventStreamError(pump, "event stream ended while starting implant mTLS listener")
|
||||
}
|
||||
if event.Job == nil || event.Job.ID != jobID {
|
||||
continue
|
||||
}
|
||||
switch event.EventType {
|
||||
case consts.JobStartedEvent:
|
||||
started = true
|
||||
case consts.JobStoppedEvent:
|
||||
if event.Err != "" {
|
||||
return fmt.Errorf("implant mTLS listener job %d stopped: %s", jobID, event.Err)
|
||||
}
|
||||
return fmt.Errorf("implant mTLS listener job %d stopped before accepting connections", jobID)
|
||||
}
|
||||
case <-ticker.C:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func waitForMatchingSession(
|
||||
ctx context.Context,
|
||||
pump *eventPump,
|
||||
process *managedProcess,
|
||||
listenerJobID uint32,
|
||||
implantName string,
|
||||
) (*clientpb.Event, error) {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
case <-process.done:
|
||||
for {
|
||||
select {
|
||||
case event, ok := <-pump.events:
|
||||
if !ok {
|
||||
return nil, eventStreamError(pump, "event stream ended while waiting for SessionOpened")
|
||||
}
|
||||
matched, err := matchingSession(event, listenerJobID, implantName)
|
||||
if matched || err != nil {
|
||||
return event, err
|
||||
}
|
||||
default:
|
||||
return nil, process.failure("generated session exited before SessionOpened")
|
||||
}
|
||||
}
|
||||
case event, ok := <-pump.events:
|
||||
if !ok {
|
||||
return nil, eventStreamError(pump, "event stream ended while waiting for SessionOpened")
|
||||
}
|
||||
matched, err := matchingSession(event, listenerJobID, implantName)
|
||||
if matched || err != nil {
|
||||
return event, err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func matchingSession(event *clientpb.Event, listenerJobID uint32, implantName string) (bool, error) {
|
||||
if err := listenerStoppedError(event, listenerJobID); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if event.EventType != consts.SessionOpenedEvent || event.Session == nil || event.Session.Name != implantName {
|
||||
return false, nil
|
||||
}
|
||||
if event.Session.OS != targetOS || event.Session.Arch != targetArch {
|
||||
return true, fmt.Errorf("session target = %s/%s, want %s/%s", event.Session.OS, event.Session.Arch, targetOS, targetArch)
|
||||
}
|
||||
if event.Session.ID == "" {
|
||||
return true, errors.New("matching SessionOpened event has an empty ID")
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func waitForMatchingBeacon(
|
||||
ctx context.Context,
|
||||
pump *eventPump,
|
||||
process *managedProcess,
|
||||
listenerJobID uint32,
|
||||
implantName string,
|
||||
) (*clientpb.Beacon, error) {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
case <-process.done:
|
||||
for {
|
||||
select {
|
||||
case event, ok := <-pump.events:
|
||||
if !ok {
|
||||
return nil, eventStreamError(pump, "event stream ended while waiting for BeaconRegistered")
|
||||
}
|
||||
beacon, matched, err := matchingBeacon(event, listenerJobID, implantName)
|
||||
if matched || err != nil {
|
||||
return beacon, err
|
||||
}
|
||||
default:
|
||||
return nil, process.failure("generated beacon exited before BeaconRegistered")
|
||||
}
|
||||
}
|
||||
case event, ok := <-pump.events:
|
||||
if !ok {
|
||||
return nil, eventStreamError(pump, "event stream ended while waiting for BeaconRegistered")
|
||||
}
|
||||
beacon, matched, err := matchingBeacon(event, listenerJobID, implantName)
|
||||
if matched || err != nil {
|
||||
return beacon, err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func matchingBeacon(event *clientpb.Event, listenerJobID uint32, implantName string) (*clientpb.Beacon, bool, error) {
|
||||
if err := listenerStoppedError(event, listenerJobID); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if event.EventType != consts.BeaconRegisteredEvent {
|
||||
return nil, false, nil
|
||||
}
|
||||
beacon := &clientpb.Beacon{}
|
||||
if err := proto.Unmarshal(event.Data, beacon); err != nil {
|
||||
return nil, false, fmt.Errorf("decode BeaconRegistered event: %w", err)
|
||||
}
|
||||
if beacon.Name != implantName {
|
||||
return nil, false, nil
|
||||
}
|
||||
if beacon.OS != targetOS || beacon.Arch != targetArch {
|
||||
return beacon, true, fmt.Errorf("beacon target = %s/%s, want %s/%s", beacon.OS, beacon.Arch, targetOS, targetArch)
|
||||
}
|
||||
if beacon.ID == "" {
|
||||
return beacon, true, errors.New("matching BeaconRegistered event has an empty ID")
|
||||
}
|
||||
return beacon, true, nil
|
||||
}
|
||||
|
||||
func listenerStoppedError(event *clientpb.Event, jobID uint32) error {
|
||||
if event.EventType != consts.JobStoppedEvent || event.Job == nil || event.Job.ID != jobID {
|
||||
return nil
|
||||
}
|
||||
if event.Err != "" {
|
||||
return fmt.Errorf("implant mTLS listener job %d stopped: %s", jobID, event.Err)
|
||||
}
|
||||
return fmt.Errorf("implant mTLS listener job %d stopped before implant verification completed", jobID)
|
||||
}
|
||||
|
||||
func eventStreamError(pump *eventPump, message string) error {
|
||||
select {
|
||||
case err := <-pump.errors:
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: %w", message, err)
|
||||
}
|
||||
default:
|
||||
}
|
||||
return errors.New(message)
|
||||
}
|
||||
@@ -0,0 +1,791 @@
|
||||
//go:build sliver_controlflow_e2e
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
clientassets "github.com/bishopfox/sliver/client/assets"
|
||||
consts "github.com/bishopfox/sliver/client/constants"
|
||||
clienttransport "github.com/bishopfox/sliver/client/transport"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
"github.com/bishopfox/sliver/protobuf/rpcpb"
|
||||
"github.com/bishopfox/sliver/protobuf/sliverpb"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
const (
|
||||
operatorName = "controlflowe2e"
|
||||
targetOS = "darwin"
|
||||
targetArch = "arm64"
|
||||
controlFlowCapability = "garble.control-flow/balanced-v1"
|
||||
processLogTailBytes = 1024 * 1024
|
||||
listenerPollInterval = 250 * time.Millisecond
|
||||
listenerDialTimeout = 500 * time.Millisecond
|
||||
cleanupGraceTimeout = 2 * time.Second
|
||||
cleanupProcessTimeout = 10 * time.Second
|
||||
sessionPingNonce = int32(0x534553)
|
||||
beaconPingNonce = int32(0x424543)
|
||||
)
|
||||
|
||||
type options struct {
|
||||
repoPath string
|
||||
serverPath string
|
||||
timeout time.Duration
|
||||
startupTimeout time.Duration
|
||||
generateTimeout time.Duration
|
||||
callbackTimeout time.Duration
|
||||
keepWorkDir bool
|
||||
}
|
||||
|
||||
type testLayout struct {
|
||||
workDir string
|
||||
serverRoot string
|
||||
clientRoot string
|
||||
homeDir string
|
||||
tempDir string
|
||||
logDir string
|
||||
artifactDir string
|
||||
garbleDebugDir string
|
||||
}
|
||||
|
||||
type generatedArtifact struct {
|
||||
name string
|
||||
path string
|
||||
data []byte
|
||||
}
|
||||
|
||||
func main() {
|
||||
opts := options{}
|
||||
flag.StringVar(&opts.repoPath, "repo", ".", "path to the Sliver repository")
|
||||
flag.StringVar(&opts.serverPath, "server", "", "prebuilt Sliver server executable; omitted builds the current checkout into the isolated work directory")
|
||||
flag.DurationVar(&opts.timeout, "timeout", 90*time.Minute, "overall integration timeout")
|
||||
flag.DurationVar(&opts.startupTimeout, "startup-timeout", 10*time.Minute, "Sliver daemon startup timeout")
|
||||
flag.DurationVar(&opts.generateTimeout, "generate-timeout", 40*time.Minute, "timeout for each Garble implant build")
|
||||
flag.DurationVar(&opts.callbackTimeout, "callback-timeout", 5*time.Minute, "timeout for each implant callback and Ping verification")
|
||||
flag.BoolVar(&opts.keepWorkDir, "keep-work-dir", false, "preserve the isolated work directory after success (failed runs are always preserved)")
|
||||
flag.Parse()
|
||||
|
||||
if err := run(opts); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "control-flow integration failed: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func run(opts options) (retErr error) {
|
||||
if err := validateOptions(&opts); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
layout, err := newTestLayout()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("Control-flow integration work directory: %s\n", layout.workDir)
|
||||
defer func() {
|
||||
if opts.keepWorkDir || retErr != nil {
|
||||
fmt.Printf("Preserved integration work directory: %s\n", layout.workDir)
|
||||
return
|
||||
}
|
||||
if err := os.RemoveAll(layout.workDir); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "warning: remove integration work directory: %v\n", err)
|
||||
}
|
||||
}()
|
||||
|
||||
isolation := map[string]string{
|
||||
"HOME": layout.homeDir,
|
||||
"SLIVER_CLIENT_ROOT_DIR": layout.clientRoot,
|
||||
"SLIVER_GARBLE_DEBUG_DIR": layout.garbleDebugDir,
|
||||
"SLIVER_ROOT_DIR": layout.serverRoot,
|
||||
"TMPDIR": layout.tempDir,
|
||||
"USERPROFILE": layout.homeDir,
|
||||
}
|
||||
restoreEnvironment, err := setProcessEnvironment(isolation)
|
||||
if err != nil {
|
||||
return fmt.Errorf("set isolated process environment: %w", err)
|
||||
}
|
||||
defer restoreEnvironment()
|
||||
isolatedEnv := environmentWith(os.Environ(), isolation)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), opts.timeout)
|
||||
defer cancel()
|
||||
|
||||
serverPath := opts.serverPath
|
||||
if serverPath == "" {
|
||||
serverPath, err = buildServer(ctx, opts.repoPath, layout, isolatedEnv)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("Built Sliver server from current checkout: %s\n", serverPath)
|
||||
} else {
|
||||
fmt.Printf("Using prebuilt Sliver server: %s\n", serverPath)
|
||||
}
|
||||
|
||||
multiplayerPort, implantPort, err := twoUnusedTCPPorts()
|
||||
if err != nil {
|
||||
return fmt.Errorf("select loopback ports: %w", err)
|
||||
}
|
||||
|
||||
serverLog := filepath.Join(layout.logDir, "sliver-server.log")
|
||||
serverProcess, err := startProcess(
|
||||
serverPath,
|
||||
[]string{
|
||||
"daemon",
|
||||
"--lhost", "127.0.0.1",
|
||||
"--lport", fmt.Sprintf("%d", multiplayerPort),
|
||||
"--force",
|
||||
},
|
||||
opts.repoPath,
|
||||
isolatedEnv,
|
||||
serverLog,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("start Sliver daemon: %w", err)
|
||||
}
|
||||
defer serverProcess.stop()
|
||||
defer func() {
|
||||
if retErr != nil {
|
||||
retErr = fmt.Errorf("%w\nSliver server log:\n%s", retErr, readLogTail(serverLog))
|
||||
}
|
||||
}()
|
||||
|
||||
fmt.Printf("Started Sliver daemon on 127.0.0.1:%d\n", multiplayerPort)
|
||||
startupCtx, startupCancel := context.WithTimeout(ctx, opts.startupTimeout)
|
||||
err = waitForTCP(startupCtx, fmt.Sprintf("127.0.0.1:%d", multiplayerPort), serverProcess)
|
||||
startupCancel()
|
||||
if err != nil {
|
||||
return fmt.Errorf("wait for Sliver daemon: %w", err)
|
||||
}
|
||||
profilePath := filepath.Join(layout.clientRoot, operatorName+".cfg")
|
||||
profileCtx, profileCancel := context.WithTimeout(ctx, 2*time.Minute)
|
||||
profileOutput, profileErr := runOwnedCommand(
|
||||
profileCtx,
|
||||
opts.repoPath,
|
||||
isolatedEnv,
|
||||
serverPath,
|
||||
"operator",
|
||||
"--name", operatorName,
|
||||
"--lhost", "127.0.0.1",
|
||||
"--lport", fmt.Sprintf("%d", multiplayerPort),
|
||||
"--permissions", "all",
|
||||
"--save", profilePath,
|
||||
)
|
||||
profileCancel()
|
||||
if profileErr != nil {
|
||||
return fmt.Errorf("generate operator profile: %w\n%s", profileErr, profileOutput)
|
||||
}
|
||||
if _, err := os.Stat(profilePath); err != nil {
|
||||
return fmt.Errorf("operator command did not create %q: %w\n%s", profilePath, err, profileOutput)
|
||||
}
|
||||
|
||||
clientConfig, err := clientassets.ReadConfig(profilePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read operator profile: %w", err)
|
||||
}
|
||||
if clientConfig.Operator != operatorName || clientConfig.LHost != "127.0.0.1" || clientConfig.LPort != multiplayerPort {
|
||||
return fmt.Errorf(
|
||||
"operator profile mismatch: got operator=%q endpoint=%s:%d",
|
||||
clientConfig.Operator,
|
||||
clientConfig.LHost,
|
||||
clientConfig.LPort,
|
||||
)
|
||||
}
|
||||
|
||||
clienttransport.SetMultiplayerConnectMode(clienttransport.MultiplayerConnectDirect)
|
||||
rpc, conn, err := clienttransport.MTLSConnect(clientConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("connect to multiplayer gRPC: %w", err)
|
||||
}
|
||||
defer clienttransport.CloseGRPCConnection(conn)
|
||||
|
||||
version, err := rpc.GetVersion(ctx, &commonpb.Empty{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("query Sliver server version: %w", err)
|
||||
}
|
||||
fmt.Printf("Connected to Sliver server %s/%s over multiplayer mTLS\n", version.OS, version.Arch)
|
||||
if err := verifyCompilerCapability(ctx, rpc); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
eventsCtx, eventsCancel := context.WithCancel(ctx)
|
||||
defer eventsCancel()
|
||||
eventStream, err := rpc.Events(eventsCtx, &commonpb.Empty{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("subscribe to Sliver events: %w", err)
|
||||
}
|
||||
pump := startEventPump(eventStream)
|
||||
|
||||
listener, err := rpc.StartMTLSListener(ctx, &clientpb.MTLSListenerReq{
|
||||
Host: "127.0.0.1",
|
||||
Port: uint32(implantPort),
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("start implant mTLS listener: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
cleanupCtx, cleanupCancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cleanupCancel()
|
||||
_, _ = rpc.KillJob(cleanupCtx, &clientpb.KillJobReq{ID: listener.JobID})
|
||||
}()
|
||||
|
||||
listenerCtx, listenerCancel := context.WithTimeout(ctx, 30*time.Second)
|
||||
err = waitForListenerReady(listenerCtx, pump, listener.JobID, fmt.Sprintf("127.0.0.1:%d", implantPort))
|
||||
listenerCancel()
|
||||
if err != nil {
|
||||
return fmt.Errorf("wait for implant mTLS listener: %w", err)
|
||||
}
|
||||
fmt.Printf("Started implant mTLS listener on 127.0.0.1:%d (job %d)\n", implantPort, listener.JobID)
|
||||
|
||||
sessionArtifact, err := generateArtifact(ctx, opts.generateTimeout, rpc, layout, implantPort, "cfoe2esession", false)
|
||||
if err != nil {
|
||||
return fmt.Errorf("generate control-flow session: %w", err)
|
||||
}
|
||||
fmt.Printf("Generated control-flow session %s (%d bytes)\n", sessionArtifact.path, len(sessionArtifact.data))
|
||||
|
||||
sessionLog := filepath.Join(layout.logDir, "session.log")
|
||||
sessionProcess, err := startProcess(sessionArtifact.path, nil, layout.artifactDir, isolatedEnv, sessionLog)
|
||||
if err != nil {
|
||||
return fmt.Errorf("start generated session: %w", err)
|
||||
}
|
||||
defer sessionProcess.stop()
|
||||
|
||||
sessionCtx, sessionCancel := context.WithTimeout(ctx, opts.callbackTimeout)
|
||||
sessionEvent, err := waitForMatchingSession(
|
||||
sessionCtx,
|
||||
pump,
|
||||
sessionProcess,
|
||||
listener.JobID,
|
||||
sessionArtifact.name,
|
||||
)
|
||||
if err == nil {
|
||||
err = verifySessionPing(sessionCtx, rpc, sessionEvent.Session.ID)
|
||||
}
|
||||
if err == nil {
|
||||
err = verifySessionFilteredLs(sessionCtx, rpc, sessionEvent.Session.ID, layout.tempDir)
|
||||
}
|
||||
sessionCancel()
|
||||
if err != nil {
|
||||
return fmt.Errorf("verify generated session callback: %w\nsession log:\n%s", err, readLogTail(sessionLog))
|
||||
}
|
||||
fmt.Printf("Verified SessionOpened %s and synchronous Ping nonce %d\n", sessionEvent.Session.ID, sessionPingNonce)
|
||||
sessionProcess.stop()
|
||||
|
||||
beaconArtifact, err := generateArtifact(ctx, opts.generateTimeout, rpc, layout, implantPort, "cfoe2ebeacon", true)
|
||||
if err != nil {
|
||||
return fmt.Errorf("generate control-flow beacon: %w", err)
|
||||
}
|
||||
fmt.Printf("Generated control-flow beacon %s (%d bytes)\n", beaconArtifact.path, len(beaconArtifact.data))
|
||||
|
||||
beaconLog := filepath.Join(layout.logDir, "beacon.log")
|
||||
beaconProcess, err := startProcess(beaconArtifact.path, nil, layout.artifactDir, isolatedEnv, beaconLog)
|
||||
if err != nil {
|
||||
return fmt.Errorf("start generated beacon: %w", err)
|
||||
}
|
||||
defer beaconProcess.stop()
|
||||
|
||||
beaconCtx, beaconCancel := context.WithTimeout(ctx, opts.callbackTimeout)
|
||||
beacon, err := waitForMatchingBeacon(
|
||||
beaconCtx,
|
||||
pump,
|
||||
beaconProcess,
|
||||
listener.JobID,
|
||||
beaconArtifact.name,
|
||||
)
|
||||
if err == nil {
|
||||
err = verifyBeaconPing(beaconCtx, rpc, beaconProcess, beacon.ID)
|
||||
}
|
||||
beaconCancel()
|
||||
if err != nil {
|
||||
return fmt.Errorf("verify generated beacon callback: %w\nbeacon log:\n%s", err, readLogTail(beaconLog))
|
||||
}
|
||||
fmt.Printf("Verified BeaconRegistered %s and completed asynchronous Ping nonce %d\n", beacon.ID, beaconPingNonce)
|
||||
beaconProcess.stop()
|
||||
|
||||
fmt.Println("PASS: Darwin/arm64 control-flow session and beacon both connected and answered Ping")
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateOptions(opts *options) error {
|
||||
if runtime.GOOS != targetOS || runtime.GOARCH != targetArch {
|
||||
return fmt.Errorf("control-flow callback harness must run natively on %s/%s, current host is %s/%s", targetOS, targetArch, runtime.GOOS, runtime.GOARCH)
|
||||
}
|
||||
if opts.timeout <= 0 || opts.startupTimeout <= 0 || opts.generateTimeout <= 0 || opts.callbackTimeout <= 0 {
|
||||
return errors.New("timeouts must be positive")
|
||||
}
|
||||
|
||||
repoPath, err := filepath.Abs(opts.repoPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("resolve repository path: %w", err)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(repoPath, "go.mod")); err != nil {
|
||||
return fmt.Errorf("repository does not contain go.mod: %w", err)
|
||||
}
|
||||
opts.repoPath = repoPath
|
||||
|
||||
if opts.serverPath == "" {
|
||||
return validateEmbeddedAssetInputs(repoPath)
|
||||
}
|
||||
serverPath, err := filepath.Abs(opts.serverPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("resolve server executable: %w", err)
|
||||
}
|
||||
stat, err := os.Stat(serverPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("stat server executable: %w", err)
|
||||
}
|
||||
if stat.IsDir() {
|
||||
return fmt.Errorf("server executable %q is a directory", serverPath)
|
||||
}
|
||||
if stat.Mode()&0o111 == 0 {
|
||||
return fmt.Errorf("server executable %q is not executable", serverPath)
|
||||
}
|
||||
opts.serverPath = serverPath
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateEmbeddedAssetInputs(repoPath string) error {
|
||||
required := []string{
|
||||
filepath.Join("server", "assets", "fs", "src.zip"),
|
||||
filepath.Join("server", "assets", "fs", targetOS, targetArch, "go.zip"),
|
||||
filepath.Join("server", "assets", "fs", targetOS, targetArch, "garble"),
|
||||
}
|
||||
for _, relativePath := range required {
|
||||
if _, err := os.Stat(filepath.Join(repoPath, relativePath)); err != nil {
|
||||
return fmt.Errorf("server build asset %q is unavailable: %w; run make once to populate embedded assets or pass --server", relativePath, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func newTestLayout() (*testLayout, error) {
|
||||
workDir, err := os.MkdirTemp("", "sliver-controlflow-e2e-")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create integration work directory: %w", err)
|
||||
}
|
||||
layout := &testLayout{
|
||||
workDir: workDir,
|
||||
serverRoot: filepath.Join(workDir, "server-root"),
|
||||
clientRoot: filepath.Join(workDir, "client-root"),
|
||||
homeDir: filepath.Join(workDir, "home"),
|
||||
tempDir: filepath.Join(workDir, "tmp"),
|
||||
logDir: filepath.Join(workDir, "logs"),
|
||||
artifactDir: filepath.Join(workDir, "artifacts"),
|
||||
garbleDebugDir: filepath.Join(workDir, "garble-debug"),
|
||||
}
|
||||
for _, dir := range []string{
|
||||
layout.serverRoot,
|
||||
layout.clientRoot,
|
||||
layout.homeDir,
|
||||
layout.tempDir,
|
||||
layout.logDir,
|
||||
layout.artifactDir,
|
||||
} {
|
||||
if err := os.MkdirAll(dir, 0o700); err != nil {
|
||||
_ = os.RemoveAll(workDir)
|
||||
return nil, fmt.Errorf("create isolated directory %q: %w", dir, err)
|
||||
}
|
||||
}
|
||||
return layout, nil
|
||||
}
|
||||
|
||||
func buildServer(ctx context.Context, repoPath string, layout *testLayout, isolatedEnv []string) (string, error) {
|
||||
binDir := filepath.Join(layout.workDir, "bin")
|
||||
goCache := filepath.Join(layout.workDir, "server-build-cache")
|
||||
goPath := filepath.Join(layout.workDir, "server-build-gopath")
|
||||
goModCache := filepath.Join(layout.workDir, "server-build-modcache")
|
||||
for _, dir := range []string{binDir, goCache, goPath, goModCache} {
|
||||
if err := os.MkdirAll(dir, 0o700); err != nil {
|
||||
return "", fmt.Errorf("create server build directory: %w", err)
|
||||
}
|
||||
}
|
||||
serverPath := filepath.Join(binDir, "sliver-server")
|
||||
buildEnv := environmentWith(isolatedEnv, map[string]string{
|
||||
"CGO_ENABLED": "0",
|
||||
"GOARCH": targetArch,
|
||||
"GOCACHE": goCache,
|
||||
"GOENV": "off",
|
||||
"GOFLAGS": "",
|
||||
"GOMODCACHE": goModCache,
|
||||
"GOOS": targetOS,
|
||||
"GOPATH": goPath,
|
||||
"GOWORK": "off",
|
||||
})
|
||||
output, err := runOwnedCommand(
|
||||
ctx,
|
||||
repoPath,
|
||||
buildEnv,
|
||||
"go",
|
||||
"build",
|
||||
"-buildvcs=false",
|
||||
"-mod=vendor",
|
||||
"-trimpath",
|
||||
"-tags=go_sqlite,server",
|
||||
"-o", serverPath,
|
||||
"./server",
|
||||
)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("build Sliver server: %w\n%s", err, output)
|
||||
}
|
||||
return serverPath, nil
|
||||
}
|
||||
|
||||
func verifyCompilerCapability(ctx context.Context, rpc rpcpb.SliverRPCClient) error {
|
||||
compiler, err := rpc.GetCompiler(ctx, &commonpb.Empty{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("query server compiler: %w", err)
|
||||
}
|
||||
hasCapability := false
|
||||
for _, capability := range compiler.Capabilities {
|
||||
if capability == controlFlowCapability {
|
||||
hasCapability = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasCapability {
|
||||
return fmt.Errorf("server does not advertise required capability %q", controlFlowCapability)
|
||||
}
|
||||
hasTarget := false
|
||||
for _, target := range compiler.Targets {
|
||||
if target.GOOS == targetOS && target.GOARCH == targetArch && target.Format == clientpb.OutputFormat_EXECUTABLE {
|
||||
hasTarget = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasTarget {
|
||||
return fmt.Errorf("server compiler does not advertise %s/%s executable support", targetOS, targetArch)
|
||||
}
|
||||
fmt.Printf("Server advertises %s and %s/%s executable support\n", controlFlowCapability, targetOS, targetArch)
|
||||
return nil
|
||||
}
|
||||
|
||||
func generateArtifact(
|
||||
ctx context.Context,
|
||||
timeout time.Duration,
|
||||
rpc rpcpb.SliverRPCClient,
|
||||
layout *testLayout,
|
||||
implantPort int,
|
||||
name string,
|
||||
isBeacon bool,
|
||||
) (*generatedArtifact, error) {
|
||||
|
||||
config := &clientpb.ImplantConfig{
|
||||
GOOS: targetOS,
|
||||
GOARCH: targetArch,
|
||||
TemplateName: "sliver",
|
||||
Debug: false,
|
||||
ObfuscateSymbols: true,
|
||||
ControlFlow: clientpb.ControlFlowPolicy_CONTROL_FLOW_BALANCED_V1,
|
||||
IsBeacon: isBeacon,
|
||||
BeaconInterval: int64(2 * time.Second),
|
||||
BeaconJitter: 0,
|
||||
Format: clientpb.OutputFormat_EXECUTABLE,
|
||||
C2: []*clientpb.ImplantC2{{URL: fmt.Sprintf("mtls://127.0.0.1:%d", implantPort)}},
|
||||
IncludeMTLS: true,
|
||||
HTTPC2ConfigName: consts.DefaultC2Profile,
|
||||
ConnectionStrategy: "rd",
|
||||
ReconnectInterval: int64(time.Second),
|
||||
PollTimeout: int64(time.Second),
|
||||
MaxConnectionErrors: 10,
|
||||
NetGoEnabled: true,
|
||||
}
|
||||
|
||||
buildCtx, buildCancel := context.WithTimeout(ctx, timeout)
|
||||
generated, err := rpc.Generate(buildCtx, &clientpb.GenerateReq{Name: name, Config: config})
|
||||
buildCancel()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if generated.File == nil || generated.File.Name == "" || len(generated.File.Data) == 0 {
|
||||
return nil, errors.New("Sliver returned an empty executable")
|
||||
}
|
||||
if generated.ImplantName != name {
|
||||
return nil, fmt.Errorf("generated implant name = %q, want %q", generated.ImplantName, name)
|
||||
}
|
||||
|
||||
if err := verifyRenderedControlFlow(layout.serverRoot, generated.ImplantName); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
transformedFiles, err := verifyGarbleControlFlowDebugDir(layout.garbleDebugDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("inspect direct Garble transformation output for %s: %w", name, err)
|
||||
}
|
||||
fmt.Printf("Verified three exact rendered directives and %d transformed %s files for %s\n", transformedFiles, controlFlowGeneratedFile, name)
|
||||
|
||||
artifactPath := filepath.Join(layout.artifactDir, filepath.Base(generated.File.Name))
|
||||
if err := os.WriteFile(artifactPath, generated.File.Data, 0o700); err != nil {
|
||||
return nil, fmt.Errorf("save generated executable: %w", err)
|
||||
}
|
||||
return &generatedArtifact{
|
||||
name: generated.ImplantName,
|
||||
path: artifactPath,
|
||||
data: generated.File.Data,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func verifySessionPing(ctx context.Context, rpc rpcpb.SliverRPCClient, sessionID string) error {
|
||||
if sessionID == "" {
|
||||
return errors.New("SessionOpened event has an empty session ID")
|
||||
}
|
||||
response, err := rpc.Ping(ctx, &sliverpb.Ping{
|
||||
Nonce: sessionPingNonce,
|
||||
Request: &commonpb.Request{
|
||||
SessionID: sessionID,
|
||||
Timeout: int64(30 * time.Second),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("synchronous session Ping: %w", err)
|
||||
}
|
||||
if response.Nonce != sessionPingNonce {
|
||||
return fmt.Errorf("session Ping nonce = %d, want %d", response.Nonce, sessionPingNonce)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func verifySessionFilteredLs(ctx context.Context, rpc rpcpb.SliverRPCClient, sessionID string, tempDir string) error {
|
||||
const proofFileName = "controlflow-filter-proof.txt"
|
||||
proofPath := filepath.Join(tempDir, proofFileName)
|
||||
if err := os.WriteFile(proofPath, []byte("control-flow directory filter proof\n"), 0o600); err != nil {
|
||||
return fmt.Errorf("create filtered Ls proof file: %w", err)
|
||||
}
|
||||
wildcardPath := filepath.Join(tempDir, "controlflow-filter-*")
|
||||
listing, err := rpc.Ls(ctx, &sliverpb.LsReq{
|
||||
Path: wildcardPath,
|
||||
Request: &commonpb.Request{
|
||||
SessionID: sessionID,
|
||||
Timeout: int64(30 * time.Second),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("filtered session Ls(%q): %w", wildcardPath, err)
|
||||
}
|
||||
if listing.Response != nil && listing.Response.Err != "" {
|
||||
return fmt.Errorf("filtered session Ls(%q) returned implant error: %s", wildcardPath, listing.Response.Err)
|
||||
}
|
||||
if !listing.Exists {
|
||||
return fmt.Errorf("filtered session Ls(%q) reported that its directory does not exist", wildcardPath)
|
||||
}
|
||||
foundProof := false
|
||||
for _, file := range listing.Files {
|
||||
if file.Name == proofFileName {
|
||||
foundProof = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundProof {
|
||||
return fmt.Errorf("filtered session Ls(%q) did not return proof file %q", wildcardPath, proofFileName)
|
||||
}
|
||||
fmt.Printf(
|
||||
"Verified transformed determineDirPathFilter via Ls(%q): matched %s in %s (%d result(s))\n",
|
||||
wildcardPath,
|
||||
proofFileName,
|
||||
listing.Path,
|
||||
len(listing.Files),
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
func verifyBeaconPing(ctx context.Context, rpc rpcpb.SliverRPCClient, process *managedProcess, beaconID string) error {
|
||||
if beaconID == "" {
|
||||
return errors.New("BeaconRegistered event has an empty beacon ID")
|
||||
}
|
||||
response, err := rpc.Ping(ctx, &sliverpb.Ping{
|
||||
Nonce: beaconPingNonce,
|
||||
Request: &commonpb.Request{
|
||||
Async: true,
|
||||
BeaconID: beaconID,
|
||||
Timeout: int64(30 * time.Second),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("queue asynchronous beacon Ping: %w", err)
|
||||
}
|
||||
if response.Response == nil || response.Response.TaskID == "" {
|
||||
return fmt.Errorf("asynchronous beacon Ping returned no task ID: %#v", response.Response)
|
||||
}
|
||||
task, err := waitForCompletedBeaconTask(ctx, rpc, process, response.Response.TaskID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(task.Response) == 0 {
|
||||
return fmt.Errorf("completed beacon Ping task %s has an empty response", task.ID)
|
||||
}
|
||||
pong := &sliverpb.Ping{}
|
||||
if err := proto.Unmarshal(task.Response, pong); err != nil {
|
||||
return fmt.Errorf("unmarshal completed beacon Ping response: %w", err)
|
||||
}
|
||||
if pong.Nonce != beaconPingNonce {
|
||||
return fmt.Errorf("beacon Ping nonce = %d, want %d", pong.Nonce, beaconPingNonce)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func waitForCompletedBeaconTask(
|
||||
ctx context.Context,
|
||||
rpc rpcpb.SliverRPCClient,
|
||||
process *managedProcess,
|
||||
taskID string,
|
||||
) (*clientpb.BeaconTask, error) {
|
||||
ticker := time.NewTicker(250 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, fmt.Errorf("wait for beacon Ping task %s: %w", taskID, ctx.Err())
|
||||
case <-process.done:
|
||||
return nil, process.failure("beacon exited before its Ping task completed")
|
||||
case <-ticker.C:
|
||||
pollCtx, pollCancel := context.WithTimeout(ctx, 3*time.Second)
|
||||
task, err := rpc.GetBeaconTaskContent(pollCtx, &clientpb.BeaconTask{ID: taskID})
|
||||
pollCancel()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
switch task.State {
|
||||
case "completed":
|
||||
return task, nil
|
||||
case "failed", "canceled":
|
||||
return nil, fmt.Errorf("beacon Ping task %s entered terminal state %q", taskID, task.State)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func twoUnusedTCPPorts() (int, int, error) {
|
||||
first, err := unusedTCPPort()
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
for attempts := 0; attempts < 8; attempts++ {
|
||||
second, err := unusedTCPPort()
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
if first != second {
|
||||
return first, second, nil
|
||||
}
|
||||
}
|
||||
return 0, 0, errors.New("could not select two distinct TCP ports")
|
||||
}
|
||||
|
||||
func unusedTCPPort() (int, error) {
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer listener.Close()
|
||||
return listener.Addr().(*net.TCPAddr).Port, nil
|
||||
}
|
||||
|
||||
func waitForTCP(ctx context.Context, address string, process *managedProcess) error {
|
||||
ticker := time.NewTicker(listenerPollInterval)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
connection, err := net.DialTimeout("tcp", address, listenerDialTimeout)
|
||||
if err == nil {
|
||||
_ = connection.Close()
|
||||
return nil
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-process.done:
|
||||
return process.failure("Sliver daemon exited before accepting multiplayer connections")
|
||||
case <-ticker.C:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func setProcessEnvironment(values map[string]string) (func(), error) {
|
||||
type previousValue struct {
|
||||
value string
|
||||
set bool
|
||||
}
|
||||
previous := make(map[string]previousValue, len(values))
|
||||
for name, value := range values {
|
||||
oldValue, set := os.LookupEnv(name)
|
||||
previous[name] = previousValue{value: oldValue, set: set}
|
||||
if err := os.Setenv(name, value); err != nil {
|
||||
for restoreName, restoreValue := range previous {
|
||||
if restoreValue.set {
|
||||
_ = os.Setenv(restoreName, restoreValue.value)
|
||||
} else {
|
||||
_ = os.Unsetenv(restoreName)
|
||||
}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return func() {
|
||||
for name, value := range previous {
|
||||
if value.set {
|
||||
_ = os.Setenv(name, value.value)
|
||||
} else {
|
||||
_ = os.Unsetenv(name)
|
||||
}
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func environmentWith(environment []string, values map[string]string) []string {
|
||||
updated := append([]string{}, environment...)
|
||||
for name, value := range values {
|
||||
updated = environmentValue(updated, name, value)
|
||||
}
|
||||
return updated
|
||||
}
|
||||
|
||||
func environmentValue(environment []string, name string, value string) []string {
|
||||
prefix := name + "="
|
||||
updated := make([]string, 0, len(environment)+1)
|
||||
for _, entry := range environment {
|
||||
if !strings.HasPrefix(entry, prefix) {
|
||||
updated = append(updated, entry)
|
||||
}
|
||||
}
|
||||
return append(updated, prefix+value)
|
||||
}
|
||||
|
||||
func runOwnedCommand(ctx context.Context, dir string, env []string, path string, args ...string) (string, error) {
|
||||
cmd := exec.Command(path, args...)
|
||||
cmd.Dir = dir
|
||||
cmd.Env = env
|
||||
var output strings.Builder
|
||||
cmd.Stdout = &output
|
||||
cmd.Stderr = &output
|
||||
prepareCommand(cmd)
|
||||
if err := cmd.Start(); err != nil {
|
||||
return output.String(), err
|
||||
}
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
done <- cmd.Wait()
|
||||
}()
|
||||
select {
|
||||
case err := <-done:
|
||||
return strings.TrimSpace(output.String()), err
|
||||
case <-ctx.Done():
|
||||
terminateProcessTree(cmd)
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(cleanupGraceTimeout):
|
||||
killProcessTree(cmd)
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(cleanupProcessTimeout):
|
||||
}
|
||||
}
|
||||
return strings.TrimSpace(output.String()), ctx.Err()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
//go:build sliver_controlflow_e2e
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type managedProcess struct {
|
||||
cmd *exec.Cmd
|
||||
done chan struct{}
|
||||
err error
|
||||
logPath string
|
||||
}
|
||||
|
||||
func startProcess(path string, args []string, dir string, env []string, logPath string) (*managedProcess, error) {
|
||||
logFile, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o600)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cmd := exec.Command(path, args...)
|
||||
cmd.Dir = dir
|
||||
cmd.Env = env
|
||||
cmd.Stdout = logFile
|
||||
cmd.Stderr = logFile
|
||||
prepareCommand(cmd)
|
||||
if err := cmd.Start(); err != nil {
|
||||
_ = logFile.Close()
|
||||
return nil, err
|
||||
}
|
||||
process := &managedProcess{
|
||||
cmd: cmd,
|
||||
done: make(chan struct{}),
|
||||
logPath: logPath,
|
||||
}
|
||||
go func() {
|
||||
process.err = cmd.Wait()
|
||||
_ = logFile.Close()
|
||||
close(process.done)
|
||||
}()
|
||||
return process, nil
|
||||
}
|
||||
|
||||
func (process *managedProcess) stop() {
|
||||
select {
|
||||
case <-process.done:
|
||||
return
|
||||
default:
|
||||
}
|
||||
terminateProcessTree(process.cmd)
|
||||
select {
|
||||
case <-process.done:
|
||||
return
|
||||
case <-time.After(cleanupGraceTimeout):
|
||||
}
|
||||
killProcessTree(process.cmd)
|
||||
select {
|
||||
case <-process.done:
|
||||
case <-time.After(cleanupProcessTimeout):
|
||||
fmt.Fprintf(os.Stderr, "timed out cleaning up owned process group for %s (pid %d)\n", process.cmd.Path, process.cmd.Process.Pid)
|
||||
}
|
||||
}
|
||||
|
||||
func (process *managedProcess) failure(message string) error {
|
||||
processErr := process.err
|
||||
if processErr == nil {
|
||||
processErr = errors.New("process exited")
|
||||
}
|
||||
return fmt.Errorf("%s: %w\n%s", message, processErr, readLogTail(process.logPath))
|
||||
}
|
||||
|
||||
func readLogTail(path string) string {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("(could not read %s: %v)", path, err)
|
||||
}
|
||||
if len(data) > processLogTailBytes {
|
||||
data = data[len(data)-processLogTailBytes:]
|
||||
}
|
||||
return strings.TrimSpace(string(data))
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
//go:build sliver_controlflow_e2e && (darwin || linux)
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func prepareCommand(cmd *exec.Cmd) {
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
||||
}
|
||||
|
||||
func terminateProcessTree(cmd *exec.Cmd) {
|
||||
_ = syscall.Kill(-cmd.Process.Pid, syscall.SIGTERM)
|
||||
}
|
||||
|
||||
func killProcessTree(cmd *exec.Cmd) {
|
||||
_ = syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
//go:build sliver_controlflow_e2e && windows
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func prepareCommand(_ *exec.Cmd) {}
|
||||
|
||||
func terminateProcessTree(cmd *exec.Cmd) {
|
||||
_ = exec.Command("taskkill.exe", "/PID", strconv.Itoa(cmd.Process.Pid), "/T").Run()
|
||||
}
|
||||
|
||||
func killProcessTree(cmd *exec.Cmd) {
|
||||
_ = exec.Command("taskkill.exe", "/PID", strconv.Itoa(cmd.Process.Pid), "/T", "/F").Run()
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
//go:build sliver_controlflow_e2e
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
balancedV1Directive = "//garble:controlflow block_splits=2 junk_jumps=2 flatten_passes=1 flatten_hardening=xor trash_blocks=0"
|
||||
controlFlowGeneratedFile = "GARBLE_controlflow.go"
|
||||
)
|
||||
|
||||
var expectedControlFlowFunctions = map[string]string{
|
||||
"determineDirPathFilter": filepath.Join("handlers", "handlers.go"),
|
||||
"registerSliver": filepath.Join("runner", "runner.go"),
|
||||
"randomCCDomain": filepath.Join("transports", "transports.go"),
|
||||
}
|
||||
|
||||
func verifyRenderedControlFlow(serverRoot string, implantName string) error {
|
||||
sourceRoot := filepath.Join(serverRoot, "slivers", targetOS, targetArch, filepath.Base(implantName), "src")
|
||||
found := map[string]string{}
|
||||
directiveCount := 0
|
||||
err := filepath.WalkDir(sourceRoot, func(path string, entry fs.DirEntry, walkErr error) error {
|
||||
if walkErr != nil {
|
||||
return walkErr
|
||||
}
|
||||
if entry.IsDir() && path != sourceRoot {
|
||||
switch entry.Name() {
|
||||
case "vendor", "protobuf":
|
||||
return filepath.SkipDir
|
||||
}
|
||||
}
|
||||
if entry.IsDir() || filepath.Ext(entry.Name()) != ".go" {
|
||||
return nil
|
||||
}
|
||||
relativePath, err := filepath.Rel(sourceRoot, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
file, err := parser.ParseFile(token.NewFileSet(), path, nil, parser.ParseComments)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse rendered source %q: %w", path, err)
|
||||
}
|
||||
for _, declaration := range file.Decls {
|
||||
function, ok := declaration.(*ast.FuncDecl)
|
||||
if !ok || function.Doc == nil {
|
||||
continue
|
||||
}
|
||||
for _, comment := range function.Doc.List {
|
||||
if comment.Text != balancedV1Directive {
|
||||
continue
|
||||
}
|
||||
directiveCount++
|
||||
expectedSuffix, expected := expectedControlFlowFunctions[function.Name.Name]
|
||||
if !expected {
|
||||
return fmt.Errorf("unexpected exact control-flow directive on %s in %s", function.Name.Name, relativePath)
|
||||
}
|
||||
if !pathHasSuffix(relativePath, expectedSuffix) {
|
||||
return fmt.Errorf("control-flow function %s rendered in %s, want suffix %s", function.Name.Name, relativePath, expectedSuffix)
|
||||
}
|
||||
if previousPath, duplicate := found[function.Name.Name]; duplicate {
|
||||
return fmt.Errorf("control-flow directive for %s appears in both %s and %s", function.Name.Name, previousPath, relativePath)
|
||||
}
|
||||
found[function.Name.Name] = relativePath
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("inspect rendered source for %s: %w", implantName, err)
|
||||
}
|
||||
if directiveCount != len(expectedControlFlowFunctions) {
|
||||
return fmt.Errorf("rendered source has %d exact control-flow directives, want %d", directiveCount, len(expectedControlFlowFunctions))
|
||||
}
|
||||
for function, suffix := range expectedControlFlowFunctions {
|
||||
if found[function] == "" {
|
||||
return fmt.Errorf("rendered source is missing control-flow directive for %s in *%s", function, suffix)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func verifyGarbleControlFlowDebugDir(debugDir string) (int, error) {
|
||||
garbledRoot := filepath.Join(debugDir, "garbled")
|
||||
foundPackages := map[string]string{}
|
||||
transformedFiles := 0
|
||||
err := filepath.WalkDir(garbledRoot, func(path string, entry fs.DirEntry, walkErr error) error {
|
||||
if walkErr != nil {
|
||||
return walkErr
|
||||
}
|
||||
if entry.IsDir() || entry.Name() != controlFlowGeneratedFile {
|
||||
return nil
|
||||
}
|
||||
transformedFiles++
|
||||
packageName := filepath.Base(filepath.Dir(path))
|
||||
if _, expected := map[string]struct{}{"handlers": {}, "runner": {}, "transports": {}}[packageName]; !expected {
|
||||
return fmt.Errorf("unexpected transformed control-flow package %q at %s", packageName, path)
|
||||
}
|
||||
if previousPath, duplicate := foundPackages[packageName]; duplicate {
|
||||
return fmt.Errorf("multiple %s files for package %s: %s and %s", controlFlowGeneratedFile, packageName, previousPath, path)
|
||||
}
|
||||
source, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !bytes.Contains(source, []byte("goto ")) {
|
||||
return fmt.Errorf("transformed source %s contains no flattened control-flow jumps", path)
|
||||
}
|
||||
if _, err := parser.ParseFile(token.NewFileSet(), path, source, parser.SkipObjectResolution); err != nil {
|
||||
return fmt.Errorf("parse transformed source %s: %w", path, err)
|
||||
}
|
||||
foundPackages[packageName] = path
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if transformedFiles != len(expectedControlFlowFunctions) {
|
||||
return 0, fmt.Errorf("Garble debug output has %d %s files, want %d", transformedFiles, controlFlowGeneratedFile, len(expectedControlFlowFunctions))
|
||||
}
|
||||
for _, packageName := range []string{"handlers", "runner", "transports"} {
|
||||
if foundPackages[packageName] == "" {
|
||||
return 0, fmt.Errorf("Garble debug output is missing transformed package %s", packageName)
|
||||
}
|
||||
}
|
||||
return transformedFiles, nil
|
||||
}
|
||||
|
||||
func pathHasSuffix(path string, suffix string) bool {
|
||||
path = filepath.ToSlash(filepath.Clean(path))
|
||||
suffix = filepath.ToSlash(filepath.Clean(suffix))
|
||||
if path == suffix {
|
||||
return true
|
||||
}
|
||||
return strings.HasSuffix(path, "/"+suffix)
|
||||
}
|
||||
+111
-41
@@ -1,8 +1,13 @@
|
||||
package assets
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type garblePlatform struct {
|
||||
@@ -10,51 +15,13 @@ type garblePlatform struct {
|
||||
arch string
|
||||
filename string
|
||||
url string
|
||||
sha256 string
|
||||
}
|
||||
|
||||
func (r *runner) buildGarbleAssets() error {
|
||||
r.logger.Section("Garble")
|
||||
|
||||
platforms := []garblePlatform{
|
||||
{
|
||||
os: "linux",
|
||||
arch: "amd64",
|
||||
filename: "garble",
|
||||
url: fmt.Sprintf("https://github.com/moloch--/garble/releases/download/v%s/garble_linux-amd64", garbleVersion),
|
||||
},
|
||||
{
|
||||
os: "linux",
|
||||
arch: "arm64",
|
||||
filename: "garble",
|
||||
url: fmt.Sprintf("https://github.com/moloch--/garble/releases/download/v%s/garble_linux-arm64", garbleVersion),
|
||||
},
|
||||
{
|
||||
os: "windows",
|
||||
arch: "amd64",
|
||||
filename: "garble.exe",
|
||||
url: fmt.Sprintf("https://github.com/moloch--/garble/releases/download/v%s/garble_windows-amd64.exe", garbleVersion),
|
||||
},
|
||||
{
|
||||
os: "windows",
|
||||
arch: "arm64",
|
||||
filename: "garble.exe",
|
||||
url: fmt.Sprintf("https://github.com/moloch--/garble/releases/download/v%s/garble_windows-arm64.exe", garbleVersion),
|
||||
},
|
||||
{
|
||||
os: "darwin",
|
||||
arch: "amd64",
|
||||
filename: "garble",
|
||||
url: fmt.Sprintf("https://github.com/moloch--/garble/releases/download/v%s/garble_darwin-amd64", garbleVersion),
|
||||
},
|
||||
{
|
||||
os: "darwin",
|
||||
arch: "arm64",
|
||||
filename: "garble",
|
||||
url: fmt.Sprintf("https://github.com/moloch--/garble/releases/download/v%s/garble_darwin-arm64", garbleVersion),
|
||||
},
|
||||
}
|
||||
|
||||
for _, platform := range platforms {
|
||||
for _, platform := range garblePlatforms() {
|
||||
r.garbleIndex++
|
||||
r.logger.Logf("Fetch garble %s/%s (%d/%d)", platform.os, platform.arch, r.garbleIndex, garbleTotal)
|
||||
outputDir := filepath.Join(r.outputDir, platform.os, platform.arch)
|
||||
@@ -62,10 +29,113 @@ func (r *runner) buildGarbleAssets() error {
|
||||
return err
|
||||
}
|
||||
destPath := filepath.Join(outputDir, platform.filename)
|
||||
if err := r.downloadFile(platform.url, destPath); err != nil {
|
||||
downloadPath, err := r.downloadToTemp(platform.url, r.workDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := verifyGarbleSHA256(downloadPath, platform.sha256); err != nil {
|
||||
_ = os.Remove(downloadPath)
|
||||
return fmt.Errorf("verify garble %s/%s: %w", platform.os, platform.arch, err)
|
||||
}
|
||||
if err := moveFile(downloadPath, destPath); err != nil {
|
||||
_ = os.Remove(downloadPath)
|
||||
return fmt.Errorf("install garble %s/%s: %w", platform.os, platform.arch, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func garblePlatforms() []garblePlatform {
|
||||
return []garblePlatform{
|
||||
{
|
||||
os: "linux",
|
||||
arch: "amd64",
|
||||
filename: "garble",
|
||||
url: fmt.Sprintf("https://github.com/moloch--/garble/releases/download/v%s/garble_linux-amd64", garbleVersion),
|
||||
sha256: "ee10c850ec4b4078155e7fb6a28c6dc50ac1c991bbb8029829c40d41c8c57b64",
|
||||
},
|
||||
{
|
||||
os: "linux",
|
||||
arch: "arm64",
|
||||
filename: "garble",
|
||||
url: fmt.Sprintf("https://github.com/moloch--/garble/releases/download/v%s/garble_linux-arm64", garbleVersion),
|
||||
sha256: "5503dc9fcb6db3b797f0c10e20078de50df16b1cd8bf00668701589a601ae821",
|
||||
},
|
||||
{
|
||||
os: "windows",
|
||||
arch: "amd64",
|
||||
filename: "garble.exe",
|
||||
url: fmt.Sprintf("https://github.com/moloch--/garble/releases/download/v%s/garble_windows-amd64.exe", garbleVersion),
|
||||
sha256: "c7b979d9d61ecf5aa7de811a8e97a90364eaf03763778a9e1ee3c5a9473366be",
|
||||
},
|
||||
{
|
||||
os: "windows",
|
||||
arch: "arm64",
|
||||
filename: "garble.exe",
|
||||
url: fmt.Sprintf("https://github.com/moloch--/garble/releases/download/v%s/garble_windows-arm64.exe", garbleVersion),
|
||||
sha256: "f1de3caed55ff9422b940108a85ea61a1e868ec3bca01ec51fc5a4163ce2b44d",
|
||||
},
|
||||
{
|
||||
os: "darwin",
|
||||
arch: "amd64",
|
||||
filename: "garble",
|
||||
url: fmt.Sprintf("https://github.com/moloch--/garble/releases/download/v%s/garble_darwin-amd64", garbleVersion),
|
||||
sha256: "62c46c30c4983b176db5b46a22256d61fce3b9da647cecb7e7d479bfe47f17b8",
|
||||
},
|
||||
{
|
||||
os: "darwin",
|
||||
arch: "arm64",
|
||||
filename: "garble",
|
||||
url: fmt.Sprintf("https://github.com/moloch--/garble/releases/download/v%s/garble_darwin-arm64", garbleVersion),
|
||||
sha256: "63626c1833f94732fb43a2d00dc2236450ecedfe0e51497cfc5a310818c32f05",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ExpectedGarbleSHA256 returns the pinned digest for the Garble artifact used
|
||||
// by a host platform. The server uses the same manifest at runtime so it does
|
||||
// not advertise control-flow support for a stale or partially extracted tool.
|
||||
func ExpectedGarbleSHA256(goos, goarch string) (string, bool) {
|
||||
for _, platform := range garblePlatforms() {
|
||||
if platform.os == goos && platform.arch == goarch {
|
||||
return platform.sha256, true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
// VerifyGarbleBinary verifies an extracted Garble binary against the artifact
|
||||
// manifest used when server assets are assembled.
|
||||
func VerifyGarbleBinary(path, goos, goarch string) error {
|
||||
expected, ok := ExpectedGarbleSHA256(goos, goarch)
|
||||
if !ok {
|
||||
return fmt.Errorf("unsupported garble host platform %s/%s", goos, goarch)
|
||||
}
|
||||
if err := verifyGarbleSHA256(path, expected); err != nil {
|
||||
return fmt.Errorf("verify garble binary for %s/%s: %w", goos, goarch, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func verifyGarbleSHA256(path, expected string) (resultErr error) {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open artifact: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := file.Close(); err != nil && resultErr == nil {
|
||||
resultErr = fmt.Errorf("close artifact: %w", err)
|
||||
}
|
||||
}()
|
||||
|
||||
hash := sha256.New()
|
||||
if _, err := io.Copy(hash, file); err != nil {
|
||||
return fmt.Errorf("hash artifact: %w", err)
|
||||
}
|
||||
actual := hex.EncodeToString(hash.Sum(nil))
|
||||
if !strings.EqualFold(actual, strings.TrimSpace(expected)) {
|
||||
return fmt.Errorf("sha256 mismatch: got %s, want %s", actual, expected)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package assets
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestVerifyGarbleSHA256(t *testing.T) {
|
||||
artifact := filepath.Join(t.TempDir(), "garble")
|
||||
data := []byte("garble-control-flow")
|
||||
if err := os.WriteFile(artifact, data, 0o600); err != nil {
|
||||
t.Fatalf("write test artifact: %v", err)
|
||||
}
|
||||
digest := sha256.Sum256(data)
|
||||
expected := hex.EncodeToString(digest[:])
|
||||
|
||||
if err := verifyGarbleSHA256(artifact, strings.ToUpper(expected)); err != nil {
|
||||
t.Fatalf("verifyGarbleSHA256() error = %v", err)
|
||||
}
|
||||
if err := verifyGarbleSHA256(artifact, strings.Repeat("0", sha256.Size*2)); err == nil {
|
||||
t.Fatal("verifyGarbleSHA256() accepted an incorrect digest")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGarblePlatformManifest(t *testing.T) {
|
||||
platforms := garblePlatforms()
|
||||
if len(platforms) != garbleTotal {
|
||||
t.Fatalf("garble platform count = %d, want %d", len(platforms), garbleTotal)
|
||||
}
|
||||
|
||||
seen := map[string]struct{}{}
|
||||
for _, platform := range platforms {
|
||||
key := platform.os + "/" + platform.arch
|
||||
if _, duplicate := seen[key]; duplicate {
|
||||
t.Fatalf("duplicate Garble platform %s", key)
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
|
||||
digest, err := hex.DecodeString(platform.sha256)
|
||||
if err != nil || len(digest) != sha256.Size {
|
||||
t.Fatalf("Garble platform %s has invalid sha256 %q", key, platform.sha256)
|
||||
}
|
||||
if !strings.Contains(platform.url, "/v"+garbleVersion+"/") {
|
||||
t.Fatalf("Garble platform %s URL %q does not use pinned version %s", key, platform.url, garbleVersion)
|
||||
}
|
||||
if platform.os == "windows" && platform.filename != "garble.exe" {
|
||||
t.Fatalf("Garble platform %s filename = %q, want garble.exe", key, platform.filename)
|
||||
}
|
||||
if platform.os != "windows" && platform.filename != "garble" {
|
||||
t.Fatalf("Garble platform %s filename = %q, want garble", key, platform.filename)
|
||||
}
|
||||
expected, ok := ExpectedGarbleSHA256(platform.os, platform.arch)
|
||||
if !ok || expected != platform.sha256 {
|
||||
t.Fatalf("ExpectedGarbleSHA256(%q, %q) = %q, %t; want %q, true", platform.os, platform.arch, expected, ok, platform.sha256)
|
||||
}
|
||||
}
|
||||
|
||||
if _, ok := ExpectedGarbleSHA256("plan9", "mips"); ok {
|
||||
t.Fatal("ExpectedGarbleSHA256() accepted an unsupported platform")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user