14

我试图找到确凿的事实,以帮助我的管理层了解对编译的 C 代码进行逆向工程的难易程度。

之前在这个站点上已经提出过类似的问题(例如,是否可以“反编译”Windows .exe?或者至少查看程序集?或者可以反编译用 C 编写的 DLL?),但这些问题的要点是反编译已编译的 C 代码“很难,但并非完全不可能”。

为了促进基于事实的答案,我包含了一个神秘函数的编译代码,我建议这个问题的答案通过它们是否可以确定这个函数的作用来衡量所提出的技术的成功或失败。这对于 SO 来说可能是不寻常的,但我认为这是对这个工程问题获得“良好的主观”或事实答案的最佳方式。因此,您对这个函数在做什么以及如何做的最好猜测是什么?

这是编译后的代码,在 Mac OSX 上使用 gcc 编译:

_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.));
}
4

3 回答 3

16

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

//-------------------------------------------------------------------------
// 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 寄存器的低位双精度,因此该函数似乎实际上适用于标量双精度而不是向量。至于 _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;
}

它产生的程序集与原始帖子非常相似。

于 2013-01-14T17:16:44.273 回答
6

逆向工程/反编译任何代码都是花费时间与这样做的好处的问题;不是做起来有多难。

如果你有一些你绝对不能让出去的秘方,那么你唯一能做的就是把秘方作为一个网络服务,在必要时被调用。这样,二进制文件就永远不会离开您的公司墙。

一旦黑客在他们控制的系统上拥有已编译的二进制文件,即使是混淆也只能追踪到任何东西。哎呀,最初的 PC 克隆是通过对 IBM BIOS 进行逆向工程创建的。

所以,回到正题:再一次,这不是一个东西有多难的问题,它更多的是一个人是否愿意尝试的问题......这是基于他们会从中获得什么感知价值。无论是直接美元(接收或储蓄)、竞争优势还是只是吹嘘自己的权利。更复杂的是应用程序的可用性:更广泛的分布等于更高的潜力找到进入黑客工作桶的方式。

如果存在这些价值观,那么您可以放心,有人会尝试并且他们会成功。这应该会引导您进入下一个问题:如果他们这样做了怎么办?最坏的结果是什么?

在某些情况下,这只是一次失败的销售,您可能无论如何都没有得到。在其他情况下,这可能是业务的损失。

于 2013-01-14T17:59:47.947 回答
1

从根本上说,进行单独的机器指令“逆向工程”非常容易,因为机器指令具有非常明确的语义。这会给你糟糕的 C 代码,但肯定不是目标。(知道文件中的某些二进制模式机器指令在技术上是图灵难的,例如,在某些情况下是不可能的;在编译器生成的代码的情况下不太可能)。

除此之外,您正在尝试推断算法和意图。这非常困难;包含这一切的知识从何而来?

你可能会发现我关于逆向工程的论文很有趣。它提出了一种编码必要知识的方法。

在某种程度上也有商业工具可以做到这一点。据我了解,这并没有达到我的论文概述的方案,但仍然产生了相当合理的 C 代码。(我对这个工具没有具体的经验,但非常尊重作者和他的工具)。

于 2013-01-14T18:44:05.697 回答