繁体   English   中英

是否可以通过复制函数指针指向的数据来在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语句创建一个数据块,而不是代码

即使在那里有说明,当您尝试调用它时,您也可能会得到DEP(数据执行保护)异常。

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

函数的大小

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

然后,“函数的字节大小”的一个主要问题是其定义。 在大多数情况下,函数是独立的代码块,但是没有什么可以阻止聪明的编译器将多个函数的相同部分合并到一个不同的位置并在那里简单地进行jmp 在这种情况下,目标函数将是不连续的,并且其大小的概念将变得模棱两可。

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

获取函数的连续字节大小的一种方法(依赖于未指定的行为)是从要复制的函数的地址中减去下一个函数的地址。 如果编译器/链接器没有重新排列它们,则应该得到所需的内容。 但是,这是一个很大的“ if”,尤其是在大型系统上工作时。 另外,它依赖于将函数指针强制转换为整数,这与将“普通”指针强制转换为整数不同且风险更大(例如,某些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平台上,您需要使用具有PROT_EXEC保护的mmap (可能在其中写入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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM