【问题标题】:C: put x86 instructions into array and execute them [duplicate]C:将x86指令放入数组并执行它们[重复]
【发布时间】:2026-01-04 11:15:01
【问题描述】:

有没有办法将处理器指令放入数组,使其内存段可执行并作为简单函数运行:

int main()
{
    char myarr[13] = {0x90, 0xc3};
    (void (*)()) myfunc = (void (*)()) myarr;
    myfunc();
    return 0;
}

【问题讨论】:

  • 这看起来像XY problem。在您寻求帮助以某种特定方式解决问题之前,请告诉我们您要解决的问题。
  • @user6292850: 或有人编写动态重新编译器/JIT
  • @user6292850 不一定; OP 可能正在尝试编写 just-in-time compiler
  • @MartinJames 不正确。
  • 致所有反对者:这是一个很好的问题,但这并不表明他是一个邪恶的破坏者或类似的东西。我,在编写一个想要与编译代码互操作的 C 解释器时,我必须解决这个问题。

标签: c memory x86 execute


【解决方案1】:

在 Unix 上(现在,这意味着“除了 Windows 和一些你可能从未听说过的嵌入式和大型机的东西之外的所有东西”)你可以通过使用 mmap 分配整个页面,将代码写入其中来做到这一点,然后使用mprotect 使它们可执行。

void execute_generated_machine_code(const uint8_t *code, size_t codelen)
{
    // in order to manipulate memory protection, we must work with
    // whole pages allocated directly from the operating system.
    static size_t pagesize;
    if (!pagesize) {
        pagesize = sysconf(_SC_PAGESIZE);
        if (pagesize == (size_t)-1) fatal_perror("getpagesize");
    }

    // allocate at least enough space for the code + 1 byte
    // (so that there will be at least one INT3 - see below),
    // rounded up to a multiple of the system page size.
    size_t rounded_codesize = ((codelen + 1 + pagesize - 1)
                               / pagesize) * pagesize;

    void *executable_area = mmap(0, rounded_codesize,
                                 PROT_READ|PROT_WRITE,
                                 MAP_PRIVATE|MAP_ANONYMOUS,
                                 -1, 0);
    if (!executable_area) fatal_perror("mmap");

    // at this point, executable_area points to memory that is writable but
    // *not* executable.  load the code into it.
    memcpy(executable_area, code, codelen);

    // fill the space at the end with INT3 instructions, to guarantee
    // a prompt crash if the generated code runs off the end.
    // must change this if generating code for non-x86.
    memset(executable_area + codelen, 0xCC, rounded_codesize - codelen);

    // make executable_area actually executable (and unwritable)
    if (mprotect(executable_area, rounded_codesize, PROT_READ|PROT_EXEC))
        fatal_perror("mprotect");

    // now we can call it. passing arguments / receiving return values
    // is left as an exercise (consult libffi source code for clues).
    ((void (*)(void)) executable_area)();

    munmap(executable_area, rounded_codesize);
}

您可能会看到此代码与cherrydt's answer 中显示的 Windows 代码几乎相同。只有系统调用的名称和参数不同。

在使用这样的代码时,重要的是要知道许多现代操作系统不允许您拥有一个同时可写和可执行的 RAM 页。如果我在对mmapmprotect 的调用中写了PROT_READ|PROT_WRITE|PROT_EXEC,它将失败。这称为W^X policy;首字母缩写词代表 Write XOR eXecute。它originates with OpenBSD,其想法是让缓冲区溢出漏洞更难将代码写入 RAM 然后执行它。 (这仍然可能,漏洞利用只需要find a way to make an appropriate call to mprotect first。)

【讨论】:

  • 在 GNU C 中,在将字节存储为数据之后,在将它们作为代码调用之前,最好使用 __builtin___clear_cache(executable_area, executable_area+rounded_codesize)。这将防止死存储消除(并在某些非 x86 ISA 上对非连贯 I-cache 进行任何必要的同步)。在 x86 上使用 mmap(不是 malloc)将阻止 GCC 知道存储不是被读取的作为数据,因此您可以在没有它的情况下逃脱。但它编译为零指令,是一个好主意。 see thismy answer here
【解决方案2】:

取决于平台。

对于 Windows,您可以使用以下代码:

// Allocate some memory as readable+writable
// TODO: Check return value for error
LPVOID memPtr = VirtualAlloc(NULL, sizeof(myarr), MEM_COMMIT, PAGE_READWRITE);

// Copy data
memcpy(memPtr, myarr, sizeof(myarr);

// Change memory protection to readable+executable
// Again, TODO: Error checking
DWORD oldProtection; // Not used but required for the function
VirtualProtect(memPtr, sizeof(myarr), PAGE_EXECUTE_READ, &oldProtection);    

// Assign and call the function
(void (*)()) myfunc = (void (*)()) memPtr;
myfunc();

// Free the memory
VirtualFree(memPtr, 0, MEM_RELEASE);

此代码假定 myarr 数组与您的问题代码中一样,并且假定 sizeof 将对其起作用,即它具有直接定义的大小,而不仅仅是从其他地方传递的指针。如果是后者,则必须以其他方式指定大小。

请注意,如果您想知道,这里有两种可能的“简化”,但我建议不要这样做:

1) 您可以使用PAGE_EXECUTE_READWRITE 调用VirtualAlloc,但这通常是一种不好的做法,因为它会为不需要的代码执行打开一个攻击向量。

2) 您可以直接在&myarr 上调用VirtualProtect,但这只会在您的内存可执行文件中生成一个随机页面,该页面恰好包含您的数组可执行文件,这比#1 更糟糕,因为可能还有其他数据在这个页面中,现在也可以突然执行了。

对于 Linux,我在 Google 上找到了this,但我对此了解不多。

【讨论】:

  • @ninjalj 我希望你意识到这是一个关于如何攻击 SELinux 的页面。我们不需要人们将 SELinux 视为障碍。请参阅zwol's answer 了解正确的做法。
  • @user6292850:不正确,这是以前的 glibc 维护者 Ulrich Drepper 关于如何执行动态生成的代码的页面。它不能用作攻击,要么您已经可以执行任意代码,并且不需要这样做,要么您不这样做,您将无法执行。
  • @ninjalj 我认为“保护测试”页面的标题清楚地表明它的用处很小。我从来没有声称它是由黑帽写的。一个不想给自己(和他的用户)开枪的程序员根本不会试图颠覆 SELinux。
  • @user6292850:这不是要颠覆 SELinux,而是要安全地执行动态生成的代码(没有同时使用 Write 和 eXec 权限的映射),以及使用诸如 SELinux 的 execmem 和 PAX 的 W 之类的东西^X.
【解决方案3】:

非常依赖于操作系统:并非所有操作系统都会故意(阅读:没有错误)允许您在数据段中执行代码。 DOS会因为它运行在Real Mode,Linux也可以拥有相应的权限。我不了解 Windows。

铸造通常是未定义的,并且有自己的注意事项,因此这里对这个主题进行了一些详细说明。来自 C11 标准草案 N1570,§J.5.7/1:

指向对象或void 的指针可能 转换为指向函数的指针,允许将数据作为 函数 (6.5.4)。

(已添加格式。)

所以,它完全没问题,应该可以按预期工作。当然,您需要遵守 ABI 的调用约定。

【讨论】:

  • @Downvoter 介意解释一下吗?
最近更新 更多