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


    栈帧就是利用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
    ",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
    ”,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
    ",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++的启动函数区域。随后执行进程终止代码。

  • 相关阅读:
    漫步ASP.NET MVC的处理管线
    HTTP压力测试工具
    javaweb学习总结(四十)——编写自己的JDBC框架
    javaweb学习总结(三十九)——数据库连接池
    javaweb学习总结(三十八)——事务
    javaweb学习总结(三十七)——获得MySQL数据库自动生成的主键
    javaweb学习总结(三十六)——使用JDBC进行批处理
    JavaWeb学习总结(三十五)——使用JDBC处理Oracle大数据
    javaweb学习总结(三十四)——使用JDBC处理MySQL大数据
    javaweb学习总结(三十三)——使用JDBC对数据库进行CRUD
  • 原文地址:https://www.cnblogs.com/ha2ha2/p/7811691.html
Copyright © 2020-2023  润新知