【问题标题】:Memory Space Layout / strange memory (stack) behaviour C/ASM?内存空间布局/奇怪的内存(堆栈)行为 C/ASM?
【发布时间】:2015-09-22 16:39:08
【问题描述】:

在玩弄内存以更好地了解进程内存布局和幕后的一般情况时,我未能完全理解它。想象一下下面的代码:

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

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

IDA 转储(dec 不是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

我的解释:

  • 1) 将EBP 推入堆栈
  • 2) 将ESPEBP 对齐
  • 3) and esp, 4294967280 编译器模式可能可以忽略 (?)
  • 4) 从ESP 中减去 48 个字节,分配 48 个字节的大小
  • N) 我使用的编译器按 16 字节的块分配内存效率低下,即如果你有一个整数,它将分配 16 个字节,如果你超过 16,它将使用 32、48、64 等等
  • 5) 调用 (3) 中与编译器模式相关的函数可能会被忽略 (?)

由于EBP+0x0-0x3 存储了EBP 指针和EBP+0x4-0x7 返回地址,我们可以在这里看到发生了什么。

  • 1) 将指针argv 移动到EAX
  • 2) 将 4 个字节添加到 EAX(现在指向 EBP+12+4
  • 3) 将指针EBP+12+4 移动到EAX
  • N) EBP+12+4 将等于 argv[1]
  • 4) 将argv[1] 的指针移动到堆栈ESP+4
  • 5) ???
  • 6) 将buf[32] 的内容存储在 ESP+0 (?) 上

这个问题,尽管如果得到回答,非常感谢,但与其说是ASM,不如说是:

据我了解,此函数的堆栈框架应如下所示:

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

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

混乱

  • 当我使用 44 字节数据 A 作为用户输入时,它导致堆栈溢出,而堆栈上的 argv[1] 指针只有 4 个其他字节,那么单个字节来自哪里? /p>

  • EBP 是 4 个字节,因为它是一个指针,但是当我使用 45 个字节的数据时,整个 EBP 已经被 A 覆盖了。

  • EIP (afaik) 由覆盖 EBP+0x3-0x7 (RET) 控制,大小也是 4 个字节。但是,46 字节的数据导致 EIP 中途被 A 重写,47 字节 3/4 和 48 字节被 A 完全覆盖 EIP

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

【问题讨论】:

  • 这看起来是一种非常糟糕的分析编译器行为的方法。只需让编译器发出带有符号和标签的程序集(例如gcc -S)。您显示的一半代码是主前设置。
  • @KerrekSB 我不想分析编译器行为。我正在尝试分析进程内存布局和堆栈行为。
  • 认为什么决定了进程的内存布局和堆栈行为?您查看的汇编代码是由编译器生成的。理解代码意味着理解编译器生成该代码的原因。你正在做的是试图分析编译器的行为。无论如何,您所问的问题都不清楚。您不恰当地使用寄存器名称来引用堆栈上的位置并没有帮助。
  • IDA 有一个调试器,您可以使用它来单步执行汇编指令并观察它们对堆栈和寄存器的影响。这样做,很多问题都会得到解答。
  • EIP 是一个寄存器,除了通过程序控制指令(JMP、CALL、RET 等)和执行指令后程序计数器的正常前进外,不能更改。它不能“从另一个 1 字节”重写。您的意思显然不是 EIP,而是返回地址存储在堆栈上的内存位置。假设这是真的,那么仍然留下“从另一个 1 字节”是什么意思的问题?还有什么字节?我可以继续这样指出你的问题不清楚的地方,但我的评论空间已经用完了。

标签: c pointers memory assembly stack


【解决方案1】:

让我们分析一下这段代码

push    ebp
mov     ebp, esp

这是一个标准的序言。

and     esp, 0ffffffff0h 

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

sub     esp, 30h

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

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 个超出字节。您首先覆盖旧的帧指针,然后是返回地址,然后是 argc 值。

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

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

但是,通过对齐堆栈指针,您实际上将 ESP 降低了一个可变的(理论上)数据量,因此上面的数字必须加上 0 到 15 之间的可变数字。例如,看起来在您的情况下,对齐在最后一个问题中将堆栈指针降低了 7 个字节。

【讨论】:

  • 非常详尽的解释。我想我可以用这个可视化来弄清楚我遇到的问题!谢谢!
猜你喜欢
  • 2023-03-21
  • 2014-12-05
  • 2016-06-23
  • 2014-05-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-02-14
  • 2015-03-01
相关资源
最近更新 更多