如何使用ProGuard进行模糊处理,但在测试时保持名称可读?

TWi*_*Rob 5 java debugging obfuscation android proguard

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

通常,ProGuarded版本会转换名称

net.twisterrob.app.pack.MyClass
Run Code Online (Sandbox Code Playgroud)

b.a.c.b.a
Run Code Online (Sandbox Code Playgroud)

反射和Android布局/菜单资源可以打破,如果他们遇到我们没有保留名称的类.

对于预发布测试来说,能够对代码进行模糊处理会非常有用,但"不是那么多",比如从

net.twisterrob.app.pack.MyClass
Run Code Online (Sandbox Code Playgroud)

net.twisterrob.app.pack.myclass // or n.t.a.p.MC or anything in between :)
Run Code Online (Sandbox Code Playgroud)

-dontobfuscate当然,proguard 有所帮助,但随后它会使所有破碎的东西再次起作用,因为类名是正确的.

我正在寻找的东西将打破完全混淆会破坏的东西,但同时很容易弄清楚什么是没有使用mapping.txt,因为名称是人类可读的.

我正在寻找http://proguard.sourceforge.net/manual/usage.html#obfuscationoptions,-*dictionary选项似乎没有这样做.

我可以自己生成一个重命名文件(它只是运行所有类并给它们一个toLowerCase或者什么):

net.twisterrob.app.pack.MyClassA -> myclassa
net.twisterrob.app.pack.MyClassB -> myclassb
Run Code Online (Sandbox Code Playgroud)

那么问题是如何将这样的文件提供给ProGuard以及格式是什么?

TWi*_*Rob 6

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

长话短说

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

映射.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
Run Code Online (Sandbox Code Playgroud)

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

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

我尝试根据传递给 proguard ( -injars) 的当前类路径生成输入映射.txt。我将所有类加载到一个URLClassLoader包含所有程序 jar 以及库jar 的文件中(例如,用于解析超类)。然后迭代每个类和每个声明的成员并输出我想要使用的名称。

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

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

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

  • 让我们proguardRelease做它的工作
    ,输出源代码mapping.txt
  • 将其转换mapping.txt为一个新文件
  • 复制proguardReleasegradle 任务并使用转换后的映射运行它

问题在于,复制整个任务确实很复杂,所有的都是、、、、、等等inputs……outputs无论如何,我实际上已经开始走这条路线,但它很快就加入了第三个解决方案。doLastdoFirst@TaskAction

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

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

  • 让我们proguardRelease做它的工作
    ,输出源代码mapping.txt
  • 将其转换mapping.txt为一个新文件
  • 使用相同的配置再次运行 proguard,
    但这次使用我的映射文件进行重命名

结果就是我想要的:(
示例模式是<package>.__<class>__.__<field>__类名和字段名大小写颠倒)

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__.__ACCESS$100__(AppCompatDelegateImplV7.java:89)
        at android.support.v7.app.__aPPcOMPATdELEGATEiMPLv7$1__.run(AppCompatDelegateImplV7.java:123)
        at android.os.Handler.handleCallback(Handler.java:733)
Run Code Online (Sandbox Code Playgroud)

或者注意这里的下划线: _<name> 未混合的名称与保留的名称混合

实施/细节

我试图使其尽可能简单,同时保持最大的灵活性。我称其为“不混淆”,因为它消除了适当的混淆,但仍然被认为是反射方面的混淆。

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

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

有一个缺点,它需要双倍的时间来保护它,我想如果你真的需要调试它,这是值得的。

这是 Gradle 的 android hooking 代码:

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
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

可能的解构

您应该能够对包名称应用转换,但我没有对此进行测试。

// 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();
Run Code Online (Sandbox Code Playgroud)

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

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.
Run Code Online (Sandbox Code Playgroud)

当然,上述所有转换都可以以一种或另一种方式被欺骗,但对于不常见的前置后缀和文本转换来说,这种可能性较小。