mirror of
https://github.com/sliverarmory/reflektor
synced 2026-08-25 09:14:48 +00:00
555 lines
16 KiB
Go
555 lines
16 KiB
Go
package bofloader
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"runtime"
|
|
"sort"
|
|
"sync"
|
|
"unsafe"
|
|
)
|
|
|
|
const (
|
|
maxObjectSize = 64 << 20
|
|
maxImageSize = 256 << 20
|
|
|
|
// Parser metadata is attacker-controlled and can reuse the same on-disk
|
|
// ranges from many sections. Keep its in-memory expansion bounded
|
|
// independently of the raw object and mapped-image limits.
|
|
maxObjectSections = maxObjectSize / (4 << 10)
|
|
maxObjectSymbols = maxObjectSize / 64
|
|
maxObjectRelocations = maxObjectSize / 64
|
|
maxObjectNameSize = 64 << 10
|
|
maxObjectNameBytes = maxObjectSize
|
|
|
|
sectionUndefined = -1
|
|
sectionAbsolute = -2
|
|
sectionCommon = -3
|
|
sectionDiscarded = -4
|
|
)
|
|
|
|
var ErrClosed = errors.New("bofloader: object is closed")
|
|
|
|
// Output is one Beacon output record emitted by a BOF.
|
|
type Output struct {
|
|
Type int
|
|
Data []byte
|
|
}
|
|
|
|
type Loader struct {
|
|
mu sync.RWMutex
|
|
region *memoryRegion
|
|
unwind *unwindRegistration
|
|
entry uintptr
|
|
closing bool
|
|
closed bool
|
|
}
|
|
|
|
type protection uint8
|
|
|
|
const (
|
|
protRead protection = 1 << iota
|
|
protWrite
|
|
protExec
|
|
)
|
|
|
|
type objectSection struct {
|
|
name string
|
|
data []byte
|
|
size uint64
|
|
align uint64
|
|
protection protection
|
|
mapped bool
|
|
offset uint64
|
|
address uintptr
|
|
}
|
|
|
|
type objectSymbol struct {
|
|
index uint32
|
|
name string
|
|
section int
|
|
value uint64
|
|
size uint64
|
|
localEntry uint64
|
|
weak bool
|
|
}
|
|
|
|
type objectRelocation struct {
|
|
section int
|
|
offset uint64
|
|
typeID uint32
|
|
symbol uint32
|
|
addend int64
|
|
hasAdd bool
|
|
width uint8
|
|
}
|
|
|
|
type objectFile struct {
|
|
format string
|
|
arch string
|
|
sections []objectSection
|
|
symbols map[uint32]objectSymbol
|
|
relocations []objectRelocation
|
|
riscvPairs map[riscvRelocationSite]riscvRelocationPair
|
|
ppc64TOC uintptr
|
|
imageBase uintptr
|
|
}
|
|
|
|
type externalSymbol struct {
|
|
name string
|
|
target uintptr
|
|
got uintptr
|
|
thunk uintptr
|
|
gotOffset uint64
|
|
thunkOff uint64
|
|
}
|
|
|
|
func Load(data []byte) (*Loader, error) {
|
|
return LoadWithOptions(data, LoadOptions{})
|
|
}
|
|
|
|
func LoadWithOptions(data []byte, options LoadOptions) (*Loader, error) {
|
|
if len(data) == 0 {
|
|
return nil, errors.New("bofloader: empty object image")
|
|
}
|
|
if len(data) > maxObjectSize {
|
|
return nil, fmt.Errorf("bofloader: object image is %d bytes; maximum is %d", len(data), maxObjectSize)
|
|
}
|
|
|
|
object, err := parseObject(data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := validateHost(object); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
entryNames, err := selectedEntryNames(options)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
entrySymbol := objectSymbol{}
|
|
for _, name := range entryNames {
|
|
symbol, ok, findErr := findDefinedSymbolDefinition(object, name)
|
|
if findErr != nil {
|
|
return nil, findErr
|
|
}
|
|
if ok {
|
|
entrySymbol = symbol
|
|
break
|
|
}
|
|
}
|
|
if entrySymbol.name == "" {
|
|
if options.EntryPoint != "" {
|
|
return nil, fmt.Errorf("bofloader: entry symbol %q not found", options.EntryPoint)
|
|
}
|
|
return nil, errors.New("bofloader: entry symbol not found (expected go or coffee)")
|
|
}
|
|
referenced := referencedLinkageSymbols(object)
|
|
imports := objectImports(object, referenced)
|
|
if options.ValidateImports != nil {
|
|
if err := options.ValidateImports(append([]Import(nil), imports...)); err != nil {
|
|
return nil, fmt.Errorf("bofloader: validate imports: %w", err)
|
|
}
|
|
}
|
|
pageSize := uint64(systemPageSize())
|
|
thunkStride := importThunkStride(object.arch)
|
|
gotStride := uint64(pointerSize())
|
|
thunkSize := alignUp(uint64(len(referenced))*thunkStride, pageSize)
|
|
gotSize := alignUp(uint64(len(referenced))*gotStride, pageSize)
|
|
if thunkSize == 0 {
|
|
thunkSize = pageSize
|
|
}
|
|
if gotSize == 0 {
|
|
gotSize = pageSize
|
|
}
|
|
|
|
offset := thunkSize + gotSize
|
|
for i := range object.sections {
|
|
section := &object.sections[i]
|
|
if !section.mapped || section.size == 0 {
|
|
continue
|
|
}
|
|
if section.size > maxImageSize {
|
|
return nil, fmt.Errorf("bofloader: section %q size %d exceeds %d", section.name, section.size, maxImageSize)
|
|
}
|
|
alignment, err := mappedSectionAlignment(*section, pageSize)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
offset = alignUp(offset, alignment)
|
|
if offset > maxImageSize || alignUp(section.size, pageSize) > maxImageSize-offset {
|
|
return nil, fmt.Errorf("bofloader: mapped object exceeds %d bytes", maxImageSize)
|
|
}
|
|
section.offset = offset
|
|
offset += alignUp(section.size, pageSize)
|
|
}
|
|
totalSize := alignUp(offset, pageSize)
|
|
if totalSize == 0 || totalSize > maxImageSize {
|
|
return nil, fmt.Errorf("bofloader: invalid mapped object size %d", totalSize)
|
|
}
|
|
|
|
region, err := allocateMemory(int(totalSize))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("bofloader: allocate mapped object: %w", err)
|
|
}
|
|
cleanup := true
|
|
defer func() {
|
|
if cleanup {
|
|
_ = region.close()
|
|
}
|
|
}()
|
|
|
|
base := region.base()
|
|
object.imageBase = base
|
|
if object.arch == "ppc64le" {
|
|
gotBase, ok := checkedAddUint64(uint64(base), thunkSize)
|
|
if !ok || gotBase > ^uint64(0)-ppc64TOCBias {
|
|
return nil, errors.New("bofloader: PPC64 ELFv2 TOC address overflows")
|
|
}
|
|
object.ppc64TOC = uintptr(gotBase + ppc64TOCBias)
|
|
if uint64(object.ppc64TOC) != gotBase+ppc64TOCBias {
|
|
return nil, errors.New("bofloader: PPC64 ELFv2 TOC address exceeds pointer size")
|
|
}
|
|
}
|
|
for i := range object.sections {
|
|
section := &object.sections[i]
|
|
if !section.mapped || section.size == 0 {
|
|
continue
|
|
}
|
|
section.address = base + uintptr(section.offset)
|
|
if uint64(len(section.data)) > section.size {
|
|
return nil, fmt.Errorf("bofloader: section %q data exceeds mapped size", section.name)
|
|
}
|
|
copy(region.data[section.offset:section.offset+uint64(len(section.data))], section.data)
|
|
}
|
|
entry, err := definedSymbolAddress(object, entrySymbol)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
externals, err := resolveExternalSymbols(object, referenced, region, thunkSize, gotSize, imports, options)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := applyRelocations(object, region, externals); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := region.protect(0, int(thunkSize), protRead|protExec); err != nil {
|
|
return nil, fmt.Errorf("bofloader: protect import thunks: %w", err)
|
|
}
|
|
if err := region.protect(int(thunkSize), int(gotSize), protRead); err != nil {
|
|
return nil, fmt.Errorf("bofloader: protect import table: %w", err)
|
|
}
|
|
for i := range object.sections {
|
|
section := &object.sections[i]
|
|
if !section.mapped || section.size == 0 {
|
|
continue
|
|
}
|
|
length := int(alignUp(section.size, pageSize))
|
|
if err := region.protect(int(section.offset), length, section.protection); err != nil {
|
|
return nil, fmt.Errorf("bofloader: protect section %q: %w", section.name, err)
|
|
}
|
|
if section.protection&protExec != 0 {
|
|
if err := region.flushInstructionCache(int(section.offset), length); err != nil {
|
|
return nil, fmt.Errorf("bofloader: flush section %q instruction cache: %w", section.name, err)
|
|
}
|
|
}
|
|
}
|
|
if err := region.flushInstructionCache(0, int(thunkSize)); err != nil {
|
|
return nil, fmt.Errorf("bofloader: flush import thunk instruction cache: %w", err)
|
|
}
|
|
|
|
unwind, err := registerUnwindInfo(object, region)
|
|
if err != nil {
|
|
if unwind != nil {
|
|
// At least one OS unwind-table entry could not be removed. Keep
|
|
// its backing memory mapped rather than leave Windows with a
|
|
// dangling function-table pointer.
|
|
cleanup = false
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
cleanup = false
|
|
return &Loader{region: region, unwind: unwind, entry: entry}, nil
|
|
}
|
|
|
|
func parseObject(data []byte) (*objectFile, error) {
|
|
if len(data) >= 4 && data[0] == 0x7f && data[1] == 'E' && data[2] == 'L' && data[3] == 'F' {
|
|
return parseELF(data)
|
|
}
|
|
if isMachOMagic(data) {
|
|
return parseMachO(data)
|
|
}
|
|
return parseCOFF(data)
|
|
}
|
|
|
|
func validateHost(object *objectFile) error {
|
|
if object == nil {
|
|
return errors.New("bofloader: nil parsed object")
|
|
}
|
|
if object.arch != runtime.GOARCH {
|
|
return fmt.Errorf("bofloader: object architecture %s does not match host %s", object.arch, runtime.GOARCH)
|
|
}
|
|
if err := validateRuntimeVariant(); err != nil {
|
|
return err
|
|
}
|
|
switch object.format {
|
|
case "coff":
|
|
if runtime.GOOS != "windows" {
|
|
return fmt.Errorf("bofloader: COFF BOFs require a Windows host; use an ELF relocatable BOF on %s", runtime.GOOS)
|
|
}
|
|
case "elf":
|
|
if runtime.GOOS != "linux" && runtime.GOOS != "darwin" {
|
|
return fmt.Errorf("bofloader: ELF BOFs are unsupported on %s", runtime.GOOS)
|
|
}
|
|
case "macho":
|
|
if runtime.GOOS != "darwin" {
|
|
return fmt.Errorf("bofloader: Mach-O BOFs require a Darwin host; use a native relocatable object on %s", runtime.GOOS)
|
|
}
|
|
default:
|
|
return fmt.Errorf("bofloader: unsupported object format %q", object.format)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func referencedLinkageSymbols(object *objectFile) []uint32 {
|
|
seen := make(map[uint32]struct{})
|
|
result := make([]uint32, 0)
|
|
for _, relocation := range object.relocations {
|
|
if relocation.section < 0 || relocation.section >= len(object.sections) || !object.sections[relocation.section].mapped {
|
|
continue
|
|
}
|
|
symbol, ok := object.symbols[relocation.symbol]
|
|
needsDefinedGOT := object.format == "elf" && elfRelocationNeedsGOT(object.arch, relocation.typeID) ||
|
|
object.format == "macho" && machoRelocationNeedsGOT(object.arch, relocation.typeID)
|
|
if !ok || (symbol.section != sectionUndefined && !needsDefinedGOT) {
|
|
continue
|
|
}
|
|
if _, ok := seen[symbol.index]; ok {
|
|
continue
|
|
}
|
|
seen[symbol.index] = struct{}{}
|
|
result = append(result, symbol.index)
|
|
}
|
|
return result
|
|
}
|
|
|
|
func importThunkStride(arch string) uint64 {
|
|
if arch == "ppc64le" {
|
|
// Keep each ELFv2 linkage stub on a 32-byte boundary. The current stub
|
|
// occupies five instructions; the larger stride also matches a common
|
|
// PPC64 instruction-cache line and leaves room for future ABI sequences.
|
|
return 32
|
|
}
|
|
return 16
|
|
}
|
|
|
|
func resolveExternalSymbols(object *objectFile, referenced []uint32, region *memoryRegion, thunkSize, gotSize uint64, imports []Import, options LoadOptions) (map[uint32]externalSymbol, error) {
|
|
resolved := make(map[uint32]externalSymbol, len(referenced))
|
|
importsByName := make(map[string]Import, len(imports))
|
|
for _, imported := range imports {
|
|
importsByName[imported.Name] = imported
|
|
}
|
|
targetsByName := make(map[string]uintptr, len(imports))
|
|
for position, index := range referenced {
|
|
symbol := object.symbols[index]
|
|
target := uintptr(0)
|
|
var err error
|
|
if object.format == "elf" && symbol.name == "_GLOBAL_OFFSET_TABLE_" {
|
|
target = region.base() + uintptr(thunkSize)
|
|
} else if object.format == "elf" && object.arch == "ppc64le" && symbol.name == ".TOC." {
|
|
if object.ppc64TOC == 0 {
|
|
return nil, errors.New("bofloader: unexpected synthetic ELF .TOC. symbol")
|
|
}
|
|
target = object.ppc64TOC
|
|
} else if symbol.section == sectionUndefined {
|
|
if cached, ok := targetsByName[symbol.name]; ok {
|
|
target = cached
|
|
} else {
|
|
if imported, ok := importsByName[symbol.name]; ok {
|
|
symbol.weak = imported.Weak
|
|
}
|
|
target, err = resolveImportedSymbol(symbol, options)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("bofloader: resolve external symbol %q: %w", symbol.name, err)
|
|
}
|
|
targetsByName[symbol.name] = target
|
|
}
|
|
} else {
|
|
linked, linkErr := linkRelocationSymbol(object, objectRelocation{symbol: index}, nil)
|
|
if linkErr != nil {
|
|
return nil, fmt.Errorf("bofloader: resolve defined GOT symbol %q: %w", symbol.name, linkErr)
|
|
}
|
|
target = uintptr(linked.address)
|
|
if uint64(target) != linked.address {
|
|
return nil, fmt.Errorf("bofloader: defined GOT symbol %q address %#x exceeds pointer size", symbol.name, linked.address)
|
|
}
|
|
}
|
|
thunkStride := importThunkStride(object.arch)
|
|
thunkOffset := uint64(position) * thunkStride
|
|
gotOffset := thunkSize + uint64(position)*uint64(pointerSize())
|
|
if thunkOffset+thunkStride > thunkSize || gotOffset+uint64(pointerSize()) > thunkSize+gotSize {
|
|
return nil, errors.New("bofloader: import linkage table overflow")
|
|
}
|
|
ext := externalSymbol{
|
|
name: symbol.name,
|
|
target: target,
|
|
got: region.base() + uintptr(gotOffset),
|
|
thunk: region.base() + uintptr(thunkOffset),
|
|
gotOffset: gotOffset,
|
|
thunkOff: thunkOffset,
|
|
}
|
|
writePointer(region.data[gotOffset:gotOffset+uint64(pointerSize())], target)
|
|
if err := writeThunk(region.data[thunkOffset:thunkOffset+thunkStride], target, ext.thunk, ext.got, object.ppc64TOC); err != nil {
|
|
return nil, fmt.Errorf("bofloader: create thunk for %q: %w", symbol.name, err)
|
|
}
|
|
resolved[index] = ext
|
|
}
|
|
return resolved, nil
|
|
}
|
|
|
|
func findDefinedSymbol(object *objectFile, name string) (uintptr, bool, error) {
|
|
symbol, ok, err := findDefinedSymbolDefinition(object, name)
|
|
if err != nil || !ok {
|
|
return 0, ok, err
|
|
}
|
|
address, err := definedSymbolAddress(object, symbol)
|
|
if err != nil {
|
|
return 0, false, err
|
|
}
|
|
return address, true, nil
|
|
}
|
|
|
|
func findDefinedSymbolDefinition(object *objectFile, name string) (objectSymbol, bool, error) {
|
|
indices := make([]uint32, 0, 1)
|
|
for index, symbol := range object.symbols {
|
|
if symbol.name == name && symbol.section != sectionUndefined {
|
|
indices = append(indices, index)
|
|
}
|
|
}
|
|
if len(indices) == 0 {
|
|
return objectSymbol{}, false, nil
|
|
}
|
|
sort.Slice(indices, func(left, right int) bool { return indices[left] < indices[right] })
|
|
if len(indices) != 1 {
|
|
return objectSymbol{}, false, fmt.Errorf("bofloader: entry symbol %q has multiple definitions at symbol indices %v", name, indices)
|
|
}
|
|
|
|
symbol := object.symbols[indices[0]]
|
|
if symbol.section < 0 || symbol.section >= len(object.sections) {
|
|
return objectSymbol{}, false, fmt.Errorf("bofloader: entry symbol %q references invalid section %d", name, symbol.section)
|
|
}
|
|
section := object.sections[symbol.section]
|
|
if !section.mapped || section.protection&protExec == 0 {
|
|
return objectSymbol{}, false, fmt.Errorf("bofloader: entry symbol %q is not in a mapped executable section", name)
|
|
}
|
|
if symbol.value >= section.size {
|
|
return objectSymbol{}, false, fmt.Errorf("bofloader: entry symbol %q value %#x exceeds section %q size %#x", name, symbol.value, section.name, section.size)
|
|
}
|
|
return symbol, true, nil
|
|
}
|
|
|
|
func definedSymbolAddress(object *objectFile, symbol objectSymbol) (uintptr, error) {
|
|
section := object.sections[symbol.section]
|
|
address, ok := checkedAddUint64(uint64(section.address), symbol.value)
|
|
if !ok || uint64(uintptr(address)) != address || address == 0 {
|
|
return 0, fmt.Errorf("bofloader: entry symbol %q address overflows the host pointer size", symbol.name)
|
|
}
|
|
return uintptr(address), nil
|
|
}
|
|
|
|
func mappedSectionAlignment(section objectSection, pageSize uint64) (uint64, error) {
|
|
alignment := section.align
|
|
if alignment == 0 {
|
|
alignment = 1
|
|
}
|
|
if alignment > maxImageSize || alignment&(alignment-1) != 0 {
|
|
return 0, fmt.Errorf("bofloader: section %q has invalid alignment %d", section.name, alignment)
|
|
}
|
|
if pageSize == 0 || pageSize&(pageSize-1) != 0 {
|
|
return 0, fmt.Errorf("bofloader: invalid system page size %d", pageSize)
|
|
}
|
|
if alignment > pageSize {
|
|
return 0, fmt.Errorf("bofloader: section %q alignment %d exceeds guaranteed allocation alignment %d", section.name, alignment, pageSize)
|
|
}
|
|
return pageSize, nil
|
|
}
|
|
|
|
func (loader *Loader) Execute(args []byte) ([]Output, error) {
|
|
loader.mu.RLock()
|
|
defer loader.mu.RUnlock()
|
|
if loader.closing || loader.closed || loader.region == nil || loader.entry == 0 {
|
|
return nil, ErrClosed
|
|
}
|
|
if len(args) > maxCallbackData {
|
|
return nil, fmt.Errorf("bofloader: argument buffer is %d bytes; maximum is %d", len(args), maxCallbackData)
|
|
}
|
|
|
|
executionLock.Lock()
|
|
defer executionLock.Unlock()
|
|
runtime.LockOSThread()
|
|
defer runtime.UnlockOSThread()
|
|
|
|
context := newExecutionContext()
|
|
activeExecution.Store(context)
|
|
defer activeExecution.Store(nil)
|
|
|
|
var address uintptr
|
|
if len(args) != 0 {
|
|
address = uintptr(unsafe.Pointer(unsafe.SliceData(args)))
|
|
}
|
|
invokeEntry(loader.entry, address, int32(len(args)))
|
|
runtime.KeepAlive(args)
|
|
|
|
outputs, err := context.result()
|
|
if err != nil {
|
|
return outputs, fmt.Errorf("bofloader: execute BOF: %w", err)
|
|
}
|
|
return outputs, nil
|
|
}
|
|
|
|
func (loader *Loader) Close() error {
|
|
loader.mu.Lock()
|
|
defer loader.mu.Unlock()
|
|
if loader.closed {
|
|
return nil
|
|
}
|
|
loader.closing = true
|
|
if loader.unwind != nil {
|
|
if err := loader.unwind.close(); err != nil {
|
|
return fmt.Errorf("bofloader: unregister unwind information: %w", err)
|
|
}
|
|
loader.unwind = nil
|
|
}
|
|
if loader.region == nil {
|
|
loader.entry = 0
|
|
loader.closed = true
|
|
return nil
|
|
}
|
|
if err := loader.region.close(); err != nil {
|
|
return fmt.Errorf("bofloader: release mapped object: %w", err)
|
|
}
|
|
loader.region = nil
|
|
loader.entry = 0
|
|
loader.closed = true
|
|
return nil
|
|
}
|
|
|
|
func alignUp(value, alignment uint64) uint64 {
|
|
if alignment <= 1 {
|
|
return value
|
|
}
|
|
return (value + alignment - 1) &^ (alignment - 1)
|
|
}
|
|
|
|
func pointerSize() int {
|
|
return int(unsafe.Sizeof(uintptr(0)))
|
|
}
|
|
|
|
func systemPageSize() int {
|
|
return memoryPageSize()
|
|
}
|