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.
1481 lines
38 KiB
Go
1481 lines
38 KiB
Go
package iokit
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"io"
|
|
"maps"
|
|
"strings"
|
|
"unicode/utf8"
|
|
|
|
"github.com/blacktop/arm64-cgo/disassemble"
|
|
"github.com/blacktop/go-macho"
|
|
)
|
|
|
|
const (
|
|
dispatchSizeClassic = uint64(24)
|
|
dispatchSize2022 = uint64(40)
|
|
dispatchSizeLegacy = uint64(48)
|
|
maxSelectorCount = 4096
|
|
)
|
|
|
|
type methodAnalysis struct {
|
|
kind string
|
|
addr uint64
|
|
owner *macho.File
|
|
arrayBase uint64
|
|
arrayBases []uint64
|
|
selectedEntries map[int]uint64
|
|
stride uint64
|
|
count int
|
|
note string
|
|
switchCases map[int]switchCaseInfo
|
|
}
|
|
|
|
type switchCaseInfo struct {
|
|
methodAddr uint64
|
|
readsStructureInput bool
|
|
readsStructureInputSize bool
|
|
}
|
|
|
|
type methodCacheKey struct {
|
|
addr uint64
|
|
kindHint string
|
|
}
|
|
|
|
type linearExpr struct {
|
|
valid bool
|
|
base uint64
|
|
coeff uint64
|
|
alts []uint64
|
|
note string
|
|
}
|
|
|
|
type decodedInst struct {
|
|
disassemble.Inst
|
|
}
|
|
|
|
func (a *analyzer) analyzeExternalMethod(addr uint64, kindHint string) methodAnalysis {
|
|
if addr == 0 {
|
|
return methodAnalysis{kind: DispatchUnknown, note: "vtable_unresolved"}
|
|
}
|
|
key := methodCacheKey{addr: addr, kindHint: kindHint}
|
|
if cached, ok := a.methods[key]; ok {
|
|
return cached
|
|
}
|
|
out := a.analyzeExternalMethodUncached(addr, kindHint)
|
|
a.methods[key] = out
|
|
return out
|
|
}
|
|
|
|
func (a *analyzer) analyzeExternalMethodUncached(addr uint64, kindHint string) methodAnalysis {
|
|
body, err := a.scanner.FunctionBodyAt(addr)
|
|
if err != nil {
|
|
return methodAnalysis{kind: DispatchUnknown, addr: addr, note: "vtable_unresolved"}
|
|
}
|
|
instrs := decodeInstructions(body.Data, body.Function.StartAddr, a.maxInst)
|
|
var regs [31]linearExpr
|
|
regs[0] = linearExpr{valid: true}
|
|
regs[1] = linearExpr{valid: true, coeff: 1}
|
|
|
|
selectorBound := -1
|
|
pendingCompare := -1
|
|
lastSelectorCompare := -1
|
|
sawSelectorBranch := false
|
|
var selectedDispatch [31]map[int]uint64
|
|
best := methodAnalysis{kind: DispatchUnknown, addr: addr, owner: body.Owner, note: "indirect"}
|
|
|
|
for idx := range instrs {
|
|
inst := &instrs[idx].Inst
|
|
if pendingCompare >= 0 {
|
|
if count, ok := selectorCountFromBranch(inst, pendingCompare); ok {
|
|
selectorBound = count
|
|
sawSelectorBranch = true
|
|
}
|
|
pendingCompare = -1
|
|
}
|
|
if count, ok := selectorCompareCount(inst, regs); ok {
|
|
pendingCompare = count
|
|
lastSelectorCompare = count
|
|
if selectorBound < 0 {
|
|
selectorBound = count
|
|
}
|
|
}
|
|
applySelectedDispatchInstruction(inst, regs[:], &selectedDispatch, lastSelectorCompare)
|
|
if isCallOrTail(inst) {
|
|
hint := a.callDispatchKindHint(inst)
|
|
if hint == "" {
|
|
hint = kindHint
|
|
}
|
|
if analysis, ok := a.dispatchAnalysisFromExpr(addr, body.Owner, regs[3], regs[4], selectorBound, hint); ok {
|
|
best = analysis
|
|
} else if analysis, ok := selectedDispatchAnalysis(addr, body.Owner, selectedDispatch[3], hint); ok {
|
|
best = analysis
|
|
}
|
|
}
|
|
applyMethodInstruction(a, body.Owner, inst, regs[:])
|
|
if isSelectorBranch(inst) {
|
|
sawSelectorBranch = true
|
|
}
|
|
}
|
|
if best.kind != DispatchUnknown {
|
|
return best
|
|
}
|
|
if sawSelectorBranch && selectorBound > 0 && selectorBound <= maxSelectorCount {
|
|
switchInstrs := a.switchAnalysisInstructions(body.Data, body.Function.StartAddr, instrs)
|
|
return methodAnalysis{
|
|
kind: DispatchSwitch,
|
|
addr: addr,
|
|
owner: body.Owner,
|
|
count: selectorBound,
|
|
note: "switch",
|
|
switchCases: a.analyzeSwitchCaseBodies(body.Owner, switchInstrs, selectorBound),
|
|
}
|
|
}
|
|
return best
|
|
}
|
|
|
|
func (a *analyzer) analyzeLegacyExternalMethod(addr uint64) methodAnalysis {
|
|
if addr == 0 {
|
|
return methodAnalysis{kind: DispatchUnknown, note: "vtable_unresolved"}
|
|
}
|
|
key := methodCacheKey{addr: addr, kindHint: "legacy"}
|
|
if cached, ok := a.methods[key]; ok {
|
|
return cached
|
|
}
|
|
out := a.analyzeLegacyExternalMethodUncached(addr)
|
|
a.methods[key] = out
|
|
return out
|
|
}
|
|
|
|
func (a *analyzer) analyzeLegacyExternalMethodUncached(addr uint64) methodAnalysis {
|
|
body, err := a.scanner.FunctionBodyAt(addr)
|
|
if err != nil {
|
|
return methodAnalysis{kind: DispatchUnknown, addr: addr, note: "vtable_unresolved"}
|
|
}
|
|
instrs := decodeInstructions(body.Data, body.Function.StartAddr, a.maxInst)
|
|
var regs [31]linearExpr
|
|
regs[0] = linearExpr{valid: true}
|
|
regs[2] = linearExpr{valid: true, coeff: 1}
|
|
|
|
selectorBound := -1
|
|
pendingCompare := -1
|
|
for idx := range instrs {
|
|
inst := &instrs[idx].Inst
|
|
if pendingCompare >= 0 {
|
|
if count, ok := selectorCountFromBranch(inst, pendingCompare); ok {
|
|
selectorBound = count
|
|
}
|
|
pendingCompare = -1
|
|
}
|
|
if count, ok := legacyIndexCompareCount(inst, regs); ok {
|
|
pendingCompare = count
|
|
if selectorBound < 0 {
|
|
selectorBound = count
|
|
}
|
|
}
|
|
if isReturn(inst) {
|
|
if analysis, ok := legacyDispatchAnalysisFromExpr(addr, body.Owner, regs[0], selectorBound); ok {
|
|
return analysis
|
|
}
|
|
}
|
|
applyMethodInstruction(a, body.Owner, inst, regs[:])
|
|
}
|
|
return methodAnalysis{kind: DispatchUnknown, addr: addr, owner: body.Owner, note: "indirect"}
|
|
}
|
|
|
|
func legacyDispatchAnalysisFromExpr(addr uint64, owner *macho.File, tableExpr linearExpr, count int) (methodAnalysis, bool) {
|
|
if !tableExpr.valid || tableExpr.base == 0 || tableExpr.coeff != dispatchSizeLegacy {
|
|
return methodAnalysis{}, false
|
|
}
|
|
analysis := methodAnalysis{
|
|
kind: DispatchExternalMethodLegacy,
|
|
addr: addr,
|
|
owner: owner,
|
|
arrayBase: tableExpr.base,
|
|
arrayBases: []uint64{tableExpr.base},
|
|
stride: dispatchSizeLegacy,
|
|
count: count,
|
|
}
|
|
if count <= 0 || count > maxSelectorCount {
|
|
analysis.note = "bounds_unknown"
|
|
}
|
|
return analysis, true
|
|
}
|
|
|
|
func selectedDispatchAnalysis(addr uint64, owner *macho.File, entries map[int]uint64, kindHint string) (methodAnalysis, bool) {
|
|
if owner == nil || len(entries) == 0 {
|
|
return methodAnalysis{}, false
|
|
}
|
|
kind := kindHint
|
|
stride := dispatchSizeClassic
|
|
if kind == DispatchExternalMethod2022 {
|
|
stride = dispatchSize2022
|
|
} else {
|
|
kind = DispatchExternalMethod
|
|
}
|
|
return methodAnalysis{
|
|
kind: kind,
|
|
addr: addr,
|
|
owner: owner,
|
|
selectedEntries: cloneSelectedDispatchEntries(entries),
|
|
stride: stride,
|
|
count: len(entries),
|
|
note: "selected_entry",
|
|
}, true
|
|
}
|
|
|
|
func applySelectedDispatchInstruction(inst *disassemble.Inst, regs []linearExpr, selected *[31]map[int]uint64, selector int) {
|
|
if inst == nil || selected == nil {
|
|
return
|
|
}
|
|
rd, hasDest := destRegIndex(inst)
|
|
if !hasDest || rd >= len(selected) {
|
|
return
|
|
}
|
|
selected[rd] = nil
|
|
if copied, ok := selectedDispatchMove(inst, selected); ok {
|
|
selected[rd] = copied
|
|
return
|
|
}
|
|
selectedSelector, entry, ok := conditionalSelectedDispatchEntry(inst, regs, selector)
|
|
if !ok {
|
|
return
|
|
}
|
|
selected[rd] = map[int]uint64{selectedSelector: entry}
|
|
}
|
|
|
|
func selectedDispatchMove(inst *disassemble.Inst, selected *[31]map[int]uint64) (map[int]uint64, bool) {
|
|
if inst == nil || inst.Operation != disassemble.ARM64_MOV || selected == nil {
|
|
return nil, false
|
|
}
|
|
rn, ok := operandRegIndex(inst, 1)
|
|
if !ok || rn >= len(selected) || len(selected[rn]) == 0 {
|
|
return nil, false
|
|
}
|
|
return cloneSelectedDispatchEntries(selected[rn]), true
|
|
}
|
|
|
|
func conditionalSelectedDispatchEntry(inst *disassemble.Inst, regs []linearExpr, selector int) (int, uint64, bool) {
|
|
if selector < 0 || inst == nil || strings.ToLower(inst.Operation.String()) != "csel" {
|
|
return 0, 0, false
|
|
}
|
|
rd, ok := destRegIndex(inst)
|
|
if !ok || rd != 3 {
|
|
return 0, 0, false
|
|
}
|
|
leftIdx, leftOK := operandRegIndex(inst, 1)
|
|
rightIdx, rightOK := operandRegIndex(inst, 2)
|
|
if !leftOK || !rightOK || leftIdx >= len(regs) || rightIdx >= len(regs) {
|
|
return 0, 0, false
|
|
}
|
|
left := regs[leftIdx]
|
|
right := regs[rightIdx]
|
|
if !isEqualCondition(inst) {
|
|
return 0, 0, false
|
|
}
|
|
if isSingleDispatchEntryExpr(left) && !right.valid {
|
|
return selector, left.base, true
|
|
}
|
|
return 0, 0, false
|
|
}
|
|
|
|
func isSingleDispatchEntryExpr(expr linearExpr) bool {
|
|
return expr.valid && expr.base != 0 && expr.coeff == 0 && len(expr.alts) == 0
|
|
}
|
|
|
|
func isEqualCondition(inst *disassemble.Inst) bool {
|
|
if inst == nil {
|
|
return false
|
|
}
|
|
for idx := 0; idx < int(inst.NumOps); idx++ {
|
|
if strings.EqualFold(inst.Operands[idx].Condition.String(inst.Operands[idx].Class), "EQ") {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func cloneSelectedDispatchEntries(entries map[int]uint64) map[int]uint64 {
|
|
if len(entries) == 0 {
|
|
return nil
|
|
}
|
|
out := make(map[int]uint64, len(entries))
|
|
maps.Copy(out, entries)
|
|
return out
|
|
}
|
|
|
|
func (a *analyzer) dispatchAnalysisFromExpr(addr uint64, owner *macho.File, dispatchExpr, countExpr linearExpr, count int, kindHint string) (methodAnalysis, bool) {
|
|
if count <= 0 && countExpr.valid && countExpr.coeff == 0 && countExpr.base > 0 && countExpr.base <= maxSelectorCount {
|
|
count = int(countExpr.base)
|
|
}
|
|
if dispatchExpr.valid && dispatchExpr.coeff == 0 && dispatchExpr.base != 0 && count > 0 && count <= maxSelectorCount {
|
|
bases := exprBases(dispatchExpr)
|
|
stride, kind, ok := a.inferDispatchStride(owner, bases, count, kindHint)
|
|
if !ok {
|
|
return methodAnalysis{}, false
|
|
}
|
|
note := ""
|
|
if len(bases) > 1 {
|
|
note = "conditional_array"
|
|
}
|
|
return methodAnalysis{
|
|
kind: kind,
|
|
addr: addr,
|
|
owner: owner,
|
|
arrayBase: bases[0],
|
|
arrayBases: bases,
|
|
stride: stride,
|
|
count: count,
|
|
note: note,
|
|
}, true
|
|
}
|
|
expr := dispatchExpr
|
|
if !expr.valid || expr.base == 0 {
|
|
return methodAnalysis{}, false
|
|
}
|
|
var kind string
|
|
switch expr.coeff {
|
|
case dispatchSizeClassic:
|
|
kind = DispatchExternalMethod
|
|
case dispatchSize2022:
|
|
kind = DispatchExternalMethod2022
|
|
default:
|
|
return methodAnalysis{}, false
|
|
}
|
|
bases := exprBases(expr)
|
|
analysis := methodAnalysis{
|
|
kind: kind,
|
|
addr: addr,
|
|
owner: owner,
|
|
arrayBase: bases[0],
|
|
arrayBases: bases,
|
|
stride: expr.coeff,
|
|
count: count,
|
|
}
|
|
if len(bases) > 1 {
|
|
analysis.note = "conditional_array"
|
|
}
|
|
if count <= 0 || count > maxSelectorCount {
|
|
analysis.note = "bounds_unknown"
|
|
}
|
|
return analysis, true
|
|
}
|
|
|
|
func (a *analyzer) inferDispatchStride(owner *macho.File, bases []uint64, count int, kindHint string) (uint64, string, bool) {
|
|
classicScore := a.dispatchStrideScore(owner, bases, count, dispatchSizeClassic)
|
|
dispatch2022Score := a.dispatchStrideScore(owner, bases, count, dispatchSize2022)
|
|
if dispatch2022Score > classicScore {
|
|
return dispatchSize2022, DispatchExternalMethod2022, dispatch2022Score > 0
|
|
}
|
|
if dispatch2022Score == classicScore && dispatch2022Score > 0 {
|
|
if stride, kind, ok := tiedDispatchStride(kindHint, a.hasDispatch2022Evidence(owner, bases, count)); ok {
|
|
return stride, kind, true
|
|
}
|
|
}
|
|
return dispatchSizeClassic, DispatchExternalMethod, classicScore > 0
|
|
}
|
|
|
|
func (a *analyzer) dispatchStrideScore(owner *macho.File, bases []uint64, count int, stride uint64) int {
|
|
score := 0
|
|
limit := min(count, 8)
|
|
for _, base := range bases {
|
|
for selector := range limit {
|
|
addr := base + uint64(selector)*stride
|
|
fn, ok := a.scanner.ReadPointerAt(owner, addr)
|
|
if !ok || fn == 0 {
|
|
continue
|
|
}
|
|
score += 2
|
|
if _, err := a.scanner.ReadUint32At(owner, addr+8); err == nil {
|
|
score++
|
|
}
|
|
if _, err := a.scanner.ReadUint32At(owner, addr+16); err == nil {
|
|
score++
|
|
}
|
|
}
|
|
}
|
|
return score
|
|
}
|
|
|
|
func tiedDispatchStride(kindHint string, hasEvidence2022 bool) (uint64, string, bool) {
|
|
if kindHint == DispatchExternalMethod2022 || hasEvidence2022 {
|
|
return dispatchSize2022, DispatchExternalMethod2022, true
|
|
}
|
|
if kindHint == DispatchExternalMethod {
|
|
return dispatchSizeClassic, DispatchExternalMethod, true
|
|
}
|
|
return 0, "", false
|
|
}
|
|
|
|
func (a *analyzer) hasDispatch2022Evidence(owner *macho.File, bases []uint64, count int) bool {
|
|
limit := min(count, 8)
|
|
for _, base := range bases {
|
|
for selector := range limit {
|
|
addr := base + uint64(selector)*dispatchSize2022
|
|
fn, ok := a.scanner.ReadPointerAt(owner, addr)
|
|
if !ok || fn == 0 {
|
|
continue
|
|
}
|
|
flags, err := a.scanner.ReadUint32At(owner, addr+24)
|
|
if err == nil && flags > 0 && flags <= 0xff {
|
|
return true
|
|
}
|
|
entitlement, ok := a.scanner.ReadPointerAt(owner, addr+32)
|
|
if !ok {
|
|
if raw, err := a.scanner.ReadUint64At(owner, addr+32); err == nil {
|
|
entitlement = raw
|
|
}
|
|
}
|
|
if entitlement != 0 {
|
|
if str, err := a.scanner.ReadCStringAt(owner, entitlement); err == nil && validLiteralString(str) {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func dispatchKindHintForFamily(family string) string {
|
|
switch family {
|
|
case "IOUserClient2022":
|
|
return DispatchExternalMethod2022
|
|
case "IOUserClient":
|
|
return DispatchExternalMethod
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func (a *analyzer) callDispatchKindHint(inst *disassemble.Inst) string {
|
|
target, ok := labelTarget(inst)
|
|
if !ok {
|
|
return ""
|
|
}
|
|
name := a.symbolName(target)
|
|
switch {
|
|
case strings.Contains(name, "IOUserClient2022::externalMethod"):
|
|
return DispatchExternalMethod2022
|
|
case strings.Contains(name, "IOUserClient::externalMethod"):
|
|
return DispatchExternalMethod
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func (a *analyzer) switchAnalysisInstructions(data []byte, start uint64, instrs []decodedInst) []decodedInst {
|
|
if a == nil || a.maxSwitchInst <= a.maxInst || len(instrs) < a.maxInst {
|
|
return instrs
|
|
}
|
|
return decodeInstructions(data, start, a.maxSwitchInst)
|
|
}
|
|
|
|
func decodeInstructions(data []byte, start uint64, maxInst int) []decodedInst {
|
|
var decoder disassemble.Decoder
|
|
out := make([]decodedInst, 0, len(data)/4)
|
|
r := bytes.NewReader(data)
|
|
addr := start
|
|
for len(out) < maxInst {
|
|
var raw uint32
|
|
err := binary.Read(r, binary.LittleEndian, &raw)
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
if err != nil {
|
|
break
|
|
}
|
|
var inst disassemble.Inst
|
|
if err := decoder.DecomposeInto(addr, raw, &inst); err == nil {
|
|
out = append(out, decodedInst{Inst: inst})
|
|
}
|
|
addr += 4
|
|
}
|
|
return out
|
|
}
|
|
|
|
func applyMethodInstruction(a *analyzer, owner *macho.File, inst *disassemble.Inst, regs []linearExpr) {
|
|
if applyConditionalSelect(inst, regs) {
|
|
return
|
|
}
|
|
switch inst.Operation {
|
|
case disassemble.ARM64_ADR, disassemble.ARM64_ADRP:
|
|
if rd, ok := destRegIndex(inst); ok {
|
|
if imm, ok := operandImm(inst, 1); ok {
|
|
regs[rd] = linearExpr{valid: true, base: imm}
|
|
}
|
|
}
|
|
case disassemble.ARM64_ADD:
|
|
applyAdd(inst, regs)
|
|
case disassemble.ARM64_LDR, disassemble.ARM64_LDUR:
|
|
applyLoad(a, owner, inst, regs)
|
|
case disassemble.ARM64_MOV:
|
|
applyMove(inst, regs)
|
|
case disassemble.ARM64_MOVZ:
|
|
if rd, ok := destRegIndex(inst); ok {
|
|
if imm, ok := operandImm(inst, 1); ok {
|
|
regs[rd] = linearExpr{valid: true, base: imm}
|
|
}
|
|
}
|
|
case disassemble.ARM64_MOVK:
|
|
applyMoveKeep(inst, regs)
|
|
case disassemble.ARM64_SMULL, disassemble.ARM64_UMULL:
|
|
applyMultiply(inst, regs)
|
|
case disassemble.ARM64_ORR:
|
|
if !applyORRMove(inst, regs) {
|
|
clearDest(inst, regs, "indirect")
|
|
}
|
|
default:
|
|
if shouldClearDest(inst) {
|
|
clearDest(inst, regs, "indirect")
|
|
}
|
|
}
|
|
if isCall(inst.Operation) {
|
|
clearVolatile(regs)
|
|
}
|
|
}
|
|
|
|
func applyAdd(inst *disassemble.Inst, regs []linearExpr) {
|
|
rd, ok := destRegIndex(inst)
|
|
if !ok {
|
|
return
|
|
}
|
|
leftIdx, ok := operandRegIndex(inst, 1)
|
|
if !ok {
|
|
clearReg(regs, rd, "indirect")
|
|
return
|
|
}
|
|
left := regs[leftIdx]
|
|
if imm, ok := operandImm(inst, 2); ok && operandIsImmediate(inst, 2) {
|
|
if !left.valid {
|
|
clearReg(regs, rd, left.note)
|
|
return
|
|
}
|
|
regs[rd] = addImmediateExpr(left, imm)
|
|
return
|
|
}
|
|
rightIdx, ok := operandRegIndex(inst, 2)
|
|
if !ok {
|
|
clearReg(regs, rd, "indirect")
|
|
return
|
|
}
|
|
right := shiftedExpr(regs[rightIdx], &inst.Operands[2])
|
|
regs[rd] = combineExpr(left, right)
|
|
}
|
|
|
|
func applyLoad(a *analyzer, owner *macho.File, inst *disassemble.Inst, regs []linearExpr) {
|
|
rd, ok := destRegIndex(inst)
|
|
if !ok {
|
|
return
|
|
}
|
|
if target, ok := labelTarget(inst); ok {
|
|
if ptr, ok := a.scanner.ReadPointerAt(owner, target); ok {
|
|
regs[rd] = linearExpr{valid: true, base: ptr}
|
|
return
|
|
}
|
|
regs[rd] = linearExpr{valid: true, base: target}
|
|
return
|
|
}
|
|
baseIdx, ok := operandRegIndex(inst, 1)
|
|
if !ok {
|
|
clearReg(regs, rd, "indirect")
|
|
return
|
|
}
|
|
base := regs[baseIdx]
|
|
if !base.valid || base.coeff != 0 {
|
|
clearReg(regs, rd, base.note)
|
|
return
|
|
}
|
|
off, _ := operandImm(inst, 1)
|
|
addr := base.base + off
|
|
if ptr, ok := a.scanner.ReadPointerAt(owner, addr); ok {
|
|
regs[rd] = linearExpr{valid: true, base: ptr}
|
|
return
|
|
}
|
|
clearReg(regs, rd, "indirect")
|
|
}
|
|
|
|
func applyMove(inst *disassemble.Inst, regs []linearExpr) {
|
|
rd, ok := destRegIndex(inst)
|
|
if !ok {
|
|
return
|
|
}
|
|
if rn, ok := operandRegIndex(inst, 1); ok {
|
|
regs[rd] = regs[rn]
|
|
return
|
|
}
|
|
if imm, ok := operandImm(inst, 1); ok {
|
|
regs[rd] = linearExpr{valid: true, base: imm}
|
|
return
|
|
}
|
|
clearReg(regs, rd, "indirect")
|
|
}
|
|
|
|
func applyMoveKeep(inst *disassemble.Inst, regs []linearExpr) {
|
|
rd, ok := destRegIndex(inst)
|
|
if !ok {
|
|
return
|
|
}
|
|
imm, ok := operandImm(inst, 1)
|
|
if !ok || !regs[rd].valid {
|
|
clearReg(regs, rd, "indirect")
|
|
return
|
|
}
|
|
shift := uint64(0)
|
|
if inst.NumOps > 1 && inst.Operands[1].ShiftValueUsed {
|
|
shift = uint64(inst.Operands[1].ShiftValue)
|
|
}
|
|
if regs[rd].coeff != 0 {
|
|
if shift >= 48 {
|
|
return
|
|
}
|
|
clearReg(regs, rd, "indirect")
|
|
return
|
|
}
|
|
mask := uint64(0xffff) << shift
|
|
regs[rd].base = (regs[rd].base &^ mask) | ((imm << shift) & mask)
|
|
}
|
|
|
|
func applyMultiply(inst *disassemble.Inst, regs []linearExpr) {
|
|
rd, ok := destRegIndex(inst)
|
|
if !ok {
|
|
return
|
|
}
|
|
leftIdx, leftOK := operandRegIndex(inst, 1)
|
|
rightIdx, rightOK := operandRegIndex(inst, 2)
|
|
if !leftOK || !rightOK {
|
|
clearReg(regs, rd, "indirect")
|
|
return
|
|
}
|
|
if scaled, ok := scaleExprByConstant(regs[leftIdx], regs[rightIdx]); ok {
|
|
regs[rd] = scaled
|
|
return
|
|
}
|
|
if scaled, ok := scaleExprByConstant(regs[rightIdx], regs[leftIdx]); ok {
|
|
regs[rd] = scaled
|
|
return
|
|
}
|
|
clearReg(regs, rd, "indirect")
|
|
}
|
|
|
|
func scaleExprByConstant(expr linearExpr, scalar linearExpr) (linearExpr, bool) {
|
|
if !expr.valid || !scalar.valid || scalar.coeff != 0 || len(scalar.alts) != 0 {
|
|
return linearExpr{}, false
|
|
}
|
|
out, ok := scaleExpr(expr, scalar.base)
|
|
return out, ok
|
|
}
|
|
|
|
func scaleExpr(expr linearExpr, scalar uint64) (linearExpr, bool) {
|
|
if !expr.valid {
|
|
return linearExpr{}, false
|
|
}
|
|
if scalar == 0 {
|
|
return linearExpr{valid: true}, true
|
|
}
|
|
base, ok := checkedMul(expr.base, scalar)
|
|
if !ok {
|
|
return linearExpr{}, false
|
|
}
|
|
coeff, ok := checkedMul(expr.coeff, scalar)
|
|
if !ok {
|
|
return linearExpr{}, false
|
|
}
|
|
out := linearExpr{valid: true, base: base, coeff: coeff, note: expr.note}
|
|
if len(expr.alts) == 0 {
|
|
return out, true
|
|
}
|
|
out.alts = make([]uint64, 0, len(expr.alts))
|
|
for _, alt := range expr.alts {
|
|
scaled, ok := checkedMul(alt, scalar)
|
|
if !ok {
|
|
return linearExpr{}, false
|
|
}
|
|
out.alts = append(out.alts, scaled)
|
|
}
|
|
out.alts = compactUint64s(out.alts)
|
|
if len(out.alts) > 0 {
|
|
out.base = out.alts[0]
|
|
}
|
|
return out, true
|
|
}
|
|
|
|
func checkedMul(left, right uint64) (uint64, bool) {
|
|
if left != 0 && right > ^uint64(0)/left {
|
|
return 0, false
|
|
}
|
|
return left * right, true
|
|
}
|
|
|
|
func applyORRMove(inst *disassemble.Inst, regs []linearExpr) bool {
|
|
rd, ok := destRegIndex(inst)
|
|
if !ok {
|
|
return false
|
|
}
|
|
rn, rnOK := operandReg(inst, 1)
|
|
rm, rmOK := operandReg(inst, 2)
|
|
if rnOK && rmOK && isZeroReg(rn) {
|
|
if idx, ok := regIndex(rm); ok {
|
|
regs[rd] = regs[idx]
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func applyConditionalSelect(inst *disassemble.Inst, regs []linearExpr) bool {
|
|
if inst == nil || strings.ToLower(inst.Operation.String()) != "csel" {
|
|
return false
|
|
}
|
|
rd, ok := destRegIndex(inst)
|
|
if !ok {
|
|
return true
|
|
}
|
|
leftIdx, leftOK := operandRegIndex(inst, 1)
|
|
rightIdx, rightOK := operandRegIndex(inst, 2)
|
|
if !leftOK || !rightOK {
|
|
clearReg(regs, rd, "indirect")
|
|
return true
|
|
}
|
|
left := regs[leftIdx]
|
|
right := regs[rightIdx]
|
|
if !left.valid || !right.valid || left.coeff != right.coeff {
|
|
clearReg(regs, rd, "indirect")
|
|
return true
|
|
}
|
|
if left.base == right.base && len(left.alts) == 0 && len(right.alts) == 0 {
|
|
regs[rd] = left
|
|
return true
|
|
}
|
|
alts := compactUint64s(append(exprBases(left), exprBases(right)...))
|
|
if len(alts) == 0 {
|
|
clearReg(regs, rd, "indirect")
|
|
return true
|
|
}
|
|
regs[rd] = linearExpr{
|
|
valid: true,
|
|
base: alts[0],
|
|
coeff: left.coeff,
|
|
alts: alts,
|
|
}
|
|
return true
|
|
}
|
|
|
|
func addImmediateExpr(expr linearExpr, imm uint64) linearExpr {
|
|
expr.base += imm
|
|
if len(expr.alts) == 0 {
|
|
return expr
|
|
}
|
|
alts := append([]uint64(nil), expr.alts...)
|
|
for idx := range alts {
|
|
alts[idx] += imm
|
|
}
|
|
alts = compactUint64s(alts)
|
|
if len(alts) > 0 {
|
|
expr.base = alts[0]
|
|
}
|
|
expr.alts = alts
|
|
return expr
|
|
}
|
|
|
|
func combineExpr(left, right linearExpr) linearExpr {
|
|
if !left.valid {
|
|
return linearExpr{note: noteFor(left)}
|
|
}
|
|
if !right.valid {
|
|
return linearExpr{note: noteFor(right)}
|
|
}
|
|
out := linearExpr{
|
|
valid: true,
|
|
base: left.base + right.base,
|
|
coeff: left.coeff + right.coeff,
|
|
}
|
|
bases := combinedBases(left, right)
|
|
switch len(bases) {
|
|
case 0:
|
|
case 1:
|
|
out.base = bases[0]
|
|
default:
|
|
out.base = bases[0]
|
|
out.alts = bases
|
|
}
|
|
return out
|
|
}
|
|
|
|
func combinedBases(left, right linearExpr) []uint64 {
|
|
leftBases := exprBases(left)
|
|
rightBases := exprBases(right)
|
|
if len(leftBases) == 1 && len(rightBases) == 1 {
|
|
return []uint64{leftBases[0] + rightBases[0]}
|
|
}
|
|
out := make([]uint64, 0, len(leftBases)*len(rightBases))
|
|
for _, leftBase := range leftBases {
|
|
for _, rightBase := range rightBases {
|
|
out = append(out, leftBase+rightBase)
|
|
}
|
|
}
|
|
return compactUint64s(out)
|
|
}
|
|
|
|
func shiftedExpr(expr linearExpr, op *disassemble.Op) linearExpr {
|
|
if !expr.valid || op == nil || !op.ShiftValueUsed {
|
|
return expr
|
|
}
|
|
shift := uint64(op.ShiftValue)
|
|
alts := append([]uint64(nil), expr.alts...)
|
|
for idx := range alts {
|
|
alts[idx] <<= shift
|
|
}
|
|
return linearExpr{
|
|
valid: true,
|
|
base: expr.base << shift,
|
|
coeff: expr.coeff << shift,
|
|
alts: alts,
|
|
}
|
|
}
|
|
|
|
func exprBases(expr linearExpr) []uint64 {
|
|
if len(expr.alts) > 0 {
|
|
return compactUint64s(expr.alts)
|
|
}
|
|
return []uint64{expr.base}
|
|
}
|
|
|
|
func compactUint64s(items []uint64) []uint64 {
|
|
if len(items) == 0 {
|
|
return nil
|
|
}
|
|
out := append([]uint64(nil), items...)
|
|
for idx := range out {
|
|
for j := idx + 1; j < len(out); j++ {
|
|
if out[j] < out[idx] {
|
|
out[idx], out[j] = out[j], out[idx]
|
|
}
|
|
}
|
|
}
|
|
write := 0
|
|
for _, item := range out {
|
|
if item == 0 {
|
|
continue
|
|
}
|
|
if write > 0 && out[write-1] == item {
|
|
continue
|
|
}
|
|
out[write] = item
|
|
write++
|
|
}
|
|
return out[:write]
|
|
}
|
|
|
|
func selectorCompareCount(inst *disassemble.Inst, regs [31]linearExpr) (int, bool) {
|
|
return compareCountForIndexReg(inst, regs, isSelectorReg)
|
|
}
|
|
|
|
func legacyIndexCompareCount(inst *disassemble.Inst, regs [31]linearExpr) (int, bool) {
|
|
return compareCountForIndexReg(inst, regs, isLegacyIndexReg)
|
|
}
|
|
|
|
func compareCountForIndexReg(inst *disassemble.Inst, regs [31]linearExpr, isIndexReg func(disassemble.Register) bool) (int, bool) {
|
|
if inst == nil || operandCount(inst) < 2 {
|
|
return 0, false
|
|
}
|
|
opName := strings.ToLower(inst.Operation.String())
|
|
if opName != "cmp" && opName != "subs" {
|
|
return 0, false
|
|
}
|
|
reg, ok := operandReg(inst, 0)
|
|
if !ok || !isIndexReg(reg) {
|
|
return 0, false
|
|
}
|
|
if imm, ok := operandImm(inst, 1); ok && imm <= maxSelectorCount {
|
|
return int(imm), true
|
|
}
|
|
idx, ok := operandRegIndex(inst, 1)
|
|
if !ok || idx >= len(regs) {
|
|
return 0, false
|
|
}
|
|
val := regs[idx]
|
|
if val.valid && val.coeff == 0 && val.base <= maxSelectorCount {
|
|
return int(val.base), true
|
|
}
|
|
return 0, false
|
|
}
|
|
|
|
func selectorCountFromBranch(inst *disassemble.Inst, compare int) (int, bool) {
|
|
if compare < 0 || inst == nil {
|
|
return 0, false
|
|
}
|
|
opName := strings.ToLower(inst.Operation.String())
|
|
switch {
|
|
case strings.Contains(opName, ".hi"):
|
|
return compare + 1, true
|
|
case strings.Contains(opName, ".hs"), strings.Contains(opName, ".cs"),
|
|
strings.Contains(opName, ".ge"):
|
|
return compare, true
|
|
default:
|
|
return 0, false
|
|
}
|
|
}
|
|
|
|
func isReturn(inst *disassemble.Inst) bool {
|
|
return inst != nil && inst.Operation == disassemble.ARM64_RET
|
|
}
|
|
|
|
func isSelectorBranch(inst *disassemble.Inst) bool {
|
|
if inst == nil {
|
|
return false
|
|
}
|
|
opName := strings.ToLower(inst.Operation.String())
|
|
return strings.HasPrefix(opName, "b.") || strings.HasPrefix(opName, "tbz") ||
|
|
strings.HasPrefix(opName, "tbnz") || strings.HasPrefix(opName, "cbz") ||
|
|
strings.HasPrefix(opName, "cbnz")
|
|
}
|
|
|
|
func isCallOrTail(inst *disassemble.Inst) bool {
|
|
if inst == nil {
|
|
return false
|
|
}
|
|
switch inst.Operation {
|
|
case disassemble.ARM64_BL, disassemble.ARM64_B,
|
|
disassemble.ARM64_BLR, disassemble.ARM64_BLRAA,
|
|
disassemble.ARM64_BLRAAZ, disassemble.ARM64_BLRAB,
|
|
disassemble.ARM64_BLRABZ, disassemble.ARM64_BR,
|
|
disassemble.ARM64_BRAA, disassemble.ARM64_BRAAZ:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func isCall(op disassemble.Operation) bool {
|
|
switch op {
|
|
case disassemble.ARM64_BL, disassemble.ARM64_BLR,
|
|
disassemble.ARM64_BLRAA, disassemble.ARM64_BLRAAZ,
|
|
disassemble.ARM64_BLRAB, disassemble.ARM64_BLRABZ:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func shouldClearDest(inst *disassemble.Inst) bool {
|
|
if inst == nil || inst.NumOps == 0 {
|
|
return false
|
|
}
|
|
if _, ok := destRegIndex(inst); !ok {
|
|
return false
|
|
}
|
|
op := strings.ToLower(inst.Operation.String())
|
|
if strings.HasPrefix(op, "st") || strings.HasPrefix(op, "b.") ||
|
|
op == "b" || op == "br" || op == "ret" || op == "cmp" {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func labelTarget(inst *disassemble.Inst) (uint64, bool) {
|
|
for idx := 0; idx < int(inst.NumOps); idx++ {
|
|
op := &inst.Operands[idx]
|
|
if op.Class == disassemble.LABEL {
|
|
return op.GetImmediate(), true
|
|
}
|
|
}
|
|
return 0, false
|
|
}
|
|
|
|
func operandReg(inst *disassemble.Inst, idx int) (disassemble.Register, bool) {
|
|
if inst == nil || int(inst.NumOps) <= idx || inst.Operands[idx].NumRegisters == 0 {
|
|
return disassemble.REG_NONE, false
|
|
}
|
|
return inst.Operands[idx].Registers[0], true
|
|
}
|
|
|
|
func operandRegIndex(inst *disassemble.Inst, idx int) (int, bool) {
|
|
reg, ok := operandReg(inst, idx)
|
|
if !ok {
|
|
return 0, false
|
|
}
|
|
return regIndex(reg)
|
|
}
|
|
|
|
func operandImm(inst *disassemble.Inst, idx int) (uint64, bool) {
|
|
if inst == nil || int(inst.NumOps) <= idx {
|
|
return 0, false
|
|
}
|
|
op := &inst.Operands[idx]
|
|
switch op.Class {
|
|
case disassemble.IMM32, disassemble.IMM64, disassemble.STR_IMM,
|
|
disassemble.MEM_PRE_IDX, disassemble.MEM_POST_IDX, disassemble.MEM_OFFSET,
|
|
disassemble.LABEL:
|
|
return op.GetImmediate(), true
|
|
default:
|
|
return 0, false
|
|
}
|
|
}
|
|
|
|
func operandIsImmediate(inst *disassemble.Inst, idx int) bool {
|
|
if inst == nil || int(inst.NumOps) <= idx {
|
|
return false
|
|
}
|
|
switch inst.Operands[idx].Class {
|
|
case disassemble.IMM32, disassemble.IMM64, disassemble.STR_IMM:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func destRegIndex(inst *disassemble.Inst) (int, bool) {
|
|
return operandRegIndex(inst, 0)
|
|
}
|
|
|
|
func regIndex(reg disassemble.Register) (int, bool) {
|
|
switch {
|
|
case reg >= disassemble.REG_X0 && reg <= disassemble.REG_X30:
|
|
return int(reg - disassemble.REG_X0), true
|
|
case reg >= disassemble.REG_W0 && reg <= disassemble.REG_W30:
|
|
return int(reg - disassemble.REG_W0), true
|
|
default:
|
|
return 0, false
|
|
}
|
|
}
|
|
|
|
func isSelectorReg(reg disassemble.Register) bool {
|
|
return reg == disassemble.REG_X1 || reg == disassemble.REG_W1
|
|
}
|
|
|
|
func isLegacyIndexReg(reg disassemble.Register) bool {
|
|
return reg == disassemble.REG_X2 || reg == disassemble.REG_W2
|
|
}
|
|
|
|
func isZeroReg(reg disassemble.Register) bool {
|
|
return reg == disassemble.REG_XZR || reg == disassemble.REG_WZR
|
|
}
|
|
|
|
func operandCount(inst *disassemble.Inst) int {
|
|
if inst == nil {
|
|
return 0
|
|
}
|
|
return int(inst.NumOps)
|
|
}
|
|
|
|
func clearDest(inst *disassemble.Inst, regs []linearExpr, note string) {
|
|
if rd, ok := destRegIndex(inst); ok {
|
|
clearReg(regs, rd, note)
|
|
}
|
|
}
|
|
|
|
func clearReg(regs []linearExpr, idx int, note string) {
|
|
if idx >= 0 && idx < len(regs) {
|
|
if note == "" {
|
|
note = "indirect"
|
|
}
|
|
regs[idx] = linearExpr{note: note}
|
|
}
|
|
}
|
|
|
|
func clearVolatile(regs []linearExpr) {
|
|
for idx := 0; idx <= 17 && idx < len(regs); idx++ {
|
|
regs[idx] = linearExpr{note: "indirect"}
|
|
}
|
|
}
|
|
|
|
func noteFor(expr linearExpr) string {
|
|
if expr.note != "" {
|
|
return expr.note
|
|
}
|
|
return "indirect"
|
|
}
|
|
|
|
func validLiteralString(s string) bool {
|
|
if s == "" || len(s) > 4096 || !utf8.ValidString(s) {
|
|
return false
|
|
}
|
|
for _, r := range s {
|
|
if r < 0x20 || r == 0x7f {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
const (
|
|
ioExternalMethodArgumentsStructureInputOffset = uint64(0x30)
|
|
ioExternalMethodArgumentsStructureInputSizeOffset = uint64(0x38)
|
|
maxSwitchCaseScanInstructions = 96
|
|
)
|
|
|
|
func (a *analyzer) analyzeSwitchCaseBodies(owner *macho.File, instrs []decodedInst, selectorCount int) map[int]switchCaseInfo {
|
|
targets := switchCaseTargets(instrs)
|
|
if len(targets) == 0 {
|
|
targets = a.switchJumpTableTargets(owner, instrs, selectorCount)
|
|
}
|
|
if len(targets) == 0 {
|
|
return nil
|
|
}
|
|
indexByAddr := make(map[uint64]int, len(instrs))
|
|
for idx := range instrs {
|
|
indexByAddr[instrs[idx].Address] = idx
|
|
}
|
|
dispatchIdx := len(instrs)
|
|
for _, target := range targets {
|
|
if idx, ok := indexByAddr[target]; ok && idx < dispatchIdx {
|
|
dispatchIdx = idx
|
|
}
|
|
}
|
|
prologueArgs := argumentRegistersBefore(instrs, dispatchIdx)
|
|
out := make(map[int]switchCaseInfo)
|
|
for selector, target := range targets {
|
|
idx, ok := indexByAddr[target]
|
|
if !ok {
|
|
continue
|
|
}
|
|
info := a.scanSwitchCaseBody(instrs, idx, prologueArgs, true)
|
|
if info.methodAddr != 0 || info.readsStructureInput || info.readsStructureInputSize {
|
|
out[selector] = info
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func switchCaseTargets(instrs []decodedInst) map[int]uint64 {
|
|
out := make(map[int]uint64)
|
|
var regs [31]linearExpr
|
|
regs[1] = linearExpr{valid: true, coeff: 1}
|
|
pendingSelector := -1
|
|
for idx := range instrs {
|
|
inst := &instrs[idx].Inst
|
|
if pendingSelector >= 0 {
|
|
if target, ok := equalBranchTarget(inst); ok {
|
|
out[pendingSelector] = target
|
|
}
|
|
pendingSelector = -1
|
|
}
|
|
if selector, ok := selectorCompareCount(inst, regs); ok {
|
|
pendingSelector = selector
|
|
}
|
|
applySwitchIndexInstruction(inst, ®s)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (a *analyzer) switchJumpTableTargets(owner *macho.File, instrs []decodedInst, selectorCount int) map[int]uint64 {
|
|
if a == nil || owner == nil || selectorCount <= 0 || selectorCount > maxSelectorCount {
|
|
return nil
|
|
}
|
|
var regs [31]linearExpr
|
|
regs[1] = linearExpr{valid: true, coeff: 1}
|
|
jumpTableLoads := make(map[int]uint64)
|
|
jumpTableTargets := make(map[int]uint64)
|
|
|
|
for idx := range instrs {
|
|
inst := &instrs[idx].Inst
|
|
if rd, ok := destRegIndex(inst); ok {
|
|
delete(jumpTableLoads, rd)
|
|
delete(jumpTableTargets, rd)
|
|
}
|
|
if offsetReg, tableBase, ok := switchJumpTableLoad(inst, regs); ok {
|
|
jumpTableLoads[offsetReg] = tableBase
|
|
}
|
|
if targetReg, tableBase, ok := switchJumpTableAdd(inst, regs, jumpTableLoads); ok {
|
|
jumpTableTargets[targetReg] = tableBase
|
|
}
|
|
if targetReg, ok := branchRegisterIndex(inst); ok {
|
|
if tableBase, ok := jumpTableTargets[targetReg]; ok {
|
|
return a.switchJumpTableTargetsFromTable(owner, tableBase, selectorCount)
|
|
}
|
|
}
|
|
applySwitchIndexInstruction(inst, ®s)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func switchJumpTableLoad(inst *disassemble.Inst, regs [31]linearExpr) (int, uint64, bool) {
|
|
if inst == nil || inst.Operation != disassemble.ARM64_LDRSW {
|
|
return 0, 0, false
|
|
}
|
|
offsetReg, ok := destRegIndex(inst)
|
|
if !ok {
|
|
return 0, 0, false
|
|
}
|
|
baseReg, indexReg, ok := memoryBaseAndIndexRegIndex(inst, 1)
|
|
if !ok || !isSwitchSelectorExpr(regs[indexReg]) {
|
|
return 0, 0, false
|
|
}
|
|
base := regs[baseReg]
|
|
if !base.valid || base.coeff != 0 || base.base == 0 {
|
|
return 0, 0, false
|
|
}
|
|
return offsetReg, base.base, true
|
|
}
|
|
|
|
func switchJumpTableAdd(inst *disassemble.Inst, regs [31]linearExpr, jumpTableLoads map[int]uint64) (int, uint64, bool) {
|
|
if inst == nil || inst.Operation != disassemble.ARM64_ADD {
|
|
return 0, 0, false
|
|
}
|
|
targetReg, ok := destRegIndex(inst)
|
|
if !ok {
|
|
return 0, 0, false
|
|
}
|
|
leftReg, leftOK := operandRegIndex(inst, 1)
|
|
rightReg, rightOK := operandRegIndex(inst, 2)
|
|
if !leftOK || !rightOK {
|
|
return 0, 0, false
|
|
}
|
|
if tableBase, ok := switchJumpTableAddBase(regs, jumpTableLoads, leftReg, rightReg); ok {
|
|
return targetReg, tableBase, true
|
|
}
|
|
if tableBase, ok := switchJumpTableAddBase(regs, jumpTableLoads, rightReg, leftReg); ok {
|
|
return targetReg, tableBase, true
|
|
}
|
|
return 0, 0, false
|
|
}
|
|
|
|
func switchJumpTableAddBase(regs [31]linearExpr, jumpTableLoads map[int]uint64, baseReg, offsetReg int) (uint64, bool) {
|
|
tableBase, ok := jumpTableLoads[offsetReg]
|
|
if !ok {
|
|
return 0, false
|
|
}
|
|
base := regs[baseReg]
|
|
if !base.valid || base.coeff != 0 || base.base != tableBase {
|
|
return 0, false
|
|
}
|
|
return tableBase, true
|
|
}
|
|
|
|
func (a *analyzer) switchJumpTableTargetsFromTable(owner *macho.File, tableBase uint64, selectorCount int) map[int]uint64 {
|
|
out := make(map[int]uint64)
|
|
for selector := range selectorCount {
|
|
raw, err := a.scanner.ReadUint32At(owner, tableBase+uint64(selector)*4)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
target, ok := switchJumpTargetAddress(tableBase, int32(raw))
|
|
if ok {
|
|
out[selector] = target
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func switchJumpTargetAddress(base uint64, offset int32) (uint64, bool) {
|
|
if offset >= 0 {
|
|
target := base + uint64(offset)
|
|
return target, target >= base
|
|
}
|
|
magnitude := uint64(-int64(offset))
|
|
if magnitude > base {
|
|
return 0, false
|
|
}
|
|
return base - magnitude, true
|
|
}
|
|
|
|
func isSwitchSelectorExpr(expr linearExpr) bool {
|
|
return expr.valid && expr.base == 0 && expr.coeff == 1 && len(expr.alts) == 0
|
|
}
|
|
|
|
func memoryBaseAndIndexRegIndex(inst *disassemble.Inst, operandIdx int) (int, int, bool) {
|
|
if inst == nil || int(inst.NumOps) <= operandIdx {
|
|
return 0, 0, false
|
|
}
|
|
op := &inst.Operands[operandIdx]
|
|
if op.NumRegisters < 2 {
|
|
return 0, 0, false
|
|
}
|
|
base, baseOK := regIndex(op.Registers[0])
|
|
index, indexOK := regIndex(op.Registers[1])
|
|
return base, index, baseOK && indexOK
|
|
}
|
|
|
|
func branchRegisterIndex(inst *disassemble.Inst) (int, bool) {
|
|
if inst == nil {
|
|
return 0, false
|
|
}
|
|
switch inst.Operation {
|
|
case disassemble.ARM64_BR, disassemble.ARM64_BRAA, disassemble.ARM64_BRAAZ,
|
|
disassemble.ARM64_BRAB, disassemble.ARM64_BRABZ:
|
|
return operandRegIndex(inst, 0)
|
|
default:
|
|
return 0, false
|
|
}
|
|
}
|
|
|
|
func equalBranchTarget(inst *disassemble.Inst) (uint64, bool) {
|
|
if inst == nil {
|
|
return 0, false
|
|
}
|
|
opName := strings.ToLower(inst.Operation.String())
|
|
if !strings.Contains(opName, ".eq") {
|
|
return 0, false
|
|
}
|
|
return labelTarget(inst)
|
|
}
|
|
|
|
func (a *analyzer) scanSwitchCaseBody(instrs []decodedInst, targetIdx int, argRegs [31]bool, scanCallee bool) switchCaseInfo {
|
|
info := switchCaseInfo{}
|
|
limit := min(len(instrs), targetIdx+maxSwitchCaseScanInstructions)
|
|
for idx := targetIdx; idx < limit; idx++ {
|
|
inst := &instrs[idx].Inst
|
|
if off, ok := argumentFieldRead(inst, argRegs); ok {
|
|
switch off {
|
|
case ioExternalMethodArgumentsStructureInputOffset:
|
|
info.readsStructureInput = true
|
|
case ioExternalMethodArgumentsStructureInputSizeOffset:
|
|
info.readsStructureInputSize = true
|
|
}
|
|
}
|
|
if scanCallee && inst.Operation == disassemble.ARM64_BL {
|
|
if target, ok := labelTarget(inst); ok {
|
|
if info.methodAddr == 0 {
|
|
info.methodAddr = target
|
|
}
|
|
calleeInfo := a.analyzeSwitchCaseCallee(target, argRegs)
|
|
info.readsStructureInput = info.readsStructureInput || calleeInfo.readsStructureInput
|
|
info.readsStructureInputSize = info.readsStructureInputSize || calleeInfo.readsStructureInputSize
|
|
}
|
|
}
|
|
applyArgumentRegisterInstruction(inst, &argRegs)
|
|
if idx > targetIdx && isCaseTerminator(inst) {
|
|
break
|
|
}
|
|
}
|
|
return info
|
|
}
|
|
|
|
func (a *analyzer) analyzeSwitchCaseCallee(addr uint64, argRegs [31]bool) switchCaseInfo {
|
|
if a == nil || addr == 0 {
|
|
return switchCaseInfo{}
|
|
}
|
|
body, err := a.scanner.FunctionBodyAt(addr)
|
|
if err != nil {
|
|
return switchCaseInfo{}
|
|
}
|
|
instrs := decodeInstructions(body.Data, body.Function.StartAddr, a.maxInst)
|
|
targetIdx := 0
|
|
for idx := range instrs {
|
|
if instrs[idx].Address == addr {
|
|
targetIdx = idx
|
|
break
|
|
}
|
|
}
|
|
return a.scanSwitchCaseBody(instrs, targetIdx, argRegs, false)
|
|
}
|
|
|
|
func argumentRegistersBefore(instrs []decodedInst, targetIdx int) [31]bool {
|
|
var argRegs [31]bool
|
|
argRegs[2] = true
|
|
for idx := 0; idx < min(targetIdx, len(instrs)); idx++ {
|
|
applyArgumentRegisterInstruction(&instrs[idx].Inst, &argRegs)
|
|
}
|
|
return argRegs
|
|
}
|
|
|
|
func argumentFieldRead(inst *disassemble.Inst, argRegs [31]bool) (uint64, bool) {
|
|
if inst == nil {
|
|
return 0, false
|
|
}
|
|
switch inst.Operation {
|
|
case disassemble.ARM64_LDR, disassemble.ARM64_LDUR:
|
|
default:
|
|
return 0, false
|
|
}
|
|
baseIdx, ok := operandRegIndex(inst, 1)
|
|
if !ok || baseIdx >= len(argRegs) || !argRegs[baseIdx] {
|
|
return 0, false
|
|
}
|
|
off, ok := operandImm(inst, 1)
|
|
if !ok {
|
|
return 0, false
|
|
}
|
|
return off, true
|
|
}
|
|
|
|
func applyArgumentRegisterInstruction(inst *disassemble.Inst, argRegs *[31]bool) {
|
|
if inst == nil {
|
|
return
|
|
}
|
|
rd, hasDest := destRegIndex(inst)
|
|
if !hasDest || rd >= len(argRegs) {
|
|
if isCall(inst.Operation) {
|
|
clearVolatileArgRegs(argRegs)
|
|
}
|
|
return
|
|
}
|
|
keep := false
|
|
switch inst.Operation {
|
|
case disassemble.ARM64_MOV:
|
|
if rn, ok := operandRegIndex(inst, 1); ok && rn < len(argRegs) && argRegs[rn] {
|
|
keep = true
|
|
}
|
|
case disassemble.ARM64_ADD:
|
|
if rn, ok := operandRegIndex(inst, 1); ok && rn < len(argRegs) && argRegs[rn] {
|
|
if _, ok := operandImm(inst, 2); ok && operandIsImmediate(inst, 2) {
|
|
keep = true
|
|
}
|
|
}
|
|
case disassemble.ARM64_ORR:
|
|
if rn, rnOK := operandRegIndex(inst, 1); rnOK && rn < len(argRegs) && argRegs[rn] {
|
|
if rm, rmOK := operandReg(inst, 2); rmOK && isZeroReg(rm) {
|
|
keep = true
|
|
}
|
|
}
|
|
if rm, rmOK := operandRegIndex(inst, 2); rmOK && rm < len(argRegs) && argRegs[rm] {
|
|
if rn, rnOK := operandReg(inst, 1); rnOK && isZeroReg(rn) {
|
|
keep = true
|
|
}
|
|
}
|
|
}
|
|
argRegs[rd] = keep
|
|
if isCall(inst.Operation) {
|
|
clearVolatileArgRegs(argRegs)
|
|
}
|
|
}
|
|
|
|
func applySwitchIndexInstruction(inst *disassemble.Inst, regs *[31]linearExpr) {
|
|
if inst == nil {
|
|
return
|
|
}
|
|
switch inst.Operation {
|
|
case disassemble.ARM64_ADR, disassemble.ARM64_ADRP:
|
|
if rd, ok := destRegIndex(inst); ok {
|
|
if imm, ok := operandImm(inst, 1); ok {
|
|
regs[rd] = linearExpr{valid: true, base: imm}
|
|
}
|
|
}
|
|
case disassemble.ARM64_MOV:
|
|
applyMove(inst, regs[:])
|
|
case disassemble.ARM64_ADD:
|
|
applyAdd(inst, regs[:])
|
|
case disassemble.ARM64_ORR:
|
|
if !applyORRMove(inst, regs[:]) {
|
|
clearDest(inst, regs[:], "indirect")
|
|
}
|
|
default:
|
|
if shouldClearDest(inst) {
|
|
clearDest(inst, regs[:], "indirect")
|
|
}
|
|
}
|
|
}
|
|
|
|
func clearVolatileArgRegs(argRegs *[31]bool) {
|
|
for idx := 0; idx <= 17; idx++ {
|
|
argRegs[idx] = false
|
|
}
|
|
}
|
|
|
|
func isCaseTerminator(inst *disassemble.Inst) bool {
|
|
if inst == nil {
|
|
return false
|
|
}
|
|
if isReturn(inst) {
|
|
return true
|
|
}
|
|
switch inst.Operation {
|
|
case disassemble.ARM64_B, disassemble.ARM64_BR, disassemble.ARM64_BRAA, disassemble.ARM64_BRAAZ,
|
|
disassemble.ARM64_BRAB, disassemble.ARM64_BRABZ:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|