MASM32汇编中关于栈的总结

关于函数调用中栈的总结

1.栈,是由高地址分配到低地址的。

32位程序中,寄存器ebp指向栈的起始位置,也就是栈的基地址(高地址)

寄存器esp指向栈的目前位置,也就是栈顶。

现在假设程序才运行,ebp和esp寄存器都是0x00190000h

接下来的指令是push 0x1800004H,那么ebp还是0x00190000h

因为push一个整数占用4字节,所以esp-4=0x0018fffcH

所以esp=0x0018FFFCH,

再来看看内存分配,这里以小端为例。

0x0018FFFCH 0X0018FFFDH 0X0018FFFEH 0X18FFFFH
04 00 00 18

所以,esp减少,代表压入数据,esp增大,代表弹出数据

2.栈的应用

1.临时保存值。

这里以寄存器为例。

假定一开始eax的值为5

指令 eax寄存器的值
push eax 5
inc eax 6
dec eax 5

2.保存函数调用的返回地址。

假设程序是顺序执行。

那么,在执行call指令的时候,会先往栈中push call指令往下一条语句的地址

3.传递函数参数。

调用一个有参数的函数,都是先push一些参数,然后再执行call指令。

4.存放函数中的局部变量。

OD跟踪一个函数执行时,往往能看见过程一开始,就是

sub esp,4这样的语句,这代表分配了4字节的空间给某个局部变量。

使用那个局部变量的时候,往往用的ebp-4来代表那个地址。

3.CALL指令的调用过程

1.将调用函数使用的参数入栈

2.将call指令往后需要执行的指令入栈,函数执行完毕后用到

3.保存原始ebp指针的值

4.为函数准备栈空间,ebp指向函数的栈的基地址

5.为函数中的局部变量开辟栈空间

6.执行函数语句

7.清理局部变量,恢复ebp指针,(leave指令)

8.返回执行call指令往后执行的指令,清理函数调用push的栈(retn)

	.386
	.model flat,stdcall
	option casemap:none

include windows.inc
include user32.inc
includelib user32.lib
include kernel32.inc
includelib kernel32.lib

	.code
_add proc a:word,b:word
	local @bl1:dword
	ret
_add endp
start:
	push 1
	push 2
	call _add
	add eax,1
end start

这样的代码,我们编译链接,在OD中运行,查看字节码。

00401000                        | 55                         | push ebp                                        |
00401001                        | 8BEC                       | mov ebp,esp                                     |
00401003                        | 83C4 FC                    | add esp,FFFFFFFC                                |
00401006                        | C745 FC 05000000           | mov dword ptr ss:[ebp-4],5                      |
0040100D                        | C9                         | leave                                           |
0040100E                        | C2 0800                    | ret 8                                           |
00401011 <my.EntryPoint>        | 6A 01                      | push 1                                          |
00401013                        | 6A 02                      | push 2                                          |
00401015                        | E8 E6FFFFFF                | call my.401000                                  |
0040101A                        | 83C0 01                    | add eax,1                                       |                                   |

接下来我们逐条解释 。

编写这8条即可保证堆栈平衡。比如调用函数前,栈里面有7个4字节的指针,运行到最后(add eax,1)的时候,栈里面就应该还是7个4字节的指针,并且数据不变。

(1)因为这个过程有两个参数,所以这里push两个参数,如果改成只push一个,那么执行完函数的时候,start函数里面,原先的栈就被破坏了,栈失衡可能会造成运行错误。

(2)大家使用OD跟着调试的时候,可以注意观察堆栈的数据,进入调用函数的时候,栈顶保存的就是call指令后面的那条指令的地址

(3)(4)(5)经常,调用过程的时候,

总是看见过程开头是这几句,就拿这里为例

00401000                        | 55                         | push ebp                                        |
00401001                        | 8BEC                       | mov ebp,esp                                     |
00401003                        | 83C4 FC                    | add esp,FFFFFFFC                                |
00401006                        | C745 FC 05000000           | mov dword ptr ss:[ebp-4],5                      |

每个函数都有自己的栈空间,ebp指向的就是函数栈空间的起点,所以这里会先push ebp,

保存start里面的栈的基址。

mov ebp,esp。调整函数的栈的基址。为什么要这样,我们接着往下看。

add esp,FFFFFFC,也就是栈顶往下移动了4字节,也就是分配给@bl1的空间。

而机器码里面可是没有这个局部变量名称的,那么如何做到使用这个空间呢?

也就是这一句 mov dword ptr ss:[ebp-4],5 。对局部变量的操作,主要通过栈基址的偏移来操作

所以,调用函数,才需要保存原始栈基址,设置新的栈基址。

(6)这里没有涉及,很多时候,通过OD,我们能看到

pushad
xxxxxx
popad

这样的语句,也是利用栈来保存寄存器的值

(7)函数一开头就保存了start的栈基址,那么,为了维持堆栈平衡,函数结束的时候,就应该恢复栈基址。

这是通过leave指令实现的。

等价于

mov esp,ebp
pop ebp

回到start函数,栈的基地址恢复了,栈顶地址也恢复了,也就是回到了调用call指令以后,执行函数过程前的状态。

(8)最后一个ret 8,因为这个函数调用,占用8字节,所以会再弹出下一条指令地址后,再弹出8字节的数据。

至此,函数调用中的栈就完毕了。

4.栈溢出

且看代码:

	.386
	.model flat,stdcall
	option casemap:none

include windows.inc
include user32.inc
includelib user32.lib
include kernel32.inc
includelib kernel32.lib
	
	.data
szText db 'HelloWorldPE',0
szText2 db 'Touch Me!',0
szShellCode dd 0fffffffh,0dddddddh,0040103ah,0

	.code
_memCopy proc _lpSrc
	local @buf[4]:byte
	pushad
	mov al,1
	mov esi,_lpSrc
	lea edi,@buf
	.while al!=0
		mov al,byte ptr [esi]
		mov byte ptr [edi],al

		inc esi
		inc edi
	.endw
	popad
	ret
_memCopy endp

start:
	invoke _memCopy,addr szShellCode
	invoke MessageBox,NULL,offset szText,NULL,MB_OK
	invoke MessageBox,NULL,offset szText2,NULL,MB_OK
	invoke ExitProcess,NULL
end start

看代码可能会以为弹出两个信息框,实际上只会弹出一个。这里的shellCode是经过专门设计的。

在_memCopy过程中,edi指向了栈中分配的局部变量的地址,随后又不断越界修改,改变了栈空间内存里面的数据,最重要的,就是改变了call指令返回以后需要执行的指令的地址,这个可以利用OD调试跟踪运行。

posted @ 2022-03-11 16:53  念秋  阅读(240)  评论(0编辑  收藏  举报