Compare commits

...
Author SHA1 Message Date
moloch-- 6d6df68972 Update Reflektor to v0.0.3
Synchronize the root and implant module graphs and vendor trees with the all-platform Go c-shared loader fixes.
2026-08-17 00:29:26 -07:00
moloch-- 7f863d3172 Fix cross-platform Reflektor integration failures
Close rendered implant files before import renames, use native GCC for Linux/386 Go shared libraries, and preserve actionable integration logs.
2026-08-17 00:28:49 -07:00
moloch-- 6ecb379bce Test Sliver shared libraries with Reflektor 2026-08-16 22:09:22 -07:00
38 changed files with 1980 additions and 310 deletions

No files matched your search

+223
View File
@@ -0,0 +1,223 @@
name: Reflektor Integration Test Matrix
on:
push:
branches:
- master
pull_request:
types:
- opened
- reopened
- synchronize
- ready_for_review
workflow_dispatch:
permissions:
contents: read
jobs:
darwin:
name: reflektor-darwin-${{ matrix.goarch }}
runs-on: ${{ matrix.runner }}
timeout-minutes: 180
strategy:
fail-fast: false
matrix:
include:
- runner: macos-15-intel
goarch: amd64
server_arch: amd64
- runner: macos-15
goarch: arm64
server_arch: arm64
defaults:
run:
shell: bash
steps:
- name: Check out code
uses: actions/checkout@v6
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version-file: go.mod
cache: true
- name: Download Sliver build assets
run: go run -mod=vendor ./util/cmd/assets
- name: Build Sliver server
env:
CGO_ENABLED: "0"
GOARCH: ${{ matrix.server_arch }}
GOOS: darwin
run: go build -mod=vendor -trimpath -tags go_sqlite,server -o sliver-server ./server
- name: Build Reflektor integration driver
env:
CGO_ENABLED: "0"
GOARCH: ${{ matrix.goarch }}
GOOS: darwin
run: go build -mod=vendor -trimpath -tags client,go_sqlite -o sliver-reflektor-e2e ./test/reflektor
- name: Load Sliver with Reflektor
run: >-
./sliver-reflektor-e2e
-repo .
-server ./sliver-server
-target-os darwin
-target-arch ${{ matrix.goarch }}
linux-native:
name: reflektor-linux-${{ matrix.goarch }}-native
runs-on: ${{ matrix.runner }}
timeout-minutes: 180
strategy:
fail-fast: false
matrix:
include:
- runner: ubuntu-24.04
goarch: amd64
server_arch: amd64
- runner: ubuntu-24.04-arm
goarch: arm64
server_arch: arm64
defaults:
run:
shell: bash
steps:
- name: Check out code
uses: actions/checkout@v6
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version-file: go.mod
cache: true
- name: Download Sliver build assets
run: go run -mod=vendor ./util/cmd/assets
- name: Build Sliver server
env:
CGO_ENABLED: "0"
GOARCH: ${{ matrix.server_arch }}
GOOS: linux
run: go build -mod=vendor -trimpath -tags go_sqlite,server -o sliver-server ./server
- name: Build Reflektor integration driver
env:
CGO_ENABLED: "0"
GOARCH: ${{ matrix.goarch }}
GOOS: linux
run: go build -mod=vendor -trimpath -tags client,go_sqlite -o sliver-reflektor-e2e ./test/reflektor
- name: Load Sliver with Reflektor
run: >-
./sliver-reflektor-e2e
-repo .
-server ./sliver-server
-target-os linux
-target-arch ${{ matrix.goarch }}
linux-386:
name: reflektor-linux-386-docker
runs-on: ubuntu-24.04
timeout-minutes: 180
env:
CONTAINER_NAME: sliver-reflektor-linux-386-${{ github.run_id }}-${{ github.run_attempt }}
IMAGE_NAME: sliver-reflektor-linux-386:${{ github.run_id }}-${{ github.run_attempt }}
defaults:
run:
shell: bash
steps:
- name: Check out code
uses: actions/checkout@v6
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build linux/386 test image
run: |
set -euo pipefail
go_version="$(awk '$1 == "go" { print $2; exit }' go.mod)"
docker buildx build \
--load \
--platform linux/386 \
--build-arg "GO_VERSION=${go_version}" \
--file test/reflektor/Dockerfile.linux-386 \
--tag "${IMAGE_NAME}" \
.
docker buildx prune --all --force
- name: Load Sliver with Reflektor
run: >-
docker run
--name "${CONTAINER_NAME}"
--platform linux/386
"${IMAGE_NAME}"
- name: Clean up linux/386 Docker resources
if: ${{ always() }}
run: |
docker rm --force "${CONTAINER_NAME}" 2>/dev/null || true
docker image rm --force "${IMAGE_NAME}" 2>/dev/null || true
docker buildx prune --all --force >/dev/null 2>&1 || true
windows:
name: reflektor-windows-${{ matrix.goarch }}
runs-on: ${{ matrix.runner }}
timeout-minutes: 180
strategy:
fail-fast: false
matrix:
include:
- runner: windows-2022
goarch: amd64
server_arch: amd64
- runner: windows-2022
goarch: 386
server_arch: amd64
- runner: windows-11-arm
goarch: arm64
server_arch: amd64
defaults:
run:
shell: bash
steps:
- name: Check out code
uses: actions/checkout@v6
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version-file: go.mod
cache: true
- name: Download Sliver build assets
run: go run -mod=vendor ./util/cmd/assets
- name: Build Sliver server
env:
CGO_ENABLED: "0"
GOARCH: ${{ matrix.server_arch }}
GOOS: windows
run: go build -mod=vendor -trimpath -tags go_sqlite,server -o sliver-server.exe ./server
- name: Build Reflektor integration driver
env:
CGO_ENABLED: "0"
GOARCH: ${{ matrix.goarch }}
GOOS: windows
run: go build -mod=vendor -trimpath -tags client,go_sqlite -o sliver-reflektor-e2e.exe ./test/reflektor
- name: Load Sliver with Reflektor
run: >-
./sliver-reflektor-e2e.exe
-repo .
-server ./sliver-server.exe
-target-os windows
-target-arch ${{ matrix.goarch }}
+1
View File
@@ -87,6 +87,7 @@ var (
"linux/arm64": true,
"windows/386": true,
"windows/amd64": true,
"windows/arm64": true,
}
ErrNoExternalBuilder = errors.New("no external builders are available")
+1 -1
View File
@@ -49,7 +49,7 @@ require (
github.com/sirupsen/logrus v1.9.4
github.com/sliverarmory/beignet v0.0.3
github.com/sliverarmory/malasada v0.0.3
github.com/sliverarmory/reflektor v0.0.2
github.com/sliverarmory/reflektor v0.0.3
github.com/sliverarmory/wasm-donut v0.0.3
github.com/spf13/cobra v1.10.2
github.com/spf13/pflag v1.0.10
+2 -2
View File
@@ -678,8 +678,8 @@ github.com/sliverarmory/beignet v0.0.3 h1:ssR6AK9lirVulHZhQMyMmVAi0FcFwWxlh9LwKC
github.com/sliverarmory/beignet v0.0.3/go.mod h1:xXfTKBJqFBY/pakRFwJ+5+oGiSlg2CiCVUg/76kW1H4=
github.com/sliverarmory/malasada v0.0.3 h1:b2KG2NhQVfBIIrbP8rGJwmdh7o8LH6NKYKH1DF4No1U=
github.com/sliverarmory/malasada v0.0.3/go.mod h1:F96IgTOoX72FU995F+Jip0ni8Z5sXiYOwfmlRGobJkQ=
github.com/sliverarmory/reflektor v0.0.2 h1:nrEEFXrYiYFLCqyMpOJpe4RsY0Pu3tc1CvwChsNoAq4=
github.com/sliverarmory/reflektor v0.0.2/go.mod h1:Rw7qTWQQlCM3YTxxeuklFt3Q+kh1Ytlmzg5UVw1Nhkg=
github.com/sliverarmory/reflektor v0.0.3 h1:tCBH9bXRykDa2Z5y8IgtcO+kGxL7OFFetJ4DdjPub8k=
github.com/sliverarmory/reflektor v0.0.3/go.mod h1:Rw7qTWQQlCM3YTxxeuklFt3Q+kh1Ytlmzg5UVw1Nhkg=
github.com/sliverarmory/wasm-donut v0.0.3 h1:sldbfAsz1YziULlhDKpU3DY/+/dAfv5gl26qSqHIpzE=
github.com/sliverarmory/wasm-donut v0.0.3/go.mod h1:D6OLBZBfFm2Tk857vqGqFS9uUi4CXqnqHxKqtONllVk=
github.com/sony/sonyflake v1.0.0 h1:MpU6Ro7tfXwgn2l5eluf9xQvQJDROTBImNCfRXn/YeM=
+1 -1
View File
@@ -11,7 +11,7 @@ require (
github.com/kbinani/screenshot v0.0.0-20191211154542-3a185f1ce18f
github.com/lesnuages/go-winio v0.4.19
github.com/miekg/dns v1.1.50
github.com/sliverarmory/reflektor v0.0.2
github.com/sliverarmory/reflektor v0.0.3
github.com/stretchr/testify v1.8.4
github.com/tetratelabs/wazero v1.7.1
github.com/things-go/go-socks5 v0.0.4
+2 -2
View File
@@ -43,8 +43,8 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sliverarmory/reflektor v0.0.2 h1:nrEEFXrYiYFLCqyMpOJpe4RsY0Pu3tc1CvwChsNoAq4=
github.com/sliverarmory/reflektor v0.0.2/go.mod h1:Rw7qTWQQlCM3YTxxeuklFt3Q+kh1Ytlmzg5UVw1Nhkg=
github.com/sliverarmory/reflektor v0.0.3 h1:tCBH9bXRykDa2Z5y8IgtcO+kGxL7OFFetJ4DdjPub8k=
github.com/sliverarmory/reflektor v0.0.3/go.mod h1:Rw7qTWQQlCM3YTxxeuklFt3Q+kh1Ytlmzg5UVw1Nhkg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+33 -7
View File
@@ -36,9 +36,10 @@ var (
)
type Module struct {
mu sync.RWMutex
image []byte
closed bool
mu sync.RWMutex
image []byte
goRuntime bool
closed bool
}
// LoadLibrary loads a Mach-O image into the darwin in-memory loader context.
@@ -51,10 +52,23 @@ func LoadLibrary(data []byte) (*Module, error) {
if err != nil {
return nil, err
}
goRuntime, err := machOHasGoBuildInfo(image)
if err != nil {
return nil, err
}
cloned := make([]byte, len(image))
copy(cloned, image)
return &Module{image: cloned}, nil
return &Module{image: cloned, goRuntime: goRuntime}, nil
}
func machOHasGoBuildInfo(image []byte) (bool, error) {
f, err := macho.NewFile(bytes.NewReader(image))
if err != nil {
return false, fmt.Errorf("inspect Mach-O build info: %w", err)
}
defer f.Close()
return f.Section("__go_buildinfo") != nil, nil
}
// Free releases the in-memory Mach-O bytes.
@@ -94,7 +108,7 @@ func (module *Module) CallExport(name string) error {
image := module.image
module.mu.RUnlock()
rc := memmodLoader(image, symbol)
rc := memmodLoader(image, symbol, module.goRuntime)
runtime.KeepAlive(image)
if rc != 0 {
@@ -395,7 +409,7 @@ func isDarwinSystemInstallName(installName string) bool {
strings.HasPrefix(installName, "/System/Library/")
}
func memmodLoader(bufferRO []byte, entrySymbol string) int {
func memmodLoader(bufferRO []byte, entrySymbol string, isolateGoRuntime bool) int {
if len(bufferRO) == 0 || entrySymbol == "" {
return 1
}
@@ -789,7 +803,14 @@ func memmodLoader(bufferRO []byte, entrySymbol string) int {
// Exported fixture entry points use the C void(void) ABI. Keep that call
// signature exact when cgo supplies the foreign-function bridge.
callVoid0(addrEntry)
if isolateGoRuntime {
if err := callVoid0OnThread(addrEntry); err != nil {
setDarwinLoaderDetail(err.Error())
return 14
}
} else {
callVoid0(addrEntry)
}
// Keep mapped and scratch memory reachable until after entry returns.
runtime.KeepAlive(mapped.mapping)
runtime.KeepAlive(scratch)
@@ -1535,6 +1556,11 @@ func loaderStatusError(code int) error {
return fmt.Errorf("failed to preload Mach-O dependencies: %s", detail)
}
return errors.New("failed to preload Mach-O dependencies")
case 14:
if detail := getDarwinLoaderDetail(); detail != "" {
return fmt.Errorf("failed to call Go export on isolated thread: %s", detail)
}
return errors.New("failed to call Go export on isolated thread")
default:
return fmt.Errorf("in-memory dyld loader failed with status %d", code)
}
@@ -2,7 +2,10 @@
package memmod
import _ "unsafe"
import (
"errors"
_ "unsafe"
)
//go:noescape
func cCall10(fn, a0, a1, a2, a3, a4, a5, a6, a7, a8, a9 uintptr) uintptr
@@ -18,6 +21,11 @@ func callVoid0(fn uintptr) {
_ = call0(fn)
}
func callVoid0OnThread(fn uintptr) error {
_ = fn
return errors.New("calling a Go c-shared export requires cgo")
}
func callDlopen(fn, name uintptr, flags int) uintptr {
return call2(fn, name, uintptr(flags))
}
@@ -3,7 +3,9 @@
package memmod
/*
#include <pthread.h>
#include <stdint.h>
#include <string.h>
typedef uintptr_t (*reflektor_fn10_t)(
uintptr_t, uintptr_t, uintptr_t, uintptr_t, uintptr_t,
@@ -33,13 +35,44 @@ static uintptr_t reflektor_call10(
) {
return ((reflektor_fn10_t)fn)(a0, a1, a2, a3, a4, a5, a6, a7, a8, a9);
}
typedef struct {
uintptr_t fn;
} reflektor_void_thread_call;
static void *reflektor_void_thread_entry(void *opaque) {
reflektor_void_thread_call *call = (reflektor_void_thread_call *)opaque;
((reflektor_void_fn0_t)call->fn)();
return NULL;
}
static int reflektor_call_void0_thread(uintptr_t fn) {
pthread_t thread;
reflektor_void_thread_call call;
memset(&call, 0, sizeof(call));
call.fn = fn;
int err = pthread_create(&thread, NULL, reflektor_void_thread_entry, &call);
if (err != 0) {
return err;
}
return pthread_join(thread, NULL);
}
*/
import "C"
import "fmt"
func callVoid0(fn uintptr) {
C.reflektor_call_void0(C.uintptr_t(fn))
}
func callVoid0OnThread(fn uintptr) error {
if errno := int(C.reflektor_call_void0_thread(C.uintptr_t(fn))); errno != 0 {
return fmt.Errorf("pthread call failed with error %d", errno)
}
return nil
}
func callDlopen(fn, name uintptr, flags int) uintptr {
return uintptr(C.reflektor_call_dlopen(C.uintptr_t(fn), C.uintptr_t(name), C.int(flags)))
}
+167 -103
View File
@@ -45,17 +45,20 @@ const (
)
type Module struct {
mu sync.RWMutex
mapping []byte
loadBias uintptr
symbols map[string]uintptr
closed bool
mu sync.RWMutex
mapping []byte
loadBias uintptr
symbols map[string]uintptr
goRuntime bool
closed bool
}
type mappedELF struct {
mapping []byte
loadBias uintptr
progs []*elf.Prog
mapping []byte
loadBias uintptr
progs []*elf.Prog
tlsOffset int64
hasTLS bool
}
type dynamicInitInfo struct {
@@ -80,6 +83,13 @@ type symbolResolver struct {
opened map[string]uintptr
}
var linuxGoTLSSlots = struct {
sync.Mutex
used [linuxGoTLSSlotCount]bool
}{}
const linuxGoTLSSlotCount = 64
func LoadLibrary(data []byte) (*Module, error) {
if len(data) == 0 {
return nil, errors.New("empty ELF image")
@@ -94,11 +104,27 @@ func LoadLibrary(data []byte) (*Module, error) {
if err := validateELFHeaders(f); err != nil {
return nil, err
}
initInfo, err := parseDynamicInitInfo(f)
if err != nil {
return nil, err
}
goRuntime, tlsSlot, tlsOffset, err := prepareLinuxGoRuntimeTLS(f)
if err != nil {
return nil, err
}
tlsSlotReserved := goRuntime
defer func() {
if tlsSlotReserved {
releaseLinuxGoTLSSlot(tlsSlot)
}
}()
mapped, err := mapELFImage(data, f)
if err != nil {
return nil, err
}
mapped.tlsOffset = tlsOffset
mapped.hasTLS = goRuntime
cleanup := true
defer func() {
if cleanup && len(mapped.mapping) != 0 {
@@ -114,19 +140,89 @@ func LoadLibrary(data []byte) (*Module, error) {
if err := applySegmentProtections(mapped); err != nil {
return nil, err
}
if err := runELFInitializers(mapped, f); err != nil {
initializers, err := collectELFInitializers(mapped, f.Class, initInfo)
if err != nil {
return nil, err
}
argc, argv, envp := linuxInitCallArgs(goRuntime)
if goRuntime && argv == 0 {
return nil, errors.New("failed to allocate Go runtime argv/environment vector")
}
// Once a Go constructor starts, its runtime may retain this TLS slot for
// process-lifetime threads even if a later initializer reports an error.
tlsSlotReserved = false
for _, initializer := range initializers {
cCallVoid3(initializer, argc, argv, envp)
}
module := &Module{
mapping: mapped.mapping,
loadBias: mapped.loadBias,
symbols: buildExportedSymbolTable(f, mapped.loadBias),
mapping: mapped.mapping,
loadBias: mapped.loadBias,
symbols: buildExportedSymbolTable(f, mapped.loadBias),
goRuntime: goRuntime,
}
cleanup = false
return module, nil
}
func prepareLinuxGoRuntimeTLS(f *elf.File) (bool, uintptr, int64, error) {
if f == nil || f.Section(".go.buildinfo") == nil {
return false, 0, 0, nil
}
var tls *elf.Prog
for _, prog := range f.Progs {
if prog.Type != elf.PT_TLS {
continue
}
if tls != nil {
return false, 0, 0, errors.New("Go ELF image has multiple PT_TLS segments")
}
tls = prog
}
if tls == nil || tls.Memsz == 0 {
return false, 0, 0, errors.New("Go ELF image is missing its runtime PT_TLS segment")
}
if tls.Filesz != 0 {
return false, 0, 0, fmt.Errorf("Go ELF PT_TLS has unsupported initialized data size %#x", tls.Filesz)
}
wordSize := uint64(8)
if f.Class == elf.ELFCLASS32 {
wordSize = 4
}
if tls.Memsz > 2*wordSize {
return false, 0, 0, fmt.Errorf("Go ELF PT_TLS size %#x exceeds the reserved size %#x", tls.Memsz, 2*wordSize)
}
linuxGoTLSSlots.Lock()
defer linuxGoTLSSlots.Unlock()
slot := uintptr(linuxGoTLSSlotCount)
for i := range linuxGoTLSSlots.used {
if !linuxGoTLSSlots.used[i] {
slot = uintptr(i)
break
}
}
if slot == linuxGoTLSSlotCount {
return false, 0, 0, fmt.Errorf("Go ELF TLS slot limit reached (%d)", linuxGoTLSSlotCount)
}
offset, err := linuxGoTLSSlotOffset(slot)
if err != nil {
return false, 0, 0, err
}
linuxGoTLSSlots.used[slot] = true
return true, slot, offset, nil
}
func releaseLinuxGoTLSSlot(slot uintptr) {
if slot >= linuxGoTLSSlotCount {
return
}
linuxGoTLSSlots.Lock()
linuxGoTLSSlots.used[slot] = false
linuxGoTLSSlots.Unlock()
}
func (module *Module) Free() {
module.mu.Lock()
defer module.mu.Unlock()
@@ -135,6 +231,15 @@ func (module *Module) Free() {
return
}
module.closed = true
if module.goRuntime {
// A Go c-shared runtime owns process-lifetime threads and cannot be
// unloaded safely. Close the Reflektor handle while leaving its mapping
// and reserved TLS slot pinned until process exit.
module.mapping = nil
module.symbols = nil
module.loadBias = 0
return
}
if len(module.mapping) != 0 {
_ = unix.Munmap(module.mapping)
@@ -171,7 +276,14 @@ func (module *Module) CallExport(name string) error {
return fmt.Errorf("resolve export %q: %w", name, err)
}
_ = cCall0(addr)
if module.goRuntime {
if err := cCallVoid0OnThread(addr); err != nil {
return fmt.Errorf("call Go export %q on isolated thread: %w", name, err)
}
return nil
}
cCallVoid0(addr)
return nil
}
@@ -437,17 +549,17 @@ func applyOneRelocation(machine elf.Machine, class elf.Class, mapped mappedELF,
switch machine {
case elf.EM_X86_64:
return applyX8664Reloc(relocType, place, mapped.loadBias, symValue, addend)
return applyX8664Reloc(relocType, place, mapped.loadBias, symValue, addend, mapped.tlsOffset, mapped.hasTLS)
case elf.EM_386:
return apply386Reloc(relocType, place, mapped.loadBias, symValue, addend)
return apply386Reloc(relocType, place, mapped.loadBias, symValue, addend, mapped.tlsOffset, mapped.hasTLS)
case elf.EM_AARCH64:
return applyAArch64Reloc(relocType, place, mapped.loadBias, symValue, addend)
return applyAArch64Reloc(relocType, place, mapped.loadBias, symValue, addend, mapped.tlsOffset, mapped.hasTLS)
default:
return fmt.Errorf("unsupported machine for relocation: %s", machine)
}
}
func applyX8664Reloc(relocType uint32, place uintptr, loadBias uintptr, symValue uintptr, addend int64) error {
func applyX8664Reloc(relocType uint32, place uintptr, loadBias uintptr, symValue uintptr, addend int64, tlsOffset int64, hasTLS bool) error {
switch elf.R_X86_64(relocType) {
case elf.R_X86_64_NONE:
return nil
@@ -455,10 +567,10 @@ func applyX8664Reloc(relocType uint32, place uintptr, loadBias uintptr, symValue
writeU64(place, uint64(int64(loadBias)+addend))
return nil
case elf.R_X86_64_TPOFF64:
// Linux TLS local-exec relocation. The pure-Go loader does not provision
// module TLS blocks, so we apply S+A and rely on payload/runtime behavior
// that does not require a non-zero static TLS offset.
writeU64(place, uint64(int64(symValue)+addend))
if !hasTLS {
return errors.New("x86_64 static TLS relocation has no reserved host TLS slot")
}
writeU64(place, uint64(tlsOffset+addend))
return nil
case elf.R_X86_64_JMP_SLOT, elf.R_X86_64_GLOB_DAT, elf.R_X86_64_64:
writeU64(place, uint64(int64(symValue)+addend))
@@ -489,7 +601,7 @@ func applyX8664Reloc(relocType uint32, place uintptr, loadBias uintptr, symValue
}
}
func apply386Reloc(relocType uint32, place uintptr, loadBias uintptr, symValue uintptr, addend int64) error {
func apply386Reloc(relocType uint32, place uintptr, loadBias uintptr, symValue uintptr, addend int64, tlsOffset int64, hasTLS bool) error {
switch elf.R_386(relocType) {
case elf.R_386_NONE:
return nil
@@ -497,8 +609,10 @@ func apply386Reloc(relocType uint32, place uintptr, loadBias uintptr, symValue u
writeU32(place, uint32(int64(loadBias)+addend))
return nil
case elf.R_386_TLS_TPOFF:
// Linux TLS local-exec relocation; see R_X86_64_TPOFF64 note above.
writeU32(place, uint32(int64(symValue)+addend))
if !hasTLS {
return errors.New("386 static TLS relocation has no reserved host TLS slot")
}
writeU32(place, uint32(tlsOffset+addend))
return nil
case elf.R_386_JMP_SLOT, elf.R_386_GLOB_DAT:
writeU32(place, uint32(symValue))
@@ -518,7 +632,7 @@ func apply386Reloc(relocType uint32, place uintptr, loadBias uintptr, symValue u
}
}
func applyAArch64Reloc(relocType uint32, place uintptr, loadBias uintptr, symValue uintptr, addend int64) error {
func applyAArch64Reloc(relocType uint32, place uintptr, loadBias uintptr, symValue uintptr, addend int64, tlsOffset int64, hasTLS bool) error {
switch elf.R_AARCH64(relocType) {
case elf.R_AARCH64_NONE:
return nil
@@ -526,8 +640,10 @@ func applyAArch64Reloc(relocType uint32, place uintptr, loadBias uintptr, symVal
writeU64(place, uint64(int64(loadBias)+addend))
return nil
case elf.R_AARCH64_TLS_TPREL64:
// Linux TLS local-exec relocation; see R_X86_64_TPOFF64 note above.
writeU64(place, uint64(int64(symValue)+addend))
if !hasTLS {
return errors.New("arm64 static TLS relocation has no reserved host TLS slot")
}
writeU64(place, uint64(tlsOffset+addend))
return nil
case elf.R_AARCH64_JUMP_SLOT, elf.R_AARCH64_GLOB_DAT, elf.R_AARCH64_ABS64:
writeU64(place, uint64(int64(symValue)+addend))
@@ -611,24 +727,22 @@ func applySegmentProtections(mapped mappedELF) error {
return nil
}
func runELFInitializers(mapped mappedELF, f *elf.File) error {
info, err := parseDynamicInitInfo(f)
func collectELFInitializers(mapped mappedELF, class elf.Class, info dynamicInitInfo) ([]uintptr, error) {
initializers := make([]uintptr, 0)
var err error
initializers, err = appendDynamicInitArray(initializers, mapped, class, info.preinitArr, info.preinitSz, "DT_PREINIT_ARRAY")
if err != nil {
return err
return nil, err
}
argc, argv, envp := linuxInitCallArgs()
skip := collectInitSkipAddrs(f, mapped.loadBias)
if err := callDynamicInitArray(mapped, f.Class, info.preinitArr, info.preinitSz, "DT_PREINIT_ARRAY", argc, argv, envp, skip); err != nil {
return err
initializers, err = appendDynamicInitFn(initializers, mapped, uintptr(info.init), "DT_INIT")
if err != nil {
return nil, err
}
if err := callDynamicInitFn(mapped, uintptr(info.init), "DT_INIT", argc, argv, envp, skip); err != nil {
return err
initializers, err = appendDynamicInitArray(initializers, mapped, class, info.initArray, info.initArraySz, "DT_INIT_ARRAY")
if err != nil {
return nil, err
}
if err := callDynamicInitArray(mapped, f.Class, info.initArray, info.initArraySz, "DT_INIT_ARRAY", argc, argv, envp, skip); err != nil {
return err
}
return nil
return initializers, nil
}
func parseDynamicInitInfo(f *elf.File) (dynamicInitInfo, error) {
@@ -703,24 +817,20 @@ func parseDynamicInitInfo(f *elf.File) (dynamicInitInfo, error) {
return info, nil
}
func callDynamicInitFn(mapped mappedELF, fn uintptr, source string, argc uintptr, argv uintptr, envp uintptr, skip map[uintptr]struct{}) error {
func appendDynamicInitFn(initializers []uintptr, mapped mappedELF, fn uintptr, source string) ([]uintptr, error) {
if fn == 0 {
return nil
return initializers, nil
}
resolved, ok := normalizeInitFnAddress(mapped, fn)
if !ok {
return fmt.Errorf("%s points outside mapped image: %#x", source, fn)
return nil, fmt.Errorf("%s points outside mapped image: %#x", source, fn)
}
if _, blocked := skip[resolved]; blocked {
return nil
}
_ = cCall3(resolved, argc, argv, envp)
return nil
return append(initializers, resolved), nil
}
func callDynamicInitArray(mapped mappedELF, class elf.Class, arrayVAddr uint64, arraySz uint64, source string, argc uintptr, argv uintptr, envp uintptr, skip map[uintptr]struct{}) error {
func appendDynamicInitArray(initializers []uintptr, mapped mappedELF, class elf.Class, arrayVAddr uint64, arraySz uint64, source string) ([]uintptr, error) {
if arrayVAddr == 0 || arraySz == 0 {
return nil
return initializers, nil
}
entrySize := 8
@@ -728,16 +838,16 @@ func callDynamicInitArray(mapped mappedELF, class elf.Class, arrayVAddr uint64,
entrySize = 4
}
if arraySz%uint64(entrySize) != 0 {
return fmt.Errorf("%s has malformed size %#x for entry size %d", source, arraySz, entrySize)
return nil, fmt.Errorf("%s has malformed size %#x for entry size %d", source, arraySz, entrySize)
}
arrayLen, err := u64ToInt(arraySz)
if err != nil {
return fmt.Errorf("%s size does not fit in int: %w", source, err)
return nil, fmt.Errorf("%s size does not fit in int: %w", source, err)
}
arrayAddr := mapped.loadBias + uintptr(arrayVAddr)
if !mappedAddressInRange(mapped.mapping, arrayAddr, arrayLen) {
return fmt.Errorf("%s range %#x..%#x is outside mapped image", source, arrayVAddr, arrayVAddr+arraySz)
return nil, fmt.Errorf("%s range %#x..%#x is outside mapped image", source, arrayVAddr, arrayVAddr+arraySz)
}
count := int(arraySz / uint64(entrySize))
@@ -754,57 +864,11 @@ func callDynamicInitArray(mapped mappedELF, class elf.Class, arrayVAddr uint64,
}
resolved, ok := normalizeInitFnAddress(mapped, fn)
if !ok {
return fmt.Errorf("%s[%d] points outside mapped image: %#x", source, i, fn)
return nil, fmt.Errorf("%s[%d] points outside mapped image: %#x", source, i, fn)
}
if _, blocked := skip[resolved]; blocked {
continue
}
_ = cCall3(resolved, argc, argv, envp)
initializers = append(initializers, resolved)
}
return nil
}
func collectInitSkipAddrs(f *elf.File, loadBias uintptr) map[uintptr]struct{} {
if f == nil || f.Section(".go.buildinfo") == nil {
return nil
}
targets := map[string]struct{}{
"_rt0_386_linux_lib": {},
"_rt0_amd64_linux_lib": {},
"_rt0_arm64_linux_lib": {},
"_rt0_386_lib": {},
"_rt0_amd64_lib": {},
"_rt0_arm64_lib": {},
}
out := make(map[uintptr]struct{})
add := func(symbols []elf.Symbol) {
for _, sym := range symbols {
if sym.Value == 0 || sym.Section == elf.SHN_UNDEF {
continue
}
name := sym.Name
if at := strings.IndexByte(name, '@'); at > 0 {
name = name[:at]
}
if _, ok := targets[name]; !ok {
continue
}
out[loadBias+uintptr(sym.Value)] = struct{}{}
}
}
if dynSyms, err := f.DynamicSymbols(); err == nil {
add(dynSyms)
}
if syms, err := f.Symbols(); err == nil {
add(syms)
}
if len(out) == 0 {
return nil
}
return out
return initializers, nil
}
func normalizeInitFnAddress(mapped mappedELF, fn uintptr) (uintptr, bool) {
@@ -2,6 +2,8 @@
package memmod
import "errors"
//go:noescape
func cCall0(fn uintptr) uintptr
@@ -13,3 +15,21 @@ func cCall2(fn, a0, a1 uintptr) uintptr
//go:noescape
func cCall3(fn, a0, a1, a2 uintptr) uintptr
func cCallVoid0(fn uintptr) {
_ = cCall0(fn)
}
func cCallVoid3(fn, a0, a1, a2 uintptr) {
_ = cCall3(fn, a0, a1, a2)
}
func cCallVoid0OnThread(fn uintptr) error {
_ = fn
return errors.New("calling a Go c-shared export requires cgo")
}
func linuxGoTLSSlotOffset(slot uintptr) (int64, error) {
_ = slot
return 0, errors.New("loading a Go c-shared image requires cgo TLS support")
}
@@ -3,13 +3,17 @@
package memmod
/*
#include <pthread.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
typedef uintptr_t (*reflektor_fn0)(void);
typedef uintptr_t (*reflektor_fn1)(uintptr_t);
typedef uintptr_t (*reflektor_fn2)(uintptr_t, uintptr_t);
typedef uintptr_t (*reflektor_fn3)(uintptr_t, uintptr_t, uintptr_t);
typedef void (*reflektor_void_fn0)(void);
typedef void (*reflektor_init_fn)(int, char **, char **);
static uintptr_t reflektor_call0(uintptr_t fn) {
return ((reflektor_fn0)fn)();
@@ -27,31 +31,89 @@ static uintptr_t reflektor_call3(uintptr_t fn, uintptr_t a0, uintptr_t a1, uintp
return ((reflektor_fn3)fn)(a0, a1, a2);
}
static uintptr_t reflektor_init_argc = 0;
static uintptr_t reflektor_init_argv = 0;
static uintptr_t reflektor_init_envp = 0;
static void reflektor_call_void0(uintptr_t fn) {
((reflektor_void_fn0)fn)();
}
// Build a tiny synthetic argv/envp/auxv vector for runtimes that expect a
// startup-like layout (argc/argv/envp/auxv). The layout is:
// argv[0] = NULL
// envp[0] = NULL
// auxv[0] = AT_NULL
// auxv[1] = 0
static void reflektor_init_call_args(uintptr_t *argc, uintptr_t *argv, uintptr_t *envp) {
if (reflektor_init_argv == 0) {
uintptr_t *vec = (uintptr_t *)calloc(4, sizeof(uintptr_t));
if (vec != NULL) {
reflektor_init_argv = (uintptr_t)vec;
reflektor_init_envp = (uintptr_t)(vec + 1);
static void reflektor_call_void3(uintptr_t fn, uintptr_t a0, uintptr_t a1, uintptr_t a2) {
((reflektor_init_fn)fn)((int)a0, (char **)a1, (char **)a2);
}
typedef struct {
uintptr_t fn;
} reflektor_thread_call;
static void *reflektor_thread_entry(void *opaque) {
reflektor_thread_call *call = (reflektor_thread_call *)opaque;
((reflektor_void_fn0)call->fn)();
return NULL;
}
static int reflektor_call_on_thread(reflektor_thread_call *call) {
pthread_t thread;
int err = pthread_create(&thread, NULL, reflektor_thread_entry, call);
if (err != 0) {
return err;
}
return pthread_join(thread, NULL);
}
static int reflektor_call_void0_thread(uintptr_t fn) {
reflektor_thread_call call;
memset(&call, 0, sizeof(call));
call.fn = fn;
return reflektor_call_on_thread(&call);
}
// Go's ELF runtime stores thread state in up to two words of static TLS.
// Reserve independent slots so manually mapped Go runtimes do not alias the
// host Go runtime or each other. Static TLS is instantiated, zeroed, and kept
// at the same thread-pointer-relative offset for every pthread.
#define REFLEKTOR_GO_TLS_SLOTS 64
static __thread uintptr_t reflektor_go_tls_slots[REFLEKTOR_GO_TLS_SLOTS][2]
__attribute__((tls_model("initial-exec")));
static intptr_t reflektor_go_tls_offset(uintptr_t slot) {
if (slot >= REFLEKTOR_GO_TLS_SLOTS) {
return INTPTR_MIN;
}
return (intptr_t)(uintptr_t)&reflektor_go_tls_slots[slot][0] -
(intptr_t)(uintptr_t)__builtin_thread_pointer();
}
extern char **environ;
static uintptr_t reflektor_empty_init_args[4];
// Build a stable argc/argv/envp/auxv vector for runtimes that expect a
// startup-like layout. argc is zero, argv[0] is NULL, the current environment
// follows it, and a terminating AT_NULL pair follows the environment.
static void reflektor_init_call_args(int include_environment, uintptr_t *argc, uintptr_t *argv, uintptr_t *envp) {
if (!include_environment) {
*argc = 0;
*argv = (uintptr_t)reflektor_empty_init_args;
*envp = (uintptr_t)(reflektor_empty_init_args + 1);
return;
}
size_t envc = 0;
while (environ != NULL && environ[envc] != NULL) {
envc++;
}
uintptr_t *vec = (uintptr_t *)calloc(envc + 4, sizeof(uintptr_t));
if (vec != NULL) {
for (size_t i = 0; i < envc; i++) {
vec[i + 1] = (uintptr_t)environ[i];
}
}
*argc = reflektor_init_argc;
*argv = reflektor_init_argv;
*envp = reflektor_init_envp;
*argc = 0;
*argv = (uintptr_t)vec;
*envp = vec == NULL ? 0 : (uintptr_t)(vec + 1);
}
*/
import "C"
import "fmt"
func cCall0(fn uintptr) uintptr {
return uintptr(C.reflektor_call0(C.uintptr_t(fn)))
}
@@ -68,10 +130,37 @@ func cCall3(fn, a0, a1, a2 uintptr) uintptr {
return uintptr(C.reflektor_call3(C.uintptr_t(fn), C.uintptr_t(a0), C.uintptr_t(a1), C.uintptr_t(a2)))
}
func linuxInitCallArgs() (uintptr, uintptr, uintptr) {
func cCallVoid0(fn uintptr) {
C.reflektor_call_void0(C.uintptr_t(fn))
}
func cCallVoid3(fn, a0, a1, a2 uintptr) {
C.reflektor_call_void3(C.uintptr_t(fn), C.uintptr_t(a0), C.uintptr_t(a1), C.uintptr_t(a2))
}
func cCallVoid0OnThread(fn uintptr) error {
if errno := int(C.reflektor_call_void0_thread(C.uintptr_t(fn))); errno != 0 {
return fmt.Errorf("pthread call failed with error %d", errno)
}
return nil
}
func linuxGoTLSSlotOffset(slot uintptr) (int64, error) {
if slot >= 64 {
return 0, fmt.Errorf("Go TLS slot %d is out of range", slot)
}
offset := C.reflektor_go_tls_offset(C.uintptr_t(slot))
return int64(offset), nil
}
func linuxInitCallArgs(includeEnvironment bool) (uintptr, uintptr, uintptr) {
var argc C.uintptr_t
var argv C.uintptr_t
var envp C.uintptr_t
C.reflektor_init_call_args(&argc, &argv, &envp)
include := C.int(0)
if includeEnvironment {
include = 1
}
C.reflektor_init_call_args(include, &argc, &argv, &envp)
return uintptr(argc), uintptr(argv), uintptr(envp)
}
@@ -2,6 +2,7 @@
package memmod
func linuxInitCallArgs() (uintptr, uintptr, uintptr) {
func linuxInitCallArgs(includeEnvironment bool) (uintptr, uintptr, uintptr) {
_ = includeEnvironment
return 0, 0, 0
}
+59 -18
View File
@@ -6,6 +6,8 @@
package memmod
import (
"bytes"
"debug/buildinfo"
"errors"
"fmt"
"strings"
@@ -28,15 +30,23 @@ func (head *addressList) free() {
}
type Module struct {
headers *IMAGE_NT_HEADERS
codeBase uintptr
modules []windows.Handle
initialized bool
isDLL bool
isRelocated bool
nameExports map[string]uint16
entry uintptr
blockedMemory *addressList
headers *IMAGE_NT_HEADERS
codeBase uintptr
modules []windows.Handle
initialized bool
isDLL bool
isRelocated bool
nameExports map[string]uint16
entry uintptr
blockedMemory *addressList
exceptionTable uintptr
goRuntime bool
runtimeStarted bool
}
func imageHasGoBuildInfo(data []byte) bool {
_, err := buildinfo.Read(bytes.NewReader(data))
return err == nil
}
func (module *Module) headerDirectory(idx int) *IMAGE_DATA_DIRECTORY {
@@ -161,14 +171,22 @@ func (module *Module) finalizeSection(sectionData *sectionFinalizeData) error {
return nil
}
var rtlAddFunctionTable = windows.NewLazySystemDLL("ntdll.dll").NewProc("RtlAddFunctionTable")
func (module *Module) registerExceptionHandlers() {
func (module *Module) registerExceptionHandlers() error {
directory := module.headerDirectory(IMAGE_DIRECTORY_ENTRY_EXCEPTION)
if directory.Size == 0 || directory.VirtualAddress == 0 {
return
entrySize := runtimeFunctionEntrySize()
if directory.Size == 0 || directory.VirtualAddress == 0 || entrySize == 0 {
return nil
}
rtlAddFunctionTable.Call(module.codeBase+uintptr(directory.VirtualAddress), uintptr(directory.Size)/unsafe.Sizeof(IMAGE_RUNTIME_FUNCTION_ENTRY{}), module.codeBase)
if uintptr(directory.Size)%entrySize != 0 {
return fmt.Errorf("malformed exception table size %#x for entry size %d", directory.Size, entrySize)
}
module.exceptionTable = module.codeBase + uintptr(directory.VirtualAddress)
count := uint32(uintptr(directory.Size) / entrySize)
if !windows.RtlAddFunctionTable((*windows.RUNTIME_FUNCTION)(unsafe.Pointer(module.exceptionTable)), count, module.codeBase) {
module.exceptionTable = 0
return errors.New("RtlAddFunctionTable returned false")
}
return nil
}
func (module *Module) finalizeSections() error {
@@ -498,7 +516,10 @@ func LoadLibrary(data []byte) (module *Module, err error) {
return nil, errors.New("Section is not page-aligned")
}
module = &Module{isDLL: (oldHeader.FileHeader.Characteristics & IMAGE_FILE_DLL) != 0}
module = &Module{
isDLL: (oldHeader.FileHeader.Characteristics & IMAGE_FILE_DLL) != 0,
goRuntime: imageHasGoBuildInfo(data),
}
defer func() {
if err != nil {
module.Free()
@@ -583,7 +604,10 @@ func LoadLibrary(data []byte) (module *Module, err error) {
}
// Register exception tables, if they exist.
module.registerExceptionHandlers()
err = module.registerExceptionHandlers()
if err != nil {
return
}
// Register function PCs.
loadedAddressRangesMu.Lock()
@@ -598,6 +622,12 @@ func LoadLibrary(data []byte) (module *Module, err error) {
}
// TLS callbacks are executed BEFORE the main loading.
if module.goRuntime {
// Go DLL initialization starts process-lifetime runtime state. From this
// point onward, an error or Close must leave the image and its imports
// pinned rather than detach and unmap live runtime-managed threads.
module.runtimeStarted = true
}
module.executeTLS()
// Get entry point of loaded module.
@@ -619,8 +649,15 @@ func LoadLibrary(data []byte) (module *Module, err error) {
return
}
// Free releases module resources and unloads it.
// Free releases module resources. Started Go runtime images remain pinned.
func (module *Module) Free() {
if module.goRuntime && module.runtimeStarted {
if module.blockedMemory != nil {
module.blockedMemory.free()
module.blockedMemory = nil
}
return
}
if module.initialized {
// Notify library about detaching from process.
syscall.Syscall(module.entry, 3, module.codeBase, uintptr(DLL_PROCESS_DETACH), 0)
@@ -633,6 +670,10 @@ func (module *Module) Free() {
}
module.modules = nil
}
if module.exceptionTable != 0 {
windows.RtlDeleteFunctionTable((*windows.RUNTIME_FUNCTION)(unsafe.Pointer(module.exceptionTable)))
module.exceptionTable = 0
}
if module.codeBase != 0 {
windows.VirtualFree(module.codeBase, 0, windows.MEM_RELEASE)
module.codeBase = 0
@@ -6,3 +6,5 @@
package memmod
const imageFileProcess = IMAGE_FILE_MACHINE_I386
func runtimeFunctionEntrySize() uintptr { return 0 }
@@ -6,3 +6,5 @@
package memmod
const imageFileProcess = IMAGE_FILE_MACHINE_AMD64
func runtimeFunctionEntrySize() uintptr { return 12 }
@@ -6,3 +6,5 @@
package memmod
const imageFileProcess = IMAGE_FILE_MACHINE_ARMNT
func runtimeFunctionEntrySize() uintptr { return 8 }
@@ -6,3 +6,5 @@
package memmod
const imageFileProcess = IMAGE_FILE_MACHINE_ARM64
func runtimeFunctionEntrySize() uintptr { return 8 }
+1 -1
View File
@@ -69,7 +69,7 @@ github.com/miekg/dns
# github.com/pmezard/go-difflib v1.0.0
## explicit
github.com/pmezard/go-difflib/difflib
# github.com/sliverarmory/reflektor v0.0.2
# github.com/sliverarmory/reflektor v0.0.3
## explicit; go 1.26.6
github.com/sliverarmory/reflektor/memmod
# github.com/stretchr/testify v1.8.4
+14 -2
View File
@@ -66,6 +66,7 @@ var (
"linux/arm64": true,
"windows/386": true,
"windows/amd64": true,
"windows/arm64": true,
}
)
@@ -844,11 +845,14 @@ func renderSliverGoCode(name string, build *clientpb.ImplantBuild, config *clien
return "", nil
}
err = fs.WalkDir(implant.FS, ".", func(fsPath string, f fs.DirEntry, err error) error {
err = fs.WalkDir(implant.FS, ".", func(fsPath string, f fs.DirEntry, walkErr error) (retErr error) {
if walkErr != nil {
return walkErr
}
if f.IsDir() {
return nil
}
buildLog.Debugf("Walking: %s %s %v", fsPath, f.Name(), err)
buildLog.Debugf("Walking: %s %s", fsPath, f.Name())
sliverGoCodeRaw, err := implant.FS.ReadFile(fsPath)
if err != nil {
@@ -882,6 +886,11 @@ func renderSliverGoCode(name string, build *clientpb.ImplantBuild, config *clien
if err != nil {
return err
}
defer func() {
if closeErr := fSliver.Close(); retErr == nil && closeErr != nil {
retErr = closeErr
}
}()
if !util.Contains([]string{".go", ".c", ".h"}, path.Ext(f.Name())) {
buildLog.Debugf("Skipping render for %s, does not appear to be source code file", f.Name())
_, err = fSliver.Write(sliverGoCodeRaw)
@@ -1376,6 +1385,9 @@ func GetCompilerTargets() []*clientpb.CompilerTarget {
platform := strings.SplitN(longPlatform, "/", 2)
switch platform[0] {
case WINDOWS:
if platform[1] != "amd64" && platform[1] != "386" {
continue
}
targets = append(targets, &clientpb.CompilerTarget{
GOOS: platform[0],
GOARCH: platform[1],
+46
View File
@@ -0,0 +1,46 @@
# syntax=docker/dockerfile:1.7
ARG GO_VERSION=1.26.6
FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-bookworm AS server-build
WORKDIR /workspace
COPY . .
RUN --mount=type=cache,id=sliver-reflektor-server-go-build,target=/root/.cache/go-build,sharing=locked \
set -eux; \
mkdir -p /out; \
go run -buildvcs=false -mod=vendor ./util/cmd/assets; \
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
go build -buildvcs=false -mod=vendor -trimpath -tags go_sqlite,server \
-o /out/sliver-server ./server; \
rm -rf /workspace/server/assets/fs
FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-bookworm AS driver-build
WORKDIR /workspace
COPY . .
RUN --mount=type=cache,id=sliver-reflektor-driver-go-build,target=/root/.cache/go-build,sharing=locked \
set -eux; \
mkdir -p /out; \
CGO_ENABLED=0 GOOS=linux GOARCH=386 \
go build -buildvcs=false -mod=vendor -trimpath -tags client,go_sqlite \
-o /out/sliver-reflektor-e2e ./test/reflektor
FROM --platform=$TARGETPLATFORM golang:${GO_VERSION}-bookworm AS runtime
RUN apt-get update \
&& apt-get install -y --no-install-recommends build-essential ca-certificates git \
&& rm -rf /var/lib/apt/lists/*
# Zig's LLD emits an invalid i386 TLS relocation for Go c-shared binaries.
# Use the native Debian i386 compiler for this same-architecture build instead.
ENV SLIVER_LINUX_CC_32=/usr/bin/i686-linux-gnu-gcc
WORKDIR /workspace
COPY go.mod go.sum ./
COPY --from=server-build /out/sliver-server /workspace/sliver-server
COPY --from=driver-build /out/sliver-reflektor-e2e /usr/local/bin/sliver-reflektor-e2e
CMD ["sliver-reflektor-e2e", "-repo", "/workspace", "-server", "/workspace/sliver-server", "-target-os", "linux", "-target-arch", "386"]
@@ -0,0 +1,31 @@
# Git metadata is not needed by the Reflektor integration image.
/.git/
# go-assets.sh downloads
/server/assets/fs/**.zip
/server/assets/fs/**/garble*
/server/assets/fs/**/zig.tar.xz
/server/assets/fs/**/zig.zip
/server/assets/fs/src.zip
/*.sdk.tar.xz
/sliver-server
/sliver-server.exe
/sliver-client
/sliver-client.exe
/.vscode/
/.vscode/*
/.github/
/.github/*
/build.py
/LICENSE
/SECURITY.md
/README.md
/CONTRIBUTING.md
.downloaded_assets
+739
View File
@@ -0,0 +1,739 @@
package main
import (
"context"
"errors"
"flag"
"fmt"
"net"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"time"
clientassets "github.com/bishopfox/sliver/client/assets"
consts "github.com/bishopfox/sliver/client/constants"
clienttransport "github.com/bishopfox/sliver/client/transport"
"github.com/bishopfox/sliver/protobuf/clientpb"
"github.com/bishopfox/sliver/protobuf/commonpb"
"github.com/bishopfox/sliver/protobuf/rpcpb"
"golang.org/x/mod/modfile"
)
const (
operatorName = "testuser"
reflektorModule = "github.com/sliverarmory/reflektor"
reflektorExport = "StartW"
processLogTailBytes = 1024 * 1024
eventBufferSize = 128
eventSyncTimeout = 30 * time.Second
listenerPollInterval = 250 * time.Millisecond
listenerDialTimeout = 500 * time.Millisecond
cleanupGraceTimeout = 2 * time.Second
cleanupProcessTimeout = 10 * time.Second
)
type options struct {
repoPath string
serverPath string
targetOS string
targetArch string
timeout time.Duration
startupTimeout time.Duration
sessionTimeout time.Duration
}
type managedProcess struct {
cmd *exec.Cmd
done chan struct{}
err error
logPath string
}
type eventPump struct {
events chan *clientpb.Event
err error
}
func main() {
opts := options{}
flag.StringVar(&opts.repoPath, "repo", ".", "path to the Sliver repository")
flag.StringVar(&opts.serverPath, "server", "", "path to the Sliver server executable")
flag.StringVar(&opts.targetOS, "target-os", runtime.GOOS, "shared library target operating system")
flag.StringVar(&opts.targetArch, "target-arch", runtime.GOARCH, "shared library target architecture")
flag.DurationVar(&opts.timeout, "timeout", 60*time.Minute, "overall integration test timeout")
flag.DurationVar(&opts.startupTimeout, "startup-timeout", 10*time.Minute, "Sliver daemon startup timeout")
flag.DurationVar(&opts.sessionTimeout, "session-timeout", 3*time.Minute, "session connection timeout after Reflektor starts")
flag.Parse()
if err := run(opts); err != nil {
fmt.Fprintf(os.Stderr, "reflektor integration test failed: %v\n", err)
os.Exit(1)
}
}
func run(opts options) error {
if err := validateOptions(&opts); err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), opts.timeout)
defer cancel()
workDir, err := os.MkdirTemp("", "sliver-reflektor-e2e-")
if err != nil {
return fmt.Errorf("create test directory: %w", err)
}
defer os.RemoveAll(workDir)
serverRoot := filepath.Join(workDir, "server")
clientRoot := filepath.Join(workDir, "client")
homeDir := filepath.Join(workDir, "home")
for _, dir := range []string{serverRoot, clientRoot, homeDir} {
if err := os.MkdirAll(dir, 0o700); err != nil {
return fmt.Errorf("create isolated test directory %q: %w", dir, err)
}
}
grpcPort, err := unusedTCPPort()
if err != nil {
return fmt.Errorf("select multiplayer port: %w", err)
}
serverEnv := os.Environ()
for name, value := range map[string]string{
"HOME": homeDir,
"SLIVER_CLIENT_ROOT_DIR": clientRoot,
"SLIVER_ROOT_DIR": serverRoot,
"USERPROFILE": homeDir,
} {
serverEnv = envWith(serverEnv, name, value)
}
serverLog := filepath.Join(workDir, "sliver-server.log")
serverProcess, err := startProcess(
opts.serverPath,
[]string{
"daemon",
"--lhost", "127.0.0.1",
"--lport", fmt.Sprintf("%d", grpcPort),
"--force",
},
opts.repoPath,
serverEnv,
serverLog,
)
if err != nil {
return fmt.Errorf("start Sliver daemon: %w", err)
}
defer serverProcess.stop()
fmt.Printf("Started Sliver daemon on 127.0.0.1:%d\n", grpcPort)
startupCtx, startupCancel := context.WithTimeout(ctx, opts.startupTimeout)
err = waitForTCP(startupCtx, fmt.Sprintf("127.0.0.1:%d", grpcPort), serverProcess)
startupCancel()
if err != nil {
return fmt.Errorf("wait for Sliver daemon: %w", err)
}
profilePath := filepath.Join(workDir, "testuser.cfg")
profileCtx, profileCancel := context.WithTimeout(ctx, 2*time.Minute)
profileOutput, profileErr := runCommand(
profileCtx,
opts.repoPath,
serverEnv,
opts.serverPath,
"operator",
"--name", operatorName,
"--lhost", "127.0.0.1",
"--lport", fmt.Sprintf("%d", grpcPort),
"--permissions", "all",
"--save", profilePath,
)
profileCancel()
if profileErr != nil {
return fmt.Errorf("generate operator profile: %w\n%s", profileErr, profileOutput)
}
if _, err := os.Stat(profilePath); err != nil {
return fmt.Errorf("operator command did not create %q: %w\n%s", profilePath, err, profileOutput)
}
clientConfig, err := clientassets.ReadConfig(profilePath)
if err != nil {
return fmt.Errorf("read operator profile: %w", err)
}
if clientConfig.Operator != operatorName || clientConfig.LHost != "127.0.0.1" || clientConfig.LPort != grpcPort {
return fmt.Errorf(
"operator profile mismatch: got operator=%q endpoint=%s:%d",
clientConfig.Operator,
clientConfig.LHost,
clientConfig.LPort,
)
}
fmt.Printf("Saved %s multiplayer profile to disk\n", operatorName)
rpc, conn, err := clienttransport.MTLSConnect(clientConfig)
if err != nil {
return fmt.Errorf("connect to multiplayer gRPC: %w", err)
}
defer clienttransport.CloseGRPCConnection(conn)
version, err := rpc.GetVersion(ctx, &commonpb.Empty{})
if err != nil {
return fmt.Errorf("query Sliver server version: %w", err)
}
fmt.Printf("Connected to Sliver server %s/%s over gRPC\n", version.OS, version.Arch)
eventsCtx, eventsCancel := context.WithCancel(ctx)
defer eventsCancel()
events, err := rpc.Events(eventsCtx, &commonpb.Empty{})
if err != nil {
return fmt.Errorf("subscribe to Sliver events: %w", err)
}
pump := startEventPump(events)
httpPort, err := unusedTCPPort()
if err != nil {
return fmt.Errorf("select HTTP port: %w", err)
}
listener, err := rpc.StartHTTPListener(ctx, &clientpb.HTTPListenerReq{
Domain: "localhost",
Host: "127.0.0.1",
Port: uint32(httpPort),
Secure: false,
EnforceOTP: false,
LongPollTimeout: int64(time.Second),
})
if err != nil {
return fmt.Errorf("start HTTP listener: %w", err)
}
defer func() {
cleanupCtx, cleanupCancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cleanupCancel()
_, _ = rpc.KillJob(cleanupCtx, &clientpb.KillJobReq{ID: listener.JobID})
}()
listenerCtx, listenerCancel := context.WithTimeout(ctx, eventSyncTimeout)
err = waitForListenerReady(listenerCtx, pump, listener.JobID, fmt.Sprintf("127.0.0.1:%d", httpPort))
listenerCancel()
if err != nil {
return fmt.Errorf("wait for HTTP listener: %w", err)
}
fmt.Printf("Started HTTP listener on 127.0.0.1:%d (job %d)\n", httpPort, listener.JobID)
implantName := fmt.Sprintf("reflektor%s%s", opts.targetOS, opts.targetArch)
generated, err := rpc.Generate(ctx, &clientpb.GenerateReq{
Name: implantName,
Config: &clientpb.ImplantConfig{
GOOS: opts.targetOS,
GOARCH: opts.targetArch,
TemplateName: "sliver",
Debug: false,
ObfuscateSymbols: false,
IsBeacon: false,
Format: clientpb.OutputFormat_SHARED_LIB,
IsSharedLib: true,
RunAtLoad: false,
Exports: []string{reflektorExport},
C2: []*clientpb.ImplantC2{{URL: fmt.Sprintf("http://127.0.0.1:%d?force-http=true&poll-timeout=1s", httpPort)}},
IncludeHTTP: true,
HTTPC2ConfigName: consts.DefaultC2Profile,
ConnectionStrategy: "s",
ReconnectInterval: int64(time.Second),
PollTimeout: int64(time.Second),
MaxConnectionErrors: 10,
NetGoEnabled: true,
},
})
if err != nil {
return fmt.Errorf("generate %s/%s shared library: %w", opts.targetOS, opts.targetArch, err)
}
if generated.File == nil || generated.File.Name == "" || len(generated.File.Data) == 0 {
return errors.New("Sliver returned an empty shared library")
}
buildEventCtx, buildEventCancel := context.WithTimeout(ctx, eventSyncTimeout)
buildEvent, buildEventErr := waitForEvent(buildEventCtx, pump, func(event *clientpb.Event) bool {
return (event.EventType == consts.BuildCompletedEvent && string(event.Data) == generated.File.Name) ||
isListenerStopped(event, listener.JobID)
})
buildEventCancel()
if buildEventErr != nil {
return fmt.Errorf("wait for shared-library build event: %w", buildEventErr)
}
if err := listenerStoppedError(buildEvent, listener.JobID); err != nil {
return err
}
libraryPath := filepath.Join(workDir, filepath.Base(generated.File.Name))
if err := os.WriteFile(libraryPath, generated.File.Data, 0o600); err != nil {
return fmt.Errorf("save generated shared library: %w", err)
}
fmt.Printf("Generated %s/%s session shared library %s\n", opts.targetOS, opts.targetArch, filepath.Base(libraryPath))
reflektorPath, reflektorVersion, err := buildReflektorCLI(ctx, opts.repoPath, workDir, opts.targetOS, opts.targetArch)
if err != nil {
return err
}
fmt.Printf("Built Reflektor CLI %s from Sliver go.mod\n", reflektorVersion)
reflektorLog := filepath.Join(workDir, "reflektor.log")
reflektorProcess, err := startProcess(
reflektorPath,
[]string{"--call-export", reflektorExport, libraryPath},
opts.repoPath,
os.Environ(),
reflektorLog,
)
if err != nil {
return fmt.Errorf("start Reflektor CLI: %w", err)
}
defer reflektorProcess.stop()
sessionCtx, sessionCancel := context.WithTimeout(ctx, opts.sessionTimeout)
defer sessionCancel()
sessionEvent, err := waitForSession(
sessionCtx,
pump,
reflektorProcess,
listener.JobID,
generated.ImplantName,
opts.targetOS,
opts.targetArch,
)
if err != nil {
return fmt.Errorf("%w\nSliver server log:\n%s", err, readLogTail(serverLog))
}
fmt.Printf(
"Reflektor loaded Sliver and connected session %s (%s, %s/%s)\n",
sessionEvent.Session.ID,
sessionEvent.Session.Name,
sessionEvent.Session.OS,
sessionEvent.Session.Arch,
)
return nil
}
func validateOptions(opts *options) error {
if opts.serverPath == "" {
return errors.New("-server is required")
}
repoPath, err := filepath.Abs(opts.repoPath)
if err != nil {
return fmt.Errorf("resolve repository path: %w", err)
}
serverPath, err := filepath.Abs(opts.serverPath)
if err != nil {
return fmt.Errorf("resolve server path: %w", err)
}
if stat, err := os.Stat(serverPath); err != nil {
return fmt.Errorf("stat server executable: %w", err)
} else if stat.IsDir() {
return fmt.Errorf("server executable %q is a directory", serverPath)
}
if _, err := os.Stat(filepath.Join(repoPath, "go.mod")); err != nil {
return fmt.Errorf("repository does not contain go.mod: %w", err)
}
opts.repoPath = repoPath
opts.serverPath = serverPath
opts.targetOS = strings.ToLower(strings.TrimSpace(opts.targetOS))
opts.targetArch = strings.ToLower(strings.TrimSpace(opts.targetArch))
supported := map[string]bool{
"darwin/amd64": true,
"darwin/arm64": true,
"linux/386": true,
"linux/amd64": true,
"linux/arm64": true,
"windows/386": true,
"windows/amd64": true,
"windows/arm64": true,
}
target := opts.targetOS + "/" + opts.targetArch
if !supported[target] {
return fmt.Errorf("target %s is not in Reflektor's supported test matrix", target)
}
if opts.targetOS != runtime.GOOS || opts.targetArch != runtime.GOARCH {
return fmt.Errorf(
"Reflektor must run as the target architecture: driver is %s/%s, target is %s",
runtime.GOOS,
runtime.GOARCH,
target,
)
}
if opts.timeout <= 0 || opts.startupTimeout <= 0 || opts.sessionTimeout <= 0 {
return errors.New("timeouts must be positive")
}
return nil
}
func unusedTCPPort() (int, error) {
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
return 0, err
}
defer listener.Close()
return listener.Addr().(*net.TCPAddr).Port, nil
}
func waitForTCP(ctx context.Context, address string, process *managedProcess) error {
ticker := time.NewTicker(listenerPollInterval)
defer ticker.Stop()
for {
conn, err := net.DialTimeout("tcp", address, listenerDialTimeout)
if err == nil {
_ = conn.Close()
return nil
}
select {
case <-ctx.Done():
return ctx.Err()
case <-process.done:
return process.failure("Sliver daemon exited before accepting connections")
case <-ticker.C:
}
}
}
func startProcess(path string, args []string, dir string, env []string, logPath string) (*managedProcess, error) {
logFile, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o600)
if err != nil {
return nil, err
}
cmd := exec.Command(path, args...)
cmd.Dir = dir
cmd.Env = env
cmd.Stdout = logFile
cmd.Stderr = logFile
prepareCommand(cmd)
if err := cmd.Start(); err != nil {
_ = logFile.Close()
return nil, err
}
process := &managedProcess{
cmd: cmd,
done: make(chan struct{}),
logPath: logPath,
}
go func() {
process.err = cmd.Wait()
_ = logFile.Close()
close(process.done)
}()
return process, nil
}
func (process *managedProcess) stop() {
select {
case <-process.done:
return
default:
}
terminateProcessTree(process.cmd)
select {
case <-process.done:
return
case <-time.After(cleanupGraceTimeout):
}
killProcessTree(process.cmd)
select {
case <-process.done:
case <-time.After(cleanupProcessTimeout):
fmt.Fprintf(os.Stderr, "timed out cleaning up process tree for %s (pid %d)\n", process.cmd.Path, process.cmd.Process.Pid)
}
}
func (process *managedProcess) failure(message string) error {
processErr := process.err
if processErr == nil {
processErr = errors.New("process exited")
}
return fmt.Errorf("%s: %w\n%s", message, processErr, readLogTail(process.logPath))
}
func readLogTail(path string) string {
data, err := os.ReadFile(path)
if err != nil {
return fmt.Sprintf("(could not read %s: %v)", path, err)
}
if len(data) > processLogTailBytes {
data = data[len(data)-processLogTailBytes:]
}
return strings.TrimSpace(string(data))
}
func runCommand(ctx context.Context, dir string, env []string, path string, args ...string) (string, error) {
cmd := exec.CommandContext(ctx, path, args...)
cmd.Dir = dir
cmd.Env = env
output, err := cmd.CombinedOutput()
return strings.TrimSpace(string(output)), err
}
func envWith(env []string, name string, value string) []string {
prefix := name + "="
updated := make([]string, 0, len(env)+1)
for _, entry := range env {
if !strings.HasPrefix(entry, prefix) {
updated = append(updated, entry)
}
}
return append(updated, prefix+value)
}
func startEventPump(stream rpcpb.SliverRPC_EventsClient) *eventPump {
pump := &eventPump{
events: make(chan *clientpb.Event, eventBufferSize),
}
go func() {
defer close(pump.events)
for {
event, err := stream.Recv()
if err != nil {
pump.err = err
return
}
select {
case pump.events <- event:
case <-stream.Context().Done():
pump.err = stream.Context().Err()
return
}
}
}()
return pump
}
func waitForListenerReady(ctx context.Context, pump *eventPump, jobID uint32, address string) error {
ticker := time.NewTicker(listenerPollInterval)
defer ticker.Stop()
started := false
for {
if started {
conn, err := net.DialTimeout("tcp", address, listenerDialTimeout)
if err == nil {
_ = conn.Close()
return nil
}
}
select {
case <-ctx.Done():
return ctx.Err()
case event, ok := <-pump.events:
if !ok {
return eventStreamError(pump, "event stream ended while starting HTTP listener")
}
if event.Job == nil || event.Job.ID != jobID {
continue
}
switch event.EventType {
case consts.JobStartedEvent:
started = true
case consts.JobStoppedEvent:
if event.Err != "" {
return fmt.Errorf("HTTP listener job %d stopped: %s", jobID, event.Err)
}
return fmt.Errorf("HTTP listener job %d stopped before accepting connections", jobID)
}
case <-ticker.C:
}
}
}
func waitForEvent(ctx context.Context, pump *eventPump, match func(*clientpb.Event) bool) (*clientpb.Event, error) {
for {
select {
case <-ctx.Done():
return nil, ctx.Err()
case event, ok := <-pump.events:
if !ok {
return nil, eventStreamError(pump, "event stream ended")
}
if match(event) {
return event, nil
}
}
}
}
func waitForSession(
ctx context.Context,
pump *eventPump,
reflektor *managedProcess,
listenerJobID uint32,
implantName string,
targetOS string,
targetArch string,
) (*clientpb.Event, error) {
for {
select {
case <-ctx.Done():
return nil, fmt.Errorf(
"wait for Reflektor-loaded session: %w\nReflektor log:\n%s",
ctx.Err(),
readLogTail(reflektor.logPath),
)
case <-reflektor.done:
for {
select {
case event, ok := <-pump.events:
if !ok {
return nil, eventStreamError(pump, "event stream ended while waiting for session")
}
if err := listenerStoppedError(event, listenerJobID); err != nil {
return nil, err
}
if matched, err := matchingSession(event, implantName, targetOS, targetArch); matched || err != nil {
return event, err
}
default:
return nil, reflektor.failure("Reflektor exited before Sliver connected")
}
}
case event, ok := <-pump.events:
if !ok {
return nil, eventStreamError(pump, "event stream ended while waiting for session")
}
if err := listenerStoppedError(event, listenerJobID); err != nil {
return nil, err
}
if matched, err := matchingSession(event, implantName, targetOS, targetArch); matched || err != nil {
return event, err
}
}
}
}
func isListenerStopped(event *clientpb.Event, jobID uint32) bool {
return event.EventType == consts.JobStoppedEvent && event.Job != nil && event.Job.ID == jobID
}
func listenerStoppedError(event *clientpb.Event, jobID uint32) error {
if !isListenerStopped(event, jobID) {
return nil
}
if event.Err != "" {
return fmt.Errorf("HTTP listener job %d stopped: %s", jobID, event.Err)
}
return fmt.Errorf("HTTP listener job %d stopped before the session connected", jobID)
}
func matchingSession(event *clientpb.Event, implantName string, targetOS string, targetArch string) (bool, error) {
if event.EventType != consts.SessionOpenedEvent || event.Session == nil {
return false, nil
}
if event.Session.Name != implantName {
return false, nil
}
if event.Session.OS != targetOS || event.Session.Arch != targetArch {
return true, fmt.Errorf(
"session target mismatch: got %s/%s, want %s/%s",
event.Session.OS,
event.Session.Arch,
targetOS,
targetArch,
)
}
return true, nil
}
func eventStreamError(pump *eventPump, message string) error {
if pump.err == nil {
return errors.New(message)
}
return fmt.Errorf("%s: %w", message, pump.err)
}
func buildReflektorCLI(
ctx context.Context,
repoPath string,
workDir string,
targetOS string,
targetArch string,
) (string, string, error) {
goMod, err := os.ReadFile(filepath.Join(repoPath, "go.mod"))
if err != nil {
return "", "", fmt.Errorf("read go.mod: %w", err)
}
parsed, err := modfile.Parse("go.mod", goMod, nil)
if err != nil {
return "", "", fmt.Errorf("parse go.mod: %w", err)
}
version := ""
for _, requirement := range parsed.Require {
if requirement.Mod.Path == reflektorModule {
version = requirement.Mod.Version
break
}
}
if version == "" {
return "", "", fmt.Errorf("%s is not required by go.mod", reflektorModule)
}
binDir := filepath.Join(workDir, "reflektor-bin")
if err := os.MkdirAll(binDir, 0o700); err != nil {
return "", "", fmt.Errorf("create Reflektor output directory: %w", err)
}
name := "reflektor"
if targetOS == "windows" {
name += ".exe"
}
path := filepath.Join(binDir, name)
cgoEnabled := "0"
if targetOS == "darwin" || targetOS == "linux" {
cgoEnabled = "1"
}
buildEnv := os.Environ()
for name, value := range map[string]string{
"CGO_ENABLED": cgoEnabled,
"GOARCH": targetArch,
"GOENV": "off",
"GOFLAGS": "",
"GOOS": targetOS,
"GOWORK": "off",
} {
buildEnv = envWith(buildEnv, name, value)
}
listCommand := exec.CommandContext(ctx, "go", "list", "-mod=readonly", "-m", "-f={{.Version}}", reflektorModule)
listCommand.Dir = repoPath
listCommand.Env = buildEnv
output, err := listCommand.CombinedOutput()
if err != nil {
return "", "", fmt.Errorf("resolve Reflektor CLI version: %w\n%s", err, output)
}
if selected := strings.TrimSpace(string(output)); selected != version {
return "", "", fmt.Errorf("Reflektor version mismatch: go.mod requires %s, module graph selected %s", version, selected)
}
buildCommand := exec.CommandContext(
ctx,
"go",
"build",
"-mod=readonly",
"-trimpath",
"-o", path,
reflektorModule+"/cli",
)
buildCommand.Dir = repoPath
buildCommand.Env = buildEnv
output, err = buildCommand.CombinedOutput()
if err != nil {
return "", "", fmt.Errorf("build Reflektor CLI %s for %s/%s: %w\n%s", version, targetOS, targetArch, err, output)
}
return path, version, nil
}
+20
View File
@@ -0,0 +1,20 @@
//go:build darwin || linux
package main
import (
"os/exec"
"syscall"
)
func prepareCommand(cmd *exec.Cmd) {
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
}
func terminateProcessTree(cmd *exec.Cmd) {
_ = syscall.Kill(-cmd.Process.Pid, syscall.SIGTERM)
}
func killProcessTree(cmd *exec.Cmd) {
_ = syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)
}
+18
View File
@@ -0,0 +1,18 @@
//go:build windows
package main
import (
"os/exec"
"strconv"
)
func prepareCommand(_ *exec.Cmd) {}
func terminateProcessTree(cmd *exec.Cmd) {
_ = exec.Command("taskkill.exe", "/PID", strconv.Itoa(cmd.Process.Pid), "/T").Run()
}
func killProcessTree(cmd *exec.Cmd) {
_ = exec.Command("taskkill.exe", "/PID", strconv.Itoa(cmd.Process.Pid), "/T", "/F").Run()
}
+33 -7
View File
@@ -36,9 +36,10 @@ var (
)
type Module struct {
mu sync.RWMutex
image []byte
closed bool
mu sync.RWMutex
image []byte
goRuntime bool
closed bool
}
// LoadLibrary loads a Mach-O image into the darwin in-memory loader context.
@@ -51,10 +52,23 @@ func LoadLibrary(data []byte) (*Module, error) {
if err != nil {
return nil, err
}
goRuntime, err := machOHasGoBuildInfo(image)
if err != nil {
return nil, err
}
cloned := make([]byte, len(image))
copy(cloned, image)
return &Module{image: cloned}, nil
return &Module{image: cloned, goRuntime: goRuntime}, nil
}
func machOHasGoBuildInfo(image []byte) (bool, error) {
f, err := macho.NewFile(bytes.NewReader(image))
if err != nil {
return false, fmt.Errorf("inspect Mach-O build info: %w", err)
}
defer f.Close()
return f.Section("__go_buildinfo") != nil, nil
}
// Free releases the in-memory Mach-O bytes.
@@ -94,7 +108,7 @@ func (module *Module) CallExport(name string) error {
image := module.image
module.mu.RUnlock()
rc := memmodLoader(image, symbol)
rc := memmodLoader(image, symbol, module.goRuntime)
runtime.KeepAlive(image)
if rc != 0 {
@@ -395,7 +409,7 @@ func isDarwinSystemInstallName(installName string) bool {
strings.HasPrefix(installName, "/System/Library/")
}
func memmodLoader(bufferRO []byte, entrySymbol string) int {
func memmodLoader(bufferRO []byte, entrySymbol string, isolateGoRuntime bool) int {
if len(bufferRO) == 0 || entrySymbol == "" {
return 1
}
@@ -789,7 +803,14 @@ func memmodLoader(bufferRO []byte, entrySymbol string) int {
// Exported fixture entry points use the C void(void) ABI. Keep that call
// signature exact when cgo supplies the foreign-function bridge.
callVoid0(addrEntry)
if isolateGoRuntime {
if err := callVoid0OnThread(addrEntry); err != nil {
setDarwinLoaderDetail(err.Error())
return 14
}
} else {
callVoid0(addrEntry)
}
// Keep mapped and scratch memory reachable until after entry returns.
runtime.KeepAlive(mapped.mapping)
runtime.KeepAlive(scratch)
@@ -1535,6 +1556,11 @@ func loaderStatusError(code int) error {
return fmt.Errorf("failed to preload Mach-O dependencies: %s", detail)
}
return errors.New("failed to preload Mach-O dependencies")
case 14:
if detail := getDarwinLoaderDetail(); detail != "" {
return fmt.Errorf("failed to call Go export on isolated thread: %s", detail)
}
return errors.New("failed to call Go export on isolated thread")
default:
return fmt.Errorf("in-memory dyld loader failed with status %d", code)
}
+9 -1
View File
@@ -2,7 +2,10 @@
package memmod
import _ "unsafe"
import (
"errors"
_ "unsafe"
)
//go:noescape
func cCall10(fn, a0, a1, a2, a3, a4, a5, a6, a7, a8, a9 uintptr) uintptr
@@ -18,6 +21,11 @@ func callVoid0(fn uintptr) {
_ = call0(fn)
}
func callVoid0OnThread(fn uintptr) error {
_ = fn
return errors.New("calling a Go c-shared export requires cgo")
}
func callDlopen(fn, name uintptr, flags int) uintptr {
return call2(fn, name, uintptr(flags))
}
@@ -3,7 +3,9 @@
package memmod
/*
#include <pthread.h>
#include <stdint.h>
#include <string.h>
typedef uintptr_t (*reflektor_fn10_t)(
uintptr_t, uintptr_t, uintptr_t, uintptr_t, uintptr_t,
@@ -33,13 +35,44 @@ static uintptr_t reflektor_call10(
) {
return ((reflektor_fn10_t)fn)(a0, a1, a2, a3, a4, a5, a6, a7, a8, a9);
}
typedef struct {
uintptr_t fn;
} reflektor_void_thread_call;
static void *reflektor_void_thread_entry(void *opaque) {
reflektor_void_thread_call *call = (reflektor_void_thread_call *)opaque;
((reflektor_void_fn0_t)call->fn)();
return NULL;
}
static int reflektor_call_void0_thread(uintptr_t fn) {
pthread_t thread;
reflektor_void_thread_call call;
memset(&call, 0, sizeof(call));
call.fn = fn;
int err = pthread_create(&thread, NULL, reflektor_void_thread_entry, &call);
if (err != 0) {
return err;
}
return pthread_join(thread, NULL);
}
*/
import "C"
import "fmt"
func callVoid0(fn uintptr) {
C.reflektor_call_void0(C.uintptr_t(fn))
}
func callVoid0OnThread(fn uintptr) error {
if errno := int(C.reflektor_call_void0_thread(C.uintptr_t(fn))); errno != 0 {
return fmt.Errorf("pthread call failed with error %d", errno)
}
return nil
}
func callDlopen(fn, name uintptr, flags int) uintptr {
return uintptr(C.reflektor_call_dlopen(C.uintptr_t(fn), C.uintptr_t(name), C.int(flags)))
}
+167 -103
View File
@@ -45,17 +45,20 @@ const (
)
type Module struct {
mu sync.RWMutex
mapping []byte
loadBias uintptr
symbols map[string]uintptr
closed bool
mu sync.RWMutex
mapping []byte
loadBias uintptr
symbols map[string]uintptr
goRuntime bool
closed bool
}
type mappedELF struct {
mapping []byte
loadBias uintptr
progs []*elf.Prog
mapping []byte
loadBias uintptr
progs []*elf.Prog
tlsOffset int64
hasTLS bool
}
type dynamicInitInfo struct {
@@ -80,6 +83,13 @@ type symbolResolver struct {
opened map[string]uintptr
}
var linuxGoTLSSlots = struct {
sync.Mutex
used [linuxGoTLSSlotCount]bool
}{}
const linuxGoTLSSlotCount = 64
func LoadLibrary(data []byte) (*Module, error) {
if len(data) == 0 {
return nil, errors.New("empty ELF image")
@@ -94,11 +104,27 @@ func LoadLibrary(data []byte) (*Module, error) {
if err := validateELFHeaders(f); err != nil {
return nil, err
}
initInfo, err := parseDynamicInitInfo(f)
if err != nil {
return nil, err
}
goRuntime, tlsSlot, tlsOffset, err := prepareLinuxGoRuntimeTLS(f)
if err != nil {
return nil, err
}
tlsSlotReserved := goRuntime
defer func() {
if tlsSlotReserved {
releaseLinuxGoTLSSlot(tlsSlot)
}
}()
mapped, err := mapELFImage(data, f)
if err != nil {
return nil, err
}
mapped.tlsOffset = tlsOffset
mapped.hasTLS = goRuntime
cleanup := true
defer func() {
if cleanup && len(mapped.mapping) != 0 {
@@ -114,19 +140,89 @@ func LoadLibrary(data []byte) (*Module, error) {
if err := applySegmentProtections(mapped); err != nil {
return nil, err
}
if err := runELFInitializers(mapped, f); err != nil {
initializers, err := collectELFInitializers(mapped, f.Class, initInfo)
if err != nil {
return nil, err
}
argc, argv, envp := linuxInitCallArgs(goRuntime)
if goRuntime && argv == 0 {
return nil, errors.New("failed to allocate Go runtime argv/environment vector")
}
// Once a Go constructor starts, its runtime may retain this TLS slot for
// process-lifetime threads even if a later initializer reports an error.
tlsSlotReserved = false
for _, initializer := range initializers {
cCallVoid3(initializer, argc, argv, envp)
}
module := &Module{
mapping: mapped.mapping,
loadBias: mapped.loadBias,
symbols: buildExportedSymbolTable(f, mapped.loadBias),
mapping: mapped.mapping,
loadBias: mapped.loadBias,
symbols: buildExportedSymbolTable(f, mapped.loadBias),
goRuntime: goRuntime,
}
cleanup = false
return module, nil
}
func prepareLinuxGoRuntimeTLS(f *elf.File) (bool, uintptr, int64, error) {
if f == nil || f.Section(".go.buildinfo") == nil {
return false, 0, 0, nil
}
var tls *elf.Prog
for _, prog := range f.Progs {
if prog.Type != elf.PT_TLS {
continue
}
if tls != nil {
return false, 0, 0, errors.New("Go ELF image has multiple PT_TLS segments")
}
tls = prog
}
if tls == nil || tls.Memsz == 0 {
return false, 0, 0, errors.New("Go ELF image is missing its runtime PT_TLS segment")
}
if tls.Filesz != 0 {
return false, 0, 0, fmt.Errorf("Go ELF PT_TLS has unsupported initialized data size %#x", tls.Filesz)
}
wordSize := uint64(8)
if f.Class == elf.ELFCLASS32 {
wordSize = 4
}
if tls.Memsz > 2*wordSize {
return false, 0, 0, fmt.Errorf("Go ELF PT_TLS size %#x exceeds the reserved size %#x", tls.Memsz, 2*wordSize)
}
linuxGoTLSSlots.Lock()
defer linuxGoTLSSlots.Unlock()
slot := uintptr(linuxGoTLSSlotCount)
for i := range linuxGoTLSSlots.used {
if !linuxGoTLSSlots.used[i] {
slot = uintptr(i)
break
}
}
if slot == linuxGoTLSSlotCount {
return false, 0, 0, fmt.Errorf("Go ELF TLS slot limit reached (%d)", linuxGoTLSSlotCount)
}
offset, err := linuxGoTLSSlotOffset(slot)
if err != nil {
return false, 0, 0, err
}
linuxGoTLSSlots.used[slot] = true
return true, slot, offset, nil
}
func releaseLinuxGoTLSSlot(slot uintptr) {
if slot >= linuxGoTLSSlotCount {
return
}
linuxGoTLSSlots.Lock()
linuxGoTLSSlots.used[slot] = false
linuxGoTLSSlots.Unlock()
}
func (module *Module) Free() {
module.mu.Lock()
defer module.mu.Unlock()
@@ -135,6 +231,15 @@ func (module *Module) Free() {
return
}
module.closed = true
if module.goRuntime {
// A Go c-shared runtime owns process-lifetime threads and cannot be
// unloaded safely. Close the Reflektor handle while leaving its mapping
// and reserved TLS slot pinned until process exit.
module.mapping = nil
module.symbols = nil
module.loadBias = 0
return
}
if len(module.mapping) != 0 {
_ = unix.Munmap(module.mapping)
@@ -171,7 +276,14 @@ func (module *Module) CallExport(name string) error {
return fmt.Errorf("resolve export %q: %w", name, err)
}
_ = cCall0(addr)
if module.goRuntime {
if err := cCallVoid0OnThread(addr); err != nil {
return fmt.Errorf("call Go export %q on isolated thread: %w", name, err)
}
return nil
}
cCallVoid0(addr)
return nil
}
@@ -437,17 +549,17 @@ func applyOneRelocation(machine elf.Machine, class elf.Class, mapped mappedELF,
switch machine {
case elf.EM_X86_64:
return applyX8664Reloc(relocType, place, mapped.loadBias, symValue, addend)
return applyX8664Reloc(relocType, place, mapped.loadBias, symValue, addend, mapped.tlsOffset, mapped.hasTLS)
case elf.EM_386:
return apply386Reloc(relocType, place, mapped.loadBias, symValue, addend)
return apply386Reloc(relocType, place, mapped.loadBias, symValue, addend, mapped.tlsOffset, mapped.hasTLS)
case elf.EM_AARCH64:
return applyAArch64Reloc(relocType, place, mapped.loadBias, symValue, addend)
return applyAArch64Reloc(relocType, place, mapped.loadBias, symValue, addend, mapped.tlsOffset, mapped.hasTLS)
default:
return fmt.Errorf("unsupported machine for relocation: %s", machine)
}
}
func applyX8664Reloc(relocType uint32, place uintptr, loadBias uintptr, symValue uintptr, addend int64) error {
func applyX8664Reloc(relocType uint32, place uintptr, loadBias uintptr, symValue uintptr, addend int64, tlsOffset int64, hasTLS bool) error {
switch elf.R_X86_64(relocType) {
case elf.R_X86_64_NONE:
return nil
@@ -455,10 +567,10 @@ func applyX8664Reloc(relocType uint32, place uintptr, loadBias uintptr, symValue
writeU64(place, uint64(int64(loadBias)+addend))
return nil
case elf.R_X86_64_TPOFF64:
// Linux TLS local-exec relocation. The pure-Go loader does not provision
// module TLS blocks, so we apply S+A and rely on payload/runtime behavior
// that does not require a non-zero static TLS offset.
writeU64(place, uint64(int64(symValue)+addend))
if !hasTLS {
return errors.New("x86_64 static TLS relocation has no reserved host TLS slot")
}
writeU64(place, uint64(tlsOffset+addend))
return nil
case elf.R_X86_64_JMP_SLOT, elf.R_X86_64_GLOB_DAT, elf.R_X86_64_64:
writeU64(place, uint64(int64(symValue)+addend))
@@ -489,7 +601,7 @@ func applyX8664Reloc(relocType uint32, place uintptr, loadBias uintptr, symValue
}
}
func apply386Reloc(relocType uint32, place uintptr, loadBias uintptr, symValue uintptr, addend int64) error {
func apply386Reloc(relocType uint32, place uintptr, loadBias uintptr, symValue uintptr, addend int64, tlsOffset int64, hasTLS bool) error {
switch elf.R_386(relocType) {
case elf.R_386_NONE:
return nil
@@ -497,8 +609,10 @@ func apply386Reloc(relocType uint32, place uintptr, loadBias uintptr, symValue u
writeU32(place, uint32(int64(loadBias)+addend))
return nil
case elf.R_386_TLS_TPOFF:
// Linux TLS local-exec relocation; see R_X86_64_TPOFF64 note above.
writeU32(place, uint32(int64(symValue)+addend))
if !hasTLS {
return errors.New("386 static TLS relocation has no reserved host TLS slot")
}
writeU32(place, uint32(tlsOffset+addend))
return nil
case elf.R_386_JMP_SLOT, elf.R_386_GLOB_DAT:
writeU32(place, uint32(symValue))
@@ -518,7 +632,7 @@ func apply386Reloc(relocType uint32, place uintptr, loadBias uintptr, symValue u
}
}
func applyAArch64Reloc(relocType uint32, place uintptr, loadBias uintptr, symValue uintptr, addend int64) error {
func applyAArch64Reloc(relocType uint32, place uintptr, loadBias uintptr, symValue uintptr, addend int64, tlsOffset int64, hasTLS bool) error {
switch elf.R_AARCH64(relocType) {
case elf.R_AARCH64_NONE:
return nil
@@ -526,8 +640,10 @@ func applyAArch64Reloc(relocType uint32, place uintptr, loadBias uintptr, symVal
writeU64(place, uint64(int64(loadBias)+addend))
return nil
case elf.R_AARCH64_TLS_TPREL64:
// Linux TLS local-exec relocation; see R_X86_64_TPOFF64 note above.
writeU64(place, uint64(int64(symValue)+addend))
if !hasTLS {
return errors.New("arm64 static TLS relocation has no reserved host TLS slot")
}
writeU64(place, uint64(tlsOffset+addend))
return nil
case elf.R_AARCH64_JUMP_SLOT, elf.R_AARCH64_GLOB_DAT, elf.R_AARCH64_ABS64:
writeU64(place, uint64(int64(symValue)+addend))
@@ -611,24 +727,22 @@ func applySegmentProtections(mapped mappedELF) error {
return nil
}
func runELFInitializers(mapped mappedELF, f *elf.File) error {
info, err := parseDynamicInitInfo(f)
func collectELFInitializers(mapped mappedELF, class elf.Class, info dynamicInitInfo) ([]uintptr, error) {
initializers := make([]uintptr, 0)
var err error
initializers, err = appendDynamicInitArray(initializers, mapped, class, info.preinitArr, info.preinitSz, "DT_PREINIT_ARRAY")
if err != nil {
return err
return nil, err
}
argc, argv, envp := linuxInitCallArgs()
skip := collectInitSkipAddrs(f, mapped.loadBias)
if err := callDynamicInitArray(mapped, f.Class, info.preinitArr, info.preinitSz, "DT_PREINIT_ARRAY", argc, argv, envp, skip); err != nil {
return err
initializers, err = appendDynamicInitFn(initializers, mapped, uintptr(info.init), "DT_INIT")
if err != nil {
return nil, err
}
if err := callDynamicInitFn(mapped, uintptr(info.init), "DT_INIT", argc, argv, envp, skip); err != nil {
return err
initializers, err = appendDynamicInitArray(initializers, mapped, class, info.initArray, info.initArraySz, "DT_INIT_ARRAY")
if err != nil {
return nil, err
}
if err := callDynamicInitArray(mapped, f.Class, info.initArray, info.initArraySz, "DT_INIT_ARRAY", argc, argv, envp, skip); err != nil {
return err
}
return nil
return initializers, nil
}
func parseDynamicInitInfo(f *elf.File) (dynamicInitInfo, error) {
@@ -703,24 +817,20 @@ func parseDynamicInitInfo(f *elf.File) (dynamicInitInfo, error) {
return info, nil
}
func callDynamicInitFn(mapped mappedELF, fn uintptr, source string, argc uintptr, argv uintptr, envp uintptr, skip map[uintptr]struct{}) error {
func appendDynamicInitFn(initializers []uintptr, mapped mappedELF, fn uintptr, source string) ([]uintptr, error) {
if fn == 0 {
return nil
return initializers, nil
}
resolved, ok := normalizeInitFnAddress(mapped, fn)
if !ok {
return fmt.Errorf("%s points outside mapped image: %#x", source, fn)
return nil, fmt.Errorf("%s points outside mapped image: %#x", source, fn)
}
if _, blocked := skip[resolved]; blocked {
return nil
}
_ = cCall3(resolved, argc, argv, envp)
return nil
return append(initializers, resolved), nil
}
func callDynamicInitArray(mapped mappedELF, class elf.Class, arrayVAddr uint64, arraySz uint64, source string, argc uintptr, argv uintptr, envp uintptr, skip map[uintptr]struct{}) error {
func appendDynamicInitArray(initializers []uintptr, mapped mappedELF, class elf.Class, arrayVAddr uint64, arraySz uint64, source string) ([]uintptr, error) {
if arrayVAddr == 0 || arraySz == 0 {
return nil
return initializers, nil
}
entrySize := 8
@@ -728,16 +838,16 @@ func callDynamicInitArray(mapped mappedELF, class elf.Class, arrayVAddr uint64,
entrySize = 4
}
if arraySz%uint64(entrySize) != 0 {
return fmt.Errorf("%s has malformed size %#x for entry size %d", source, arraySz, entrySize)
return nil, fmt.Errorf("%s has malformed size %#x for entry size %d", source, arraySz, entrySize)
}
arrayLen, err := u64ToInt(arraySz)
if err != nil {
return fmt.Errorf("%s size does not fit in int: %w", source, err)
return nil, fmt.Errorf("%s size does not fit in int: %w", source, err)
}
arrayAddr := mapped.loadBias + uintptr(arrayVAddr)
if !mappedAddressInRange(mapped.mapping, arrayAddr, arrayLen) {
return fmt.Errorf("%s range %#x..%#x is outside mapped image", source, arrayVAddr, arrayVAddr+arraySz)
return nil, fmt.Errorf("%s range %#x..%#x is outside mapped image", source, arrayVAddr, arrayVAddr+arraySz)
}
count := int(arraySz / uint64(entrySize))
@@ -754,57 +864,11 @@ func callDynamicInitArray(mapped mappedELF, class elf.Class, arrayVAddr uint64,
}
resolved, ok := normalizeInitFnAddress(mapped, fn)
if !ok {
return fmt.Errorf("%s[%d] points outside mapped image: %#x", source, i, fn)
return nil, fmt.Errorf("%s[%d] points outside mapped image: %#x", source, i, fn)
}
if _, blocked := skip[resolved]; blocked {
continue
}
_ = cCall3(resolved, argc, argv, envp)
initializers = append(initializers, resolved)
}
return nil
}
func collectInitSkipAddrs(f *elf.File, loadBias uintptr) map[uintptr]struct{} {
if f == nil || f.Section(".go.buildinfo") == nil {
return nil
}
targets := map[string]struct{}{
"_rt0_386_linux_lib": {},
"_rt0_amd64_linux_lib": {},
"_rt0_arm64_linux_lib": {},
"_rt0_386_lib": {},
"_rt0_amd64_lib": {},
"_rt0_arm64_lib": {},
}
out := make(map[uintptr]struct{})
add := func(symbols []elf.Symbol) {
for _, sym := range symbols {
if sym.Value == 0 || sym.Section == elf.SHN_UNDEF {
continue
}
name := sym.Name
if at := strings.IndexByte(name, '@'); at > 0 {
name = name[:at]
}
if _, ok := targets[name]; !ok {
continue
}
out[loadBias+uintptr(sym.Value)] = struct{}{}
}
}
if dynSyms, err := f.DynamicSymbols(); err == nil {
add(dynSyms)
}
if syms, err := f.Symbols(); err == nil {
add(syms)
}
if len(out) == 0 {
return nil
}
return out
return initializers, nil
}
func normalizeInitFnAddress(mapped mappedELF, fn uintptr) (uintptr, bool) {
+20
View File
@@ -2,6 +2,8 @@
package memmod
import "errors"
//go:noescape
func cCall0(fn uintptr) uintptr
@@ -13,3 +15,21 @@ func cCall2(fn, a0, a1 uintptr) uintptr
//go:noescape
func cCall3(fn, a0, a1, a2 uintptr) uintptr
func cCallVoid0(fn uintptr) {
_ = cCall0(fn)
}
func cCallVoid3(fn, a0, a1, a2 uintptr) {
_ = cCall3(fn, a0, a1, a2)
}
func cCallVoid0OnThread(fn uintptr) error {
_ = fn
return errors.New("calling a Go c-shared export requires cgo")
}
func linuxGoTLSSlotOffset(slot uintptr) (int64, error) {
_ = slot
return 0, errors.New("loading a Go c-shared image requires cgo TLS support")
}
+109 -20
View File
@@ -3,13 +3,17 @@
package memmod
/*
#include <pthread.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
typedef uintptr_t (*reflektor_fn0)(void);
typedef uintptr_t (*reflektor_fn1)(uintptr_t);
typedef uintptr_t (*reflektor_fn2)(uintptr_t, uintptr_t);
typedef uintptr_t (*reflektor_fn3)(uintptr_t, uintptr_t, uintptr_t);
typedef void (*reflektor_void_fn0)(void);
typedef void (*reflektor_init_fn)(int, char **, char **);
static uintptr_t reflektor_call0(uintptr_t fn) {
return ((reflektor_fn0)fn)();
@@ -27,31 +31,89 @@ static uintptr_t reflektor_call3(uintptr_t fn, uintptr_t a0, uintptr_t a1, uintp
return ((reflektor_fn3)fn)(a0, a1, a2);
}
static uintptr_t reflektor_init_argc = 0;
static uintptr_t reflektor_init_argv = 0;
static uintptr_t reflektor_init_envp = 0;
static void reflektor_call_void0(uintptr_t fn) {
((reflektor_void_fn0)fn)();
}
// Build a tiny synthetic argv/envp/auxv vector for runtimes that expect a
// startup-like layout (argc/argv/envp/auxv). The layout is:
// argv[0] = NULL
// envp[0] = NULL
// auxv[0] = AT_NULL
// auxv[1] = 0
static void reflektor_init_call_args(uintptr_t *argc, uintptr_t *argv, uintptr_t *envp) {
if (reflektor_init_argv == 0) {
uintptr_t *vec = (uintptr_t *)calloc(4, sizeof(uintptr_t));
if (vec != NULL) {
reflektor_init_argv = (uintptr_t)vec;
reflektor_init_envp = (uintptr_t)(vec + 1);
static void reflektor_call_void3(uintptr_t fn, uintptr_t a0, uintptr_t a1, uintptr_t a2) {
((reflektor_init_fn)fn)((int)a0, (char **)a1, (char **)a2);
}
typedef struct {
uintptr_t fn;
} reflektor_thread_call;
static void *reflektor_thread_entry(void *opaque) {
reflektor_thread_call *call = (reflektor_thread_call *)opaque;
((reflektor_void_fn0)call->fn)();
return NULL;
}
static int reflektor_call_on_thread(reflektor_thread_call *call) {
pthread_t thread;
int err = pthread_create(&thread, NULL, reflektor_thread_entry, call);
if (err != 0) {
return err;
}
return pthread_join(thread, NULL);
}
static int reflektor_call_void0_thread(uintptr_t fn) {
reflektor_thread_call call;
memset(&call, 0, sizeof(call));
call.fn = fn;
return reflektor_call_on_thread(&call);
}
// Go's ELF runtime stores thread state in up to two words of static TLS.
// Reserve independent slots so manually mapped Go runtimes do not alias the
// host Go runtime or each other. Static TLS is instantiated, zeroed, and kept
// at the same thread-pointer-relative offset for every pthread.
#define REFLEKTOR_GO_TLS_SLOTS 64
static __thread uintptr_t reflektor_go_tls_slots[REFLEKTOR_GO_TLS_SLOTS][2]
__attribute__((tls_model("initial-exec")));
static intptr_t reflektor_go_tls_offset(uintptr_t slot) {
if (slot >= REFLEKTOR_GO_TLS_SLOTS) {
return INTPTR_MIN;
}
return (intptr_t)(uintptr_t)&reflektor_go_tls_slots[slot][0] -
(intptr_t)(uintptr_t)__builtin_thread_pointer();
}
extern char **environ;
static uintptr_t reflektor_empty_init_args[4];
// Build a stable argc/argv/envp/auxv vector for runtimes that expect a
// startup-like layout. argc is zero, argv[0] is NULL, the current environment
// follows it, and a terminating AT_NULL pair follows the environment.
static void reflektor_init_call_args(int include_environment, uintptr_t *argc, uintptr_t *argv, uintptr_t *envp) {
if (!include_environment) {
*argc = 0;
*argv = (uintptr_t)reflektor_empty_init_args;
*envp = (uintptr_t)(reflektor_empty_init_args + 1);
return;
}
size_t envc = 0;
while (environ != NULL && environ[envc] != NULL) {
envc++;
}
uintptr_t *vec = (uintptr_t *)calloc(envc + 4, sizeof(uintptr_t));
if (vec != NULL) {
for (size_t i = 0; i < envc; i++) {
vec[i + 1] = (uintptr_t)environ[i];
}
}
*argc = reflektor_init_argc;
*argv = reflektor_init_argv;
*envp = reflektor_init_envp;
*argc = 0;
*argv = (uintptr_t)vec;
*envp = vec == NULL ? 0 : (uintptr_t)(vec + 1);
}
*/
import "C"
import "fmt"
func cCall0(fn uintptr) uintptr {
return uintptr(C.reflektor_call0(C.uintptr_t(fn)))
}
@@ -68,10 +130,37 @@ func cCall3(fn, a0, a1, a2 uintptr) uintptr {
return uintptr(C.reflektor_call3(C.uintptr_t(fn), C.uintptr_t(a0), C.uintptr_t(a1), C.uintptr_t(a2)))
}
func linuxInitCallArgs() (uintptr, uintptr, uintptr) {
func cCallVoid0(fn uintptr) {
C.reflektor_call_void0(C.uintptr_t(fn))
}
func cCallVoid3(fn, a0, a1, a2 uintptr) {
C.reflektor_call_void3(C.uintptr_t(fn), C.uintptr_t(a0), C.uintptr_t(a1), C.uintptr_t(a2))
}
func cCallVoid0OnThread(fn uintptr) error {
if errno := int(C.reflektor_call_void0_thread(C.uintptr_t(fn))); errno != 0 {
return fmt.Errorf("pthread call failed with error %d", errno)
}
return nil
}
func linuxGoTLSSlotOffset(slot uintptr) (int64, error) {
if slot >= 64 {
return 0, fmt.Errorf("Go TLS slot %d is out of range", slot)
}
offset := C.reflektor_go_tls_offset(C.uintptr_t(slot))
return int64(offset), nil
}
func linuxInitCallArgs(includeEnvironment bool) (uintptr, uintptr, uintptr) {
var argc C.uintptr_t
var argv C.uintptr_t
var envp C.uintptr_t
C.reflektor_init_call_args(&argc, &argv, &envp)
include := C.int(0)
if includeEnvironment {
include = 1
}
C.reflektor_init_call_args(include, &argc, &argv, &envp)
return uintptr(argc), uintptr(argv), uintptr(envp)
}
@@ -2,6 +2,7 @@
package memmod
func linuxInitCallArgs() (uintptr, uintptr, uintptr) {
func linuxInitCallArgs(includeEnvironment bool) (uintptr, uintptr, uintptr) {
_ = includeEnvironment
return 0, 0, 0
}
+59 -18
View File
@@ -6,6 +6,8 @@
package memmod
import (
"bytes"
"debug/buildinfo"
"errors"
"fmt"
"strings"
@@ -28,15 +30,23 @@ func (head *addressList) free() {
}
type Module struct {
headers *IMAGE_NT_HEADERS
codeBase uintptr
modules []windows.Handle
initialized bool
isDLL bool
isRelocated bool
nameExports map[string]uint16
entry uintptr
blockedMemory *addressList
headers *IMAGE_NT_HEADERS
codeBase uintptr
modules []windows.Handle
initialized bool
isDLL bool
isRelocated bool
nameExports map[string]uint16
entry uintptr
blockedMemory *addressList
exceptionTable uintptr
goRuntime bool
runtimeStarted bool
}
func imageHasGoBuildInfo(data []byte) bool {
_, err := buildinfo.Read(bytes.NewReader(data))
return err == nil
}
func (module *Module) headerDirectory(idx int) *IMAGE_DATA_DIRECTORY {
@@ -161,14 +171,22 @@ func (module *Module) finalizeSection(sectionData *sectionFinalizeData) error {
return nil
}
var rtlAddFunctionTable = windows.NewLazySystemDLL("ntdll.dll").NewProc("RtlAddFunctionTable")
func (module *Module) registerExceptionHandlers() {
func (module *Module) registerExceptionHandlers() error {
directory := module.headerDirectory(IMAGE_DIRECTORY_ENTRY_EXCEPTION)
if directory.Size == 0 || directory.VirtualAddress == 0 {
return
entrySize := runtimeFunctionEntrySize()
if directory.Size == 0 || directory.VirtualAddress == 0 || entrySize == 0 {
return nil
}
rtlAddFunctionTable.Call(module.codeBase+uintptr(directory.VirtualAddress), uintptr(directory.Size)/unsafe.Sizeof(IMAGE_RUNTIME_FUNCTION_ENTRY{}), module.codeBase)
if uintptr(directory.Size)%entrySize != 0 {
return fmt.Errorf("malformed exception table size %#x for entry size %d", directory.Size, entrySize)
}
module.exceptionTable = module.codeBase + uintptr(directory.VirtualAddress)
count := uint32(uintptr(directory.Size) / entrySize)
if !windows.RtlAddFunctionTable((*windows.RUNTIME_FUNCTION)(unsafe.Pointer(module.exceptionTable)), count, module.codeBase) {
module.exceptionTable = 0
return errors.New("RtlAddFunctionTable returned false")
}
return nil
}
func (module *Module) finalizeSections() error {
@@ -498,7 +516,10 @@ func LoadLibrary(data []byte) (module *Module, err error) {
return nil, errors.New("Section is not page-aligned")
}
module = &Module{isDLL: (oldHeader.FileHeader.Characteristics & IMAGE_FILE_DLL) != 0}
module = &Module{
isDLL: (oldHeader.FileHeader.Characteristics & IMAGE_FILE_DLL) != 0,
goRuntime: imageHasGoBuildInfo(data),
}
defer func() {
if err != nil {
module.Free()
@@ -583,7 +604,10 @@ func LoadLibrary(data []byte) (module *Module, err error) {
}
// Register exception tables, if they exist.
module.registerExceptionHandlers()
err = module.registerExceptionHandlers()
if err != nil {
return
}
// Register function PCs.
loadedAddressRangesMu.Lock()
@@ -598,6 +622,12 @@ func LoadLibrary(data []byte) (module *Module, err error) {
}
// TLS callbacks are executed BEFORE the main loading.
if module.goRuntime {
// Go DLL initialization starts process-lifetime runtime state. From this
// point onward, an error or Close must leave the image and its imports
// pinned rather than detach and unmap live runtime-managed threads.
module.runtimeStarted = true
}
module.executeTLS()
// Get entry point of loaded module.
@@ -619,8 +649,15 @@ func LoadLibrary(data []byte) (module *Module, err error) {
return
}
// Free releases module resources and unloads it.
// Free releases module resources. Started Go runtime images remain pinned.
func (module *Module) Free() {
if module.goRuntime && module.runtimeStarted {
if module.blockedMemory != nil {
module.blockedMemory.free()
module.blockedMemory = nil
}
return
}
if module.initialized {
// Notify library about detaching from process.
syscall.Syscall(module.entry, 3, module.codeBase, uintptr(DLL_PROCESS_DETACH), 0)
@@ -633,6 +670,10 @@ func (module *Module) Free() {
}
module.modules = nil
}
if module.exceptionTable != 0 {
windows.RtlDeleteFunctionTable((*windows.RUNTIME_FUNCTION)(unsafe.Pointer(module.exceptionTable)))
module.exceptionTable = 0
}
if module.codeBase != 0 {
windows.VirtualFree(module.codeBase, 0, windows.MEM_RELEASE)
module.codeBase = 0
@@ -6,3 +6,5 @@
package memmod
const imageFileProcess = IMAGE_FILE_MACHINE_I386
func runtimeFunctionEntrySize() uintptr { return 0 }
@@ -6,3 +6,5 @@
package memmod
const imageFileProcess = IMAGE_FILE_MACHINE_AMD64
func runtimeFunctionEntrySize() uintptr { return 12 }
@@ -6,3 +6,5 @@
package memmod
const imageFileProcess = IMAGE_FILE_MACHINE_ARMNT
func runtimeFunctionEntrySize() uintptr { return 8 }
@@ -6,3 +6,5 @@
package memmod
const imageFileProcess = IMAGE_FILE_MACHINE_ARM64
func runtimeFunctionEntrySize() uintptr { return 8 }
+1 -1
View File
@@ -1328,7 +1328,7 @@ github.com/sliverarmory/beignet/internal/stager
## explicit; go 1.25.6
github.com/sliverarmory/malasada
github.com/sliverarmory/malasada/internal/aplib
# github.com/sliverarmory/reflektor v0.0.2
# github.com/sliverarmory/reflektor v0.0.3
## explicit; go 1.26.6
github.com/sliverarmory/reflektor/memmod
# github.com/sliverarmory/wasm-donut v0.0.3