【问题标题】:Copy function from user space to kernel and execute将函数从用户空间复制到内核并执行
【发布时间】:2026-01-18 16:00:02
【问题描述】:

首先,我这样做是为了好玩,所以不要评判我。

我所做的是将函数指针从用户空间传递到内核,使用 copy_from_user 将函数体复制到内核中的静态数组并开始跳转到该数组中执行。

在内核中:

static char handler_text[PAGE_SIZE] __page_aligned_data;
copy_from_user((void *)handler_text , (const void __user *)my_handler , PAGE_SIZE);
((void (*)())(handler_text))();

在用户空间,这个函数的作用很简单,如下

void my_handler(){
volatile unsigned long * p = (volatile unsigned long *)0xF0000c10;
*p = 0x0000000;
}

10000938 <my_handler>: 
10000938:   3d 20 f0 00     lis     r9,-4096 
1000093c:   39 40 00 00     li      r10,0 
10000940:   61 29 0c 10     ori     r9,r9,3088 
10000944:   91 49 00 00     stw     r10,0(r9) 
10000948:   4e 80 00 20     blr 
1000094c:   00 01 88 08     .long 0x18808

问题是我第一次这样做总是会产生一个糟糕的问题。但是我第二次这样做之后,问题就消失了,不再出现 Oops。 通过读取内存,我可以清楚地看到该函数是由内核执行的。我正在运行一个 PowerPc 目标,所以糟糕显示异常是 700,这是程序异常。从 Oops 中,我可以看到指令转储,其中 nip(之后)与 my_handler 完全相同。

Instruction dump:
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
00000000 00000000 00000000 00000000 <3d20f000> 39400000 61290c10 91490000

我想不出任何意义。任何人都可以吗?谢谢

【问题讨论】:

  • 如果我可以将图片放入 cmets,我会放入 this
  • 你能提供更多的上下文吗?内核代码是自定义系统调用,即如何从用户空间调用它?你能提供内核oops的完整信息吗?哪种 Powerpc 型号?

标签: c linux linux-kernel


【解决方案1】:

我不想阻止一个令人钦佩的想法,但如果没有一些严肃的额外工作,你想要做的事情即使不是不可能也很困难。

您的函数链接在 user 空间中的位置 F。您将其复制到静态数组位置的内核空间:AA 可能在内核的数据部分,因此可能无法执行。此外,您的函数链接在错误位置(例如F != A)。

此外,即使您的函数可以链接到正确的位置 A,您如何处理其中的符号重定位(例如,如果它调用 printk,您如何重新链接函数内的地址以匹配实际printk 地址)?

创建一个内核模块并加载它(通过modprobe)要容易得多,你可以做任何你想做的事情。

旁注:这是一个巨大的安全漏洞。 “Stuxnet”蠕虫使用了一个类似的蠕虫来渗透 Windows。


更新:

转储发生 [in time] 很长时间后 异常事件。到那时,它有正确的数据,所以转储显示当前状态,可以这么说,但不是在有问题的确切周期发生了什么[由于这种“自我修改”代码的性质]。

但是,当最初执行时,它可能有垃圾(即 700)。我不确定 PPC,但其他拱门有单独的 inst 和数据缓存。乱序执行。数据将在数据缓存中,但不一定在 inst 缓存 [或队列] 中。而且,它们倾向于独立运行以提高速度 [“哈佛”架构]。

(例如)在 x86 上,设置静态区域后,您必须刷新/同步,以便执行单元重新获取该区域。否则,它可能已经推测性地预取了指令数据(例如,它不希望它是“自我修改”),其中的数据不是预期的 [可能是 0x00000000]。 p>

考虑:在copy_from_user 之后,所需数据位于数据缓存中,但尚未刷新到 RAM。执行单元 [和 inst 缓存],没有来自静态区域的任何数据,将从 RAM 中获取。由于自修改代码很少见,因此 inst 和数据缓存不会相互窥探[这会减慢速度]。

因此,执行单元从 RAM(例如 0x00000000)而不是加载的数据 [这是 在数据缓存中] 获取其数据。

第二次有效,因为执行单元获取的数据来自第一次尝试期间的数据[已经有时间刷新到 RAM]。也就是说,静态区域现在已被填充,第二个 copy_from_user 实际上是一个 NOP。

该区域的“事后”转储 [如上所述] 将无法显示这种差异。

【讨论】:

  • 我也想到了内存保护,但为什么要第二次起作用呢?
  • @Ctx 很难说没有函数源的发布,为它构建方法,将方法加载到内核中,以及已经在内核中的代码允许这个。另外,如果没有这个上下文,“第二次工作”实际上是什么意思?这可能意味着 OOPS 不是恐慌,“第二次”只是一个 NOP [有效地]。
  • 我不是 OP。但据我所知,如果 kmalloc 的内存在他的平台上是可执行的(函数是 PIC 并且没有外部引用),它应该可以工作。或者如果它不可执行,则永远不应该工作。不幸的是,这因拱门而异。
  • @ctx 此外,在没有上下文的情况下,还有许多其他考虑因素,例如锁定、调用前的当前状态(关于每个 CPU 等)。内核足够复杂,需要更多信息才能对此进行推测。而且,OP 表示“静态”,IIRC,所以没有kmalloc。还想到了内核线程的抢占[在不同的内核上重新启动]。另一件事:内核代码通常具有简单的用户应用程序不需要的特殊编译器和链接器选项。因此,OP 必须确保使用它们(该函数是来自 .o 还是可执行文件?)
  • 但这是lis-instruction 处的程序异常,我想不出这可能失败的原因。如果是违反保护,则应生成 ISI 异常 (400)。锁定、状态等不能成为加载寄存器操作异常的原因。好神秘……
【解决方案2】:

想通了。原来是缓存的东西。感谢 Ctx 和 Craig,我添加了一个

flush_dcache_icache_page(virt_to_page((unsigned long)(handler_text)));

之后

copy_from_user((void *)handler_text , (const void __user *)my_handler , PAGE_SIZE);

现在一切都很好。在我问这个问题之前,我只是尝试了 flush_dcache_page 并且它没有用。所以我必须同时刷新 dcache 和 icache 才能完成这项工作。再次感谢。

【讨论】:

    最近更新 更多