如何使用 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 任务并 运行 使用转换后的映射
问题在于复制整个任务真的很复杂,因为它是 inputs
、outputs
、doLast
、doFirst
、@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
,每行都会调用它。该行被分成几部分,混淆名称前后的文本保持原样(两个 substring
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.
当然,上述所有转换都可以通过某种方式被欺骗,但使用不常见的前置后缀和文本转换的可能性较小。
我的应用程序处于预发布阶段,我开始编译发布版本 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 任务并 运行 使用转换后的映射
问题在于复制整个任务真的很复杂,因为它是 inputs
、outputs
、doLast
、doFirst
、@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
,每行都会调用它。该行被分成几部分,混淆名称前后的文本保持原样(两个 substring
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.
当然,上述所有转换都可以通过某种方式被欺骗,但使用不常见的前置后缀和文本转换的可能性较小。