为什么 Java 源代码与其反编译结果之间存在(有时是主要的)差异?

逆向工程 反编译 爪哇
2021-06-26 09:22:11

我将一些 [相对复杂的] Java 代码编译成一个 .class 文件,然后使用 jad 将其反编译回 java。当然,代码被混淆了,这是意料之中的。但是,鉴于我拥有原始代码,我认为我可以相当轻松地查看反编译代码。但是,我注意到代码中的差异,例如某些变量的定义位置(包括范围的差异)。

这有什么主要原因吗?我想这是在反编译代码时发生的事情之一,但我更好奇是什么因素导致了变化(例如代码的复杂性,是否引用了其他文件等)。

有人可以就哪些因素导致前后代码差异向我提供一个很好的解释吗?

编辑

从技术上讲,.class 文件是从 jar 中提取的。我提取了内容并在其中使用了 .class 文件。

至于我使用的混淆器,我使用了带有以下选项的 Retroguard 混淆器(我目前只是在探索混淆并找出每件事对最终结果的影响):

.option Application
.option Applet
.option Repackage
.option Annotations
.option MapClassString
.attribute LineNumberTable
.attribute EnclosingMethod
.attribute Deprecated

该脚本的文档可以在他们的站点上找到它有点无组织,但你应该能够在那里找到足够的解释。同样值得注意的是,我剥离了泛型和局部变量表。

我现在还设置了一种方法(受 Minecraft Coder Pack 的创建者启发)使用来自文件(或多个文件)的数据重命名源,该文件被传递到包、类、方法和字段的列表字典.

# snippet from the MCP version (mine's slightly different) (all in Python):
srg_types = {'PK:': ['obf_name', 'deobf_name'],
            'CL:': ['obf_name', 'deobf_name'],
            'FD:': ['obf_name', 'deobf_name'],
            'MD:': ['obf_name', 'obf_desc', 'deobf_name', 'deobf_desc']}
parsed_dict = {'PK': [],
               'CL': [],
               'FD': [],
               'MD': []}

然后从文件中解析出一行,并将其传递到 中parsed_dict,然后用于重命名所有内容(来回)。编译后实现而不是第一次反编译(在我注意到差异之后)。

3个回答

Java 编译为在 JVM 中运行的字节码,并存储在 .class 文件中。此字节码不是原始代码的 1:1 表示,并且包括几个编译器实现的优化。当执行这些优化时,信息会丢失,并且由于丢失的信息,反编译器无法将代码重建回原来的样子。

补充一下 Ditmar 所说的,最大的问题可能是你的混淆。普通的 Java 字节码实际上非常接近原始源代码,至少从 C 或 C++(甚至 Scala)的角度来看是如此。您总是会丢失一些信息,但未混淆的 Java 可以反编译为接近原始版本的内容,尤其是在使用调试信息进行编译时。

然而,所有这些都随着混淆而消失,因为混淆器故意试图操纵代码的结构。在反编译混淆代码时,你应该更惊讶的是,当你发现完全相似的时候。

您选择的反编译器会极大地影响结果。您应该尝试 JODE 或 Fernflower(我相信它来自 Androchef)。

在反编译之前,最好自己做一些简单的反混淆处理。例如,你可以尝试

  • 将用作类/方法/变量名称的 Java 关键字重新映射到合法的 Java 标识符。通过使用 ASM 库http://asm.ow2.org/asm40/javadoc/user/org/objectweb/asm/commons/RemappingClassAdapter.html提供的重映射适配器,可以轻松完成此任务

  • 一些简单的块重组。许多混淆器会将 goto 粘贴到 Java 源代码中“在单个表达式内”的尴尬位置,例如,将某些内容推入堆栈,然后跳转,然后将其存储到目标中的局部变量中,这在字节码中是完全可行的,但是理解字节码只是在做 var = value 可能会给一些反编译器带来困难。