为什么拆卸不是一门精确的科学?

逆向工程 拆卸 重新组装
2021-06-11 01:17:29

新手来了

来自维基百科

反汇编不是一门精确的科学:在具有可变宽度指令的 CISC 平台上,或者在存在自修改代码的情况下,单个程序可能有两个或更多个合理的反汇编。确定在程序运行期间实际会遇到哪些指令简化为已证明无法解决的停机问题。

  1. 为什么CISC上的反汇编不是一门精确的科学? 我知道重新组装反汇编代码可能会为同一条指令产生不同但相似的操作码,但由于它们相似,因此不应影响生成的程序。而且,如果它不是一门精确的科学,即 assemble(disassemble(opcode)) != opcode,CPU 如何确定用哪种方式来解释操作码流?

  2. 附带说明一下,这是否意味着在 RISC 平台上进行反汇编是一门精确的科学?

4个回答

先回答第一个问题。最大的问题是您无法真正将数据与代码分开。基本上有两种拆卸方法:

  1. 线性扫描
  2. 递归遍历

使用线性扫描的反汇编器从某个地址开始并一条一条地反汇编指令直到结束,而不会以任何方式跟随跳转或对反汇编代码进行推理。例如 SoftICE 和 Windbg 使用线性扫描。这可能是一个问题,因为您不知道何时停止。你不知道指令在哪里结束,数据从哪里开始。为此,您必须依赖可执行格式元数据,例如节大小。这就是问题所在。

另一方面,递归遍历算法考虑了跳转和调用,反汇编器跟随跳转,只反汇编实际执行的代码。例如 IDA 和 OllyDBG 使用这种方法。它的好处是它本质上知道什么是代码,什么是数据。但很明显,它有它的缺点。首先,通过纯递归遍历,并非所有代码都会被反汇编。例如,在运行时通过计算地址调用的未被直接引用的函数将不会被发现。同样,引擎有一些启发式方法可以绕过这个问题。

举个例子,一个程序向后跳了几条指令,但不是停留在先前反汇编和执行的指令的开头,而是跳转到它的中间。反汇编器应该如何决定显示哪一个?如果这是编码人员的意图,两者都可能是有效的。递归遍历反汇编器可能会显示第一个反汇编的版本,并只标记一个跳转到函数的中间。但是如果不详细检查字节,就很难判断跳转后实际执行了什么。

还有很多其他的例子。

这两种方法的另一个大问题是自修改代码。它只是不能静态完成。

CPU 本身没有任何这些问题,因为它正在执行代码,换句话说,它是动态的。

要回答第二个问题,我认为这不仅仅是 CISC 架构的问题,其中一些也可以应用于 RISC。

除了前面答案中描述的问题之外,还有另一种程序可以进行多次反汇编的方式。考虑具有以下逻辑的代码(我对语法大屠杀表示歉意):

buf = ...一些字符串...
val = read ()
if (val == 7) {
  mprotect (buf, ..., PROT_EXEC ); /* 使 buf 可执行 */
  转到buf; /* 执行 buf */
}
else {
  打印(buf);
}

在这种情况下,buf是代码(因此应该反汇编)还是数据(因此不应反汇编)取决于输入。因此,该程序有多种可能的反汇编。请注意,这是因为代码与数据从根本上无法区分,并且与 RISC 与 CISC 无关。

在这篇论文中,有一个关于如何将代码伪装成看似合理的数据的令人愉快的讨论:

J. Mason、S. Small、F. Monrose、G. MacManus。英文外壳代码。 第 16 届 ACM 计算机和通信安全(CCS)会议论文集,伊利诺伊州芝加哥。2009 年 11 月。[PDF]

我认为评论的意思是在CISC上,以x86为例,反汇编有几种可能的合理表示但我认为这也可以部分说明典型的 RISC 实现,其中 - 然而 - 汇编器可以提供类似 CISC 的助记符,由基本 (RISC) 操作码的不同组合表示。

例如,rep前缀在许多操作码上是没有意义的,但在过去已经被使用来增加逆向工程师的难度。但是,对于最终查看反汇编的人来说,哪种表现更好?rep前缀的或不前缀的(这更多地反映了 CPU 的作用)。

如果反汇编程序对其发现的内容保持真实,则应包含rep前缀。另一方面,反汇编程序的工作是使机器代码对人类可读。因此,为了简洁起见,加倍努力并确保删除多余的前缀是非常有意义的。类似地,表示一个nop(无操作)的多字节操作码可以被完全剥离或以一种从其余部分中脱颖而出的方式表示(例如 IDA 对对齐字节所做的)或nop分别表示(对于 1 和 5 字节版本一样) .

所以从我的角度来看,反汇编机器代码一门精确的科学,因为对于每个操作码都有一个精确的助记符(让我们在这里忽略jne/jnz对偶)。否则,正如您所指出的,CPU 将如何处理并弄清楚要做什么?但是,为了向人类逆向工程师提供表示,有时(我敢说经常吗?)对其进行后处理以提高可读性和可理解性。这就是反汇编程序和反汇编程序不同的地方。


另一个问题是,为了遵循所有可能的代码路径,必须使用启发式方法(这是一个好的反汇编器的显着特征之一)。否则很难区分数据和代码。但据我所知,“CISC”不能因为这个特征而被单独挑出来,所以我认为这不是发表评论的主要原因。

反汇编中的一个关键问题在于将代码与二进制可执行文件中的数据精确分离。为此,您需要进行静态分析,以精确确定间接跳转(跳转到运行时计算的地址)的目标。有几个间接跳转的例子,包括 C++ 中的switch目标计算和 vtables。

如果存在这种类型的静态分析,将能够解决已知不可判定停机问题因此,这种静态分析是不可能存在的。基于此,反汇编工具需要依靠启发式和/或近似值来确定间接跳转目标。有关更多详细信息,请参见此处类似问题的答案ARM 反汇编的健全性如果二进制文件被混淆和/或具有自修改代码,则代码/数据分离的任务会变得更加困难。

从二进制文件中提取了代码和数据的近似值。然后需要处理将语义附加到它们的问题,这也很困难。例如struct { int i; int j;}int arr [2]当通过指令访问时,a看起来类似于 an 因此,很难在不依赖调试符号等外部信息的情况下为执行的代码附加独特的语义。