x86跟ARM在一些指令集上有区别,但是大致逻辑一直。下面是以一个有栈溢出漏洞的vc6.0编写的C程序做示例。

C中,函数调用的逻辑为:

##1. 函数参数入栈

如func(int a, int b, int c)
会将三个参数从右到左依次入栈。也就是

1
2
3
push c
push b
push a

##2.函数的返回地址入栈

让程序知道执行完当前程序后怎么处理下一步。在汇编中的表现形式和3合并

##3.代码区跳转

跳转到被调用函数的入口地址,方便执行。
它和2的表现为

1
call 函数地址

##4.栈帧调整

这里会有3步

1. 保存当前的栈帧:push ebp
2. 设置新栈帧的底部:move ebp, esp
3. 抬高esp,也就是分配栈帧空间: sub esp, xx

##5. 【本函数调用完后】降低栈帧,释放空间

add esp, xx

这样也就完成了一个函数的整体调用,具体的看下面的截图和其中的注释

对于下面的局部变量申明,有一点需要注意的。
比如第2张图,var_444 = -444h,这个负号是因为它比当前的ebp先入栈。对于当前函数来说,高于ebp是正数,低于的就是负数。ebp可以理解为0这个基线,-444h就能找到它的指定位置

x86汇编7
x86汇编1
x86汇编2
x86汇编3
x86汇编4
x86汇编5
x86汇编6

x86汇编8

对于verify_password函数来说,它的栈帧为(从下到上):

1
2
3
4
5
6
char buff[0~3]
char buff[4~7]
int ret
上一个栈帧的EBP
verify_password函数的返回地址
形参password

此时的EBP指针指向”上一个栈帧的EBP”, ESP指向”char buff[0~3]”。

##绕过密码检测

对于ret来说,int值为4个字节,所以只需要在strcpy时让buff越界四位,那么buff[8],buff[9], buff[10], buff[11]就会覆盖ret的值。这里有个tips,对于字符串而言,末尾会自动加入一个NULL结束符,所以只要我们输入任意的8个字符,如12345678,就能通过这个密码程序(正确的密码是1234567)。
ret的值其实来源于strcmp的返回值,根据字符串的串顺序,如果password>1234567如12345678),那么ret=0x00000001,这个时候我们就可以用NULL覆盖的方法让它变为0x00000000。而如果password<1234567(如12345668),函数返回-1,此时ret=0xFFFFFF00,这时候用NULL就覆盖不了了

下图为输入1234567后栈顶保存的数据,它指向0018FB44这个地址,而这个地址的内容为(ASCII码)31,32,33,34,35,36,37。也就是数字的1234567.最后的00,是存放字符串的NULL的
图片

图片