调用位于堆中的函数时出现分段错误

Del*_*hts 15 c linux mprotect

我试图在这里略微调整规则和malloc缓冲区,然后将函数复制到缓冲区.

调用缓冲函数有效,但是当我试图调用其中的另一个函数时,该函数会抛出分段错误.

有什么想法?

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

int foo(int x)
{
    printf("%d\n", x);
}

int bar(int x)
{
}

int main()
{
    int foo_size = bar - foo;

    void* buf_ptr;

    buf_ptr = malloc(1024);

    memcpy(buf_ptr, foo, foo_size);

    mprotect((void*)(((int)buf_ptr) & ~(sysconf(_SC_PAGE_SIZE) - 1)),
             sysconf(_SC_PAGE_SIZE),
             PROT_READ|PROT_WRITE|PROT_EXEC);

    int (*ptr)(int) = buf_ptr;

    printf("%d\n", ptr(3));

    return 0;
}
Run Code Online (Sandbox Code Playgroud)

此代码将抛出一个段错误,除非我将foo函数更改为:

int foo(int x)
{
    //Anything but calling another function.
    x = 4;
    return x;
}
Run Code Online (Sandbox Code Playgroud)

注意:

代码成功复制foo到缓冲区,我知道我做了一些假设,但在我的平台上他们没问题.

Art*_*Art 38

您的代码不是位置独立的,即使它是,您也没有正确的重定位来将其移动到任意位置.您对printf(或任何其他功能)的调用将通过pc相对寻址完成(通过PLT,但除了这一点之外).这意味着为调用printf而生成的指令不是对静态地址的调用,而是"从当前指令指针调用函数X字节".由于您移动了代码,因此调用了一个错误的地址.(我在这里假设i386或amd64,但一般来说这是一个安全的假设,那些在奇怪平台上的人通常会提到这一点).

更具体地说,x86有两个不同的函数调用指令.一个是相对于指令指针的调用,它通过向当前指令指针添加一个值来确定函数调用的目的地.这是最常用的函数调用.第二条指令是对寄存器或存储器位置内的指针的调用.编译器常常使用它,因为它需要更多的内存间接并使管道停滞不前.实现共享库的方式(您printf实际上将调用共享库)是对于您在自己的代码之外进行的每个函数调用,编译器将在您的代码附近插入伪函数(这是我上面提到的PLT).你的代码对这个假函数执行普通的pc相对调用,伪函数会找到真正的地址printf并调用它.但这并不重要.几乎所有正常的函数调用都是pc相关的,并且会失败.你在这样的代码中唯一的希望就是函数指针.

您可能还会对可执行文件遇到一些限制mprotect.检查mprotect我的系统的返回值,你的代码不能再工作了一个原因:mprotect不允许我这样做.可能是因为后端内存分配器malloc有额外的限制,阻止其内存的可执行保护.这让我想到了下一点:

您将通过调用mprotect非您管理的内存来解决问题.这包括你得到的记忆malloc.你应该只有mprotect自己从内核中获得的东西mmap.

这是一个演示如何使这项工作的版本(在我的系统上):

#include <stdio.h>
#include <sys/mman.h>
#include <unistd.h>
#include <string.h>
#include <err.h>

int
foo(int x, int (*fn)(const char *, ...))
{
        fn("%d\n", x);
        return 42;
}

int
bar(int x)
{
        return 0;
}

int
main(int argc, char **argv)
{
        size_t foo_size = (char *)bar - (char *)foo;
        int ps = getpagesize();

        void *buf_ptr = mmap(NULL, ps, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_ANON|MAP_PRIVATE, -1, 0);

        if (buf_ptr == MAP_FAILED)
                err(1, "mmap");

        memcpy(buf_ptr, foo, foo_size);

        int (*ptr)(int, int (*)(const char *, ...)) = buf_ptr;

        printf("%d\n", ptr(3, printf));

        return 0;
}
Run Code Online (Sandbox Code Playgroud)

在这里,我滥用了编译器如何为函数调用生成代码的知识.通过使用函数指针,我强制它生成一个不是pc相对的调用指令.此外,我自己管理内存分配,以便我们从开始获得正确的权限,而不会遇到任何brk可能的限制.作为奖励,我们做错误处理实际上帮助我在本实验的第一个版本中找到了一个错误,并且我还纠正了其他一些小错误(比如缺少包括),它允许我在编译器中启用警告并捕获另一个潜在的问题.

如果你想深入研究这个,你可以做这样的事情.我添加了两个版本的函数:

int
oldfoo(int x)
{
        printf("%d\n", x);
        return 42;
}

int
foo(int x, int (*fn)(const char *, ...))
{
        fn("%d\n", x);
        return 42;
}
Run Code Online (Sandbox Code Playgroud)

编译整个东西并拆解它:

$ cc -Wall -o foo foo.c
$ objdump -S foo | less
Run Code Online (Sandbox Code Playgroud)

我们现在可以看看两个生成的函数:

0000000000400680 <oldfoo>:
  400680:       55                      push   %rbp
  400681:       48 89 e5                mov    %rsp,%rbp
  400684:       48 83 ec 10             sub    $0x10,%rsp
  400688:       89 7d fc                mov    %edi,-0x4(%rbp)
  40068b:       8b 45 fc                mov    -0x4(%rbp),%eax
  40068e:       89 c6                   mov    %eax,%esi
  400690:       bf 30 08 40 00          mov    $0x400830,%edi
  400695:       b8 00 00 00 00          mov    $0x0,%eax
  40069a:       e8 91 fe ff ff          callq  400530 <printf@plt>
  40069f:       b8 2a 00 00 00          mov    $0x2a,%eax
  4006a4:       c9                      leaveq
  4006a5:       c3                      retq

00000000004006a6 <foo>:
  4006a6:       55                      push   %rbp
  4006a7:       48 89 e5                mov    %rsp,%rbp
  4006aa:       48 83 ec 10             sub    $0x10,%rsp
  4006ae:       89 7d fc                mov    %edi,-0x4(%rbp)
  4006b1:       48 89 75 f0             mov    %rsi,-0x10(%rbp)
  4006b5:       8b 45 fc                mov    -0x4(%rbp),%eax
  4006b8:       48 8b 55 f0             mov    -0x10(%rbp),%rdx
  4006bc:       89 c6                   mov    %eax,%esi
  4006be:       bf 30 08 40 00          mov    $0x400830,%edi
  4006c3:       b8 00 00 00 00          mov    $0x0,%eax
  4006c8:       ff d2                   callq  *%rdx
  4006ca:       b8 2a 00 00 00          mov    $0x2a,%eax
  4006cf:       c9                      leaveq
  4006d0:       c3                      retq
Run Code Online (Sandbox Code Playgroud)

在这种printf情况下,函数调用的指令是"e8 91 fe ff ff".这是一个与pc相关的函数调用.我们的指令指针前面的0xfffffe91字节.它被视为带符号的32位值,计算中使用的指令指针是下一条指令的地址.所以0x40069f(下一条指令) - 0x16f(前面的0xfffffe91是带有符号数学的0x16f字节)给我们地址0x400530,并查看反汇编代码,我在地址找到了这个:

0000000000400530 <printf@plt>:
  400530:       ff 25 ea 0a 20 00       jmpq   *0x200aea(%rip)        # 601020 <_GLOBAL_OFFSET_TABLE_+0x20>
  400536:       68 01 00 00 00          pushq  $0x1
  40053b:       e9 d0 ff ff ff          jmpq   400510 <_init+0x28>
Run Code Online (Sandbox Code Playgroud)

这是我前面提到的神奇的"假功能".让我们不要了解这是如何工作的.共享库是必要的,这是我们现在需要知道的全部内容.

第二个函数生成函数调用指令"ff d2".这意味着"在存储在rdx寄存器中的地址处调用函数".没有PC相对寻址,这就是它的工作原理.

  • @Medals IMO检查错误会使调试变得更加令人沮丧并且更有趣. (3认同)
  • 由于W ^ X,`mprotect()`可能无法在您的系统上运行:https://en.wikipedia.org/wiki/W%5EX (2认同)