【问题标题】:Executing code in mmap to produce executable code segfaults在 mmap 中执行代码以产生可执行代码段错误
【发布时间】:2018-05-04 17:09:00
【问题描述】:

我正在尝试编写一个复制函数(并最终修改其程序集)并返回它的函数。这适用于一级间接,但在二级我得到一个段错误。

这是一个最小(不)工作的例子:

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

#define BODY_SIZE 100

int f(void) { return 42; }
int (*G(void))(void) { return f; }
int (*(*H(void))(void))(void) { return G; }

int (*g(void))(void) {
    void *r = mmap(0, BODY_SIZE, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
    memcpy(r, f, BODY_SIZE);
    return r;
}

int (*(*h(void))(void))(void) {
    void *r = mmap(0, BODY_SIZE, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
    memcpy(r, g, BODY_SIZE);
    return r;
}

int main() {
    printf("%d\n", f());
    printf("%d\n", G()());
    printf("%d\n", g()());
    printf("%d\n", H()()());
    printf("%d\n", h()()()); // This one fails - why?

    return 0;
}

我可以一次 memcpy 到一个 mmap'ed 区域,以创建一个可以调用的有效函数 (g()())。但是如果我尝试再次应用它(h()()()),它会出现段错误。我已经确认它正确地创建了g 的复制版本,但是当我执行该版本时,我得到了一个段错误。

我不能从另一个 mmap 区域执行一个 mmap 区域中的代码有什么原因吗?从带有x/i 的探索性gdb-ing 检查看来我可以成功调用,但是当我返回时,我来自的函数已被删除并替换为0。

我怎样才能让这种行为起作用?有没有可能?

大编辑:

很多人问我的理由,因为我显然在这里做一个 XY 问题。这是真实的和故意的。你看,不到一个月前this 问题发布在代码高尔夫堆栈交换上。它还为自己提供了一个不错的 bounty 用于 C/Assembly 解决方案。我对这个问题进行了一些闲散的思考,并意识到通过复制函数体,同时用一些唯一值存根地址,我可以在其内存中搜索该值并将其替换为有效地址,从而使我能够有效地创建 lambda 函数将单个指针作为参数。使用这个我可以让单一的柯里化工作,但我需要更一般的柯里化。因此,我目前的部分解决方案是链接here。这是展示我试图避免的段错误的完整代码。虽然这几乎是一个坏主意的定义,但我觉得它很有趣,并且想知道我的方法是否可行。我唯一缺少的是能够运行从函数创建的函数,但我无法让它工作。

【问题讨论】:

  • 你确定g的body size小于100字节吗?
  • 是的,反汇编显示它正好是 76 个字节。为了安全起见,我高估了。
  • 因为使用更大的 BODY_SIZE 值不会产生段错误(但不是正确的值)。
  • 我尝试转储内存地址,它们是相同的(可能是因为我的编译器)都有 76 字节的汇编,后跟 24 字节的 0。

标签: c linux assembly function-pointers currying


【解决方案1】:

代码使用相对调用来调用mmapmemcpy,因此复制的代码最终会调用无效位置。

您可以通过指针调用它们,例如:

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

#define BODY_SIZE 100

void* (*mmap_ptr)(void *addr, size_t length, int prot, int flags,
                  int fd, off_t offset) = mmap;
void* (*memcpy_ptr)(void *dest, const void *src, size_t n) = memcpy;

int f(void) { return 42; }
int (*G(void))(void) { return f; }
int (*(*H(void))(void))(void) { return G; }

int (*g(void))(void) {
    void *r = mmap_ptr(0, BODY_SIZE, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
    memcpy_ptr(r, f, BODY_SIZE);
    return r;
}

int (*(*h(void))(void))(void) {
    void *r = mmap_ptr(0, BODY_SIZE, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
    memcpy_ptr(r, g, BODY_SIZE);
    return r;
}

int main() {
    printf("%d\n", f());
    printf("%d\n", G()());
    printf("%d\n", g()());
    printf("%d\n", H()()());
    printf("%d\n", h()()()); // This one fails - why?

    return 0;
}

【讨论】:

  • 失败的方式完全相同。
  • 在这里工作正常。您使用的是 32 位还是 64 位?
  • 64 位可能对数据使用 rip-relative 寻址,因此与相对调用相同的问题仍然存在。
  • 有趣,不知道这个...需要认真挖掘 64 位寻址。
  • 不接受,因为我已经尝试过了,但无论哪种方式都值得一提(我意识到的第一件事是,我传递给这些复制函数的任何函数都必须存储在指针中,因为它们会相对跳到)。
【解决方案2】:

我正在尝试编写一个复制函数的函数

我认为这实际上不是正确的方法,除非您非常了解您的平台的机器代码(然后您不会问这个问题)。请注意position independent code(很有用,因为通常mmap(2) 会使用ASLR 并在地址中给出一些“随机性”)。顺便说一句,真正的 self-modifying machine code(即更改某些现有有效机器代码的某些字节)现在 cachebranch-predictor 不友好,应在实践中避免使用。

我建议两种相关的方法(选择其中一种)。

  • 生成一些临时 C 文件(另见this),例如在/tmp/generated.c 中,然后将使用gcc -Wall -g -O -fPIC /tmp/generated.c -shared -o /tmp/generated.so 的编译分叉成plugin,然后dlopen(3)(用于dynamic loading/tmp/generated.soshared object 插件(并且可能使用dlsym(3) 来查找函数指针在里面...)。有关共享对象的更多信息,请阅读 Drepper 的 How To Write Shared Libraries 论文。今天,您可以dlopen 数十万个这样的共享库(参见我的manydl.c 示例)和C 编译器(如最近的GCC)足够快,可以在与交互兼容的时间内编译几千行代码(例如,不到十分之一秒)。生成 C 代码是 widely used 的做法。在实践中,您将在生成的 C 代码的内存中表示一些 AST,然后再发出它。

  • 使用一些JIT compilation库,例如GCCJIT,或LLVM,或libjit,或asmjit等......这将在内存中生成一个函数,执行所需的relocations,并给你一些指向它。

顺便说一句,您可以考虑使用一些homoiconic 语言实现(例如用于Common Lisp 的SBCL,它在每次REPL 交互或任何动态构造的S-expr 时编译为机器码,而不是用C 编码)程序表示)。

closurescallbacks 的概念值得了解。阅读SICP,也许还有Lisp In Small Pieces(当然还有Dragon Book,了解一般编译器文化)。

【讨论】:

  • 机敏的你注意到我正在有效地尝试关闭。根据我的编辑,我实际上将最终在生成的代码中填充的结构命名为闭包,所以是的 - 我正在尝试递归执行 JIT。
  • 那就考虑使用一些JIT编译库
【解决方案3】:

这个问题发布在代码 golf.SE 上

我更新了 the 8086 16-bit code-golf answer 上的 sum-of-args currying 问题,以包括注释的反汇编。

您也许可以在具有堆栈参数调用约定的 32 位代码中使用相同的想法来制作机器代码函数的修改副本,该函数附加在 push imm32 上。不过,它不再是固定大小的,因此您需要在复制的机器代码中更新函数大小。

在正常的调用约定中,第一个 arg 最后推送,因此您不能在固定大小的 call target / leave / ret 预告片之前附加另一个 push imm32 .如果编写纯 asm 答案,您可以使用另一种调用约定,其中 args 以其他顺序推送。或者你可以有一个固定大小的介绍,然后是不断增长的push imm32 + call / leave / ret。

currying 函数本身可以使用 register-arg 调用约定,即使您希望目标函数使用 i386 System V 例如(堆栈参数)。

您肯定希望通过不支持超过 32 位的 args 来简化,因此不支持值结构,也不支持 double。 (当然,您可以将多次调用链接到 currying 函数以构建更大的 arg。)

考虑到新的代码高尔夫挑战的编写方式,我想您应该将 curried args 的总数与目标“输入”函数采用的 args 数量进行比较。


我认为你不可能只用 memcpy 在纯 C 中完成这项工作;你必须修改机器代码。

【讨论】:

    猜你喜欢
    • 2020-11-11
    • 2022-11-02
    • 1970-01-01
    • 2016-06-14
    • 2013-07-07
    • 1970-01-01
    • 2016-08-14
    • 2018-05-28
    • 1970-01-01
    相关资源
    最近更新 更多