mirror of
https://github.com/BishopFox/sliver.git
synced 2026-11-08 01:38:17 -10:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d6df68972 | ||
|
|
7f863d3172 | ||
|
|
6ecb379bce |
No files matched your search
@@ -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 }}
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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))
|
||||
}
|
||||
|
||||
Generated
Vendored
+33
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
Generated
Vendored
+2
-1
@@ -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
@@ -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
|
||||
|
||||
+2
@@ -6,3 +6,5 @@
|
||||
package memmod
|
||||
|
||||
const imageFileProcess = IMAGE_FILE_MACHINE_I386
|
||||
|
||||
func runtimeFunctionEntrySize() uintptr { return 0 }
|
||||
+2
@@ -6,3 +6,5 @@
|
||||
package memmod
|
||||
|
||||
const imageFileProcess = IMAGE_FILE_MACHINE_AMD64
|
||||
|
||||
func runtimeFunctionEntrySize() uintptr { return 12 }
|
||||
+2
@@ -6,3 +6,5 @@
|
||||
package memmod
|
||||
|
||||
const imageFileProcess = IMAGE_FILE_MACHINE_ARMNT
|
||||
|
||||
func runtimeFunctionEntrySize() uintptr { return 8 }
|
||||
+2
@@ -6,3 +6,5 @@
|
||||
package memmod
|
||||
|
||||
const imageFileProcess = IMAGE_FILE_MACHINE_ARM64
|
||||
|
||||
func runtimeFunctionEntrySize() uintptr { return 8 }
|
||||
Vendored
+1
-1
@@ -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
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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))
|
||||
}
|
||||
|
||||
+33
@@ -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
@@ -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
@@ -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
@@ -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
-1
@@ -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
@@ -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
|
||||
|
||||
+2
@@ -6,3 +6,5 @@
|
||||
package memmod
|
||||
|
||||
const imageFileProcess = IMAGE_FILE_MACHINE_I386
|
||||
|
||||
func runtimeFunctionEntrySize() uintptr { return 0 }
|
||||
+2
@@ -6,3 +6,5 @@
|
||||
package memmod
|
||||
|
||||
const imageFileProcess = IMAGE_FILE_MACHINE_AMD64
|
||||
|
||||
func runtimeFunctionEntrySize() uintptr { return 12 }
|
||||
+2
@@ -6,3 +6,5 @@
|
||||
package memmod
|
||||
|
||||
const imageFileProcess = IMAGE_FILE_MACHINE_ARMNT
|
||||
|
||||
func runtimeFunctionEntrySize() uintptr { return 8 }
|
||||
+2
@@ -6,3 +6,5 @@
|
||||
package memmod
|
||||
|
||||
const imageFileProcess = IMAGE_FILE_MACHINE_ARM64
|
||||
|
||||
func runtimeFunctionEntrySize() uintptr { return 8 }
|
||||
Vendored
+1
-1
@@ -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
|
||||
|
||||
Reference in new issue
Block a user