}; int init_module(void) { mprotect_kretprobe.kp. addr = (kprobe_opcode_t * )kallsyms_lo okup_name(?? sys_mprotect??); register_kretprobe(&mprotect_kretprobe); } 上述为kretprobes 示例。 可见,我们使用了 kallsyms_lookup_name(),但是事实上probe可以被虚拟地用 于内核中的任何指令,即任何你知道的地址如通金system.map。给出的代码十分很简单。 当sys_mprotect返回时,桟顶的摊址(返回地址)被修改成kretpribe_trampoline()函数的地址,该函数进一步调用我们自己的mprotect_ret_handler()函数,此时我们就可以修改内核中的数据了。2. 4,_我们将详细地较少kretprobe_trampoline函数。 Kprobes 实现 1. Kprobe 实现 本节将首先大概地介绍kprobe的实现,下面的内容来自内核源码中的kprobes. txt: 当我们注册了一个Kprobe之后,kprobes就会拷贝一份指令并将该指令的头一个字节 或几个字节改为断点指令如1386八86_64上的int3。 当CPU运行到该断点指令时,将产生一个trap,保存CPU寄存器同时通过 notifier_call_chain机制将控制权转给kprobeSoKprobes执行其预处理函数,并将kprobe 结构和保存的寄存器传给该处理函数。在处理函数结束时,如果我们还设置了后处理函数, 那么该函数将被调用。 需要注意的是,当注册一个kpr0be时,我们需要指定一个预处理从而使得用户可以查 看或修改内核数据。而后处理函数是可有可无的。 由于本文主要使用kprobe接口的扩展 jprobes和kretprobes,本文将主要讨论jprobes 和kretprobes的实现而非纯kprobe。读者现在只需要知道注册一个基本的kprobe将在指 定位置插入一个断点指令,并执行指定的处理函数。在jprobes和kretprobes的实现中我们将可以看到处理函数的例子,这些处理函数指向特殊的内核函数 [/usr/src/1 inux/arch/x86/kernel/kprobes. c]。这些特殊函数类似于真正的处理函数的 prologue和 epilogue。 2. Jprobe 实现 如果我们能够了解jprobes和kretprobes的内部实现,那么我们将能够更好地使用它 们并使其完成比预期的更多的功能。 首先我们来看下下面这个结构: structjprobe { struct kprobe kp; void *entry; /* probe将要跳到的处理函数*/ 图 3. Jprobes 结构。 当我们调用 register_jprobe ()时,该函数将进一步调用 register_jprobes (&jp, 1)。 Register_jprobes ()函数的功能是设置jprobe的处理函数。如图4所示: jp->kp .pre_handler = sstjmp_pre_handler; jp->kp.break_handler = longjmp_break_handler; ret = register_kprobe(&jp->kp); 图 4.register_jprobes()片段,位于/usr/src/linux/kernel/kprobes. c。 预处理函数在真正的化理函数之前被调用,并负责^栈令‘^容,寄存器及设置£1卩。 在正常情况下,用户是无法控制jprobes的预处理函数和后处理函数的,因为其不指向用户 自定义的函数。 V J 你可能会想象jprobe的执行过程如下:、^\ 1 1. [jprobe预处理函数]-保存栈和寄存器状态。 2. [jprobe处理函数]-执行用户的需求 3. [jprobe后处理器]-恢复原始的栈和寄存器。 我们来看下预处理器的实现代磉:VV/I int _kprobes setjmp_pre_handler(struct kprobe *p, struct pt_regs *regs) { structjprobe *jp = container_ofl^p, structjprobe, kp); unsigned long addr; struct kprobe_ctlbUc *kcb = get_kprobe_ctlblk(); kcb->jprobe_saved_regs = *regs; kcb->jprobe_saved_sp = stack_addr(regs); addr = (unsigned long)(kcb->jprobe_saved_sp); /* * Linus指出,gcc假定被调用函数拥有其参数并可以覆写其参数。 *因此为了绝对保证安全,我们会保存足够多的栈中数据来保证 *覆盖参数区。 */ memcpy(kcb->jprobes_stack, (kprobe_opcode_t *)addr, MW_STACK_SEE(addr)); ~ ~ ~ regs->flags &=?X86_EFLAGS_ff; trace_hardirqs_off(); regs->ip = (unsigned long)0'p->entry); return 1; 图5. Jprobe预处理函数代码。 特别注意代码中的注释,既然这是匕化^说的,那么就一定是正确的。 PROGRAMMING ANALYSE 从代码中可以看到,该函数首先使用81&4一34如()宏得到当前栈的位置,然后使用 memcpy将其复制到kcb_>jprobes_stack中,该地址用于保存和恢复原始栈。在jprobe处 理函数运行结束后,』口10^后处理函数将被调用,代码如下: int _kprobes longjmp_break_handler(struct kprobe *p, struct pt_regs *regs) { struct kprobe_ctlbUc *kcb = get_kprobe_ctlbUc(); u8 *addr = (u8 *) (regs->ip - 1); structjprobe *jp = container_ofl^p, structjprobe, kp); if ((addr > (u8 *) jprobe_retum) && (addr < (u8 *) jprobe_retum_end)) { if (stack_addr(regs) != kcb->jprobe_saved_sp) { |