Skip to content

Instantly share code, notes, and snippets.

@Sculas
Created May 8, 2023 18:34
Show Gist options
  • Save Sculas/95642cd7a151f162b95f84f5081b881c to your computer and use it in GitHub Desktop.
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.
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