如果我有这样的功能
void check(char *buffer)
{
//some stuff that only needs to be run once
}
我有什么办法可以删除'我打电话一次后从记忆中得到这个功能吗?
我可以轻松地在纯ASM中执行此操作。但我想知道是否有更标准的方法。
答案 0 :(得分:8)
C ++标准(甚至是C标准)认为程序代码的总体是不可变的。所以"删除"是没有意义的。一个功能。
但是,让我们务实。大多数情况下,您的C ++程序被编译成在某些操作系统中运行的某些executable(我假设操作系统是Linux,但您可以根据其他操作系统调整我的答案)。有关操作系统的一般概述,阅读Operating Systems: Three Easy Pieces (可免费下载)
然后,当你的程序开始时(可能是你的shell在某些fork(2)中执行execve(2)然后process),会创建一个新的virtual address space(并进行管理)由内核配置MMU并通过page faults处理paging适当的数据,例如来自磁盘)。
通常,程序的ELF可执行文件包含code segment,它在虚拟地址空间的某些只读和可执行段中进行内存映射。由于该部分是只读的,因此您无法删除"或"写"它。使用shared libraries,虚拟地址空间更复杂,并且包含几个代码段(每个库可能有一个或多个)。
某些(现代)操作系统上运行的程序我可以轻松地在纯ASM中执行此操作。
错误。由于代码段是只读的,因此您需要更改虚拟地址空间(仅 方式)通过适当的系统调用,例如mmap
,munmap
,mprotect
;您可以在C ++,C或甚至ASM中调用它们。某些功能已在汇编程序中编码的事实不会改变这一点。甚至汇编程序代码也不能覆盖代码段(在这种情况下你仍会得到一些segmentation fault),而不会改变你的虚拟地址空间。
你可以玩低级技巧,例如使用munmap(2)系统调用 - 来自某些C ++代码或C代码或汇编代码 - 来改变你的虚拟地址空间并从中删除一些页面。我强烈建议不这样做。但是,如果你走这条路,你需要了解ELF可执行文件和共享对象以及虚拟地址空间的所有细节。请注意ASLR,elf(5),ld-linux(8),vdso(7)。使用objdump(1)& readelf(1)&请参阅ld(1)以了解ELF可执行文件的详细信息。
您可以将(check
和其他几个函数的代码)放入某些plugin中,但在您的情况下,您可能不应该这样做。在这种情况下,你可以dynamically load插件然后卸载它。您将使用dlopen(3)加载插件,并dlclose
将其卸载。插件的dlopen
会执行几个 mmap(2)(因此增加虚拟地址空间)并处理relocations,其dlclose
执行多个/proc/
{3}}(缩小虚拟地址空间)。
在Linux上使用cat /proc/self/maps
(请参阅munmap(2))。在终端中尝试cat /proc/$$/maps
和cat /proc/$$/status
以及/proc/self/maps
。并从您的C ++程序中读取wc /proc/4735/maps
(例如,请参阅proc(5))。
例如,this是用C ++编码的shell。它现在在我的桌面上运行pid 4735。它的虚拟地址空间有69个段,与556dad571000-556dad697000 r-xp 00000000 08:01 11272485 /usr/bin/fish
一样。它的代码段被映射(只读和可执行):
7fa96bcb3000-7fa96be4c000 r-xp 00000000 08:01 1704209 /lib/x86_64-linux-gnu/libc-2.25.so
我的C标准库的代码段可能在
ValueTuple
我不了解您的安全问题(仅在评论中提及,这应该包含在问题中),因为您的代码段是只读。实际上,恶意函数可以改变某些代码段的保护(使用fish)然后改变它。但是你需要解释代码注入(恶意函数)是如何发生的(你可以编写一个依赖于mprotect(2)的程序并用process isolation进行验证以避免这种情况发生,假设你的Linux内核是值得信赖的。)
也许您有隐私问题(但这会产生不同的问题)。显然,您不希望在代码中将密码保留为明文。
可能(但不太可能)您正在编写独立程序(没有任何操作系统),例如对于嵌入式系统及其微控制器(例如formal methods)。在这种情况下,您的代码在闪存中。如何更改或删除它将变为特定于硬件。顺便说一下,多次覆盖闪存会损害硬件。
答案 1 :(得分:1)
你可以这样做,但会有轻微的性能影响(如果你想只调用一次该功能则无关紧要):
在.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代码。