Java和.NET反编译器可以(通常)生成几乎完美的源代码,通常与原始代码非常接近。

为什么不能对本机代码执行相同的操作?我尝试了一些,但是它们要么不起作用,要么产生混乱的getos和带有指针的强制转换。

评论

您写了这篇文章真是太好了,但是仍然需要以问答形式进行。如果您可以将其转化为一系列问题,那就更好了:)

更好吗?

您真的扩展了如何使高级代码的恢复变得困难吗?我将跳过问题的这一部分,而只涉及反编译。通过imo,您的答案非常好。

@IgorSkochinsky您是否只是用此编辑将Hex-Rays反编译器称为糟糕的? :P

好吧,我的一般观点是,您可以在许多这样的问题中阅读:)

#1 楼

TL; DR:机器代码反编译器非常有用,但是不要期望它们为托管语言提供同样的奇迹。列举一些限制:结果通常无法重新编译,缺少原始源代码中的名称,类型和其他关键信息,比原始源代码减去注释更难阅读,并且可能会很奇怪反编译列表中特定于处理器的工件。



为什么反编译器如此受欢迎?

反编译器是非常有吸引力的反向工程工具,因为它们具有节省大量工作的潜力。实际上,它们对于诸如Java和.NET之类的托管语言而言过于合理,以至于几乎没有“ Java和.NET逆向工程”这一主题。这种情况使许多初学者想知道机器代码是否同样如此。不幸的是,这种情况并非如此。机器代码反编译器的确存在,并且在节省分析师时间方面很有用。但是,它们仅是非常手动的过程的辅助。这是真的,原因是字节码语言和机器代码反编译器面临着一系列不同的挑战。 >
在整个编译过程中,语义信息的丢失会带来一些挑战。托管语言通常保留变量的名称,例如对象中字段的名称。因此,很容易为人类分析师提供程序员创建的名称,希望这些名称有意义。这样可以提高对反编译机器代码的理解速度。

另一方面,机器代码程序的编译器通常会在编译程序时销毁所有这些信息中的大多数(也许以调试信息的形式将其中的一些信息抛在后面)。因此,即使机器代码反编译器在其他方面都很完美,它仍会呈现非信息性的变量名(例如“ v11”,“ a0”,“ esi0”等),这会减慢人类理解的速度。 。


我可以重新编译反编译的程序吗?

一些与拆卸程序有关的挑战。在Java和.NET等字节码语言中,与已编译对象关联的元数据通常将描述对象中所有代码字节的位置。也就是说,所有函数都将在对象标题的某些表中具有一个条目。

另一方面,以机器语言为例,以x86 Windows反汇编为例,而无需诸如此类的大量调试信息作为PDB,反汇编程序不知道二进制代码中的代码位于何处。它给出了一些提示,例如程序的入口点。结果,机器代码反汇编程序被迫实施自己的算法以发现二进制文件中的代码位置。它们通常使用两种算法:线性扫描(扫描文本部分以查找通常表示函数开头的已知字节序列)和递归遍历(遇到固定位置的调用指令时,将该位置视为包含代码) )。

但是,由于编译器优化(例如过程间寄存器分配会修改导致线性扫描组件失败的函数序言的过程间寄存器分配)以及自然发生的间接控制流(例如,通过函数指针)导致递归遍历失败。因此,即使机器代码反编译器没有遇到其他问题,它也通常无法对整个程序进行反编译,因此结果将无法重新编译。

代码上述/数据分离问题属于理论问题的特殊类别,称为“不确定”问题,它与其他无法解决的问题(如“暂停”问题)共享。因此,不希望找到一个自动的机器代码反编译器,该反编译器将生成可重新编译以获取原始二进制文件克隆的输出。反编译程序?

与托管语言相比,诸如C和C ++之类的语言的本质还存在挑战。我将在这里讨论类型信息。在Java字节码中,有一条称为“ new”的专用指令来分配对象。它采用一个整数参数,该整数参数被解释为.class文件元数据的参考,该文件描述了要分配的对象。该元数据依次描述了类的布局,成员的名称和类型,等等。这样就很容易以令人愉悦的方式对类的引用进行反编译。

另一方面,当编译C ++程序时,在没有诸如RTTI之类的调试信息的情况下,对象的创建不会以整洁的方式进行。它调用用户指定的内存分配器,然后将结果指针作为参数传递给构造函数(也可以内联,因此不是函数)。访问类成员的指令在语法上与局部变量引用,数组引用等没有区别。此外,类的布局未存储在二进制文件的任何位置。实际上,发现剥离二进制文件中数据结构的唯一方法是通过数据流分析。因此,反编译器必须执行自己的类型重构以应对这种情况。实际上,流行的反编译器Hex-Rays大多将这项任务留给了人工分析人员(尽管它也提供了对人工有用的帮助)。


反汇编是否基本类似于原始源代码就其控制流结构而言?

一些挑战来自于将编译器优化应用于已编译二进制文件。与较不那么激进的编译器相比,称为“尾巴合并”的流行优化导致程序的控制流程被残缺,后者通常在反编译中表现为很多goto语句。稀疏switch语句的编译会导致类似的问题。另一方面,托管语言通常具有switch语句指令。


当涉及处理器的晦涩难懂的方面时,反编译器是否会提供有意义的输出?

一些挑战源于所讨论处理器的体系结构特征。例如,x86上内置的浮点单元是一场噩梦。没有浮点“寄存器”,没有浮点“堆栈”,并且必须对其进行精确跟踪才能正确地反编译程序。相反,托管语言通常具有专门的指令来处理本身就是变量的浮点值。 (Hex-Rays可以很好地处理浮点算术。)或者考虑一下x86上有数百种合法指令类型的事实,其中大多数都不是由常规编译器生成的,除非用户明确指定应通过固有。反编译器必须对它本机支持的指令进行特殊处理,因此大多数反编译器仅使用内联汇编或(最好)不支持的内在函数来支持对编译器最常生成的指令的支持。 >

这些只是困扰机器代码反编译器的一些挑战性示例。我们可以预见,在可预见的将来,限制仍然存在。因此,不要寻求像托管语言反编译器一样有效的魔术子弹。

评论


您是否希望在其他方面选择新答案或将其编辑为答案?通常,我对在此代表级别进行编辑感到不舒服(对于私人测试版来说可能有所不同吗?),因为它最终会排入队列等。但是无所谓。那是什么呢? :)

– 0xC0000022L♦
13年3月27日在17:07

您可以随时对其进行编辑,或提出新的主题,我将对其进行编辑。

–滚轴
13年3月27日在22:07

On6。代码经过流水线优化后,单个操作的逻辑序列可能会与前一个和/或下一个逻辑操作块混合在一起。

–杂件
13-10-19在23:01

#2 楼

反编译很困难,因为反编译器必须恢复二进制/字节码目标中缺少的源代码抽象。

有几种抽象类型:


功能:标识与高功能相对应的代码,包括其入口,参数,返回值和出口。
变量:每个函数中的局部变量以及任何全局或静态变量。
类型:每个变量的类型以及每个函数的参数和返回值。
高级控制流:程序的控制流模式,例如while (...) { if (...) {...} else {...} }

反编译本机代码很困难,因为这些抽象均未在本机代码中明确表示。因此,要生成好的反编译代码(即,不在各处​​使用goto),反编译器必须根据本机代码的行为来重新推断这些抽象。这是一个困难的过程,并且已经撰写了许多有关如何推断这些抽象的论文。初学者请参见Balakrishnan和Lee。结果,字节码通常包含对函数(或方法),变量以及每个变量的类型的显式抽象。字节码中缺少的主要抽象是高级控制流。