Simplify 通过虚拟执行应用来理解其行为,然后尝试优化代码,使其行为相同但更易于人类理解。每种优化类型都简单且通用,因此无论使用了何种具体的混淆技术都适用。
左侧代码是混淆应用的反编译结果,右侧代码是经过反混淆处理的代码。
项目包含三个部分:smalivm、simplify 和 demo 应用。
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 开发工具包 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
要了解哪些内容会被反混淆,请查看 Obfuscated App 的 README。
如果 Simplify 执行失败,请按顺序尝试以下建议:
-it 选项仅定位少量方法或类。--max-address-visits、--max-call-depth 和 --max-method-visits。-v 或 -v 2 运行,并结合日志和 DEX 或 APK 的哈希值报告问题。如果在 Windows 上构建失败,并出现类似于以下错误:
Could not find tools.jar. Please check that C:\Program Files\Java\jre1.8.0_151 contains a valid JDK installation.
这意味着 Gradle 无法找到正确的 JDK 路径。请确保已安装 JDK,将 JAVA_HOME 环境变量设置为您的 JDK 路径,并确保关闭后重新打开构建时使用的命令提示符。
不要害羞。我认为虚拟执行和反混淆是非常迷人的问题。任何对此感兴趣的人自动就很酷,欢迎贡献,即使只是修正一个拼写错误。欢迎在 Issue 中提问并提交拉取请求。
请包含 APK 或 DEX 的链接以及您使用的完整命令。这将大大方便问题的重现(从而修复)。
如果您无法分享样本,请包含文件哈希值(SHA1、SHA256 等)。
如果某个操作将值放置到可以转为常量的类型中(如字符串、数字或布尔值),此优化会用常量替换该操作。例如:
const-string v0, "VGVsbCBtZSBvZiB5b3VyIGhvbWV3b3JsZCwgVXN1bC4="
invoke-static {v0}, Lmy/string/Decryptor;->decrypt(Ljava/lang/String;)Ljava/lang/String;
# 解密为:"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 })。如果代码是可达的,但不会影响方法外部的任何状态,即没有副作用,则可视为死代码。例如,代码可能不影响方法的返回值、不改变任何类变量、也不执行任何 I/O。在静态分析中这很难判断。幸运的是,smalivm 无需聪明,它只是愚蠢地执行所有能执行的内容,并在无法确定时假定存在副作用。考虑常量传播中的例子:
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 操作、将 Ljava/lang/String;-><init> 调用替换为 const-string 等等。
.method public static test1()I
.locals 2
new-instance v0, Ljava/lang/Integer;
const/4 v1, 0x1
invoke-direct {v0, v1}, Ljava/lang/Integer;-><init>(I)V
invoke-virtual {v0}, Ljava/lang/Integer;->intValue()I
move-result v0
return v0
.end method
这实际上只是 v0 = 1。
.method public static test1()I
.locals 2
new-instance v0, Ljava/lang/Integer;
const/4 v1, 0x1
invoke-direct {v0, v1}, Ljava/lang/Integer;-><init>(I)V
invoke-virtual {v0}, Ljava/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-stringconst-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 文件。