refactor(deobf): split deobfuscation conditions (#2040)

This commit is contained in:
Skylot
2023-11-05 20:00:22 +00:00
parent f7002c7fad
commit a989fa7e64
22 changed files with 482 additions and 246 deletions
+10 -6
View File
@@ -32,7 +32,8 @@ import jadx.api.plugins.loader.JadxPluginLoader;
import jadx.api.usage.IUsageInfoCache;
import jadx.api.usage.impl.InMemoryUsageInfoCache;
import jadx.core.deobf.DeobfAliasProvider;
import jadx.core.deobf.DeobfCondition;
import jadx.core.deobf.conditions.DeobfWhitelist;
import jadx.core.deobf.conditions.JadxRenameConditions;
import jadx.core.plugins.PluginContext;
import jadx.core.utils.files.FileUtils;
@@ -103,7 +104,10 @@ public class JadxArgs implements Closeable {
private int deobfuscationMinLength = 0;
private int deobfuscationMaxLength = Integer.MAX_VALUE;
private String deobfuscationWhitelist = "";
/**
* List of classes and packages (ends with '.*') to exclude from deobfuscation
*/
private List<String> deobfuscationWhitelist = DeobfWhitelist.DEFAULT_LIST;
/**
* Nodes alias provider for deobfuscator and rename visitor
@@ -113,7 +117,7 @@ public class JadxArgs implements Closeable {
/**
* Condition to rename node in deobfuscator
*/
private IRenameCondition renameCondition = new DeobfCondition();
private IRenameCondition renameCondition = JadxRenameConditions.buildDefault();
private boolean escapeUnicode = false;
private boolean replaceConsts = true;
@@ -436,11 +440,11 @@ public class JadxArgs implements Closeable {
this.deobfuscationMaxLength = deobfuscationMaxLength;
}
public String getDeobfuscationWhitelist() {
public List<String> getDeobfuscationWhitelist() {
return this.deobfuscationWhitelist;
}
public void setDeobfuscationWhitelist(String deobfuscationWhitelist) {
public void setDeobfuscationWhitelist(List<String> deobfuscationWhitelist) {
this.deobfuscationWhitelist = deobfuscationWhitelist;
}
@@ -678,7 +682,7 @@ public class JadxArgs implements Closeable {
public String makeCodeArgsHash(@Nullable JadxDecompiler decompiler) {
String argStr = "args:" + decompilationMode + useImports + showInconsistentCode
+ inlineAnonymousClasses + inlineMethods + moveInnerClasses + allowInlineKotlinLambda
+ deobfuscationOn + deobfuscationMinLength + deobfuscationMaxLength
+ deobfuscationOn + deobfuscationMinLength + deobfuscationMaxLength + deobfuscationWhitelist
+ resourceNameSource
+ useKotlinMethodsForVarNames
+ insertDebugLines + extractFinally
@@ -0,0 +1,31 @@
package jadx.api.deobf;
import jadx.api.deobf.impl.CombineDeobfConditions;
import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.FieldNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.nodes.PackageNode;
import jadx.core.dex.nodes.RootNode;
/**
* Utility interface to simplify merging several rename conditions to build {@link IRenameCondition}
* instance with {@link CombineDeobfConditions#combine(IDeobfCondition...)}.
*/
public interface IDeobfCondition {
enum Action {
NO_ACTION,
FORCE_RENAME,
FORBID_RENAME,
}
void init(RootNode root);
Action check(PackageNode pkg);
Action check(ClassNode cls);
Action check(FieldNode fld);
Action check(MethodNode mth);
}
@@ -0,0 +1,73 @@
package jadx.api.deobf.impl;
import java.util.Arrays;
import java.util.List;
import java.util.function.Function;
import jadx.api.deobf.IDeobfCondition;
import jadx.api.deobf.IRenameCondition;
import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.FieldNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.nodes.PackageNode;
import jadx.core.dex.nodes.RootNode;
public class CombineDeobfConditions implements IRenameCondition {
public static IRenameCondition combine(List<IDeobfCondition> conditions) {
return new CombineDeobfConditions(conditions);
}
public static IRenameCondition combine(IDeobfCondition... conditions) {
return new CombineDeobfConditions(Arrays.asList(conditions));
}
private final List<IDeobfCondition> conditions;
private CombineDeobfConditions(List<IDeobfCondition> conditions) {
if (conditions == null || conditions.isEmpty()) {
throw new IllegalArgumentException("Conditions list can't be empty");
}
this.conditions = conditions;
}
private boolean combineFunc(Function<IDeobfCondition, IDeobfCondition.Action> check) {
for (IDeobfCondition c : conditions) {
switch (check.apply(c)) {
case NO_ACTION:
// ignore
break;
case FORCE_RENAME:
return true;
case FORBID_RENAME:
return false;
}
}
return false;
}
@Override
public void init(RootNode root) {
conditions.forEach(c -> c.init(root));
}
@Override
public boolean shouldRename(PackageNode pkg) {
return combineFunc(c -> c.check(pkg));
}
@Override
public boolean shouldRename(ClassNode cls) {
return combineFunc(c -> c.check(cls));
}
@Override
public boolean shouldRename(FieldNode fld) {
return combineFunc(c -> c.check(fld));
}
@Override
public boolean shouldRename(MethodNode mth) {
return combineFunc(c -> c.check(mth));
}
}
@@ -1,103 +0,0 @@
package jadx.core.deobf;
import java.util.HashSet;
import java.util.Set;
import jadx.api.JadxArgs;
import jadx.api.deobf.IRenameCondition;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.instructions.args.ArgType;
import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.FieldNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.nodes.PackageNode;
import jadx.core.dex.nodes.RootNode;
public class DeobfCondition implements IRenameCondition {
private int minLength;
private int maxLength;
private final Set<String> avoidClsNames = new HashSet<>();
@Override
public void init(RootNode root) {
JadxArgs args = root.getArgs();
this.minLength = args.getDeobfuscationMinLength();
this.maxLength = args.getDeobfuscationMaxLength();
for (PackageNode pkg : root.getPackages()) {
avoidClsNames.add(pkg.getPkgInfo().getName());
}
}
@Override
public boolean shouldRename(PackageNode pkg) {
String name = pkg.getAliasPkgInfo().getName();
return shouldRename(name)
&& !pkg.hasAlias()
&& !TldHelper.contains(name);
}
@Override
public boolean shouldRename(ClassNode cls) {
if (cls.contains(AFlag.DONT_RENAME)
|| cls.getClassInfo().hasAlias()
|| isR(cls.getTopParentClass())) {
return false;
}
String name = cls.getAlias();
if (avoidClsNames.contains(name)) {
return true;
}
return shouldRename(name);
}
@Override
public boolean shouldRename(FieldNode fld) {
return shouldRename(fld.getAlias())
&& !fld.contains(AFlag.DONT_RENAME)
&& !fld.getFieldInfo().hasAlias()
&& !isR(fld.getTopParentClass());
}
@Override
public boolean shouldRename(MethodNode mth) {
return shouldRename(mth.getAlias())
&& !mth.contains(AFlag.DONT_RENAME)
&& !mth.getMethodInfo().hasAlias()
&& !mth.isConstructor();
}
private boolean shouldRename(String s) {
int len = s.length();
return len < minLength || len > maxLength;
}
private static boolean isR(ClassNode cls) {
if (cls.contains(AFlag.ANDROID_R_CLASS)) {
return true;
}
if (!cls.getClassInfo().getShortName().equals("R")) {
return false;
}
if (!cls.getMethods().isEmpty() || !cls.getFields().isEmpty()) {
return false;
}
for (ClassNode inner : cls.getInnerClasses()) {
for (MethodNode m : inner.getMethods()) {
if (!m.getMethodInfo().isConstructor() && !m.getMethodInfo().isClassInit()) {
return false;
}
}
for (FieldNode field : cls.getFields()) {
ArgType type = field.getType();
if (type != ArgType.INT && (!type.isArray() || type.getArrayElement() != ArgType.INT)) {
return false;
}
}
}
cls.add(AFlag.ANDROID_R_CLASS);
return true;
}
}
@@ -1,78 +0,0 @@
package jadx.core.deobf;
import java.util.ArrayList;
import java.util.List;
import jadx.api.JadxArgs;
import jadx.api.deobf.IRenameCondition;
import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.FieldNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.nodes.PackageNode;
import jadx.core.dex.nodes.RootNode;
public class DeobfWhitelist implements IRenameCondition {
private static DeobfWhitelist whitelist = null;
private final List<String> packages = new ArrayList<>();
private final List<String> classes = new ArrayList<>();
public static DeobfWhitelist getWhitelist() {
if (whitelist == null) {
whitelist = new DeobfWhitelist();
}
return whitelist;
}
@Override
public void init(RootNode root) {
packages.clear();
classes.clear();
JadxArgs args = root.getArgs();
String whitelistStr = args.getDeobfuscationWhitelist();
String[] whitelisteItems = whitelistStr.split(":");
for (String whitelistItem : whitelisteItems) {
if (!whitelistItem.isEmpty()) {
if (whitelistItem.endsWith(".*")) {
packages.add(whitelistItem.substring(0, whitelistItem.length() - 2));
} else {
classes.add(whitelistItem);
}
}
}
}
@Override
public boolean shouldRename(PackageNode pkg) {
String fullname = pkg.getPkgInfo().getFullName();
for (String p : packages) {
if (fullname.equals(p)) {
return false;
}
}
return true;
}
@Override
public boolean shouldRename(ClassNode cls) {
String fullname = cls.getFullName();
for (String c : classes) {
if (fullname.equals(c)) {
return false;
}
}
return true;
}
@Override
public boolean shouldRename(FieldNode fld) {
return true;
}
@Override
public boolean shouldRename(MethodNode mth) {
return true;
}
}
@@ -19,8 +19,6 @@ public class DeobfuscatorVisitor extends AbstractVisitor {
if (!args.isDeobfuscationOn()) {
return;
}
DeobfWhitelist whitelist = DeobfWhitelist.getWhitelist();
whitelist.init(root);
DeobfPresets mapping = DeobfPresets.build(root);
if (args.getGeneratedRenamesMappingFileMode().shouldRead()) {
if (mapping.load()) {
@@ -34,11 +32,9 @@ public class DeobfuscatorVisitor extends AbstractVisitor {
}
public static void process(RootNode root, IRenameCondition renameCondition, IAliasProvider aliasProvider) {
DeobfWhitelist whitelist = DeobfWhitelist.getWhitelist();
boolean pkgUpdated = false;
for (PackageNode pkg : root.getPackages()) {
if (whitelist.shouldRename(pkg) && renameCondition.shouldRename(pkg)) {
if (renameCondition.shouldRename(pkg)) {
String alias = aliasProvider.forPackage(pkg);
if (alias != null) {
pkg.rename(alias, false);
@@ -1,37 +0,0 @@
package jadx.core.deobf;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.HashSet;
import java.util.Set;
import jadx.core.utils.exceptions.JadxRuntimeException;
/**
* Provides a list of all top level domains with 3 characters and less,
* so we can exclude them from deobfuscation.
*/
public class TldHelper {
private static final Set<String> TLD_SET = loadTldFile();
private static Set<String> loadTldFile() {
Set<String> tldNames = new HashSet<>();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(TldHelper.class.getResourceAsStream("tld_3.txt")))) {
String line;
while ((line = reader.readLine()) != null) {
line = line.trim();
if (!line.startsWith("#") && !line.isEmpty()) {
tldNames.add(line);
}
}
return tldNames;
} catch (Exception e) {
throw new JadxRuntimeException("Failed to load top level domain list tld_3.txt", e);
}
}
public static boolean contains(String name) {
return TLD_SET.contains(name);
}
}
@@ -0,0 +1,35 @@
package jadx.core.deobf.conditions;
import jadx.api.deobf.IDeobfCondition;
import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.FieldNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.nodes.PackageNode;
import jadx.core.dex.nodes.RootNode;
public abstract class AbstractDeobfCondition implements IDeobfCondition {
@Override
public void init(RootNode root) {
}
@Override
public Action check(PackageNode pkg) {
return Action.NO_ACTION;
}
@Override
public Action check(ClassNode cls) {
return Action.NO_ACTION;
}
@Override
public Action check(FieldNode fld) {
return Action.NO_ACTION;
}
@Override
public Action check(MethodNode mth) {
return Action.NO_ACTION;
}
}
@@ -0,0 +1,29 @@
package jadx.core.deobf.conditions;
import java.util.HashSet;
import java.util.Set;
import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.PackageNode;
import jadx.core.dex.nodes.RootNode;
public class AvoidClsAndPkgNamesCollision extends AbstractDeobfCondition {
private final Set<String> avoidClsNames = new HashSet<>();
@Override
public void init(RootNode root) {
avoidClsNames.clear();
for (PackageNode pkg : root.getPackages()) {
avoidClsNames.add(pkg.getName());
}
}
@Override
public Action check(ClassNode cls) {
if (avoidClsNames.contains(cls.getAlias())) {
return Action.FORCE_RENAME;
}
return Action.NO_ACTION;
}
}
@@ -0,0 +1,49 @@
package jadx.core.deobf.conditions;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.FieldNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.nodes.PackageNode;
/**
* Disable deobfuscation for nodes:
* - with 'DONT_RENAME' flag
* - already renamed
*/
public class BaseDeobfCondition extends AbstractDeobfCondition {
@Override
public Action check(PackageNode pkg) {
if (pkg.contains(AFlag.DONT_RENAME) || pkg.hasAlias()) {
return Action.FORBID_RENAME;
}
return Action.NO_ACTION;
}
@Override
public Action check(ClassNode cls) {
if (cls.contains(AFlag.DONT_RENAME) || cls.getClassInfo().hasAlias()) {
return Action.FORBID_RENAME;
}
return Action.NO_ACTION;
}
@Override
public Action check(MethodNode mth) {
if (mth.contains(AFlag.DONT_RENAME)
|| mth.getMethodInfo().hasAlias()
|| mth.isConstructor()) {
return Action.FORBID_RENAME;
}
return Action.NO_ACTION;
}
@Override
public Action check(FieldNode fld) {
if (fld.contains(AFlag.DONT_RENAME) || fld.getFieldInfo().hasAlias()) {
return Action.FORBID_RENAME;
}
return Action.NO_ACTION;
}
}
@@ -0,0 +1,50 @@
package jadx.core.deobf.conditions;
import jadx.api.JadxArgs;
import jadx.api.deobf.IDeobfCondition;
import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.FieldNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.nodes.PackageNode;
import jadx.core.dex.nodes.RootNode;
public class DeobfLengthCondition implements IDeobfCondition {
private int minLength;
private int maxLength;
@Override
public void init(RootNode root) {
JadxArgs args = root.getArgs();
this.minLength = args.getDeobfuscationMinLength();
this.maxLength = args.getDeobfuscationMaxLength();
}
private Action checkName(String s) {
int len = s.length();
if (len < minLength || len > maxLength) {
return Action.FORCE_RENAME;
}
return Action.NO_ACTION;
}
@Override
public Action check(PackageNode pkg) {
return checkName(pkg.getName());
}
@Override
public Action check(ClassNode cls) {
return checkName(cls.getName());
}
@Override
public Action check(FieldNode fld) {
return checkName(fld.getName());
}
@Override
public Action check(MethodNode mth) {
return checkName(mth.getName());
}
}
@@ -0,0 +1,58 @@
package jadx.core.deobf.conditions;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.PackageNode;
import jadx.core.dex.nodes.RootNode;
import jadx.core.utils.Utils;
public class DeobfWhitelist extends AbstractDeobfCondition {
public static final List<String> DEFAULT_LIST = Arrays.asList(
"android.support.v4.*",
"android.support.v7.*",
"android.support.v4.os.*",
"android.support.annotation.Px",
"androidx.core.os.*",
"androidx.annotation.Px");
public static final String DEFAULT_STR = Utils.listToString(DEFAULT_LIST, " ");
private final Set<String> packages = new HashSet<>();
private final Set<String> classes = new HashSet<>();
@Override
public void init(RootNode root) {
packages.clear();
classes.clear();
for (String whitelistItem : root.getArgs().getDeobfuscationWhitelist()) {
if (!whitelistItem.isEmpty()) {
if (whitelistItem.endsWith(".*")) {
packages.add(whitelistItem.substring(0, whitelistItem.length() - 2));
} else {
classes.add(whitelistItem);
}
}
}
}
@Override
public Action check(PackageNode pkg) {
if (packages.contains(pkg.getPkgInfo().getFullName())) {
return Action.FORBID_RENAME;
}
return Action.NO_ACTION;
}
@Override
public Action check(ClassNode cls) {
if (classes.contains(cls.getClassInfo().getFullName())) {
return Action.FORBID_RENAME;
}
return Action.NO_ACTION;
}
}
@@ -0,0 +1,45 @@
package jadx.core.deobf.conditions;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.instructions.args.ArgType;
import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.FieldNode;
import jadx.core.dex.nodes.MethodNode;
public class ExcludeAndroidRClass extends AbstractDeobfCondition {
@Override
public Action check(ClassNode cls) {
if (isR(cls.getTopParentClass())) {
return Action.FORBID_RENAME;
}
return Action.NO_ACTION;
}
private static boolean isR(ClassNode cls) {
if (cls.contains(AFlag.ANDROID_R_CLASS)) {
return true;
}
if (!cls.getClassInfo().getShortName().equals("R")) {
return false;
}
if (!cls.getMethods().isEmpty() || !cls.getFields().isEmpty()) {
return false;
}
for (ClassNode inner : cls.getInnerClasses()) {
for (MethodNode m : inner.getMethods()) {
if (!m.getMethodInfo().isConstructor() && !m.getMethodInfo().isClassInit()) {
return false;
}
}
for (FieldNode field : cls.getFields()) {
ArgType type = field.getType();
if (type != ArgType.INT && (!type.isArray() || type.getArrayElement() != ArgType.INT)) {
return false;
}
}
}
cls.add(AFlag.ANDROID_R_CLASS);
return true;
}
}
@@ -0,0 +1,42 @@
package jadx.core.deobf.conditions;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.Set;
import java.util.stream.Collectors;
import jadx.core.dex.nodes.PackageNode;
import jadx.core.utils.exceptions.JadxRuntimeException;
/**
* Provides a list of all top level domains with 3 characters and less,
* so we can exclude them from deobfuscation.
*/
public class ExcludePackageWithTLDNames extends AbstractDeobfCondition {
/**
* Lazy load TLD set
*/
private static class TldHolder {
private static final Set<String> TLD_SET = loadTldFile();
}
private static Set<String> loadTldFile() {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(TldHolder.class.getResourceAsStream("tld_3.txt")))) {
return reader.lines()
.map(String::trim)
.filter(line -> !line.startsWith("#") && !line.isEmpty())
.collect(Collectors.toSet());
} catch (Exception e) {
throw new JadxRuntimeException("Failed to load top level domain list file: tld_3.txt", e);
}
}
@Override
public Action check(PackageNode pkg) {
if (TldHolder.TLD_SET.contains(pkg.getName())) {
return Action.FORBID_RENAME;
}
return Action.NO_ACTION;
}
}
@@ -0,0 +1,30 @@
package jadx.core.deobf.conditions;
import java.util.ArrayList;
import java.util.List;
import jadx.api.deobf.IDeobfCondition;
import jadx.api.deobf.IRenameCondition;
import jadx.api.deobf.impl.CombineDeobfConditions;
public class JadxRenameConditions {
/**
* This method provides a mutable list of default deobfuscation conditions used by jadx.
* To build {@link IRenameCondition} use {@link CombineDeobfConditions#combine(List)} method.
*/
public static List<IDeobfCondition> buildDefaultDeobfConditions() {
List<IDeobfCondition> list = new ArrayList<>();
list.add(new BaseDeobfCondition());
list.add(new DeobfWhitelist());
list.add(new ExcludePackageWithTLDNames());
list.add(new ExcludeAndroidRClass());
list.add(new AvoidClsAndPkgNamesCollision());
list.add(new DeobfLengthCondition());
return list;
}
public static IRenameCondition buildDefault() {
return CombineDeobfConditions.combine(buildDefaultDeobfConditions());
}
}
@@ -130,6 +130,14 @@ public class PackageNode extends LineAttrNode
}
}
public String getName() {
return pkgInfo.getName();
}
public String getFullName() {
return pkgInfo.getFullName();
}
public PackageInfo getPkgInfo() {
return pkgInfo;
}
@@ -9,7 +9,6 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.core.deobf.NameMapper;
import jadx.core.deobf.TldHelper;
public class BetterName {
private static final Logger LOG = LoggerFactory.getLogger(BetterName.class);
@@ -43,9 +42,6 @@ public class BetterName {
if (NameMapper.isValidIdentifier(str)) {
rating += 50;
}
if (TldHelper.contains(str)) {
rating += 20;
}
if (str.contains("_")) {
// rare in obfuscated names
rating += 100;