内存 Space 布局/奇怪的内存(堆栈)行为 C/ASM?

Memory Space Layout / strange memory (stack) behaviour C/ASM?

当为了更好地了解进程内存布局和幕后花絮而摆弄内存时,我未能完全理解它。想象一下下面的代码:

#include <stdio.h>
#include <string.h>

int main(int argc,char **argv) {
    char buf[32];
    strcpy(buf,argv[1]);
    return 0;
}

转储自 IDAdec 而非 hex):

已添加

var_30= dword ptr -30h
var_2C= dword ptr -2Ch
var_20= dword ptr -20h
arg_4= dword ptr  0Ch

结束

push    ebp
mov     ebp, esp
and     esp, 4294967280
sub     esp, 48
call    sub_401920
mov     eax, [ebp+12]
add     eax, 4
mov     eax, [eax]
mov     [esp+4], eax
lea     eax, [esp+16]
mov     [esp], eax
call    strcpy
mov     eax, 0
leave
retn

我的解读:

因为 EBP+0x0-0x3 存储了 EBP 指针和 EBP+0x4-0x7 return 地址,我们可以看到这里发生了什么。

这个问题,虽然,如果回答的话,非常感谢,与其说是 ASM,不如说:

根据我的理解,这个函数的栈帧应该是这样的:

[   ] < ESP+0x0-0x3 
[   ]
[   ]
[   ]
[   ]
[   ]
[   ]
[   ]
[   ]
[   ]
[   ]
[   ] < ESP+0x2C-0x2F
[EBP] < EBP+0x0-0x3
[RET] < EBP+0x4-0x7
[ARG]

其中 ARG (EBP+0x8+) 包含函数的参数。

迷茫

最后,发送太大的缓冲区时 EIP 不再被重写有什么原因吗?那是因为它开始覆盖之前的堆栈帧导致更早的速成课程吗?

让我们分析一下代码

push    ebp
mov     ebp, esp

这是标准序言。

and     esp, 0ffffffff0h 

ESP的低半字节(4位)被清除。在最坏的情况下,这会将堆栈指针降低 15 个字节,而在最好的情况下什么也不降低。
但是,此操作将堆栈对齐到 16 字节边界。这种行为最近有所增加(当我开始查看反汇编的二进制文件时,没有编译器对齐堆栈),这是由于越来越多地使用 SSEAVX 说明。

sub     esp, 30h

此处为局部变量分配了 space。理论上你有 32 个字节的局部变量,所以 20h 个字节。 编译器在这里做了一些非常切肉刀的事情。它注意到 strcpy 需要两个 4 字节的参数。因此,它分配了 space 而不是使用两个 push 指令 直接在此处获取该参数。 为了保持堆栈对齐,它需要达到 16 的倍数。它不能简单地保留 28h 字节,而是保留 30h 字节。为了对齐堆栈指针,浪费 8 个字节并不是什么大损失。
所以分配的space是

EBP         <-- Old Frame Pointer (Saved EBP)
...
EBP - 20h   <-- Start of 32 byte array (Up to EBP-01h included)
EBP - 24h   <-- Unused
EBP - 28h   <-- Unused
EBP - 2ch   <-- strcpy source ptr
EBP - 30h   <-- strcpy destination ptr

在这张图片中,为了有明确的偏移量和清楚起见,我特意省略了序言开头的堆栈对齐操作
下一条指令是

call    sub_401920

没有完全反汇编的符号很难分辨,但这很可能是 CRT 初始化。 GCC汇编源中所谓的__main

main 有两个参数:argcargv上面EBP的内存布局是:

...
EBP + 0ch    <-- argv
EBP + 08h    <-- argc
EBP + 04h    <-- Return address
EBP          <-- Previous Frame Pointer (Saved EBP)
EBP - 04h    <-- Locals (Array)
EBP - 08h    <-- Locals (Array)
...

接下来的指令只是加载 argv[1]

mov     eax, [ebp+0ch]        ;<-- argv ptr
add     eax, 4                ;<-- &argv[1]
mov     eax, [eax]            ;<-- argv[1]
mov     [esp+4], eax          ;<-- Like a push

记住,当最后一条指令被执行时,堆栈指针只是 局部以下

...
EBP - 20h   <-- Start of 32 byte array (Up to EBP-01h included)
EBP - 24h   <-- Unused
EBP - 28h   <-- Unused
EBP - 2ch   <-- ESP+04h (strcpy source ptr)
EBP - 30h   <-- STACK POINTER (strcpy destination ptr)

目的地也一样

lea     eax, [esp+10h]    ;Pointer to ebp-20h (EAX = ebp-20h)
mov     [esp], eax        ;Like a push
call    strcpy

最后是标准的结尾

mov     eax, 0
leave
retn

现在是时候了解堆栈对齐的全貌了。在 EBP 寄存器被保存后,对齐堆栈降低堆栈指针。引用本地变量主要是通过 ESP 完成的。

      +---------+
      |  argv   |  EBP + 0ch
      +---------+
      |  argc   |  EBP + 08h
      +---------+
      | ret adr |  EBP + 04h
      +---------+
EBP ->| Old EBP |  EBP
      +---------+
      | Unused  |  EBP - 04h    \
          ...                    > Variable length (min: 0, max = 0fh)
      | Unused  |  ESP + 30h    /
      +---------+
      |  Array  |  
          ...
      |  Array  |  ESP + 10h 
      +---------+
      | Unused  |  ESP + 0ch
      +---------+
      | Unused  |  ESP + 08h
      +---------+
      | src ptr |  ESP + 04h
      +---------+
ESP ->| dst ptr |  ESP
      +---------+

关于您最后的问题,无法确定性地回答。
如果编译器没有对齐堆栈,答案将​​是:

  1. 当你输入44个字节时,你开始写在EBP-20h所以有12个超出的字节。您首先覆盖旧帧指针,然后覆盖 return 地址,然后覆盖 argc 值。

  2. EBP 是 4 个字节,因为它是 32 位寄存器。使用 45 个字节,您将覆盖保存在堆栈中的旧帧指针 (EBP)。见上。

  3. 您开始用 37 字节的数据覆盖 return 地址(需要 40 字节才能完全覆盖)。

然而,通过对齐堆栈指针,您实际上将 ESP 降低了一个可变的(理论上)数据量,因此上面的数字必须加上一个介于 0 和 15 之间的可变数字。所以对于例如,您的案例中的对齐似乎在最后一个问题中将堆栈指针降低了 7 个字节。