全局分配数组优化

逆向工程 拆卸 x86 C
2021-06-11 07:38:22

通过两种方式编译C代码片段VS2010

int g_arra[3];

int main() {
  int idx = 2;
  g_arra[0] = 10;
  g_arra[1] = 20;
  g_arra[2] = 30;
  g_arra[idx] = 40;
  return 0;
}
  1. /O2优化
  2. 没有/O2优化

关于/O2优化:

此选项可优化速度。这是通常推荐的优化级别。在 O2 和更高级别启用编译器矢量化。使用此选项,编译器执行一些基本循环优化、内联内联、文件内过程间优化和最常见的编译器优化技术。

当试图扭转它时:

  1. /O2优化:

    ; int __cdecl main(int argc, const char **argv, const char **envp)
    .text:00401000 _main        proc near    ; CODE XREF: ___tmainCRTStartup+11Dp
    .text:00401000              mov     dword_403390, 10
    .text:0040100A              mov     dword_403394, 20
    .text:00401014              mov     dword_403398, 40
    .text:0040101E              xor     eax, eax
    .text:00401020              retn
    .text:00401020 _main        endp
    
  2. 没有/O2

     ; int __cdecl main(int argc, const char **argv, const char **envp)
    .text:00401000 _main        proc near    ; CODE XREF: ___tmainCRTStartup+11Dp
    .text:00401000 var_4        = dword ptr -4
    .text:00401000 argc         = dword ptr  8
    .text:00401000 argv         = dword ptr  0Ch
    .text:00401000 envp         = dword ptr  10h
    .text:00401000              push    ebp
    .text:00401001              mov     ebp, esp
    .text:00401003              push    ecx
    .text:00401004              mov     [ebp+var_4], 2
    .text:0040100B              mov     dword_403390, 0Ah
    .text:00401015              mov     dword_403394, 14h
    .text:0040101F              mov     dword_403398, 1Eh
    .text:00401029              mov     eax, [ebp+var_4]
    .text:0040102C              mov     dword_403390[eax*4], 28h
    .text:00401037              xor     eax, eax
    .text:00401039              mov     esp, ebp
    .text:0040103B              pop     ebp
    .text:0040103C              retn
    .text:0040103C _main        endp
    

编译的代码没有/O2给我关于全局数组的明确解释,我可以计算它的大小(每个 4 字节[eax*4])以及它有多少个元素。

我的问题是,如何处理第一种情况?编译器隐藏其他指令在哪里?如何检测,函数具有全局分配数组堆栈分配数组

2个回答

在一般情况下,相同类型的数组变量和顺序放置的变量之间没有区别。有关数组和更复杂数据类型的信息在编译期间丢失。

但是,有多种方法可以从上下文中恢复此信息(它们都不适用于您的特定情况,因为它太简单了,因此可以优化)。

  • 从分配。如果使用 malloc 或 new [] 运算符分配某些内容,则它可能是数组或类/结构对象,并且可以通过观察对该内存的访问来揭示元素大小。
  • 从访问模式。如果你看到一个循环访问内存,它可能是一个数组
  • 通过未访问的内存间隙,堆栈和全局(之前提到的“漏洞”)。如果编译器无法检测和“展开”对数组的访问,它将动态计算被访问元素的偏移量,并将此偏移量添加到数组的第一个元素的地址。如果您尝试分析此类代码及其变量分配,您将看到数组的“基数”和其后的一些未引用内存,这些内存将作为 *(base +computed_offset) = something 进行访问。

关于“隐藏指令”:

优化代码中不需要它们。您的两个变体在功能上是等效的。编译器只是在编译过程中计算了所需的索引,所以没有人在这里隐藏任何东西。

在 x86 上,ESPEBP寄存器用于访问堆栈上的数据。如果你看到像这样的代码

mov [ebp - 4], 1

它访问堆栈。ws 的回答告诉您如何区分对单个变量或数组的访问。
没有理智的编译器或编码器会使用 esp/ebp 除了堆栈之外的任何东西,但是,如果您想真正确定(在运行时),您可以从 TEB(仅限 Windows)获取堆栈边界。

对全局变量的访问将始终是对固定内存位置的访问。

mov     dword_403390, 0Ah

在这个例子中,403390 是固定的内存位置(它是实际固定的还是由于 ASLR/重定位而改变并不重要,因为它在反汇编中是不可见的)。
为了进一步验证,您可以检查地址是否位于加载的可执行文件或 DLL/共享模块的边界内。