簡體   English   中英

如何使用 ProGuard 進行混淆但在測試時保持名稱可讀?

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

我的應用程序處於預發布階段,我開始編譯發布版本assembleRelease而不是assembleDebug 然而,混淆會破壞事物,並且很難破譯什么是什么。 調試幾乎是不可能的,即使保留了行號,變量類也是不可讀的。 雖然發布版本不穩定,但我想讓混淆不那么痛苦,但它仍然應該表現得像完全混淆一樣。

通常 ProGuarded 版本將名稱從

net.twisterrob.app.pack.MyClass

b.a.c.b.a

如果反射和 Android 布局/菜單資源遇到我們沒有保留名稱的類,它們可能會中斷。

能夠混淆代碼對預發布測試真的很有幫助,但“沒有那么多” ,比如從

net.twisterrob.app.pack.MyClass

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

proguard -dontobfuscate當然有幫助,但它會使所有損壞的東西再次工作,因為類名是正確的。

我正在尋找的東西將打破完全混淆會破壞的東西,但同時很容易弄清楚不使用 mapping.txt 是什么,因為名稱保持人類可讀。

我環顧四周http://proguard.sourceforge.net/manual/usage.html#obfuscationoptions-*dictionary選項似乎沒有這樣做。

我可以自己生成一個重命名文件(它只是運行所有類並給它們一個toLowerCase或其他東西):

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

問題是我如何將這樣的文件提供給 ProGuard 以及格式是什么?

所以看起來我已經設法跳過了我鏈接的部分中的選項-applymapping

TL; 博士

跳轉到實現/詳細信息部分並將這兩個 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

收縮的類和成員根本沒有列出。 所以所有可用的信息,如果我可以生成相同的信息或轉換它,那么成功的機會就很大。

解決方案 1:轉儲所有類 [失敗]

我試圖根據傳遞給 proguard ( -injars ) 的當前類路徑生成輸入 mapping.txt 。 我在URLClassLoader加載了所有類,其中包含所有程序 jar 和 libraryjar(例如解析超類)。 然后遍歷每個類和每個聲明的成員並輸出我希望使用的名稱。

這有一個大問題:這個解決方案包含應用程序中每個可重命名的東西的混淆名稱。 這里的問題是-applymapping從字面上-applymapping並嘗試應用輸入映射文件中的所有映射,忽略-keep規則,導致有關重命名沖突的警告。 所以我放棄了這條路,因為我不想復制proguard config,也不想自己實現proguard config parser。

解決方案 2:運行proguardRelease兩次 [失敗]

基於上述失敗,我想到了另一種解決方案,它可以利用所有配置並保持存在。 流程如下:

  • proguardRelease完成它的工作
    這將輸出源mapping.txt
  • mapping.txt轉換為新文件
  • 復制proguardRelease gradle 任務並使用轉換后的映射運行它

問題是復制整個任務真的很復雜,包括inputsoutputsdoLastdoFirst@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)

或者注意這里的下划線: _<name> 未混淆的名稱與保留的名稱混合

實施/細節

我試圖讓它盡可能簡單,同時保持最大的靈活性。 我稱之為 unfuscation,因為它正在取消適當的混淆,但仍然被認為是在例如反射方面的混淆。

我實施了一些守衛,因為第二輪做了一些假設。 顯然,如果沒有混淆,就不需要 unfuscated。 此外,如果關閉調試,則 unfuscate(並且可能會意外釋放)幾乎毫無意義,因為 unfuscation 在 IDE 中最有幫助。 如果應用程序經過測試和混淆,則 AndroidProguardTask 的內部正在使用映射文件,我現在不想處理它。

所以我繼續創建了一個 unfuscate 任務,它進行轉換並運行 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
    }
}

整體的另一部分是轉化。 我設法快速編寫了一個全匹配的正則表達式模式,所以它非常簡單。 您可以放心地忽略類結構和重新映射方法。 關鍵是processLine ,它為每一行調用。 該行被拆分為部分,混淆名稱前后的文本保持原樣(兩個substring ),名稱在中間更改。 更改為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.

當然,上述所有轉換都可以以一種或另一種方式被欺騙,但不太可能使用不常見的前置后綴和文本轉換。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM