From 91ee7565ac513f1994139bae964f8840c04720e8 Mon Sep 17 00:00:00 2001 From: Skylot Date: Mon, 14 Sep 2020 19:07:59 +0100 Subject: [PATCH] fix: resolved regression in Kotlin metadata parser --- .../java/jadx/core/deobf/Deobfuscator.java | 88 ++++++++----------- .../utils/kotlin/KotlinMetadataUtils.java | 59 +++++++++++++ .../integration/deobf/TestKotlinMetadata.java | 48 ++++++++++ .../test/smali/deobf/TestKotlinMetadata.smali | 73 +++++++++++++++ 4 files changed, 217 insertions(+), 51 deletions(-) create mode 100644 jadx-core/src/main/java/jadx/core/utils/kotlin/KotlinMetadataUtils.java create mode 100644 jadx-core/src/test/java/jadx/tests/integration/deobf/TestKotlinMetadata.java create mode 100644 jadx-core/src/test/smali/deobf/TestKotlinMetadata.smali diff --git a/jadx-core/src/main/java/jadx/core/deobf/Deobfuscator.java b/jadx-core/src/main/java/jadx/core/deobf/Deobfuscator.java index 0330b8d9a..e5c75fbee 100644 --- a/jadx-core/src/main/java/jadx/core/deobf/Deobfuscator.java +++ b/jadx-core/src/main/java/jadx/core/deobf/Deobfuscator.java @@ -28,6 +28,7 @@ import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.FieldNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.RootNode; +import jadx.core.utils.kotlin.KotlinMetadataUtils; public class Deobfuscator { private static final Logger LOG = LoggerFactory.getLogger(Deobfuscator.class); @@ -36,9 +37,6 @@ public class Deobfuscator { public static final String CLASS_NAME_SEPARATOR = "."; public static final String INNER_CLASS_SEPARATOR = "$"; - public static final String KOTLIN_METADATA_ANNOTATION = "kotlin.Metadata"; - public static final String KOTLIN_METADATA_D2_PARAMETER = "d2"; - public static final String KOTLIN_METADATA_CLASSNAME_REGEX = "(L.*;)"; private final JadxArgs args; private final RootNode root; @@ -351,9 +349,8 @@ public class Deobfuscator { } else { if (!clsMap.containsKey(classInfo)) { String clsShortName = classInfo.getShortName(); - if (shouldRename(clsShortName) || reservedClsNames.contains(clsShortName)) { - makeClsAlias(cls); - } + boolean badName = shouldRename(clsShortName) || reservedClsNames.contains(clsShortName); + makeClsAlias(cls, badName); } } for (ClassNode innerCls : cls.getInnerClasses()) { @@ -366,7 +363,7 @@ public class Deobfuscator { if (deobfClsInfo != null) { return deobfClsInfo.getAlias(); } - return makeClsAlias(cls); + return makeClsAlias(cls, true); } public String getPkgAlias(ClassNode cls) { @@ -387,41 +384,35 @@ public class Deobfuscator { } } - private String makeClsAlias(ClassNode cls) { - ClassInfo classInfo = cls.getClassInfo(); - - String metadataClassName = ""; - String metadataPackageName = ""; + private String makeClsAlias(ClassNode cls, boolean badName) { + String alias = null; + String pkgName = null; if (this.parseKotlinMetadata) { - String rawClassName = getRawClassNameFromMetadata(cls); - if (rawClassName != null) { - metadataClassName = rawClassName.substring(rawClassName.lastIndexOf(".") + 1, rawClassName.length() - 1); - if (rawClassName.lastIndexOf(".") != -1) { - metadataPackageName = rawClassName.substring(1, rawClassName.lastIndexOf(".")); - } + ClassInfo kotlinCls = KotlinMetadataUtils.getClassName(cls); + if (kotlinCls != null) { + alias = prepareNameFull(kotlinCls.getShortName(), "C"); + pkgName = kotlinCls.getPackage(); } } - String alias = null; - - if (this.useSourceNameAsAlias) { + if (alias == null && this.useSourceNameAsAlias) { alias = getAliasFromSourceFile(cls); } + ClassInfo classInfo = cls.getClassInfo(); if (alias == null) { - if (metadataClassName.isEmpty()) { + if (badName) { String clsName = classInfo.getShortName(); String prefix = makeClsPrefix(cls); alias = String.format("%sC%04d%s", prefix, clsIndex++, prepareNamePart(clsName)); } else { - alias = metadataClassName; + // rename not needed + return classInfo.getShortName(); } } - PackageNode pkg; - if (metadataPackageName.isEmpty()) { - pkg = getPackageNode(classInfo.getPackage(), true); - } else { - pkg = getPackageNode(metadataPackageName, true); + if (pkgName == null) { + pkgName = classInfo.getPackage(); } + PackageNode pkg = getPackageNode(pkgName, true); clsMap.put(classInfo, new DeobfClsInfo(this, cls, pkg, alias)); return alias; } @@ -484,29 +475,6 @@ public class Deobfuscator { return result; } - /** - * Try to get class name form Kotlin meta data - * - * @param cls - * @return - */ - @Nullable - private String getRawClassNameFromMetadata(ClassNode cls) { - if (cls.getAnnotation(KOTLIN_METADATA_ANNOTATION) != null - && cls.getAnnotation(KOTLIN_METADATA_ANNOTATION).getValues().get(KOTLIN_METADATA_D2_PARAMETER) != null - && cls.getAnnotation(KOTLIN_METADATA_ANNOTATION).getValues().get(KOTLIN_METADATA_D2_PARAMETER) instanceof List) { - Object rawClassNameObject = - ((List) cls.getAnnotation(KOTLIN_METADATA_ANNOTATION).getValues().get(KOTLIN_METADATA_D2_PARAMETER)).get(0); - if (rawClassNameObject instanceof String) { - String rawClassName = ((String) rawClassNameObject).trim().replace("/", "."); - if (rawClassName.length() > 1 && rawClassName.matches(KOTLIN_METADATA_CLASSNAME_REGEX)) { - return rawClassName; - } - } - } - return null; - } - @Nullable private String getAliasFromSourceFile(ClassNode cls) { SourceFileAttr sourceFileAttr = cls.get(AType.SOURCE_FILE); @@ -628,6 +596,24 @@ public class Deobfuscator { return NameMapper.removeInvalidCharsMiddle(name); } + private String prepareNameFull(String name, String prefix) { + if (name.length() > maxLength) { + return makeHashName(name, prefix); + } + String result = NameMapper.removeInvalidChars(name, prefix); + if (result.isEmpty()) { + return makeHashName(name, prefix); + } + if (NameMapper.isReserved(result)) { + return prefix + result; + } + return result; + } + + private static String makeHashName(String name, String invalidPrefix) { + return invalidPrefix + 'x' + Integer.toHexString(name.hashCode()); + } + private void dumpClassAlias(ClassNode cls) { PackageNode pkg = getPackageNode(cls.getPackage(), false); diff --git a/jadx-core/src/main/java/jadx/core/utils/kotlin/KotlinMetadataUtils.java b/jadx-core/src/main/java/jadx/core/utils/kotlin/KotlinMetadataUtils.java new file mode 100644 index 000000000..faf850326 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/utils/kotlin/KotlinMetadataUtils.java @@ -0,0 +1,59 @@ +package jadx.core.utils.kotlin; + +import java.util.List; + +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jadx.api.plugins.input.data.annotations.EncodedType; +import jadx.api.plugins.input.data.annotations.EncodedValue; +import jadx.api.plugins.input.data.annotations.IAnnotation; +import jadx.core.dex.info.ClassInfo; +import jadx.core.dex.nodes.ClassNode; + +// TODO: parse data from d1 (protobuf encoded) to get original method names and other useful info +public class KotlinMetadataUtils { + private static final Logger LOG = LoggerFactory.getLogger(KotlinMetadataUtils.class); + + private static final String KOTLIN_METADATA_ANNOTATION = "Lkotlin/Metadata;"; + private static final String KOTLIN_METADATA_D2_PARAMETER = "d2"; + private static final String KOTLIN_METADATA_CLASSNAME_REGEX = "(L.*;)"; + + /** + * Try to get class info from Kotlin Metadata annotation + */ + @Nullable + public static ClassInfo getClassName(ClassNode cls) { + IAnnotation metadataAnnotation = cls.getAnnotation(KOTLIN_METADATA_ANNOTATION); + List d2Param = getParamAsList(metadataAnnotation, KOTLIN_METADATA_D2_PARAMETER); + if (d2Param == null || d2Param.isEmpty()) { + return null; + } + EncodedValue firstValue = d2Param.get(0); + if (firstValue == null || firstValue.getType() != EncodedType.ENCODED_STRING) { + return null; + } + try { + String rawClassName = ((String) firstValue.getValue()).trim(); + if (rawClassName.matches(KOTLIN_METADATA_CLASSNAME_REGEX)) { + return ClassInfo.fromName(cls.root(), rawClassName); + } + } catch (Exception e) { + LOG.error("Failed to parse kotlin metadata", e); + } + return null; + } + + @SuppressWarnings("unchecked") + private static List getParamAsList(IAnnotation annotation, String paramName) { + if (annotation == null) { + return null; + } + EncodedValue encodedValue = annotation.getValues().get(paramName); + if (encodedValue == null || encodedValue.getType() != EncodedType.ENCODED_ARRAY) { + return null; + } + return (List) encodedValue.getValue(); + } +} diff --git a/jadx-core/src/test/java/jadx/tests/integration/deobf/TestKotlinMetadata.java b/jadx-core/src/test/java/jadx/tests/integration/deobf/TestKotlinMetadata.java new file mode 100644 index 000000000..485a40fd9 --- /dev/null +++ b/jadx-core/src/test/java/jadx/tests/integration/deobf/TestKotlinMetadata.java @@ -0,0 +1,48 @@ +package jadx.tests.integration.deobf; + +import org.junit.jupiter.api.Test; + +import jadx.tests.api.SmaliTest; + +import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; + +public class TestKotlinMetadata extends SmaliTest { + // @formatter:off + /* + @file:JvmName("TestKotlinMetadata") + class TestMetaData { + + @JvmField + val id = 1 + + @JvmName("makeTwo") + fun double(x: Int): Int { + return 2 * x + } + } + */ + // @formatter:on + + @Test + public void test() { + prepareArgs(true); + assertThat(getClassNodeFromSmali()) + .code() + .containsOne("class TestMetaData {"); + } + + @Test + public void testIgnoreMetadata() { + prepareArgs(false); + assertThat(getClassNodeFromSmali()) + .code() + .containsOne("class C0000TestKotlinMetadata {"); + } + + private void prepareArgs(boolean parseKotlinMetadata) { + enableDeobfuscation(); + args.setDeobfuscationMinLength(100); // rename everything + getArgs().setParseKotlinMetadata(parseKotlinMetadata); + disableCompilation(); + } +} diff --git a/jadx-core/src/test/smali/deobf/TestKotlinMetadata.smali b/jadx-core/src/test/smali/deobf/TestKotlinMetadata.smali new file mode 100644 index 000000000..bcfa4b443 --- /dev/null +++ b/jadx-core/src/test/smali/deobf/TestKotlinMetadata.smali @@ -0,0 +1,73 @@ +.class public final Ldeobf/TestKotlinMetadata; +.super Ljava/lang/Object; +.source "TestMetaData.kt" + + +# annotations +.annotation runtime Lkotlin/Metadata; + bv = { + 0x1, + 0x0, + 0x3 + } + d1 = { + "\u0000\u0014\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\u0008\u0002\n\u0002\u0010\u0008\n\u0002\u0008\u0004\u0018\u00002\u00020\u0001B\u0005\u00a2\u0006\u0002\u0010\u0002J\u0015\u0010\u0005\u001a\u00020\u00042\u0006\u0010\u0006\u001a\u00020\u0004H\u0007\u00a2\u0006\u0002\u0008\u0007R\u0010\u0010\u0003\u001a\u00020\u00048\u0006X\u0087D\u00a2\u0006\u0002\n\u0000\u00a8\u0006\u0008" + } + d2 = { + "LTestMetaData;", + "", + "()V", + "id", + "", + "double", + "x", + "makeTwo", + "test" + } + k = 0x1 + mv = { + 0x1, + 0x4, + 0x0 + } +.end annotation + + +# instance fields +.field public final id:I + .annotation build Lkotlin/jvm/JvmField; + .end annotation +.end field + + +# direct methods +.method public constructor ()V + .registers 2 + + .prologue + .line 4 + invoke-direct {p0}, Ljava/lang/Object;->()V + + .line 7 + const/4 v0, 0x1 + + iput v0, p0, Ldeobf/TestKotlinMetadata;->id:I + + return-void +.end method + + +# virtual methods +.method public final makeTwo(I)I + .registers 3 + .param p1, "x" # I + .annotation build Lkotlin/jvm/JvmName; + name = "makeTwo" + .end annotation + + .prologue + .line 11 + mul-int/lit8 v0, p1, 0x2 + + return v0 +.end method