【问题标题】:Memcpy with function pointers leads to a segfault带有函数指针的 Memcpy 会导致段错误
【发布时间】:2016-07-18 20:56:54
【问题描述】:

我知道我可以通过引用复制该函数,但我想了解以下产生段错误的代码中发生了什么。

#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); 定义不明确。
  • 某事告诉我,整个事情没有很好地定义。
  • 首先,函数(代码)驻留在标记为可执行的内存段中。分配的数据没有如此标记,特别是如果您的系统正在使用 DEP(数据执行保护)。如果您希望执行数据段中的代码,您需要弄清楚如何将该数据标记为可执行。其次,memcpy(r0c, return0, 100); 可能是从内存末尾以外的地方进行复制。第三,很可能包含代码的内存位置受到保护,无法访问。
  • 用 gcc 和参数 -Wall -Wpedantic -std=c11 编译它会给出几个警告。听听你的编译器。
  • memcpy(r0c, return0, 100); 是一个问题,因为return0 不能很好地转换为void*

标签: c pointers self-modifying


【解决方案1】:

malloc 用于分配内存的内存页面未标记为可执行。您不能将代码复制到堆中并期望它运行。

如果你想做这样的事情,你必须深入操作系统,并自己分配页面。然后你需要将它们标记为可执行文件。您很可能需要管理员权限才能在内存页上设置可执行标志。

而且真的很危险。如果您在您分发的程序中执行此操作并且存在某种错误,使攻击者可以使用我们的程序写入那些分配的内存页面,那么攻击者可以获得管理员权限并控制计算机。


您的代码还存在其他问题,例如指向函数的指针可能无法很好地转换为所有平台上的通用指针。预测或以其他方式获取函数的大小非常困难(更不用说非标准)。您还在代码示例中打印出错误的指针。 (使用"%p" 格式打印void *,需要将指针转换为void *)。

此外,当您声明像 int fun() 这样的函数时,这与声明不带参数的函数不同。如果你想声明一个不带参数的函数,你应该像int fun(void)一样显式使用void

【讨论】:

  • 我只是想提供一个最小的例子......这只是关于理解程序的语义。
  • 在大多数操作系统上将页面设置为可执行文件不需要管理员权限。
【解决方案2】:

标准说:

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

[C2011, 7.24.2.1/2;重点补充]

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

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

在其他情况下,UB 可以完全符合您的期望,但依赖它是不安全的。在这种特殊情况下,其他答案提供了很好的理由期望得到您希望的行为。

【讨论】:

  • 这一切都是真的,但 GCC 是松懈的,结果证明这不是导致 segfault 的原因。
  • @AndrewSalmon,恰恰相反 - 段错误 总是 由实现定义或未定义的行为引起,我准确指出哪些操作会在您的程序。您可以依靠实现扩展来获得您想要的行为,但这样做本质上是不可移植的。不过,这对您来说可能是可以接受的。
【解决方案3】:

正如在某些 cmets 中所说,您需要使数据可执行。这需要与操作系统通信以更改对数据的保护。在 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;
}

而且它有效。

【讨论】:

    【解决方案4】:

    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;
    }
    

    【讨论】:

    • 感谢您的回答,但我解释说我知道可以通过引用调用该函数;只是我想知道是否/如何使用函数指针来执行实际数据,而不仅仅是使用函数指针通过引用来调用。
    猜你喜欢
    • 2014-12-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-11-11
    • 2011-07-03
    • 1970-01-01
    • 2021-05-08
    • 1970-01-01
    相关资源
    最近更新 更多