如何使用 ProGuard 混淆但在测试时保持名称可读?

How to obfuscate with ProGuard but keep names readable while testing?

我的应用程序处于预发布阶段,我开始编译发布版本 assembleRelease 而不是 assembleDebug。然而,混淆会破坏事物,并且很难破译什么是什么。调试几乎是不可能的,即使保留行号,变量 classes 也是不可读的。虽然发布版本不稳定,但我想让混淆变得不那么痛苦,但它仍然应该表现得像完全混淆一样。

通常 ProGuarded 版本会将名称从

net.twisterrob.app.pack.MyClass

b.a.c.b.a

反射和 Android layout/menu 资源可以中断,如果它们遇到 class 我们没有保留其名称的元素。

预发布测试能够混淆代码真的很有帮助,但是"not that much",比如从

转换名字
net.twisterrob.app.pack.MyClass

net.twisterrob.app.pack.myclass // or n.t.a.p.MC or anything in between :)

proguard -dontobfuscate 当然有帮助,但它会使所有损坏的东西重新工作,因为 class 名称是正确的。

我正在寻找的东西会破坏完全混淆会破坏的东西,但与此同时,不使用 mapping.txt 也很容易弄清楚是什么,因为名称保持人类可读性。

我正在四处查看 http://proguard.sourceforge.net/manual/usage.html#obfuscationoptions,但 -*dictionary 选项似乎没有这样做。

我可以自己生成一个重命名文件(它只是 运行 通过所有 classes 并给他们一个 toLowerCase 或其他东西):

net.twisterrob.app.pack.MyClassA -> myclassa
net.twisterrob.app.pack.MyClassB -> myclassb

接下来的问题是 我如何将这样的文件提供给 ProGuard,格式是什么?

看来我已经成功跳过了我链接部分中的选项 -applymapping

TL;DR

跳转到实现/细节部分并将这两块Gradle/Groovy代码复制到您的Android子项目的build.gradle文件中。

mapping.txt

mapping.txt的格式很简单:

full.pack.age.Class -> obf.usc.ate.d:
    Type mField -> mObfusc
    #:#:Type method(Arg,s) -> methObfusc
kept.Class -> kept.Class:
    Type mKept -> mKept
    #:#:Type kept() -> kept

缩小的 classes 和成员根本没有列出。所以所有可用的信息,如果我能生成相同的信息或对其进行转换,那么成功的机会就很大。

解决方案 1:转储所有 classes [失败]

我尝试根据传递给 proguard (-injars) 的当前 class 路径生成输入 mapping.txt。我将所有 classes 加载到 URLClassLoader 中,其中包含所有程序 jar 和 libraryjars(例如解析超级 classes)。然后遍历每个 class 和每个声明的成员并输出一个我想使用的名称。

这有一个大问题:此解决方案包含应用程序中每个可重命名的事物的混淆名称。这里的问题是 -applymapping 按字面意思理解并尝试应用输入映射文件中的所有映射,忽略 -keep 规则,导致有关重命名冲突的警告。所以我放弃了这条路,因为我不想复制proguard配置,也不想自己实现proguard配置解析器。

解决方案 2:运行 proguardRelease 两次 [失败]

基于上述失败,我想到了另一种解决方案,该方案将利用所有配置并保留在那里。流程如下:

  • proguardRelease完成它的工作
    这输出源 mapping.txt
  • mapping.txt 转换为新文件
  • 复制 proguardRelease gradle 任务并 运行 使用转换后的映射

问题在于复制整个任务真的很复杂,因为它是 inputsoutputsdoLastdoFirst@TaskAction, etc...其实我是从这条路线开始的,但很快就加入了第三种解决方案。

解决方案 3:使用 proguardRelease 的输出 [成功]

在尝试复制整个任务并分析 proguard/android 插件代码时,我意识到再次模拟 proguardRelease 正在做的事情会容易得多。这是最终流程:

  • proguardRelease完成它的工作
    这输出源 mapping.txt
  • mapping.txt 转换为新文件
  • 运行 使用相同的配置再次混淆,
    但这次使用我的映射文件进行重命名

结果就是我想要的:
(例如,模式是 <package>.__<class>__.__<field>__ 和 class 以及大小写倒置的字段名称)

java.lang.NullPointerException: Cannot find actionView! Is it declared in XML and kept in proguard?
        at net.twisterrob.android.utils.tools.__aNDROIDtOOLS__.__PREPAREsEARCH__(AndroidTools.java:533)
        at net.twisterrob.inventory.android.activity.MainActivity.onCreateOptionsMenu(MainActivity.java:181)
        at android.app.Activity.onCreatePanelMenu(Activity.java:2625)
        at android.support.v4.app.__fRAGMENTaCTIVITY__.onCreatePanelMenu(FragmentActivity.java:277)
        at android.support.v7.internal.view.__wINDOWcALLBACKwRAPPER__.onCreatePanelMenu(WindowCallbackWrapper.java:84)
        at android.support.v7.app.__aPPcOMPATdELEGATEiMPLbASE$aPPcOMPATwINDOWcALLBACK__.onCreatePanelMenu(AppCompatDelegateImplBase.java:251)
        at android.support.v7.app.__aPPcOMPATdELEGATEiMPLv7__.__PREPAREpANEL__(AppCompatDelegateImplV7.java:1089)
        at android.support.v7.app.__aPPcOMPATdELEGATEiMPLv7__.__DOiNVALIDATEpANELmENU__(AppCompatDelegateImplV7.java:1374)
        at android.support.v7.app.__aPPcOMPATdELEGATEiMPLv7__.__ACCESS0__(AppCompatDelegateImplV7.java:89)
        at android.support.v7.app.__aPPcOMPATdELEGATEiMPLv7__.run(AppCompatDelegateImplV7.java:123)
        at android.os.Handler.handleCallback(Handler.java:733)

或者注意这里的下划线:

实施/细节

我试图让它尽可能简单,同时保持最大的灵活性。 我称它为 unfuscation,因为它正在取消适当的混淆,但仍然被认为是反射方面的混淆。

我实施了一些守卫,因为第二轮做了一些假设。显然,如果没有混淆,就没有必要取消混淆。此外,如果关闭调试,取消混淆(并且可能被意外释放)几乎毫无意义,因为取消混淆对 IDE 内部的帮助最大。如果应用程序经过测试和混淆,AndroidProguardTask 的内部正在使用映射文件,我现在不想处理它。

所以我继续创建了一个 unfuscate 任务,它执行转换和 运行s proguard。遗憾的是,proguard 配置没有在 proguard.gradle.ProguardTask 中公开,但这什么时候阻止了任何人?! :)

有一个缺点,proguard需要双倍的时间,如果你真的需要debug,我想这是值得的。

这是 Gradle 的 android 挂钩代码:

afterEvaluate {
    project.android.applicationVariants.all { com.android.build.gradle.api.ApplicationVariant variant ->
        Task obfuscateTask = variant.obfuscation
        def skipReason = [ ];
        if (obfuscateTask == null) { skipReason += "not obfuscated" }
        if (!variant.buildType.debuggable) { skipReason += "not debuggable" }
        if (variant.testVariant != null) { skipReason += "tested" }
        if (!skipReason.isEmpty()) {
            logger.info("Skipping unfuscation of {} because it is {}", variant.name, skipReason);
            return;
        }

        File mapping = variant.mappingFile
        File newMapping = new File(mapping.parentFile, "unmapping.txt")

        Task unfuscateTask = project.task("${obfuscateTask.name}Unfuscate") {
            inputs.file mapping
            outputs.file newMapping
            outputs.upToDateWhen { mapping.lastModified() <= newMapping.lastModified() }
            doLast {
                java.lang.reflect.Field configField =
                        proguard.gradle.ProGuardTask.class.getDeclaredField("configuration")
                configField.accessible = true
                proguard.Configuration config = configField.get(obfuscateTask) as proguard.Configuration
                if (!config.obfuscate) return; // nothing to unfuscate when -dontobfuscate

                java.nio.file.Files.copy(mapping.toPath(), new File(mapping.parentFile, "mapping.txt.bck").toPath(),
                        java.nio.file.StandardCopyOption.REPLACE_EXISTING)
                logger.info("Writing new mapping file: {}", newMapping)
                new Mapping(mapping).remap(newMapping)

                logger.info("Re-executing {} with new mapping...", obfuscateTask.name)
                config.applyMapping = newMapping // use our re-written mapping file
                //config.note = [ '**' ] // -dontnote **, it was noted in the first run

                LoggingManager loggingManager = getLogging();
                // lower level of logging to prevent duplicate output
                loggingManager.captureStandardOutput(LogLevel.WARN);
                loggingManager.captureStandardError(LogLevel.WARN);
                new proguard.ProGuard(config).execute();
            }
        }
        unfuscateTask.dependsOn obfuscateTask
        variant.dex.dependsOn unfuscateTask
    }
}

整体的另一部分是改造。我设法快速组成了一个全匹配的正则表达式模式,所以它非常简单。您可以安全地忽略 class 结构和重映射方法。关键是 processLine ,每行都会调用它。该行被分成几部分,混淆名称前后的文本保持原样(两个 substrings),名称在中间更改。更改 unfuscate 中的 return 语句以满足您的需要。

class Mapping {
    private static java.util.regex.Pattern MAPPING_PATTERN =
            ~/^(?<member>    )?(?<location>\d+:\d+:)?(?:(?<type>.*?) )?(?<name>.*?)(?:\((?<args>.*?)\))?(?: -> )(?<obfuscated>.*?)(?<class>:?)$/;
    private static int MAPPING_PATTERN_OBFUSCATED_INDEX = 6;

    private final File source
    public Mapping(File source) {
        this.source = source
    }

    public void remap(File target) {
        target.withWriter { source.eachLine Mapping.&processLine.curry(it) }
    }

    private static void processLine(Writer out, String line, int num) {
        java.util.regex.Matcher m = MAPPING_PATTERN.matcher(line)
        if (!m.find()) {
            throw new IllegalArgumentException("Line #${num} is not recognized: ${line}")
        }
        try {
            def originalName = m.group("name")
            def obfuscatedName = m.group("obfuscated")
            def newName = originalName.equals(obfuscatedName) ? obfuscatedName : unfuscate(originalName, obfuscatedName)
            out.write(line.substring(0, m.start(MAPPING_PATTERN_OBFUSCATED_INDEX)))
            out.write(newName)
            out.write(line.substring(m.end(MAPPING_PATTERN_OBFUSCATED_INDEX)))
            out.write('\n')
        } catch (Exception ex) {
            StringBuilder sb = new StringBuilder("Line #${num} failed: ${line}\n");
            0.upto(m.groupCount()) { sb.append("Group #${it}: '${m.group(it)}'\n") }
            throw new IllegalArgumentException(sb.toString(), ex)
        }
    }

    private static String unfuscate(String name, String obfuscated) {
        int lastDot = name.lastIndexOf('.') + 1;
        String pkgWithDot = 0 < lastDot ? name.substring(0, lastDot) : "";
        name = 0 < lastDot ? name.substring(lastDot) : name;
        // reassemble the names with something readable, but still breaking changes
        // pkgWithDot will be empty for fields and methods
        return pkgWithDot + '_' + name;
    }
}

可能的反混淆

您应该可以对包名称应用转换,但我没有测试过。

// android.support.v4.a.a, that is the original obfuscated one
return obfuscated;

// android.support.v4.app._Fragment
return pkgWithDot + '_' + name;

// android.support.v4.app.Fragment_a17d4670
return pkgWithDot + name + '_' + Integer.toHexString(name.hashCode());

// android.support.v4.app.Fragment_a
return pkgWithDot + name + '_' + afterLastDot(obfuscated)

// android.support.v4.app.fRAGMENT
return pkgWithDot + org.apache.commons.lang.StringUtils.swapCase(name);
// needs the following in build.gradle:
buildscript {
    repositories { jcenter() }
    dependencies { classpath 'commons-lang:commons-lang:2.6' }
}

// android.support.v4.app.fragment
return pkgWithDot + name.toLowerCase();

警告:不可逆的转换很容易出错。考虑以下因素:

class X {
    private static final Factory FACTORY = ...;
    ...
    public interface Factory {
    }
}
// notice how both `X.Factory` and `X.FACTORY` become `X.factory` which is not allowed.

当然,上述所有转换都可以通过某种方式被欺骗,但使用不常见的前置后缀和文本转换的可能性较小。