逆向工程核心原理——学习笔记_栈帧

栈帧就是利用EBP(栈帧指针,注意不是ESP)寄存器访问栈内局部变量、参数、函数返回地址等的手段。

调用某函数时,先要把用作基准点(函数起始地址)的ESP值保存到EBP中,并维持在函数内部。

这样无论ESP的值如何变化,以EBP的值作为基准(base)能够安全访问到相关函数的局部变量、参数、返回地址,这就是EBP寄存器作为栈帧指针的作用。

 

栈帧对应的汇编代码:

PUSH EBP                        ;函数开始(使用EBP前先把已有值保存到栈中)
MOV EBP,ESP                     ;保存到当前ESP到EBP中

...                             ;函数体
                                ;无论ESP值如何变化,EBP都保持不变,可以安全访问函数的局部变量、参数            


MOV ESP, EBP                    ;将函数的起始地址返回到ESP中
POP EBP                         ;函数返回前弹出保存在栈中的EBP值
RETN                            ;函数终止

 

#include<stdio.h>

long add(long a ,long b)
{
  long x = a , y = b;
  return (x+y);
}

int main(int argc, char* arg[])
{
  long a = 1 , b = 2 ;
  printf("%d\n",add(a,b));
  return 0;
}

  

 

 

0x1:开始执行main()函数&生成栈帧

int main(int argc , char* argv[])

{

函数main()是程序开始执行的地方。开始执行main()函数时栈的状态如图所示:

切记地址401279保存在ESP(0012FF84)中,它是main函数执行完毕后要返回的地址。

main()函数一开始运行就生成与其对应的函数栈帧。

PUSH是一条压栈指令。“把EBP值压入栈中”。

main()函数中,EBP为栈帧指针,用来把EBP之前的值备份到栈中(main()函数执行完毕,返回之前,该值会再次恢复)。

 

MOV是一条传送数据指令,上面这条MOV语句的命令是“把ESP值传送到EBP”。

从这条命令开始,EBP就持有与当前ESP相同的值,并且直到main()函数执行完毕,EBP的值始终保持不变。

执行完这两条命令后,main()函数的栈帧就生成了(设置好EBP了)。

 

进入OllyDbg的栈窗口,单击鼠标右键,选择Adress-Relative to EBP

当前EBP值为12FF80,与ESP值一致,12FF80地址处保存着12FFC0,它是main()函数开始执行时EBP持有的初始值。

 

0x2:

汇编代码详解:

00401063 |. 83EC 48          sub esp,0x48                              //为函数的局部变量申请一段空间

00401066 |. 53            push ebx
00401067 |. 56             push esi                                  //寄存器压栈,保存现场
00401068 |. 57            push edi

00401069 |. 8D7D B8         lea edi,dword ptr ss:[ebp - 0x48]          //将局部变量的堆栈中开始地址保存到edi寄存器
0040106C |. B9 12000000      mov ecx,0x12                               //将重复执行指令的次数放到ecx
00401071 |. B8 CCCCCCCC      mov eax,0xCCCCCCCC                        //初始化eax
00401076 |. F3:AB            rep stos dword ptr es:[edi]                //用eax中的值初始化到es:[edi]指向的地址,长度为dword,循环执行次数为ecx中的值

  

 详解:http://blog.csdn.net/ypist/article/details/8467163 

rep指令的目的是重复其上面的指令.ECX的值是重复的次数.
STOS指令的作用是将eax中的值拷贝到ES:EDI指向的地址.          

REP的作用是根据ecx的值,重复执行后面的串传送指令。
REP能够引发其后的字符串指令被重复, 只要ecx的值不为0, 重复就会继续. 
每一次字符串指令执行后, ecx的值都会减小.

 

如果设置了direction flag, 那么edi会在该指令执行后减小, 
如果没有设置direction flag, 那么edi的值会增加.

 

stos((store into String),意思是把eax的内容拷贝到目的地址。
用法:stos dst,dst是一个目的地址,例如:stos dword ptr es:[edi]。dword ptr前缀告诉stos,一次拷贝双字(4个字节)的数据到目的地址。为什么一次非要拷贝双字呢?这和eax寄存器有关。

执行stos之前必须往eax(32为寄存器)放入要拷贝的数据。上图中,eax的内容是cccccccc,意思是int3中断。
这段代码是初始化堆栈和分配局部变量用的,往分配好的局部变量空间放入int3中断的原因是:防止该空间被意外执行。这样发生意外时执行堆栈里面的内容会引发调试中断。  

 0x12*4 (字节)刚好是为局部变量申请的那段空间的大小。

 

执行完后栈中的情况:

 

 

 0x3:设置局部变量                                               

 long a = 1 , b = 2;

上面两条MOV指令的含义“把数据1与2分别保存到[ebp - 0x4]与[ebp - 0x8]中”,即[[ebp - 0x4]代表局部变量a,[ebp - 0x8]代表局部变量b。

执行完上面两条语句后,函数栈内的情况如下:

 

【提示】:

DWORD PTR SS:[EBP-0x4]语句中,SS是Stack Segment 的缩写,表示栈段。由于Windows中使用的是段内存模型(Segment Memory Model),使用时需要指出相关内存属于哪一个段区。其实,32位的Windows OS中,SS、DS、ES 的值均为0,所以采用这种方式附上区段并没有什么意义。因EBP与ESP是指向栈的寄存器,所以添加了SS寄存器。 

 

 

0x4:add()函数参数传递与调用

printf(“%d\n”,add(a,b));  

 

0040108E 处的CALL 401005命令,该命令用于调用401005处的函数,而401005处的函数即为add()函数。函数add()接收a、b这两个长整型参数,所以调用add()之前需要把2个参数压入栈。

参数入栈的顺序与C语言源码中的参数顺序恰好相反。换言之,变量b([EBP - 0x8])首先入栈,接着变量a([EBP - 0x4])再入栈。

执行完地址00401086-0040108D之间的行代码后,栈内情况:

接下来进入add()函数(00401005)内部,分析整个函数调用过程。

返回地址

执行CALL命令进入被调用的函数之前,CPU会先把函数的返回地址压入栈,用作函数执行完毕后的返回地址。

由图中可知,在地址0040108E处掉调用了add()函数,他的下一条命令的地址为00401093。函数add()执行完毕后,程序执行流应该返回到00401093地址处,该地址即被被称为add()函数的返回地址。执行完0040108E地址处的CALL命令后进入该函数,栈内情况如图:

 

间接调用

00401093地址处的CALL 00401005命令用于调用add()函数,不是直接转到add()函数,而是通过通过中间地址00401005地址处的JMP命令跳转。

 

 

 

 0x5:开始执行add()函数&生成栈帧

 long add(long a, long b)

{

 

函数开始执行时,栈中会单独生成与其对应的栈帧。

上面2行代码与开始执行main()函数时的代码完全相同,先把EBP值(main()函数的基址指针)保存到栈中,再把当前ESP存储到EBP中,这样函数add()的栈帧就生成了。

可以看到main()函数使用的EBP值(12FF80)被备份到栈中,然后EBP的值被设置为一个新值0012FF1C。

 

 0x6:设置add()函数内部的局部变量(x,y)

long x = a, y = b;

上面一行语句声明了2个长整型的局部变量(x,y),并使用2个形式参数(a,b)分别为他们赋初始值。密切关注形参与局部变量在函数内部以何种方式表示。

00401023  |.  83EC 48       sub esp,0x48                  //为局部变量申请一段空间
00401026  |.  53            push ebx
00401027  |.  56            push esi                      //寄存器压栈,保存现场
00401028  |.  57            push edi
00401029  |.  8D7D B8       lea edi,dword ptr ss:[ebp - 0x48]
0040102C  |.  B9 12000000   mov ecx,0x12
00401031  |.  B8 CCCCCCCC   mov eax,0xCCCCCCCC
00401036  |.  F3:AB         rep stos dword ptr es:[edi]   //将 为局部变量开辟的这段空间设置为CC (int 3 中断)

 密切关注形式参数与局部变量在函数内部以何种方式表示

 add函数的栈帧生成之后,EBP的值发生了变化,[EBP+8]与[EBP+C]分别指向参数a 和 参数b,而[EBP - 4] 与 [EBP - 8] 则分别指向add()函数的2个局部变量x、y。

执行完上述语句后栈内情况如图所示

 

 

0x7:ADD运算

return (x + y);

 

上述MOV语句中,局部变量x的值被传送到eax中。

上面这条语句中,变量y([EBP - 8 ] = 2)与 原EAX值(x)相加,且运算结果被存储在EAX中,运算完成后EAX中的值为3。

EAX是一种通用寄存器,在算数运算中存储输入输出数据,为函数提供返回值。函数返回时,若像EAX中输入某个值,该值就会原封不动的返回。执行运算的过程中栈内情况保持不变。

 

0x8: 删除函数add()的栈帧&函数执行完毕(返回)

return (x + y)
}

 

      //恢复现场

这三条指令与00401026 到 00401028之间的4条指令相对应

 执行完加运算后,要返回函数add(),在此之前先删除函数add()的栈帧。

             

上面这条命令把当前EBP的值赋给ESP,与地址00401021处的MOV EBP,ESP命令相对应。在地址00401021处MOV EBP,ESP命令把函数add()开始执行时的ESP值(12FF1C)放入EBP,函数执行完毕时,使用0040104D处的MOV ESP,EBP命令再把存储到EBP中的值恢复到ESP中。

 

【提示】:执行完上面的命令后,地址00401023处的SUB ESP,0X48 命令就会失效,即函数add()的两个局部变量x,y不再有效。

        

上面这条命令用于恢复函数add()开始执行时备份到栈中的EBP值,他与00401020处的PUSH EBP命令对应。EBP的值恢复12FF80,它是main()函数的EBP值。到此,add()函数的栈就被删除了。

执行完上述命令后,栈内情形如图所示:

 

可以看到,ESP的值为12FF20,该地址的值为00401093,它是执行CALL  401005的命令时CPU存储到栈中的返回地址。

执行上述RETN命令,存储在栈中的返回地址即被返回,此时栈内的情形如图所示:

从图中可以看到,调用栈已经完全返回到调用add()函数之前的状态。可以对比0X4中的第一个栈内图。

 

0x9:从栈中删除函数add()的参数(整理栈)

现在程序执行流已经重新返回main()函数中。

上面语句使用ADD命令将ESP加上8。看上上图,此图中的栈窗口,地址12FF24与12FF28存储的是传递给函数add()的参数a与b。函数add()执行完毕后,就不再需要参数a与b了,所以要把ESP加上8,将他们从栈中清理掉(参数a与b都是长整型,合占4个字节,合起来共8个字节)。

【提示】:调用add()函数之前先使用PUSH命令把参数a、b压入栈。

执行完上述命令后,栈内情况如图所示

 

 0x10:调用printf()函数

printf("%d\n",add(a , b));

地址0401096处的EAX寄存器中存储着函数add()的返回值,它是执行假发运算后的结果值3,。地址0040109C处的CALL 004010D0命令中调用的是004010D0地址处的函数,它是一个c标准库函数printf(),所有C标准库函数都有Visual C++编写而成。由于上面的printf()函数有2个参数,大小为8个字节(32位寄存器+32位常量=64位=8字节),所以在004010A1地址处使用ADD命令,将ESP加上8个字节,把函数的参数从栈中删除。函数printf()执行完毕后通过ADD命令删除参数后,栈内的情形如图所示:

 

 0x11:设置返回值

return 0;

main()函数使用该语句设置返回值(0)。

两个相同的值进行XOR运算结果为0。XOR命令比MOV EAX,0命令执行速度快,常用与寄存器的初始化操作。

 

0x12:删除栈帧&main()函数终止

return 0
}

最终主函数终止执行,同add()函数一样,其返回前要先从栈中删除与其对应的栈帧。

004010A6 |. 5F            pop edi
004010A7 |. 5E             pop esi                              //寄存器出栈,恢复现场
004010A8 |. 5B            pop ebx

004010A9 |. 83C4 48              add esp,0x48                         //释放局部变量,平衡堆栈

004010AC |. 38EC                 cmp ebp.esp                          //检查堆栈是否平衡

004010AE |. EB 9D000000          call StackTra.00401150

  

执行完上面2两条命令后,main()函数的栈帧即被删除,且其局部变量a、b也不再有效。执行至此,栈内情形如图所示:

 

执行完毕上面的命令后,主函数执行完毕并返回,程序执行流跳转到返回地址处(00401279),该函数指向Visual C++的启动函数区域。随后执行进程终止代码。

 

posted @ 2017-12-21 21:52  ha2  阅读(968)  评论(0编辑  收藏  举报