【问题标题】:C++ Delete A FunctionC++ 删除一个函数
【发布时间】:2017-11-22 05:48:39
【问题描述】:

如果我有这样的功能

void check(char *buffer)
{
    //some stuff that only needs to be run once
}

有什么方法可以在我调用一次后从内存中“删除”这个函数?

我可以轻松地在纯 ASM 中执行此操作。但我想知道是否有更标准的方法。

【问题讨论】:

标签: c++ memory


【解决方案1】:

C++ 标准(甚至是 C 标准)认为程序代码的整体是不可变的。所以“删除”一个函数是没有意义的。

但让我们务实一点。大多数情况下,您的 C++ 程序被编译成在某些操作系统中运行的 executable(我假设操作系统是 Linux,但您可以将我的答案调整为其他操作系统)。有关操作系统的一般概述,阅读Operating Systems: Three Easy Pieces(可免费下载)

然后,当您的程序在某些process 中启动时(可能是通过您的shell 执行fork(2) 然后是execve(2)),会创建一个新的virtual address space(并由配置@987654327 的内核管理) @ 并通过paging 处理page faults 适当的数据,例如来自磁盘)。

一般来说,您的程序的ELF 可执行文件包含一个code segment,它在您的虚拟地址空间的一些只读和可执行段中进行内存映射。由于该部分是只读的,因此您不能“删除”或“写入”它。使用shared libraries,虚拟地址空间更加复杂,包含几个代码段(每个库可能一个或多个)。

我可以轻松地在纯 ASM 中执行此操作。

错误对于在某些(现代)操作系统上运行的程序。因为代码段是只读的,所以你需要改变你的虚拟地址空间(only方法是通过适当的系统调用,如mmap,@ 987654356@、mprotect;您可以在 C++、C 甚至 ASM 中调用它们)。某些功能已在汇编程序中编码的事实不会改变这一点。即使是汇编代码也无法覆盖代码段(在这种情况下,您仍然会得到一些segmentation fault)而不改变您的虚拟地址空间。

您可以玩一些低级技巧,例如使用munmap(2) 系统调用(来自某些 C++ 代码或 C 代码或汇编代码)来更改您的虚拟地址空间并从中删除一些页面。我强烈建议不要这样做。但是如果你走这条路,你需要了解你的 ELF 可执行文件和共享对象以及你的虚拟地址空间的所有细节。请注意ASLRelf(5)ld-linux(8)vdso(7)。使用 objdump(1)readelf(1) 并查看 ld(1) 了解您的 ELF 可执行文件的详细信息。

您可以将您的代码(您的check 和其他几个函数)放入一些plugin,但在您的情况下,您可能不应该这样做。在这种情况下,您可以dynamically load 该插件然后卸载它。您将使用dlopen(3) 加载插件,并使用dlclose 卸载它。插件的dlopen-ing 执行几个 mmap(2)(因此增加了虚拟地址空间)并处理relocations,它的dlclose 执行了几个munmap(2)(因此缩小了虚拟地址空间)。

在 Linux 上使用/proc/(请参阅proc(5))。在终端中尝试 cat /proc/self/mapscat /proc/$$/mapscat /proc/$$/status。并从您的 C++ 程序中读取/proc/self/maps(例如,参见this)。

例如,fish 是一个用 C++ 编码的 shell。它现在在我的桌面上以 pid 4735 的进程运行。它的虚拟地址空间有 69 个段,以wc /proc/4735/maps 计算。它的代码段被映射(只读和可执行):

556dad571000-556dad697000 r-xp 00000000 08:01 11272485  /usr/bin/fish

我的 C 标准库的代码段大概在

7fa96bcb3000-7fa96be4c000 r-xp 00000000 08:01 1704209  /lib/x86_64-linux-gnu/libc-2.25.so

我不了解您的安全问题(仅在评论中提到,应该进入问题),因为您的代码段是只读。实际上,恶意函数可以更改某些代码段的保护(使用mprotect(2)),然后对其进行更改。但是您首先需要解释(该恶意函数的)代码注入是如何发生的(您可以编写一个依赖于process isolation 并用formal methods 证明的程序来避免这种情况发生,假设您的 Linux 内核是可信任的) .

也许您有隐私方面的顾虑(但这是一个不同的问题)。显然,您不想在代码中以明文形式保留密码。


也许(但不太可能)您正在编写一个独立程序(没有任何操作系统),例如对于嵌入式系统及其微控制器(例如Arduino like)。在这种情况下,您的代码在闪存中。以及如何更改或删除它成为硬件特定的。顺便说一句,多次覆盖闪存会损害硬件。

【讨论】:

  • 您也可以使用mprotect 取消对页面的保护,并使用nop 将函数体覆盖到最终的ret。这避免了取消映射整个页面的需要。
  • 是的,但您为什么要这样做?默认情况下,代码段是只读的!
  • 您还会解决 OP 对安全性的担忧吗?比如,你是否担心。
  • @BasileStarynkevitch 为什么你会说我在 ASM 中轻松做到这一点是错误的?它非常简单,我用 nop 替换 sub 中的所有指令。这对我正在做的事情很好。我在问这个问题,看看 C++ 中是否有办法做到这一点。我知道你不能。无论哪种方式,感谢您提供有用信息的长时间回复。
  • 因为 ASM 中的编码不会改变您的虚拟地址空间和代码段的只读属性。更改虚拟地址空间的唯一方法是使用相关的系统调用(mmapmprotectmunmap)。 ASM 与它无关(即使您显然可以在 ASM 中进行系统调用)
【解决方案2】:

您可以这样做,但对性能有轻微影响(如果您只想调用该函数一次,则无关紧要):

在 .c 文件中:

static void check_private(char *buffer)
{
    //some stuff that only needs to be run once
    check = NULL; // disable calling this ever again
}

// declare check as pointer to function (pointer to char) returning void
// and intialize to check_private
void (*check)(char*) = check_private;

在对应的.h文件中:

extern void (*check)(char*);

只要你去掉调试符号,并且不要将它存储在程序的其他地方,check 指针将是唯一存储check_private 地址的地方,所以一旦你覆盖它,它就消失了。


NULL 是一个不错的禁用值,如果您知道自己在具有内存保护的普通操作系统上运行,因为它将产生的 UB 将是段错误,并且很容易检查。

如果你不想要段错误,你可以将它设置为指向另一个函数,例如:

static void check_disabled(char *buffer)
{
    (void)buffer; // disable compilation warning about unused parameter
    abort(); // maybe this
    exit(EXIT_FAILURE); // or this
    return; // or this for no-op
    for(;;); // or even this on some tiny embedded system
}

如果这是一个库,并且您想要某种基本的硬保护以防止无效使用,那么您可以使用以下导出函数来代替指针:

void check(char *buffer)
{
    static int called = 0;
    if (!called) {
        called = 1;
        check_private(buffer);
    } else {
        // called 2nd time, do something, see above for suggestions
    }
}

同样,只要去掉调试符号,只有called变量的地址存储在这个函数的代码中,所以没有办法访问它。好吧,除了调用 UB 以某种方式猜测它的地址并使用指针来修改它,但特别是对于现代优化编译器,这真的是 UB 而不仅仅是 hack,因为不能保证汇编和数据编译器实际为这个 C 代码生成什么.

【讨论】:

  • check 和/或check_private 的初始代码(通常)仍在代码段中。所以我觉得这个答案并没有解决 OP 的(令人困惑的)问题
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2015-10-16
  • 2018-11-12
  • 2011-01-30
  • 2012-05-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多