首页 > 解决方案 > 为什么我的 C 代码会抛出分段错误,即使返回指针指向看似有效的 shellcode 的内存地址?

问题描述

我正在尝试学习有关缓冲区溢出的教程(Vivek Ramachandran 的缓冲区溢出入门)。我确实在遵循他的代码,该代码在演示中对他有用,并且在此之前一直对我有用。

下面的 C 程序的目标是将退出系统调用的 shellcode 分配给一个变量,然后将指向 __lib_start_main 的 main 函数的默认返回地址替换为 shellcode 变量的内存地址,这样程序在完成 main 函数后执行 shellcode,然后优雅地退出程序,值为 20(如执行“exit(20)”)。不幸的是,程序以分段错误结束。我在 32 位 Linux Mint 上运行它。我正在使用 gcc 编译代码,并使用 --ggdb 和 -mpreferred-stack-boundary=2 选项对其进行了编译,并且我尝试了使用和不使用 -fno-stack-protector 选项。

这是代码:

#include<stdio.h>

char shellcode[] = "\xbb\x16\x00\x00\x00"
                   "\xb8\x01\x00\x00\x00"
                   "\xcd\x80";

int main(){

        int *ret;

        ret = (int *)&ret +2;

        (*ret) = (int)shellcode;

}
  1. 它首先定义一个名为 shellcode 的变量来保存 shellcode。
  2. main 函数被调用并定义了 ret 变量,该变量被加载到栈顶
  3. ret 变量的内存位置,加上 2 个整数空格,表示堆栈下 8 个字节的内存位置(返回指针的地址)被分配为 ret 变量的值。
  4. shellcode 变量的内存地址被写入由 ret 变量的值表示的内存地址——即——返回地址。
  5. 当函数到达返回指令时,它执行shellcode,也就是exit函数。

我已经通过 gdb 运行了这个,一切似乎都检查出来了: shellcode 变量的内存位置是 0x804a01c

在 main 执行开始时,返回值位于第 3 个十六进制字并指向 __lib_start_main

执行完 ret = (ret *)&ret +2 后,栈中的 ret 的值比栈首多 8 个字节

执行 (*ret) = (int)shellcode 后,返回指针(第 3 个十六进制字)包含 shellcode 的地址,而不是 __lib_start_main

该程序似乎开始在 shellcode 的内存地址处恢复执行,但仍然以分段错误结束。

提前致谢!

标签: cstackbuffer-overflowshellcode

解决方案


传统的缓冲区溢出漏洞利用确实涉及在堆栈上执行代码,但您的程序不会这样做。您的shellcode数组不在堆栈上,并且您用来破坏main返回地址以指向shellcode数组的构造不涉及在堆栈上执行代码。当我在我的 Linux 机器上运行你的程序(也在 x86 CPU 上运行),用 编译时gcc -O0 -m32,它确实将 EIP 寄存器设置为指向shellcode. 但是,正如它为您所做的那样,它会因分段错误而崩溃。

它崩溃的原因是因为shellcode被加载到标记为不可执行的内存区域中。(该内存区域的名称是“数据段”。)处理器拒绝从该区域执行机器指令,而是生成内核的“异常”(这是一个硬件概念,与 C++ 异常不同)转换为 SIGSEGV 信号。

关于编写 shellcode 和缓冲区溢出漏洞利用的旧教程不会警告您这种可能性,因为老一代的 x86 架构无法在每页的基础上将内存标记为不可执行。在大多数基于 x86 的 32 位操作系统使用的“平面”段寄存器配置中,任何可读的页面也是可执行的。但是,架构的最后几代已经能够将单个页面标记为不可执行,您必须解决这个问题。(如果我没记错的话,每页可执行性大约在 2003 年被添加到 x86 架构中,与 64 位模式同时,但操作系统支持变得普遍需要相当长的时间。)

在我的 Linux 机器上,如上所述,您的程序的这个修改版本成功地将控制权转移到并执行shellcode. 它使用mprotect系统调用来使包含shellcode可执行文件的内存区域。

#include <stdint.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/mman.h>

const char shellcode[] =
    "\xbb\x16\x00\x00\x00"
    "\xb8\x01\x00\x00\x00"
    "\xcd\x80";

int main(void)
{
  uintptr_t pagesize = sysconf(_SC_PAGESIZE);
  if (mprotect((void *)(((uintptr_t)shellcode) & ~(pagesize - 1)),
               pagesize, PROT_READ|PROT_EXEC)) {
    perror("mprotect");
    return 1;
  }

  void **ret;
  ret = (void **) &ret;
  ret[9] = (void *)shellcode;

  return 0;
}

除了mprotect操作本身,请注意添加该代码块如何更改堆栈布局并将返回地址放在不同的位置。如果我在优化的情况下进行编译,堆栈布局会再次更改,并且不会覆盖返回地址。还要注意我是如何shellcode做到的const char。如果我没有这样做,我将需要PROT_READ|PROT_WRITE|PROT_EXECmprotect调用中使用以避免过早崩溃,因为某些随机全局变量在 C 库预期的情况下突然不可写,并且内核可能会导致mprotect调用失败由于“ W^X ”安全策略。

根据您的内核和 C 库的年龄,制作shellcodebeconst char本身可能就足够了,但是对于我所拥有的内核 4.19 和 glibc 2.28,只读数据也不能执行。


推荐阅读