在Linux系统中,2G~3G的内存地址是普通用户进程的加载空间,其内存布局如下图所示:

从图中可以看出栈结构是从高地址增长到低地址,而进行函数调用时系统所作的“序幕”工作就是将函数的返回地址和EBP压栈,再将ESP赋给EBP使其成为局部基指针,最后ESP减去一定的值为局部变量留出空间。这样当程序将过长的字符串拷贝到缓冲区时就会依次覆盖EBP和返回地址。当用AAAA覆盖返回地址,函数退栈时系统就将0x41414141(A的16进制ASCII码)赋给EIP去执行,由于是一个非法的内存地址,故程序崩溃。但如果用一个实际存在的地址覆盖返回地址,那么程序就转而去执行该地址处的指令,通常黑客会在这个地址植入所谓的shellcode,由shellcode产生一个shell,如果被攻击程序设置了suid位,那么产生的shell就是root shell,黑客也就获得了系统的最高控制权,这一过程就是基本的缓冲区溢出攻击。
覆盖函数的返回地址是比较常见的攻击方式,但缓冲区溢出攻击的手法是灵活多样的,往往在编程中的一个小小纰漏就可能导致被攻击,下面简单介绍一下几种较为高级的攻击方式。
(1)通过覆盖函数指针进行攻击:
/* vulprog */
int main(int argc , char * argv[])
{
void (* fp)(char *) = (void (*)(char *))&puts;
char buff[256];
strcpy(buff,argc[1]);
fp(argc[2]);
exit(1);
}
int main(int argc , char * argv[])
{
void (* fp)(char *) = (void (*)(char *))&puts;
char buff[256];
strcpy(buff,argc[1]);
fp(argc[2]);
exit(1);
}
上面这个程序在执行拷贝时没有检查边界,这样用户数据就有可能覆盖函数指针fp,如果用shllcode的地址去覆盖fp,那么函数指针调用时就会去执行shellcode。
这种覆盖函数指针的方式是一种较直接的覆盖方式(因为函数指针就在缓冲区上面),还有一种间接的覆盖方式,就是当函数指针不直接在缓冲区上面时,通过覆盖另外一个指针来覆盖函数指针,再将shellcode的地址填充函数指针。