有 Java 编程相关的问题?

你可以在下面搜索框中键入要查询的问题!

java如何使用ProGuard进行混淆,但在测试时保持名称的可读性?

我的应用程序处于发布前阶段,我开始编译发布版本assembleRelease,而不是assembleDebug。然而,混淆会破坏事物,很难破译什么是什么。调试几乎是不可能的,即使保留行号,变量类也无法读取。虽然发布版本不稳定,但我想让模糊处理变得不那么痛苦,但它的行为应该仍然是完全模糊的

通常,预先发布的版本会将名称从

net.twisterrob.app.pack.MyClass

b.a.c.b.a

如果反射和安卓布局/菜单资源遇到了我们没有保留名称的类,它们可以使用这些资源中断

对于发布前测试来说,能够混淆代码真的很有帮助,但“没有那么多”,比如从

net.twisterrob.app.pack.MyClass

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

proguard -dontobfuscate当然有帮助,但它会让所有坏的东西再次工作,因为类名是正确的

我所寻找的将打破完全模糊的东西,但同时不使用映射就可以很容易地找出什么是什么。txt,因为名字是人类可读的

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

我可以自己生成一个重命名文件(只需运行所有类,并给它们一个toLowerCase之类的文件):

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

问题是我如何将这样一个文件提供给ProGuard,格式是什么


共 (1) 个答案

  1. # 1 楼答案

    看起来我已经跳过了我链接的部分中的选项^{}

    TL;DR

    跳转到Implementation/details部分,将这两段Gradle/Groovy代码复制到Android子项目的build.gradle文件中

    地图。txt

    映射的格式。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
    

    缩小的班级和成员根本没有列出。所以所有可用的信息,如果我能生成相同的信息或转换这些信息,就有很大的成功机会

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

    我试图生成一个输入映射。基于当前传递给proguard的类路径(-injars)。我在一个URLClassLoader中加载了所有的类,这个URLClassLoader中有所有的程序JAR和LibraryJAR(例如用于解析超类)。然后遍历每个类和每个声明的成员,并输出我希望使用的名称

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

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

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

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

    问题是,复制整个任务非常复杂,包括inputsoutputsdoLastdoFirst@TaskAction等等。。。我其实是从这条路线开始的,但很快就加入了第三种解决方案

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

    在尝试复制整个任务并分析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)
    

    或者注意下面的下划线: _<name> unfuscated names mixed with kept names

    实施/细节

    我试着让它尽可能简单,同时保持最大的灵活性。 我叫它unfousation,因为它取消了适当的模糊处理,但仍然考虑了模糊处理的反射

    我实施了一些防护措施,因为第二轮有一些假设。很明显,如果没有混淆,就没有必要解开。此外,如果调试被关闭,那么取消暂停(可能会被意外释放)几乎毫无意义,因为取消暂停在IDE中最有帮助。如果应用程序经过测试和模糊处理,AndroidDrugardTask的内部正在使用映射文件,我现在不想处理这个问题

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

    有一个缺点,它需要两倍的时间来升级,我想如果你真的需要调试它,这是值得的

    以下是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
        }
    }
    

    整体的另一部分是转变。我成功地快速构建了一个完全匹配的正则表达式模式,所以它非常漂亮易于理解的可以安全地忽略类结构和remap方法。键是processLine,每行调用它。该行被拆分为部分,模糊名称前后的文本保留为IS(2^ { }s),并且在中间改变名称。更改为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.
    

    当然,上面所有的转换都可能以这样或那样的方式被欺骗,但不常见的前置后缀和文本转换则不太可能