Simplify 以虚拟方式执行应用程序以了解其行为,然后尝试优化代码,使其行为相同但更易于人们理解。每种优化类型都是简单且通用的,因此使用哪种特定类型的混淆并不重要。
左边的代码是混淆应用程序的反编译,右边的代码已经被反混淆。
该项目由三个部分组成:smalivm、simple 和演示应用程序。
if
或switch
条件都会导致采用两个分支。 usage: java -jar simplify.jar <input> [options]
deobfuscates a dalvik executable
-et,--exclude-types <pattern> Exclude classes and methods which include REGEX, eg: "com/android", applied after include-types
-h,--help Display this message
-ie,--ignore-errors Ignore errors while executing and optimizing methods. This may lead to unexpected behavior.
--include-support Attempt to execute and optimize classes in Android support library packages, default: false
-it,--include-types <pattern> Limit execution to classes and methods which include REGEX, eg: ";->targetMethod("
--max-address-visits <N> Give up executing a method after visiting the same address N times, limits loops, default: 10000
--max-call-depth <N> Do not call methods after reaching a call depth of N, limits recursion and long method chains, default: 50
--max-execution-time <N> Give up executing a method after N seconds, default: 300
--max-method-visits <N> Give up executing a method after executing N instructions in that method, default: 1000000
--max-passes <N> Do not run optimizers on a method more than N times, default: 100
-o,--output <file> Output simplified input to FILE
--output-api-level <LEVEL> Set output DEX API compatibility to LEVEL, default: 15
-q,--quiet Be quiet
--remove-weak Remove code even if there are weak side effects, default: true
-v,--verbose <LEVEL> Set verbosity to LEVEL, default: 0
构建需要安装 Java Development Kit 8 (JDK)。
因为该项目包含 Android 框架的子模块,所以可以使用--recursive
进行克隆:
git clone --recursive https://github.com/CalebFenton/simplify.git
或者随时更新子模块:
git submodule update --init --recursive
然后,构建一个包含所有依赖项的 jar:
./gradlew fatjar
Simplify jar 将位于simplify/build/libs/
中。您可以通过简化提供的混淆示例应用程序来测试它的工作情况。以下是运行它的方式(您可能需要更改simplify.jar
):
java -jar simplify/build/libs/simplify.jar -it " org/cf/obfuscated " -et " MainActivity " simplify/obfuscated-app.apk
要了解反混淆的内容,请查看混淆应用程序的自述文件。
如果简化失败,请按顺序尝试以下建议:
-it
选项仅针对少数方法或类。--max-address-visits
、 --max-call-depth
和--max-method-visits
。-v
或-v 2
并通过日志和 DEX 或 APK 的哈希值报告问题。如果在 Windows 上构建,并且构建失败并出现类似以下内容的错误:
找不到tools.jar。请检查 C:Program FilesJavajre1.8.0_151 是否包含有效的 JDK 安装。
这意味着 Gradle 无法找到正确的 JDK 路径。确保已安装 JDK,将JAVA_HOME
环境变量设置为 JDK 路径,并确保关闭并重新打开用于构建的命令提示符。
别害羞。我认为虚拟执行和反混淆是令人着迷的问题。任何感兴趣的人都会自然而然地很酷,并且欢迎做出贡献,即使只是为了修复一个拼写错误。请随意在问题中提出问题并提交拉取请求。
请包含 APK 或 DEX 的链接以及您正在使用的完整命令。这使得重现(从而修复)您的问题变得更加容易。
如果您无法共享示例,请包含文件哈希(SHA1、SHA256 等)。
如果一个操作放置了一个可以转换为常量(例如字符串、数字或布尔值)的值,则此优化将用该常量替换该操作。例如:
const-string v0, "VGVsbCBtZSBvZiB5b3VyIGhvbWV3b3JsZCwgVXN1bC4="
invoke-static {v0}, Lmy/string/Decryptor;->decrypt(Ljava/lang/String;)Ljava/lang/String;
# Decrypts to: "Tell me of your homeworld, Usul."
move-result v0
在此示例中,加密的字符串被解密并放入v0
中。由于字符串是“可常量化”的,因此move-result v0
可以替换为const-string
:
const-string v0, "VGVsbCBtZSBvZiB5b3VyIGhvbWV3b3JsZCwgVXN1bC4="
invoke-static {v0}, Lmy/string/Decryptor;->decrypt(Ljava/lang/String;)Ljava/lang/String;
const-string v0, "Tell me of your homeworld, Usul."
如果删除代码不可能改变应用程序的行为,则代码已死亡。最明显的情况是代码无法访问,例如if (false) { // dead }
)。如果代码是可达的,如果它不影响方法之外的任何状态,即它没有副作用,则可以认为它是死的。例如,代码可能不会影响方法的返回值、更改任何类变量或执行任何 IO。这在静态分析中很难确定。幸运的是,smalvm 并不需要很聪明。它只是愚蠢地执行它能执行的所有操作,并假设如果不能确定就会产生副作用。考虑恒定传播中的示例:
const-string v0, "VGVsbCBtZSBvZiB5b3VyIGhvbWV3b3JsZCwgVXN1bC4="
invoke-static {v0}, Lmy/string/Decryptor;->decrypt(Ljava/lang/String;)Ljava/lang/String;
const-string v0, "Tell me of your homeworld, Usul."
在此代码中, invoke-static
不再影响方法的返回值,我们假设它不会执行任何奇怪的操作,例如将字节写入文件系统或网络套接字,因此它没有副作用。它可以简单地被移除。
const-string v0, "VGVsbCBtZSBvZiB5b3VyIGhvbWV3b3JsZCwgVXN1bC4="
const-string v0, "Tell me of your homeworld, Usul."
最后,第一个const-string
将一个值分配给寄存器,但该值从未被使用,即分配已失效。也可以将其移除。
const-string v0, "Tell me of your homeworld, Usul."
好哇!
Java 静态分析的一大挑战是反射。如果不进行仔细的数据流分析,就不可能知道参数是否适用于反射方法。有很多聪明的方法可以做到这一点,但 smalivm 只需执行代码即可做到这一点。当它发现反射的方法调用时,例如:
invoke-virtual {v0, v1, v2}, Ljava/lang/reflect/Method;->invoke(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;
它可以知道v0
、 v1
和v2
的值。如果确定这些值是什么,它可以用实际的非反射方法调用替换对Method.invoke()
的调用。这同样适用于反射字段和类查找。
对于所有不完全适合特定类别的事物,都有窥视孔优化。这包括删除无用的check-cast
操作,用const-string
替换Ljava/lang/String;-><init>
调用,等等。
.method public static test1 () I
.locals 2
new-instance v0 , L java/lang/Integer ;
const/4 v1 , 0x1
invoke-direct { v0 , v1 }, L java/lang/Integer ; -> <init> ( I ) V
invoke-virtual { v0 }, L java/lang/Integer ; -> intValue () I
move-result v0
return v0
.end method
这一切所做的就是v0 = 1
。
.method public static test1 () I
.locals 2
new-instance v0 , L java/lang/Integer ;
const/4 v1 , 0x1
invoke-direct { v0 , v1 }, L java/lang/Integer ; -> <init> ( I ) V
invoke-virtual { v0 }, L java/lang/Integer ; -> intValue () I
const/4 v0 , 0x1
return v0
.end method
move-result v0
被替换为const/4 v0, 0x1
。这是因为intValue()I
只有一个可能的返回值,并且返回类型可以设为常量。参数v0
和v1
是明确的并且不会改变。也就是说, intValue()I
处的每个可能的执行路径都有一个值的共识。其他可以转换为常量的值类型:
const/4
、 const/16
等const-string
const-class
.method public static test1 () I
.locals 2
const/4 v0 , 0x1
return v0
.end method
由于const/4 v0, 0x1
上面的代码不会影响方法外部的状态(无副作用),因此可以将其删除而不改变行为。如果有一个方法调用向文件系统或网络写入了某些内容,则无法将其删除,因为它会影响方法外部的状态。或者,如果test()I
采用可变参数,例如LinkedList
,则任何访问它的指令都不能被视为已死。
死代码的其他示例:
if (false) { dead_code(); }
该工具可在双重许可证下使用:适用于闭源项目的商业许可证和可在开源软件中使用的 GPL 许可证。
根据您的需要,您必须选择其中之一并遵循其政策。 LICENSE.COMMERCIAL 和 LICENSE.GPL 文件中提供了每种许可证类型的策略和协议的详细信息。