从用户空间复制函数到内核并执行

5

首先,我做这个只是为了好玩,所以请不要批评我。

我的做法是将一个函数指针从用户空间传递到内核,使用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。但第二次和之后再执行,问题就消失了,不再出现任何Oops。通过读取内存,我可以清楚地看到该函数是由内核执行的。我正在运行一个PowerPc目标,因此Oops显示异常为700,即程序异常。从Oops中,我可以看到指令转储,其中nip(之后)正好是与my_handler相同的指令。

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

我无法理解它的意义。有人能理解吗?谢谢。


如果我可以在评论中放图片,我会放这个 - larsks
你能提供更多的上下文吗?内核代码是自定义系统调用吗?也就是说,它是如何从用户空间调用的?你能提供内核 oops 的完整信息吗?使用的 Powerpc 型号是哪个? - Ctx
2个回答

4
找到了答案。原来是缓存的问题。感谢 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才能使其正常工作。再次感谢。


4
我不想打击一个令人钦佩的想法,但是你所尝试的做法需要进行一些严肃的额外工作才能实现,如果没有这些工作,可能会很困难,甚至是不可能的。
你的函数在用户空间的位置 F 上链接。你将它复制到静态数组 A 的内核空间位置。 A 可能位于内核的数据段中,因此可能无法执行。而且,你的函数链接到了错误的位置(例如,F != A)。
此外,即使你的函数可以链接到正确的位置 A,你如何处理其中符号的重定位(例如,如果它调用 printk,你如何重新链接函数内部的地址以匹配实际的 printk 地址)?
创建一个内核模块并加载它(通过 modprobe)要容易得多,而且你可以做任何你想做的事情。
附注:这是一个巨大的安全漏洞。类似的漏洞曾被“Stuxnet”蠕虫用来渗透 Windows。
更新:
转储发生在异常事件之后很长时间。到那时,它已经有了正确的数据,因此转储显示了当前状态,但不显示确切的周期发生了什么(由于这种“自修改”的代码的性质)。
但是,当初始执行时,它可能会有垃圾数据(例如 700)。我不确定 PPC,但其他架构有单独的指令和数据缓存。乱序执行时,数据将在数据缓存中,但不一定在指令缓存(或队列)中。它们倾向于独立运行以提高速度(哈佛结构)。
例如,在 x86 上,设置静态区域后,必须刷新/同步,以便执行单元重新获取该区域。否则,它可能已经根据预期之外的数据(例如它不认为它是“自修改的”)预取了指令数据。考虑到自修改代码很少见,指令和数据缓存不会互相监视(这会减慢速度)。
因此,执行单元从 RAM 中获取了它的数据(例如 0x00000000),而不是加载的数据(仅在数据缓存中)。
第二次能够工作是因为执行单元获取的数据来自第一次尝试期间的数据[已有时间刷新到RAM]。也就是说,静态区现在已经被填充,第二个copy_from_user实际上是无操作。
如上所述,“事后”转储该区域将无法显示此不一致性。

内存保护也在我的脑海中浮现,但为什么第二次会起作用呢? - Ctx
@Ctx 没有提供函数源代码、构建方法、将其加载到内核的方法以及内核中已经存在的代码,很难说清楚问题所在。此外,没有上下文的情况下,“第二次工作”实际上意味着什么?这可能意味着OOPS不是一个紧急情况,而“第二次”只是一个NOP(无操作)[有效地]。 - Craig Estey
我不是原帖发布者。但据我所见,如果 kmalloc 分配的内存在他的平台上是可执行的(该函数为 PIC,并且没有外部引用),则它应该可以工作。如果内存不可执行,则永远都不应该工作。不幸的是,这在不同架构之间有很大的差异。 - Ctx
但这是一个lis指令的程序异常,我想不出为什么会失败。如果是保护违规,应该会生成ISI异常(400)。锁定、状态等不能成为加载寄存器操作异常的原因。相当神秘... - Ctx
证据在于“oops”:<3d20f000> <- 这是失败的指令,“lis r9,-4096”。这非常可靠。 - Ctx
显示剩余4条评论

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