【问题标题】:How can you mark a segment of memory as executable in C?如何在 C 中将一段内存标记为可执行文件?
【发布时间】:2019-12-18 22:55:30
【问题描述】:

我最近正在研究一些 JIT 编译器。据我所知,JIT 是一种即时(在执行之前)将一些脚本语言代码编译为本机代码的技术。正如我想象的这样一个编译器的内部,我发现生成的本机代码所在的地方肯定有一段动态分配的缓冲区。但是我们需要一种方法来从保存数据的缓冲区中开始运行代码。我的意思是,您不能只是将一些代码放入char[],然后因为安全隐患而跳入执行,为此操作系统必须阻止您这样做。必须有某种方法将缓冲区标记为可执行文件。考虑以下幼稚的方法:

#include <stdlib.h>

void *jit_some_native_code(void) {
  void *code_segment = malloc(1024);
  /*
   * bla bla bla...
   * Generate code into this code_segment.
   */

  return code_segment;
}

int main(void) {
  void *code = jit_some_native_code();
  /*
   * How can I start executing instruction in code?
   */

  typedef void (*func_ptr_t)(void);

  /*
   * This won't work. OS bans you doing so.
   */
  ((func_ptr_t)code)();

}

在 Ubuntu 上,代码会运行,但会以状态码 26 退出。 鉴于 C 的类型不安全性质,代码可以编译,但对于 C++,编译器只会阻止您。这是否意味着 JIT 必须绕过编译器以及设置可执行标志?

编辑:除了mprotect,如果使用mmap,还可以指定要映射的页面的权限:

   PROT_EXEC  Pages may be executed.
   PROT_READ  Pages may be read.
   PROT_WRITE Pages may be written.
   PROT_NONE  Pages may not be accessed.

因此该页面将具有可执行权限。

【问题讨论】:

  • ((func_t) &amp;code) - 你正在执行code 指针的地址,而不是函数......它应该是((func_t)code)()for C++, the compiler simply stops you ?只需转换 void *code = (void*)&amp;some_jit_func 即可使用 C++ 进行编译。
  • @KamilCuk 感谢您指出代码中的错误。但我记得 C++ 禁止将 void* 转换为函数指针?
  • 神奇的是,static_cast 禁止这种转换,而 C 风格转换允许它!
  • @ryyker 问题是关于如何标记内存区域可执行,而不是如何正确调用函数指针。我相信我的评论不是答案。
  • 一个 void* 可能不足以容纳一个函数指针。转换为不同大小的指针是未定义的行为。所以,不,您不能只将 void * 大小写为函数指针。在 C 和 C++ 中都没有。为什么不将code = &amp;some_jit_func 声明为函数指针呢? jit-me-this-bit-of-script 函数应该返回一个函数指针而不是 void*。

标签: c executable jit


【解决方案1】:

如果你想在堆中创建一个可执行的区域,你可以使用mprotect

int main() {
  typedef void (*func_t)(void);
  void *code = &some_jit_func;
  int pagesize = getpagesize();
  mprotect(code, pagesize,PROT_EXEC);
  ((func_t)code)();
}

您也可以使用 PROT_READ/PROT_WRITE 或标志

【讨论】:

  • 如何使代码可执行? mprotect 不只是将一段内存标记为受保护吗? (即因为它是可执行的。)
  • 措辞有点混乱,但 PROT_EXEC 实际上意味着允许执行该区域。如果您根本不想访问该内存区域,您将使用 PROT_NONE。如果您只想读取和写入,您将使用 PROT_READ|PROT_WRITE。默认情况下,堆不是内存的可执行区域。
  • @ryyker 没有。正如其manpage 中明确提到的,mprotect 更改内存页面的访问权限。除此之外,大多数操作系统会拒绝执行一个既可执行又可写的页面。
  • @Nina 和 mosvy - 感谢您的澄清!
  • @cgsdfc:如果他们有能力调用mprotect,他们已经有能力执行代码了。 mprotect 所做的只是将其扩展到执行动态生成的代码。只有当有人找到一种方法(通常利用程序中的其他缺陷)用恶意代码填充缓冲区说服程序调用@987654327时,才会出现安全问题@ 在上面。但它实际上并没有提供任何新的特权。任何程序不允许做的事情,它也不能用新代码做;您还需要提权才能做到这一点。
【解决方案2】:

在您的代码中,您正在获取现有函数的地址。这自然会指向一个已经可以执行的内存区域。但是同一区域在任何现代系统上都不可写。

另一方面,如果你 malloc() 一些内存,它将是可写但不可执行的。因此,您在 jit 编译器中构建的任何代码都将无法执行,并且尝试调用您在那里构建的函数将会失败。您必须首先使用mprotect 使内存可执行。

出于安全原因,您应该遵循W^X 原则。这意味着任何页面只能是可写或可执行的,但不能两者兼而有之。当您使用 mprotect 使代码可执行时,也使其不可写,反之亦然。永远不要将可写和可执行结合起来。

【讨论】:

    猜你喜欢
    • 2013-02-20
    • 1970-01-01
    • 2011-06-05
    • 1970-01-01
    • 2017-11-23
    • 1970-01-01
    • 2020-07-14
    • 2012-03-24
    • 1970-01-01
    相关资源
    最近更新 更多