mirror of
https://github.com/sliverarmory/reflektor
synced 2026-08-25 09:14:48 +00:00
1591 lines
39 KiB
Go
1591 lines
39 KiB
Go
//go:build linux && (386 || amd64 || arm64)
|
|
|
|
package memmod
|
|
|
|
import (
|
|
"bytes"
|
|
"debug/elf"
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"unsafe"
|
|
|
|
"golang.org/x/sys/unix"
|
|
)
|
|
|
|
type linuxDynAPI struct {
|
|
dlopen uintptr
|
|
dlsym uintptr
|
|
dlerror uintptr
|
|
}
|
|
|
|
var (
|
|
linuxAPIOnce sync.Once
|
|
linuxAPI linuxDynAPI
|
|
linuxAPIErr error
|
|
)
|
|
|
|
const (
|
|
rtldNow = 0x2
|
|
rtldGlobal = 0x100
|
|
|
|
// ELF dynamic tags used for runtime initialization hooks.
|
|
dynTagNull = 0
|
|
dynTagInit = 12
|
|
dynTagInitArray = 25
|
|
dynTagInitArraySz = 27
|
|
dynTagPreinitArr = 32
|
|
dynTagPreinitSz = 33
|
|
)
|
|
|
|
type Module struct {
|
|
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
|
|
tlsOffset int64
|
|
hasTLS bool
|
|
}
|
|
|
|
type dynamicInitInfo struct {
|
|
init uint64
|
|
initArray uint64
|
|
initArraySz uint64
|
|
preinitArr uint64
|
|
preinitSz uint64
|
|
}
|
|
|
|
type runtimeELFModule struct {
|
|
path string
|
|
base uintptr
|
|
score int
|
|
}
|
|
|
|
type symbolResolver struct {
|
|
api *linuxDynAPI
|
|
modules []runtimeELFModule
|
|
resolved map[string]uintptr
|
|
misses map[string]error
|
|
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")
|
|
}
|
|
|
|
f, err := elf.NewFile(bytes.NewReader(data))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid ELF image: %w", err)
|
|
}
|
|
defer f.Close()
|
|
|
|
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 {
|
|
_ = unix.Munmap(mapped.mapping)
|
|
}
|
|
}()
|
|
|
|
resolver := newSymbolResolver(f)
|
|
if err := applyDynamicRelocations(mapped, f, resolver); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := applySegmentProtections(mapped); err != nil {
|
|
return nil, err
|
|
}
|
|
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),
|
|
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()
|
|
|
|
if module.closed {
|
|
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)
|
|
module.mapping = nil
|
|
}
|
|
module.symbols = nil
|
|
module.loadBias = 0
|
|
}
|
|
|
|
func (module *Module) CallExport(name string) error {
|
|
name = strings.TrimSpace(name)
|
|
if name == "" {
|
|
return errors.New("export name cannot be empty")
|
|
}
|
|
|
|
candidates := []string{name}
|
|
if strings.HasPrefix(name, "_") {
|
|
candidates = append(candidates, strings.TrimPrefix(name, "_"))
|
|
} else {
|
|
candidates = append(candidates, "_"+name)
|
|
}
|
|
|
|
var (
|
|
addr uintptr
|
|
err error
|
|
)
|
|
for _, candidate := range candidates {
|
|
addr, err = module.ProcAddressByName(candidate)
|
|
if err == nil {
|
|
break
|
|
}
|
|
}
|
|
if err != nil {
|
|
return fmt.Errorf("resolve export %q: %w", name, err)
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
func (module *Module) ProcAddressByName(name string) (uintptr, error) {
|
|
name = strings.TrimSpace(name)
|
|
if name == "" {
|
|
return 0, errors.New("export name cannot be empty")
|
|
}
|
|
|
|
module.mu.RLock()
|
|
defer module.mu.RUnlock()
|
|
|
|
if module.closed {
|
|
return 0, errors.New("library is closed")
|
|
}
|
|
if len(module.mapping) == 0 {
|
|
return 0, errors.New("library image is empty")
|
|
}
|
|
if module.symbols == nil {
|
|
return 0, errors.New("symbol table is empty")
|
|
}
|
|
|
|
if addr, ok := module.symbols[name]; ok && addr != 0 {
|
|
return addr, nil
|
|
}
|
|
return 0, fmt.Errorf("symbol %q not found", name)
|
|
}
|
|
|
|
func (module *Module) ProcAddressByOrdinal(ordinal uint16) (uintptr, error) {
|
|
_ = ordinal
|
|
return 0, errors.New("ProcAddressByOrdinal is not supported on linux; use ProcAddressByName")
|
|
}
|
|
|
|
func mapELFImage(raw []byte, f *elf.File) (mappedELF, error) {
|
|
pageSize := uint64(unix.Getpagesize())
|
|
if pageSize == 0 {
|
|
return mappedELF{}, errors.New("invalid page size")
|
|
}
|
|
|
|
var (
|
|
minVAddr uint64 = ^uint64(0)
|
|
maxVAddr uint64
|
|
progs []*elf.Prog
|
|
)
|
|
|
|
for _, p := range f.Progs {
|
|
if p.Type != elf.PT_LOAD || p.Memsz == 0 {
|
|
continue
|
|
}
|
|
segStart := alignDown64(p.Vaddr, pageSize)
|
|
segEnd := alignUp64(p.Vaddr+p.Memsz, pageSize)
|
|
if segEnd <= segStart {
|
|
return mappedELF{}, fmt.Errorf("invalid PT_LOAD range vaddr=%#x memsz=%#x", p.Vaddr, p.Memsz)
|
|
}
|
|
if segStart < minVAddr {
|
|
minVAddr = segStart
|
|
}
|
|
if segEnd > maxVAddr {
|
|
maxVAddr = segEnd
|
|
}
|
|
progs = append(progs, p)
|
|
}
|
|
if len(progs) == 0 || minVAddr == ^uint64(0) || maxVAddr <= minVAddr {
|
|
return mappedELF{}, errors.New("ELF image has no loadable segments")
|
|
}
|
|
|
|
mapSize := maxVAddr - minVAddr
|
|
if mapSize == 0 {
|
|
return mappedELF{}, errors.New("ELF image mapping size is zero")
|
|
}
|
|
mapLen, err := u64ToInt(mapSize)
|
|
if err != nil {
|
|
return mappedELF{}, err
|
|
}
|
|
|
|
mapping, err := unix.Mmap(-1, 0, mapLen, unix.PROT_READ|unix.PROT_WRITE, unix.MAP_PRIVATE|unix.MAP_ANON)
|
|
if err != nil {
|
|
return mappedELF{}, fmt.Errorf("mmap ELF image: %w", err)
|
|
}
|
|
if len(mapping) == 0 {
|
|
return mappedELF{}, errors.New("mmap ELF image returned empty mapping")
|
|
}
|
|
|
|
loadBias := uintptr(unsafe.Pointer(&mapping[0])) - uintptr(minVAddr)
|
|
for _, p := range progs {
|
|
if p.Filesz == 0 {
|
|
continue
|
|
}
|
|
if p.Off > uint64(len(raw)) || p.Filesz > uint64(len(raw))-p.Off {
|
|
_ = unix.Munmap(mapping)
|
|
return mappedELF{}, fmt.Errorf("segment file range out of bounds off=%#x filesz=%#x", p.Off, p.Filesz)
|
|
}
|
|
dstLen, err := u64ToInt(p.Filesz)
|
|
if err != nil {
|
|
_ = unix.Munmap(mapping)
|
|
return mappedELF{}, err
|
|
}
|
|
dst := unsafe.Slice((*byte)(unsafe.Pointer(loadBias+uintptr(p.Vaddr))), dstLen)
|
|
src := raw[p.Off : p.Off+p.Filesz]
|
|
copy(dst, src)
|
|
}
|
|
|
|
return mappedELF{
|
|
mapping: mapping,
|
|
loadBias: loadBias,
|
|
progs: progs,
|
|
}, nil
|
|
}
|
|
|
|
func applyDynamicRelocations(mapped mappedELF, f *elf.File, resolver *symbolResolver) error {
|
|
if f.Class != elf.ELFCLASS32 && f.Class != elf.ELFCLASS64 {
|
|
return fmt.Errorf("unsupported ELF class: %s", f.Class)
|
|
}
|
|
if f.Data != elf.ELFDATA2LSB {
|
|
return fmt.Errorf("unsupported ELF endianness: %s", f.Data)
|
|
}
|
|
|
|
dynSyms, err := f.DynamicSymbols()
|
|
if err != nil {
|
|
return fmt.Errorf("read dynamic symbol table: %w", err)
|
|
}
|
|
|
|
for _, sec := range relocationSections(f) {
|
|
data, err := sec.Data()
|
|
if err != nil {
|
|
return fmt.Errorf("read relocation section %s: %w", sec.Name, err)
|
|
}
|
|
if len(data) == 0 {
|
|
continue
|
|
}
|
|
|
|
switch sec.Type {
|
|
case elf.SHT_RELA:
|
|
if err := applyRELASection(data, f, mapped, dynSyms, resolver, sec.Name); err != nil {
|
|
return err
|
|
}
|
|
case elf.SHT_REL:
|
|
if err := applyRELSection(data, f, mapped, dynSyms, resolver, sec.Name); err != nil {
|
|
return err
|
|
}
|
|
default:
|
|
return fmt.Errorf("unsupported relocation section type %s in %s", sec.Type, sec.Name)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func relocationSections(f *elf.File) []*elf.Section {
|
|
names := []string{
|
|
".rela.dyn",
|
|
".rela.plt",
|
|
".rela.plt.sec",
|
|
".rel.dyn",
|
|
".rel.plt",
|
|
".rel.plt.sec",
|
|
}
|
|
out := make([]*elf.Section, 0, len(names))
|
|
for _, name := range names {
|
|
if sec := f.Section(name); sec != nil {
|
|
out = append(out, sec)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func applyRELASection(data []byte, f *elf.File, mapped mappedELF, dynSyms []elf.Symbol, resolver *symbolResolver, sectionName string) error {
|
|
switch f.Class {
|
|
case elf.ELFCLASS64:
|
|
const ent = 24
|
|
if len(data)%ent != 0 {
|
|
return fmt.Errorf("malformed %s: size %d is not a multiple of %d", sectionName, len(data), ent)
|
|
}
|
|
for i := 0; i < len(data); i += ent {
|
|
off := binary.LittleEndian.Uint64(data[i : i+8])
|
|
info := binary.LittleEndian.Uint64(data[i+8 : i+16])
|
|
addend := int64(binary.LittleEndian.Uint64(data[i+16 : i+24]))
|
|
if err := applyOneRelocation(f.Machine, f.Class, mapped, dynSyms, resolver, uint32(elf.R_SYM64(info)), uint32(elf.R_TYPE64(info)), off, addend, true); err != nil {
|
|
return fmt.Errorf("%s[%d]: %w", sectionName, i/ent, err)
|
|
}
|
|
}
|
|
case elf.ELFCLASS32:
|
|
const ent = 12
|
|
if len(data)%ent != 0 {
|
|
return fmt.Errorf("malformed %s: size %d is not a multiple of %d", sectionName, len(data), ent)
|
|
}
|
|
for i := 0; i < len(data); i += ent {
|
|
off := uint64(binary.LittleEndian.Uint32(data[i : i+4]))
|
|
info := binary.LittleEndian.Uint32(data[i+4 : i+8])
|
|
addend := int64(int32(binary.LittleEndian.Uint32(data[i+8 : i+12])))
|
|
if err := applyOneRelocation(f.Machine, f.Class, mapped, dynSyms, resolver, elf.R_SYM32(info), elf.R_TYPE32(info), off, addend, true); err != nil {
|
|
return fmt.Errorf("%s[%d]: %w", sectionName, i/ent, err)
|
|
}
|
|
}
|
|
default:
|
|
return fmt.Errorf("unsupported ELF class in %s: %s", sectionName, f.Class)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func applyRELSection(data []byte, f *elf.File, mapped mappedELF, dynSyms []elf.Symbol, resolver *symbolResolver, sectionName string) error {
|
|
switch f.Class {
|
|
case elf.ELFCLASS64:
|
|
const ent = 16
|
|
if len(data)%ent != 0 {
|
|
return fmt.Errorf("malformed %s: size %d is not a multiple of %d", sectionName, len(data), ent)
|
|
}
|
|
for i := 0; i < len(data); i += ent {
|
|
off := binary.LittleEndian.Uint64(data[i : i+8])
|
|
info := binary.LittleEndian.Uint64(data[i+8 : i+16])
|
|
if err := applyOneRelocation(f.Machine, f.Class, mapped, dynSyms, resolver, uint32(elf.R_SYM64(info)), uint32(elf.R_TYPE64(info)), off, 0, false); err != nil {
|
|
return fmt.Errorf("%s[%d]: %w", sectionName, i/ent, err)
|
|
}
|
|
}
|
|
case elf.ELFCLASS32:
|
|
const ent = 8
|
|
if len(data)%ent != 0 {
|
|
return fmt.Errorf("malformed %s: size %d is not a multiple of %d", sectionName, len(data), ent)
|
|
}
|
|
for i := 0; i < len(data); i += ent {
|
|
off := uint64(binary.LittleEndian.Uint32(data[i : i+4]))
|
|
info := binary.LittleEndian.Uint32(data[i+4 : i+8])
|
|
if err := applyOneRelocation(f.Machine, f.Class, mapped, dynSyms, resolver, elf.R_SYM32(info), elf.R_TYPE32(info), off, 0, false); err != nil {
|
|
return fmt.Errorf("%s[%d]: %w", sectionName, i/ent, err)
|
|
}
|
|
}
|
|
default:
|
|
return fmt.Errorf("unsupported ELF class in %s: %s", sectionName, f.Class)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func applyOneRelocation(machine elf.Machine, class elf.Class, mapped mappedELF, dynSyms []elf.Symbol, resolver *symbolResolver, symIndex uint32, relocType uint32, offset uint64, addend int64, hasAddend bool) error {
|
|
place := mapped.loadBias + uintptr(offset)
|
|
|
|
wordSize := 8
|
|
if class == elf.ELFCLASS32 {
|
|
wordSize = 4
|
|
}
|
|
if !mappedAddressInRange(mapped.mapping, place, wordSize) {
|
|
return fmt.Errorf("relocation target %#x out of mapped image", offset)
|
|
}
|
|
|
|
if !hasAddend {
|
|
switch class {
|
|
case elf.ELFCLASS64:
|
|
addend = int64(readU64(place))
|
|
case elf.ELFCLASS32:
|
|
addend = int64(int32(readU32(place)))
|
|
default:
|
|
return fmt.Errorf("unsupported ELF class: %s", class)
|
|
}
|
|
}
|
|
|
|
var symValue uintptr
|
|
if symIndex != 0 {
|
|
resolved, err := resolveRelocationSymbol(symIndex, dynSyms, mapped.loadBias, resolver)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
symValue = resolved
|
|
}
|
|
|
|
switch machine {
|
|
case elf.EM_X86_64:
|
|
return applyX8664Reloc(relocType, place, mapped.loadBias, symValue, addend, mapped.tlsOffset, mapped.hasTLS)
|
|
case elf.EM_386:
|
|
return apply386Reloc(relocType, place, mapped.loadBias, symValue, addend, mapped.tlsOffset, mapped.hasTLS)
|
|
case elf.EM_AARCH64:
|
|
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, tlsOffset int64, hasTLS bool) error {
|
|
switch elf.R_X86_64(relocType) {
|
|
case elf.R_X86_64_NONE:
|
|
return nil
|
|
case elf.R_X86_64_RELATIVE:
|
|
writeU64(place, uint64(int64(loadBias)+addend))
|
|
return nil
|
|
case elf.R_X86_64_TPOFF64:
|
|
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))
|
|
return nil
|
|
case elf.R_X86_64_32:
|
|
v := int64(symValue) + addend
|
|
if v < 0 || v > 0xffffffff {
|
|
return fmt.Errorf("x86_64 32 relocation overflow: value=%d", v)
|
|
}
|
|
writeU32(place, uint32(v))
|
|
return nil
|
|
case elf.R_X86_64_32S:
|
|
v := int64(symValue) + addend
|
|
if v < -0x80000000 || v > 0x7fffffff {
|
|
return fmt.Errorf("x86_64 32S relocation overflow: value=%d", v)
|
|
}
|
|
writeU32(place, uint32(int32(v)))
|
|
return nil
|
|
case elf.R_X86_64_PC32:
|
|
v := int64(symValue) + addend - int64(place)
|
|
if v < -0x80000000 || v > 0x7fffffff {
|
|
return fmt.Errorf("x86_64 PC32 relocation overflow: value=%d", v)
|
|
}
|
|
writeU32(place, uint32(int32(v)))
|
|
return nil
|
|
default:
|
|
return fmt.Errorf("unsupported x86_64 relocation type: %d", relocType)
|
|
}
|
|
}
|
|
|
|
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
|
|
case elf.R_386_RELATIVE:
|
|
writeU32(place, uint32(int64(loadBias)+addend))
|
|
return nil
|
|
case elf.R_386_TLS_TPOFF:
|
|
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))
|
|
return nil
|
|
case elf.R_386_32, elf.R_386_32PLT:
|
|
writeU32(place, uint32(int64(symValue)+addend))
|
|
return nil
|
|
case elf.R_386_PC32:
|
|
v := int64(symValue) + addend - int64(place)
|
|
if v < -0x80000000 || v > 0x7fffffff {
|
|
return fmt.Errorf("386 PC32 relocation overflow: value=%d", v)
|
|
}
|
|
writeU32(place, uint32(int32(v)))
|
|
return nil
|
|
default:
|
|
return fmt.Errorf("unsupported 386 relocation type: %d", relocType)
|
|
}
|
|
}
|
|
|
|
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
|
|
case elf.R_AARCH64_RELATIVE:
|
|
writeU64(place, uint64(int64(loadBias)+addend))
|
|
return nil
|
|
case elf.R_AARCH64_TLS_TPREL64:
|
|
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))
|
|
return nil
|
|
default:
|
|
return fmt.Errorf("unsupported aarch64 relocation type: %d", relocType)
|
|
}
|
|
}
|
|
|
|
func resolveRelocationSymbol(symIndex uint32, dynSyms []elf.Symbol, loadBias uintptr, resolver *symbolResolver) (uintptr, error) {
|
|
if symIndex == 0 {
|
|
return 0, nil
|
|
}
|
|
|
|
sym, ok := dynSymbolByIndex(dynSyms, symIndex)
|
|
if !ok {
|
|
return 0, fmt.Errorf("relocation references invalid symbol index %d", symIndex)
|
|
}
|
|
bind := elf.ST_BIND(sym.Info)
|
|
if sym.Section == elf.SHN_UNDEF && bind == elf.STB_WEAK {
|
|
// Undefined weak symbols are optional and resolve to 0 by ELF rules.
|
|
return 0, nil
|
|
}
|
|
if sym.Section != elf.SHN_UNDEF && sym.Value != 0 {
|
|
return loadBias + uintptr(sym.Value), nil
|
|
}
|
|
if sym.Name == "" {
|
|
return 0, fmt.Errorf("relocation symbol index %d is undefined and unnamed", symIndex)
|
|
}
|
|
|
|
addr, err := resolver.Resolve(sym.Name)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("resolve external symbol %q: %w", sym.Name, err)
|
|
}
|
|
if addr == 0 {
|
|
return 0, fmt.Errorf("resolved external symbol %q to nil address", sym.Name)
|
|
}
|
|
return addr, nil
|
|
}
|
|
|
|
func dynSymbolByIndex(dynSyms []elf.Symbol, symIndex uint32) (elf.Symbol, bool) {
|
|
// debug/elf.DynamicSymbols omits the null symbol at dynsym index 0.
|
|
if symIndex == 0 {
|
|
return elf.Symbol{}, false
|
|
}
|
|
idx := int(symIndex - 1)
|
|
if idx < 0 || idx >= len(dynSyms) {
|
|
return elf.Symbol{}, false
|
|
}
|
|
return dynSyms[idx], true
|
|
}
|
|
|
|
func applySegmentProtections(mapped mappedELF) error {
|
|
pageSize := uint64(unix.Getpagesize())
|
|
if pageSize == 0 {
|
|
return errors.New("invalid page size")
|
|
}
|
|
|
|
for _, p := range mapped.progs {
|
|
if p.Type != elf.PT_LOAD || p.Memsz == 0 {
|
|
continue
|
|
}
|
|
start := alignDown64(p.Vaddr, pageSize)
|
|
end := alignUp64(p.Vaddr+p.Memsz, pageSize)
|
|
if end <= start {
|
|
continue
|
|
}
|
|
length, err := u64ToInt(end - start)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
addr := mapped.loadBias + uintptr(start)
|
|
if !mappedAddressInRange(mapped.mapping, addr, length) {
|
|
return fmt.Errorf("segment protection range out of mapped image vaddr=%#x len=%#x", start, end-start)
|
|
}
|
|
seg := unsafe.Slice((*byte)(unsafe.Pointer(addr)), length)
|
|
if err := unix.Mprotect(seg, progFlagsToProt(p.Flags)); err != nil {
|
|
return fmt.Errorf("mprotect PT_LOAD vaddr=%#x memsz=%#x: %w", p.Vaddr, p.Memsz, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
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 nil, err
|
|
}
|
|
initializers, err = appendDynamicInitFn(initializers, mapped, uintptr(info.init), "DT_INIT")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
initializers, err = appendDynamicInitArray(initializers, mapped, class, info.initArray, info.initArraySz, "DT_INIT_ARRAY")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return initializers, nil
|
|
}
|
|
|
|
func parseDynamicInitInfo(f *elf.File) (dynamicInitInfo, error) {
|
|
var info dynamicInitInfo
|
|
if f == nil {
|
|
return info, nil
|
|
}
|
|
sec := f.Section(".dynamic")
|
|
if sec == nil {
|
|
return info, nil
|
|
}
|
|
data, err := sec.Data()
|
|
if err != nil {
|
|
return info, fmt.Errorf("read .dynamic section: %w", err)
|
|
}
|
|
if len(data) == 0 {
|
|
return info, nil
|
|
}
|
|
|
|
switch f.Class {
|
|
case elf.ELFCLASS64:
|
|
const ent = 16
|
|
if len(data)%ent != 0 {
|
|
return info, fmt.Errorf("malformed .dynamic section: size %d is not a multiple of %d", len(data), ent)
|
|
}
|
|
for i := 0; i < len(data); i += ent {
|
|
tag := int64(binary.LittleEndian.Uint64(data[i : i+8]))
|
|
val := binary.LittleEndian.Uint64(data[i+8 : i+16])
|
|
if tag == dynTagNull {
|
|
break
|
|
}
|
|
switch tag {
|
|
case dynTagInit:
|
|
info.init = val
|
|
case dynTagInitArray:
|
|
info.initArray = val
|
|
case dynTagInitArraySz:
|
|
info.initArraySz = val
|
|
case dynTagPreinitArr:
|
|
info.preinitArr = val
|
|
case dynTagPreinitSz:
|
|
info.preinitSz = val
|
|
}
|
|
}
|
|
case elf.ELFCLASS32:
|
|
const ent = 8
|
|
if len(data)%ent != 0 {
|
|
return info, fmt.Errorf("malformed .dynamic section: size %d is not a multiple of %d", len(data), ent)
|
|
}
|
|
for i := 0; i < len(data); i += ent {
|
|
tag := int64(int32(binary.LittleEndian.Uint32(data[i : i+4])))
|
|
val := uint64(binary.LittleEndian.Uint32(data[i+4 : i+8]))
|
|
if tag == dynTagNull {
|
|
break
|
|
}
|
|
switch tag {
|
|
case dynTagInit:
|
|
info.init = val
|
|
case dynTagInitArray:
|
|
info.initArray = val
|
|
case dynTagInitArraySz:
|
|
info.initArraySz = val
|
|
case dynTagPreinitArr:
|
|
info.preinitArr = val
|
|
case dynTagPreinitSz:
|
|
info.preinitSz = val
|
|
}
|
|
}
|
|
default:
|
|
return info, fmt.Errorf("unsupported ELF class for .dynamic parsing: %s", f.Class)
|
|
}
|
|
return info, nil
|
|
}
|
|
|
|
func appendDynamicInitFn(initializers []uintptr, mapped mappedELF, fn uintptr, source string) ([]uintptr, error) {
|
|
if fn == 0 {
|
|
return initializers, nil
|
|
}
|
|
resolved, ok := normalizeInitFnAddress(mapped, fn)
|
|
if !ok {
|
|
return nil, fmt.Errorf("%s points outside mapped image: %#x", source, fn)
|
|
}
|
|
return append(initializers, resolved), nil
|
|
}
|
|
|
|
func appendDynamicInitArray(initializers []uintptr, mapped mappedELF, class elf.Class, arrayVAddr uint64, arraySz uint64, source string) ([]uintptr, error) {
|
|
if arrayVAddr == 0 || arraySz == 0 {
|
|
return initializers, nil
|
|
}
|
|
|
|
entrySize := 8
|
|
if class == elf.ELFCLASS32 {
|
|
entrySize = 4
|
|
}
|
|
if arraySz%uint64(entrySize) != 0 {
|
|
return nil, fmt.Errorf("%s has malformed size %#x for entry size %d", source, arraySz, entrySize)
|
|
}
|
|
arrayLen, err := u64ToInt(arraySz)
|
|
if err != nil {
|
|
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 nil, fmt.Errorf("%s range %#x..%#x is outside mapped image", source, arrayVAddr, arrayVAddr+arraySz)
|
|
}
|
|
|
|
count := int(arraySz / uint64(entrySize))
|
|
for i := 0; i < count; i++ {
|
|
entryAddr := arrayAddr + uintptr(i*entrySize)
|
|
var fn uintptr
|
|
if entrySize == 8 {
|
|
fn = uintptr(readU64(entryAddr))
|
|
} else {
|
|
fn = uintptr(readU32(entryAddr))
|
|
}
|
|
if fn == 0 || fn == ^uintptr(0) {
|
|
continue
|
|
}
|
|
resolved, ok := normalizeInitFnAddress(mapped, fn)
|
|
if !ok {
|
|
return nil, fmt.Errorf("%s[%d] points outside mapped image: %#x", source, i, fn)
|
|
}
|
|
initializers = append(initializers, resolved)
|
|
}
|
|
return initializers, nil
|
|
}
|
|
|
|
func normalizeInitFnAddress(mapped mappedELF, fn uintptr) (uintptr, bool) {
|
|
if fn == 0 {
|
|
return 0, false
|
|
}
|
|
if mappedAddressInRange(mapped.mapping, fn, 1) {
|
|
return fn, true
|
|
}
|
|
rebased := mapped.loadBias + fn
|
|
if mappedAddressInRange(mapped.mapping, rebased, 1) {
|
|
return rebased, true
|
|
}
|
|
return 0, false
|
|
}
|
|
|
|
func buildExportedSymbolTable(f *elf.File, loadBias uintptr) map[string]uintptr {
|
|
out := make(map[string]uintptr)
|
|
if dynSyms, err := f.DynamicSymbols(); err == nil {
|
|
addELFSymbols(out, dynSyms, loadBias)
|
|
}
|
|
if syms, err := f.Symbols(); err == nil {
|
|
addELFSymbols(out, syms, loadBias)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func addELFSymbols(dst map[string]uintptr, symbols []elf.Symbol, loadBias uintptr) {
|
|
for _, sym := range symbols {
|
|
if sym.Name == "" || sym.Value == 0 || sym.Section == elf.SHN_UNDEF {
|
|
continue
|
|
}
|
|
bind := elf.ST_BIND(sym.Info)
|
|
if bind != elf.STB_GLOBAL && bind != elf.STB_WEAK {
|
|
continue
|
|
}
|
|
typ := elf.ST_TYPE(sym.Info)
|
|
if typ != elf.STT_FUNC && typ != elf.STT_NOTYPE {
|
|
continue
|
|
}
|
|
addr := loadBias + uintptr(sym.Value)
|
|
if _, ok := dst[sym.Name]; !ok {
|
|
dst[sym.Name] = addr
|
|
}
|
|
if at := strings.IndexByte(sym.Name, '@'); at > 0 {
|
|
base := sym.Name[:at]
|
|
if _, ok := dst[base]; !ok {
|
|
dst[base] = addr
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func newSymbolResolver(f *elf.File) *symbolResolver {
|
|
resolver := &symbolResolver{
|
|
resolved: make(map[string]uintptr),
|
|
misses: make(map[string]error),
|
|
opened: make(map[string]uintptr),
|
|
}
|
|
if modules, err := runtimeModules(); err == nil {
|
|
resolver.modules = modules
|
|
}
|
|
if api, err := getLinuxDynAPI(); err == nil {
|
|
resolver.api = api
|
|
}
|
|
if f != nil {
|
|
resolver.primeDependencies(f)
|
|
}
|
|
return resolver
|
|
}
|
|
|
|
func (resolver *symbolResolver) primeDependencies(f *elf.File) {
|
|
libs := collectNeededLibraries(f)
|
|
libs = append(libs, commonLinuxDependencies()...)
|
|
for _, lib := range libs {
|
|
_ = resolver.ensureLibraryLoaded(lib)
|
|
}
|
|
}
|
|
|
|
func collectNeededLibraries(f *elf.File) []string {
|
|
if f == nil {
|
|
return nil
|
|
}
|
|
imports, err := f.ImportedLibraries()
|
|
if err != nil || len(imports) == 0 {
|
|
return nil
|
|
}
|
|
out := make([]string, 0, len(imports))
|
|
seen := make(map[string]struct{}, len(imports))
|
|
for _, lib := range imports {
|
|
lib = strings.TrimSpace(lib)
|
|
if lib == "" {
|
|
continue
|
|
}
|
|
if _, exists := seen[lib]; exists {
|
|
continue
|
|
}
|
|
seen[lib] = struct{}{}
|
|
out = append(out, lib)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func commonLinuxDependencies() []string {
|
|
deps := []string{
|
|
"libc.so.6",
|
|
"libdl.so.2",
|
|
"libpthread.so.0",
|
|
}
|
|
switch runtime.GOARCH {
|
|
case "amd64":
|
|
deps = append(deps, "ld-linux-x86-64.so.2", "ld-musl-x86_64.so.1")
|
|
case "386":
|
|
deps = append(deps, "ld-linux.so.2", "ld-musl-i386.so.1")
|
|
case "arm64":
|
|
deps = append(deps, "ld-linux-aarch64.so.1", "ld-musl-aarch64.so.1")
|
|
}
|
|
return deps
|
|
}
|
|
|
|
func (resolver *symbolResolver) ensureLibraryLoaded(name string) error {
|
|
name = strings.TrimSpace(name)
|
|
if name == "" {
|
|
return nil
|
|
}
|
|
if resolver.hasModule(name) {
|
|
return nil
|
|
}
|
|
if resolver.api == nil || resolver.api.dlopen == 0 {
|
|
return errors.New("dlopen is unavailable")
|
|
}
|
|
|
|
var lastErr error
|
|
for _, candidate := range dlopenCandidates(name) {
|
|
if candidate == "" {
|
|
continue
|
|
}
|
|
if resolver.hasModule(candidate) {
|
|
return nil
|
|
}
|
|
if _, opened := resolver.opened[candidate]; opened {
|
|
continue
|
|
}
|
|
|
|
handle, err := openWithDlopen(resolver.api, candidate)
|
|
if err != nil {
|
|
lastErr = err
|
|
continue
|
|
}
|
|
if handle == 0 {
|
|
continue
|
|
}
|
|
resolver.opened[candidate] = handle
|
|
resolver.opened[name] = handle
|
|
resolver.refreshModules()
|
|
if resolver.hasModule(name) || resolver.hasModule(candidate) {
|
|
return nil
|
|
}
|
|
}
|
|
if resolver.hasModule(name) {
|
|
return nil
|
|
}
|
|
if lastErr == nil {
|
|
lastErr = fmt.Errorf("dlopen(%s): returned nil handle", name)
|
|
}
|
|
return lastErr
|
|
}
|
|
|
|
func (resolver *symbolResolver) refreshModules() {
|
|
if modules, err := runtimeModules(); err == nil {
|
|
resolver.modules = modules
|
|
}
|
|
}
|
|
|
|
func (resolver *symbolResolver) hasModule(name string) bool {
|
|
name = strings.TrimSpace(name)
|
|
if name == "" {
|
|
return false
|
|
}
|
|
base := filepath.Base(name)
|
|
for _, module := range resolver.modules {
|
|
if module.path == name {
|
|
return true
|
|
}
|
|
if base != "" && filepath.Base(module.path) == base {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func dlopenCandidates(name string) []string {
|
|
name = strings.TrimSpace(name)
|
|
if name == "" {
|
|
return nil
|
|
}
|
|
out := make([]string, 0, 8)
|
|
seen := make(map[string]struct{}, 8)
|
|
add := func(v string) {
|
|
v = strings.TrimSpace(v)
|
|
if v == "" {
|
|
return
|
|
}
|
|
if _, exists := seen[v]; exists {
|
|
return
|
|
}
|
|
seen[v] = struct{}{}
|
|
out = append(out, v)
|
|
}
|
|
|
|
add(name)
|
|
base := filepath.Base(name)
|
|
add(base)
|
|
|
|
switch base {
|
|
case "libc.so":
|
|
add("libc.so.6")
|
|
case "libdl.so":
|
|
add("libdl.so.2")
|
|
case "libpthread.so":
|
|
add("libpthread.so.0")
|
|
}
|
|
if idx := strings.Index(base, ".so."); idx > 0 {
|
|
add(base[:idx+3])
|
|
}
|
|
|
|
for _, dir := range linuxLibrarySearchDirs() {
|
|
add(filepath.Join(dir, base))
|
|
}
|
|
return out
|
|
}
|
|
|
|
func linuxLibrarySearchDirs() []string {
|
|
dirs := []string{"/lib", "/lib64", "/usr/lib", "/usr/lib64"}
|
|
switch runtime.GOARCH {
|
|
case "amd64":
|
|
dirs = append(dirs, "/lib/x86_64-linux-gnu", "/usr/lib/x86_64-linux-gnu")
|
|
case "386":
|
|
dirs = append(dirs, "/lib/i386-linux-gnu", "/usr/lib/i386-linux-gnu")
|
|
case "arm64":
|
|
dirs = append(dirs, "/lib/aarch64-linux-gnu", "/usr/lib/aarch64-linux-gnu")
|
|
}
|
|
return dirs
|
|
}
|
|
|
|
func (resolver *symbolResolver) Resolve(name string) (uintptr, error) {
|
|
if addr, ok := resolver.resolved[name]; ok {
|
|
return addr, nil
|
|
}
|
|
if err, ok := resolver.misses[name]; ok {
|
|
return 0, err
|
|
}
|
|
|
|
if addr, err := resolveFromRuntimeModules(resolver.modules, name); err == nil && addr != 0 {
|
|
resolver.resolved[name] = addr
|
|
return addr, nil
|
|
}
|
|
|
|
if resolver.api != nil {
|
|
if addr, err := resolveWithDLSym(resolver.api, name); err == nil && addr != 0 {
|
|
resolver.resolved[name] = addr
|
|
return addr, nil
|
|
}
|
|
}
|
|
|
|
if resolver.api != nil && resolver.api.dlopen != 0 {
|
|
for _, dep := range commonLinuxDependencies() {
|
|
_ = resolver.ensureLibraryLoaded(dep)
|
|
}
|
|
if addr, err := resolveFromRuntimeModules(resolver.modules, name); err == nil && addr != 0 {
|
|
resolver.resolved[name] = addr
|
|
return addr, nil
|
|
}
|
|
if addr, err := resolveWithDLSym(resolver.api, name); err == nil && addr != 0 {
|
|
resolver.resolved[name] = addr
|
|
return addr, nil
|
|
}
|
|
}
|
|
|
|
if at := strings.IndexByte(name, '@'); at > 0 {
|
|
base := name[:at]
|
|
if base != "" && base != name {
|
|
if addr, err := resolver.Resolve(base); err == nil && addr != 0 {
|
|
resolver.resolved[name] = addr
|
|
return addr, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
err := fmt.Errorf("unresolved external symbol %q", name)
|
|
resolver.misses[name] = err
|
|
return 0, err
|
|
}
|
|
|
|
func resolveFromRuntimeModules(modules []runtimeELFModule, name string) (uintptr, error) {
|
|
for _, module := range modules {
|
|
off, err := findELFSymbolOffset(module.path, name)
|
|
if err != nil || off == 0 {
|
|
continue
|
|
}
|
|
return module.base + off, nil
|
|
}
|
|
return 0, fmt.Errorf("symbol %q not found in loaded ELF modules", name)
|
|
}
|
|
|
|
func runtimeModules() ([]runtimeELFModule, error) {
|
|
entries, err := readProcMaps()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
byPath := make(map[string]runtimeELFModule)
|
|
for _, entry := range entries {
|
|
if entry.path == "" || !strings.HasPrefix(entry.path, "/") {
|
|
continue
|
|
}
|
|
if entry.start < entry.offset {
|
|
continue
|
|
}
|
|
base := entry.start - entry.offset
|
|
current, exists := byPath[entry.path]
|
|
if !exists || base < current.base {
|
|
byPath[entry.path] = runtimeELFModule{
|
|
path: entry.path,
|
|
base: base,
|
|
score: libcPathScore(entry.path),
|
|
}
|
|
}
|
|
}
|
|
|
|
modules := make([]runtimeELFModule, 0, len(byPath))
|
|
for _, module := range byPath {
|
|
modules = append(modules, module)
|
|
}
|
|
sort.Slice(modules, func(i, j int) bool {
|
|
if modules[i].score != modules[j].score {
|
|
return modules[i].score > modules[j].score
|
|
}
|
|
return modules[i].path < modules[j].path
|
|
})
|
|
return modules, nil
|
|
}
|
|
|
|
func resolveWithDLSym(api *linuxDynAPI, name string) (uintptr, error) {
|
|
if api == nil || api.dlsym == 0 {
|
|
return 0, errors.New("dlsym is unavailable")
|
|
}
|
|
cName, err := cStringBytes(name)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if api.dlerror != 0 {
|
|
_ = cCall0(api.dlerror)
|
|
}
|
|
sym := cCall2(api.dlsym, 0, cStringPtr(cName))
|
|
runtime.KeepAlive(cName)
|
|
if api.dlerror != 0 {
|
|
if err := lastDLError(api); err != nil {
|
|
return 0, fmt.Errorf("dlsym(%s): %w", name, err)
|
|
}
|
|
}
|
|
if sym == 0 {
|
|
return 0, fmt.Errorf("dlsym(%s): symbol address is nil", name)
|
|
}
|
|
return sym, nil
|
|
}
|
|
|
|
func openWithDlopen(api *linuxDynAPI, name string) (uintptr, error) {
|
|
if api == nil || api.dlopen == 0 {
|
|
return 0, errors.New("dlopen is unavailable")
|
|
}
|
|
cName, err := cStringBytes(name)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if api.dlerror != 0 {
|
|
_ = cCall0(api.dlerror)
|
|
}
|
|
handle := cCall2(api.dlopen, cStringPtr(cName), uintptr(rtldNow|rtldGlobal))
|
|
runtime.KeepAlive(cName)
|
|
if api.dlerror != 0 {
|
|
if err := lastDLError(api); err != nil {
|
|
return 0, fmt.Errorf("dlopen(%s): %w", name, err)
|
|
}
|
|
}
|
|
if handle == 0 {
|
|
return 0, fmt.Errorf("dlopen(%s): symbol handle is nil", name)
|
|
}
|
|
return handle, nil
|
|
}
|
|
|
|
func mappedAddressInRange(mapping []byte, addr uintptr, size int) bool {
|
|
if len(mapping) == 0 || size < 0 {
|
|
return false
|
|
}
|
|
start := uintptr(unsafe.Pointer(&mapping[0]))
|
|
end := start + uintptr(len(mapping))
|
|
if addr < start {
|
|
return false
|
|
}
|
|
if uintptr(size) > end-addr {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func progFlagsToProt(flags elf.ProgFlag) int {
|
|
prot := 0
|
|
if flags&elf.PF_R != 0 {
|
|
prot |= unix.PROT_READ
|
|
}
|
|
if flags&elf.PF_W != 0 {
|
|
prot |= unix.PROT_WRITE
|
|
}
|
|
if flags&elf.PF_X != 0 {
|
|
prot |= unix.PROT_EXEC
|
|
}
|
|
return prot
|
|
}
|
|
|
|
func alignDown64(v, a uint64) uint64 {
|
|
if a == 0 {
|
|
return v
|
|
}
|
|
return v &^ (a - 1)
|
|
}
|
|
|
|
func alignUp64(v, a uint64) uint64 {
|
|
if a == 0 {
|
|
return v
|
|
}
|
|
return (v + (a - 1)) &^ (a - 1)
|
|
}
|
|
|
|
func u64ToInt(v uint64) (int, error) {
|
|
max := ^uint(0) >> 1
|
|
if v > uint64(max) {
|
|
return 0, fmt.Errorf("value %d does not fit in int", v)
|
|
}
|
|
return int(v), nil
|
|
}
|
|
|
|
func readU32(addr uintptr) uint32 {
|
|
b := unsafe.Slice((*byte)(unsafe.Pointer(addr)), 4)
|
|
return binary.LittleEndian.Uint32(b)
|
|
}
|
|
|
|
func writeU32(addr uintptr, v uint32) {
|
|
b := unsafe.Slice((*byte)(unsafe.Pointer(addr)), 4)
|
|
binary.LittleEndian.PutUint32(b, v)
|
|
}
|
|
|
|
func readU64(addr uintptr) uint64 {
|
|
b := unsafe.Slice((*byte)(unsafe.Pointer(addr)), 8)
|
|
return binary.LittleEndian.Uint64(b)
|
|
}
|
|
|
|
func writeU64(addr uintptr, v uint64) {
|
|
b := unsafe.Slice((*byte)(unsafe.Pointer(addr)), 8)
|
|
binary.LittleEndian.PutUint64(b, v)
|
|
}
|
|
|
|
func cStringBytes(s string) ([]byte, error) {
|
|
if strings.ContainsRune(s, '\x00') {
|
|
return nil, errors.New("string contains NUL")
|
|
}
|
|
b := make([]byte, len(s)+1)
|
|
copy(b, s)
|
|
return b, nil
|
|
}
|
|
|
|
func cStringPtr(b []byte) uintptr {
|
|
if len(b) == 0 {
|
|
return 0
|
|
}
|
|
return uintptr(unsafe.Pointer(&b[0]))
|
|
}
|
|
|
|
func cStringFromPtr(ptr uintptr) string {
|
|
if ptr == 0 {
|
|
return ""
|
|
}
|
|
const maxLen = 1 << 20
|
|
buf := make([]byte, 0, 64)
|
|
for i := 0; i < maxLen; i++ {
|
|
ch := *(*byte)(unsafe.Pointer(ptr + uintptr(i)))
|
|
if ch == 0 {
|
|
return string(buf)
|
|
}
|
|
buf = append(buf, ch)
|
|
}
|
|
return string(buf)
|
|
}
|
|
|
|
func lastDLError(api *linuxDynAPI) error {
|
|
if api == nil || api.dlerror == 0 {
|
|
return nil
|
|
}
|
|
msg := cStringFromPtr(cCall0(api.dlerror))
|
|
if msg == "" {
|
|
return nil
|
|
}
|
|
return errors.New(msg)
|
|
}
|
|
|
|
func getLinuxDynAPI() (*linuxDynAPI, error) {
|
|
linuxAPIOnce.Do(func() {
|
|
linuxAPIErr = initLinuxDynAPI()
|
|
})
|
|
if linuxAPIErr != nil {
|
|
return nil, linuxAPIErr
|
|
}
|
|
return &linuxAPI, nil
|
|
}
|
|
|
|
func initLinuxDynAPI() error {
|
|
modules, err := runtimeModules()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
dlopenAddr, err := resolveRuntimeAPISymbol(modules, "dlopen")
|
|
if err != nil {
|
|
return fmt.Errorf("resolve runtime symbol dlopen: %w", err)
|
|
}
|
|
dlsymAddr, err := resolveRuntimeAPISymbol(modules, "dlsym")
|
|
if err != nil {
|
|
return fmt.Errorf("resolve runtime symbol dlsym: %w", err)
|
|
}
|
|
dlerrorAddr, err := resolveRuntimeAPISymbol(modules, "dlerror")
|
|
if err != nil {
|
|
return fmt.Errorf("resolve runtime symbol dlerror: %w", err)
|
|
}
|
|
|
|
linuxAPI = linuxDynAPI{
|
|
dlopen: dlopenAddr,
|
|
dlsym: dlsymAddr,
|
|
dlerror: dlerrorAddr,
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type procMapEntry struct {
|
|
start uintptr
|
|
offset uintptr
|
|
perms string
|
|
path string
|
|
}
|
|
|
|
func resolveRuntimeAPISymbol(modules []runtimeELFModule, symbol string) (uintptr, error) {
|
|
for _, module := range modules {
|
|
off, err := findELFSymbolOffset(module.path, symbol)
|
|
if err != nil || off == 0 {
|
|
continue
|
|
}
|
|
return module.base + off, nil
|
|
}
|
|
return 0, fmt.Errorf("symbol %q not found in runtime modules", symbol)
|
|
}
|
|
|
|
func libcPathScore(path string) int {
|
|
p := strings.ToLower(path)
|
|
switch {
|
|
case strings.Contains(p, "libc.so"):
|
|
return 100
|
|
case strings.Contains(p, "libc-"):
|
|
return 95
|
|
case strings.Contains(p, "ld-musl"):
|
|
return 90
|
|
case strings.Contains(p, "musl"):
|
|
return 85
|
|
case strings.Contains(p, "ld-linux"):
|
|
return 80
|
|
default:
|
|
return -1
|
|
}
|
|
}
|
|
|
|
func readProcMaps() ([]procMapEntry, error) {
|
|
raw, err := os.ReadFile("/proc/self/maps")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read /proc/self/maps: %w", err)
|
|
}
|
|
|
|
lines := strings.Split(string(raw), "\n")
|
|
entries := make([]procMapEntry, 0, len(lines))
|
|
for _, line := range lines {
|
|
line = strings.TrimSpace(line)
|
|
if line == "" {
|
|
continue
|
|
}
|
|
fields := strings.Fields(line)
|
|
if len(fields) < 5 {
|
|
continue
|
|
}
|
|
if !strings.Contains(fields[1], "x") {
|
|
continue
|
|
}
|
|
|
|
rangeParts := strings.SplitN(fields[0], "-", 2)
|
|
if len(rangeParts) != 2 {
|
|
continue
|
|
}
|
|
start, startErr := parseHexUintptr(rangeParts[0])
|
|
offset, offsetErr := parseHexUintptr(fields[2])
|
|
if startErr != nil || offsetErr != nil {
|
|
continue
|
|
}
|
|
|
|
path := ""
|
|
if len(fields) >= 6 {
|
|
path = strings.Join(fields[5:], " ")
|
|
path = strings.TrimSuffix(path, " (deleted)")
|
|
}
|
|
if path == "" || !strings.HasPrefix(path, "/") {
|
|
continue
|
|
}
|
|
|
|
entries = append(entries, procMapEntry{
|
|
start: start,
|
|
offset: offset,
|
|
perms: fields[1],
|
|
path: path,
|
|
})
|
|
}
|
|
return entries, nil
|
|
}
|
|
|
|
func parseHexUintptr(s string) (uintptr, error) {
|
|
var out uintptr
|
|
for _, r := range s {
|
|
out <<= 4
|
|
switch {
|
|
case r >= '0' && r <= '9':
|
|
out += uintptr(r - '0')
|
|
case r >= 'a' && r <= 'f':
|
|
out += uintptr(r-'a') + 10
|
|
case r >= 'A' && r <= 'F':
|
|
out += uintptr(r-'A') + 10
|
|
default:
|
|
return 0, fmt.Errorf("invalid hex string %q", s)
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func findELFSymbolOffset(path string, symbol string) (uintptr, error) {
|
|
f, err := elf.Open(path)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("open elf %s: %w", path, err)
|
|
}
|
|
defer f.Close()
|
|
|
|
if syms, err := f.DynamicSymbols(); err == nil {
|
|
if off, ok := matchSymbolOffset(syms, symbol); ok {
|
|
return off, nil
|
|
}
|
|
}
|
|
if syms, err := f.Symbols(); err == nil {
|
|
if off, ok := matchSymbolOffset(syms, symbol); ok {
|
|
return off, nil
|
|
}
|
|
}
|
|
return 0, fmt.Errorf("symbol %s not found in %s", symbol, path)
|
|
}
|
|
|
|
func matchSymbolOffset(symbols []elf.Symbol, want string) (uintptr, bool) {
|
|
for _, s := range symbols {
|
|
if s.Value == 0 {
|
|
continue
|
|
}
|
|
if s.Name == want || strings.HasPrefix(s.Name, want+"@") {
|
|
return uintptr(s.Value), true
|
|
}
|
|
}
|
|
return 0, false
|
|
}
|
|
|
|
func validateELFForCurrentArch(data []byte) error {
|
|
f, err := elf.NewFile(bytes.NewReader(data))
|
|
if err != nil {
|
|
return fmt.Errorf("invalid ELF image: %w", err)
|
|
}
|
|
defer f.Close()
|
|
return validateELFHeaders(f)
|
|
}
|
|
|
|
func validateELFHeaders(f *elf.File) error {
|
|
machine, err := currentELFMachine()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if f.Machine != machine {
|
|
return fmt.Errorf("foreign platform (provided: %s, expected: %s)", f.Machine, machine)
|
|
}
|
|
if f.Type != elf.ET_DYN {
|
|
return fmt.Errorf("unsupported ELF file type: %s", f.Type)
|
|
}
|
|
if f.Data != elf.ELFDATA2LSB {
|
|
return fmt.Errorf("unsupported ELF endianness: %s", f.Data)
|
|
}
|
|
if f.Class != elf.ELFCLASS32 && f.Class != elf.ELFCLASS64 {
|
|
return fmt.Errorf("unsupported ELF class: %s", f.Class)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func currentELFMachine() (elf.Machine, error) {
|
|
switch runtime.GOARCH {
|
|
case "386":
|
|
return elf.EM_386, nil
|
|
case "amd64":
|
|
return elf.EM_X86_64, nil
|
|
case "arm64":
|
|
return elf.EM_AARCH64, nil
|
|
default:
|
|
return 0, fmt.Errorf("unsupported linux architecture: %s", runtime.GOARCH)
|
|
}
|
|
}
|