栈溢出(Stack Overflow)与函数调用中的栈变化

详细探讨栈溢出原理以及x86-32架构下函数调用中的栈帧变化。

栈溢出(stack overflow)

一些术语/概念

  • 类型安全(Type safety)
    • In computer science, type safety and type soundness are the extent to which a programming language discourages or prevents type errors.

      ————From wikipidiea

前置知识:x86架构下函数调用中的栈变化

  • 初始:

    • 栈示意图:

    初始

  • 压入参数(arg1, arg2)

    • 指令:
      1
      2
      
      push arg2
      push arg1 ;注意,逆序压入参数
      
    • 栈示意图:

    压入参数

  • 调用函数

    • 指令
      1
      2
      3
      4
      
      call fuc
      ; 等价于
      push eip+5 ;5为call指令的长度
      jmp fuc
      
    • 栈示意图

    调用函数

  • 函数序言(Prologue)

    • 指令:
    1
    2
    3
    
    push ebp     ;保存调用者的EBP
    mov ebp, esp ;设置当前函数的EBP
    sub esp, 8   ;为局部变量分配空间(示例中分配8字节)
    
    • 栈示意图

    Prologue

  • 访问数据

    • 指令:
    1
    2
    3
    
    mov eax, [ebp+8]  ;访问arg1
    mov ebx, [ebp+12] ;访问arg2
    mov ecx, [ebp-4]  ;访问val1
    
    • 栈示意图

    访问数据

  • 函数尾声

    • 指令
    1
    2
    3
    
    mov esp, ebp ;释放局部变量
    pop ebp      ;恢复调用者ebp
    ret          ;返回到调用者
    
    • 栈示意图(Epilogue)

    Epilogue

  • 调用者清理参数

    • 指令
    1
    
    sub esp, 8
    
    • 栈示意图:

    清理参数

栈溢出

缓冲区溢出

  • 当数据写入到分配给特定数据结构的内存边界范围之外时,就会发生缓冲区溢出

  • 当缓冲区边界被忽略和未检查时会发生

    • 示例:
    1
    2
    3
    4
    5
    6
    7
    8
    
    #include<stdio.h>
    int main()
    {
      char a[5];
      gets(a);
      puts(a);
      printf("%c", a[5]);
    }
    

    直接编译

    1
    
    gcc buffer_overflow1.c
    

    可以看到如下warning

    1
    2
    3
    4
    5
    6
    7
    
    buffer_overflow1.c: In function ‘main’:
    buffer_overflow1.c:6:5: warning: implicit declaration of function ‘gets’; did you mean ‘fgets’? [-Wimplicit-function-declaration]
        6 |     gets(a);
          |     ^~~~
          |     fgets
    /usr/bin/ld: /tmp/ccA63mQo.o: in function `main':
    buffer_overflow1.c:(.text+0x28): warning: the `gets' function is dangerous and should not be used.
    

    这是因为gets(puts)是一个不安全的函数,缺少缓冲区边界检查,即没有对读入字符个数的检查和限制。 现在编译后的可执行文件

    1
    
    ./a.out
    

    输入6个字母abcdef,可以看到如下输出

    1
    2
    3
    4
    
    abcdef
    abcdef
    *** stack smashing detected ***: terminated
    Aborted (core dumped)
    

    可以看到程序被终止,并且给出了*** stack smashing detected ***: terminated,这是由堆栈保护机制(Stack Smashing Protection, SSP)发出的警告信息。当编译器开启了 SSP 选项(例如 GCC 的 -fstack-protector 或 -fstack-protector-all)时,它会在函数栈帧中插入一个被称为“canary”(金丝雀)的随机值。如果缓冲区溢出发生,覆盖了返回地址,那么这个 canary 值也会被改变。在函数返回之前,程序会检查这个 canary 值是否被篡改。如果被篡改,就意味着发生了缓冲区溢出,程序会立即终止执行,并打印这个警告信息。 现在在关闭相关保护机制的情况下编译

    1
    
    gcc -fno-stack-protector -no-pie buffer_overflow1.c
    

    其中的编译选项含义如下:

    • -fno-stack-protector: 禁用堆栈保护(stack canary)。这会阻止编译器在缓冲区溢出发生时自动终止程序。
    • -no-pie: 禁用位置独立可执行文件(Position Independent Executable),使得程序加载到固定的内存地址。这与另一种针对栈溢出的防御地址空间布局随机化(ASLR)有关。

    运行后得到如下输出

    1
    2
    3
    
    abcdef
    abcdef
    f
    

    可以看到输入的字符成功覆盖了字符串的后一个字节。即可以利用栈溢出篡改内存中的字节,这是栈溢出攻击的基本原理。

利用栈溢出的攻击方式————拿shell

即通过栈溢出注入shellcode来获取目标程序的shell,得到shell后就可以劫持数据流

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计