给定以下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);
}


未经任何优化(甚至没有剥离)进行了编译使用clang作为64位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的请求,我为函数添加了汇编代码(并将main更改为bar): >这是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


评论

这个问题(像样的反编译器在重构结构上真的不好吗,还是我错过了一些配置?)确实引起了人们的意见,这是令人沮丧的。但是看到这个问题得到了(似乎被您接受的)答案,这似乎是潜在的问题,因此无需我干预。请记住,对于RE.SE(和其他SE网站)的问题,征求意见通常是一个不好的信号。

你是对的。我已改掉这个问题。对不起。

#1 楼

默认的编译选项不会嵌入完整的调试信息,按值传递的小结构与通过寄存器传递的一堆单独的参数是无法区分的(请参阅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);
}


评论


感谢您提供DWARF调试信息。但是我不确定结构传递的不可区分性。 System V ABI x86-64(第3.2.3节)说,超过8个字节的结构或包含未对齐数据的结构将属于MEMORY类,即其字段将通过堆栈传递/返回。

– Ta Thanh Dinh
19年11月29日在16:09

为什么反编译器会认识到rdi,rsi,...不是在main中初始化的,所以它们不会用作foo的参数?

– Ta Thanh Dinh
19年11月29日在16:18

#2 楼

您的示例中有几处使得难以反编译的原因。 main()很麻烦,因为如果您阅读C ++标准,它或多或少是一个vararg函数,并且您可以看到,至少IDA猜测堆栈上有三个参数。

int和long在您的结构定义中,这可能会或可能不会在生成的代码中创建堆栈的填充或掩码。它也可以是声明(主)的一种方法,也可以是通过值将其传递给(叶)的函数的另一种方法。将在堆栈上有一个可能使用的红色区域。

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

拆卸看起来像什么?

编辑:哦,拆卸真的很重要!关键是LLVM取决于IR是否适合优化,因为在优化之前,代码看起来就像是舔乐高积木的人。然后把石头扔给它:D难怪它会使反编译器感到困惑:)例如,看那个有趣的字节大小的“ bonus parameter”和“ unsensical” movsx指令。再次。不使用红色区域。甚至不需要序言,因为堆栈上没有存储任何内容,所有计算都在RCX和RAX上完成。既然您已经摆脱了main()中的所有堆栈变量,那么让您失望的是,您正在按值传递一个小的,堆栈分配的结构。在C语言中,看起来像传递一个Blob作为参数实际上是将每个字段都视为一个单独的参数。我猜想IDA和Ghidra都将能够理解这一点,如果不是因为这里的“ alignment”(?)字节。也许不是,因为程序集看起来仍然像是在堆栈上传递四个单独的参数:|

Tl; dr:除非经过优化,否则clang会生成真正奇怪的代码。再加上按值传递堆栈分配的结构,这将使反编译器和困倦的逆向工程师(例如我)感到困惑。借此机会养成按值传递结构的习惯,并爱上const-refs;)

评论


谢谢,我添加了反汇编代码。我已将main更改为普通功能栏,以避免main的麻烦。

– Ta Thanh Dinh
19年11月30日在10:31

我从未想到过红色区域(我不知道)。实际上,到目前为止,似乎使用了红色区域,因为该函数中没有堆栈帧分配(不减去rsp)。但是我仍然不明白为什么它使反编译变得困难:局部变量是否仍基于rbp?

– Ta Thanh Dinh
19-11-30在10:35



将main更改为bar并没有帮助,结果大致相同。但可以预期,将s放在堆上会得到更清晰的结果。

– Ta Thanh Dinh
19年11月30日10:39

@TaThanhDinh我真的不知道您是否收到有关更新答案的通知:我更新了答案。

– Tobias
19年11月30日在12:36

@ ta-thanh-dinh我只是意识到我没有回答有关局部变量和RBP的问题;不,在x86-64代码中很少使用帧指针,因为IA64和AMD64 ABI都定义了函数及其堆栈布局,其严格程度足以使您在大多数情况下实际上并不需要。

– Tobias
19/12/2在20:33