函数指针的 memcpy 导致了段错误

3

我知道我可以通过引用复制函数,但我想了解以下代码中导致段错误的原因。

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

int return0()
{
    return 0;
}

int main()
{
    int (*r0c)(void) = malloc(100);
    memcpy(r0c, return0, 100);
    printf("Address of r0c is: %x\n", r0c);
    printf("copied is: %d\n", (*r0c)());
    return 0;
}

这是我认为应该奏效的心理模型。
进程拥有r0c分配的内存。我们从相应于return0的数据段中复制数据,复制成功。
我认为解引用函数指针与调用函数指针所指向的数据段相同。如果是这样的话,那么指令指针应该移动到相应于r0c的数据段,其中将包含函数return0的指令。对应于return0的二进制代码不包含任何跳转或函数调用,这些都取决于return0的地址,因此它应该只返回0并恢复ip... 100字节当然足够存储函数指针,而0xc3也在r0c的范围内(它位于第11个字节)。
那么为什么会出现分段错误呢?这是C函数指针语义的误解,还是有一些我不知道的防止自修改代码的安全功能?

printf("Address of r0c is: %x\n", r0c); 的定义不明确。 - chux - Reinstate Monica
3
有什么东西告诉我整件事情没有被定义清楚。 - HolyBlackCat
3
首先,函数(代码)存放在被标记为可执行的内存段中。分配的数据没有被标记,特别是如果您的系统正在使用DEP(数据执行防止)。如果您希望执行位于数据段中的代码,您需要找出如何将该数据标记为可执行。其次,“memcpy(r0c, return0, 100)”可能从内存末尾以外的位置开始复制。第三,包含代码的内存位置很可能受到访问保护。 - GreatAndPowerfulOz
2
使用gcc编译时加上参数-Wall -Wpedantic -std=c11 会产生几个警告,请听从你的编译器。 - Random Davis
1
“memcpy(r0c, return0, 100);” 是一个问题,因为“return0”不能很好地转换为“void*”。 - chux - Reinstate Monica
4个回答

6
malloc用于分配内存的页面不被标记为可执行。你不能将代码复制到堆中并期望它能运行。
如果你想做这样的事情,你需要深入操作系统,并自己分配页面。然后你需要将它们标记为可执行。你很可能需要管理员权限才能设置内存页面上的可执行标志。
而且这非常危险。如果你在分发的程序中这样做,并且有某种缺陷让攻击者使用我们的程序写入那些已分配的内存页面,那么攻击者可以获得管理员权限并控制计算机。
此外,你的代码还存在其他问题,例如函数指针可能无法在所有平台上正确转换为通用指针。预测或以其他方式获取函数大小非常困难(更别说不标准)。你的代码示例中也打印出指针错误。(使用“%p”格式来打印void *,需要将指针强制转换为void *)。
还有当你声明一个如int fun()的函数时,这与声明一个不带参数的函数不同。如果你想声明一个没有参数的函数,你应该明确使用void,如int fun(void)

我只是试图提供一个最小的例子...这只是为了理解程序的语义。 - A.S
在大多数操作系统中,将页面设置为可执行文件不需要管理员权限。 - Daniel

2
标准规定:

memcpy函数将从s2指向的对象中复制n个字符到s1指向的对象中。

[C2011, 7.24.2.1/2; 强调添加]

在标准的术语中,函数不属于“对象”。标准没有定义源指针指向函数的情况下应该采取的行为,因此这样的memcpy()调用会产生未定义的行为。

此外,malloc()返回的指针是一个对象指针。C语言不提供将对象指针直接转换为函数指针的方法,也不提供将对象作为函数调用的方法。可以通过中间整数值在对象指针和函数指针之间进行转换,但这样做的影响至少是双重实现定义的。在某些情况下,这是未定义的。

与其他情况一样,未定义行为可能恰好是您所希望的行为,但依赖此行为是不安全的。在这种特殊情况下,其他答案提供了不期望获得所期望行为的充分理由。


这一切都是真的,但GCC很宽松,结果发现这并没有导致段错误。 - A.S
@AndrewSalmon,相反地——段错误总是源于实现定义或未定义的行为,我正在指出您程序中导致这种行为的确切操作。您可能可以依赖实现扩展来获得所需的行为,但这样做本质上是不可移植的。尽管如此,这对您可能是可以接受的。 - John Bollinger

0
正如一些评论中所说,您需要使数据可执行。这需要与操作系统通信以更改数据的保护。在Linux上,这是系统调用int mprotect(void* addr, size_t len, int prot)(请参见http://man7.org/linux/man-pages/man2/mprotect.2.html)。
以下是使用VirtualProtect的Windows解决方案。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#ifdef _WIN32
#include <Windows.h>
#endif

int return0()
{
    return 0;
}

int main()
{
    int (*r0c)(void) = malloc(100);
    memcpy((void*) r0c, (void*) return0, 100);
    printf("Address of r0c is: %p\n", (void*) r0c);
#ifdef _WIN32
    long unsigned int out_protect;
    if(!VirtualProtect((void*) r0c, 100, PAGE_EXECUTE_READWRITE, &out_protect)){
        puts("Failed to mark r0c as executable");
        exit(1);
    }
#endif
    printf("copied is: %d\n", (*r0c)());
    return 0;
}

它有效。


-1

Malloc返回一个指向已分配内存的指针(在您的情况下为100字节)。这个内存区域是未初始化的;假设该内存可以被CPU执行,为了使您的代码工作,您需要用函数实现的可执行指令填充这100个字节(如果确实可以在100个字节中容纳)。但正如已经指出的那样,您的分配位于堆上,而不是文本(程序)段中,我认为它不能作为指令执行。也许这会实现您想要的效果:

int return0()
{
    return 0;
}

typedef int (*r0c)(void);

int main(void)
{
    r0c pf = return0;
    printf("Address of r0c is: %x\n", pf);
    printf("copied is: %d\n", pf());
    return 0;
}

我感谢您的回答,但是我已经解释过我知道可以通过引用调用函数;只是我想知道是否/如何使用函数指针执行实际数据,而不仅仅是使用函数指针来进行引用调用。 - A.S

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接