Files
ipsw/pkg/dyld/image.go
blacktop 14457b7d96 feat: address sandbox research feedback
Add dyld softlink/import/disassembly helpers, sandbox reach and diff support, richer WebKit IPC metadata, and expanded IOKit method recovery.

Fix MOVK softlink address reconstruction and avoid cross-case argument-state poisoning while scanning switch-dispatched user-client methods.
2026-05-24 16:28:00 -06:00

1389 lines
37 KiB
Go

package dyld
import (
"bytes"
"encoding/binary"
"fmt"
"io"
"os"
"path/filepath"
"sort"
"strings"
"sync"
"text/tabwriter"
"github.com/apex/log"
"github.com/blacktop/go-macho"
"github.com/blacktop/go-macho/pkg/trie"
"github.com/blacktop/go-macho/types"
"github.com/blacktop/ipsw/internal/utils"
"github.com/blacktop/ipsw/pkg/disass"
"github.com/blacktop/ipsw/pkg/symbols"
"github.com/pkg/errors"
)
type rangeEntry struct {
StartAddr uint64
FileOffset uint64
Size uint32
}
type Patch interface {
GetName() string
GetKind() string
GetImplOffset() uint64
GetClientIndex() uint32
GetPatchLocations() any
GetGotLocations() any
}
type PatchableExport struct {
Name string
Kind string
OffsetOfImpl uint32
ClientIndex uint32
PatchLocations []CachePatchableLocationV1
PatchLocationsV2 []CachePatchableLocationV2
PatchLocationsV4 []CachePatchableLocationV4
}
func (pe PatchableExport) GetName() string {
return pe.Name
}
func (pe PatchableExport) GetKind() string {
return pe.Kind
}
func (pe PatchableExport) GetImplOffset() uint64 {
return uint64(pe.OffsetOfImpl)
}
func (pe PatchableExport) GetClientIndex() uint32 {
return pe.ClientIndex
}
func (pe PatchableExport) GetPatchLocations() any {
if len(pe.PatchLocations) > 0 {
return pe.PatchLocations
} else if len(pe.PatchLocationsV2) > 0 {
return pe.PatchLocationsV2
} else if len(pe.PatchLocationsV4) > 0 {
return pe.PatchLocationsV4
} else {
return nil
}
}
func (pe PatchableExport) GetGotLocations() any {
return nil
}
type PatchableGotExport struct {
Name string
Kind string
OffsetOfImpl uint32
ImageIndex uint32
GotLocationsV3 []CachePatchableLocationV3
GotLocationsV4 []CachePatchableLocationV4Got
}
func (pg PatchableGotExport) GetName() string {
return pg.Name
}
func (pg PatchableGotExport) GetKind() string {
return pg.Kind
}
func (pg PatchableGotExport) GetImplOffset() uint64 {
return uint64(pg.OffsetOfImpl)
}
func (pg PatchableGotExport) GetClientIndex() uint32 {
return 0
}
func (pg PatchableGotExport) GetPatchLocations() any {
return nil
}
func (pg PatchableGotExport) GetGotLocations() any {
if len(pg.GotLocationsV3) > 0 {
return pg.GotLocationsV3
} else if len(pg.GotLocationsV4) > 0 {
return pg.GotLocationsV4
} else {
return nil
}
}
type astate struct {
mu sync.Mutex
Deps bool
Got bool
Stubs bool
Helpers bool
Exports bool
ParsingExports bool
Privates bool
Starts bool
ObjC bool
Slide bool
Swift bool
}
func (a *astate) SetDeps(done bool) {
a.mu.Lock()
a.Deps = done
a.mu.Unlock()
}
func (a *astate) IsDepsDone() bool {
a.mu.Lock()
defer a.mu.Unlock()
return a.Deps
}
func (a *astate) SetGot(done bool) {
a.mu.Lock()
a.Got = done
a.mu.Unlock()
}
func (a *astate) IsGotDone() bool {
a.mu.Lock()
defer a.mu.Unlock()
return a.Got
}
func (a *astate) SetHelpers(done bool) {
a.mu.Lock()
a.Helpers = done
a.mu.Unlock()
}
func (a *astate) IsHelpersDone() bool {
a.mu.Lock()
defer a.mu.Unlock()
return a.Helpers
}
func (a *astate) SetStubs(done bool) {
a.mu.Lock()
a.Stubs = done
a.mu.Unlock()
}
func (a *astate) IsStubsDone() bool {
a.mu.Lock()
defer a.mu.Unlock()
return a.Stubs
}
func (a *astate) SetExports(done bool) {
a.mu.Lock()
a.Exports = done
a.ParsingExports = false
a.mu.Unlock()
}
func (a *astate) IsExportsDone() bool {
a.mu.Lock()
defer a.mu.Unlock()
return a.Exports
}
func (a *astate) BeginExports() bool {
a.mu.Lock()
defer a.mu.Unlock()
if a.Exports || a.ParsingExports {
return false
}
a.ParsingExports = true
return true
}
func (a *astate) FinishExports(done bool) {
a.mu.Lock()
a.Exports = done
a.ParsingExports = false
a.mu.Unlock()
}
func (a *astate) SetPrivates(done bool) {
a.mu.Lock()
a.Privates = done
a.mu.Unlock()
}
func (a *astate) IsPrivatesDone() bool {
a.mu.Lock()
defer a.mu.Unlock()
return a.Privates
}
func (a *astate) SetStarts(done bool) {
a.mu.Lock()
a.Starts = done
a.mu.Unlock()
}
func (a *astate) IsStartsDone() bool {
a.mu.Lock()
defer a.mu.Unlock()
return a.Starts
}
func (a *astate) SetObjC(done bool) {
a.mu.Lock()
a.ObjC = done
a.mu.Unlock()
}
func (a *astate) IsObjcDone() bool {
a.mu.Lock()
defer a.mu.Unlock()
return a.ObjC
}
func (a *astate) SetSlideInfo(done bool) {
a.mu.Lock()
a.Slide = done
a.mu.Unlock()
}
func (a *astate) IsSlideInfoDone() bool {
a.mu.Lock()
defer a.mu.Unlock()
return a.Slide
}
func (a *astate) SetSwift(done bool) {
a.mu.Lock()
a.Swift = done
a.mu.Unlock()
}
func (a *astate) IsSwiftDone() bool {
a.mu.Lock()
defer a.mu.Unlock()
return a.Swift
}
type analysis struct {
State astate
Dependencies []string
GotPointers map[uint64]uint64
SymbolStubs map[uint64]uint64
Helpers map[uint64]uint64
}
// CacheImage represents a dyld dylib image.
type CacheImage struct {
Name string
Index uint32
Info CacheImageInfo
Mappings *cacheMappingsWithSlideInfo
CacheLocalSymbolsEntry64
CacheImageInfoExtra
CacheImageTextInfo
Initializer uint64
DOFSectionAddr uint64
DOFSectionSize uint32
SlideInfo slideInfo
RangeEntries []rangeEntry
PatchableExports []Patch
PatchableGOTs []Patch
LocalSymbols []*CacheLocalSymbol64
PublicSymbols []*Symbol
ObjC objcInfo
Analysis analysis
cache *File // pointer back to the dyld cache that the image belongs to
cuuid types.UUID
CacheReader
m *macho.File
pm *macho.File // partial macho
sinfo map[uint64]uint64
}
// NewCacheReader returns a CacheReader that reads from r
// starting at offset off and stops with EOF after n bytes.
// It also stubs out the MachoReader required SeekToAddr and ReadAtAddr
func NewCacheReader(off int64, n int64, u types.UUID) CacheReader {
return CacheReader{off, off, off + n, u}
}
// CacheReader implements Read, Seek, and ReadAt on a section
// of an underlying ReaderAt.
type CacheReader struct {
base int64
off int64
limit int64
ruuid types.UUID
}
// Free frees the underlying data so the GC can reclaim it.
func (i *CacheImage) Free() {
i.m = nil
i.pm = nil
i.sinfo = nil
i.RangeEntries = nil
i.PatchableExports = nil
i.PatchableGOTs = nil
i.LocalSymbols = nil
i.PublicSymbols = nil
i.ObjC = objcInfo{}
i.Analysis = analysis{}
}
func (i *CacheImage) Read(p []byte) (n int, err error) {
if i.off >= i.limit {
return 0, io.EOF
}
if max := i.limit - i.off; int64(len(p)) > max {
p = p[0:max]
}
n, err = i.cache.r[i.ruuid].ReadAt(p, i.off)
i.off += int64(n)
return
}
func (i *CacheImage) Seek(offset int64, whence int) (int64, error) {
switch whence {
default:
return 0, fmt.Errorf("Seek: invalid whence")
case io.SeekStart:
offset += i.base
case io.SeekCurrent:
offset += i.off
case io.SeekEnd:
offset += i.limit
}
// if scinfo := i.cache.GetSubCacheInfo(i.ruuid); scinfo != nil {
// if offset > int64(scinfo.CacheVMOffset) {
// if next := i.cache.GetNextSubCacheInfo(i.ruuid); next != nil {
// if offset <= int64(next.CacheVMOffset) {
// i.ruuid = next.UUID
// offset = offset - int64(scinfo.CacheVMOffset)
// }
// } else {
// return 0, fmt.Errorf("Seek: invalid offset")
// }
// }
// } else {
if offset < i.base {
return 0, fmt.Errorf("Seek: invalid offset")
}
// }
i.off = offset
return offset - i.base, nil
}
func (i *CacheImage) ReadAt(p []byte, off int64) (n int, err error) {
if i.pm == nil {
i.pm, err = i.GetPartialMacho()
if err != nil {
return -1, err
}
}
if i.pm.Segment("__LINKEDIT") != nil {
i.ruuid, _, err = i.cache.GetOffset(i.pm.Segment("__LINKEDIT").Addr)
} else {
return -1, fmt.Errorf("failed to get __LINKEDIT segment")
}
if err != nil {
return -1, err
}
if off < 0 || off >= i.limit-i.base {
return 0, io.EOF
}
off += i.base
if max := i.limit - off; int64(len(p)) > max {
p = p[0:max]
n, err = i.cache.r[i.ruuid].ReadAt(p, off)
if err == nil {
err = io.EOF
}
return n, err
}
// fmt.Printf("image.ReadAt: cache_uuid=%s, uuid=%s, off=%#x\n", i.cuuid, uuid, off)
return i.cache.r[i.ruuid].ReadAt(p, off)
}
func (i *CacheImage) SeekToAddr(addr uint64) error {
uuid, offset, err := i.cache.GetOffset(addr)
if err != nil {
return err
}
i.ruuid = uuid
i.Seek(int64(offset), io.SeekStart)
return nil
}
// ReadAtAddr reads data at a given virtual address
func (i *CacheImage) ReadAtAddr(buf []byte, addr uint64) (int, error) {
uuid, off, err := i.cache.GetOffset(addr)
if err != nil {
return -1, err
}
i.ruuid = uuid
// fmt.Printf("image.ReadAt: cache_uuid=%s, uuid=%s, off=%#x\n", i.cuuid, uuid, off)
return i.cache.r[i.ruuid].ReadAt(buf, int64(off))
}
// GetOffset returns the offset for a given virtual address
func (i *CacheImage) GetOffset(address uint64) (uint64, error) {
u, o, err := i.cache.GetOffset(address)
if err != nil {
return 0, err
}
i.ruuid = u
// fmt.Printf("prim_uuid=%s, cache_uuid=%s, uuid=%s, off=%#x\n", i.cache.UUID, i.cuuid, u, o)
return o, nil
}
// GetVMAddress returns the virtual address for a given offset
func (i *CacheImage) GetVMAddress(offset uint64) (uint64, error) {
return i.cache.GetVMAddressForUUID(i.cuuid, offset)
}
func (i *CacheImage) SlidePointer(addr uint64) uint64 {
if addr == 0 {
return addr
}
// check if addr is in the cache (not slid)
if _, _, err := i.cache.GetMappingForVMAddress(addr); err == nil {
return addr
}
// try and slide the encoded pointer
return i.cache.SlideInfo.SlidePointer(addr)
}
func (i *CacheImage) relativeSelectorBase() (uint64, error) {
return i.cache.relativeSelectorBase()
}
func (i *CacheImage) partialRelativeSelectorBase() (uint64, error) {
if i.Name == "/usr/lib/libobjc.A.dylib" {
return 0, nil
}
return i.relativeSelectorBase()
}
func (f *File) relativeSelectorBase() (uint64, error) {
f.rsBaseOnce.Do(func() {
if _, err := f.Image("/usr/lib/libobjc.A.dylib"); err != nil {
return
}
opt, err := f.GetOptimizations()
if err != nil {
f.rsBaseErr = err
return
}
f.rsBase = f.relativeSelectorBaseForOptimization(opt)
})
return f.rsBase, f.rsBaseErr
}
func (f *File) relativeSelectorBaseForOptimization(opt Optimization) uint64 {
base := opt.RelativeMethodListsBaseAddress(f.objcOptRoAddr)
if _, ok := opt.(*ObjCOptimizationHeader); ok {
base += f.Headers[f.UUID].SharedRegionStart // TODO: can I trust SharedRegionStart? should this be Mapping[0].Address?
}
return base
}
// GetMacho parses dyld image as a MachO (slow)
func (i *CacheImage) GetMacho() (*macho.File, error) {
if i.m != nil {
return i.m, nil
}
offset, err := i.GetOffset(i.LoadAddress)
if err != nil {
return nil, err
}
rsBase, err := i.relativeSelectorBase()
if err != nil {
return nil, err
}
i.CacheReader = NewCacheReader(0, 1<<63-1, i.cuuid)
vma := types.VMAddrConverter{
Converter: func(addr uint64) uint64 {
return i.SlidePointer(addr)
},
VMAddr2Offet: func(address uint64) (uint64, error) {
return i.GetOffset(address)
},
Offet2VMAddr: func(offset uint64) (uint64, error) {
return i.GetVMAddress(offset)
},
}
i.m, err = macho.NewFile(io.NewSectionReader(i.cache.r[i.cuuid], int64(offset), int64(i.TextSegmentSize)), macho.FileConfig{
Offset: int64(offset),
SectionReader: types.NewCustomSectionReader(i.cache.r[i.cuuid], &vma, 0, 1<<63-1),
CacheReader: i,
VMAddrConverter: vma,
RelativeSelectorBase: rsBase,
})
if err != nil {
return nil, err
}
return i.m, nil
}
// GetPartialMacho parses dyld image as a partial MachO (fast)
func (i *CacheImage) GetPartialMacho() (*macho.File, error) {
if i.pm != nil {
return i.pm, nil
}
offset, err := i.GetOffset(i.LoadAddress)
if err != nil {
return nil, err
}
i.CacheReader = NewCacheReader(0, 1<<63-1, i.cuuid)
var rsBase uint64
if base, err := i.partialRelativeSelectorBase(); err == nil {
rsBase = base
}
vma := types.VMAddrConverter{
Converter: func(addr uint64) uint64 {
return i.SlidePointer(addr)
},
VMAddr2Offet: func(address uint64) (uint64, error) {
return i.GetOffset(address)
},
Offet2VMAddr: func(offset uint64) (uint64, error) {
return i.GetVMAddress(offset)
},
}
i.pm, err = macho.NewFile(io.NewSectionReader(i.cache.r[i.cuuid], int64(offset), int64(i.TextSegmentSize)), macho.FileConfig{
LoadIncluding: []types.LoadCmd{
types.LC_SEGMENT,
types.LC_SEGMENT_64,
types.LC_DYLD_INFO,
types.LC_DYLD_INFO_ONLY,
types.LC_ID_DYLIB,
types.LC_UUID,
types.LC_BUILD_VERSION,
types.LC_SOURCE_VERSION,
types.LC_SUB_FRAMEWORK,
types.LC_SUB_CLIENT,
types.LC_REEXPORT_DYLIB,
types.LC_LOAD_DYLIB,
types.LC_LOAD_WEAK_DYLIB,
types.LC_LOAD_UPWARD_DYLIB},
Offset: int64(offset),
SectionReader: types.NewCustomSectionReader(i.cache.r[i.cuuid], &vma, 0, 1<<63-1),
CacheReader: i,
VMAddrConverter: vma,
RelativeSelectorBase: rsBase,
})
if err != nil {
return nil, err
}
return i.pm, nil
}
// Analyze analyzes an image by parsing it's symbols, stubs and GOT
func (i *CacheImage) Analyze() error {
if err := i.ParseObjC(); err != nil {
log.Errorf("failed to parse objc data for image %s: %v", filepath.Base(i.Name), err)
// return fmt.Errorf("failed to parse objc data for image %s: %v", filepath.Base(i.Name), err) FIXME: should this error out?
}
if err := i.ParsePublicSymbols(false); err != nil {
log.Errorf("failed to parse exported symbols for %s: %w", i.Name, err)
}
if err := i.ParseLocalSymbols(false); err != nil {
if !errors.Is(err, ErrNoLocals) {
return fmt.Errorf("failed to parse local symbols for %s: %w", i.Name, err)
}
}
if !i.cache.IsArm64() {
utils.Indent(log.Warn, 2)("image analysis of stubs and GOT only works on arm64 architectures")
}
if !i.Analysis.State.IsSwiftDone() {
// TODO: add more swift runtime metadata
if i.cache.IsArm64() {
if err := i.ParseSwiftStrings(); err != nil {
return fmt.Errorf("failed to parse swift strings for %s: %w", i.Name, err)
}
}
i.Analysis.State.SetSwift(true)
}
if !i.Analysis.State.IsSlideInfoDone() {
if err := i.ParseSlideInfo(); err != nil {
return fmt.Errorf("failed to parse slide info for %s: %w", i.Name, err)
}
}
if !i.Analysis.State.IsHelpersDone() && i.cache.IsArm64() {
log.Debugf("parsing %s symbol stub helpers", i.Name)
if err := i.ParseHelpers(); err != nil {
if !errors.Is(err, macho.ErrMachOSectionNotFound) {
return fmt.Errorf("failed to parse stub helpers for %s: %w", i.Name, err)
}
}
for start, target := range i.Analysis.Helpers {
if slide, ok := i.sinfo[start]; ok {
target = slide
}
if symName, ok := i.cache.AddressToSymbol.Get(target); ok {
i.cache.AddressToSymbol.Set(start, fmt.Sprintf("%s%s", symbols.PrefixStubHelper, symName))
} else {
i.cache.AddressToSymbol.Set(start, fmt.Sprintf("%s%x", symbols.PrefixStubHelper, target))
}
}
}
if !i.Analysis.State.IsGotDone() && i.cache.IsArm64() {
log.Debugf("parsing %s global offset table", i.Name)
if err := i.ParseGOT(); err != nil {
return fmt.Errorf("failed to parse GOT for %s: %w", i.Name, err)
}
for entry, target := range i.Analysis.GotPointers {
if slide, ok := i.sinfo[entry]; ok {
target = slide
}
if symName, ok := i.cache.AddressToSymbol.Get(target); ok {
i.cache.AddressToSymbol.Set(entry, fmt.Sprintf("%s%s", symbols.PrefixGot, symName))
} else {
if img, err := i.cache.GetImageContainingVMAddr(target); err == nil {
if err := img.Analyze(); err != nil {
// FIXME: return fmt.Errorf("failed parse GOT target %#x: failed to analyze image %s: %w", target, img.Name, err)
log.Errorf("failed parse GOT target %#x: failed to analyze image %s: %w", target, img.Name, err)
}
if symName, ok := i.cache.AddressToSymbol.Get(target); ok {
i.cache.AddressToSymbol.Set(entry, fmt.Sprintf("%s%s", symbols.PrefixGot, symName))
} else if laptr, ok := i.Analysis.GotPointers[target]; ok {
if symName, ok := i.cache.AddressToSymbol.Get(laptr); ok {
i.cache.AddressToSymbol.Set(entry, fmt.Sprintf("%s%s", symbols.PrefixGot, symName))
}
} else {
utils.Indent(log.Debug, 2)(fmt.Sprintf("no sym found for GOT entry %#x => %#x in %s", entry, target, img.Name))
i.cache.AddressToSymbol.Set(entry, fmt.Sprintf("%s%x ; %s", symbols.PrefixGotFallback, target, filepath.Base(img.Name)))
}
} else {
i.cache.AddressToSymbol.Set(entry, fmt.Sprintf("%s%x", symbols.PrefixGotFallback, target))
}
}
}
}
if !i.Analysis.State.IsStubsDone() && i.cache.IsArm64() {
log.Debugf("parsing %s symbol stubs", i.Name)
if err := i.ParseStubs(); err != nil {
return fmt.Errorf("failed to parse stubs for %s: %w", i.Name, err)
}
for stub, target := range i.Analysis.SymbolStubs {
if slide, ok := i.sinfo[stub]; ok {
target = slide
}
if symName, ok := i.cache.AddressToSymbol.Get(target); ok {
if !strings.HasPrefix(symName, symbols.PrefixJump) {
i.cache.AddressToSymbol.Set(stub, symbols.PrefixJump+strings.TrimPrefix(symName, symbols.PrefixStubHelper))
} else {
i.cache.AddressToSymbol.Set(stub, symName)
}
} else {
img, err := i.cache.GetImageContainingVMAddr(target)
if err != nil {
return fmt.Errorf("failed to find image containing stub target %#x: %w", target, err)
}
if err := img.Analyze(); err != nil {
return fmt.Errorf("failed to lookup symbol stub target %#x: failed to analyze image %s: %w", target, img.Name, err)
}
if symName, ok := i.cache.AddressToSymbol.Get(target); ok {
i.cache.AddressToSymbol.Set(stub, fmt.Sprintf("%s%s", symbols.PrefixJump, symName))
} else {
utils.Indent(log.Debug, 2)(fmt.Sprintf("no sym found for stub %#x => %#x in %s", stub, target, img.Name))
i.cache.AddressToSymbol.Set(stub, fmt.Sprintf("%s%x ; %s", symbols.PrefixStubFallback, target, filepath.Base(img.Name)))
}
}
}
}
if !i.Analysis.State.IsStartsDone() {
i.ParseStarts()
}
return nil
}
// ParseSlideInfo parse the shared_cache slide info corresponding to the MachO
func (i *CacheImage) ParseSlideInfo() error {
i.sinfo = make(map[uint64]uint64)
m, err := i.GetPartialMacho()
if err != nil {
return err
}
for _, seg := range m.Segments() {
uuid, mapping, err := i.cache.GetMappingForVMAddress(seg.Addr)
if err != nil {
return err
}
if mapping.SlideInfoOffset == 0 {
continue
}
startAddr := seg.Addr - mapping.Address
endAddr := ((seg.Addr + seg.Memsz) - mapping.Address) + uint64(i.cache.SlideInfo.GetPageSize())
start := startAddr / uint64(i.cache.SlideInfo.GetPageSize())
end := endAddr / uint64(i.cache.SlideInfo.GetPageSize())
rs, err := i.cache.GetRebaseInfoForPages(uuid, mapping, start, end)
if err != nil {
return err
}
for _, r := range rs {
i.sinfo[r.Pointer.Raw()] = r.Target
}
}
i.Analysis.State.SetSlideInfo(true)
return nil
}
// GetSlideInfo returns a slide info map for the image
func (i *CacheImage) GetSlideInfo() (map[uint64]uint64, error) {
if !i.Analysis.State.IsSlideInfoDone() {
if err := i.ParseSlideInfo(); err != nil {
return nil, err
}
}
return i.sinfo, nil
}
// ParseStarts parse function starts in MachO
func (i *CacheImage) ParseStarts() {
if i.m != nil {
for _, fn := range i.m.GetFunctions() {
if ok := i.cache.AddressToSymbol.Has(fn.StartAddr); !ok {
i.cache.AddressToSymbol.Set(fn.StartAddr, fmt.Sprintf("sub_%x", fn.StartAddr))
}
}
}
i.Analysis.State.SetStarts(true)
}
// ParseObjC parse ObjC runtime for MachO image
func (i *CacheImage) ParseObjC() error {
if !i.Analysis.State.IsObjcDone() {
if err := i.cache.CFStringsForImage(i.Name); err != nil {
return fmt.Errorf("failed to parse objc cfstrings for image %s: %v", filepath.Base(i.Name), err)
}
// TODO: add objc methods in the -[Class sel:] form
if err := i.cache.MethodsForImage(i.Name); err != nil {
return fmt.Errorf("failed to parse objc methods for image %s: %v", filepath.Base(i.Name), err)
}
if strings.Contains(i.Name, "libobjc.A.dylib") {
if _, err := i.cache.GetAllObjCSelectors(false); err != nil {
return fmt.Errorf("failed to parse objc all selectors: %v", err)
}
} else {
if err := i.cache.SelectorsForImage(i.Name); err != nil {
return fmt.Errorf("failed to parse objc selectors for image %s: %v", filepath.Base(i.Name), err)
}
}
if err := i.cache.ClassesForImage(i.Name); err != nil {
return fmt.Errorf("failed to parse objc classes for image %s: %v", filepath.Base(i.Name), err)
}
if err := i.cache.CategoriesForImage(i.Name); err != nil {
return fmt.Errorf("failed to parse objc categories for image %s: %v", filepath.Base(i.Name), err)
}
if err := i.cache.ProtocolsForImage(i.Name); err != nil {
return fmt.Errorf("failed to parse objc protocols for image %s: %v", filepath.Base(i.Name), err)
}
if err := i.cache.GetObjCStubsForImage(i.Name); err != nil && !errors.Is(err, macho.ErrObjcSectionNotFound) {
return fmt.Errorf("failed to parse objc stubs for image %s: %v", filepath.Base(i.Name), err)
}
i.Analysis.State.SetObjC(true)
}
return nil
}
// ParseGOT parse global offset table in MachO
func (i *CacheImage) ParseGOT() error {
i.Analysis.GotPointers = make(map[uint64]uint64)
m, err := i.GetPartialMacho()
if err != nil {
return fmt.Errorf("failed to get MachO for image %s; %v", i.Name, err)
}
defer m.Close()
for _, secName := range []string{"__got", "__auth_got", "__auth_ptr"} {
for _, sec := range m.Sections {
if (sec.Seg == "__AUTH_CONST" || sec.Seg == "__DATA_CONST") && sec.Name == secName {
dat := make([]byte, sec.Size)
if _, err := i.ReadAtAddr(dat, sec.Addr); err != nil {
return fmt.Errorf("failed to read GOT section %s: %v", secName, err)
}
ptrs := make([]uint64, sec.Size/8)
if err := binary.Read(bytes.NewReader(dat), binary.LittleEndian, &ptrs); err != nil {
return fmt.Errorf("failed to read %s.%s got pointers; %v", sec.Seg, sec.Name, err)
}
for idx, ptr := range ptrs {
if ptr != 0 {
i.Analysis.GotPointers[sec.Addr+uint64(idx*8)] = i.cache.SlideInfo.SlidePointer(ptr)
}
}
}
}
}
i.Analysis.State.SetGot(true)
return nil
}
// ParseStubs parse symbol stubs in MachO
func (i *CacheImage) ParseStubs() error {
m, err := i.GetPartialMacho()
if err != nil {
return fmt.Errorf("failed to get MachO for image %s; %v", i.Name, err)
}
i.Analysis.SymbolStubs = make(map[uint64]uint64)
for _, sec := range m.Sections {
if sec.Flags.IsSymbolStubs() {
dat := make([]byte, sec.Size)
if _, err := i.ReadAtAddr(dat, sec.Addr); err != nil {
return err
}
stubs, err := disass.ParseStubsASM(dat, sec.Addr, func(u uint64) (uint64, error) {
return i.cache.ReadPointerAtAddress(u)
})
if err != nil {
return err
}
for k, v := range stubs {
i.Analysis.SymbolStubs[k] = i.cache.SlideInfo.SlidePointer(v)
}
}
}
i.Analysis.State.SetStubs(true)
return nil
}
// ResolveStubAtAddr checks if addr is in a stub section (__stubs, __auth_stubs)
// and if so, uses ParseStubs to find the target, then resolves it to a symbol.
// Returns the target address and symbol name, or an error if not a stub or unresolvable.
func (i *CacheImage) ResolveStubAtAddr(addr uint64) (uint64, string, error) {
if !i.Analysis.State.IsStubsDone() {
if err := i.ParseStubs(); err != nil {
return 0, "", err
}
}
target, ok := i.Analysis.SymbolStubs[addr]
if !ok {
return 0, "", fmt.Errorf("address %#x is not a known stub", addr)
}
// Resolve target symbol: search the target image's symtab + local symbols
if img, err := i.cache.GetImageContainingTextAddr(target); err == nil {
tm, err := img.GetMacho()
if err == nil {
defer tm.Close()
if syms, err := tm.FindAddressSymbols(target); err == nil {
for _, s := range syms {
if s.Name != "<redacted>" && s.Name != "" {
return target, s.Name, nil
}
}
}
if name, err := img.FindLocalSymbolAtAddr(target); err == nil && name != "" {
return target, name, nil
}
}
}
return target, "", fmt.Errorf("stub at %#x -> %#x: target symbol not found", addr, target)
}
// ParseHelpers parse symbol stub helpers in MachO
func (i *CacheImage) ParseHelpers() error {
m, err := i.GetPartialMacho()
if err != nil {
return fmt.Errorf("failed to get MachO for image %s; %v", i.Name, err)
}
i.Analysis.Helpers, err = disass.ParseHelpersASM(m)
if err != nil {
return err
}
i.Analysis.State.SetHelpers(true)
return nil
}
func (i *CacheImage) ParseSwiftStrings() error {
m, err := i.GetPartialMacho()
if err != nil {
return fmt.Errorf("failed to get MachO for image %s; %v", i.Name, err)
}
text := m.Section("__TEXT", "__text")
if text == nil {
return fmt.Errorf("no __TEXT.__text section found")
}
data, err := text.Data()
if err != nil {
return fmt.Errorf("failed to get __TEXT.__text data: %v", err)
}
engine := disass.NewMachoDisass(m, &disass.Config{
Data: data,
StartAddress: text.Addr,
Middle: text.Addr + text.Size,
})
strs, err := engine.FindSwiftStrings()
if err != nil {
return fmt.Errorf("failed to find swift strings: %v", err)
}
for addr, str := range strs {
if len(str) > 0 {
i.cache.AddressToSymbol.Set(addr, fmt.Sprintf("%v", str))
}
}
return nil
}
// FindLocalSymbolAtAddr searches only this image's DSC local symbol nlist entries
// for a symbol at the given address, without populating the full a2s cache.
// Returns the symbol name or an error if not found.
func (i *CacheImage) FindLocalSymbolAtAddr(addr uint64) (string, error) {
var uuid types.UUID
if i.cache.IsDyld4 {
uuid = i.cache.symUUID
} else {
uuid = i.cache.UUID
}
if i.cache.Headers[uuid].LocalSymbolsOffset == 0 {
return "", fmt.Errorf("no local symbols")
}
nlistCount := int(i.cache.Images[i.Index].NlistCount)
if nlistCount == 0 {
return "", fmt.Errorf("no local symbols for image")
}
// Compute direct offset to this image's nlist entries using NlistStartIndex
nlistOffset := int64(i.cache.LocalSymInfo.NListFileOffset) +
int64(i.cache.Images[i.Index].NlistStartIndex)*nlist64Size
// Bulk-read all nlist entries for this image
nlistBuf := make([]byte, nlistCount*nlist64Size)
if _, err := i.cache.r[uuid].ReadAt(nlistBuf, nlistOffset); err != nil {
return "", fmt.Errorf("failed to read nlist entries: %w", err)
}
// Scan for matching address
strPoolBase := int64(i.cache.LocalSymInfo.StringsFileOffset)
strPoolSize := int64(i.cache.LocalSymInfo.StringsSize)
for n := range nlistCount {
off := n * nlist64Size
nameIdx, value := parseNlist64(nlistBuf[off:])
if value == addr {
s, _, err := readStringPool(i.cache.r[uuid], strPoolBase, strPoolSize, int64(nameIdx), nil)
if err != nil {
return "", err
}
return s, nil
}
}
return "", fmt.Errorf("no local symbol at %#x", addr)
}
// ParseLocalSymbols parses and caches, with the option to dump, all the local/private symbols for an image
func (i *CacheImage) ParseLocalSymbols(dump bool) error {
if !i.Analysis.State.IsPrivatesDone() {
var uuid types.UUID
if i.cache.IsDyld4 {
uuid = i.cache.symUUID
} else {
uuid = i.cache.UUID
}
if i.cache.Headers[uuid].LocalSymbolsOffset == 0 {
i.Analysis.State.SetPrivates(true) // TODO: does this have any bad side-effects ?
return fmt.Errorf("failed to parse local syms for image %s: %w", filepath.Base(i.Name), ErrNoLocals)
}
nlistCount := int(i.cache.Images[i.Index].NlistCount)
if nlistCount == 0 {
i.Analysis.State.SetPrivates(true)
return nil
}
// Compute direct offset to this image's nlist entries using NlistStartIndex
nlistOffset := int64(i.cache.LocalSymInfo.NListFileOffset) +
int64(i.cache.Images[i.Index].NlistStartIndex)*nlist64Size
// Bulk-read all nlist entries for this image at once
nlistBuf := make([]byte, nlistCount*nlist64Size)
if _, err := i.cache.r[uuid].ReadAt(nlistBuf, nlistOffset); err != nil {
return fmt.Errorf("failed to read nlist entries for %s: %w", filepath.Base(i.Name), err)
}
// Read strings from string pool (reuse buffer across iterations)
strPoolBase := int64(i.cache.LocalSymInfo.StringsFileOffset)
strPoolSize := int64(i.cache.LocalSymInfo.StringsSize)
var (
strBuf = make([]byte, 512)
s string
readErr error
)
for n := range nlistCount {
off := n * nlist64Size
nameIdx, value := parseNlist64(nlistBuf[off:])
s, strBuf, readErr = readStringPool(i.cache.r[uuid], strPoolBase, strPoolSize, int64(nameIdx), strBuf)
if readErr != nil {
log.Errorf("failed to read local symbol name for image %s: %v", filepath.Base(i.Name), readErr)
continue
}
nlist := types.Nlist64{}
nlist.Name = nameIdx
nlist.Type = types.NType(nlistBuf[off+4])
nlist.Sect = nlistBuf[off+5]
nlist.Desc = types.NDescType(binary.LittleEndian.Uint16(nlistBuf[off+6:]))
nlist.Value = value
i.cache.AddressToSymbol.Set(value, s)
i.cache.Images[i.Index].LocalSymbols = append(i.cache.Images[i.Index].LocalSymbols, &CacheLocalSymbol64{
Name: s,
Nlist64: nlist,
FoundInDylib: i.Name,
})
if dump {
m, err := i.GetPartialMacho()
if err != nil {
return err
}
fmt.Println(CacheLocalSymbol64{
Name: s,
Nlist64: nlist,
Macho: m,
FoundInDylib: filepath.Base(i.Name),
}.String(true))
}
}
sort.Slice(i.LocalSymbols, func(j, k int) bool {
return i.LocalSymbols[j].Name < i.LocalSymbols[k].Name
})
i.Analysis.State.SetPrivates(true)
}
return nil
}
// GetLocalSymbol returns the local symbol matching the given name
func (i *CacheImage) GetLocalSymbol(name string) (*CacheLocalSymbol64, error) {
i.ParseLocalSymbols(false)
idx := sort.Search(len(i.LocalSymbols), func(idx int) bool { return i.LocalSymbols[idx].Name >= name })
if idx < len(i.LocalSymbols) && i.LocalSymbols[idx].Name == name {
return i.LocalSymbols[idx], nil
}
return nil, fmt.Errorf("local symbol %s not found in image %s", name, filepath.Base(i.Name))
}
// GetLocalSymbolsAsMachoSymbols converts all the dylibs private symbols into MachO symtab public symbols
func (i *CacheImage) GetLocalSymbolsAsMachoSymbols() []macho.Symbol {
var syms []macho.Symbol
for _, lsym := range i.LocalSymbols {
syms = append(syms, macho.Symbol{
Name: lsym.Name,
Type: lsym.Type,
Sect: lsym.Sect,
Desc: lsym.Desc,
Value: lsym.Value,
})
}
return syms
}
// ParsePublicSymbols parses and caches, with the option to dump, all the exports, symtab and dyld_info symbols in the image/dylib
func (i *CacheImage) ParsePublicSymbols(dump bool) error {
if i.Analysis.State.BeginExports() {
defer func() {
if !i.Analysis.State.IsExportsDone() {
i.Analysis.State.FinishExports(false)
}
}()
var w *tabwriter.Writer
if dump {
w = tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', 0)
}
// try to parse exports from the cache's export trie or the dylib's LC_DYLD_EXPORTS_TRIE
if syms, err := i.cache.GetExportTrieSymbols(i); err == nil {
for _, sym := range syms {
if err := i.resolveReExport(&sym); err != nil {
return err
}
if dump {
fmt.Fprintf(w, "%s\n", sym)
} else {
i.cache.AddressToSymbol.Set(sym.Address, sym.Name)
i.PublicSymbols = append(i.PublicSymbols, &Symbol{
Name: sym.Name,
Address: sym.Address,
Type: sym.Type(),
Kind: EXPORT,
})
}
}
if dump {
w.Flush()
}
}
// try to parse the dylib's symbol table
m, err := i.GetMacho()
if err != nil {
return err
}
for _, sym := range m.Symtab.Syms {
if sym.Name == "<redacted>" {
continue
}
// TODO: Handle ReExports
var sec string
if sym.Sect > 0 && int(sym.Sect) <= len(m.Sections) {
sec = fmt.Sprintf("%s.%s", m.Sections[sym.Sect-1].Seg, m.Sections[sym.Sect-1].Name)
}
if dump {
fmt.Fprintf(w, "%#09x:\t(%s)\t%s\n", sym.Value, sym.Type.String(sec), sym.Name)
} else {
i.cache.AddressToSymbol.Set(sym.Value, sym.Name)
i.PublicSymbols = append(i.PublicSymbols, &Symbol{
Name: sym.Name,
Address: sym.Value,
Type: sym.Type.String(sec),
Kind: SYMTAB,
})
}
}
if dump {
w.Flush()
}
// try to parse LC_DYLD_INFO binds
if binds, err := m.GetBindInfo(); err == nil {
for _, bind := range binds {
if dump {
fmt.Fprintf(w, "%#09x:\t(%s.%s|from %s)\t%s\n", bind.Start+bind.SegOffset, bind.Segment, bind.Section, bind.Dylib, bind.Name)
} else {
i.cache.AddressToSymbol.Set(bind.Start+bind.SegOffset, bind.Name)
i.PublicSymbols = append(i.PublicSymbols, &Symbol{
Name: bind.Name,
Address: bind.Start + bind.SegOffset,
Type: fmt.Sprintf("%s|%s", bind.Kind, bind.Dylib),
Kind: BIND,
})
}
}
if dump {
w.Flush()
}
}
// try to parse LC_DYLD_INFO rebases TODO: this is slide info and not sym info
// if rebases, err := m.GetRebaseInfo(); err == nil {
// for _, rebase := range rebases {
// rebase.
// }
// }
// try to parse LC_DYLD_INFO exports TODO: is this redundant???
if exports, err := m.GetExports(); err == nil {
for _, export := range exports {
if err := i.resolveReExport(&export); err != nil {
return err
}
if dump {
fmt.Fprintf(w, "%s\n", export)
} else {
i.cache.AddressToSymbol.Set(export.Address, export.Name)
i.PublicSymbols = append(i.PublicSymbols, &Symbol{
Name: export.Name,
Address: export.Address,
Type: export.Type(),
Kind: EXPORT,
})
}
}
if dump {
w.Flush()
}
}
sort.Slice(i.PublicSymbols, func(j, k int) bool {
return i.PublicSymbols[j].Name < i.PublicSymbols[k].Name
})
i.Analysis.State.FinishExports(true)
}
return nil
}
// returns the public symbol matching the given name
func (i *CacheImage) GetPublicSymbol(name string) (*Symbol, error) {
if err := i.ParsePublicSymbols(false); err != nil {
return nil, err
}
if !i.Analysis.State.IsExportsDone() {
for _, sym := range i.PublicSymbols {
if sym.Name == name {
return sym, nil
}
}
return nil, fmt.Errorf("public symbols for image %s are still being parsed", filepath.Base(i.Name))
}
idx := sort.Search(len(i.PublicSymbols), func(idx int) bool { return i.PublicSymbols[idx].Name >= name })
if idx < len(i.PublicSymbols) && i.PublicSymbols[idx].Name == name {
return i.PublicSymbols[idx], nil
}
return nil, fmt.Errorf("public symbol %s not found in image %s", name, filepath.Base(i.Name))
}
func (i *CacheImage) resolveReExport(export *trie.TrieExport) error {
if !export.Flags.ReExport() {
return nil
}
m, err := i.GetPartialMacho()
if err != nil {
return err
}
export.FoundInDylib, err = reexportLibraryName(m.ImportedLibraries(), export.Other)
if err != nil {
return err
}
if len(export.ReExport) == 0 {
return nil
}
reimg, err := i.cache.Image(export.FoundInDylib)
if err != nil {
return err
}
if resym, err := reimg.GetPublicSymbol(export.ReExport); err == nil {
export.Address = resym.Address
}
return nil
}
func reexportLibraryName(importedLibraries []string, ordinal uint64) (string, error) {
if ordinal == 0 || ordinal > uint64(len(importedLibraries)) {
return "", fmt.Errorf("re-export ordinal %d outside imported library table with %d entries", ordinal, len(importedLibraries))
}
return importedLibraries[int(ordinal)-1], nil
}
// GetExport returns the trie export symbol matching the given name
func (i *CacheImage) GetExport(symbol string) (*trie.TrieExport, error) {
var eTrieAddr, eTrieSize uint64
if i.CacheImageInfoExtra.ExportsTrieAddr > 0 {
eTrieAddr = i.CacheImageInfoExtra.ExportsTrieAddr
eTrieSize = uint64(i.CacheImageInfoExtra.ExportsTrieSize)
} else {
m, err := i.GetMacho()
if err != nil {
return nil, fmt.Errorf("failed to parse MachO for image %s: %v", filepath.Base(i.Name), err)
}
if m.DyldExportsTrie() != nil {
return m.GetDyldExport(symbol)
} else if m.DyldInfo() != nil {
eTrieAddr, _ = i.GetVMAddress(uint64(m.DyldInfo().ExportOff))
eTrieSize = uint64(m.DyldInfo().ExportSize)
} else {
return nil, fmt.Errorf("failed to get export trie data for image %s: %w", filepath.Base(i.Name), ErrNoExportTrieInMachO)
}
}
uuid, eTrieOffset, err := i.cache.GetOffset(eTrieAddr)
if err != nil {
return nil, fmt.Errorf("failed to get offset of export trie addr")
}
sr := io.NewSectionReader(i.cache.r[uuid], 0, 1<<63-1)
if _, err := sr.Seek(int64(eTrieOffset), io.SeekStart); err != nil {
return nil, fmt.Errorf("failed to seek to export trie offset in cache: %v", err)
}
exportTrie := make([]byte, eTrieSize)
if err := binary.Read(sr, i.cache.ByteOrder, &exportTrie); err != nil {
return nil, fmt.Errorf("failed to read export trie data: %v", err)
}
r := bytes.NewReader(exportTrie)
if _, err = trie.WalkTrie(r, symbol); err != nil {
return nil, err
}
return trie.ReadExport(r, symbol, i.LoadAddress)
}
// GetSymbol retuns a Symbol private or public matching a given name
func (i *CacheImage) GetSymbol(name string) (*Symbol, error) {
// check local symbols
if lsym, err := i.GetLocalSymbol(name); err == nil {
m, err := i.GetPartialMacho()
if err != nil {
return nil, err
}
var sec string
if lsym.Sect > 0 && int(lsym.Sect) <= len(m.Sections) {
sec = fmt.Sprintf("%s.%s", m.Sections[lsym.Sect-1].Seg, m.Sections[lsym.Sect-1].Name)
}
return &Symbol{
Name: lsym.Name,
Address: lsym.Value,
Type: lsym.Type.String(sec),
Image: i.Name,
Kind: LOCAL,
}, nil
}
// check public symbols
if sym, err := i.GetPublicSymbol(name); err == nil {
sym.Image = i.Name
return sym, nil
} else {
return nil, err
}
}