mirror of
https://github.com/blacktop/ipsw.git
synced 2026-06-07 12:27:36 +00:00
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.
597 lines
15 KiB
Go
597 lines
15 KiB
Go
package iokit
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"maps"
|
|
"path/filepath"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/blacktop/go-macho"
|
|
mtypes "github.com/blacktop/go-macho/types"
|
|
"github.com/blacktop/ipsw/pkg/kernelcache"
|
|
"github.com/blacktop/ipsw/pkg/kernelcache/cpp"
|
|
"github.com/blacktop/ipsw/pkg/signature"
|
|
"github.com/blacktop/ipsw/pkg/symbols"
|
|
)
|
|
|
|
const (
|
|
defaultMaxFunctionInstructions = 320
|
|
defaultSwitchMaxFunctionInstructions = 2048
|
|
defaultMaxVtableSlots = 240
|
|
)
|
|
|
|
type analyzer struct {
|
|
root *macho.File
|
|
scanner *cpp.Scanner
|
|
classes []cpp.Class
|
|
infos []*classInfo
|
|
byName map[string][]*classInfo
|
|
symbolMap map[uint64]string
|
|
symCache map[uint64]string
|
|
methods map[methodCacheKey]methodAnalysis
|
|
ucRefs map[uint64][]string
|
|
stderr io.Writer
|
|
maxInst int
|
|
maxSwitchInst int
|
|
maxSlots int
|
|
userClient map[string]struct{}
|
|
}
|
|
|
|
type classInfo struct {
|
|
cpp.Class
|
|
index int
|
|
family string
|
|
isService bool
|
|
userClient bool
|
|
}
|
|
|
|
func Scan(root *macho.File, conf Config) ([]Record, error) {
|
|
if root == nil {
|
|
return nil, fmt.Errorf("nil kernelcache")
|
|
}
|
|
maxInst, maxSwitchInst := normalizeMaxInstructionCaps(conf.MaxFunctionInstructions)
|
|
if conf.MaxVtableSlots == 0 {
|
|
conf.MaxVtableSlots = defaultMaxVtableSlots
|
|
}
|
|
classFilter, err := compileClassFilter(conf.ClassFilter)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
scanner := cpp.NewScanner(root, cpp.Config{})
|
|
classes, err := scanner.Scan()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cpp scan: %w", err)
|
|
}
|
|
|
|
a := newAnalyzer(root, scanner, classes, conf, maxInst, maxSwitchInst)
|
|
userClients := a.userClientClasses()
|
|
if len(userClients) == 0 {
|
|
return nil, ErrNoIOUserClients
|
|
}
|
|
progress(conf.Stderr, "kernel iokit-methods: discovered %d IOUserClient subclasses\n", len(userClients))
|
|
|
|
records, err := a.methodRecords(userClients)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
records = append(records, a.serviceClientRecords()...)
|
|
records = dedupeRecords(records)
|
|
records = filterRecordsByClass(records, classFilter)
|
|
SortRecords(records)
|
|
return records, nil
|
|
}
|
|
|
|
func normalizeMaxInstructionCaps(requestedMaxInst int) (int, int) {
|
|
if requestedMaxInst <= 0 {
|
|
return defaultMaxFunctionInstructions, defaultSwitchMaxFunctionInstructions
|
|
}
|
|
return requestedMaxInst, requestedMaxInst
|
|
}
|
|
|
|
func newAnalyzer(root *macho.File, scanner *cpp.Scanner, classes []cpp.Class, conf Config, maxInst, maxSwitchInst int) *analyzer {
|
|
a := &analyzer{
|
|
root: root,
|
|
scanner: scanner,
|
|
classes: classes,
|
|
byName: make(map[string][]*classInfo),
|
|
symbolMap: loadKernelSymbolMap(conf.Kernelcache, conf.Stderr),
|
|
symCache: make(map[uint64]string),
|
|
methods: make(map[methodCacheKey]methodAnalysis),
|
|
ucRefs: make(map[uint64][]string),
|
|
stderr: conf.Stderr,
|
|
maxInst: maxInst,
|
|
maxSwitchInst: maxSwitchInst,
|
|
maxSlots: conf.MaxVtableSlots,
|
|
userClient: make(map[string]struct{}),
|
|
}
|
|
a.buildClassInfos()
|
|
return a
|
|
}
|
|
|
|
func (a *analyzer) buildClassInfos() {
|
|
a.infos = make([]*classInfo, 0, len(a.classes))
|
|
for idx := range a.classes {
|
|
info := &classInfo{
|
|
Class: a.classes[idx],
|
|
index: idx,
|
|
}
|
|
if reachesClass(a.classes, idx, "IOUserClient2022") {
|
|
info.family = "IOUserClient2022"
|
|
} else if reachesClass(a.classes, idx, "IOUserClient") {
|
|
info.family = "IOUserClient"
|
|
}
|
|
info.userClient = info.family != "" &&
|
|
info.Name != "IOUserClient" &&
|
|
info.Name != "IOUserClient2022"
|
|
info.isService = reachesClass(a.classes, idx, "IOService") && info.Name != "IOService"
|
|
a.infos = append(a.infos, info)
|
|
a.byName[info.Name] = append(a.byName[info.Name], info)
|
|
if info.userClient {
|
|
a.userClient[info.Name] = struct{}{}
|
|
}
|
|
}
|
|
for _, infos := range a.byName {
|
|
sort.SliceStable(infos, func(i, j int) bool {
|
|
if infos[i].Bundle != infos[j].Bundle {
|
|
return infos[i].Bundle < infos[j].Bundle
|
|
}
|
|
return infos[i].MetaPtr < infos[j].MetaPtr
|
|
})
|
|
}
|
|
}
|
|
|
|
func reachesClass(classes []cpp.Class, idx int, target string) bool {
|
|
seen := make(map[int]struct{})
|
|
for idx >= 0 && idx < len(classes) {
|
|
if _, ok := seen[idx]; ok {
|
|
return false
|
|
}
|
|
seen[idx] = struct{}{}
|
|
if classes[idx].Name == target {
|
|
return true
|
|
}
|
|
idx = classes[idx].SuperIndex
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (a *analyzer) userClientClasses() []*classInfo {
|
|
out := make([]*classInfo, 0, 128)
|
|
for _, info := range a.infos {
|
|
if info.userClient {
|
|
out = append(out, info)
|
|
}
|
|
}
|
|
sort.SliceStable(out, func(i, j int) bool {
|
|
if out[i].Bundle != out[j].Bundle {
|
|
return out[i].Bundle < out[j].Bundle
|
|
}
|
|
return out[i].Name < out[j].Name
|
|
})
|
|
return out
|
|
}
|
|
|
|
func (a *analyzer) methodRecords(userClients []*classInfo) ([]Record, error) {
|
|
slots, err := a.externalMethodSlots(userClients)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
legacySlots := a.legacyMethodSlots(userClients)
|
|
records := make([]Record, 0, len(userClients))
|
|
for _, info := range userClients {
|
|
slot, ok := slots[info.family]
|
|
if !ok {
|
|
records = append(records, a.unknownMethodRecord(info, 0, "vtable_unresolved"))
|
|
continue
|
|
}
|
|
entry, ok := a.scanner.VtableEntry(info.Class, slot)
|
|
if !ok || entry.Address == 0 {
|
|
records = append(records, a.unknownMethodRecord(info, 0, "vtable_unresolved"))
|
|
continue
|
|
}
|
|
analysis := a.analyzeExternalMethod(entry.Address, dispatchKindHintForFamily(info.family))
|
|
switch analysis.kind {
|
|
case DispatchExternalMethod, DispatchExternalMethod2022:
|
|
recs, err := a.dispatchRecords(info, analysis)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
records = append(records, recs...)
|
|
case DispatchSwitch:
|
|
records = append(records, a.switchRecords(info, analysis)...)
|
|
default:
|
|
if recs, ok, err := a.legacyMethodRecordsForInfo(info, legacySlots); err != nil {
|
|
return nil, err
|
|
} else if ok {
|
|
records = append(records, recs...)
|
|
continue
|
|
}
|
|
note := analysis.note
|
|
if note == "" {
|
|
note = "indirect"
|
|
}
|
|
records = append(records, a.unknownMethodRecord(info, entry.Address, note))
|
|
}
|
|
}
|
|
return records, nil
|
|
}
|
|
|
|
func (a *analyzer) externalMethodSlots(userClients []*classInfo) (map[string]int, error) {
|
|
symbolSlots := make(map[string]map[int]int)
|
|
for _, info := range userClients {
|
|
for _, entry := range a.scanner.VtableEntries(info.Class, a.maxSlots) {
|
|
if isNamedMethod(a.symbolName(entry.Address), "externalMethod") {
|
|
addSlot(symbolSlots, info.family, entry.Index)
|
|
}
|
|
}
|
|
}
|
|
|
|
out := make(map[string]int)
|
|
for family, slots := range symbolSlots {
|
|
slot, err := chooseSlot(family, slots, "symbol")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out[family] = slot
|
|
}
|
|
|
|
patternSlots := make(map[string]map[int]int)
|
|
for _, info := range userClients {
|
|
if _, ok := out[info.family]; ok {
|
|
continue
|
|
}
|
|
for _, entry := range a.scanner.VtableEntries(info.Class, a.maxSlots) {
|
|
analysis := a.analyzeExternalMethod(entry.Address, dispatchKindHintForFamily(info.family))
|
|
if analysis.kind == DispatchExternalMethod ||
|
|
analysis.kind == DispatchExternalMethod2022 ||
|
|
analysis.kind == DispatchSwitch {
|
|
addSlot(patternSlots, info.family, entry.Index)
|
|
}
|
|
}
|
|
}
|
|
for family, slots := range patternSlots {
|
|
if _, ok := out[family]; ok {
|
|
continue
|
|
}
|
|
slot, err := chooseSlot(family, slots, "pattern")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out[family] = slot
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (a *analyzer) legacyMethodSlots(userClients []*classInfo) map[string]int {
|
|
symbolSlots := make(map[string]map[int]int)
|
|
for _, info := range userClients {
|
|
for _, entry := range a.scanner.VtableEntries(info.Class, a.maxSlots) {
|
|
if isNamedMethod(a.symbolName(entry.Address), "getTargetAndMethodForIndex") {
|
|
addSlot(symbolSlots, info.family, entry.Index)
|
|
}
|
|
}
|
|
}
|
|
out := make(map[string]int)
|
|
for family, slots := range symbolSlots {
|
|
if slot, err := chooseSlot(family, slots, "legacy"); err == nil {
|
|
out[family] = slot
|
|
}
|
|
}
|
|
|
|
patternSlots := make(map[string]map[int]int)
|
|
for _, info := range userClients {
|
|
if _, ok := out[info.family]; ok {
|
|
continue
|
|
}
|
|
for _, entry := range a.scanner.VtableEntries(info.Class, a.maxSlots) {
|
|
analysis := a.analyzeLegacyExternalMethod(entry.Address)
|
|
if analysis.kind == DispatchExternalMethodLegacy {
|
|
addSlot(patternSlots, info.family, entry.Index)
|
|
}
|
|
}
|
|
}
|
|
for family, slots := range patternSlots {
|
|
if _, ok := out[family]; ok {
|
|
continue
|
|
}
|
|
if slot, err := chooseSlot(family, slots, "legacy pattern"); err == nil {
|
|
out[family] = slot
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (a *analyzer) legacyMethodRecordsForInfo(info *classInfo, legacySlots map[string]int) ([]Record, bool, error) {
|
|
slot, ok := legacySlots[info.family]
|
|
if !ok {
|
|
return nil, false, nil
|
|
}
|
|
entry, ok := a.scanner.VtableEntry(info.Class, slot)
|
|
if !ok || entry.Address == 0 {
|
|
return nil, false, nil
|
|
}
|
|
analysis := a.analyzeLegacyExternalMethod(entry.Address)
|
|
if analysis.kind != DispatchExternalMethodLegacy {
|
|
return nil, false, nil
|
|
}
|
|
records, err := a.legacyDispatchRecords(info, analysis)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
return records, true, nil
|
|
}
|
|
|
|
func addSlot(slots map[string]map[int]int, family string, slot int) {
|
|
if family == "" || slot < 0 {
|
|
return
|
|
}
|
|
if slots[family] == nil {
|
|
slots[family] = make(map[int]int)
|
|
}
|
|
slots[family][slot]++
|
|
}
|
|
|
|
func chooseSlot(family string, slots map[int]int, source string) (int, error) {
|
|
if len(slots) == 0 {
|
|
return -1, fmt.Errorf("no %s externalMethod slot candidates for %s", source, family)
|
|
}
|
|
type candidate struct {
|
|
slot int
|
|
count int
|
|
}
|
|
candidates := make([]candidate, 0, len(slots))
|
|
for slot, count := range slots {
|
|
candidates = append(candidates, candidate{slot: slot, count: count})
|
|
}
|
|
sort.Slice(candidates, func(i, j int) bool {
|
|
if candidates[i].count != candidates[j].count {
|
|
return candidates[i].count > candidates[j].count
|
|
}
|
|
return candidates[i].slot < candidates[j].slot
|
|
})
|
|
if len(candidates) > 1 && candidates[0].count == candidates[1].count {
|
|
return -1, fmt.Errorf("externalMethod vtable slot differs across %s hierarchy (%s candidates: %v)", family, source, slots)
|
|
}
|
|
return candidates[0].slot, nil
|
|
}
|
|
|
|
func isNamedMethod(symbolName string, method string) bool {
|
|
if symbolName == "" {
|
|
return false
|
|
}
|
|
return strings.Contains(symbolName, "::"+method+"(") ||
|
|
strings.Contains(symbolName, "::"+method+" ")
|
|
}
|
|
|
|
func (a *analyzer) unknownMethodRecord(info *classInfo, methodAddr uint64, note string) Record {
|
|
extra := map[string]string{"slice_notes": note}
|
|
return Record{
|
|
Kind: KindMethod,
|
|
Class: info.Name,
|
|
Bundle: info.Bundle,
|
|
Selector: -1,
|
|
MethodAddr: hexAddr(methodAddr),
|
|
MethodSymbol: a.symbolName(methodAddr),
|
|
DispatchKind: DispatchUnknown,
|
|
ScalarInputCount: -1,
|
|
ScalarOutputCount: -1,
|
|
StructInputSize: -1,
|
|
StructOutputSize: -1,
|
|
Flags: -1,
|
|
Resolved: false,
|
|
Extra: extra,
|
|
}
|
|
}
|
|
|
|
func (a *analyzer) symbolName(addr uint64) string {
|
|
if a == nil || addr == 0 {
|
|
return ""
|
|
}
|
|
if a.symCache != nil {
|
|
if cached, ok := a.symCache[addr]; ok {
|
|
return cached
|
|
}
|
|
}
|
|
if name := a.symbolMap[addr]; name != "" {
|
|
return a.cacheSymbolName(addr, symbols.DemangleSymbolName(name))
|
|
}
|
|
if a.scanner == nil {
|
|
return ""
|
|
}
|
|
if name := a.scanner.SymbolName(addr); name != "" {
|
|
return a.cacheSymbolName(addr, name)
|
|
}
|
|
if body, err := a.scanner.FunctionBodyAt(addr); err == nil && body.Function.StartAddr != addr {
|
|
if name := a.symbolMap[body.Function.StartAddr]; name != "" {
|
|
return a.cacheSymbolName(addr, symbols.DemangleSymbolName(name))
|
|
}
|
|
if name := a.scanner.SymbolName(body.Function.StartAddr); name != "" {
|
|
return a.cacheSymbolName(addr, name)
|
|
}
|
|
}
|
|
return a.cacheSymbolName(addr, "")
|
|
}
|
|
|
|
func (a *analyzer) cacheSymbolName(addr uint64, name string) string {
|
|
if a != nil && a.symCache != nil {
|
|
a.symCache[addr] = name
|
|
}
|
|
return name
|
|
}
|
|
|
|
func loadKernelSymbolMap(path string, stderr io.Writer) map[uint64]string {
|
|
out := make(map[uint64]string)
|
|
if path == "" {
|
|
return out
|
|
}
|
|
smap := signature.NewSymbolMap()
|
|
for _, candidate := range kernelSymbolMapCandidates(path) {
|
|
if err := smap.LoadJSON(candidate); err == nil {
|
|
maps.Copy(out, smap)
|
|
progress(stderr, "kernel iokit-methods: loaded symbol map %s\n", candidate)
|
|
return out
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func kernelSymbolMapCandidates(path string) []string {
|
|
base := filepath.Base(path) + ".symbols.json"
|
|
seen := make(map[string]struct{})
|
|
var out []string
|
|
add := func(candidate string) {
|
|
candidate = filepath.Clean(candidate)
|
|
if _, ok := seen[candidate]; ok {
|
|
return
|
|
}
|
|
seen[candidate] = struct{}{}
|
|
out = append(out, candidate)
|
|
}
|
|
dir := filepath.Dir(path)
|
|
for range 4 {
|
|
add(filepath.Join(dir, base))
|
|
matches, _ := filepath.Glob(filepath.Join(dir, "*", base))
|
|
sort.Strings(matches)
|
|
for _, match := range matches {
|
|
add(match)
|
|
}
|
|
next := filepath.Dir(dir)
|
|
if next == dir {
|
|
break
|
|
}
|
|
dir = next
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (a *analyzer) serviceClientRecords() []Record {
|
|
records := a.personalityRecords()
|
|
records = append(records, a.newUserClientRecords()...)
|
|
return records
|
|
}
|
|
|
|
func classBundle(infos []*classInfo) string {
|
|
if len(infos) == 0 {
|
|
return ""
|
|
}
|
|
return infos[0].Bundle
|
|
}
|
|
|
|
func classExists(infos []*classInfo) bool {
|
|
return len(infos) > 0
|
|
}
|
|
|
|
func hexAddr(addr uint64) string {
|
|
if addr == 0 {
|
|
return "0x0"
|
|
}
|
|
return fmt.Sprintf("%#x", addr)
|
|
}
|
|
|
|
func dedupeRecords(records []Record) []Record {
|
|
if len(records) < 2 {
|
|
return records
|
|
}
|
|
SortRecords(records)
|
|
out := records[:0]
|
|
var last string
|
|
for _, rec := range records {
|
|
key := recordKey(rec)
|
|
if key == last {
|
|
continue
|
|
}
|
|
out = append(out, rec)
|
|
last = key
|
|
}
|
|
return out
|
|
}
|
|
|
|
func compileClassFilter(pattern string) (*regexp.Regexp, error) {
|
|
pattern = strings.TrimSpace(pattern)
|
|
if pattern == "" {
|
|
return nil, nil
|
|
}
|
|
re, err := regexp.Compile(pattern)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid class filter %q: %w", pattern, err)
|
|
}
|
|
return re, nil
|
|
}
|
|
|
|
func filterRecordsByClass(records []Record, classFilter *regexp.Regexp) []Record {
|
|
if classFilter == nil || len(records) == 0 {
|
|
return records
|
|
}
|
|
out := records[:0]
|
|
for _, rec := range records {
|
|
if recordMatchesClassFilter(rec, classFilter) {
|
|
out = append(out, rec)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func recordMatchesClassFilter(rec Record, classFilter *regexp.Regexp) bool {
|
|
if classFilter == nil {
|
|
return true
|
|
}
|
|
return classFilter.MatchString(rec.Class) ||
|
|
classFilter.MatchString(rec.ServiceClass) ||
|
|
classFilter.MatchString(rec.UserClientClass)
|
|
}
|
|
|
|
func recordKey(r Record) string {
|
|
return strings.Join([]string{
|
|
r.Kind,
|
|
r.Bundle,
|
|
r.Class,
|
|
fmt.Sprint(r.Selector),
|
|
r.MethodAddr,
|
|
r.DispatchKind,
|
|
r.ServiceBundle,
|
|
r.ServiceClass,
|
|
r.UserClientBundle,
|
|
r.UserClientClass,
|
|
r.Source,
|
|
extraSortString(r.Extra),
|
|
}, "\x00")
|
|
}
|
|
|
|
func progress(w io.Writer, format string, args ...any) {
|
|
if w != nil {
|
|
fmt.Fprintf(w, format, args...)
|
|
}
|
|
}
|
|
|
|
func rootForPrelink(root *macho.File) *macho.File {
|
|
if root == nil || root.FileHeader.Type != mtypes.MH_FILESET {
|
|
return root
|
|
}
|
|
if kernelFile, err := root.GetFileSetFileByName("com.apple.kernel"); err == nil {
|
|
return kernelFile
|
|
}
|
|
if kernelFile, err := root.GetFileSetFileByName("kernel"); err == nil {
|
|
return kernelFile
|
|
}
|
|
return root
|
|
}
|
|
|
|
func kextBundles(root *macho.File) []kernelcache.CFBundle {
|
|
if root == nil {
|
|
return nil
|
|
}
|
|
if bundles, err := kernelcache.GetKexts(root); err == nil {
|
|
return bundles
|
|
}
|
|
if kernelRoot := rootForPrelink(root); kernelRoot != root {
|
|
if bundles, err := kernelcache.GetKexts(kernelRoot); err == nil {
|
|
return bundles
|
|
}
|
|
}
|
|
return nil
|
|
}
|