进入函数之前
EAX = 0041C077 EBX = 002FA000 ECX = 0041C077 EDX = 00000001 ESI = 00411023 EDI = 0019FEE8 EIP = 004118B6 ESP = 0019FE04 EBP = 0019FEE8 EFL = 0000024647:intmain(){00411890push ebp00411891mov ebp,esp00411893sub esp,0D8h00411899push ebx0041189A push esi0041189B push edi0041189C lea edi,[ebp-18h]0041189Fmov ecx,6004118A4 mov eax,0CCCCCCCCh004118A9 rep stos dword ptr es:[edi]004118AB mov ecx,41C077h004118B0 call00411325004118B5 nop48:intstart=0;//断点在此处004118B6 mov dword ptr[ebp-8],049:func();004118BD call004112E9004118C2 nop50:intend=0;004118C3 mov dword ptr[ebp-14h],051:return0;004118CA xor eax,eax52:}5:voidfunc(){00411820push ebp00411821mov ebp,esp00411823sub esp,0CCh00411829push ebx0041182A push esi0041182B push edi0041182C lea edi,[ebp-0Ch]0041182Fmov ecx,300411834mov eax,0CCCCCCCCh00411839rep stos dword ptr es:[edi]0041183B mov ecx,41C077h00411840call0041132500411845nop6:intnum=0;00411846mov dword ptr[ebp-8],07:num++;0041184D mov eax,dword ptr[ebp-8]00411850add eax,100411853mov dword ptr[ebp-8],eax8:return;9:}将断点下在 函数汇编的第一行
00411820 push ebp执行到函数内部第一行汇编处 的 变化
EAX = 0041C077 EBX = 002FA000 ECX = 0041C077 EDX = 00000001 ESI = 00411023 EDI = 0019FEE8 EIP = 00411820 ESP = 0019FE00 EBP = 0019FEE8 EFL = 00000246其中 变了的两个寄存器
EIP = 00411820 ESP = 0019FE00原因:
在计算机架构与汇编语言中,call指令在执行时主要产生以下两个核心效果:
- 保存返回地址:
将call指令下一条指令的地址(即当前IP/EIP/RIP的值)压入栈(Stack)中。这样做的目的是为了在子程序执行完毕后,程序能够通过ret指令返回到调用处继续执行 [1, 2]。 - 转移控制权:
将指令指针寄存器(IP/EIP/RIP)的内容修改为被调用子程序的入口地址。这会导致处理器跳转到目标代码段开始执行任务 [2, 3]。
此时 ESP 的值变小了 ,说明栈被抬升了,因为栈增长的方向是小地址,ESP作为栈顶指针,每次栈填充数据,栈顶指针就要指向下一个位置,
位置的偏移量取决于填充的数据大小(和可能的数据对齐)
此时ESP 减小的大小是4字节
执行push ebp
这个条指令只是把 ebp 的值 压入栈中,并不修改ebp的值
EAX = 0041C077 EBX = 002FA000 ECX = 0041C077 EDX = 00000001 ESI = 00411023 EDI = 0019FEE8 EIP = 00411821 ESP = 0019FDFC EBP = 0019FEE8 EFL = 00000246修改的寄存器是
EIP = 00411821 ESP = 0019FDFC此时ESP 减小的大小是4字节,因为 EBP是4字节大小的
到目前为止,没有改变EBP,现在考虑下EBP的值是什么,EBP存储的是函数的栈底部,相当于一个基准点 ,- 通过EBP可以访问函数的参数和局部变量,- 每个函数都有自己的EBP值,指向自己的栈帧开始位置
在main函数中:
[ebp-8]访问局部变量start[ebp-14h]访问局部变量endEBP指向main的栈帧底部
这种相对关系存在于 main 函数中,也就是说,我只需要获取到 EBP的值,就可以获取到其他局部变量的值
现在我在 func 函数中保存了 EBP的值,所以实际上现在 在 func 中是可以通过 EBP 的值去访问 main中存储的局部变量的
执行 mov ebp,esp
EAX = 0041C077 EBX = 00352000 ECX = 0041C077 EDX = 00000001 ESI = 00411023 EDI = 0019FEE8 EIP = 00411823 ESP = 0019FDFC EBP = 0019FDFC EFL = 00000246变化的寄存器是
EIP = 00411823 EBP = 0019FDFC现在开始真正改变 EBP了,EBP的值如果被改变了,那当函数结束后,之前 局部变量再通过之前 和 EBP的关系 不就定位不到了吗,其实不用关心,上一步已经保存了 上一个函数 的EBP, 当这个函数将要结束时,会取出保存的EBP
这里修改EBP,而且将 EBP 指向和ESP指向的同一块位置 ,现在EBP ESP都位于 上一个函数的栈帧的顶部
ESP指向的是栈顶而不是 栈顶的下一个位置 把 ESP 想象成**书堆的顶部**: - 你放一本新书时:先把手指移到新位置(ESP移动),然后放书(写入数据) - 你的手指总是指向最上面的那本书(最后一个有效数据) - 不是指向书上面的空气(下一个空闲位置)执行 sub esp,0CCh
EAX = 0041C077 EBX = 0027A000 ECX = 0041C077 EDX = 00000001 ESI = 00411023 EDI = 0019FEE8 EIP = 00411829 ESP = 0019FD30 EBP = 0019FDFC EFL = 00000206改变的寄存器是
EIP = 00411829 ESP = 0019FD30 EFL = 00000206为什么mov ebp,esp 之后是 sub esp,0CCh ,现在不是ebp指向了和esp相同的位置了吗,按道理说只需要push push 就可以抬升 esp,为什么使用sub操作抬升
原因是使用sub esp, N一次性分配所有局部变量空间比用多个push指令更高效,
1.效率考量
sub esp, 0CCh:1条指令,分配 204 字节用
push:需要约 51 条push指令(204÷4)节省指令数,执行更快
2.访问方式不同
用push分配的局部变量:
assembly
push 0 ; [esp] 是变量1
push 0 ; [esp] 是变量2,[esp+4] 是变量1
push 0 ; [esp] 是变量3,[esp+4] 是变量2,[esp+8] 是变量1
问题:ESP 一直在变,访问局部变量时偏移计算复杂!
用sub esp, N分配:
assembly
sub esp, 12 ; 分配12字节
mov [ebp-4], 0 ; 局部变量1
mov [ebp-8], 0 ; 局部变量2
mov [ebp-12], 0 ; 局部变量3
优势:EBP 固定,通过固定偏移访问局部变量
执行保存易变寄存器
00411829 push ebx 0041182A push esi 0041182B push ediEAX = 0041C077 EBX = 0027A000 ECX = 0041C077 EDX = 00000001 ESI = 00411023 EDI = 0019FEE8 EIP = 0041182C ESP = 0019FD24 EBP = 0019FDFC EFL = 00000206变化的寄存器是
EIP = 0041182A ESP = 0019FD2C EIP = 0041182B ESP = 0019FD28 EIP = 0041182C ESP = 0019FD24执行 lea edi,[ebp-0Ch]
EAX = 0041C077 EBX = 0027A000 ECX = 0041C077 EDX = 00000001 ESI = 00411023 EDI = 0019FDF0 EIP = 0041182F ESP = 0019FD24 EBP = 0019FDFC EFL = 00000206变化的寄存器是
EDI = 0019FDF0 EIP = 0041182F执行 mov ecx,3
EAX = 0041C077 EBX = 0027A000 ECX = 00000003 EDX = 00000001 ESI = 00411023 EDI = 0019FDF0 EIP = 00411834 ESP = 0019FD24 EBP = 0019FDFC EFL = 00000206变化的寄存器是
ECX = 00000003 EIP = 00411834执行 mov eax,0CCCCCCCCh
EAX = CCCCCCCC EBX = 0027A000 ECX = 00000003 EDX = 00000001 ESI = 00411023 EDI = 0019FDF0 EIP = 00411839 ESP = 0019FD24 EBP = 0019FDFC EFL = 00000206变化的寄存器是
EAX = CCCCCCCC EIP = 00411839执行 rep stos dword ptr es:[edi]
EAX=CCCCCCCC EBX=0027A000 ECX=00000000EDX=00000001ESI=00411023EDI=0019FDFC EIP=0041183B ESP=0019FD24 EBP=0019FDFC EFL=00000206变化的寄存器是
ECX = 00000000 EDI = 0019FDFC EIP = 0041183BSTOS(Store String)是x86汇编中的字符串存储指令,用于将累加器(AL/AX/EAX)的值存储到内存中,
综合上述的 mov eax,0CCCCCCCCh 和 mov ecx,3 和 lea edi,[ebp-0Ch]
可知是以 ebp为基点,向上抬升3个字节大小(0ch是12),同时指定循环次数是3次,同时向 edi 指向的 处开始填充数据 0CCCCCCCCh,每次填充数据,会自动移动填充数据的指向指针
执行安全检查函数
0041183B mov ecx,41C077h ; 将安全 Cookie 值加载到 ECX 00411840 call 00411325 ; 调用 __security_check_cookie 函数 00411845 nop ; 对齐填充(无操作)这是 Windows 程序的栈溢出保护机制:
工作原理:
函数开始时:在栈帧中插入一个随机值(Cookie)
函数结束时:检查这个值是否被改变
如果被改变:说明发生了栈溢出,程序立即终止
执行 mov dword ptr [ebp-8],0
使用ebp作为基点,在一个字节的偏移处设置数据为 0
执行自增操作
0041184D mov eax,dword ptr [ebp-8] 00411850 add eax,1 00411853 mov dword ptr [ebp-8],eax将局部变量存储到 临时寄存器,对寄存器自增,将自增的值放回局部变量
执行恢复易变寄存器
00411856 pop edi 00411857 pop esi 00411858 pop ebx还原 Esp到 ebp的初始位置
add esp,0CCh注意从 执行 mov ebp,esp (不包括这个指令) 到现在 ,ebp作为基础点都没有修改过
验证ESP 是否真的还原成功
cmp ebp,esp现在如果正确执行的话, EBP 和 ESP 都是指向同一个位置的
验证ESP的值是否有效
call 00411244打开显示符号名
00411861 call __RTC_CheckEsp (0411244h)执行一个看起来没什么用的指令
mov esp,ebp因为 esp 和ebp 在之前都指向同一个位置了
执行 pop ebp
pop指令负责从栈中取出一个元素,并使得 esp +4,栈的高度下降,同时 这里是放到ebp中,
此时的寄存器的值是
EAX = 00000001 EBX = 0027A000 ECX = 0041C077 EDX = 00000001 ESI = 00411023 EDI = 0019FEE8 EIP = 00411869 ESP = 0019FE00 EBP = 0019FEE8 EFL = 00000246刚好和 执行完执行 push ebp 保存 ebp 时, EBP 寄存器的数据相同
此时堆栈平衡,现在的EBP的值就是上一个函数的EBP的值了