逆向C++学习手记(1)

来源:互联网 发布:虚拟摇杆软件 编辑:程序博客网 时间:2024/05/17 22:02

转自 http://driftcloudy.iteye.com/blog/1065619

 

最近常遇到的尴尬是看着汇编代码却无法在脑中反应出正确的C、C++代码 。近日偶得一篇好文《reverse C++》,细读下来收获不少。遂打算在《reverse C++》的基础上扩展,从汇编角度来认识一些C、C++中的语法现象~先从一些简单的开始...

一、数组

数组是非常基本的数据结构,C++中的数组表现为一段连续可用的内存。注意这段内存地址是连续的,这与很多动态语言中的数组不一样,举例来说PHP中的数组实际上是一种hash表式的实现,因而肯定不会是一段连续的内存。

 

对于充当全局变量的数组,会自动为数组中的每个变量附上默认值;而对于声明在函数体内部的局部变量,则需要手动进行初始化。来看一段简单的代码:

Cpp代码 复制代码 收藏代码
  1. #include <iostream.h>   
  2. void main()   
  3. {   
  4.     int arr[10];   
  5.     arr[3] = 22;   
  6.     cout<<arr;   
  7. }  

这段C++代码在main函数中声明了一个长度为10的arr数组,不过并没有初始化,而是仅仅给数组中的一个单元赋予了初始值。在VC6 release下进行编译,优化选项为最快速度,得到的汇编代码如下:

Asm代码 复制代码 收藏代码
  1. 00401000   sub     esp, 0x28               //在栈上开辟40个字节的空间,每个int占4字节   
  2. 00401003   lea     eax, dword ptr [esp]    //arr 指向栈顶,因此栈顶相当于arr[0]   
  3. 00401007   mov     ecx, 0040B9B8           //cout指针存入ecx,为方法调用做准备   
  4. 0040100C   push    eax   
  5. 0040100D   mov     dword ptr [esp+10], 16  //相当于arr[3]=22  
  6. 00401015   call    00401020                //eax中存放的arr,执行cout<<ar   
  7. 0040101A   add     esp, 28                 //回收数组占用的40个字节的空间   
  8. 0040101D   retn  

这里的汇编看着有点乱,比较理想的编译出来的代码应该是:

sub     esp, 28                                 // 执行 int arr[10]

mov     dword ptr [esp+C], 16       // 注意这里是esp+C ,esp 是 arr[0],所以 arr[3] = esp + C
lea     eax, dword ptr [esp]
push    eax

mov     ecx, 0040B9B8
call    00401020
add     esp, 28
retn

可能有人注意到了这里栈并不平衡,中间的push并没有对应的pop操作,这是因为调用cout对象的<<操作符相当于调用一个类得成员函数,需满足thiscall约定,当参数个数确定的时候,由被调用的call本身来清理堆栈(push的参数个数不确定时,才由调用方来清理)。

 

题外话,cout对象也是一个全局对象,必须要确保在main函数调用之前完成初始化,这样在main里才能直接使用。从Entry Point到main函数调用(5):_cinit 中介绍了cinit 函数,其实cout 对象的生成也是在cinit中完成的。准确的说,是在

Cpp代码 复制代码 收藏代码
  1. /*   
  2.  * do C++ initializations   
  3.  */    
  4. _initterm( __xc_a, __xc_z )   

 

中完成的。在CRT源码中的IOSTRINI.CPP 文件中有如下语句:

Cpp代码 复制代码 收藏代码
  1. ostream_withassign cout(_new_crt filebuf(1));  

可见cout 是 ostream_withassign 类型的一个实例。

 

1)数组初始化

还是从一段简单的示例代码说起:

Cpp代码 复制代码 收藏代码
  1. void main()   
  2. {   
  3.     int arr[10] = {22,99};   
  4. }  

arr依然是局部变量, 这里只会将arr[0] 和 arr[1] 初始化为22、99,数组中其余的8个变量都会初始化为0。来具体看看main函数的汇编代码:

Asm代码 复制代码 收藏代码
  1. push    ebp   
  2. mov     ebp, esp   
  3. sub     esp, 28                   // 开辟40个字节空间当数组   
  4. push    edi                       // 暂存edi的值, 因为下面要用到edi寄存器   
  5. mov     dword ptr [ebp-28], 16    // arr[0] = 22  
  6. mov     dword ptr [ebp-24], 63    // arr[1] = 99  
  7. mov     ecx, 8  
  8. xor     eax, eax   
  9. lea     edi, dword ptr [ebp-20]  // 将arr[2] 的地址放入edi   
  10. rep     stos dword ptr es:[edi]  // 该指令将arr[2] - arr[9] 全部清0  
  11. pop     edi                      // 回复edi寄存器   
  12. mov     esp, ebp   
  13. pop     ebp   
  14. retn  

rep stos dword ptr es:[edi] 会根据ECX的值重复执行,它所作的动作就是从EAX中取4个字节,然后传到EDI所指的内存地址,这个动作会不停重复直到ECX减到0为止。由于是将arr[2] - arr[9]全部置0,所以先将EAX清0。

 

2)全局数组

这里仅仅是验证一下全局变量并非存放在栈上。

Cpp代码 复制代码 收藏代码
  1. int arr[10] = {1,2,3};   
  2. void main()   
  3. {   
  4.     arr[5]=10;   
  5. }  

在PE文件的.data 区发现了:

 

数组arr 被放到了数据区 00406030 - 00406057 的区域。很显然,这里无法利用ESP + XXX 或者 EBP - XXX 来直接对数组中的内容进行访问,因为arr 并不是存在于栈区。因此在main函数中,对该数组的访问直接写成了硬编码的地址。一旦PE 被映射进内存,就可以直接操作写死的内存地址。这里的main 函数编译成如下:

mov     dword ptr [406044], 1
retn

 

一般对数组的访问都是由基地址(数组首地址)+偏移量组成。

首地址形如: ESP+XXX 或者 EBP-XXX

偏移量形如: ElementSize*INDEX(ElementSize是数组中元素的大小,index放在eax或者ecx等寄存器中)

 

但是这里直接写死的地址406044,406044就表示arr[5],甚至连“基址 + 偏移量”都没用到。

 

原创粉丝点击