diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9671b7a..8487fa6 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -9,7 +9,7 @@ jobs: matrix: ghidra: # - "latest" - - "11.1.2" + - "11.2.1" # - "11.1.1" # - "11.1" # - "11.0.3" @@ -25,7 +25,7 @@ jobs: uses: actions/setup-java@v4 with: distribution: 'temurin' - java-version: '17' + java-version: '21' - name: Install Gradle uses: gradle/actions/setup-gradle@v3 diff --git a/.gitignore b/.gitignore index b63da45..1c80d53 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,11 @@ build/ !**/src/main/**/build/ !**/src/test/**/build/ +## Ghidra + +lib/ + + ### IntelliJ IDEA ### .idea/modules.xml .idea/jarRepositories.xml @@ -39,4 +44,4 @@ bin/ .vscode/ ### Mac OS ### -.DS_Store \ No newline at end of file +.DS_Store diff --git a/build.gradle b/build.gradle index 8802add..6551ab9 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,8 @@ plugins { - id 'org.jetbrains.kotlin.jvm' -}// Builds a Ghidra Extension for a given Ghidra installation. + id 'org.jetbrains.kotlin.jvm' version "1.9.23" + id 'idea' +} +// Builds a Ghidra Extension for a given Ghidra installation. // // An absolute path to the Ghidra installation directory must be supplied either by setting the // GHIDRA_INSTALL_DIR environment variable or Gradle project property: @@ -43,9 +45,18 @@ dependencies { testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:5.10.3" } kotlin { - jvmToolchain(17) + jvmToolchain(21) } + test { useJUnitPlatform() } + +sourceSets { + main { + kotlin { + srcDirs 'ghidra_scripts' + } + } +} //----------------------END "DO NOT MODIFY" SECTION------------------------------- diff --git a/src/main/java/lol/fairplay/ghidraapple/actions/ChooseMsgSendCalleeAction.kt b/src/main/java/lol/fairplay/ghidraapple/actions/ChooseMsgSendCalleeAction.kt new file mode 100644 index 0000000..044ede7 --- /dev/null +++ b/src/main/java/lol/fairplay/ghidraapple/actions/ChooseMsgSendCalleeAction.kt @@ -0,0 +1,66 @@ +package ghidra.app.plugin.core.decompile.actions; + +import docking.action.MenuData +import ghidra.app.decompiler.ClangFuncNameToken +import ghidra.app.decompiler.DecompilerLocation +import ghidra.app.plugin.core.decompile.DecompilerActionContext +import ghidra.program.model.pcode.PcodeOp +import lol.fairplay.ghidraapple.GhidraApplePluginPackage +import lol.fairplay.ghidraapple.actions.ChooseMsgSendCalleeDialog +import lol.fairplay.ghidraapple.analysis.utilities.getConstantFromVarNode +import lol.fairplay.ghidraapple.analysis.utilities.getFunctionForPCodeCall + +class ChooseMsgSendCalleeAction: AbstractDecompilerAction("Choose msgSend Callee") { + + init { + description = "" + popupMenuData = MenuData(arrayOf("Choose Dynamic Dispatch Callee"), GhidraApplePluginPackage.PKG_NAME) + } + + override fun isEnabledForDecompilerContext(ctx: DecompilerActionContext): Boolean { + val location = ctx.location as DecompilerLocation + if (location.token !is ClangFuncNameToken) { + return false + } + val pCodeOp: PcodeOp? = getPcodeOp(ctx) + val optFunc = getFunctionForPCodeCall(ctx.program, pCodeOp) + if (optFunc.isEmpty) { + return false + } + val func = optFunc.get() + if (func.name == "_objc_msgSend") { + return true + } + // Check if the function is a trampoline by checking if it is in the trampoline section `__objc_stubs` + if (func.program.memory.getBlock(func.entryPoint).name == "__objc_stubs") { + return true + } + return false + + } + + private fun getPcodeOp(ctx: DecompilerActionContext): PcodeOp? { + val location = ctx.location as DecompilerLocation + val pCodeOp = location.token.pcodeOp; + return pCodeOp + } + + override fun decompilerActionPerformed(ctx: DecompilerActionContext) { + // Get the selector name that is called here + // This can either be the selector argument in x1, or the name of the stub function + // For now we only support the renamed stub + + val pCodeOp = getPcodeOp(ctx) + with (ctx) { + val func = getFunctionForPCodeCall(program, pCodeOp).get() + if (func.name == "_objc_msgSend") { + // TODO: We can extract the selector here (if it is a constant) and use it to narrow down the search + tool.showDialog(ChooseMsgSendCalleeDialog(tool, program, location.address, null)); + } else { + tool.showDialog(ChooseMsgSendCalleeDialog(tool, program, location.address, func.name)); + } + + } + + } +} \ No newline at end of file diff --git a/src/main/java/lol/fairplay/ghidraapple/actions/ChooseMsgSendCalleeDialog.kt b/src/main/java/lol/fairplay/ghidraapple/actions/ChooseMsgSendCalleeDialog.kt new file mode 100644 index 0000000..2583142 --- /dev/null +++ b/src/main/java/lol/fairplay/ghidraapple/actions/ChooseMsgSendCalleeDialog.kt @@ -0,0 +1,103 @@ +package lol.fairplay.ghidraapple.actions + +import docking.DialogComponentProvider +import docking.Tool +import docking.widgets.table.TableColumnDescriptor +import docking.widgets.table.threaded.ThreadedTableModelStub +import ghidra.program.model.address.Address +import ghidra.program.model.listing.Function +import ghidra.program.model.listing.FunctionManager +import ghidra.program.model.listing.Program +import ghidra.program.model.symbol.SourceType +import ghidra.program.util.ProgramLocation +import ghidra.program.util.ProgramSelection +import ghidra.util.datastruct.Accumulator +import ghidra.util.table.GhidraFilterTable +import ghidra.util.table.ProgramTableModel +import ghidra.util.task.TaskMonitor +import lol.fairplay.ghidraapple.analysis.utilities.addColumn +import lol.fairplay.ghidraapple.analysis.utilities.setCallTarget + +class ChooseMsgSendCalleeDialog( + tool: Tool, + private val program: Program, + private val callsite: Address, + selector: String?, // TODO: Later we can add a selector to filter the functions +) : DialogComponentProvider("Choose msgSend Callee", true, true, true, false) { + private var functionsTable: GhidraFilterTable + + init { + + val matchingFunctionsTable = FunctionsForSelectorTable(tool, program.functionManager, selector) + functionsTable = GhidraFilterTable(matchingFunctionsTable) + + rootPanel.add(functionsTable) + + // Add select button with action + addDismissButton() + addApplyButton() + } + + override fun applyCallback() { + // For simplicity, we support two scenarios here: + // 1. If there is only one possible target being displayed, either due to filtering during doLoad + // or due to user filtering, we automatically treat that as selected + // 2. If there are multiple options, then the user needs to select a table entry + val function: Function = + if (functionsTable.model.modelData.size == 1) { + functionsTable.model.modelData[0] + } else { + functionsTable.selectedRowObject ?: return + } + + program.withTransaction("ChooseMsgSendCalleeDialog") { + program.referenceManager.setCallTarget(callsite, function, SourceType.USER_DEFINED) + } + close() + } +} + +class FunctionsForSelectorTable( + tool: Tool, + private val functionManager: FunctionManager, + private val selector: String?, +) : ThreadedTableModelStub("", tool), + ProgramTableModel { + override fun createTableColumnDescriptor(): TableColumnDescriptor { + val descriptor: TableColumnDescriptor = TableColumnDescriptor() + descriptor.addColumn("Name", true) { it.name } + descriptor.addColumn("Class", true) { it.parentNamespace.name } + + return descriptor + } + + override fun doLoad( + accumulator: Accumulator, + monitor: TaskMonitor, + ) { + with(functionManager) { + monitor.initialize(functionCount.toLong()) + getFunctions(true) + .filter { + monitor.checkCancelled() + (selector == null || it.name == selector) && it.parentNamespace.name != "stub" + }.forEach { + monitor.checkCancelled() + monitor.incrementProgress(1) + accumulator.add(it) + } + } + } + + override fun getProgramLocation( + modelRow: Int, + modelColumn: Int, + ): ProgramLocation { + val func = getRowObject(modelRow) + return ProgramLocation(func.program, func.entryPoint) + } + + override fun getProgramSelection(modelRows: IntArray?): ProgramSelection = ProgramSelection() + + override fun getProgram(): Program = functionManager.program +} diff --git a/src/main/java/lol/fairplay/ghidraapple/analysis/objectivec/GhidraTypeBuilder.kt b/src/main/java/lol/fairplay/ghidraapple/analysis/objectivec/GhidraTypeBuilder.kt index 7221ee2..168d095 100644 --- a/src/main/java/lol/fairplay/ghidraapple/analysis/objectivec/GhidraTypeBuilder.kt +++ b/src/main/java/lol/fairplay/ghidraapple/analysis/objectivec/GhidraTypeBuilder.kt @@ -1,30 +1,9 @@ package lol.fairplay.ghidraapple.analysis.objectivec -import ghidra.program.model.data.ArrayDataType -import ghidra.program.model.data.BooleanDataType -import ghidra.program.model.data.CategoryPath -import ghidra.program.model.data.CharDataType -import ghidra.program.model.data.DataType -import ghidra.program.model.data.DoubleDataType -import ghidra.program.model.data.FloatDataType -import ghidra.program.model.data.IntegerDataType -import ghidra.program.model.data.LongDataType -import ghidra.program.model.data.LongDoubleDataType -import ghidra.program.model.data.LongLongDataType -import ghidra.program.model.data.PointerDataType -import ghidra.program.model.data.ShortDataType -import ghidra.program.model.data.StructureDataType -import ghidra.program.model.data.UnionDataType -import ghidra.program.model.data.UnsignedCharDataType -import ghidra.program.model.data.UnsignedIntegerDataType -import ghidra.program.model.data.UnsignedLongDataType -import ghidra.program.model.data.UnsignedLongLongDataType -import ghidra.program.model.data.UnsignedShortDataType -import ghidra.program.model.data.VoidDataType +import ghidra.program.model.data.* import ghidra.program.model.listing.Program import lol.fairplay.ghidraapple.core.objc.encodings.TypeNode import lol.fairplay.ghidraapple.core.objc.encodings.TypeNodeVisitor - import java.security.SecureRandom fun getRandomHexString(length: Int): String { @@ -57,13 +36,19 @@ class GhidraTypeBuilder(val program: Program) : TypeNodeVisitor { return GhidraTypeBuilder(program) } - fun tryResolveDefinedStruct(name: String): DataType? { + fun getGAType(name: String): DataType? { val category = CategoryPath("/GA_OBJC") return program.dataTypeManager.getDataType(category, name) } - fun tryResolveStructPtr(name: String): DataType? { - return PointerDataType(tryResolveDefinedStruct(name) ?: return null) + fun createUnionDT(name: String): DataType { + val category = CategoryPath("/GA_OBJC") + return program.dataTypeManager.addDataType(UnionDataType(category, name), null) + } + + fun createStructureDT(name: String): DataType { + val category = CategoryPath("/GA_OBJC") + return program.dataTypeManager.addDataType(StructureDataType(category, name, 0), null) } override fun visitStruct(struct: TypeNode.Struct) { @@ -71,8 +56,13 @@ class GhidraTypeBuilder(val program: Program) : TypeNodeVisitor { if (it.isEmpty()) "anon__${getRandomHexString(6)}" else it } - val ghidraStruct = (tryResolveDefinedStruct(name) ?: StructureDataType(name, 0)) as StructureDataType + var ghidraStruct = getGAType(name) as Structure? + if (ghidraStruct != null) { + result = ghidraStruct + return + } + ghidraStruct = createStructureDT(name) as Structure if (struct.fields == null) { result = ghidraStruct return @@ -104,8 +94,8 @@ class GhidraTypeBuilder(val program: Program) : TypeNodeVisitor { return } - val resolved = tryResolveStructPtr(obj.name) - result = resolved ?: idType + val objDT = getGAType(obj.name) ?: createStructureDT(obj.name) + result = PointerDataType(objDT, 8) } override fun visitUnion(union: TypeNode.Union) { @@ -114,8 +104,13 @@ class GhidraTypeBuilder(val program: Program) : TypeNodeVisitor { if (it.isEmpty()) "anon__${getRandomHexString(6)}" else it } - val ghidraUnion = UnionDataType(name) + var ghidraUnion = getGAType(name) + if (ghidraUnion != null) { + result = ghidraUnion + return + } + ghidraUnion = createUnionDT(name) as Union if (union.fields == null) { result = ghidraUnion return @@ -149,8 +144,9 @@ class GhidraTypeBuilder(val program: Program) : TypeNodeVisitor { 'C' -> UnsignedCharDataType.dataType 's' -> ShortDataType.dataType 'S' -> UnsignedShortDataType.dataType - 'i' -> IntegerDataType.dataType - 'I' -> UnsignedIntegerDataType.dataType +// 'i' -> IntegerDataType.dataType +// 'I' -> UnsignedIntegerDataType.dataType + 'i', 'I' -> Undefined4DataType.dataType 'l' -> LongDataType.dataType 'L' -> UnsignedLongDataType.dataType 'q' -> LongLongDataType.dataType @@ -160,7 +156,7 @@ class GhidraTypeBuilder(val program: Program) : TypeNodeVisitor { 'v' -> VoidDataType.dataType 'B' -> BooleanDataType.dataType 'D' -> LongDoubleDataType.dataType - '*' -> PointerDataType(CharDataType.dataType) + '*' -> PointerDataType(CharDataType.dataType, 8) else -> throw Exception("Unknown primitive type: ${primitive.type}") } } @@ -169,7 +165,7 @@ class GhidraTypeBuilder(val program: Program) : TypeNodeVisitor { val visitor = extend() pointer.pointee.accept(visitor) - result = PointerDataType(visitor.getResult()) + result = PointerDataType(visitor.getResult(), 8) } override fun visitBitfield(bitfield: TypeNode.Bitfield) { @@ -181,7 +177,7 @@ class GhidraTypeBuilder(val program: Program) : TypeNodeVisitor { } override fun visitFunctionPointer(fnPtr: TypeNode.FunctionPointer) { - result = PointerDataType(VoidDataType.dataType) + result = PointerDataType(VoidDataType.dataType, 8) } override fun visitSelector(fnPtr: TypeNode.Selector) { diff --git a/src/main/java/lol/fairplay/ghidraapple/analysis/objectivec/TypeResolver.kt b/src/main/java/lol/fairplay/ghidraapple/analysis/objectivec/TypeResolver.kt index b12203c..f3ee863 100644 --- a/src/main/java/lol/fairplay/ghidraapple/analysis/objectivec/TypeResolver.kt +++ b/src/main/java/lol/fairplay/ghidraapple/analysis/objectivec/TypeResolver.kt @@ -26,7 +26,7 @@ class TypeResolver(val program: Program) { return builder.getResult() } - fun tryResolveStructPtr(name: String): DataType? { + fun tryResolveDefinedStructPtr(name: String): DataType? { return PointerDataType(tryResolveDefinedStruct(name) ?: return null) } diff --git a/src/main/java/lol/fairplay/ghidraapple/analysis/objectivec/modelling/StructureParsing.kt b/src/main/java/lol/fairplay/ghidraapple/analysis/objectivec/modelling/StructureParsing.kt index bd3352e..c197427 100644 --- a/src/main/java/lol/fairplay/ghidraapple/analysis/objectivec/modelling/StructureParsing.kt +++ b/src/main/java/lol/fairplay/ghidraapple/analysis/objectivec/modelling/StructureParsing.kt @@ -2,27 +2,16 @@ package lol.fairplay.ghidraapple.analysis.objectivec.modelling import ghidra.program.model.listing.Data import ghidra.program.model.listing.Program -import ghidra.program.model.scalar.Scalar import ghidra.program.model.symbol.Namespace +import lol.fairplay.ghidraapple.analysis.utilities.StructureHelpers.deref +import lol.fairplay.ghidraapple.analysis.utilities.StructureHelpers.derefUntyped +import lol.fairplay.ghidraapple.analysis.utilities.StructureHelpers.get +import lol.fairplay.ghidraapple.analysis.utilities.StructureHelpers.longValue import lol.fairplay.ghidraapple.analysis.utilities.address import lol.fairplay.ghidraapple.analysis.utilities.dataAt import lol.fairplay.ghidraapple.analysis.utilities.tryResolveNamespace -import lol.fairplay.ghidraapple.analysis.utilities.StructureHelpers.get -import lol.fairplay.ghidraapple.analysis.utilities.StructureHelpers.longValue -import lol.fairplay.ghidraapple.analysis.utilities.StructureHelpers.deref -import lol.fairplay.ghidraapple.analysis.utilities.StructureHelpers.derefUntyped -import lol.fairplay.ghidraapple.core.objc.encodings.EncodedSignature -import lol.fairplay.ghidraapple.core.objc.encodings.EncodedSignatureType -import lol.fairplay.ghidraapple.core.objc.encodings.EncodingLexer -import lol.fairplay.ghidraapple.core.objc.encodings.TypeEncodingParser -import lol.fairplay.ghidraapple.core.objc.encodings.parseEncodedProperty -import lol.fairplay.ghidraapple.core.objc.encodings.parseSignature -import lol.fairplay.ghidraapple.core.objc.modelling.OCClass -import lol.fairplay.ghidraapple.core.objc.modelling.OCFieldContainer -import lol.fairplay.ghidraapple.core.objc.modelling.OCIVar -import lol.fairplay.ghidraapple.core.objc.modelling.OCMethod -import lol.fairplay.ghidraapple.core.objc.modelling.OCProperty -import lol.fairplay.ghidraapple.core.objc.modelling.OCProtocol +import lol.fairplay.ghidraapple.core.objc.encodings.* +import lol.fairplay.ghidraapple.core.objc.modelling.* class StructureParsing(val program: Program) { @@ -40,6 +29,10 @@ class StructureParsing(val program: Program) { val data = dataAt(program, program.address(address)) ?: return null + if (data.primarySymbol == null) { + return null + } + if (data.primarySymbol.parentNamespace.getName(true) == namespace.getName(true)) { return data } else { @@ -76,10 +69,10 @@ class StructureParsing(val program: Program) { val instanceProperties = parsePropertyList(struct[7].longValue(false)) // I think only one of these are filled in at a time...? - val instanceMethods = parseMethodList(struct[3].longValue(false), true) - val classMethods = parseMethodList(struct[4].longValue(false), false) - val optionalInstanceMethods = parseMethodList(struct[5].longValue(false), true) - val optionalClassMethods = parseMethodList(struct[6].longValue(false), false) + val instanceMethods = parseMethodList(struct[3].longValue(false)) + val classMethods = parseMethodList(struct[4].longValue(false)) + val optionalInstanceMethods = parseMethodList(struct[5].longValue(false)) + val optionalClassMethods = parseMethodList(struct[6].longValue(false)) val methodListCoalesced = instanceMethods ?: classMethods ?: optionalInstanceMethods ?: optionalClassMethods @@ -121,7 +114,9 @@ class StructureParsing(val program: Program) { name = dat[0].deref(), attributes = encoding.attributes, type = encoding.type, - backingIvar = encoding.backingIvar + customGetter = encoding.customGetter, + customSetter = encoding.customSetter, + backingIvar = encoding.backingIvar, ) } @@ -140,7 +135,7 @@ class StructureParsing(val program: Program) { ) } - fun parseClass(address: Long): OCClass? { + fun parseClass(address: Long, isMetaclass: Boolean = false): OCClass? { val klassRo = datResolve(address, nsClass ?: return null) ?: return null // get the class_t->data (class_rw_t *) field... @@ -149,23 +144,47 @@ class StructureParsing(val program: Program) { val klass = OCClass( name = rwStruct[3].deref(), - flags = rwStruct[0].longValue(false), - baseMethods = null, + flags = rwStruct[0].longValue(false).toULong(), + superclass = parseClass(superAddress), + baseClassMethods = null, + baseInstanceMethods = null, baseProtocols = null, instanceVariables = null, - baseProperties = null, + baseClassProperties = null, + baseInstanceProperties = null, weakIvarLayout = rwStruct[7].longValue(false), - superclass = parseClass(superAddress), ) - parentStack.add(klass) + // Parse regular stuff. + if (!isMetaclass) { + parentStack.add(klass) + } + + // Parse the metaclass field only if we are not a metaclass. + val metaclass = if (!isMetaclass) { + parseClass(klassRo[0].longValue(false), isMetaclass = true) + } else { + null + } + + klass.baseClassMethods = metaclass?.baseInstanceMethods + klass.baseClassProperties = metaclass?.baseInstanceProperties - klass.baseMethods = parseMethodList(rwStruct[4].longValue(false)) + klass.baseInstanceMethods = parseMethodList(rwStruct[4].longValue(false)) klass.baseProtocols = parseProtocolList(rwStruct[5].longValue(false)) klass.instanceVariables = parseIvarList(rwStruct[6].longValue(false)) - klass.baseProperties = parsePropertyList(rwStruct[8].longValue(false)) + klass.baseInstanceProperties = parsePropertyList(rwStruct[8].longValue(false)) - parentStack.removeLast() + if (!isMetaclass) { + parentStack.removeLast() + } + +// // add class-members to our baseProtocols +// klass.baseProtocols?.forEach { baseProtocol -> +// metaclass?.baseProtocols?.find { mp -> mp.name == baseProtocol.name }?.let { metaProtocol -> +// baseProtocol.classMethods +// } +// } return klass } @@ -192,14 +211,13 @@ class StructureParsing(val program: Program) { return result.toList() } - fun parseMethod(dat: Data, instanceMethod: Boolean): OCMethod? { + fun parseMethod(dat: Data): OCMethod? { if (dat.dataType.name == "method_t") { return OCMethod( parent = parentStack.last(), name = dat[0].deref(), signature = parseSignature(dat[1].deref(), EncodedSignatureType.METHOD_SIGNATURE), implAddress = dat[2].longValue(false), - isInstanceMethod = instanceMethod ) } else if (dat.dataType.name == "method_small_t") { val addresses = (0 until dat.numComponents).map { @@ -213,23 +231,24 @@ class StructureParsing(val program: Program) { ) val implementation = addresses[2] + val parent = parentStack.last() + return OCMethod( - parent = parentStack.last(), + parent = parent, name = name, signature = signature, implAddress = implementation.unsignedOffset, - isInstanceMethod = instanceMethod ) } else { return null } } - fun parseMethodList(address: Long, instanceMethods: Boolean = true): List? { + fun parseMethodList(address: Long): List? { val struct = datResolve(address, nsMethodList ?: return null) ?: return null val result = mutableListOf() for (i in 2 until struct.numComponents) { - result.add(parseMethod(struct[i], instanceMethods)!!) + result.add(parseMethod(struct[i])!!) } return result.toList() } diff --git a/src/main/java/lol/fairplay/ghidraapple/analysis/passes/objcclasses/OCClassFieldAnalyzer.kt b/src/main/java/lol/fairplay/ghidraapple/analysis/passes/objcclasses/OCClassFieldAnalyzer.kt deleted file mode 100644 index 55e71ea..0000000 --- a/src/main/java/lol/fairplay/ghidraapple/analysis/passes/objcclasses/OCClassFieldAnalyzer.kt +++ /dev/null @@ -1,174 +0,0 @@ -package lol.fairplay.ghidraapple.analysis.passes.objcclasses - -import ghidra.app.services.AbstractAnalyzer -import ghidra.app.services.AnalysisPriority -import ghidra.app.services.AnalyzerType -import ghidra.app.util.importer.MessageLog -import ghidra.program.model.address.AddressSetView -import ghidra.program.model.address.GenericAddress -import ghidra.program.model.data.DataType -import ghidra.program.model.data.Structure -import ghidra.program.model.listing.Data -import ghidra.program.model.listing.Program -import ghidra.program.model.scalar.Scalar -import ghidra.program.model.symbol.Symbol -import ghidra.util.task.TaskMonitor -import lol.fairplay.ghidraapple.analysis.objectivec.GhidraTypeBuilder -import lol.fairplay.ghidraapple.analysis.objectivec.TypeResolver -import lol.fairplay.ghidraapple.analysis.utilities.tryResolveNamespace -import lol.fairplay.ghidraapple.core.objc.encodings.EncodingLexer -import lol.fairplay.ghidraapple.core.objc.encodings.TypeEncodingParser - -private data class IVarField(val name: String, val type: String, val size: Int, val offset: Int) -private data class IVarFieldList(val classSymbol: Symbol, val ivars: List) - -@Deprecated("Deprecated in favor of OCStructureAnalyzer") -class OCClassFieldAnalyzer : AbstractAnalyzer(NAME, DESCRIPTION, AnalyzerType.DATA_ANALYZER) { - - lateinit var program: Program - lateinit var log: MessageLog - - companion object { - const val NAME = "Objective-C Class Field Analyzer (Deprecated)" - const val DESCRIPTION = "Creates field entries in class structures generated by the \"Objective-C Structures\" pass." - val PRIORITY = AnalysisPriority.DATA_ANALYSIS - } - - init { - priority = PRIORITY - setPrototype() - setSupportsOneTimeAnalysis() - } - - override fun canAnalyze(program: Program): Boolean { - this.program = program - - val objcConstSection = program.memory.getBlock("__objc_const") -// return objcConstSection != null - return false - } - - override fun added(program: Program, set: AddressSetView, monitor: TaskMonitor, log: MessageLog): Boolean { - this.log = log - val typeResolver = TypeResolver(program) - val idDataType = program.dataTypeManager.getDataType("/_objc2_/ID") - val fieldLists = getIVarListsInAddressSet(set, monitor) ?: return false - - monitor.message = "Applying class structure fields..." - monitor.progress = 0 - monitor.maximum = fieldLists.size.toLong() - - program.withTransaction("Apply fields.") { - for (it in fieldLists) { - val definedClassStruct = typeResolver.tryResolveDefinedStruct(it.classSymbol.name) as Structure? - if (definedClassStruct == null) { - log.appendMsg("Couldn't find defined structure for ${it.classSymbol.name} ivar list.") - continue - } - - for (field in it.ivars) { - var fieldType: DataType? = null - try { - println("Encoded: ${field.type}") - fieldType = getTypeFromEncoding(field.type) - } catch (e: Exception) { - log.appendMsg("Failed to resolve type: ${field.type}") - continue - } - - println("${field.name}: ${fieldType.name}") - - val fieldSize = field.size.toInt() - - definedClassStruct.insertAtOffset(field.offset, fieldType, fieldSize, field.name, null) - } - - monitor.incrementProgress() - } - } - - program.withTransaction("Change __objc_const section permissions.") { - val objcConstSection = program.memory.getBlock("__objc_const") - objcConstSection.setPermissions(true, false, false) - } - - return true - } - - private fun getTypeFromEncoding(encoded: String): DataType { - val lexer = EncodingLexer(encoded) - val parser = TypeEncodingParser(lexer) - - val root = parser.parse() - val builder = GhidraTypeBuilder(program) - root.accept(builder) - - return builder.getResult() - } - - private fun getIVarListsInAddressSet(set: AddressSetView, monitor: TaskMonitor?): List? { - - monitor?.message = "Parsing ivar list structures..." - - val ivarNamespace = tryResolveNamespace(program, "objc", "ivar_list_t") ?: return null - - val ivarNamespaceName = ivarNamespace.getName(true) - - var ivarLists = program.listing.getDefinedData(set, true) - .filter { data -> - val primarySymbol = data.primarySymbol - val parentNamespace = primarySymbol?.parentNamespace - primarySymbol != null && - parentNamespace != null && - parentNamespace.getName(true) == ivarNamespaceName - } - - monitor?.maximum = ivarLists.size.toLong() - monitor?.progress = 0 - - val parsedLists = ivarLists.mapNotNull { data -> - val result = parseIVarFieldList(data) - monitor?.incrementProgress() - result - }.toList() - - return if (parsedLists.isNotEmpty()) parsedLists else null - } - - private fun parseIVarFieldList(data: Data): IVarFieldList? { - val definedClassStruct = data.primarySymbol - val ivFields = mutableListOf() - - if (data.numComponents <= 2) - return null - - for (i in 2 until data.numComponents) { - - // struct ivar_t { - // Off Type Len Name - // 0 qword* 8 offset "" - // 8 string* 8 name "" - // 16 string* 8 type "" - // 24 dword 4 alignment "" - // 28 dword 4 size "" - // } - - if (data.getComponent(i).dataType.name != "ivar_t") - continue - - val fields = (0.. - monitor.incrementProgress() - var parsed: OCClass? = null - - try { - parsed = context.parseClass(data.address.unsignedOffset) - } catch (e: Exception) { - Msg.error(this, "Could not parse class $name into a model: $e") - return@map null - } catch (e: Error) { - Msg.error(this, "Could not parse class $name into a model: $e") - return@map null - } - - return@map parsed - }.filterNotNull() - - monitor.message = "Analyzing class models..." - monitor.progress = 0 - monitor.maximum = models.size.toLong() - - program.withTransaction("Apply types") { - for (klass in models) { - monitor.incrementProgress() - - monitor.message = "Analyzing properties: ${klass.name}" - Msg.info(this, "Analyzing properties for ${klass.name}") - val properties = klass.getCollapsedProperties() ?: continue - val methodMapping = klass.baseMethods?.associateBy { it.name } ?: continue - - properties.forEach { property -> - val methodGetter = methodMapping[property.name] ?: return@forEach - - val setterName = "set${property.name[0].uppercase()}${property.name.substring(1)}:" - val methodSetter = methodMapping[setterName] ?: return@forEach - - val propertyDataType = runCatching { - val parsed = property.type?.first ?: return@runCatching null - typeResolver.buildParsed(parsed) - }.onFailure { - Msg.error(this, "Failed to recover type for ${property.name} ${property.type}") - }.getOrNull() ?: return@forEach - - val fnGetter = program.functionManager.getFunctionAt(program.address(methodGetter.implAddress!!.toLong())) - val fnSetter = program.functionManager.getFunctionAt(program.address(methodSetter.implAddress!!.toLong())) - - fnGetter.setReturnType(propertyDataType, SourceType.ANALYSIS) - fnSetter.getParameter(2).setDataType(propertyDataType, SourceType.ANALYSIS) - } - } - } - - return true - } - -} +//package lol.fairplay.ghidraapple.analysis.passes.objcclasses +// +//import ghidra.app.services.AbstractAnalyzer +//import ghidra.app.services.AnalyzerType +//import ghidra.app.util.importer.MessageLog +//import ghidra.program.model.address.AddressSetView +//import ghidra.program.model.listing.Program +//import ghidra.program.model.symbol.SourceType +//import ghidra.util.Msg +//import ghidra.util.task.TaskMonitor +//import lol.fairplay.ghidraapple.analysis.objectivec.TypeResolver +//import lol.fairplay.ghidraapple.analysis.objectivec.modelling.StructureParsing +//import lol.fairplay.ghidraapple.analysis.utilities.address +//import lol.fairplay.ghidraapple.analysis.utilities.idealClassStructures +//import lol.fairplay.ghidraapple.core.objc.encodings.PropertyAttribute +//import lol.fairplay.ghidraapple.core.objc.modelling.OCClass +// +//class OCClassPropertiesAnalyzer : AbstractAnalyzer(NAME, DESCRIPTION, AnalyzerType.FUNCTION_ANALYZER) { +// +// companion object { +// const val NAME = "Objective-C: Class Properties" +// const val DESCRIPTION = "Apply property types to their corresponding methods. Depends on structure analysis pass." +// val PRIORITY = OCStructureAnalyzer.PRIORITY.after() +// } +// +// init { +// priority = PRIORITY +// setPrototype() +// setSupportsOneTimeAnalysis() +// } +// +// lateinit var program: Program +// +// override fun canAnalyze(program: Program): Boolean { +// this.program = program +// +// program.memory.getBlock("__objc_classlist") ?: return false +// program.memory.getBlock("__objc_protolist") ?: return false +// +// return true +// } +// +// override fun added(program: Program, set: AddressSetView, monitor: TaskMonitor, log: MessageLog): Boolean { +// +// monitor.message = "Analyzing class structures..." +// monitor.isIndeterminate = true +// monitor.progress = 0 +// +// val structures = idealClassStructures(program) ?: return false +// +// monitor.maximum = structures.size.toLong() +// monitor.isIndeterminate = false +// +// val context = StructureParsing(program) +// val typeResolver = TypeResolver(program) +// +// val models = structures.map { (name, data) -> +// monitor.incrementProgress() +// var parsed: OCClass? = null +// +// try { +// parsed = context.parseClass(data.address.unsignedOffset) +// } catch (e: Exception) { +// Msg.error(this, "Could not parse class $name into a model: $e") +// return@map null +// } catch (e: Error) { +// Msg.error(this, "Could not parse class $name into a model: $e") +// return@map null +// } +// +// return@map parsed +// }.filterNotNull() +// +// monitor.message = "Analyzing class models..." +// monitor.progress = 0 +// monitor.maximum = models.size.toLong() +// +// program.withTransaction("Apply types") { +// for (klass in models) { +// monitor.incrementProgress() +// +// monitor.message = "Analyzing properties: ${klass.name}" +// Msg.info(this, "Analyzing properties for ${klass.name}") +// val properties = klass.resolvedProperties() +// val methodMapping = klass.baseInstanceMethods?.associateBy { it.name } ?: continue +// +// properties.forEach { property -> +// println("Property: ${property.name}") +// if (property.parent != klass) { +// Msg.error(this, "Unsupported property: ${property.name} is not from the current class") +// return@forEach +// } +// +// val getterName = property.customGetter ?: property.name +// val methodGetter = methodMapping[getterName] ?: return@forEach +// +// val propertyDataType = runCatching { +// val parsed = property.type?.first ?: return@runCatching null +// typeResolver.buildParsed(parsed) +// }.onFailure { +// Msg.error(this, "Failed to recover type for ${property.name} ${property.type}") +// }.getOrNull() ?: return@forEach +// +// val fnGetter = program.functionManager.getFunctionAt(program.address(methodGetter.implAddress!!.toLong())) +// fnGetter.setReturnType(propertyDataType, SourceType.ANALYSIS) +// +// if (!property.attributes.contains(PropertyAttribute.READ_ONLY)) { +// val setterName = property.customSetter ?: "set${property.name[0].uppercase()}${property.name.substring(1)}:" +// val methodSetter = methodMapping[setterName]!! +// val fnSetter = program.functionManager.getFunctionAt(program.address(methodSetter.implAddress!!.toLong())) +// fnSetter.getParameter(2).setDataType(propertyDataType, SourceType.ANALYSIS) +// } +// +// } +// } +// } +// +// return true +// } +// +//} diff --git a/src/main/java/lol/fairplay/ghidraapple/analysis/passes/objcclasses/OCMethodAnalyzer.kt b/src/main/java/lol/fairplay/ghidraapple/analysis/passes/objcclasses/OCMethodAnalyzer.kt new file mode 100644 index 0000000..22f750b --- /dev/null +++ b/src/main/java/lol/fairplay/ghidraapple/analysis/passes/objcclasses/OCMethodAnalyzer.kt @@ -0,0 +1,299 @@ +package lol.fairplay.ghidraapple.analysis.passes.objcclasses + +import ghidra.app.services.AbstractAnalyzer +import ghidra.app.services.AnalyzerType +import ghidra.app.util.importer.MessageLog +import ghidra.program.model.address.AddressSetView +import ghidra.program.model.listing.Function +import ghidra.program.model.listing.ParameterImpl +import ghidra.program.model.listing.Program +import ghidra.program.model.symbol.SourceType +import ghidra.util.Msg +import ghidra.util.task.TaskMonitor +import lol.fairplay.ghidraapple.analysis.objectivec.TypeResolver +import lol.fairplay.ghidraapple.analysis.objectivec.modelling.StructureParsing +import lol.fairplay.ghidraapple.analysis.utilities.address +import lol.fairplay.ghidraapple.analysis.utilities.parseObjCListSection +import lol.fairplay.ghidraapple.core.objc.encodings.EncodedSignature +import lol.fairplay.ghidraapple.core.objc.modelling.* + +class OCMethodAnalyzer : AbstractAnalyzer(NAME, DESCRIPTION, AnalyzerType.FUNCTION_ANALYZER) { + + lateinit var program: Program + lateinit var log: MessageLog + + companion object { + const val NAME = "Objective-C: Method Analyzer" + const val DESCRIPTION = "Performs a variety of method-related analyses." + const val PROPERTY_TAG_GETTER = "OBJC_PROPERTY_GETTER" + const val PROPERTY_TAG_SETTER = "OBJC_PROPERTY_SETTER" + val PRIORITY = OCStructureAnalyzer.PRIORITY.after() + } + + init { + priority = PRIORITY + setPrototype() + } + + override fun canAnalyze(program: Program): Boolean { + this.program = program + return program.memory.getBlock("__objc_classlist") != null + } + + override fun added(program: Program, set: AddressSetView, monitor: TaskMonitor, log: MessageLog): Boolean { + this.log = log + + monitor.message = "Reading classes..." + + val klasses = parseObjCListSection(program, "__objc_classlist") ?: return false + + monitor.maximum = klasses.size.toLong() + monitor.message = "Parsing class structures..." + + val parser = StructureParsing(program) + klasses.forEach { klassData -> + monitor.incrementProgress() + + val model = runCatching { + parser.parseClass(klassData.address.unsignedOffset) + }.onFailure { exception -> + Msg.error(this, "Could not parse class at ${klassData.address.unsignedOffset}", exception) + }.getOrNull() ?: return@forEach + + monitor.message = "Propagating signatures for ${model.name}..." + propagateSignatures(model, monitor) + + monitor.message = "Analyzing properties for ${model.name}..." + processProperties(model, monitor) + } + + return true + } + + private fun propagateSignatures(klass: OCClass, taskMonitor: TaskMonitor?) { + taskMonitor?.message = "Propagating signatures: ${klass.name}" + + val typeResolver = TypeResolver(program) + val methods = klass.resolvedMethods() + + methods.forEach { resolution -> + val method = resolution.concrete() + + if (method.implAddress == null) { + Msg.error(this, "Method ${klass.name}->${method.name} has no implementation address!") + return@forEach + } + + if (method.parent != klass) { + Msg.info(this, "Method ${klass.name}->${method.name} does not belong to class ${klass.name}! Skipping...") + return@forEach + } + + val fcnEntity = program.listing.getFunctionAt(program.address(method.implAddress)) + if (fcnEntity == null) { + Msg.error(this, "Could not find method ${klass.name}->${method.name} at ${method.implAddress}") + return@forEach + } + + val encSignature = resolution.bestSignature().first ?: return@forEach + + // Reconstruct the return type for the method. + applySignature(typeResolver, encSignature, klass, method, fcnEntity) + applyMethodInsights(resolution, fcnEntity) + } + } + + private fun definitionChain( + resolution: ResolvedEntity, + indent: String + ): String { + // fixme: this is kind of sloppy + val chain = resolution.chain().reversed() + .joinToString(" -> ") { + val type = when (it.first) { + is OCClass -> "Class" + is OCProtocol -> "Protocol" + else -> "Category" + } + + "${it.first.name} ($type)" + }.let { + if (resolution.stack.size == 1) "" else "\n${indent}Origin: ${it}" + } + + return chain + } + + private fun applyMethodInsights( + resolution: ResolvedMethod, + fcnEntity: Function + ) { + println("Applying method insights to ${resolution.concrete().name}...") + val method = resolution.concrete() + val chain = definitionChain(resolution, " ") + + fcnEntity.comment = """ + + Member of: ${method.parent.name}$chain + + ${method.prototypeString()} + + """.trimIndent() + } + + private fun applySignature( + typeResolver: TypeResolver, + encSignature: EncodedSignature, + klass: OCClass, + method: OCMethod, + fcnEntity: Function + ) { + val returnDT = runCatching { + typeResolver.buildParsed(encSignature.returnType.first) + }.onFailure { exception -> + Msg.error(this, "Could not parse return type for ${klass.name}->${method.name}", exception) + }.getOrNull() + + // starting at 2 to skip `self` and SEL. + val parameters = mutableListOf() + + val recvType = typeResolver.tryResolveDefinedStructPtr(klass.name) + ?: program.dataTypeManager.getDataType("/_objc2_/ID")!! + + parameters.add(ParameterImpl("self", recvType, 0, program)) + parameters.add(ParameterImpl("selector", program.dataTypeManager.getDataType("/_objc2_/SEL")!!, 8, program)) + var newNames = parameterNamesForMethod(method.name) + + // Reconstruct and apply parameter types. + encSignature.parameters.forEachIndexed { i, (type, stackOffset, modifiers) -> + val paramDT = runCatching { + typeResolver.buildParsed(type) + }.onFailure { exception -> + Msg.error( + this, + "Could not parse argument ${i + 2} type for ${klass.name}->${method.name}", + exception + ) + }.getOrNull() ?: return@forEachIndexed + + Msg.info(this, "Applying argument ${i + 2} type to function for ${klass.name}->${method.name}...") + + parameters.add(ParameterImpl(newNames[i], paramDT, stackOffset, program)) + } + + + val returnVar = fcnEntity.getReturn() + if (returnDT != null) { + returnVar.setDataType(returnDT, SourceType.ANALYSIS) + } + + println(newNames) + + fcnEntity.updateFunction( + null, + returnVar, + parameters, + Function.FunctionUpdateType.DYNAMIC_STORAGE_ALL_PARAMS, + true, + SourceType.ANALYSIS + ) + } + + private fun applyPropertyInsights(resolution: ResolvedProperty, getter: Function, setter: Function?) { + val declaration = resolution.concrete().declaration() + val definitionChain = definitionChain(resolution, " ") + + println("Applying property insights to ${resolution.concrete().name}...") + + getter.addTag(PROPERTY_TAG_GETTER) + val comment = """ + + Member of: ${resolution.concrete().parent.name}$definitionChain + + $declaration + """.trimIndent() + + getter.comment = comment + + setter?.addTag(PROPERTY_TAG_SETTER) + setter?.comment = comment + } + + private fun processProperties(klass: OCClass, taskMonitor: TaskMonitor?) { + + val fm = program.functionManager + if (fm.functionTagManager.getFunctionTag(PROPERTY_TAG_GETTER) == null) { + fm.functionTagManager.createFunctionTag(PROPERTY_TAG_GETTER, "Objective-C Property Getter Implementation") + } + if (fm.functionTagManager.getFunctionTag(PROPERTY_TAG_SETTER) == null) { + fm.functionTagManager.createFunctionTag(PROPERTY_TAG_SETTER, "Objective-C Property Setter Implementation") + } + + val baseMethods = klass.baseInstanceMethods?.associateBy { it.name } ?: return + + klass.resolvedProperties().forEach { + val property = it.concrete() + + taskMonitor?.message = "Analyzing property: ${property.name}" + + val getterName = property.customGetter ?: property.name + val setterName = property.customSetter ?: "set${property.name.replaceFirstChar { it.uppercase() }}:" + + val methodGetter = baseMethods[getterName] ?: return@forEach + val methodSetter = baseMethods[setterName] + + val getter = fm.getFunctionAt(program.address(methodGetter.implAddress!!.toLong())) + val setter = methodSetter?.let { fm.getFunctionAt(program.address(it.implAddress!!.toLong())) } + + applyPropertyInsights(it, getter, setter) + } + } + + fun splitCamelCase(input: String): List { + return input.split(Regex("(?<=[a-zA-Z])(?=[A-Z])")) + } + + private fun parameterNamesForMethod(methodName: String): List { + // todo: make this optional. + // create parameter names, acknowledging common objective-c naming conventions. + + val keywords = listOf("with", "for", "from", "to", "in", "at") + + val baseNames = methodName.split(":") + .filter{ !it.isEmpty() } + .map { part -> + val ccSplit = splitCamelCase(part) + + val matchIndex = ccSplit.indexOfFirst { + it.lowercase() in keywords + } + val match = ccSplit.getOrNull(matchIndex) ?: return@map part + + when (match.lowercase()) { + "for" -> { + if (part.startsWith(match)) { + part.substringAfter(match).replaceFirstChar { it.lowercase() } + } else { + part.substringAfter(match).replaceFirstChar { it.lowercase() } + } + } + in keywords -> part.substringAfter(match).replaceFirstChar { it.lowercase() } + else -> part + } + } + + val uniqueNames = mutableMapOf( + "self" to 1, + "selector" to 1, + ) + + val result = baseNames.map { name -> + val count = uniqueNames.getOrDefault(name, 0) + uniqueNames[name] = count + 1 + if (count > 0) "${name}_$count" else name + } + + return result + } + +} diff --git a/src/main/java/lol/fairplay/ghidraapple/analysis/passes/objcclasses/OCRetypeRecvAnalyzer.kt b/src/main/java/lol/fairplay/ghidraapple/analysis/passes/objcclasses/OCRetypeRecvAnalyzer.kt deleted file mode 100644 index a3c6740..0000000 --- a/src/main/java/lol/fairplay/ghidraapple/analysis/passes/objcclasses/OCRetypeRecvAnalyzer.kt +++ /dev/null @@ -1,87 +0,0 @@ -package lol.fairplay.ghidraapple.analysis.passes.objcclasses - -import ghidra.app.services.AbstractAnalyzer -import ghidra.app.services.AnalysisPriority -import ghidra.app.services.AnalyzerType -import ghidra.app.util.importer.MessageLog -import ghidra.program.model.address.AddressSetView -import ghidra.program.model.data.CategoryPath -import ghidra.program.model.data.DataType -import ghidra.program.model.data.PointerDataType -import ghidra.program.model.listing.Function -import ghidra.program.model.listing.Program -import ghidra.program.model.symbol.SourceType -import ghidra.util.task.TaskMonitor - -class OCRetypeRecvAnalyzer : AbstractAnalyzer(NAME, DESCRIPTION, AnalyzerType.FUNCTION_ANALYZER) { - - lateinit var program: Program - lateinit var log: MessageLog - - companion object { - const val NAME = "Objective-C: Retype Receiver Arguments" - const val DESCRIPTION = "Retype receiver arguments for class methods." - val PRIORITY = AnalysisPriority.FUNCTION_ANALYSIS.after() - } - - init { - priority = PRIORITY - setPrototype() - setSupportsOneTimeAnalysis() - } - - override fun canAnalyze(program: Program): Boolean { - this.program = program - - val objcConstSection = program.memory.getBlock("__objc_const") - return objcConstSection != null - } - - override fun added(program: Program, addresses: AddressSetView, monitor: TaskMonitor, log: MessageLog): Boolean { - this.log = log - val classMethods = getClassMethods() - - program.withTransaction("Apply receiver types to class methods.") { - classMethods.forEach { (typedef, methods) -> - println("CLASS ${typedef.name}") - - for (method in methods) { - println(" METHOD ${method.name}") - - if (method.parameterCount == 0) { - continue - } - - val param = method.getParameter(0) - if (!param.isAutoParameter) { - param.setDataType(PointerDataType(typedef), SourceType.ANALYSIS) - } - } - } - } - - return true - } - - private fun getClassMethods(): HashMap> { - val dtCategory = CategoryPath("/GA_OBJC") - - val classTypes = program.dataTypeManager.getCategory(dtCategory).dataTypes - - val cns = program.symbolTable.classNamespaces.asSequence().toList() - - val result = hashMapOf>() - for (entry in classTypes) { - val klass = cns.firstOrNull { - it.name.toString() == entry.name - } ?: continue - - val fcns = program.functionManager.getFunctions(klass.body, true) - - result[entry] = fcns.toList() - } - - return result - } - -} diff --git a/src/main/java/lol/fairplay/ghidraapple/analysis/passes/objcclasses/OCStructureAnalyzer.kt b/src/main/java/lol/fairplay/ghidraapple/analysis/passes/objcclasses/OCStructureAnalyzer.kt index d9cb933..923595e 100644 --- a/src/main/java/lol/fairplay/ghidraapple/analysis/passes/objcclasses/OCStructureAnalyzer.kt +++ b/src/main/java/lol/fairplay/ghidraapple/analysis/passes/objcclasses/OCStructureAnalyzer.kt @@ -6,34 +6,32 @@ import ghidra.app.services.AnalyzerType import ghidra.app.util.importer.MessageLog import ghidra.program.model.address.AddressSetView import ghidra.program.model.data.CategoryPath -import ghidra.program.model.data.DataType import ghidra.program.model.data.Structure import ghidra.program.model.data.StructureDataType import ghidra.program.model.listing.Data import ghidra.program.model.listing.Program -import ghidra.program.model.symbol.Namespace import ghidra.util.Msg import ghidra.util.task.TaskMonitor import lol.fairplay.ghidraapple.analysis.objectivec.TypeResolver import lol.fairplay.ghidraapple.analysis.objectivec.modelling.StructureParsing +import lol.fairplay.ghidraapple.analysis.utilities.StructureHelpers.deref import lol.fairplay.ghidraapple.analysis.utilities.StructureHelpers.derefUntyped import lol.fairplay.ghidraapple.analysis.utilities.StructureHelpers.get -import lol.fairplay.ghidraapple.analysis.utilities.getMembers -import lol.fairplay.ghidraapple.analysis.utilities.tryResolveNamespace -import lol.fairplay.ghidraapple.core.objc.modelling.OCClass +import lol.fairplay.ghidraapple.analysis.utilities.parseObjCListSection class OCStructureAnalyzer : AbstractAnalyzer(NAME, DESCRIPTION, AnalyzerType.BYTE_ANALYZER) { companion object { - private const val NAME = "Objective-C Structures" + private const val NAME = "Objective-C: Structures" private const val DESCRIPTION = "" val PRIORITY = AnalysisPriority.BLOCK_ANALYSIS.after() } - var classNamespace: Namespace? = null lateinit var program: Program + val structureCategory = CategoryPath("/GA_OBJC") + init { priority = PRIORITY setPrototype() @@ -41,110 +39,109 @@ class OCStructureAnalyzer : AbstractAnalyzer(NAME, DESCRIPTION, AnalyzerType.BYT override fun canAnalyze(program: Program): Boolean { this.program = program + return program.memory.getBlock("__objc_classlist") != null + || program.memory.getBlock("__objc_protolist") != null } override fun added(program: Program, set: AddressSetView, monitor: TaskMonitor, log: MessageLog?): Boolean { - classNamespace = tryResolveNamespace(program, "objc", "class_t") ?: return false - val category = CategoryPath("/GA_OBJC") - val namespace = classNamespace!! - - monitor.message = "Creating structures..." - - // Get the most information-rich form of each class structure. - // This works by preferring classes that do not have an `isa` field value of `_OBJC_METACLASS_$_NSObject` - val definedStructures = mutableMapOf() - val idealStructures = mutableMapOf() - namespace.getMembers().forEach { member -> - if (member.name !in idealStructures) { - val classStruct = StructureDataType(category, member.name, 0) - definedStructures[member.name] = program.dataTypeManager.addDataType(classStruct, null) - Msg.info(this, "Added ${member.name} structure.") - - idealStructures[member.name] = program.listing.getDefinedDataAt(member.address) - } else { - val data = idealStructures[member.name]!! - val superclassExisting = data[0].derefUntyped() - if (superclassExisting.primarySymbol.name.startsWith("_OBJC_METACLASS_\$")) { - idealStructures[member.name] = program.listing.getDefinedDataAt(member.address) - } - } - } + monitor.message = "Reading list sections..." + + val classStructures = (parseObjCListSection(program, "__objc_classlist")?.mapNotNull { klassT -> + // class_t[4]->class_rw[3]->name + runCatching { + klassT[4].derefUntyped()[3].deref() to klassT + }.onFailure { + Msg.error(this, "Failed to parse class data at ${klassT.address}") + }.getOrNull() + }?.toMap() ?: emptyMap()).toMutableMap() + + + val protocolStructures = (parseObjCListSection(program, "__objc_protolist")?.associate { protoT -> + // protocol_t[1]->name + protoT[1].deref() to protoT + } ?: emptyMap()).toMutableMap() // Some class symbols that are not inside the objc::class_t namespace are prefixed with `_OBJC_CLASS_$_` // These are not parsable, but are still useful for analysis purposes. - program.symbolTable.symbolIterator.filter { + val externalClasses = program.symbolTable.symbolIterator.filter { it.name.startsWith("_OBJC_CLASS_\$_") - }.forEach { + }.mapNotNull { val className = it.name.removePrefix("_OBJC_CLASS_\$_") - if (className !in definedStructures) { - val classStruct = StructureDataType(category, className, 0) - definedStructures[className] = program.dataTypeManager.addDataType(classStruct, null) - Msg.info(this, "Added structure for external class $className") + + // just for sanity, ensure it's not already in either of the structure mappings. + if (className !in classStructures && className !in protocolStructures) { + className + } else { + null } } - monitor.message = "Creating fields..." - monitor.maximum = idealStructures.size.toLong() - monitor.progress = 0 + buildStructureTypes(classStructures, protocolStructures, externalClasses, monitor) + + return true + } + + private fun buildStructureTypes( + klassData: Map, + protoData: Map, + externalClasses: List, + taskMonitor: TaskMonitor? + ): Boolean { - val context = StructureParsing(program) + val parser = StructureParsing(program) val typeResolver = TypeResolver(program) - // Recover the structure fields by parsing each eligible class into our detailed model... - - program.withTransaction("Applying class structure fields.") { - for ((name, data) in idealStructures) { - monitor.incrementProgress() - - Msg.info(this, "Analyzing class $name at ${data.address}...") - - val structAddress = data.address - - // Parse the class structure into our custom model. - var classModel: OCClass? = null - try { - classModel = context.parseClass(structAddress.unsignedOffset) ?: continue - } catch (e: Exception) { - Msg.error(this, "Could not parse class $name into a model: $e") - continue - } catch (e: Error) { - Msg.error(this, "Could not parse class $name into a model: $e") - continue - } - - val definedStructure = definedStructures[name] ?: continue - - for (ivar in classModel.instanceVariables ?: continue) { - - // Attempt to reconstruct the Ghidra DataType from the encoded type AST... - var fieldType: DataType? = null - try { - Msg.info(this, "Reconstructing type for ivar ${ivar.name}: ${ivar.type}") - fieldType = typeResolver.buildParsed(ivar.type) ?: continue - } catch (exception: Exception) { - Msg.error(this, "Could not recover type for $ivar: $exception") - continue - } catch (error: Error) { - Msg.error(this, "Could not recover type for $ivar: $error") - continue - } - - // Apply the new field to our pre-defined structure. - (definedStructure as Structure).insertAtOffset( - ivar.offset.toInt(), - fieldType, - ivar.size, - ivar.name, - null - ) - - Msg.info(this, "${ivar.name} -> $fieldType (${ivar.type})") - } + taskMonitor?.maximum = (klassData.size + protoData.size + externalClasses.size).toLong() + taskMonitor?.progress = 0 + + taskMonitor?.message = "Creating nullary types..." + + externalClasses.forEach { + println("Creating nullary: $it") + program.dataTypeManager.addDataType(StructureDataType(structureCategory, it, 0), null) + } + + protoData.forEach { (name, data) -> + taskMonitor?.incrementProgress() + + program.dataTypeManager.addDataType(StructureDataType(structureCategory, "<$name>", 0), null) + } + + // Create class types with fields. + klassData.forEach { (name, data) -> + val dataType = program.dataTypeManager.addDataType(StructureDataType(structureCategory, name, 0), null) + + taskMonitor?.incrementProgress() + + // Attempt to parse the class into the analysis models. + val model = runCatching { + parser.parseClass(data.address.unsignedOffset) + }.onFailure { + Msg.error(this, "Could not parse class $name into a model: $it") + }.getOrNull() ?: return@forEach + + // Create the instance variables for the structure. + for (ivar in model.instanceVariables ?: return@forEach) { + + var fieldType = runCatching { + typeResolver.buildParsed(ivar.type) + }.onFailure { + Msg.error(this, "Could not reconstruct type for ivar ${model.name}->${ivar.name}") + }.getOrNull() ?: continue + + (dataType as Structure).insertAtOffset( + ivar.offset.toInt(), + fieldType, + ivar.size, + ivar.name, + null + ) } } return true } - } + + diff --git a/src/main/java/lol/fairplay/ghidraapple/analysis/passes/objcclasses/OCTypeInjectorAnalyzer.kt b/src/main/java/lol/fairplay/ghidraapple/analysis/passes/objcclasses/OCTypeInjectorAnalyzer.kt index fca3bf9..8da420b 100644 --- a/src/main/java/lol/fairplay/ghidraapple/analysis/passes/objcclasses/OCTypeInjectorAnalyzer.kt +++ b/src/main/java/lol/fairplay/ghidraapple/analysis/passes/objcclasses/OCTypeInjectorAnalyzer.kt @@ -89,7 +89,7 @@ class OCTypeInjectorAnalyzer : AbstractAnalyzer(NAME, DESCRIPTION, AnalyzerType. val objectLookUp: Map = program.symbolTable.symbolIterator .filter { it.name.startsWith("_OBJC_CLASS_\$_") || it.parentNamespace.name == "class_t" } .filter { !it.isExternal } - .map { it.address.offset to it}.toMap() + .associate { it.address.offset to it } val decompiler = DecompInterface() decompiler.openProgram(program) @@ -145,8 +145,12 @@ class OCTypeInjectorAnalyzer : AbstractAnalyzer(NAME, DESCRIPTION, AnalyzerType. } val allocatedSymbol = objectLookUp[const.offset] if (allocatedSymbol != null){ - val ocType = getDataTypeFromSymbol(allocatedSymbol!!) - result.add(AllocInfo(function, allocCall.seqnum.target, generateFunctionSignatureForType(ocType))) + runCatching { + val ocType = getDataTypeFromSymbol(allocatedSymbol!!) + result.add(AllocInfo(function, allocCall.seqnum.target, generateFunctionSignatureForType(ocType))) + }.onFailure { error -> + Msg.error(this, "Failed to inject type for ${allocatedSymbol.name} in ${function.name}", error) + } } else { messageLog.appendMsg(NAME, "Could not find symbol for constant $const in ${function.name}") diff --git a/src/main/java/lol/fairplay/ghidraapple/analysis/passes/selectortrampoline/ARCFixupInstaller.kt b/src/main/java/lol/fairplay/ghidraapple/analysis/passes/selectortrampoline/ARCFixupInstaller.kt index aaba00d..7792476 100644 --- a/src/main/java/lol/fairplay/ghidraapple/analysis/passes/selectortrampoline/ARCFixupInstaller.kt +++ b/src/main/java/lol/fairplay/ghidraapple/analysis/passes/selectortrampoline/ARCFixupInstaller.kt @@ -30,88 +30,69 @@ class ARCFixupInstallerAnalyzer() : AbstractAnalyzer(NAME, DESCRIPTION, Analyzer return ObjectiveC2_Constants.isObjectiveC2(program) } - override fun added(program: Program?, set: AddressSetView?, monitor: TaskMonitor?, log: MessageLog?): Boolean { - val specExtension = SpecExtension(program) - - val retainSpec = """ - - - - - - - - - - - - - - - - """.trimIndent() - val releaseSpec = """ - - + private fun createCallFixupXML(name: String, code: String, vararg targets: String ): String { + return """ + + ${targets.joinToString("\n") { "" }} """.trimIndent() + } + override fun added(program: Program?, set: AddressSetView?, monitor: TaskMonitor?, log: MessageLog?): Boolean { + val specExtension = SpecExtension(program) - val storeStrongSpec = """ - - - - - - - """.trimIndent() - val loadSpec = """ - - - - - - - - """.trimIndent() + val retainSpec = createCallFixupXML( + "_objc_retain", + "x0 = x0;", + "_objc_retain", + "_objc_retainAutoreleasedReturnValue", + "_objc_retainAutoreleaseReturnValue", + "_objc_autoreleaseReturnValue", + "_objc_retainAutorelease", + "_objc_autorelease", + "_objc_claimAutoreleasedReturnValue", + "___chkstk_darwin", + "_objc_opt_self" + ) - val getPropertySpec = """ - - - - - - - """.trimIndent() + val releaseSpec = createCallFixupXML( + "objc_release", + "x0 = 0;", + "_objc_release") - val setPropertSpec = """ - - - - - - - """.trimIndent() + val storeStrongSpec = createCallFixupXML( + "_objc_storeStrong", + "*x0 = x1;", + "_objc_storeStrong" + ) + + val loadSpec = createCallFixupXML( + "_objc_loadWeakRetained", + "x0 = *x0;", + "_objc_loadWeakRetained" + ) + + val getPropertySpec = createCallFixupXML( + "_objc_getProperty", + "x0 = *(x0 + x2);", + "_objc_getProperty" + ) + + val setPropertySpec = createCallFixupXML( + "_objc_setProperty", + "*(x0 + x3) = x2;", + "_objc_setProperty" + ) val specs = listOf( - retainSpec, releaseSpec, storeStrongSpec, loadSpec, getPropertySpec, setPropertSpec, + retainSpec, releaseSpec, storeStrongSpec, loadSpec, getPropertySpec, setPropertySpec, ) specs.forEach { specExtension.addReplaceCompilerSpecExtension(it, monitor) diff --git a/src/main/java/lol/fairplay/ghidraapple/analysis/passes/selectortrampoline/SelectorTrampolineAnalyzer.kt b/src/main/java/lol/fairplay/ghidraapple/analysis/passes/selectortrampoline/SelectorTrampolineAnalyzer.kt index 219b04e..c3af688 100644 --- a/src/main/java/lol/fairplay/ghidraapple/analysis/passes/selectortrampoline/SelectorTrampolineAnalyzer.kt +++ b/src/main/java/lol/fairplay/ghidraapple/analysis/passes/selectortrampoline/SelectorTrampolineAnalyzer.kt @@ -80,8 +80,6 @@ class SelectorTrampolineAnalyzer : AbstractAnalyzer(NAME, DESCRIPTION, AnalyzerT } - - /** * Core of the trampoline analysis */ diff --git a/src/main/java/lol/fairplay/ghidraapple/analysis/utilities/pCodeUtils.kt b/src/main/java/lol/fairplay/ghidraapple/analysis/utilities/pCodeUtils.kt index 7d724ea..e3ae1df 100644 --- a/src/main/java/lol/fairplay/ghidraapple/analysis/utilities/pCodeUtils.kt +++ b/src/main/java/lol/fairplay/ghidraapple/analysis/utilities/pCodeUtils.kt @@ -4,6 +4,7 @@ import ghidra.program.model.address.Address import ghidra.program.model.pcode.PcodeOp import ghidra.program.model.pcode.Varnode import ghidra.program.model.listing.Program +import ghidra.program.model.listing.Function import ghidra.util.Msg import java.util.* @@ -42,6 +43,7 @@ fun getConstantFromPcodeOp(pcodeOp: PcodeOp): Optional
{ // Multiequal is a phi node, so we can't get _one_ constant from it PcodeOp.MULTIEQUAL -> return Optional.empty
() PcodeOp.INDIRECT -> return getConstantFromVarNode(pcodeOp.inputs[0]) + PcodeOp.CALL -> return Optional.empty() else -> { Msg.error("getConstantFromPcodeOp", "Unknown opcode ${pcodeOp.mnemonic} encountered at ${pcodeOp.seqnum.target}") @@ -63,3 +65,13 @@ fun Address.toDefaultAddressSpace(program: Program): Address { return program.addressFactory.defaultAddressSpace.getAddress(this.offset) } + +fun getFunctionForPCodeCall(program: Program, pcodeOp: PcodeOp?): Optional { + if (pcodeOp != null && pcodeOp.opcode == PcodeOp.CALL) { + val target = pcodeOp.inputs.getOrNull(0) ?: return Optional.empty() + if (target.isAddress) { + return Optional.of(program.functionManager.getFunctionAt(target.address)) + } + } + return Optional.empty() +} \ No newline at end of file diff --git a/src/main/java/lol/fairplay/ghidraapple/analysis/utilities/programUtils.kt b/src/main/java/lol/fairplay/ghidraapple/analysis/utilities/programUtils.kt index 3d1f8f8..492a003 100644 --- a/src/main/java/lol/fairplay/ghidraapple/analysis/utilities/programUtils.kt +++ b/src/main/java/lol/fairplay/ghidraapple/analysis/utilities/programUtils.kt @@ -1,11 +1,20 @@ package lol.fairplay.ghidraapple.analysis.utilities +import docking.widgets.table.AbstractDynamicTableColumn +import docking.widgets.table.TableColumnDescriptor +import ghidra.docking.settings.Settings +import ghidra.framework.plugintool.ServiceProvider import ghidra.program.model.address.Address import ghidra.program.model.address.AddressSetView +import ghidra.program.model.data.PointerDataType import ghidra.program.model.data.StructureDataType import ghidra.program.model.listing.Data +import ghidra.program.model.listing.Function import ghidra.program.model.listing.Program import ghidra.program.model.symbol.Namespace +import ghidra.program.model.symbol.RefType +import ghidra.program.model.symbol.ReferenceManager +import ghidra.program.model.symbol.SourceType import ghidra.program.model.symbol.Symbol import lol.fairplay.ghidraapple.analysis.utilities.StructureHelpers.derefUntyped import lol.fairplay.ghidraapple.analysis.utilities.StructureHelpers.get @@ -16,11 +25,12 @@ import lol.fairplay.ghidraapple.analysis.utilities.StructureHelpers.get * @param value The long value to be converted to an Address. * @return The Address object corresponding to the given long value. */ -fun Program.address(value: Long): Address { - return this.addressFactory.defaultAddressSpace.getAddress(value) -} +fun Program.address(value: Long): Address = this.addressFactory.defaultAddressSpace.getAddress(value) -fun tryResolveNamespace(program: Program, vararg fqnParts: String): Namespace? { +fun tryResolveNamespace( + program: Program, + vararg fqnParts: String, +): Namespace? { var ns = program.globalNamespace for (part in fqnParts) { ns = program.symbolTable.getNamespace(part, ns) ?: return null @@ -28,16 +38,22 @@ fun tryResolveNamespace(program: Program, vararg fqnParts: String): Namespace? { return ns } -fun Namespace.getMembers(): Iterable { - return this.symbol.program.symbolTable.getChildren(this.symbol) -} +fun Namespace.getMembers(): Iterable = + this.symbol.program.symbolTable + .getChildren(this.symbol) -fun dataBlocksForNamespace(program: Program, ns: Namespace, addresses: AddressSetView): List { - var dataBlocks = program.listing.getDefinedData(addresses, true) - .filter { data -> - val primarySymbol = data.primarySymbol - val parentNamespace = primarySymbol?.parentNamespace - primarySymbol != null && +fun dataBlocksForNamespace( + program: Program, + ns: Namespace, + addresses: AddressSetView, +): List { + var dataBlocks = + program.listing + .getDefinedData(addresses, true) + .filter { data -> + val primarySymbol = data.primarySymbol + val parentNamespace = primarySymbol?.parentNamespace + primarySymbol != null && parentNamespace != null && parentNamespace.getName(true) == ns.getName(true) } @@ -45,6 +61,7 @@ fun dataBlocksForNamespace(program: Program, ns: Namespace, addresses: AddressSe return dataBlocks } +@Deprecated("Use parseObjCListSection instead.") fun idealClassStructures(program: Program): Map? { val result = mapOf() val namespace = tryResolveNamespace(program, "objc", "class_t") ?: return null @@ -65,5 +82,68 @@ fun idealClassStructures(program: Program): Map? { return idealStructures } -fun dataAt(program: Program, address: Address): Data? = - program.listing.getDefinedDataAt(address) +fun parseObjCListSection(program: Program, sectionName: String): List? { + val sectionBlock = program.memory.getBlock(sectionName) ?: return null + val entries = sectionBlock.size / 8 + val start = sectionBlock.start + + return (0 until entries).map { + val pointerAddress = start.add(it * 8) + var data = program.listing.getDataAt(pointerAddress) + if (!data.isPointer) { + data = program.listing.createData(pointerAddress, PointerDataType.dataType) + } + val datAddress = data + .getPrimaryReference(0) + .toAddress + program.listing.getDefinedDataAt(datAddress) + } +} + +fun dataAt( + program: Program, + address: Address, +): Data? = program.listing.getDefinedDataAt(address) + +fun ReferenceManager.setCallTarget( + callsite: Address, + targetFunction: Function, + sourceType: SourceType, +) { + val ref = addMemoryReference(callsite, targetFunction.entryPoint, RefType.UNCONDITIONAL_CALL, sourceType, 0) + setPrimary(ref, true) +} + +fun TableColumnDescriptor.addColumn( + name: String, + visible: Boolean, + accessor: (ROW_TYPE) -> COLUMN_TYPE, +) { + if (visible) { + addVisibleColumn( + object : AbstractDynamicTableColumn() { + override fun getColumnName(): String = name + + override fun getValue( + rowObject: ROW_TYPE, + settings: Settings, + data: Any?, + serviceProvider: ServiceProvider, + ): COLUMN_TYPE = accessor(rowObject) + }, + ) + } else { + addHiddenColumn( + object : AbstractDynamicTableColumn() { + override fun getColumnName(): String = name + + override fun getValue( + rowObject: ROW_TYPE, + settings: Settings, + data: Any?, + serviceProvider: ServiceProvider, + ): COLUMN_TYPE = accessor(rowObject) + }, + ) + } +} diff --git a/src/main/java/lol/fairplay/ghidraapple/core/objc/encodings/Properties.kt b/src/main/java/lol/fairplay/ghidraapple/core/objc/encodings/Properties.kt index 497a7dd..6fea3ac 100644 --- a/src/main/java/lol/fairplay/ghidraapple/core/objc/encodings/Properties.kt +++ b/src/main/java/lol/fairplay/ghidraapple/core/objc/encodings/Properties.kt @@ -13,18 +13,37 @@ enum class PropertyAttribute(val code: Char) { WEAK('W'), STRONG('P'), NON_ATOMIC('N'), - OPTIONAL('?'); + NULLABLE('?'); companion object { fun fromCode(code: Char): PropertyAttribute? { return PropertyAttribute.entries.find {it.code == code} } } + + fun annotationString(): String? { + return when (this) { + READ_ONLY -> "readonly" + BY_COPY -> "copy" + BY_REFERENCE -> "retain" + DYNAMIC -> "dynamic" + CUSTOM_GETTER -> "getter=" + CUSTOM_SETTER -> "setter=" + BACKING_IVAR -> "ivar=" + WEAK -> "weak" + STRONG -> "strong" + NON_ATOMIC -> "nonatomic" + NULLABLE -> "nullable" + else -> null + } + } } data class EncodedProperty( val attributes: List, val type: Pair?>?, + val customGetter: String? = null, + val customSetter: String? = null, val backingIvar: String? = null, ) @@ -35,6 +54,8 @@ fun parseEncodedProperty(input: String): EncodedProperty { var type: Pair?>? = null var ivarName: String? = null val attributes = mutableListOf() + var customSetter: String? = null + var customGetter: String? = null for (a in stmts) { val signal = PropertyAttribute.fromCode(a[0])!! @@ -57,10 +78,21 @@ fun parseEncodedProperty(input: String): EncodedProperty { PropertyAttribute.BACKING_IVAR -> { ivarName = a.substring(1) } + PropertyAttribute.CUSTOM_GETTER, PropertyAttribute.CUSTOM_SETTER -> { + a.substring(1).let { + if (signal == PropertyAttribute.CUSTOM_SETTER) { + attributes.add(PropertyAttribute.CUSTOM_SETTER) + customSetter = it + } else { + attributes.add(PropertyAttribute.CUSTOM_GETTER) + customGetter = it + } + } + } else -> attributes.add(signal) } } - return EncodedProperty(attributes, type, ivarName) + return EncodedProperty(attributes, type, customGetter, customSetter, ivarName) } diff --git a/src/main/java/lol/fairplay/ghidraapple/core/objc/encodings/SignatureParser.kt b/src/main/java/lol/fairplay/ghidraapple/core/objc/encodings/SignatureParser.kt index 74a065d..b3649a2 100644 --- a/src/main/java/lol/fairplay/ghidraapple/core/objc/encodings/SignatureParser.kt +++ b/src/main/java/lol/fairplay/ghidraapple/core/objc/encodings/SignatureParser.kt @@ -40,8 +40,8 @@ class SignatureParser(lexer: EncodingLexer, val sigType: EncodedSignatureType) : modifiers.add(SignatureTypeModifier.fromCode(modifier.value)!!) // if the result of fromCode is null, we have bigger problems. } val type = parseType() - val number = expectToken().value - return Triple(type, number, modifiers.let { if (it.isEmpty()) null else it.toList() }) + val stackOffset = expectToken().value + return Triple(type, stackOffset, modifiers.let { if (it.isEmpty()) null else it.toList() }) } private fun parseType(): TypeNode { @@ -52,7 +52,6 @@ class SignatureParser(lexer: EncodingLexer, val sigType: EncodedSignatureType) : } fun parseSignature(input: String, type: EncodedSignatureType): EncodedSignature { -// println("Signature: $input") val lexer = EncodingLexer(input) val parser = SignatureParser(lexer, type) return parser.parse() diff --git a/src/main/java/lol/fairplay/ghidraapple/core/objc/encodings/TypeAST.kt b/src/main/java/lol/fairplay/ghidraapple/core/objc/encodings/TypeAST.kt index d6be858..0d976bb 100644 --- a/src/main/java/lol/fairplay/ghidraapple/core/objc/encodings/TypeAST.kt +++ b/src/main/java/lol/fairplay/ghidraapple/core/objc/encodings/TypeAST.kt @@ -82,5 +82,5 @@ interface TypeNodeVisitor { fun visitBitfield(bitfield: TypeNode.Bitfield) fun visitBlock(block: TypeNode.Block) fun visitFunctionPointer(fnPtr: TypeNode.FunctionPointer) - fun visitSelector(fnPtr: TypeNode.Selector) + fun visitSelector(selector: TypeNode.Selector) } diff --git a/src/main/java/lol/fairplay/ghidraapple/core/objc/encodings/TypeStringify.kt b/src/main/java/lol/fairplay/ghidraapple/core/objc/encodings/TypeStringify.kt new file mode 100644 index 0000000..d97872f --- /dev/null +++ b/src/main/java/lol/fairplay/ghidraapple/core/objc/encodings/TypeStringify.kt @@ -0,0 +1,152 @@ +package lol.fairplay.ghidraapple.core.objc.encodings + + +class TypeStringify : TypeNodeVisitor { + + private lateinit var result: String + + companion object { + fun getResult(node: TypeNode): String { + val vis = TypeStringify() + node.accept(vis) + return vis.result + } + + private fun indent(str: String) = str.prependIndent(" ") + } + + override fun visitStruct(struct: TypeNode.Struct) { + val builder = StringBuilder() + builder.append("struct ") + + if (struct.name != null) { + builder.append(struct.name) + } + + builder.append("{") + + if (struct.fields != null) { + for ((name, node) in struct.fields) { + + builder.append("\n") + val typeStr = getResult(node) + + var field = if (name != null) { + "$typeStr $name" + } else { + typeStr + } + + if (node is TypeNode.Bitfield) { + field += " : ${node.size}" + } + + builder.append("${indent(field)};") + } + } + + builder.append("\n}") + result = builder.toString() + } + + override fun visitClassObject(classObject: TypeNode.ClassObject) { + result = "[${classObject.name} class]" + } + + override fun visitObject(obj: TypeNode.Object) { + result = obj.name?.let { + "$it *" + } ?: "id" + } + + override fun visitUnion(union: TypeNode.Union) { + val builder = StringBuilder() + builder.append("union ") + + if (union.name != null) { + builder.append(union.name) + } + + builder.append("{") + + if (union.fields != null) { + for ((name, node) in union.fields) { + builder.append("\n") + val typeStr = getResult(node) + + val field = if (name != null) { + "$typeStr $name" + } else { + typeStr + } + + builder.append("${indent(field)};") + } + } + + builder.append("\n}") + result = builder.toString() + } + + override fun visitArray(array: TypeNode.Array) { + val type = getResult(array.elementType) + result = "$type [${array.size}]" + } + + override fun visitPrimitive(primitive: TypeNode.Primitive) { + result = when (primitive.type) { + 'c' -> "char" // this could also be `BOOL` + 'C' -> "unsigned char" + 's' -> "short" + 'S' -> "unsigned short" + 'i' -> "int" + 'I' -> "unsigned int" + 'l' -> "long" + 'L' -> "unsigned long" + 'q' -> "long long" + 'Q' -> "unsigned long long" + 'f' -> "float" + 'd' -> "double" + 'v' -> "void" + 'B' -> "bool" + 'D' -> "long double" + '*' -> "char*" + else -> throw Exception("Unknown primitive type: ${primitive.type}") + } + } + + override fun visitPointer(pointer: TypeNode.Pointer) { + result = getResult(pointer.pointee) + "*" + } + + override fun visitBitfield(bitfield: TypeNode.Bitfield) { + result = "" + } + + override fun visitBlock(block: TypeNode.Block) { + val builder = StringBuilder() + + if (block.returnType != null) { + builder.append(getResult(block.returnType)) + } + + builder.append(" (^)(") + + if (block.parameters != null && block.parameters.isNotEmpty()) { + block.parameters + .joinToString(", ") { getResult(it.first) } + .let { builder.append(it) } + } + + builder.append(")") + result = builder.toString() + } + + override fun visitFunctionPointer(fnPtr: TypeNode.FunctionPointer) { + result = "void*" + } + + override fun visitSelector(selector: TypeNode.Selector) { + result = "SEL" + } +} diff --git a/src/main/java/lol/fairplay/ghidraapple/core/objc/modelling/Flags.kt b/src/main/java/lol/fairplay/ghidraapple/core/objc/modelling/Flags.kt new file mode 100644 index 0000000..9c44bb5 --- /dev/null +++ b/src/main/java/lol/fairplay/ghidraapple/core/objc/modelling/Flags.kt @@ -0,0 +1,11 @@ +package lol.fairplay.ghidraapple.core.objc.modelling + +enum class ClassFlags(val bit: ULong) { + IS_SWIFT(1uL shl 0); + + companion object { + fun fromValue(flags: ULong): Set { + return entries.filter { (flags and it.bit) != 0uL }.toSet() + } + } +} diff --git a/src/main/java/lol/fairplay/ghidraapple/core/objc/modelling/Models.kt b/src/main/java/lol/fairplay/ghidraapple/core/objc/modelling/Models.kt index fd07936..e2b346e 100644 --- a/src/main/java/lol/fairplay/ghidraapple/core/objc/modelling/Models.kt +++ b/src/main/java/lol/fairplay/ghidraapple/core/objc/modelling/Models.kt @@ -4,103 +4,241 @@ import lol.fairplay.ghidraapple.core.objc.encodings.EncodedSignature import lol.fairplay.ghidraapple.core.objc.encodings.PropertyAttribute import lol.fairplay.ghidraapple.core.objc.encodings.SignatureTypeModifier import lol.fairplay.ghidraapple.core.objc.encodings.TypeNode +import lol.fairplay.ghidraapple.core.objc.encodings.TypeStringify -open class OCFieldContainer +open class OCFieldContainer(open val name: String) + +abstract class OCField(open val name: String) { + abstract fun parent(): OCFieldContainer +} + + +open class ResolvedEntity(val name: String, initial: T? = null) { + + // stack order is concrete to abstract + internal val stack = mutableListOf() + fun chain(): List> = stack.map { it.parent() to it } + fun concrete(): T = stack[0] + fun abstract(): List = if (stack.size != 1) stack.subList(1, stack.size) else listOf(stack[0]) + protected fun pushConcrete(t: T) = stack.add(0, t) + internal fun pushAbstract(t: T) = stack.add(t) + + init { + initial?.let { + pushConcrete(it) + } + } + + internal fun append(other: ResolvedEntity) { + other.stack.forEach { pushAbstract(it) } + } + + override fun toString(): String { + return "ResolvedEntity(name='$name', stack=$stack)" + } +} + +class ResolvedMethod(name: String, initial: OCMethod? = null) : ResolvedEntity(name, initial) { + + fun bestSignature(): Pair { + val impl = stack.find { + it.parent is OCProtocol && (it.parent as OCProtocol).extendedSignatures != null + } ?: stack.first() + return impl.getSignature() to impl.parent + } + +} + +class ResolvedProperty(name: String, initial: OCProperty? = null) : ResolvedEntity(name, initial) data class OCClass( - val name: String, - val flags: Long, + override val name: String, + val flags: ULong, val superclass: OCClass?, - var baseMethods: List?, + var baseClassMethods: List?, + var baseInstanceMethods: List?, var baseProtocols: List?, var instanceVariables: List?, - var baseProperties: List?, + var baseClassProperties: List?, + var baseInstanceProperties: List?, val weakIvarLayout: Long, -) : OCFieldContainer() { +) : OCFieldContainer(name) { - fun getInheritance(): List? { - if (superclass == null) { - return null - } - val superInheritance = superclass.getInheritance() - return if (superInheritance != null) { - listOf(superclass) + superInheritance + /** + * Returns the list of superclasses from concrete to abstract. + */ + fun getInheritance(): List { + return if (superclass == null) { + listOf() } else { - listOf(superclass) + listOf(superclass) + superclass.getInheritance() } } - fun getProperties(): List? { - // aggregate all baseProperties and properties from protocols. - val props = baseProperties?.toMutableList() ?: return null - baseProtocols?.forEach { - it.instanceProperties?.let { props.addAll(it) } + fun isSwift(): Boolean = (flags and ClassFlags.IS_SWIFT.bit) != 0uL + + fun resolvedProperties(): List { + // Initialize resolution mapping with the most concrete forms relative to this class. + val propertyMapping = baseProperties().associate { + it.name to ResolvedProperty(it.name, it) + }.toMutableMap() + + // Then, obtain resolutions for protocol methods, and append to ours. + baseProtocols?.forEach { protocol -> + protocol.resolvedProperties().forEach { propResolution -> + if (propertyMapping[propResolution.name] == null) { + propertyMapping[propResolution.name] = propResolution + } else { + propertyMapping[propResolution.name]!!.append(propResolution) + } + } } - return props.toList() - } - fun getCollapsedProperties(): List? { - val inheritance = getInheritance() - println("inheritance: $inheritance") - val myProperties = getProperties() - if (inheritance == null) { - return myProperties + // Leverage the inheritance path graph to further augment the resolutions. + superclass?.resolvedProperties()?.forEach { propResolution -> + if (propertyMapping[propResolution.name] == null) { + propertyMapping[propResolution.name] = propResolution + } else { + propertyMapping[propResolution.name]!!.append(propResolution) + } } - val startMap = myProperties?.associate { it.name to it } - ?.toMutableMap() ?: return null + return propertyMapping.values.toList() + } - inheritance.forEach { - it.baseProperties?.forEach { prop -> - if (!startMap.containsKey(prop.name)) { - startMap[prop.name] = prop + fun resolvedMethods(): List { + val methodMapping = baseMethods().associate { + it.name to ResolvedMethod(it.name, it) + }.toMutableMap() + + // collect resolved methods from implemented protocols + baseProtocols?.forEach { protocol -> + protocol.resolvedMethods().forEach { methodResolution -> + if (methodMapping[methodResolution.name] == null) { + methodMapping[methodResolution.name] = methodResolution + } else { + methodMapping[methodResolution.name]!!.append(methodResolution) } } } - return startMap.values.toList() + superclass?.resolvedMethods()?.forEach { methodResolution -> + if (methodMapping[methodResolution.name] == null) { + methodMapping[methodResolution.name] = methodResolution + } else { + methodMapping[methodResolution.name]!!.append(methodResolution) + } + } + + return methodMapping.values.toList() + } + + fun baseMethods(): List { + return (baseInstanceMethods ?: listOf()) + (baseClassMethods ?: listOf()) + } + + fun baseProperties(): List { + return (baseInstanceProperties ?: listOf()) + (baseClassProperties ?: listOf()) } } data class OCProtocol( - val name: String, + override val name: String, var protocols: List?, var instanceMethods: List?, var classMethods: List?, var optionalInstanceMethods: List?, var optionalClassMethods: List?, var instanceProperties: List?, +// var classProperties: List?, var extendedSignatures: List? -) : OCFieldContainer() +) : OCFieldContainer(name) { + + fun resolvedProperties(): List { + val propertyMapping = instanceProperties?.associate { + it.name to ResolvedProperty(it.name, it) + }.orEmpty().toMutableMap() + + protocols?.forEach { protocol -> + protocol.resolvedProperties().forEach { propResolution -> + if (propertyMapping[propResolution.name] == null) { + propertyMapping[propResolution.name] = propResolution + } else { + propertyMapping[propResolution.name]!!.append(propResolution) + } + } + } + + return propertyMapping.values.toList() + } + + fun resolvedMethods(): List { + val methodMapping = baseMethods().associate { + it.name to ResolvedMethod(it.name, it) + }.toMutableMap() + + protocols?.forEach { protocol -> + protocol.resolvedMethods().forEach { methodResolution -> + if (methodMapping[methodResolution.name] == null) { + methodMapping[methodResolution.name] = methodResolution + } else { + methodMapping[methodResolution.name]!!.append(methodResolution) + } + } + } + + return methodMapping.values.toList() + } + + fun baseMethods(): List { +//// fixme: I believe this function was devised in a misconception that only one of these +//// fields could be non-null per instance. + return (instanceMethods ?: listOf()) + + (classMethods ?: listOf()) + + (optionalInstanceMethods ?: listOf()) + + (optionalClassMethods ?: listOf()) + } + +} data class OCMethod( - val parent: OCFieldContainer, - val name: String, - val isInstanceMethod: Boolean = true, + var parent: OCFieldContainer, + override val name: String, private val signature: EncodedSignature, val implAddress: Long?, -) { +) : OCField(name) { + + override fun parent(): OCFieldContainer = parent override fun toString(): String { - return "OCMethod(name='$name', signature=$signature, implAddress=$implAddress)" -// return prototypeString() + return "OCMethod(name='$name', signature=${getSignature()}, implAddress=$implAddress)" + } + + fun isClassMethod(): Boolean { + return if (parent is OCClass) { + (parent as OCClass).baseClassMethods?.contains(this) == true + } else { + val cond1 = (parent as OCProtocol).classMethods?.contains(this) == true + val cond2 = (parent as OCProtocol).optionalClassMethods?.contains(this) == true + + cond1 || cond2 + } } fun getSignature(): EncodedSignature? { - if (parent is OCProtocol && parent.extendedSignatures != null) { + if (parent is OCProtocol && (parent as OCProtocol).extendedSignatures != null) { + val protoParent = parent as OCProtocol + // find the non-null method list, and then the index of ourselves in that list // then, if it's not null, access that index of `extendedSignatures` and return it. - - val methods = parent.instanceMethods - ?: parent.classMethods - ?: parent.optionalInstanceMethods - ?: parent.optionalClassMethods - ?: return signature + // use the activeMethodList instead of resolvedInstanceMethods because we are operating on the + // presumption that only local methods will have entries in the extended signatures + val methods = protoParent.baseMethods() val index = methods.indexOf(this) - return parent.extendedSignatures!!.getOrNull(index) ?: signature + return protoParent.extendedSignatures!!.getOrNull(index) ?: signature } // otherwise, return our field. return signature @@ -110,20 +248,20 @@ data class OCMethod( * Get the method prototype in Objective-C syntax. */ fun prototypeString(): String { - // todo: (low priority) decouple this method from OCMethod for separation of concern. - val sig = getSignature() ?: return "" - val prefix = if (isInstanceMethod) "-" else "+" - val returnType = sig.returnType.first ?: return "" + val prefix = if (isClassMethod()) "+" else "-" + val returnType = TypeStringify.getResult(sig.returnType.first) if (sig.parameters.count() > 0) { var nsplit = name.split(":").filter { it.trim().isNotEmpty() } - println(nsplit) - var result = "$prefix($returnType)$name:${sig.parameters.first().first}\"" + var result = "$prefix($returnType)${nsplit[0]}:(${TypeStringify.getResult(sig.parameters.first().first)})" + for (i in 1 until sig.parameters.count()) { - result += " ${nsplit[i]}\$:(${sig.parameters[i].first})" + val typeStr = TypeStringify.getResult(sig.parameters[i].first) + result += " ${nsplit[i]}:(${typeStr})" } + return "$result;" } else { return "$prefix($returnType)$name;" @@ -134,35 +272,83 @@ data class OCMethod( data class OCIVar( val ocClass: OCClass, - val name: String, + override val name: String, val offset: Int, val type: TypeNode, val alignment: Int, val size: Int, -) { +) : OCField(name) { + + override fun parent(): OCFieldContainer = ocClass + override fun toString(): String { return "OCIVar(name='$name', offset=$offset, type=$type)" } } data class OCProperty( - val parent: OCFieldContainer, - val name: String, + var parent: OCFieldContainer, + override val name: String, val attributes: List, val type: Pair?>?, + val customGetter: String?, + val customSetter: String?, private val backingIvar: String? -) { +) : OCField(name) { + + override fun parent(): OCFieldContainer = parent override fun toString(): String { return "OCProperty(name='$name', attributes=$attributes, type=$type)" } + fun isClassProperty(): Boolean { + return if (parent is OCClass) { + (parent as OCClass).baseClassProperties?.contains(this) == true + } else { + false + } + } + + fun declaration(): String { + val builder = StringBuilder() + builder.append("@property ") + + if (attributes.filterNot { it == PropertyAttribute.TYPE_ENCODING }.isNotEmpty()) { + builder.append("(") + attributes + .mapNotNull { it.annotationString() } + .let { + if (isClassProperty()) { + it + "class" + } else { + it + } + }.map { + when (it) { + "getter=" -> "getter=$customGetter" + "setter=" -> "setter=$customSetter" + else -> it + } + } + .joinToString(", ", postfix = ") ") { it } + .let { builder.append(it) } + } + val typeString = TypeStringify.getResult(type!!.first) + builder.append("$typeString $name;") + builder.append("\n") + + return builder.toString() + } + fun getBackingIvar(): OCIVar? { return if (parent is OCClass) { - parent.instanceVariables?.find { it.name == backingIvar } + (parent as OCClass).instanceVariables?.find { it.name == backingIvar } } else { null } } } + + diff --git a/src/main/java/lol/fairplay/ghidraapple/graph/ClassAbstractionGraph.kt b/src/main/java/lol/fairplay/ghidraapple/graph/ClassAbstractionGraph.kt index 338a196..0e09515 100644 --- a/src/main/java/lol/fairplay/ghidraapple/graph/ClassAbstractionGraph.kt +++ b/src/main/java/lol/fairplay/ghidraapple/graph/ClassAbstractionGraph.kt @@ -9,6 +9,8 @@ import ghidra.service.graph.EmptyGraphType import ghidra.util.task.Task import ghidra.util.task.TaskMonitor import lol.fairplay.ghidraapple.core.objc.modelling.OCClass +import lol.fairplay.ghidraapple.core.objc.modelling.OCFieldContainer +import lol.fairplay.ghidraapple.core.objc.modelling.OCProtocol // reference code: ghidra.app.plugin.core.decompile.actions.PCodeDfgGraphTask @@ -38,30 +40,53 @@ class ClassAbstractionGraphTask( } private fun buildGraph() { - var previousVertex: AttributedVertex? = null - var currentClass: OCClass? = classModel - var currentVertex = classVertex(classModel.name) - - while (currentClass != null) { - if (previousVertex != null) createInheritsEdge(currentVertex, previousVertex) - currentClass.baseProtocols?.forEach { - val baseProtocol = it.name - val baseProtocolVertex = protoVertex(baseProtocol) - createImplementsEdge(baseProtocolVertex, currentVertex) + val visited = mutableSetOf() + val stack = mutableListOf() + stack.add(classModel) + + while (stack.isNotEmpty()) { + val current = stack.removeLast() + + if (current is OCClass) { + val cVertex = getOrCreateClassVertex(current.name) + current.baseProtocols?.forEach { protocol -> + val protoVertex = getOrCreateProtoVertex(protocol.name) + createImplementsEdge(protoVertex, cVertex) + if (!visited.contains(protocol.name)) + stack.add(protocol) + } + current.superclass?.let { + val superVertex = getOrCreateClassVertex(it.name) + createInheritsEdge(superVertex, cVertex) + if (!visited.contains(it.name)) + stack.add(it) + } + } else if (current is OCProtocol) { + val pVertex = getOrCreateProtoVertex(current.name) + current.protocols?.forEach { protocol -> + val protoVertex = getOrCreateProtoVertex(protocol.name) + createImplementsEdge(protoVertex, pVertex) + if (!visited.contains(protocol.name)) + stack.add(protocol) + } } - previousVertex = currentVertex - currentClass = currentClass.superclass ?: break - currentVertex = classVertex(currentClass.name) + + visited.add(current.name) } + } - private fun protoVertex(name: String): AttributedVertex { + private fun getOrCreateProtoVertex(name: String): AttributedVertex { + if (graph.getVertex(name) != null) return graph.getVertex(name)!! + val v = graph.addVertex(name) v.setAttribute("color", "blue") return v } - private fun classVertex(name: String): AttributedVertex { + private fun getOrCreateClassVertex(name: String): AttributedVertex { + if (graph.getVertex(name) != null) return graph.getVertex(name)!! + val v = graph.addVertex(name) return v } diff --git a/src/main/java/lol/fairplay/ghidraapple/loading/UniversalBinaryLoader.kt b/src/main/java/lol/fairplay/ghidraapple/loading/UniversalBinaryLoader.kt new file mode 100644 index 0000000..7e8b01f --- /dev/null +++ b/src/main/java/lol/fairplay/ghidraapple/loading/UniversalBinaryLoader.kt @@ -0,0 +1,68 @@ +package lol.fairplay.ghidraapple.loading + +import ghidra.app.util.Option +import ghidra.app.util.bin.ByteProvider +import ghidra.app.util.importer.MessageLog +import ghidra.app.util.opinion.Loaded +import ghidra.app.util.opinion.MachoLoader +import ghidra.framework.model.Project +import ghidra.program.model.listing.Program +import ghidra.util.task.TaskMonitor + +class UniversalBinaryLoader : MachoLoader() { + + override fun getPreferredFileName(byteProvider: ByteProvider): String { + val original = super.getPreferredFileName(byteProvider) + val ubType = "universalbinary" + // If this isn't a universal binary then just return the original file name. + if (!byteProvider.fsrl.toStringPart().startsWith("$ubType://")) return original + + // The fsrl is two-fold: the path to the binary, and a path within the binary. We take the former + // and extract the name (which will be the last path component, the binary name). + val binaryName = byteProvider.fsrl.split()[0].name + // We prefix with the binary name as the original name is just the architecture and CPU. + return "$binaryName-$original" + } + + override fun postLoadProgramFixups( + loadedPrograms: MutableList>?, + project: Project?, + options: MutableList