查看: 3300|回复: 1

[分享] 好文分享-什么是栈溢出攻击

[复制链接]
  • TA的每日心情
    开心
    2025-7-11 08:53
  • 签到天数: 301 天

    连续签到: 2 天

    [LV.8]以坛为家I

    3954

    主题

    7579

    帖子

    0

    管理员

    Rank: 9Rank: 9Rank: 9

    积分
    40401
    最后登录
    2025-9-19
    发表于 2022-3-4 13:13:31 | 显示全部楼层 |阅读模式
    好文分享-什么是栈溢出攻击


    什么是栈
    简单来说,栈 是一种 LIFO(Last In Frist Out,后进先出) 形式的数据结构。栈一般是从高地址向低地址增长,并且栈支持 push(入栈) 和 pop(出栈) 两个操作。如下图所示:
    14.png
    push 操作先将 栈顶(sp指针) 向下移动一个位置,然后将数据写入到新的栈顶;而 pop 操作会从 栈顶 读取数据,并且将 栈顶(sp指针) 向上移动一个位置。


    例如,将 0x100 压入栈,过程如下图所示:
    15.png
    我们再来看看 出栈 操作,如下图所示:
    16.png
    栈帧
    栈帧,也就是 Sack Frame,其本质就是一种栈,只是这种栈专门用于保存函数调用过程中的各种信息(参数,返回地址,本地变量等)。


    栈帧 有 栈顶 和 栈底 之分,其中栈顶的地址最低,栈底的地址最高。SP(栈指针) 就是一直指向栈顶的。在 x86 的 32 位 CPU 中,我们用 %ebp 寄存器指向栈底,也就是基址指针;用 %esp 寄存器指向栈顶,也就是栈指针。下面是一个栈帧的示意图:
    17.png
    一般来说,我们将 %ebp 到 %esp 之间区域当做栈帧。并不是整个栈空间只有一个栈帧,每调用一个函数,就会生成一个新的栈帧。


    在函数调用过程中,我们将调用函数的函数称为:调用者(caller),将被调用的函数称为:被调用者(callee)。在这个过程中:


    调用者 需要知道在哪里获取 被调用者 返回的值(一般存放到 %eax 寄存器)。
    被调用者 需要知道传入的参数在哪里和调用完后的返回地址在哪里。
    我们需要保证在 被调用者 返回后,%ebp 和 %esp 寄存器的值应该和调用前一致。
    函数调用
    现在,我们来看看函数调用时,栈帧是如何变化的。


    我们以一个函数调用的实例来解说,代码如下:
    1. // stack.c
    2. int add_func(int a, int b)
    3. {
    4.     int c, d;
    5.     c = a;
    6.     d = b;
    7.     return c + d;
    8. }
    9. int main(int argc, char *argv[])
    10. {
    11.     int total;
    12.     total = add_func(1, 2);
    13.     return 0;
    14. }
    复制代码
    我们使用命令 gcc -S -m32 stack.c 来编译上面的代码,获取的汇编代码如下所示(去掉一些无关紧要的信息):
    1. add_func:
    2.     pushl   %ebp                // 保存ebp寄存器到栈
    3.     movl    %esp, %ebp          // 把ebp进程设置为esp的值
    4.     subl    $16, %esp           // 为局部变量申请空间
    5.     movl    8(%ebp), %eax       // 把参数a保存到eax寄存器中
    6.     movl    %eax, -8(%ebp)      // 把eax寄存器的值保存到局部变量c中(c = a)
    7.     movl    12(%ebp), %eax      // 把参数b保存到eax寄存器中
    8.     movl    %eax, -4(%ebp)      // 把eax寄存器到值保存到局部变量d中(d = b)
    9.     movl    -8(%ebp), %edx      // 把d的值保存到edx寄存器中
    10.     movl    -4(%ebp), %eax      // 把c的值保存到eax寄存器中
    11.     addl    %edx, %eax          // 将eax寄存器与edx寄存器的值相加,保存到eax中(返回值)
    12.     leave
    13.     ret                         // 函数返回
    复制代码

    可能汇编代码比较难看懂,我们用下面的插图来说明这个调用过程:
    18.png
    如上图所示,调用过程如下:


    在 main() 函数调用 add_func() 函数前,先将调用 add_func() 函数的参数压栈。
    在调用 add_func() 函数时,会将 返回地址 压栈,接着进入 add_func() 函数。
    add_func() 函数执行时,会将原来的 ebp寄存器 的值压栈,然后把 ebp寄存器 的设置为 esp寄存器 的值。
    接着 add_func() 函数会为局部变量申请空间,也就是将 esp寄存器 向下移动。
    然后把局部变量 c 设置为参数 a 的值,局部变量 d 设置为 参数 b 的值。
    最后将局部变量 c 和 d 的值相加,放置到 eax寄存器 中(C语言规定以 eax寄存器 传递返回值),然后调用 ret 指令返回到 main() 函数。
    函数返回
    上面介绍了 函数调用 的过程,现在我们来介绍一下函数调用完毕后,从被调用函数返回到原来的函数过程是如何处理的。



    从 add_func() 函数的汇编代码可以看到,当被调用函数执行完毕返回到调用函数前,会执行 leave 指令,这条指令等价于:
    1. movl %ebp, %esp
    2. popl %ebp
    复制代码
    这两条汇编指令的意思是,将 esp寄存器 和 ebp寄存器 恢复到调用函数前的值。


    然后,调用 ret 指令返回到原来的函数。ret 指令会从栈顶获取 返回地址,然后跳转到(jmp指令)此地址继续执行。这时的 栈帧 的结构如下图所示:
    19.png
    栈溢出攻击
    前面说了那么,都是为了 栈溢出攻击 这节作铺垫的。通过前面的学习,我们知道调用函数的 参数 、执行完函数后的 返回地址 和被调用函数的 局部变量 都是存放在栈中的。


    如果在调用函数时,不小心将 返回地址 覆盖了,那么调用完函数后,将不会跳转到原来的函数继续执行,而是跳转到覆盖后的地址执行。如下图所示:
    20.png
    那么,怎样才能把 返回地址 覆盖呢?我们可以通过下面的例子来说明:
    1. #include <stdio.h>
    2. #include <string.h>
    3. #include <unistd.h>
    4. #include <stdlib.h>
    5. #include <stdint.h>
    6. #define PTR_SIZE 8   // 指针的大小
    7. #define EBP_SIZE 8   // ebp寄存器的大小
    8. void inject_callback()
    9. {
    10.     printf("inject_callback called...\n");
    11.     exit(0);
    12. }
    13. void func_call(char *addr, int len)
    14. {
    15.     char tmpBuf[16] = {0xff};
    16.     memcpy(tmpBuf + 16 + EBP_SIZE, addr, len);
    17.     printf("func_call called...\n");
    18. }
    19. int main(int argc, char** argv)
    20. {
    21.     uint64_t injectPtr = (uint64_t)&inject_callback;
    22.     func_call(&injectPtr, PTR_SIZE);
    23.     printf("main exited...\n");
    24.     return 0;
    25. }
    复制代码
    我们使用以下命令编译上面代码,并且执行:
    1. $ gcc stack-overflow.c -fno-stack-protector -o stack-overflow
    2. $ ./stack-overflow
    3. func_call called...
    4. inject_callback called...
    复制代码
    在编译上面程序时,一定要加上 -fno-stack-protector 参数,否则将会触发栈溢出保护,导致执行失败。


    在上面的代码中,我们并没有直接调用 inject_callback() 函数,而是通过把 inject_callback() 函数的地址复制到 func_call() 函数的局部变量 tmpBuf 中。


    由于局部变量 tmpBuf 的类型为字符串数组,而且大小为 16 个字节。但我们复制数据是从 24(16 + 8)处开始复制,已经超出了局部变量 tmpBuf 的大小,如下图所示:
    21.png
    从上图可以看出,func_call() 函数在调用 memcpy() 函数复制数据时,由于不小心用 inject_callback() 函数的地址覆盖了返回地址,导致 func_call() 函数执行完毕后,跳转到 inject_callback() 函数处执行。


    这就是 栈溢出攻击 的原理,而导致 栈溢出攻击 的原因就是:调用 memcpy()、strcpy() 等函数复制数据时,没有对数据的长度进行验证,从而 返回地址 被复制的数据覆盖了。


    黑客可以利用 栈溢出攻击 来把函数的返回地址修改成入侵代码的地址,从而实现攻击的目的。


    qiandao qiandao
    回复

    使用道具 举报

  • TA的每日心情
    开心
    昨天 13:15
  • 签到天数: 1505 天

    连续签到: 1 天

    [LV.Master]伴坛终老

    97

    主题

    4698

    帖子

    12

    版主

    Rank: 7Rank: 7Rank: 7

    积分
    10119
    最后登录
    2025-9-20
    发表于 2022-3-4 14:10:11 | 显示全部楼层
    同样,如果把栈的内容都复制出来,再加以分析,就可以分析出来在密码匹配的时候的操作流程了。
    该会员没有填写今日想说内容.
    回复 支持 反对

    使用道具 举报

    您需要登录后才可以回帖 注册/登录

    本版积分规则

    关闭

    站长推荐上一条 /3 下一条

    Archiver|手机版|小黑屋|恩智浦技术社区

    GMT+8, 2025-9-21 04:56 , Processed in 0.102927 second(s), 23 queries , MemCache On.

    Powered by Discuz! X3.4

    Copyright © 2001-2024, Tencent Cloud.

    快速回复 返回顶部 返回列表