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

时间:2015-05-29 21:44:43

标签: c function-pointers

我已经写了这段代码:

#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. 调用该函数的副本。
  6. 在我调用&#34; helloCpyPtr()&#34;之前一切正常。此时,我遇到了一个段错误。

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

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

    谢谢堆栈溢出。

2 个答案:

答案 0 :(得分:3)

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

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

其他复杂情况是,许多主要的现代操作系统都不会让程序从创建的数据内存中执行代码。您的malloc语句会创建一个 数据 ,而不是 代码

即使您收到了相关说明,当您尝试调用时,也可能会遇到DEP (Data Execution Prevention)异常。

答案 1 :(得分:2)

您的方法存在多个问题(并且通过不告知哪个是您的目标平台,您将更加努力)。也就是说,尽管可以在运行时创建可执行代码,但它并不意味着哑字节副本将始终有效。

函数的大小

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

然后,函数&#34;字节大小的一个主要问题是&#34;是它的定义。在大多数情况下,函数是自包含的代码块,但没有什么可以阻止聪明的编译器将多个函数的相同部分合并到一个不同的位置,而只是jmp。在这种情况下,目标函数将是非连续的,其大小的概念将变得模糊。

由于在答案中正确怀疑abelenky,标准说(C11,6.5.3.4./1)&#34; sizeof运算符不应用于具有函数类型&#34的表达式;。据我所知,这并不意味着这样做是UB,因为任何事情都可能发生,但它确实意味着你无法在任何情况下做到你认为它做的事情。 GCC和Clang将其评估为1并发出警告; Visual Studio IIRC将返回函数的连续字节大小。

获取函数的连续字节大小的一种方法(依赖于未指定的行为)是从要复制的函数的地址中减去下一个函数的地址。 如果编译器/链接器没有重新排列它们,你应该得到你想要的。但是,这是一个相当大的问题,特别是如果你正在大型系统上工作的话。另外,它依赖于对整数的转换函数指针,这种指针与铸造&#34; normal&#34;不同且风险更大。指向整数的指针(例如,一些ABI,像大多数PowerPC ABI一样,需要的不仅仅是用于定义函数指针的代码指针)。除了实验目的,我不会做任何事情。

void test()
{
    // copy me
}

void test_end()
{
}

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

可重定位代码

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

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

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

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

可执行内存

正如abelenky也正确指出的那样,现代平台将拒绝执行记忆 没有标记可执行文件。这是一个安全功能,非常有用。但是,这意味着您需要通过特定的箍来分配可执行内存。 malloc不分配可执行内存。

在POSIX平台上,您需要使用mmap PROT_EXEC保护(可能PROT_WRITE写入)来分配可执行内存。在Windows上,您需要使用VirtualAlloc。我不记得那些旗帜,但文件不应该太难找到。

整个过程

更简单的方法是手工制作需要使用汇编语言复制的函数,并确保它不使用指令地址相对寻址。然后,您可以在内存中的任何位置复制函数,其余的过程大部分都是正确的:一旦分配了内存并复制了可执行代码,很可能(取决于您的平台;它可以工作) 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

正如您所看到的,我使用的是直接的本机代码,而不是复制现有的函数。