Created
May 8, 2023 18:34
-
-
Save Sculas/95642cd7a151f162b95f84f5081b881c to your computer and use it in GitHub Desktop.
JADX script to remap obfuscated APKs. Intended for GMS, but should work on anything.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import jadx.api.plugins.input.data.attributes.IJadxAttrType | |
import jadx.api.plugins.input.data.attributes.IJadxAttribute | |
import jadx.core.dex.instructions.ConstStringNode | |
import jadx.core.dex.instructions.InsnType | |
import jadx.core.dex.instructions.InvokeNode | |
import jadx.core.dex.instructions.InvokeType | |
import jadx.core.dex.instructions.args.RegisterArg | |
import jadx.core.dex.nodes.ClassNode | |
import jadx.core.dex.nodes.InsnNode | |
import jadx.core.dex.nodes.MethodNode | |
import jadx.core.dex.nodes.RootNode | |
import jadx.plugins.script.runtime.data.ScriptOrderedPreparePass | |
// setup | |
val jadx = getJadxInstance() | |
jadx.args.isDeobfuscationOn = false | |
jadx.args.renameFlags = emptySet() | |
val illegalChars = arrayOf('{', '[', '(', ' ', ',', '=', ':', ';') | |
object RemappedAttr : IJadxAttribute { | |
override fun getAttrType(): IJadxAttrType<RemappedAttr> = Type | |
object Type : IJadxAttrType<RemappedAttr> | |
} | |
fun remapClass(phase: String, cls: ClassNode, newName: String): Boolean { | |
if (cls.fullName == newName || cls.name == newName) return false | |
if (illegalChars.any(newName::contains)) { | |
log.warn("$phase: Extracted class name '$newName' contains illegal characters, skipping...") | |
return cls.altName(phase, newName) | |
} | |
if (cls.name.any(Char::isUpperCase)) { | |
log.warn("$phase: Class '${cls.fullName}' most likely already has a proper name, skipping...") | |
return cls.altName(phase, newName) | |
} | |
if (cls.contains(RemappedAttr.Type)) { | |
val logName = if (cls.fullName == newName) { | |
cls.fullName | |
} else { | |
"${cls.fullName}/$newName" | |
} | |
log.warn("$phase: Class '$logName' already mapped, skipping...") | |
return cls.altName(phase, newName) | |
} | |
log.info("$phase: Remapped class: ${cls.fullName} -> $newName") | |
cls.addInfoComment("$phase: Remapped class: ${cls.fullName} -> $newName") | |
cls.rename(newName) | |
cls.addAttr(RemappedAttr) | |
return true | |
} | |
fun MethodNode.insnList(): List<InsnNode> { | |
this.load() | |
return instructions?.filterNotNull() ?: emptyList(); | |
} | |
fun ClassNode.extends(vararg names: String): Boolean { | |
return superClass?.isObject == true && superClass?.`object` in names | |
} | |
fun ClassNode.altName(phase: String, newName: String): Boolean { | |
addInfoComment("$phase: Alternative class name: $newName") | |
return false | |
} | |
jadx.addPass(object : ScriptOrderedPreparePass( | |
jadx, | |
"RenameAll", | |
runBefore = listOf("RenameVisitor"), | |
) { | |
override fun init(root: RootNode) { | |
val methods = root.classes.flatMap(ClassNode::getMethods) | |
// remap classes that leak their name in Binder impl | |
methods.forEach(::binderPhase) | |
// remap classes that leak their name in via Log calls | |
methods.forEach(::logPhase) | |
} | |
}) | |
var binderRemaps = 0 | |
var logRemaps = 0 | |
jadx.afterLoad { | |
log.info("BinderPhase: Remapped $binderRemaps classes.") | |
log.info("LogPhase: Remapped $logRemaps classes.") | |
} | |
fun binderPhase(mtd: MethodNode) { | |
if (!mtd.isConstructor || mtd.insnsCount < 2) return | |
// the opcode pattern also triggers on exceptions, which don't contain class names | |
if (mtd.parentClass.extends("java.lang.Exception", "java.lang.RuntimeException")) return | |
val insns = mtd.insnList() | |
if (insns[0].type != InsnType.CONST_STR) return | |
if ((insns[1] as? InvokeNode ?: return).invokeType != InvokeType.DIRECT) return | |
var fqcn = (insns[0] as ConstStringNode).string | |
val isClass = fqcn.split('.').run { | |
size > 2 && !fqcn.contains(' ') && !last().all { it.isUpperCase() || it == '_' } | |
} | |
if (!isClass) return // most likely not a class name | |
val isBinder = | |
mtd.argTypes.size == 1 && mtd.argTypes[0].isObject && mtd.argTypes[0].`object` == "android.os.IBinder" | |
if (isBinder) fqcn += "Binder" | |
if (remapClass("BinderPhase", mtd.parentClass, fqcn)) | |
binderRemaps++ | |
} | |
fun logPhase(mtd: MethodNode) { | |
if (mtd.insnsCount < 2) return | |
val insns = mtd.insnList() | |
for (insn in insns) { | |
if (insn !is InvokeNode || !insn.isStaticCall || insn.callMth.declClass.fullName != "android.util.Log") continue | |
val rLogTag = (insn.getArg(0) as? RegisterArg ?: continue).regNum | |
// walk up the method to find the first const-string with the same register | |
val sLogTag = (insns.takeWhile { it != insn }.reversed().firstOrNull { | |
it.type == InsnType.CONST_STR && it.result.regNum == rLogTag | |
} as? ConstStringNode ?: continue).string.substringBefore('/') | |
if (remapClass("LogPhase", mtd.parentClass, sLogTag)) | |
logRemaps++ | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment