反编译器中的结构重建

逆向工程 反编译 小精灵 结构
2021-06-11 01:18:28

我正在测试几个关于结构重建的反编译器,给出以下C示例:

struct S {
    int x;
    int y;
    long z;
    long t;
};

int foo(struct S s) {
    return s.x + s.y + s.z + s.t;
}

int main() {
    struct S s;
    s.x = 10; s.y = 15; s.z = 20; s.t = 25;
    return foo(s);
}

使用clang64 位 ELF 在没有任何优化(甚至没有剥离)的情况下编译,即 ABI 是System V x86-64.

我认为这是一个微不足道的案例,所以体面的反编译器应该给出正确的结果,但不幸的是,它们并非如此。

以下结果由IDA 7.4.191122

int __cdecl main(int argc, const char **argv, const char **envp)
{
  __int64 v3; // r8
  __int64 v4; // r9

  return foo(*(__int64 *)&argc, (__int64)argv, (__int64)envp, 20LL, v3, v4, 0xF0000000ALL, 20, 25);
}

__int64 __fastcall foo(__int64 a1, __int64 a2, __int64 a3, __int64 a4, __int64 a5, __int64 a6, __int64 a7, int a8, int a9)
{
  return (unsigned int)(a9 + a8 + HIDWORD(a7) + a7);
}

接下来,JEB 3.7.0

unsigned long main() {
  return foo();
}

unsigned long foo() {
  unsigned int v0 = v1 + v2;
  return (unsigned long)(((unsigned int)(((long)v0 + v3 + v4)));
}

Ghidra 9.1

void main(void)
{
  foo();
  return;
}

ulong foo(void)
{
  int param_7;
  undefined8 param_7_00;
  int iStack000000000000000c;
  long param_8;
  long param_9;

  return (param_7 + iStack000000000000000c) + param_8 + param_9 & 0xffffffff;
}

我不能说结果是“好”的,它们甚至不正确。我是否错过了这些反编译器的一些配置?

编辑:由于@Tobias 的请求,我为函数添加了汇编代码(并更改mainbar):

这是foo

0x0         55                                   push rbp
0x1         48 89 e5                             mov rbp, rsp
0x4         48 8d 45 10                          lea rax, [rbp+0x10]
0x8         8b 08                                mov ecx, [rax]
0xa         03 48 08                             add ecx, [rax+0x8]
0xd         48 63 d1                             movsxd rdx, ecx
0x10        48 03 50 10                          add rdx, [rax+0x10]
0x14        48 03 50 18                          add rdx, [rax+0x18]
0x18        48 0f be 40 04                       movsx rax, byte ptr [rax+0x4]
0x1d        48 01 c2                             add rdx, rax
0x20        89 d0                                mov eax, edx
0x22        5d                                   pop rbp
0x23        c3                                   ret

bar

0x30        55                                   push rbp
0x31        48 89 e5                             mov rbp, rsp
0x34        48 83 ec 40                          sub rsp, 0x40
0x38        c7 45 e0 0a 00 00 00                 mov dword ptr [rbp-0x20], 0xa
0x3f        c7 45 e8 0f 00 00 00                 mov dword ptr [rbp-0x18], 0xf
0x46        48 c7 45 f0 14 00 00 00              mov qword ptr [rbp-0x10], 0x14
0x4e        48 c7 45 f8 19 00 00 00              mov qword ptr [rbp-0x8], 0x19
0x56        c6 45 e4 1e                          mov byte ptr [rbp-0x1c], 0x1e
0x5a        48 8d 45 e0                          lea rax, [rbp-0x20]
0x5e        48 8b 08                             mov rcx, [rax]
0x61        48 89 0c 24                          mov [rsp], rcx
0x65        48 8b 48 08                          mov rcx, [rax+0x8]
0x69        48 89 4c 24 08                       mov [rsp+0x8], rcx
0x6e        48 8b 48 10                          mov rcx, [rax+0x10]
0x72        48 89 4c 24 10                       mov [rsp+0x10], rcx
0x77        48 8b 40 18                          mov rax, [rax+0x18]
0x7b        48 89 44 24 18                       mov [rsp+0x18], rax
0x80        e8 7b ff ff ff                       call foo
0x85        48 83 c4 40                          add rsp, 0x40
0x89        5d                                   pop rbp
0x8a        c3                                   ret
2个回答

您的示例中有几件事使反编译变得困难。

s是 main() 中第一个也是唯一的局部(堆栈中的)变量。main() 很麻烦,因为如果您阅读 C++ 标准,它或多或少是一个可变参数函数,并且您可以看到至少 IDA 猜测堆栈上有三个参数。

您在结构定义中同时使用 int 和 long,这可能会也可能不会在生成的代码中创建堆栈填充或掩码。它也可以是您声明它(主要)的一种方式,以及将其按值传递给(叶)函数的另一种方式。

而且, foo() 是一个叶函数,这意味着它将在堆栈上有一个可能被使用的红色区域。

尝试放在s堆上,您可能会看到非常不同的结果:)

拆机是什么样子的?

编辑:哦,拆卸真的很重要!关键是 LLVM 取决于 IR 对优化的适合程度,因为在优化之前,代码看起来像是用乐高积木搭建的代码。然后向它扔石头 :D 难怪它会混淆反编译器 :) 例如,看看那个有趣的字节大小的“奖金参数”和“荒谬的”movsx 指令。

无论如何,再次认真面对。未使用红色区域。甚至不需要序言,因为堆栈中没有存储任何内容,所有计算都在 RCX 和 RAX 上完成。现在您已经摆脱了 main() 中的任何堆栈变量,让您失望的是您正在按值传递一个小的、堆栈分配的结构。在 C 中看起来像将单个 blob 作为参数传递的内容实际上将每个字段视为一个单独的参数。我猜想 IDA 和 Ghidra 都能够理解这一点,如果不是因为在那里抛出的“对齐”(?)字节。或者可能不是,因为程序集看起来仍然像是在堆栈上传递四个单独的参数:|

Tl; dr:除非优化,否则clang会生成非常奇怪的代码。再加上按值传递堆栈分配的结构,它会使反编译器和像我这样昏昏欲睡的逆向工程师混淆。借此机会养成按值传递结构的习惯,并学会喜欢 const-refs ;)

默认编译选项不嵌入完整的调试信息,并且按值传递的小结构与在寄存器中传递的一堆单个参数无法区分(请参阅 ABI 规范)。如果启用 DWARF 调试信息生成 ( -gdwarf),您将获得稍微好一点的输出至少 IDA 可以利用 DWARF 信息来导入类型、应用函数参数和局部变量信息:

int __cdecl foo(S s)
{
  return LODWORD(s.t) + LODWORD(s.z) + s.y + s.x;
}
int __cdecl main(int argc, const char **argv, const char **envp)
{
  S v4; // [rsp+0h] [rbp-40h]

  *(_QWORD *)&v4.x = 0xF0000000ALL;
  v4.z = 20LL;
  v4.t = 25LL;
  return foo(v4);
}