反汇编汇编代码有多难?

18

我正在寻找硬性事实,以帮助我的管理层了解反编译编译后的 C 代码有多难/易。

此前在该网站上曾提出过类似的问题(例如,参见Is it possible to “decompile” a Windows .exe? Or at least view the Assembly?Possible to decompile DLL written in C?),但这些问题的要点是,反编译编译后的 C 代码“很难,但并非完全不可能”。

为了方便基于事实的回答,我包含了一个神秘函数的编译代码,并建议回答这个问题的答案通过是否能够确定这个函数的功能来衡量所提出技术的成功或失败。 这在 SO 上可能不太常见,但我认为这是获得对这个工程问题的“好的主观”或客观答案的最佳方式。因此,你认为这个函数在做什么,以及它是如何实现的?

以下是使用 gcc 在 Mac OSX 上编译的代码:

_mystery:
Leh_func_begin1:
    pushq   %rbp
Ltmp0:
    movq    %rsp, %rbp
Ltmp1:
    movsd   LCPI1_0(%rip), %xmm1
    subsd   %xmm0, %xmm1
    pxor    %xmm2, %xmm2
    ucomisd %xmm1, %xmm2
    jbe     LBB1_2
    xorpd   LCPI1_1(%rip), %xmm1
LBB1_2:
    ucomisd LCPI1_2(%rip), %xmm1
    jb      LBB1_8
    movsd   LCPI1_0(%rip), %xmm1
    movsd   LCPI1_3(%rip), %xmm2
    pxor    %xmm3, %xmm3
    movsd   LCPI1_1(%rip), %xmm4
    jmp     LBB1_4
    .align  4, 0x90
LBB1_5:
    ucomisd LCPI1_2(%rip), %xmm1
    jb      LBB1_9
    movapd  %xmm5, %xmm1
LBB1_4:
    movapd  %xmm0, %xmm5
    divsd   %xmm1, %xmm5
    addsd   %xmm1, %xmm5
    mulsd   %xmm2, %xmm5
    movapd  %xmm5, %xmm1
    mulsd   %xmm1, %xmm1
    subsd   %xmm0, %xmm1
    ucomisd %xmm1, %xmm3
    jbe     LBB1_5
    xorpd   %xmm4, %xmm1
    jmp     LBB1_5
LBB1_8:
    movsd   LCPI1_0(%rip), %xmm5
LBB1_9:
    movapd  %xmm5, %xmm0
    popq    %rbp
    ret 
Leh_func_end1:

更新

@Igor Skochinsky是第一个找到正确答案的人:这确实是Heron算法计算平方根的一种简单实现。原始源代码在此处:

#include <stdio.h>

#define EPS 1e-7

double mystery(double x){
  double y=1.;
  double diff;
  diff=y*y-x;
  diff=diff<0?-diff:diff;
  while(diff>=EPS){
    y=(y+x/y)/2.;
    diff=y*y-x;
    diff=diff<0?-diff:diff;
  }
  return y;
}

int main() {
  printf("The square root of 2 is %g\n", mystery(2.));
}

15
你的声望已经超过7千,却称呼“网站管理员”?难道你还不了解这个网站是如何运作的吗? - Kerrek SB
3
“@djechlin: How is "guess what my assembler does?" ever a valid question? (or was that sarcasm?)” 的意思是:“‘猜猜我的汇编器是干什么用的?’怎么可能是一个合理的问题?(或者这是讽刺吗?)”。 - Oliver Charlesworth
2
@lindelof - 我会给你另一个例子在这里,其中10行内联函数和C++模板编译成4-5条机器指令。有多大的可能性能够重现原始源代码? - Bo Persson
2
一般情况下是不可能的,原始代码是绝对不可能的,在极少数情况下,如果没有使用优化器并且代码非常简单,你可以重构出一个功能相同的东西,而不需要回到C语言。 - old_timer
3
这就像将音频文件转换为mp3,将图像转换为jpg,将电影转换为mpeg等,这是一种有损压缩。您无法恢复原始信号。编译器中也会发生同样的情况,源代码中的信息在编译过程中会丢失,输出结果中不可见,您无法返回到原始状态。如果可能,功能上类似的C代码并不比汇编语言更易读或更易维护,如果必须进行修改,则最好使用汇编语言或根据汇编语言的分析手动编写C代码。 - old_timer
显示剩余9条评论
3个回答

18

这是使用Hex-Rays Decompiler反编译后,我将代码转换为x86(目前不支持x64),添加了一些原始帖子中缺失的数据定义并进行汇编的结果:

//-------------------------------------------------------------------------
// Data declarations

double LCPI1_0 =  1.0; // weak
double LCPI1_1[2] = {  0.0,  0.0 }; // weak
double LCPI1_2 =  1.2; // weak
double LCPI1_3 =  1.3; // weak


//----- (00000000) --------------------------------------------------------
void __usercall mystery(__m128d a1<xmm0>)
{
  __m128d v1; // xmm1@1
  __m128d v2; // xmm1@4
  __int128 v3; // xmm2@4
  __m128d v4; // xmm5@7
  __m128d v5; // xmm1@7

  v1 = (__m128d)*(unsigned __int64 *)&LCPI1_0;
  v1.m128d_f64[0] = LCPI1_0 - a1.m128d_f64[0];
  if ( LCPI1_0 - a1.m128d_f64[0] < 0.0 )
    v1 = _mm_xor_pd(v1, *(__m128d *)LCPI1_1);
  if ( v1.m128d_f64[0] >= LCPI1_2 )
  {
    v2 = (__m128d)*(unsigned __int64 *)&LCPI1_0;
    v3 = *(unsigned __int64 *)&LCPI1_3;
    while ( 1 )
    {
      v4 = a1;
      v4.m128d_f64[0] = (v4.m128d_f64[0] / v2.m128d_f64[0] + v2.m128d_f64[0]) * *(double *)&v3;
      v5 = v4;
      v5.m128d_f64[0] = v5.m128d_f64[0] * v5.m128d_f64[0] - a1.m128d_f64[0];
      if ( v5.m128d_f64[0] < 0.0 )
        v5 = _mm_xor_pd(a1, (__m128d)*(unsigned __int64 *)LCPI1_1);
      if ( v5.m128d_f64[0] < LCPI1_2 )
        break;
      v2 = a1;
    }
  }
}
// 90: using guessed type double LCPI1_0;
// 98: using guessed type double LCPI1_1[2];
// A8: using guessed type double LCPI1_2;
// B0: using guessed type double LCPI1_3;

// ALL OK, 1 function(s) have been successfully decompiled

显然,它需要改进(目前XMM支持有些基础),但我认为基本算法已经可以理解了。

编辑:由于明显仅使用所有XMM寄存器的低double,因此似乎该函数实际上是使用标量double而不是向量。至于_mm_xor_pd(xorpd)内部函数,我认为这只是编译器实现符号反转的方式 - 通过与预定义的常量进行异或操作,该常量在符号位位置具有1,在其他位置都为0。考虑到上述情况,并进行一些清理后,我得到以下代码:

double mystery(double a1)
{
  double v1; // xmm1@1
  double v2; // xmm1@4
  double v3; // xmm2@4
  double v4; // xmm5@7
  double v5; // xmm1@7

  v1 = LCPI1_0 - a1;
  if ( v1 < 0.0 )
    v1 = -v1;
  if ( v1 < LCPI1_2 )
  {
    v4 = LCPI1_0;
  }
  else
  {
    v2 = LCPI1_0;
    v3 = LCPI1_3;
    while ( 1 )
    {
      v4 = a1;
      v4 = (v4 / v2 + v2) * v3;
      v5 = v4;
      v5 = v5 * v5 - a1;
      if ( v5 < 0.0 )
        v5 = -v5;
      if ( v5 < LCPI1_2 )
        break;
      v2 = a1;
    }
  }
  return v4;
}

它生成的汇编代码与原始帖子非常相似。


那么,你认为这段代码在做什么方面最有可能?我认为你需要在低级代码恢复的基础上进行算法识别。附言:尽管被关闭了,但是反向工程做得很好,给你点赞+1 :) - Ira Baxter
1
看起来像是求平方根的巴比伦方法。LCPI1_0是初始近似值,LCPI1_2是epsilon,而LCPI1_3是常数0.5。 - Igor Skochinsky
1
@IgorSkochinsky 恭喜你,你做到了! - lindelof

6
逆向工程/反编译任何代码都是时间成本与收益之间的问题,而不是难度问题。
如果您有一些绝对不能外泄的秘方,那么您唯一能做的就是将其作为Web服务,并在必要时调用。这样,二进制文件永远不会离开公司墙壁。
即使混淆也只能做到一定程度,因为一旦黑客控制了编译后的二进制文件,任何东西都可以被追踪。事实上,最初的PC克隆是通过逆向工程IBM BIOS创建的。
所以,回到重点:再次强调,这不是一个难度问题,而更多地取决于是否有人想尝试...这基于他们认为自己可以从中获取的价值。无论是直接的金钱(收入或节省),竞争优势还是简单的炫耀权利。加剧这一点的是应用程序的可用性:更广泛的分发意味着更高的潜在风险,可能会落入黑客手中进行攻击。
如果存在这些价值,那么您可以确信有人会尝试并且他们会成功。这应该引导您思考下一个问题:如果他们真的成功了怎么办?最坏的结果是什么?
在某些情况下,这只是一个失去的销售机会,也许您本来就不会得到。而在其他情况下,可能会导致业务损失。

2
基本上,进行单个机器指令的“逆向工程”相当容易,因为机器指令具有极其明确定义的语义。这将给您带来糟糕的C代码,但这显然不是目标。(在某些情况下,知道文件中的某些二进制模式是机器指令在技术上是图灵难题,即有时是不可能的;在编译器生成的代码的情况下不太可能出现这种情况)。
除此之外,您正在尝试推断算法和意图。这非常困难;所有这些知识来自何处?
您可能会发现我的关于逆向工程的论文有趣。它提出了一种编码必要知识的方法。
也有商业工具可以做到这一点到某种程度。这没有达到我论文所概述的方案的程度,但据我了解,它仍然可以产生相当合理的C代码。(我对这个工具没有具体的经验,但对作者及其工具非常尊重)。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接