Files
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

342 lines
11 KiB
Go

package iokit
import (
"fmt"
"sort"
)
type dispatchEntry struct {
object uint64
function uint64
adjustor uint64
scalarInputCount uint32
structInputSize uint32
scalarOutputCount uint32
structOutputSize uint32
flags int64
checkEntitlementAddr uint64
checkEntitlement string
count0 uint64
count1 uint64
}
func (a *analyzer) dispatchRecords(info *classInfo, analysis methodAnalysis) ([]Record, error) {
if analysis.kind == DispatchExternalMethod2022 && analysis.stride != dispatchSize2022 {
return nil, fmt.Errorf("IOExternalMethodDispatch2022 layout mismatch: stride=%d assumed=%d class=%s", analysis.stride, dispatchSize2022, info.Name)
}
if len(analysis.selectedEntries) > 0 {
return a.selectedDispatchRecords(info, analysis), nil
}
if analysis.note == "bounds_unknown" || analysis.count <= 0 {
return []Record{a.unresolvedMethodRecord(info, analysis, -1, "bounds_unknown")}, nil
}
bases := dispatchTableBases(analysis)
records := make([]Record, 0, len(bases)*analysis.count)
for tableIndex, tableBase := range bases {
tableAnalysis := analysis
tableAnalysis.arrayBase = tableBase
if analysis.note == "conditional_array" {
tableAnalysis.note = ""
}
for selector := 0; selector < analysis.count; selector++ {
rec := a.dispatchRecord(info, tableAnalysis, selector)
if analysis.note == "conditional_array" {
addConditionalTableExtra(rec.Extra, tableIndex, tableBase)
}
records = append(records, rec)
}
}
return records, nil
}
func (a *analyzer) selectedDispatchRecords(info *classInfo, analysis methodAnalysis) []Record {
selectors := make([]int, 0, len(analysis.selectedEntries))
for selector := range analysis.selectedEntries {
selectors = append(selectors, selector)
}
sort.Ints(selectors)
records := make([]Record, 0, len(selectors))
for _, selector := range selectors {
entryAnalysis := analysis
entryAnalysis.arrayBase = analysis.selectedEntries[selector]
rec := a.dispatchRecord(info, entryAnalysis, 0)
rec.Selector = selector
rec.Extra["slice_notes"] = appendNote(rec.Extra["slice_notes"], "selected_entry")
records = append(records, rec)
}
return records
}
func dispatchTableBases(analysis methodAnalysis) []uint64 {
if analysis.note == "conditional_array" && len(analysis.arrayBases) > 0 {
return analysis.arrayBases
}
return []uint64{analysis.arrayBase}
}
func addConditionalTableExtra(extra map[string]string, tableIndex int, tableBase uint64) {
if extra == nil {
return
}
extra["table"] = fmt.Sprintf("table_%d", tableIndex)
extra["table_base"] = hexAddr(tableBase)
extra["slice_notes"] = appendNote(extra["slice_notes"], "conditional_array")
}
func (a *analyzer) dispatchRecord(info *classInfo, analysis methodAnalysis, selector int) Record {
entry, note := a.readDispatchEntry(analysis, selector)
rec := Record{
Kind: KindMethod,
Class: info.Name,
Bundle: info.Bundle,
Selector: selector,
MethodAddr: hexAddr(entry.function),
MethodSymbol: a.symbolName(entry.function),
DispatchKind: analysis.kind,
ScalarInputCount: int64(entry.scalarInputCount),
ScalarOutputCount: int64(entry.scalarOutputCount),
StructInputSize: int64(entry.structInputSize),
StructOutputSize: int64(entry.structOutputSize),
Flags: entry.flags,
Resolved: entry.function != 0 && note == "",
Extra: map[string]string{},
}
if note != "" {
rec.ScalarInputCount = -1
rec.ScalarOutputCount = -1
rec.StructInputSize = -1
rec.StructOutputSize = -1
rec.Flags = -1
rec.Extra["slice_notes"] = note
}
if entry.checkEntitlement != "" {
rec.Extra["check_entitlement"] = entry.checkEntitlement
} else if entry.checkEntitlementAddr != 0 {
rec.Extra["check_entitlement_addr"] = hexAddr(entry.checkEntitlementAddr)
}
return rec
}
func (a *analyzer) legacyDispatchRecords(info *classInfo, analysis methodAnalysis) ([]Record, error) {
if analysis.stride != dispatchSizeLegacy {
return nil, fmt.Errorf("IOExternalMethod layout mismatch: stride=%d assumed=%d class=%s", analysis.stride, dispatchSizeLegacy, info.Name)
}
if analysis.note == "bounds_unknown" || analysis.count <= 0 {
return []Record{a.unresolvedMethodRecord(info, analysis, -1, "bounds_unknown")}, nil
}
records := make([]Record, 0, analysis.count)
for selector := 0; selector < analysis.count; selector++ {
records = append(records, a.legacyDispatchRecord(info, analysis, selector))
}
return records, nil
}
func (a *analyzer) legacyDispatchRecord(info *classInfo, analysis methodAnalysis, selector int) Record {
entry, note := a.readLegacyDispatchEntry(analysis, selector)
scalarIn, scalarOut, structIn, structOut, countNote := legacyCountsForFlags(entry.flags, entry.count0, entry.count1)
note = appendNote(note, countNote)
rec := Record{
Kind: KindMethod,
Class: info.Name,
Bundle: info.Bundle,
Selector: selector,
MethodAddr: hexAddr(entry.function),
MethodSymbol: a.symbolName(entry.function),
DispatchKind: DispatchExternalMethodLegacy,
ScalarInputCount: scalarIn,
ScalarOutputCount: scalarOut,
StructInputSize: structIn,
StructOutputSize: structOut,
Flags: entry.flags,
Resolved: entry.function != 0 && note == "",
Extra: map[string]string{},
}
if entry.object != 0 {
rec.Extra["target_addr"] = hexAddr(entry.object)
}
if entry.adjustor != 0 {
rec.Extra["method_adjustor"] = hexAddr(entry.adjustor)
}
if note != "" {
rec.ScalarInputCount = -1
rec.ScalarOutputCount = -1
rec.StructInputSize = -1
rec.StructOutputSize = -1
rec.Flags = -1
rec.Extra["slice_notes"] = note
}
return rec
}
func (a *analyzer) readLegacyDispatchEntry(analysis methodAnalysis, selector int) (dispatchEntry, string) {
addr := analysis.arrayBase + uint64(selector)*analysis.stride
entry := dispatchEntry{}
if object, ok := a.scanner.ReadPointerAt(analysis.owner, addr); ok {
entry.object = object
}
function, ok := a.scanner.ReadPointerAt(analysis.owner, addr+8)
if !ok {
if raw, err := a.scanner.ReadUint64At(analysis.owner, addr+8); err == nil {
function = raw
}
}
if function == 0 {
return entry, "indirect"
}
entry.function = function
if adjustor, err := a.scanner.ReadUint64At(analysis.owner, addr+16); err == nil {
entry.adjustor = adjustor
}
flags, err := a.scanner.ReadUint32At(analysis.owner, addr+24)
if err != nil {
return entry, "indirect"
}
entry.flags = int64(flags)
count0, err := a.scanner.ReadUint64At(analysis.owner, addr+32)
if err != nil {
return entry, "indirect"
}
entry.count0 = count0
count1, err := a.scanner.ReadUint64At(analysis.owner, addr+40)
if err != nil {
return entry, "indirect"
}
entry.count1 = count1
return entry, ""
}
func legacyCountsForFlags(flags int64, count0, count1 uint64) (int64, int64, int64, int64, string) {
var scalarIn, scalarOut, structIn, structOut int64
switch uint32(flags) & 0xf {
case 0:
scalarIn = legacyCountToInt64(count0)
scalarOut = legacyCountToInt64(count1)
case 2:
scalarIn = legacyCountToInt64(count0)
structOut = legacyCountToInt64(count1)
case 3:
structIn = legacyCountToInt64(count0)
structOut = legacyCountToInt64(count1)
case 4:
scalarIn = legacyCountToInt64(count0)
structIn = legacyCountToInt64(count1)
default:
return -1, -1, -1, -1, "legacy_flags_unknown"
}
if scalarIn < 0 || scalarOut < 0 || structIn < 0 || structOut < 0 {
return -1, -1, -1, -1, "legacy_count_overflow"
}
return scalarIn, scalarOut, structIn, structOut, ""
}
func legacyCountToInt64(count uint64) int64 {
const maxInt64Uint64 = uint64(1<<63 - 1)
if count > maxInt64Uint64 {
return -1
}
return int64(count)
}
func (a *analyzer) readDispatchEntry(analysis methodAnalysis, selector int) (dispatchEntry, string) {
addr := analysis.arrayBase + uint64(selector)*analysis.stride
function, ok := a.scanner.ReadPointerAt(analysis.owner, addr)
if !ok || function == 0 {
return dispatchEntry{}, "indirect"
}
scalarIn, err := a.scanner.ReadUint32At(analysis.owner, addr+8)
if err != nil {
return dispatchEntry{function: function}, "indirect"
}
structIn, err := a.scanner.ReadUint32At(analysis.owner, addr+12)
if err != nil {
return dispatchEntry{function: function}, "indirect"
}
scalarOut, err := a.scanner.ReadUint32At(analysis.owner, addr+16)
if err != nil {
return dispatchEntry{function: function}, "indirect"
}
structOut, err := a.scanner.ReadUint32At(analysis.owner, addr+20)
if err != nil {
return dispatchEntry{function: function}, "indirect"
}
entry := dispatchEntry{
function: function,
scalarInputCount: scalarIn,
structInputSize: structIn,
scalarOutputCount: scalarOut,
structOutputSize: structOut,
flags: 0,
}
if analysis.kind == DispatchExternalMethod2022 {
flags, err := a.scanner.ReadUint32At(analysis.owner, addr+24)
if err != nil {
return entry, "indirect"
}
entry.flags = int64(flags & 0xff)
if entitlement, ok := a.scanner.ReadPointerAt(analysis.owner, addr+32); ok {
entry.checkEntitlementAddr = entitlement
if str, err := a.scanner.ReadCStringAt(analysis.owner, entitlement); err == nil && validLiteralString(str) {
entry.checkEntitlement = str
}
}
}
return entry, ""
}
func (a *analyzer) unresolvedMethodRecord(info *classInfo, analysis methodAnalysis, selector int, note string) Record {
return Record{
Kind: KindMethod,
Class: info.Name,
Bundle: info.Bundle,
Selector: selector,
MethodAddr: "0x0",
MethodSymbol: "",
DispatchKind: analysis.kind,
ScalarInputCount: -1,
ScalarOutputCount: -1,
StructInputSize: -1,
StructOutputSize: -1,
Flags: -1,
Resolved: false,
Extra: map[string]string{"slice_notes": note},
}
}
func (a *analyzer) switchRecords(info *classInfo, analysis methodAnalysis) []Record {
count := analysis.count
if count <= 0 || count > maxSelectorCount {
return []Record{a.unresolvedMethodRecord(info, analysis, -1, "bounds_unknown")}
}
records := make([]Record, 0, count)
for selector := range count {
extra := map[string]string{"slice_notes": "switch"}
var methodAddr uint64
if caseInfo, ok := analysis.switchCases[selector]; ok {
methodAddr = caseInfo.methodAddr
if caseInfo.readsStructureInput {
extra["reads_structure_input"] = "true"
}
if caseInfo.readsStructureInputSize {
extra["reads_structure_input_size"] = "true"
}
}
records = append(records, Record{
Kind: KindMethod,
Class: info.Name,
Bundle: info.Bundle,
Selector: selector,
MethodAddr: hexAddr(methodAddr),
MethodSymbol: a.symbolName(methodAddr),
DispatchKind: DispatchSwitch,
ScalarInputCount: -1,
ScalarOutputCount: -1,
StructInputSize: -1,
StructOutputSize: -1,
Flags: -1,
Resolved: methodAddr != 0,
Extra: extra,
})
}
return records
}