为什么不能轻松地反编译本机代码?


16

使用基于字节码的虚拟机语言(例如Java,VB.NET,C#,ActionScript 3.0等),有时您会听到,只需从Internet下载一些反编译器,并在其中运行字节码一次,这是多么容易,并且通常,您会在几秒钟内想到与原始源代码相差不大的内容。据说这种语言特别容易受到攻击。

最近,我开始想知道,为什么您至少不了解本机二进制代码最初是用哪种语言编写的(以及尝试将其反编译成哪种语言),却为什么对本机二进制代码却听不懂呢。很长一段时间以来,我发现这仅仅是因为本地机器语言比典型的字节码疯狂得多,而且更加复杂。

但是字节码是什么样的?看起来像这样:

1000: 2A 40 F0 14
1001: 2A 50 F1 27
1002: 4F 00 F0 F1
1003: C9 00 00 F2

本地机器代码是什么样(十六进制)?当然,它看起来像这样:

1000: 2A 40 F0 14
1001: 2A 50 F1 27
1002: 4F 00 F0 F1
1003: C9 00 00 F2

指令来自类似的思维框架:

1000: mov EAX, 20
1001: mov EBX, loc1
1002: mul EAX, EBX
1003: push ECX

因此,考虑到尝试将某些本机二进制文件反编译为C ++的语言,它有什么困难?马上想到的只有两个想法:1)实际上比字节码复杂得多,或者2)操作系统倾向于分页程序并分散程序段会导致太多问题的事实。如果这些可能性之一是正确的,请解释。但是,无论哪种方式,您为什么基本上都没有听说过?

注意

我将接受其中一个答案,但我想先提一下。几乎每个人都在指一个事实,即不同的原始源代码片段可能映射到相同的机器代码。局部变量名称丢失,您不知道最初使用的是哪种类型的循环,等等。

但是,在我看来,像刚才提到的两个例子是微不足道的。但是,有些答案倾向于指出,机器代码和原始源代码之间的差异远比这种琐碎的事情大得多。

但是例如,当涉及局部变量名称和循环类型之类的内容时,字节码也会丢失此信息(至少对于ActionScript 3.0而言)。之前,我已经通过反编译器将这些东西拉回来,而且我真的不在乎变量是被调用strMyLocalString:String还是loc1。我仍然可以在较小的本地范围内进行查看,看看它的使用方式没有太大麻烦。而一个for循环是相当多的同样的事情while循环,如果您考虑一下。同样,即使当我通过irrFuscator运行源代码(与secureSWF不同,它不仅要做随机化成员变量和函数名称之外,所做的工作不多),看起来仍然可以像在较小的类中开始隔离某些变量和函数一样,如图了解如何使用它们,为它们分配自己的名称,然后从那里开始工作。

为了使这成为一个大问题,机器代码将需要丢失比这更多的信息,并且某些答案确实可以解决这个问题。


35
用汉堡包做牛很难。
卡兹龙2014年

4
主要问题是本机二进制文件保留有关程序的极少元数据。它不保留有关类的信息(使得C ++特别难于反编译),也不保留有关函数的任何信息–不必要,因为CPU固有地以相当线性的方式执行代码,一次只执行一条指令。另外,不可能区分代码和数据(link)。有关更多信息,您可能需要考虑在RE.SE上进行搜索或重新询问。
ntoskrnl 2014年

Answers:


39

在编译的每个步骤中,您都会丢失无法恢复的信息。您从原始来源丢失的信息越多,反编译的难度就越大。

您可以为字节码创建一个有用的反编译器,因为从原始源保留的信息要比生成最终目标机器代码时保留的信息多得多。

编译器的第一步是将源代码转换成通常表示为树的中间表示形式。传统上,这棵树不包含非语义信息,例如注释,空格等。一旦丢弃该信息,就无法从该树中恢复原始源。

下一步是将树渲染为某种形式的中间语言,从而使优化更加容易。这里有很多选择,每个编译器基础结构都有自己的选择。但是,通常通常会丢失诸如局部变量名称,大型控制流结构(例如,是否使用for或while循环)之类的信息。此处通常会发生一些重要的优化操作,例如恒定传播,不变代码运动,函数内联等。每一种优化都会将表示形式转换为功能相同但外观却大不相同的表示形式。

之后的步骤是生成实际的机器指令,该机器指令可能涉及所谓的“窥孔”优化,从而产生常见指令模式的优化版本。

在每一步中,您将丢失越来越多的信息,直到最后,您损失了如此之多,以至于无法恢复任何类似于原始代码的内容。

另一方面,字节码通常会保存有趣的转换性优化,直到生成目标机器代码的JIT阶段(即时编译器)为止。字节码包含许多元数据,例如局部变量类型,类结构,以允许将相同的字节码编译为多个目标机器码。所有这些信息在C ++程序中都是不必要的,在编译过程中将被丢弃。

有用于各种目标机器代码的反编译器,但是它们通常不会产生有用的结果(您可以对其进行修改然后重新编译),因为会丢失太多的原始源代码。如果您具有可执行文件的调试信息,则可以做得更好。但是,如果您有调试信息,则可能也有原始来源。


5
保留信息以使JIT更好地工作这一事实是关键。
btilly 2014年

那么,C ++ DLL是否容易反编译?
Panzercrisis

1
没有什么我认为有用的。
chuckj

1
元数据不是“允许将相同的字节码编译到多个目标”,而是在这里进行反射。可重新定向的中间表示形式不需要任何该元数据。
SK-logic

2
那是不对的。那里有很多数据需要反射,但反射并不是唯一的用途。例如,接口和类定义用于在目标机器上创建定义字段偏移量,构造虚拟表等,从而允许以目标机器最有效的方式构造它们。这些表由编译器和/或链接器在生成本机代码时构造。完成此操作后,将丢弃用于构造它们的数据。
chuckj

11

正如其他答案所指出的那样,信息丢失是有原因的,但这不是破坏者。毕竟,你不要指望原来的程序回来,你只是想任何一个高级语言表示。如果代码是内联的,则可以顺其自然,也可以自动排除常见的计算。原则上您可以撤消许多优化。但是有些操作原则上是不可逆的(至少没有无限量的计算)。

例如,分支可能成为计算的跳转。像这样的代码:

select (x) {
case 1:
    // foo
    break;
case 2:
    // bar
    break;
}

可能会被编译成(抱歉,这不是真正的汇编程序):

0x1000:   jump to 0x1000 + 4*x
0x1004:   // foo
0x1008:   // bar
0x1012:   // qux

现在,如果您知道x可以是1或2,则可以查看跳跃并轻松地将其反转。但是地址0x1012呢?您是否也应该case 3为此创建一个?在最坏的情况下,您必须跟踪整个程序以找出允许的值。更糟糕的是,您可能必须考虑所有可能的用户输入!问题的核心是您无法区分数据和指令。

话虽这么说,我不会完全悲观。正如您在上面的“汇编器”中可能已经注意到的那样,如果x来自外部并且不能保证为1或2,那么您实质上就存在一个严重的错误,该错误使您可以跳转到任何地方。但是,如果您的程序没有此类错误,则推理起来会容易得多。(“ CLR IL”或Java字节码之类的“安全”中间语言更容易反编译,即使将元数据放在一边也绝非偶然。)因此,在实践中,应该可以对某些行为规范的代码进行反编译。程式。我正在考虑没有副作用且定义明确的输入的个性化功能样式例程。我认为周围有几个反编译器可以为简单功能提供伪代码,但是我对这类工具没有太多经验。


9

机器代码无法轻松转换回原始源代码的原因是,编译期间会丢失大量信息。可以内联方法和非导出类,丢失本地变量名,完全丢失文件名和结构,编译器可以进行非显而易见的优化。另一个原因是,多个不同的源文件可以产生完全相同的程序集。

例如:

int DoSomething()
{
    return Add(5, 2);
}

int Add(int x, int y)
{
    return x + y;
}

int main()
{
    return DoSomething();
}

可以编译为:

main:
mov eax, 7;
ret;

我的程序集非常生锈,但是如果编译器可以验证优化是否可以正确完成,它就会这样做。这是由于已编译的二进制文件不需要知道名称DoSomethingAdd,以及该Add方法具有两个命名参数的事实,因此编译器还知道该DoSomething方法本质上返回一个常量,并且可以内联方法调用和方法本身。

编译器的目的是创建程序集,而不是捆绑源文件的方法。


考虑将最后一条指令更改为just ret,只是说您正在假设C调用约定。
chuckj 2014年

3

这里的一般原则是多对一映射,并且缺少规范的代表。

对于多对一现象的简单示例,您可以考虑一下使用带有一些局部变量的函数并将其编译为机器代码时会发生什么。有关变量的所有信息都将丢失,因为它们只是成为内存地址。循环也会发生类似的情况。您可以采用foror或while循环,如果它们的结构恰到好处,那么您可能会获得带有jump指令的相同机器代码。

这也导致缺少机器码指令的原始源代码的规范代表。当您尝试反编译循环时,如何将jump指令映射回循环结构?您使它们for循环还是while循环。

现代编译器执行各种形式的折叠和内联的事实使这个问题更加恼怒。因此,当您接触到机器代码时,几乎不可能分辨出高层机器代码是从哪里来的。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.