0x01 内存图

程序在运行时是加载到内存里面执行的(内存指的是内存条,也就是程序运行时它就被从硬盘里复制了一份到内存条里)。在一块内存中分为5个区:

栈(stack)在中文里叫做”堆栈”,而堆(heap)就叫”堆”。这个我不明白是为什么。

0x02 全局变量

全局变量的地址在编译的时候就已经确定了。

全局变量可以被所有的函数读写。

全局变量会一直占用内存,直到整个线程结束。

全局变量在汇编里的特征是: 在读写全局变量时,地址是一个固定的十六进制值,例如:

1
MOV EAX,DWORD PTR DS:[0x11111111]

0x03 局部变量

局部变量没有固定的地址

局部变量只有被创建(局部变量所在的函数被调用)时才会分配内存

局部变量储存在堆栈里面

局部变量所在的函数运行完毕后就被丢弃了,虽然局部变量所储存的值仍然存放在堆栈里,但是它已经是一个垃圾值了。下一次调用其它函数时可能会被覆盖掉。

局部变量的特征是: 在读写局部变量时,地址是通过栈底指针或栈顶指针加减偏移量来找到地址的,例如:

1
MOV EAX,DWORD PTR DS:[EBP-4]

0x04 参数个数

函数调用的第一步就是参数压栈,所以在判断函数的参数个数的时候,第一步先看在函数调用时有多少个push,然后再通过堆栈平衡的代码来验证。

如果是外平栈,就看平衡堆栈时把堆栈指针回退了几个单位。

如果是内平栈,就看平衡堆栈时RET的值是多少。

在windows中堆栈的地址顺序是从大到小的,所以堆栈指针回退就应该是地址变大(加),提升就应该是地址变小(减)。

通过这两个步骤在一般情况下就可以知道函数有几个参数了。假如有两个push,平衡堆栈的代码也正好是add esp,0x8,那么参数的个数就应该是两个。假如有3个push,平衡堆栈的代码是add esp,0xC,那么参数的个数就应该是3个。

0x05 if语句

如果有几行影响标志寄存器的指令,然后下面紧接着就是一个JCC指令,那这就很可能是一个if语句。

这是一个c语言的if语句的正向代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# include <stdio.h>
int MAX = 0;

int fun(int a,int b){
if (a > b){
MAX = a;
}

return 0;
}

int main(){
fun(5,3);

return 0;
}

定义了一个函数fun,有两个int类型的形参a和b。然后函数里面有一个判断,如果参数a大于参数b,那么就把a的值赋给全局变量MAX。

在VC6里编译运行,然后右键查看它的汇编代码,调用fun函数的汇编代码:

1
2
3
4
00401078   push        3
0040107A push 5
0040107C call @ILT+0(_fun) (00401005)
00401081 add esp,8

首先就是两个push,把fun()函数的两个参数压栈,然后就是call指令开始调用函数。call指令下面的add指令是为了实现堆栈平衡,上面有2个push,平衡堆栈的指令正好是把堆栈指针回退8个字节(2个单位),由此可以知道fun()函数的参数个数是2个。

在call指令那里f11单步步入,进入fun()函数:

首先是堆栈操作(0x401020到0x401036),提升堆栈,填充缓冲区。对堆栈的操作是在堆栈图的时候学习的,就不用多说了。

再下面就是if语句了

1
2
3
00401038   mov         eax,dword ptr [ebp+8]
0040103B cmp eax,dword ptr [ebp+0Ch]
0040103E jle fun+29h (00401049)

首先是MOV指令把ebp+0x8的地址里的值放到eax寄存器,然后cmp指令把 ebp+0xC的地址的值 与 eax 的值进行比较,紧接着就是一个JCC指令。cmp指令会影响标志寄存器,而JCC指令就是根据标志寄存器来决定是否跳转的。

c语言的if语句和汇编里的jcc指令的逻辑是完全相反的,例如c语言里if判断的是大于,那么在汇编里jcc指令就是jle,jle的意思是小于等于则跳转。为什么逻辑是完全相反的其实这个很好理解,因为正向代码里if的条件如果成立,就会执行if下面的代码。

cmp指令是判断if条件是否成立,而jcc指令是判断是否执行if下的代码。

if判断的是大于,那么jcc指令就是jle,小于等于时跳转,如果if条件的大于是成立的,那么jle指令就不会跳转,汇编执行jle执行下面的那一行代码(在正向代码就相当于if成立,就执行if括号里的代码块)。如果if条件的大于是不成立的,只要大于不成立,那么小于或等于就必定是成立的,那么jle就会跳转,jle就跳走啦,跳去执行其他地方的代码了(在正向代码里就相当于if条件不成立,那么if下面的代码就不能执行了,转去执行if的花括号之外的代码了)。就像在这个例子里,它jle跳到0x401049了,0x401049的代码就是if的花括号之外的代码,是xor eax,eax;把eax清零了。也就是c语言的return 0;这一行代码

在正向代码里,如果a大于b,就把a的值赋给全局变量MAX,来看汇编里是怎样实现的:

1
2
00401040   mov         ecx,dword ptr [ebp+8]
00401043 mov dword ptr [_MAX (00427c50)],ecx

首先把ebp+8的地址的值放到ecx寄存器,然后把ecx寄存器的值放到0x427c50。上面也说了全局变量的主要特征之一就是它的地址是固定的一个十六进制值。

0x06 ebp+和ebp-

ebp+是参数,ebp-是局部变量。

在调用函数的时候,第一步就是把参数压入堆栈,c语言是从右到左的顺序,所以fun(5,3);的压栈顺序是push 3然后push 5;。

参数入栈以后就执行call指令,call执行调用函数时会把返回地址压入堆栈,现在堆栈里就有3个数据了,这三个数据从栈顶到栈底的顺序分别是函数返回地址、函数左边第一个参数、函数左边第二个参数。

call指令返回地址入栈以后就真正进入被调用的函数里面了,进入函数以后就开始堆栈操作,第一行就是push ebp,把当前栈底地址压入堆栈。然后mov ebp,esp,把当前栈顶地址作为栈底地址完成堆栈提升。

push ebp后,堆栈里就有4个数据了,从栈顶到栈底的顺序分别是函数调用前的栈底地址、函数返回地址、函数左边第一个参数、函数左边第二个参数。

然后mov ebp,esp把栈顶作为栈底,在windows中,堆栈地址是按照从大到小的顺序,那么ebp + 4就是栈底往后退4个字节,就是函数返回地址,ebp + 8就是最后一个压入堆栈的参数(左边第一个),ebp + c就是倒数第二个压入堆栈的参数(左边第二个)。

所以ebp+偏移就是传入函数的参数了,ebp+0x4是函数调用前的栈底地址。

ebp+0x8就是最后一个压入堆栈的参数,也就是函数左边第一个参数

ebp+0xC就是倒数第二个压入堆栈的参数,也就是函数左边第二个参数

ebp+0x10就是倒数第三个压入堆栈的参数,也就是函数左边第三个参数

ebp+0x14就是倒数第四个压入堆栈的参数,也就函数左边第四个参数

以此类推,这个很好理解。