内存 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;
}
转储自 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) 将
ESP
与 EBP
对齐
- 3)
and esp, 4294967280
编译器模式可能会被忽略 (?)
- 4) 从
ESP
中减去 48 个字节,分配 48 个字节的大小
- N) 我使用的编译器低效地按 16 字节的块分配内存,即如果你有一个整数,它将分配 16 个字节,如果超过 16 个,它将使用 32、48、64 等等
- 5) (3)中对编译器模式相关函数的调用可能会被忽略(?)
因为 EBP+0x0-0x3
存储了 EBP
指针和 EBP+0x4-0x7
return 地址,我们可以看到这里发生了什么。
- 1) 将指针
argv
移动到EAX
- 2) 向
EAX
添加 4 个字节(现在指向 EBP+12+4
)
- 3) 将指针
EBP+12+4
移动到EAX
- N)
EBP+12+4
等于 argv[1]
- 4) 将
argv[1]
的指针移动到堆栈上 ESP+4
- 5) ???
- 6) 在 ESP+0 (?)
上存储 buf[32]
的内容
这个问题,虽然,如果回答的话,非常感谢,与其说是 ASM
,不如说:
根据我的理解,这个函数的栈帧应该是这样的:
[ ] < ESP+0x0-0x3
[ ]
[ ]
[ ]
[ ]
[ ]
[ ]
[ ]
[ ]
[ ]
[ ]
[ ] < ESP+0x2C-0x2F
[EBP] < EBP+0x0-0x3
[RET] < EBP+0x4-0x7
[ARG]
其中 ARG
(EBP+0x8+) 包含函数的参数。
迷茫
当我使用 44 个字节的数据 A
作为用户输入时,它导致堆栈溢出,而堆栈上的 argv[1]
指针只有 4 个其他字节,所以在哪里单字节是从哪里来的?
EBP
是 4 个字节,因为它是一个指针,但是当我使用 45 个字节的数据时,整个 EBP
已经被 A 覆盖。
EIP
(afaik) 由覆盖 EBP+0x3-0x7
(RET) 控制,大小也是 4 个字节。但是,46字节的数据导致EIP
被A的中途改写,47字节的3/4,48字节的EIP
被A的
完全覆盖
最后,发送太大的缓冲区时 EIP
不再被重写有什么原因吗?那是因为它开始覆盖之前的堆栈帧导致更早的速成课程吗?
让我们分析一下代码
push ebp
mov ebp, esp
这是标准序言。
and esp, 0ffffffff0h
ESP
的低半字节(4位)被清除。在最坏的情况下,这会将堆栈指针降低 15 个字节,而在最好的情况下什么也不降低。
但是,此操作将堆栈对齐到 16 字节边界。这种行为最近有所增加(当我开始查看反汇编的二进制文件时,没有编译器对齐堆栈),这是由于越来越多地使用 SSE 和 AVX 说明。
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
有两个参数:argc
和 argv
。 上面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
+---------+
关于您最后的问题,无法确定性地回答。
如果编译器没有对齐堆栈,答案将是:
当你输入44个字节时,你开始写在EBP-20h
所以有12个超出的字节。您首先覆盖旧帧指针,然后覆盖 return 地址,然后覆盖 argc
值。
EBP
是 4 个字节,因为它是 32 位寄存器。使用 45 个字节,您将覆盖保存在堆栈中的旧帧指针 (EBP
)。见上。
您开始用 37 字节的数据覆盖 return 地址(需要 40 字节才能完全覆盖)。
然而,通过对齐堆栈指针,您实际上将 ESP
降低了一个可变的(理论上)数据量,因此上面的数字必须加上一个介于 0 和 15 之间的可变数字。所以对于例如,您的案例中的对齐似乎在最后一个问题中将堆栈指针降低了 7 个字节。
当为了更好地了解进程内存布局和幕后花絮而摆弄内存时,我未能完全理解它。想象一下下面的代码:
#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) 将
ESP
与EBP
对齐
- 3)
and esp, 4294967280
编译器模式可能会被忽略 (?) - 4) 从
ESP
中减去 48 个字节,分配 48 个字节的大小 - N) 我使用的编译器低效地按 16 字节的块分配内存,即如果你有一个整数,它将分配 16 个字节,如果超过 16 个,它将使用 32、48、64 等等
- 5) (3)中对编译器模式相关函数的调用可能会被忽略(?)
因为 EBP+0x0-0x3
存储了 EBP
指针和 EBP+0x4-0x7
return 地址,我们可以看到这里发生了什么。
- 1) 将指针
argv
移动到EAX
- 2) 向
EAX
添加 4 个字节(现在指向EBP+12+4
) - 3) 将指针
EBP+12+4
移动到EAX
- N)
EBP+12+4
等于argv[1]
- 4) 将
argv[1]
的指针移动到堆栈上ESP+4
- 5) ???
- 6) 在 ESP+0 (?) 上存储
buf[32]
的内容
这个问题,虽然,如果回答的话,非常感谢,与其说是 ASM
,不如说:
根据我的理解,这个函数的栈帧应该是这样的:
[ ] < ESP+0x0-0x3
[ ]
[ ]
[ ]
[ ]
[ ]
[ ]
[ ]
[ ]
[ ]
[ ]
[ ] < ESP+0x2C-0x2F
[EBP] < EBP+0x0-0x3
[RET] < EBP+0x4-0x7
[ARG]
其中 ARG
(EBP+0x8+) 包含函数的参数。
迷茫
当我使用 44 个字节的数据
A
作为用户输入时,它导致堆栈溢出,而堆栈上的argv[1]
指针只有 4 个其他字节,所以在哪里单字节是从哪里来的?EBP
是 4 个字节,因为它是一个指针,但是当我使用 45 个字节的数据时,整个EBP
已经被 A 覆盖。EIP
(afaik) 由覆盖EBP+0x3-0x7
(RET) 控制,大小也是 4 个字节。但是,46字节的数据导致EIP
被A的中途改写,47字节的3/4,48字节的EIP
被A的 完全覆盖
最后,发送太大的缓冲区时 EIP
不再被重写有什么原因吗?那是因为它开始覆盖之前的堆栈帧导致更早的速成课程吗?
让我们分析一下代码
push ebp
mov ebp, esp
这是标准序言。
and esp, 0ffffffff0h
ESP
的低半字节(4位)被清除。在最坏的情况下,这会将堆栈指针降低 15 个字节,而在最好的情况下什么也不降低。
但是,此操作将堆栈对齐到 16 字节边界。这种行为最近有所增加(当我开始查看反汇编的二进制文件时,没有编译器对齐堆栈),这是由于越来越多地使用 SSE 和 AVX 说明。
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
有两个参数:argc
和 argv
。 上面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
+---------+
关于您最后的问题,无法确定性地回答。
如果编译器没有对齐堆栈,答案将是:
当你输入44个字节时,你开始写在
EBP-20h
所以有12个超出的字节。您首先覆盖旧帧指针,然后覆盖 return 地址,然后覆盖argc
值。EBP
是 4 个字节,因为它是 32 位寄存器。使用 45 个字节,您将覆盖保存在堆栈中的旧帧指针 (EBP
)。见上。您开始用 37 字节的数据覆盖 return 地址(需要 40 字节才能完全覆盖)。
然而,通过对齐堆栈指针,您实际上将 ESP
降低了一个可变的(理论上)数据量,因此上面的数字必须加上一个介于 0 和 15 之间的可变数字。所以对于例如,您的案例中的对齐似乎在最后一个问题中将堆栈指针降低了 7 个字节。