我可以通过复制函数指针指向的数据来移动 C 中的函数吗?

Can I move a function in C by copying the data a function pointer points to?

我写过这段代码:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void hello(){
        puts("hey");
}

int main(){

        char* helloCpy = (char*)malloc(sizeof(*hello));

        strcpy(helloCpy, (char*)&hello);
        void (*helloCpyPtr)() = (void (*)()) helloCpy;

        hello();
        helloCpyPtr();

        return 0;
}

我正在努力:

  1. 获取函数指针。
  2. 分配函数大小的内存。
  3. 将函数复制到内存中。
  4. 将复制的内存转换为函数指针。
  5. 调用函数的副本。

一切正常,直到我调用 "helloCpyPtr()"。在这一点上,我遇到了段错误。

如果我想做的事情是不可能的,我也不会感到惊讶。如果不可能,我很想知道为什么不可能。

如果不是不可能的话,有人知道我在这里做错了什么吗?

谢谢堆栈溢出。

我希望 sizeof(*hello) 不是整个函数的大小,而是函数指针的大小(可能是 4 个字节)。

我知道没有办法获得整个函数的大小,所以你的建议是不可能的。

其他复杂情况是许多主要的现代操作系统不允许程序从作为数据存储器创建的代码中执行代码。您的 malloc 语句创建了一个 data 块,而不是 code

即使您得到了其中的说明,您在尝试调用它时也可能会遇到 DEP (Data Execution Prevention) 异常。

您的方法存在多个问题(并且您通过不告诉哪个是您的目标平台而使这变得更加困难)。也就是说,虽然可以在运行时创建可执行代码,但这并不一定意味着哑字节副本将始终有效。

函数的大小

首先,strcpy 是个坏主意。您的函数可能包含空字节,并且您的函数很可能不会被空字节终止(ret 在 x86 上是 0xc3)。

然后,"byte size of a function" 的一个主要问题是它的定义。在大多数情况下,函数是独立的代码块,但没有什么可以阻止聪明的编译器将多个函数的相同部分合并到一个不同的位置,并简单地 jmp 在那里。在这种情况下,目标函数将是不连续的,其大小的概念将变得不明确。

正如 abelenky 在他的回答中正确怀疑的那样,标准说 (C11, 6.5.3.4./1) "the sizeof operator shall not be applied to an expression that has a function type"。据我所知,这并不意味着在任何事情都可能发生的意义上这样做就是 UB,但这确实意味着您不能期望它在所有情况下都按照您的想法行事。 GCC 和 Clang 将其评估为 1 并发出警告; Visual Studio,IIRC,将 return 函数的连续字节大小。

获取函数的连续字节大小的一种方法(依赖于未指定的行为)是从您要复制的函数的地址中减去下一个函数的地址。 如果 compiler/linker 没有饲养运行它们,你应该得到你想要的。但是,这是一个相当大的 "if",尤其是当您在大型系统上工作时。此外,它依赖于将函数指针转换为整数,这与将 "normal" 指针转换为整数不同且风险更高(例如,某些 ABI,如大多数 PowerPC ABI,需要的不仅仅是代码指针来定义函数指针) .除了实验目的,我不会这样做。

void test()
{
    // copy me
}

void test_end()
{
}

int main()
{
    size_t testSize = (intptr_t)test_end - (intptr_t)test;
}

可重定位代码

并非所有代码都可以 运行 来自内存中的任何位置。指定相对于当前执行代码的内存地址的代码无法复制到任何地方。 x86_64 有一种称为 "RIP-relative" 的寻址模式,您可以在其中获取已执行指令的地址并为其添加偏移量。 ARM 有一个等效(但名称不同)的模式,并广泛使用它。这可用于访问全局变量或全局符号。

此外,在大多数平台上,对程序中声明的符号的大多数调用和跳转都使用指令地址相对寻址。例如,如果 test 在我之前的示例中调用了 test_end,您将得到类似 call +3 的内容(假设 test_end 在内存中为 3 个字节)。

这些技术可以安全地将您的程序作为一个整体移动到内存中的任何位置,但如果您只复制部分程序,则会失败。再次以 call +3 为例,如果您仅复制 test 并执行它,您的程序将在尝试使用 test_end 时崩溃,因为您没有复制它。

这意味着您必须格外小心您在计划手动重定位的函数中编写的内容。

可执行内存

abelenky 也正确指出,现代平台将拒绝执行内存 那没有被标记为可执行文件。这是一项安全功能,而且非常有用。但是,这意味着您需要经过特定的环节才能分配可执行内存。 malloc 不分配可执行内存。

在 POSIX 平台上,您需要使用 mmapPROT_EXEC 保护(并且可能 PROT_WRITE 在那里写入)来分配可执行内存。在 Windows 上,您需要使用 VirtualAlloc。我不记得这些标志了,但文档应该不难找到。

整个过程

一个更简单的方法是使用汇编语言手工制作您需要复制的函数,并确保它不使用指令地址相对寻址。然后你可以在内存中的任何地方复制 this 函数,你的过程的其余部分大部分是正确的:一旦分配了内存并复制了可执行代码,机会是(取决于你的平台;它适用于 x86,我相信它也适用于 ARM),您可以将此内存转换为函数指针并调用它。这是一个例子。

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

/* assembly code to run execve("/bin/sh") on an x86_64 Linux:
    // push '/bin///sh\x00'
    push 0x68
    mov rax, 0x732f2f2f6e69622f
    push rax

    // call execve('rsp', 0, 0)
    mov rdi, rsp
    xor esi, esi
    push 0x3b
    pop rax
    cdq // Set rdx to 0, rax is known to be positive
    syscall
*/
unsigned char executableCode[] = {
    0x6A, 0x68, 0x48, 0xB8, 0x2F, 0x62, 0x69, 0x6E, 0x2F, 0x2F, 0x2F, 0x73,
    0x50, 0x48, 0x89, 0xE7, 0x31, 0xF6, 0x6A, 0x3B, 0x58, 0x99, 0x0F, 0x05, 
};

int main()
{
    void* memory = mmap(NULL, 0x1000, PROT_WRITE | PROT_EXEC, MAP_ANON | MAP_PRIVATE, -1, 0);
    memcpy(memory, executableCode, sizeof executableCode);
    void (*start_shell)() = (void (*)())memory;
    start_shell();
}

摘自 shellcraft 的汇编代码。

如您所见,我没有复制现有函数,而是直接使用了本机代码。