package cpp import ( "encoding/binary" "fmt" "slices" "sort" "strings" "unicode/utf8" "github.com/blacktop/arm64-cgo/disassemble" "github.com/blacktop/go-macho" "github.com/blacktop/go-macho/types" ) func normalizeEntryID(entry string) string { switch strings.TrimSpace(entry) { case "", "kernel", "__kernel__", kernelBundleName: return kernelBundleName default: return entry } } func validKernelPointer(ptr uint64) bool { return ptr >= kernelAddrFloor } func validMetaPointer(ptr uint64) bool { return ptr >= kernelAddrFloor && ptr%8 == 0 } func addSignedOffset(addr uint64, offset int64) (uint64, bool) { if offset >= 0 { return addr + uint64(offset), true } neg := uint64(-offset) if neg > addr { return 0, false } return addr - neg, true } func decodeBLTarget(addr uint64, instr uint32) (uint64, bool) { if (instr >> 26) != 0b100101 { return 0, false } imm26 := int64(instr & 0x03ffffff) if imm26&(1<<25) != 0 { imm26 |= ^int64(0x03ffffff) } return addSignedOffset(addr, imm26<<2) } func decodeBTarget(addr uint64, instr uint32) (uint64, bool) { if (instr >> 26) != 0b000101 { return 0, false } imm26 := int64(instr & 0x03ffffff) if imm26&(1<<25) != 0 { imm26 |= ^int64(0x03ffffff) } return addSignedOffset(addr, imm26<<2) } func decodeADRPImmediate(pc uint64, raw uint32) (uint64, bool) { immlo := int64((raw >> 29) & 0x3) immhi := int64((raw >> 5) & 0x7ffff) imm := (immhi << 2) | immlo if (imm & (int64(1) << 20)) != 0 { imm |= ^((int64(1) << 21) - 1) } offset := imm << 12 base := pc & ^uint64(0xfff) if offset >= 0 { return base + uint64(offset), true } return base - uint64(-offset), true } func isArm64Nop(instr uint32) bool { switch instr { case 0xd503201f, 0xd503205f, 0xd503207f, 0xd50320bf, 0xd50320ff: return true default: return false } } func isPacibsp(raw uint32) bool { return raw == 0xd503237f } func (s *Scanner) decodeArm64Instruction(addr uint64, raw uint32, inst *disassemble.Inst) error { return s.decoder.DecomposeInto(addr, raw, inst) } func operandCount(inst *disassemble.Inst) int { if inst == nil { return 0 } return int(inst.NumOps) } func instPtr(ok bool, inst *disassemble.Inst) *disassemble.Inst { if !ok { return nil } return inst } func operandRegisterCount(op *disassemble.Op) int { if op == nil { return 0 } return int(op.NumRegisters) } func operandRegister(op *disassemble.Op, idx int) (disassemble.Register, bool) { if idx < 0 || idx >= operandRegisterCount(op) { return 0, false } return op.Registers[idx], true } func operandHasRegister(op *disassemble.Op, reg disassemble.Register) bool { if op == nil { return false } for idx := 0; idx < int(op.NumRegisters); idx++ { if op.Registers[idx] == reg { return true } } return false } func trackedStaticAddress(regBase [31]uint64, op *disassemble.Op) (uint64, bool) { if op == nil { return 0, false } if op.Class == disassemble.LABEL { return op.GetImmediate(), true } baseReg, ok := operandRegister(op, 0) if !ok { return 0, false } baseIdx, ok := registerToIndex(baseReg) if !ok || baseIdx >= 31 || regBase[baseIdx] == 0 { return 0, false } switch op.Class { case disassemble.MEM_OFFSET, disassemble.MEM_PRE_IDX, disassemble.MEM_POST_IDX: return addSignedOffset(regBase[baseIdx], int64(op.GetImmediate())) default: return 0, false } } func operandIsSPPreIndex(op *disassemble.Op) bool { baseReg, ok := operandRegister(op, 0) return ok && op.Class == disassemble.MEM_PRE_IDX && baseReg == disassemble.REG_SP } func isFrameSetupInstruction(inst *disassemble.Inst) bool { if inst == nil { return false } switch inst.Operation { case disassemble.ARM64_PACIBSP: return true case disassemble.ARM64_STP: if operandCount(inst) < 3 { return false } return operandHasRegister(&inst.Operands[0], disassemble.REG_X29) && operandHasRegister(&inst.Operands[1], disassemble.REG_X30) && operandIsSPPreIndex(&inst.Operands[2]) case disassemble.ARM64_SUB: if operandCount(inst) < 3 { return false } return operandHasRegister(&inst.Operands[0], disassemble.REG_SP) && operandHasRegister(&inst.Operands[1], disassemble.REG_SP) && inst.Operands[2].Class == disassemble.IMM64 && inst.Operands[2].GetImmediate() > 0 default: return false } } type staticValueTrackOptions struct { acceptAnyLoadAddr bool propagateLoadAddrInAdd bool handleLoadPairs bool } func (s *Scanner) trackStaticValueInstruction(owner *macho.File, regBase, regLoadAddr, regValue *[31]uint64, ins *disassemble.Inst, opts staticValueTrackOptions) { if ins == nil || operandCount(ins) == 0 { return } switch ins.Operation { case disassemble.ARM64_ADD: dstReg, dstOK := operandRegister(&ins.Operands[0], 0) srcReg, srcOK := operandRegister(&ins.Operands[1], 0) if operandCount(ins) < 3 || !dstOK || !srcOK { return } dstIdx, dstOK := registerToIndex(dstReg) srcIdx, srcOK := registerToIndex(srcReg) if !dstOK || !srcOK || dstIdx >= 31 || srcIdx >= 31 { return } srcBase := regBase[srcIdx] srcLoadAddr := regLoadAddr[srcIdx] srcValue := regValue[srcIdx] regBase[dstIdx], regLoadAddr[dstIdx], regValue[dstIdx] = 0, 0, 0 if srcBase != 0 { if addr, ok := addSignedOffset(srcBase, int64(ins.Operands[2].GetImmediate())); ok { regBase[dstIdx] = addr regValue[dstIdx] = addr } return } if opts.propagateLoadAddrInAdd && srcLoadAddr != 0 { if addr, ok := addSignedOffset(srcLoadAddr, int64(ins.Operands[2].GetImmediate())); ok { regLoadAddr[dstIdx] = addr } return } if srcValue != 0 { if value, ok := addSignedOffset(srcValue, int64(ins.Operands[2].GetImmediate())); ok { regValue[dstIdx] = value } } case disassemble.ARM64_MOV: dstReg, dstOK := operandRegister(&ins.Operands[0], 0) if !dstOK { return } dstIdx, dstOK := registerToIndex(dstReg) if !dstOK || dstIdx >= 31 { return } regBase[dstIdx], regLoadAddr[dstIdx], regValue[dstIdx] = 0, 0, 0 if operandCount(ins) <= 1 { return } srcReg, srcOK := operandRegister(&ins.Operands[1], 0) if !srcOK { regValue[dstIdx] = ins.Operands[1].GetImmediate() return } srcIdx, srcOK := registerToIndex(srcReg) if !srcOK || srcIdx >= 31 { return } regBase[dstIdx] = regBase[srcIdx] regLoadAddr[dstIdx] = regLoadAddr[srcIdx] regValue[dstIdx] = regValue[srcIdx] case disassemble.ARM64_ORR: dstReg, dstOK := operandRegister(&ins.Operands[0], 0) if operandCount(ins) < 3 || !dstOK { return } dstIdx, dstOK := registerToIndex(dstReg) if !dstOK || dstIdx >= 31 { return } regBase[dstIdx], regLoadAddr[dstIdx], regValue[dstIdx] = 0, 0, 0 reg1, reg1OK := operandRegister(&ins.Operands[1], 0) reg2, reg2OK := operandRegister(&ins.Operands[2], 0) switch { case reg1OK && reg2OK && (reg1 == disassemble.REG_XZR || reg1 == disassemble.REG_WZR): if srcIdx, ok := registerToIndex(reg2); ok && srcIdx < 31 { regBase[dstIdx] = regBase[srcIdx] regLoadAddr[dstIdx] = regLoadAddr[srcIdx] regValue[dstIdx] = regValue[srcIdx] } case reg1OK && reg2OK && (reg2 == disassemble.REG_XZR || reg2 == disassemble.REG_WZR): if srcIdx, ok := registerToIndex(reg1); ok && srcIdx < 31 { regBase[dstIdx] = regBase[srcIdx] regLoadAddr[dstIdx] = regLoadAddr[srcIdx] regValue[dstIdx] = regValue[srcIdx] } } case disassemble.ARM64_LDR, disassemble.ARM64_LDUR: dstReg, dstOK := operandRegister(&ins.Operands[0], 0) if operandCount(ins) < 2 || !dstOK { return } dstIdx, dstOK := registerToIndex(dstReg) if !dstOK || dstIdx >= 31 { return } regBase[dstIdx], regLoadAddr[dstIdx], regValue[dstIdx] = 0, 0, 0 addr, ok := trackedStaticAddress(*regBase, &ins.Operands[1]) if !ok { return } regLoadAddr[dstIdx] = addr if ptr, ok := s.resolvePointerAt(owner, addr); ok { regValue[dstIdx] = ptr return } if opts.acceptAnyLoadAddr || validKernelPointer(addr) { regValue[dstIdx] = addr } case disassemble.ARM64_LDP: if !opts.handleLoadPairs { return } if operandCount(ins) < 3 { return } if reg, ok := operandRegister(&ins.Operands[0], 0); ok { if dstIdx, ok := registerToIndex(reg); ok && dstIdx < 31 { regBase[dstIdx], regLoadAddr[dstIdx], regValue[dstIdx] = 0, 0, 0 } } if reg, ok := operandRegister(&ins.Operands[1], 0); ok { if dstIdx, ok := registerToIndex(reg); ok && dstIdx < 31 { regBase[dstIdx], regLoadAddr[dstIdx], regValue[dstIdx] = 0, 0, 0 } } addr, ok := trackedStaticAddress(*regBase, &ins.Operands[2]) if !ok { return } if reg, ok := operandRegister(&ins.Operands[0], 0); ok { if dstIdx, ok := registerToIndex(reg); ok && dstIdx < 31 { regLoadAddr[dstIdx] = addr if ptr, ok := s.resolvePointerAt(owner, addr); ok { regValue[dstIdx] = ptr } else if opts.acceptAnyLoadAddr || validKernelPointer(addr) { regValue[dstIdx] = addr } } } if reg, ok := operandRegister(&ins.Operands[1], 0); ok { if dstIdx, ok := registerToIndex(reg); ok && dstIdx < 31 { if addr2, ok := addSignedOffset(addr, 8); ok { regLoadAddr[dstIdx] = addr2 if ptr, ok := s.resolvePointerAt(owner, addr2); ok { regValue[dstIdx] = ptr } else if opts.acceptAnyLoadAddr || validKernelPointer(addr2) { regValue[dstIdx] = addr2 } } } } } } func findFunctionStartInSection(sectionAddr uint64, data []byte, targetAddr uint64) (uint64, error) { if targetAddr < sectionAddr || targetAddr >= sectionAddr+uint64(len(data)) { return 0, fmt.Errorf("target %#x not inside section [%#x, %#x)", targetAddr, sectionAddr, sectionAddr+uint64(len(data))) } cursor := int(targetAddr - sectionAddr) cursor -= cursor % 4 const maxScan = 256 var decoder disassemble.Decoder for steps := 0; steps < maxScan && cursor >= 0; steps++ { if cursor+4 > len(data) { break } raw := binary.LittleEndian.Uint32(data[cursor : cursor+4]) addr := sectionAddr + uint64(cursor) if isPacibsp(raw) { return addr, nil } var inst disassemble.Inst if err := decoder.DecomposeInto(addr, raw, &inst); err == nil && isFrameSetupInstruction(&inst) { return addr, nil } cursor -= 4 } return 0, fmt.Errorf("no prologue found before %#x", targetAddr) } func (s *Scanner) includeEntry(entry string) bool { if len(s.cfg.Entries) == 0 { return true } norm := normalizeEntryID(entry) for _, candidate := range s.cfg.Entries { want := normalizeEntryID(candidate) if norm == want || strings.EqualFold(entry, candidate) || strings.HasSuffix(strings.ToLower(norm), strings.ToLower(candidate)) { return true } } return false } func (s *Scanner) buildTargets() ([]scanTarget, error) { targets := make([]scanTarget, 0, 1+len(s.root.FileSets())) if s.root.FileHeader.Type != types.MH_FILESET { // Non-fileset: the root file IS the kernel. if s.includeEntry(kernelBundleName) { targets = append(targets, scanTarget{file: s.root, entryID: kernelBundleName}) } return targets, nil } // MH_FILESET: each fileset entry (including com.apple.kernel) // is a separate Mach-O scoped to its own code. Using the root // file as a kernel target would scan every kext's functions and // __mod_init_func pointers, mis-attributing them to com.apple.kernel. for _, fs := range s.root.FileSets() { entryID := normalizeEntryID(fs.EntryID) if !s.includeEntry(entryID) { continue } m, err := s.root.GetFileSetFileByName(fs.EntryID) if err != nil { return nil, fmt.Errorf("open fileset entry %s: %w", fs.EntryID, err) } s.fileEntries[m] = entryID targets = append(targets, scanTarget{file: m, entryID: entryID}) } return targets, nil } func (s *Scanner) buildVMRangeIndex() { ranges := make([]vmRangeOwner, 0, len(s.root.Sections)+len(s.root.Segments())) appendFileRanges := func(m *macho.File) { if m == nil { return } for _, seg := range m.Segments() { if seg == nil || seg.Filesz == 0 || seg.Addr == 0 { continue } ranges = append(ranges, vmRangeOwner{ start: seg.Addr, end: seg.Addr + seg.Memsz, file: m, }) } for _, sec := range m.Sections { if sec == nil || sec.Size == 0 { continue } ranges = append(ranges, vmRangeOwner{ start: sec.Addr, end: sec.Addr + sec.Size, file: m, }) } } for _, target := range s.targets { if target.file == nil || target.file == s.root { continue } appendFileRanges(target.file) } appendFileRanges(s.root) sort.SliceStable(ranges, func(i, j int) bool { if ranges[i].start != ranges[j].start { return ranges[i].start < ranges[j].start } return ranges[i].end > ranges[j].end }) s.vmRanges = ranges } func (s *Scanner) fileForVMAddr(addr uint64) *macho.File { if s.root == nil { return nil } if len(s.vmRanges) == 0 { s.buildVMRangeIndex() } idx := sort.Search(len(s.vmRanges), func(i int) bool { return s.vmRanges[i].start > addr }) for i := idx - 1; i >= 0; i-- { rng := s.vmRanges[i] if rng.end <= addr { break } if rng.start <= addr && addr < rng.end { return rng.file } } return nil } func (s *Scanner) pointerLookupOwner(owner *macho.File, addr uint64) (*macho.File, pointerOwnerSource) { if owner != nil && fileOwnsVMAddr(owner, addr) { return owner, pointerOwnerSourceProvided } if s.root != nil { if resolved := s.fileForVMAddr(addr); resolved != nil { return resolved, pointerOwnerSourceIndexed } } if owner != nil { return owner, pointerOwnerSourceOwnerFallback } return s.root, pointerOwnerSourceRootFallback } func (s *Scanner) entryForFile(m *macho.File) string { if entry, ok := s.fileEntries[m]; ok { return entry } if m == s.root { return kernelBundleName } return "" } func (s *Scanner) readSectionData(m *macho.File, sec *types.Section) ([]byte, error) { key := sectionKey{file: m, addr: sec.Addr} if data, ok := s.sectionData[key]; ok { return data, nil } data, err := sec.Data() if err != nil { return nil, err } s.sectionData[key] = data return data, nil } func (s *Scanner) functionsForFile(m *macho.File) ([]types.Function, error) { if funcs, ok := s.functions[m]; ok { return funcs, nil } funcs := m.GetFunctions() if len(funcs) == 0 { var err error funcs, err = m.GenerateFunctionStarts() if err != nil { return nil, err } } sort.Slice(funcs, func(i, j int) bool { return funcs[i].StartAddr < funcs[j].StartAddr }) s.functions[m] = funcs return funcs, nil } func (s *Scanner) functionForAddr(m *macho.File, addr uint64) (types.Function, error) { funcs, err := s.functionsForFile(m) if err == nil { idx := sort.Search(len(funcs), func(i int) bool { return funcs[i].StartAddr > addr }) if idx > 0 { fn := funcs[idx-1] if addr >= fn.StartAddr && addr < fn.EndAddr { return fn, nil } } } sec := m.FindSectionForVMAddr(addr) if sec == nil { return types.Function{}, fmt.Errorf("address %#x not inside executable section", addr) } data, err := s.readSectionData(m, sec) if err != nil { return types.Function{}, err } start, err := findFunctionStartInSection(sec.Addr, data, addr) if err != nil { return types.Function{}, err } funcs, err = s.functionsForFile(m) if err != nil { return types.Function{}, err } end := sec.Addr + sec.Size idx := sort.Search(len(funcs), func(i int) bool { return funcs[i].StartAddr > start }) if idx < len(funcs) { end = funcs[idx].StartAddr } fn := types.Function{StartAddr: start, EndAddr: end} funcs = append(funcs, fn) sort.Slice(funcs, func(i, j int) bool { return funcs[i].StartAddr < funcs[j].StartAddr }) s.functions[m] = slices.CompactFunc(funcs, func(a, b types.Function) bool { return a.StartAddr == b.StartAddr && a.EndAddr == b.EndAddr }) delete(s.callerIndex, m) delete(s.pointerIndex, m) return fn, nil } func (s *Scanner) functionForAddrInAnyFile(preferred *macho.File, addr uint64) (types.Function, *macho.File, error) { if preferred != nil { if fn, err := s.functionForAddr(preferred, addr); err == nil { return fn, preferred, nil } } if owner := s.fileForVMAddr(addr); owner != nil && owner != preferred { if fn, err := s.functionForAddr(owner, addr); err == nil { return fn, owner, nil } } return types.Function{}, nil, fmt.Errorf("address %#x not inside known function", addr) } func (s *Scanner) functionDataFor(m *macho.File, fn types.Function) ([]byte, error) { key := fileFuncKey{file: m, start: fn.StartAddr} if data, ok := s.functionData[key]; ok { return data, nil } if fn.EndAddr > fn.StartAddr { if sec := m.FindSectionForVMAddr(fn.StartAddr); sec != nil { if fn.EndAddr <= sec.Addr+sec.Size { if data, err := s.readSectionData(m, sec); err == nil { start := int(fn.StartAddr - sec.Addr) end := int(fn.EndAddr - sec.Addr) if start >= 0 && end <= len(data) && start < end { window := data[start:end:end] s.functionData[key] = window return window, nil } } } } } data, err := m.GetFunctionData(fn) if err != nil { return nil, err } s.functionData[key] = data return data, nil } func getCStringFromAny(root *macho.File, owner *macho.File, addr uint64) (string, error) { if owner != nil { if str, err := owner.GetCString(addr); err == nil && str != "" { return str, nil } } return root.GetCString(addr) } func (s *Scanner) cachedCStringAt(addr uint64) (string, error) { if addr == 0 { return "", fmt.Errorf("zero cstring address") } if cached, ok := s.nameStrings[addr]; ok { if !cached.ok { return "", fmt.Errorf("cached cstring miss at %#x", addr) } return cached.value, nil } value, err := getCStringFromAny(s.root, s.fileForVMAddr(addr), addr) s.nameStrings[addr] = cachedCString{value: value, ok: err == nil && value != ""} return value, err } func looksLikeRecoveredClassName(name string) bool { if name == "" || !utf8.ValidString(name) { return false } first := true hasUpper := false leadingUnderscore := false for _, r := range name { if r < 0x20 || r == 0x7f { return false } if first { first = false leadingUnderscore = r == '_' } switch { case r >= 'a' && r <= 'z': case r >= 'A' && r <= 'Z': hasUpper = true case r >= '0' && r <= '9': case r == '_', r == ':', r == '<', r == '>', r == '*', r == '·': default: return false } } return hasUpper || leadingUnderscore || looksLikeRecoveredLowercaseClassName(name) } func looksLikeRecoveredLowercaseClassName(name string) bool { switch { case strings.HasPrefix(name, "com_"): return true case strings.HasSuffix(name, "_t"): return true case name == "cache": return true case strings.HasSuffix(name, "_init"), strings.HasSuffix(name, "_bootstrap"), strings.HasSuffix(name, "_event"): return false default: return false } } func recoveredClassNameScore(name string) int { if name == "" { return 0 } if strings.HasPrefix(name, "UnknownClass_") { return 1 } if !looksLikeRecoveredClassName(name) { return 1 } score := 2 if strings.ContainsAny(name, "ABCDEFGHIJKLMNOPQRSTUVWXYZ:<>*·") { score++ } switch { case strings.HasSuffix(name, "_init"), strings.HasSuffix(name, "_bootstrap"), strings.HasSuffix(name, "_event"): score-- } if score < 1 { return 1 } return score } func hasStrongClassEvidence(class discoveredClass) bool { return class.SuperMeta != 0 || class.MetaVtableAddr != 0 || class.VtableAddr != 0 } func fileOwnsVMAddr(m *macho.File, addr uint64) bool { for _, seg := range m.Segments() { if seg == nil || seg.Memsz == 0 { continue } if seg.Addr <= addr && addr < seg.Addr+seg.Memsz { return true } } for _, sec := range m.Sections { if sec.Addr <= addr && addr < sec.Addr+sec.Size { return true } } return false } func (s *Scanner) clearDiscoveryCaches() { clear(s.functionData) clear(s.sectionData) clear(s.callerIndex) clear(s.callsiteCtx) clear(s.getMetaCands) clear(s.metaPtrInfer) clear(s.metaPtrBusy) clear(s.staticCalls) } func (s *Scanner) resolvePointerAtReason(owner *macho.File, addr uint64, reason pointerReason) (uint64, bool) { stats := &s.stats.pointerReasons[reason] stats.lookups++ // Fast path: check owner and root forward caches before doing // any expensive owner lookup. if owner != nil { if fwd := s.forwardPointers[owner]; fwd != nil { if ptr, ok := fwd[addr]; ok { s.stats.ptrCacheHits++ stats.forwardHits++ stats.successes++ return ptr, true } } } if s.root != nil && s.root != owner { if fwd := s.forwardPointers[s.root]; fwd != nil { if ptr, ok := fwd[addr]; ok { s.stats.ptrCacheHits++ stats.forwardHits++ stats.successes++ return ptr, true } } } // After root fixup seeding, all chained fixup pointers are in the // per-file forward caches. Skip expensive owner lookup and pread. if s.rootFixupsSeeded { s.stats.ptrCacheMisses++ stats.misses++ return 0, false } // Slow path: find the owner via linear scan and fall through to // pread if no cache entry exists (only reached before fixup seed). lookupOwner, ownerSource := s.pointerLookupOwner(owner, addr) if ownerSource == pointerOwnerSourceIndexed { stats.ownerIndexHits++ } if lookupOwner != nil && lookupOwner != owner && lookupOwner != s.root { if fwd := s.forwardPointers[lookupOwner]; fwd != nil { if ptr, ok := fwd[addr]; ok { s.stats.ptrCacheHits++ stats.forwardHits++ stats.successes++ return ptr, true } } } s.stats.ptrCacheMisses++ if lookupOwner != nil && s.forwardPointers[lookupOwner] != nil && pointerCacheCoversAddress(lookupOwner, addr) { stats.misses++ return 0, false } if lookupOwner != nil && lookupOwner != owner && s.forwardPointers[owner] != nil && pointerCacheCoversAddress(owner, addr) { stats.misses++ return 0, false } if lookupOwner != nil && s.root != nil && lookupOwner != s.root && s.forwardPointers[s.root] != nil && fileOwnsVMAddr(s.root, addr) && pointerCacheCoversAddress(s.root, addr) { stats.misses++ return 0, false } if lookupOwner != nil { stats.slidAttempts++ if ptr, err := lookupOwner.GetSlidPointerAtAddress(addr); err == nil && validKernelPointer(ptr) { stats.successes++ return ptr, true } stats.rawAttempts++ if ptr, err := lookupOwner.GetPointerAtAddress(addr); err == nil && validKernelPointer(ptr) { stats.successes++ return ptr, true } } if owner != nil && owner != lookupOwner { stats.slidAttempts++ if ptr, err := owner.GetSlidPointerAtAddress(addr); err == nil && validKernelPointer(ptr) { stats.successes++ return ptr, true } stats.rawAttempts++ if ptr, err := owner.GetPointerAtAddress(addr); err == nil && validKernelPointer(ptr) { stats.successes++ return ptr, true } } if s.root != nil && s.root != lookupOwner && s.root != owner { stats.slidAttempts++ if ptr, err := s.root.GetSlidPointerAtAddress(addr); err == nil && validKernelPointer(ptr) { stats.successes++ return ptr, true } stats.rawAttempts++ if ptr, err := s.root.GetPointerAtAddress(addr); err == nil && validKernelPointer(ptr) { stats.successes++ return ptr, true } } stats.misses++ return 0, false } func (s *Scanner) repairClassesFromCallsite(classes []discoveredClass) { for i := range classes { owner := classes[i].file if owner == nil { owner = s.fileForVMAddr(classes[i].Ctor) classes[i].file = owner } if owner == nil || classes[i].Ctor == 0 { continue } if validMetaPointer(classes[i].SuperMeta) && classes[i].MetaVtableAddr != 0 { continue } ctx, ok := s.recoverCallsiteContext(owner, &classes[i]) if !ok { continue } if classes[i].MetaPtr == 0 && validKernelPointer(ctx.x0) { classes[i].MetaPtr = ctx.x0 } if !validMetaPointer(classes[i].SuperMeta) { if super := s.recoverSuperMetaFromCtorPattern(owner, &classes[i]); validMetaPointer(super) { classes[i].SuperMeta = super } } if !validMetaPointer(classes[i].SuperMeta) { if super := s.normalizeLoadedPointer(owner, ctx.x2); validMetaPointer(super) { classes[i].SuperMeta = super } } if classes[i].Size == 0 && ctx.x3 > 0 && ctx.x3 <= 0xffffffff { classes[i].Size = uint32(ctx.x3) } if classes[i].MetaVtableAddr == 0 && validKernelPointer(ctx.metaVtab) { classes[i].MetaVtableAddr = ctx.metaVtab } } } // recoverSuperMetaFromModInit fills in missing SuperMeta values by // statically analyzing mod_init_func wrapper functions. Each wrapper // sets up x0 (metaclass ptr) and x2 (parent metaclass ptr) before // calling the constructor. When x2 is loaded from a GOT slot // (cross-kext parent), the pointer is resolved via chained fixups. func (s *Scanner) recoverSuperMetaFromModInit(classes []discoveredClass) { needsRecovery := false for i := range classes { if classes[i].SuperMeta == 0 && classes[i].MetaPtr != 0 { needsRecovery = true break } } if !needsRecovery { return } metaPtrToIdx := make(map[uint64]int, len(classes)) for i := range classes { if classes[i].MetaPtr != 0 && classes[i].SuperMeta == 0 { metaPtrToIdx[classes[i].MetaPtr] = i } } for _, target := range s.targets { if len(metaPtrToIdx) == 0 { break } ptrs, err := s.modInitPointers(target.file) if err != nil { continue } for _, ptr := range ptrs { x0, x2 := s.extractModInitPair(target.file, ptr) if x0 == 0 || x2 == 0 { continue } idx, ok := metaPtrToIdx[x0] if !ok { continue } if validMetaPointer(x2) { classes[idx].SuperMeta = x2 delete(metaPtrToIdx, x0) } } } } // extractModInitPair statically traces a mod_init_func wrapper to // extract the x0 (metaclass ptr) and x2 (parent metaclass ptr) // values at the last BL/BRAA call that has both set. When x2 is // loaded from a GOT slot, the chained fixup pointer is resolved. func (s *Scanner) extractModInitPair(owner *macho.File, funcAddr uint64) (uint64, uint64) { buf := make([]byte, 80*4) if _, err := s.root.ReadAtAddr(buf, funcAddr); err != nil { return 0, 0 } type regState struct { adrpBase uint64 computed uint64 loadAddr uint64 isLoad bool } var regs [31]regState var bestX0, bestX2 uint64 for i := 0; i+4 <= len(buf); i += 4 { pc := funcAddr + uint64(i) raw := readUint32At(buf, i) // ADRP if (raw & 0x9f000000) == 0x90000000 { rd := int(raw & 0x1f) if rd < 31 { if addr, ok := decodeADRPImmediate(pc, raw); ok { regs[rd] = regState{adrpBase: addr} } } continue } // ADD imm (64-bit, sf=1) if (raw & 0xff800000) == 0x91000000 { rd := int(raw & 0x1f) rn := int((raw >> 5) & 0x1f) imm12 := uint64((raw >> 10) & 0xfff) if (raw>>22)&1 == 1 { imm12 <<= 12 } if rd < 31 && rn < 31 && regs[rn].adrpBase != 0 { regs[rd] = regState{computed: regs[rn].adrpBase + imm12} } continue } // LDR unsigned offset (64-bit) if (raw & 0xffc00000) == 0xf9400000 { rt := int(raw & 0x1f) rn := int((raw >> 5) & 0x1f) imm12 := uint64((raw>>10)&0xfff) * 8 if rt < 31 && rn < 31 { var la uint64 if regs[rn].adrpBase != 0 { la = regs[rn].adrpBase + imm12 } else if regs[rn].computed != 0 { la = regs[rn].computed + imm12 } regs[rt] = regState{loadAddr: la, isLoad: true} } continue } // MOV Xd, Xm (ORR Xd, XZR, Xm) if (raw & 0xffe0ffe0) == 0xaa0003e0 { rd := int(raw & 0x1f) rm := int((raw >> 16) & 0x1f) if rd < 31 && rm < 31 { regs[rd] = regs[rm] } continue } // MOVZ (64-bit) if (raw & 0xff800000) == 0xd2800000 { rd := int(raw & 0x1f) if rd < 31 { regs[rd] = regState{} } continue } // RET if raw == 0xd65f03c0 || raw == 0xd65f0fff { break } // BL or BRAA/BLRAA - capture register state isBL := (raw >> 26) == 0b100101 isAuthBranch := (raw&0xfffff800) == 0xd71f0800 || (raw&0xfffff800) == 0xd63f0800 if !isBL && !isAuthBranch { continue } // x0: prefer computed (ADRP+ADD), fall back to adrpBase var x0 uint64 if regs[0].computed != 0 { x0 = regs[0].computed } else if regs[0].adrpBase != 0 { x0 = regs[0].adrpBase } // x2: if loaded from GOT, resolve the pointer var x2 uint64 if regs[2].computed != 0 { x2 = regs[2].computed } else if regs[2].adrpBase != 0 && !regs[2].isLoad { x2 = regs[2].adrpBase } else if regs[2].loadAddr != 0 { if ptr, ok := s.resolvePointerAtReason(owner, regs[2].loadAddr, pointerReasonX2LoadRecovery); ok { x2 = ptr } } if x0 != 0 && x2 != 0 { bestX0, bestX2 = x0, x2 } } return bestX0, bestX2 } func (s *Scanner) validateSuperMeta(classes []discoveredClass) { known := make(map[uint64]bool, len(classes)) for i := range classes { if classes[i].MetaPtr != 0 { known[classes[i].MetaPtr] = true } } for i := range classes { if validMetaPointer(classes[i].SuperMeta) { continue } owner := classes[i].file if owner == nil { owner = s.fileForVMAddr(classes[i].Ctor) } if recovered := s.recoverSuperMetaFromCtorPattern(owner, &classes[i]); validMetaPointer(recovered) { classes[i].SuperMeta = recovered continue } if ctx, ok := s.recoverCallsiteContext(owner, &classes[i]); ok { if super := s.normalizeLoadedPointer(owner, ctx.x2); validMetaPointer(super) { classes[i].SuperMeta = super continue } } if classes[i].SuperMeta != 0 && known[classes[i].SuperMeta] { continue } classes[i].SuperMeta = 0 } } func (s *Scanner) normalizeLoadedPointer(owner *macho.File, value uint64) uint64 { if validKernelPointer(value) || value == 0 { return value } // Try root first — in a fileset cache, the root MH_FILESET // decodes chained fixup binds to canonical kernel addresses, // while individual entry handles may resolve to local offsets. if s.root != nil { if slid := s.root.SlidePointer(value); validKernelPointer(slid) { return slid } } if owner != nil && owner != s.root { if slid := owner.SlidePointer(value); validKernelPointer(slid) { return slid } } return value } func pointerCacheCoversAddress(m *macho.File, addr uint64) bool { if m == nil { return false } for _, sec := range m.Sections { if sec == nil || sec.Size < 8 { continue } if sec.Addr > addr || addr >= sec.Addr+sec.Size { continue } if sec.Seg == "__TEXT" || sec.Seg == "__TEXT_EXEC" { return false } if strings.HasSuffix(sec.Name, "__bss") || strings.HasSuffix(sec.Name, "__common") { return false } return true } return false } func (s *Scanner) resolvePointerAt(owner *macho.File, addr uint64) (uint64, bool) { return s.resolvePointerAtReason(owner, addr, pointerReasonOther) } func (s *Scanner) fallbackPointerAt(owner *macho.File, addr uint64) (uint64, bool) { lookupOwner, _ := s.pointerLookupOwner(owner, addr) if lookupOwner == nil { return 0, false } // Fast path: check forward pointer cache built by buildPointerIndex. if fwd := s.forwardPointers[lookupOwner]; fwd != nil { if ptr, ok := fwd[addr]; ok { return ptr, true } } if lookupOwner != owner { if fwd := s.forwardPointers[owner]; fwd != nil { if ptr, ok := fwd[addr]; ok { return ptr, true } } } if s.root != nil && lookupOwner != s.root { if fwd := s.forwardPointers[s.root]; fwd != nil { if ptr, ok := fwd[addr]; ok { return ptr, true } } } // After root fixup seeding, all chained fixup pointers are in the // forward cache. Any address not found is genuinely absent. if s.rootFixupsSeeded { return 0, false } // Skip slow I/O path only for addresses covered by the warmed cache. if s.forwardPointers[lookupOwner] != nil && pointerCacheCoversAddress(lookupOwner, addr) { return 0, false } if lookupOwner != owner && s.forwardPointers[owner] != nil && pointerCacheCoversAddress(owner, addr) { return 0, false } if s.root != nil && lookupOwner != s.root && s.forwardPointers[s.root] != nil && fileOwnsVMAddr(s.root, addr) && pointerCacheCoversAddress(s.root, addr) { return 0, false } // Slow path: I/O via go-macho (only reached before cache warm). if ptr, err := lookupOwner.GetSlidPointerAtAddress(addr); err == nil && validKernelPointer(ptr) { return ptr, true } if ptr, err := lookupOwner.GetPointerAtAddress(addr); err == nil && validKernelPointer(ptr) { return ptr, true } if owner != nil && owner != lookupOwner { if ptr, err := owner.GetSlidPointerAtAddress(addr); err == nil && validKernelPointer(ptr) { return ptr, true } if ptr, err := owner.GetPointerAtAddress(addr); err == nil && validKernelPointer(ptr) { return ptr, true } } if s.root != nil && s.root != lookupOwner && s.root != owner { if ptr, err := s.root.GetSlidPointerAtAddress(addr); err == nil && validKernelPointer(ptr) { return ptr, true } if ptr, err := s.root.GetPointerAtAddress(addr); err == nil && validKernelPointer(ptr) { return ptr, true } } return 0, false } // isStubFor checks whether stubAddr is a small auth stub (ADRP+ADD+BR // or ADRP+LDR+BR) that ultimately branches to targetFunc. func (s *Scanner) isStubFor(m *macho.File, stubAddr, targetFunc uint64) bool { if stubAddr == 0 || targetFunc == 0 { return false } var buf [12]byte owner := m if s.root != nil { if _, err := s.root.ReadAtAddr(buf[:], stubAddr); err != nil { if owner == nil { return false } if _, err := owner.ReadAtAddr(buf[:], stubAddr); err != nil { return false } } } else if owner != nil { if _, err := owner.ReadAtAddr(buf[:], stubAddr); err != nil { return false } } else { return false } i0 := binary.LittleEndian.Uint32(buf[0:4]) i1 := binary.LittleEndian.Uint32(buf[4:8]) // First instruction must be ADRP Xd, #page if i0&0x9F000000 != 0x90000000 { return false } rd := i0 & 0x1F immlo := (i0 >> 29) & 0x3 immhi := (i0 >> 5) & 0x7FFFF imm := int64((uint64(immhi)<<2 | uint64(immlo)) << 12) if imm&(1<<32) != 0 { imm |= ^int64((1 << 33) - 1) } page := (stubAddr &^ 0xFFF) + uint64(imm) // Second instruction: ADD Xd, Xd, #imm12 or LDR Xd, [Xd, #imm12] if i1&0xFFC00000 == 0x91000000 && (i1&0x1F) == rd && ((i1>>5)&0x1F) == rd { // ADD immediate addImm := uint64((i1 >> 10) & 0xFFF) if (i1>>22)&1 != 0 { addImm <<= 12 } target := page + addImm return target == targetFunc } if i1&0xFFC00000 == 0xF9400000 && (i1&0x1F) == rd && ((i1>>5)&0x1F) == rd { // LDR X, [X, #imm12] (unsigned offset, scale 8) ldrOff := uint64((i1>>10)&0xFFF) * 8 gotSlot := page + ldrOff ptr, ok := s.resolvePointerAtReason(m, gotSlot, pointerReasonVtableStub) if !ok { return false } return ptr == targetFunc } return false } func (s *Scanner) recoverSuperMetaFromCtorPattern(m *macho.File, class *discoveredClass) uint64 { if class == nil || class.Ctor == 0 { return 0 } fn, owner, err := s.functionForAddrInAnyFile(m, class.Ctor) if err != nil { return 0 } data, err := s.functionDataFor(owner, fn) if err != nil { return 0 } callOffset := int(class.Ctor - fn.StartAddr) if callOffset < 0 || callOffset >= len(data) { return 0 } start := max(callOffset-32*4, 0) var regBase [31]uint64 var regLoadAddr [31]uint64 for i := start; i+4 <= callOffset; i += 4 { pc := fn.StartAddr + uint64(i) raw := readUint32At(data, i) if (raw & 0x9f000000) == 0x90000000 { rd := int(raw & 0x1f) if rd < 31 { if addr, ok := decodeADRPImmediate(pc, raw); ok { regBase[rd] = addr regLoadAddr[rd] = 0 } } continue } var ins disassemble.Inst if err := s.decodeArm64Instruction(pc, raw, &ins); err != nil { continue } switch ins.Operation { case disassemble.ARM64_ADD: dstReg, dstOK := operandRegister(&ins.Operands[0], 0) srcReg, srcOK := operandRegister(&ins.Operands[1], 0) if operandCount(&ins) < 3 || !dstOK || !srcOK { continue } dstIdx, dstOk := registerToIndex(dstReg) srcIdx, srcOk := registerToIndex(srcReg) if dstOk && srcOk && dstIdx < 31 && srcIdx < 31 { srcBase := regBase[srcIdx] regLoadAddr[dstIdx] = 0 regBase[dstIdx] = 0 if srcBase != 0 { if addr, ok := addSignedOffset(srcBase, int64(ins.Operands[2].GetImmediate())); ok { regBase[dstIdx] = addr } } } case disassemble.ARM64_SUB, disassemble.ARM64_ADR: if dstReg, ok := operandRegister(&ins.Operands[0], 0); operandCount(&ins) > 0 && ok { if dstIdx, ok := registerToIndex(dstReg); ok && dstIdx < 31 { regBase[dstIdx] = 0 regLoadAddr[dstIdx] = 0 } } case disassemble.ARM64_MOV: dstReg, dstOK := operandRegister(&ins.Operands[0], 0) if operandCount(&ins) < 2 || !dstOK { continue } dstIdx, dstOK := registerToIndex(dstReg) if !dstOK || dstIdx >= 31 { continue } regBase[dstIdx] = 0 regLoadAddr[dstIdx] = 0 srcReg, srcOK := operandRegister(&ins.Operands[1], 0) if !srcOK { continue } srcIdx, srcOK := registerToIndex(srcReg) if !srcOK || srcIdx >= 31 { continue } regBase[dstIdx] = regBase[srcIdx] regLoadAddr[dstIdx] = regLoadAddr[srcIdx] case disassemble.ARM64_ORR: dstReg, dstOK := operandRegister(&ins.Operands[0], 0) if operandCount(&ins) < 3 || !dstOK { continue } dstIdx, dstOK := registerToIndex(dstReg) if !dstOK || dstIdx >= 31 { continue } regBase[dstIdx] = 0 regLoadAddr[dstIdx] = 0 reg1, reg1OK := operandRegister(&ins.Operands[1], 0) reg2, reg2OK := operandRegister(&ins.Operands[2], 0) switch { case reg1OK && reg2OK && (reg1 == disassemble.REG_XZR || reg1 == disassemble.REG_WZR): if srcIdx, ok := registerToIndex(reg2); ok && srcIdx < 31 { regBase[dstIdx] = regBase[srcIdx] regLoadAddr[dstIdx] = regLoadAddr[srcIdx] } case reg1OK && reg2OK && (reg2 == disassemble.REG_XZR || reg2 == disassemble.REG_WZR): if srcIdx, ok := registerToIndex(reg1); ok && srcIdx < 31 { regBase[dstIdx] = regBase[srcIdx] regLoadAddr[dstIdx] = regLoadAddr[srcIdx] } } case disassemble.ARM64_LDR, disassemble.ARM64_LDUR: dstReg, dstOK := operandRegister(&ins.Operands[0], 0) if operandCount(&ins) < 2 || !dstOK { continue } dstIdx, dstOK := registerToIndex(dstReg) if !dstOK || dstIdx >= 31 { continue } regBase[dstIdx] = 0 regLoadAddr[dstIdx] = 0 if addr, ok := trackedStaticAddress(regBase, &ins.Operands[1]); ok { regLoadAddr[dstIdx] = addr } case disassemble.ARM64_LDP: if operandCount(&ins) < 3 { continue } if reg, ok := operandRegister(&ins.Operands[0], 0); ok { if dstIdx, ok := registerToIndex(reg); ok && dstIdx < 31 { regBase[dstIdx] = 0 regLoadAddr[dstIdx] = 0 } } if reg, ok := operandRegister(&ins.Operands[1], 0); ok { if dstIdx, ok := registerToIndex(reg); ok && dstIdx < 31 { regBase[dstIdx] = 0 regLoadAddr[dstIdx] = 0 } } addr, ok := trackedStaticAddress(regBase, &ins.Operands[2]) if !ok { continue } if reg, ok := operandRegister(&ins.Operands[0], 0); ok { if dstIdx, ok := registerToIndex(reg); ok && dstIdx < 31 { regLoadAddr[dstIdx] = addr } } if reg, ok := operandRegister(&ins.Operands[1], 0); ok { if dstIdx, ok := registerToIndex(reg); ok && dstIdx < 31 { if addr2, ok := addSignedOffset(addr, 8); ok { regLoadAddr[dstIdx] = addr2 } } } } } if regLoadAddr[2] != 0 { if ptr, ok := s.resolvePointerAtReason(owner, regLoadAddr[2], pointerReasonX2LoadRecovery); ok { return ptr } } if validKernelPointer(regBase[2]) { return regBase[2] } return 0 } func readUint32At(data []byte, offset int) uint32 { return binary.LittleEndian.Uint32(data[offset : offset+4]) } func (s *Scanner) inferMetaPtrFromDirectCallers(owner *macho.File, target uint64) uint64 { return s.inferMetaPtrFromDirectCallersDepth(owner, target, 0) } func (s *Scanner) inferMetaPtrFromDirectCallersDepth(owner *macho.File, target uint64, depth int) uint64 { if owner == nil || target == 0 || depth > s.cfg.MaxWrapperDepth { return 0 } s.stats.inferCalls++ if depth > s.stats.inferMaxDepth { s.stats.inferMaxDepth = depth } key := metaInferKey{file: owner, addr: target, depth: depth} if cached, ok := s.metaPtrInfer[key]; ok && cached.ok { s.stats.inferCacheHits++ return cached.value } if s.metaPtrBusy[key] { s.stats.inferBusyHits++ return 0 } s.metaPtrBusy[key] = true defer delete(s.metaPtrBusy, key) index, err := s.directCallerIndex(owner) if err != nil { s.metaPtrInfer[key] = cachedMetaPtr{ok: true} return 0 } for _, callerStart := range index[target] { fn, err := s.functionForAddr(owner, callerStart) if err != nil { continue } metaPtr, found := s.metaPtrAtDirectCall(owner, fn, target) if !found { continue } if validKernelPointer(metaPtr) { return metaPtr } if inferred := s.inferMetaPtrFromDirectCallersDepth(owner, callerStart, depth+1); validKernelPointer(inferred) { s.metaPtrInfer[key] = cachedMetaPtr{value: inferred, ok: true} return inferred } } s.metaPtrInfer[key] = cachedMetaPtr{ok: true} return 0 } func (s *Scanner) directCallerIndex(m *macho.File) (map[uint64][]uint64, error) { if index, ok := s.callerIndex[m]; ok { return index, nil } funcs, err := s.functionsForFile(m) if err != nil { return nil, err } index := make(map[uint64][]uint64) for _, fn := range funcs { data, err := s.functionDataFor(m, fn) if err != nil { continue } for off := 0; off+4 <= len(data); off += 4 { pc := fn.StartAddr + uint64(off) raw := readUint32At(data, off) if target, ok := decodeBLTarget(pc, raw); ok && target != 0 { index[target] = append(index[target], fn.StartAddr) continue } if target, ok := decodeBTarget(pc, raw); ok && target != 0 { index[target] = append(index[target], fn.StartAddr) } } } for target, callers := range index { slices.Sort(callers) index[target] = slices.Compact(callers) } s.callerIndex[m] = index return index, nil } func (s *Scanner) metaPtrAtDirectCall(owner *macho.File, fn types.Function, target uint64) (uint64, bool) { data, err := s.functionDataFor(owner, fn) if err != nil { s.stats.metaPtrDirectMisses++ return 0, false } var regBase [31]uint64 var regValue [31]uint64 for i := 0; i+4 <= len(data); i += 4 { pc := fn.StartAddr + uint64(i) raw := readUint32At(data, i) // BL / B to target → return tracked x0. if callTarget, ok := decodeBLTarget(pc, raw); ok && callTarget == target { s.stats.metaPtrDirectHits++ return metaPtrResult(regBase, regValue), true } if callTarget, ok := decodeBTarget(pc, raw); ok && callTarget == target { s.stats.metaPtrDirectHits++ return metaPtrResult(regBase, regValue), true } trackRegisterRawWithReason(s, owner, raw, pc, ®Base, ®Value, pointerReasonMetaPtrDirectCall) } s.stats.metaPtrDirectMisses++ return 0, false } func metaPtrResult(regBase, regValue [31]uint64) uint64 { if validKernelPointer(regValue[0]) { return regValue[0] } if validKernelPointer(regBase[0]) { return regBase[0] } return 0 } // trackRegisterRaw performs register tracking using raw ARM64 // instruction bitmasks, avoiding CGo entirely. It handles the // instruction subset needed by metaPtrAtDirectCall and similar // callers: ADRP, ADD(imm), LDR(uoff/reg), LDUR, LDP, MOV, ORR, SUB(imm). func trackRegisterRaw(s *Scanner, owner *macho.File, raw uint32, pc uint64, regBase, regValue *[31]uint64) bool { return trackRegisterRawWithReason(s, owner, raw, pc, regBase, regValue, pointerReasonOther) } func trackRegisterRawWithReason(s *Scanner, owner *macho.File, raw uint32, pc uint64, regBase, regValue *[31]uint64, reason pointerReason) bool { switch { // ADRP: sf 1 0000 immlo immhi Rd case (raw & 0x9f000000) == 0x90000000: rd := int(raw & 0x1f) if rd < 31 { if addr, ok := decodeADRPImmediate(pc, raw); ok { regBase[rd] = addr regValue[rd] = 0 } } // ADR: sf 0 0000 immlo immhi Rd case (raw & 0x9f000000) == 0x10000000: rd := int(raw & 0x1f) if rd < 31 { immhi := int64((raw >> 5) & 0x7ffff) immlo := int64((raw >> 29) & 0x3) offset := (immhi << 2) | immlo if offset&(1<<20) != 0 { offset |= ^int64((1 << 21) - 1) } addr := uint64(int64(pc) + offset) regBase[rd] = addr regValue[rd] = addr } // ADD (immediate, 64-bit): 1 00 10001 sh imm12 Rn Rd case (raw & 0xff000000) == 0x91000000: rd := int(raw & 0x1f) rn := int((raw >> 5) & 0x1f) if rd < 31 && rn < 31 { imm := uint64((raw >> 10) & 0xfff) if (raw>>22)&1 == 1 { imm <<= 12 } srcBase := regBase[rn] regBase[rd] = 0 regValue[rd] = 0 if srcBase != 0 { regBase[rd] = srcBase + imm regValue[rd] = srcBase + imm } else if regValue[rn] != 0 { regValue[rd] = regValue[rn] + imm } } // SUB (immediate, 64-bit): 1 10 10001 sh imm12 Rn Rd case (raw & 0xff000000) == 0xd1000000: rd := int(raw & 0x1f) if rd < 31 { regBase[rd] = 0 regValue[rd] = 0 } // ORR (shifted register, 64-bit): 1 01 01010 sh 0 Rm imm6 Rn Rd // MOV register is ORR Xd, XZR, Xm (Rn == XZR, shift == 0, imm6 == 0) case (raw & 0xff200000) == 0xaa000000: rd := int(raw & 0x1f) rn := int((raw >> 5) & 0x1f) rm := int((raw >> 16) & 0x1f) imm6 := (raw >> 10) & 0x3f if rd < 31 { regBase[rd] = 0 regValue[rd] = 0 if rn == 31 && imm6 == 0 && rm < 31 { // MOV Xd, Xm regBase[rd] = regBase[rm] regValue[rd] = regValue[rm] } else if rm == 31 && imm6 == 0 && rn < 31 { // MOV Xd, Xn (rare but valid) regBase[rd] = regBase[rn] regValue[rd] = regValue[rn] } } // MOVZ (32/64): sf 10 100101 hw imm16 Rd case (raw & 0x7f800000) == 0x52800000: rd := int(raw & 0x1f) if rd < 31 { imm := uint64((raw >> 5) & 0xffff) hw := (raw >> 21) & 0x3 regBase[rd] = 0 regValue[rd] = imm << (hw * 16) if (raw>>31)&1 == 0 { regValue[rd] &= 0xffff_ffff } } // MOVK (32/64): sf 11 100101 hw imm16 Rd case (raw & 0x7f800000) == 0x72800000: rd := int(raw & 0x1f) if rd < 31 { imm := uint64((raw >> 5) & 0xffff) hw := (raw >> 21) & 0x3 shift := hw * 16 mask := uint64(0xffff) << shift regBase[rd] = 0 regValue[rd] = (regValue[rd] &^ mask) | (imm << shift) if (raw>>31)&1 == 0 { regValue[rd] &= 0xffff_ffff } } // LDR (unsigned offset, 64-bit): 11 111 00101 imm12 Rn Rt case (raw & 0xffc00000) == 0xf9400000: rt := int(raw & 0x1f) rn := int((raw >> 5) & 0x1f) if rt < 31 && rn < 31 { imm := uint64((raw>>10)&0xfff) * 8 regBase[rt] = 0 regValue[rt] = 0 base := regBase[rn] if base == 0 { base = regValue[rn] } if base != 0 { addr := base + imm if ptr, ok := s.resolvePointerAtReason(owner, addr, reason); ok { regValue[rt] = ptr } else if validKernelPointer(addr) { regValue[rt] = addr } } } // LDR (unsigned offset, 32-bit): 10 111 00101 imm12 Rn Rt case (raw & 0xffc00000) == 0xb9400000: rt := int(raw & 0x1f) rn := int((raw >> 5) & 0x1f) if rt < 31 && rn < 31 { imm := uint64((raw>>10)&0xfff) * 4 regBase[rt] = 0 regValue[rt] = 0 base := regBase[rn] if base == 0 { base = regValue[rn] } if base != 0 { addr := base + imm if ptr, ok := s.resolvePointerAtReason(owner, addr, reason); ok { regValue[rt] = ptr } else if validKernelPointer(addr) { regValue[rt] = addr } } } // LDUR (64-bit): 11 111000 010 imm9 00 Rn Rt case (raw & 0xffe00c00) == 0xf8400000: rt := int(raw & 0x1f) rn := int((raw >> 5) & 0x1f) if rt < 31 && rn < 31 { imm9 := int64((raw >> 12) & 0x1ff) if imm9&(1<<8) != 0 { imm9 |= ^int64((1 << 9) - 1) } regBase[rt] = 0 regValue[rt] = 0 base := regBase[rn] if base == 0 { base = regValue[rn] } if base != 0 { addr := uint64(int64(base) + imm9) if ptr, ok := s.resolvePointerAtReason(owner, addr, reason); ok { regValue[rt] = ptr } else if validKernelPointer(addr) { regValue[rt] = addr } } } // LDP (signed offset, 64-bit): x0 101 0 010 1 imm7 Rt2 Rn Rt case (raw & 0x7fc00000) == 0xa9400000: rt := int(raw & 0x1f) rn := int((raw >> 5) & 0x1f) rt2 := int((raw >> 10) & 0x1f) if rn < 31 { imm7 := int64((raw >> 15) & 0x7f) if imm7&(1<<6) != 0 { imm7 |= ^int64((1 << 7) - 1) } base := regBase[rn] if base == 0 { base = regValue[rn] } addr := uint64(0) if base != 0 { addr = uint64(int64(base) + imm7*8) } if rt < 31 { regBase[rt] = 0 regValue[rt] = 0 if addr != 0 { if ptr, ok := s.resolvePointerAtReason(owner, addr, reason); ok { regValue[rt] = ptr } else if validKernelPointer(addr) { regValue[rt] = addr } } } if rt2 < 31 { regBase[rt2] = 0 regValue[rt2] = 0 if addr != 0 { addr2 := addr + 8 if ptr, ok := s.resolvePointerAtReason(owner, addr2, reason); ok { regValue[rt2] = ptr } else if validKernelPointer(addr2) { regValue[rt2] = addr2 } } } } default: return false } return true } // batchStaticCallContexts does a single linear pass through fn, // tracking register state and capturing wrapperContext at every // BL/B to an OSMetaClass variant. O(function_size) total instead // of O(N × function_size) when called per-callsite. func (s *Scanner) batchStaticCallContexts(owner *macho.File, fn types.Function, plan microPlan) map[uint64]wrapperContext { data, err := s.functionDataFor(owner, fn) if err != nil { return nil } out := make(map[uint64]wrapperContext) var regBase [31]uint64 var regValue [31]uint64 for i := 0; i+4 <= len(data); i += 4 { pc := fn.StartAddr + uint64(i) raw := readUint32At(data, i) idx := i / 4 if idx < len(plan.tags) && plan.tags[idx]&(microTagBL|microTagB) != 0 && s.isOSMetaClassVariant(plan.targets[idx]) { ctx := s.staticDirectCallTrackedContext(owner, regBase, regValue, pc) out[pc] = ctx } trackRegisterRawWithReason(s, owner, raw, pc, ®Base, ®Value, pointerReasonStaticDirectCall) } return out } func (s *Scanner) staticDirectCallContext(owner *macho.File, fn types.Function, callsite uint64, target uint64) (wrapperContext, bool) { s.stats.staticDirectCalls++ key := staticCallKey{file: owner, start: fn.StartAddr, callsite: callsite, target: target} if cached, ok := s.staticCalls[key]; ok { s.stats.staticDirectCache++ s.stats.recordStaticDirectResolution(cached.ctx, cached.ok) return cached.ctx, cached.ok } data, err := s.functionDataFor(owner, fn) if err != nil { s.staticCalls[key] = cachedWrapperContext{} return wrapperContext{}, false } var regBase [31]uint64 var regValue [31]uint64 for i := 0; i+4 <= len(data); i += 4 { pc := fn.StartAddr + uint64(i) raw := readUint32At(data, i) if (callsite == 0 || pc == callsite) && target != 0 { if callTarget, ok := decodeBLTarget(pc, raw); ok && callTarget == target { ctx := s.staticDirectCallTrackedContext(owner, regBase, regValue, pc) s.staticCalls[key] = cachedWrapperContext{ctx: ctx, ok: true} s.stats.recordStaticDirectResolution(ctx, true) return ctx, true } if callTarget, ok := decodeBTarget(pc, raw); ok && callTarget == target { ctx := s.staticDirectCallTrackedContext(owner, regBase, regValue, pc) s.staticCalls[key] = cachedWrapperContext{ctx: ctx, ok: true} s.stats.recordStaticDirectResolution(ctx, true) return ctx, true } } trackRegisterRawWithReason(s, owner, raw, pc, ®Base, ®Value, pointerReasonStaticDirectCall) } s.staticCalls[key] = cachedWrapperContext{} return wrapperContext{}, false } func (s *Scanner) staticDirectCallTrackedContext(owner *macho.File, regBase [31]uint64, regValue [31]uint64, callsite uint64) wrapperContext { for reg := range 4 { if regValue[reg] == 0 && regBase[reg] != 0 { regValue[reg] = regBase[reg] } if reg == 2 && regValue[reg] != 0 && !validKernelPointer(regValue[reg]) { regValue[reg] = s.normalizeLoadedPointer(owner, regValue[reg]) } } return wrapperContext{ x0: regValue[0], x1: regValue[1], x2: regValue[2], x3: regValue[3], callsite: callsite, } } func mergeWrapperContext(dst, src wrapperContext) wrapperContext { if dst.x0 == 0 { dst.x0 = src.x0 } if dst.x1 == 0 { dst.x1 = src.x1 } if dst.x2 == 0 { dst.x2 = src.x2 } if dst.x3 == 0 { dst.x3 = src.x3 } if dst.metaVtab == 0 { dst.metaVtab = src.metaVtab } if dst.callsite == 0 { dst.callsite = src.callsite } return dst } func wrapperContextEmpty(ctx *wrapperContext) bool { return ctx == nil || (ctx.x0 == 0 && ctx.x1 == 0 && ctx.x2 == 0 && ctx.x3 == 0 && ctx.metaVtab == 0) } func (s *Scanner) recoverStaticWrapperContext(owner *macho.File, startAddr uint64, canonicalStart uint64) (*wrapperContext, bool) { ctx, ok := s.recoverStaticWrapperContextDepth(owner, startAddr, canonicalStart, 0, wrapperContext{}) if !ok { return nil, false } return &ctx, true } func (s *Scanner) recoverStaticWrapperContextDepth(owner *macho.File, startAddr uint64, canonicalStart uint64, depth int, accum wrapperContext) (wrapperContext, bool) { if owner == nil || depth > s.cfg.MaxWrapperDepth { return wrapperContext{}, false } fn, _, err := s.functionForAddrInAnyFile(owner, startAddr) if err != nil { return wrapperContext{}, false } return s.staticWrapperContextAlongPath(owner, fn, canonicalStart, depth, accum) } func (s *Scanner) staticWrapperContextAlongPath(owner *macho.File, fn types.Function, canonicalStart uint64, depth int, accum wrapperContext) (wrapperContext, bool) { data, err := s.functionDataFor(owner, fn) if err != nil { return wrapperContext{}, false } var regBase [31]uint64 var regLoadAddr [31]uint64 var regValue [31]uint64 for i := 0; i+4 <= len(data); i += 4 { pc := fn.StartAddr + uint64(i) raw := readUint32At(data, i) if (raw & 0x9f000000) == 0x90000000 { rd := int(raw & 0x1f) if rd < 31 { if addr, ok := decodeADRPImmediate(pc, raw); ok { regBase[rd] = addr regLoadAddr[rd] = 0 regValue[rd] = 0 } } continue } var ins disassemble.Inst if err := s.decodeArm64Instruction(pc, raw, &ins); err != nil || operandCount(&ins) == 0 { continue } s.trackStaticValueInstruction(owner, ®Base, ®LoadAddr, ®Value, &ins, staticValueTrackOptions{ acceptAnyLoadAddr: true, propagateLoadAddrInAdd: false, handleLoadPairs: false, }) for reg := range 4 { if regValue[reg] == 0 && regBase[reg] != 0 { regValue[reg] = regBase[reg] } if reg == 2 && regValue[reg] != 0 && !validKernelPointer(regValue[reg]) { regValue[reg] = s.normalizeLoadedPointer(owner, regValue[reg]) } } var nextTarget uint64 if blTarget, ok := decodeBLTarget(pc, raw); ok { nextTarget = blTarget } else if bTarget, ok := decodeBTarget(pc, raw); ok { nextTarget = bTarget } if nextTarget == 0 || s.isOSMetaClassVariant(nextTarget) { continue } ctx := mergeWrapperContext(accum, wrapperContext{ x0: regValue[0], x1: regValue[1], x2: regValue[2], x3: regValue[3], callsite: pc, }) if nextTarget == canonicalStart { return ctx, true } if depth >= s.cfg.MaxWrapperDepth { continue } if nested, ok := s.recoverStaticWrapperContextDepth(owner, nextTarget, canonicalStart, depth+1, ctx); ok { return nested, true } } return wrapperContext{}, false }