ssize_t vfs_write(struct file *file, const char __user *buf, size_t count, loff_t *pos);如果这个prefunc钩子函数的参数和vfs_write的一样那多好啊,整个逻辑就成了:
ssize_t prefunc(struct file *file, const char __user *buf, size_t count, loff_t *pos) { todo_something(.....); return vfs_write(file, buf, count, pos); }但是不幸的是,kprobe做不到。因为它是基于INT 3异常/中断来处理的,而Intel的异常/中断的处理有一套特定的规程,即保存所有的上下文环境,因此它的参数就只有struct pt_regs *regs一个,即所有的寄存器信息。要想还原vfs_write的参数,你必须针对这个regs参数做一个“深度解析”才行,而这又一次将你引入了平台相关的地狱,如果你在X86平台,你就不得不对它的寄存器使用规约做一番详细的了解才能还原被钩函数的参数,对于X86来讲,参数保存在栈中(也可以通过寄存器传参),要想还原被钩函数的参数现场,你要分析的就是regs->sp,下面我就不说了。
prefunc(kprobe, regs) { 保存regs寄存器现场 保存栈的内容 //因为jprobe使用和被钩函数相同的栈,可能会改变栈的内容 替换regs里面ip指针为jprobe钩子的指针 返回 }就这样一个kprobe的prefunc钩子函数就把INT 3返回正常流,但是请注意,在这个prefunc中,将regs的ip改变了,改成了jprobe的entry函数,而栈信息一点都没有变,因此返回正常流之后,栈上的参数信息没有变,只是执行的函数变了,变成了entry!等jprobe的entry执行完了之后,调用jprobe_return来还原,这个return实际上就是再次进入INT 3异常,然后调用kprobe的另一个钩子函数来还原现场,即将prefunc保存的regs现场以及栈现场还原。是不是很像setjmp和longjmp啊!是的,几乎是一样的!到此为止,程序进入了被钩函书,整个流程就是:
static struct jprobe steal_jprobe = { .entry = steal_ip_local_deliver, .kp = { .symbol_name = "ip_local_deliver", } }; int steal_ip_local_deliver(struct sk_buff *skb) { if (skb && skb->mark == 1004) { ip_local_deliver_finish(skb); } jprobe_return(); return 0; }这段代码也许表达了我的目的,即从ip_local_deliver开始,数据包将不再经原生的Linux协议栈处理,而是被偷到了我的steal_ip_local_deliver,在其内部,可以实现自己的协议栈处理逻辑,当然为了简单,我只是调用了 ip_local_deliver_finish将数据包直接绕过NF_HOOK往上传递。
int steal_ip_local_deliver(struct sk_buff *skb) { if (skb && skb->mark == 1004) { ip_local_deliver_finish(skb_copy(skb, GFP_ATOMIC)); } jprobe_return(); return 0; }这样做之后,在steal中传入 ip_local_deliver_finish的只是skb的一个副本,待返回正常的ip_local_deliver后,原始的skb还是可用的。可是这就将一个数据流fork成了两个,对于TCP协议而言,TCP逻辑会自动丢掉重复的,但是对于像UDP或者ICMP之类的数据流而言,将会收到双份的数据,一个来自正常的协议栈,另一个来自steal的协议栈。现在的问题在于,如何阻止掉正常的协议栈处理。
int stub(struct sk_buff *skb) { return 0; }我要做的就是将返回原始正常流后原本要调用ip_local_deliver的指令改为调用stub,要实现这个就要进行动态的二进制指令修改。深入到kprobe细节的都应该知道kprobe结构体包含一个字段:
/* copy of the original instruction */ struct arch_specific_insn ainsn;我连注释也一并贴上了,因为这省了我解释了,注意命名,ainsn中的a就是arch的意思,这个多加的层为上层屏蔽了平台相关的细节,对于X86而言,它就是:
u8 *insn;是的,一连串的二进制指令,很显然,这里保存的指令肯定是jmp ip_local_deliver之类的,因为这段指令的目的就是跳转回原始的执行流。我只需要将其改为jmp stub就可以了。就是说,在jprobe的entry钩子中,将kprobe的ainsn.insn改为jmp stub,然后为了不影响不相关的后续的执行流返回ip_local_deliver,在stub中再将kprobe的ainsn.insn改回去。
#include <linux/kernel.h> #include <linux/module.h> #include <linux/kprobes.h> #include <linux/hardirq.h> #include <linux/skbuff.h> // 从/proc/kallsyms中找出的ip_local_deliver_finish地址 // 我只是想在jprobe函数中直接调用finish,企图跳过NF_HOOK #define func 0xffffffff812b70f3 int (*f)(struct sk_buff *); // 保存全局变量,因为无法从steal钩子函数中取到kprobe struct kprobe *k = NULL; #define JMP_CODE_SIZE 12 #define ADDR_SIZE sizeof(void *) u8 saved[MAX_INSN_SIZE] = {0}; // 注意,不要太在意下面的二进制指令码的具体细节!主要含义理解即可:将地址送入寄存器,jmp到该处 u8 jmpcode[JMP_CODE_SIZE] = {0x48, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xe0}; int stub(struct sk_buff *skb) { memcpy(k->ainsn.insn, saved, MAX_INSN_SIZE); return 0; } int steal_ip_local_deliver(struct sk_buff *skb) { if (skb && skb->mark == 1234) { // 先保存原始的替换指令码。 memcpy(saved, k->ainsn.insn, MAX_INSN_SIZE); // 替换为jmp到steal函数的指令码。 memcpy(k->ainsn.insn, jmpcode, JMP_CODE_SIZE); // 调用自己的函数,为了简单,我只是调用了ip_local_deliver_finish。 (*f)(skb); // 从这里返回后,由于指令码已被替换为steal函数stub,因此就不会 // 再返回正常的ip_local_deliver了。 } jprobe_return(); return 0; } static struct jprobe steal_jprobe = { .entry = steal_ip_local_deliver, .kp = { .symbol_name = "ip_local_deliver", } }; static int __init jprobe_init(void) { int ret; int i = 0, j = 9; unsigned long addr = (unsigned long)&stub; ret = register_jprobe(&steal_jprobe); if (ret < 0) { printk("register_jprobe failed:%d\n", ret); return -1; } k = &steal_jprobe.kp; f = func; // 根据stub函数的地址填充jmpcode指令码数组 for (i = 0; i < ADDR_SIZE; i++, j--) { jmpcode[j] = (addr&0xff00000000000000)>>56; addr <<= 8; } return 0; } static void __exit jprobe_exit(void) { unregister_jprobe(&steal_jprobe); } module_init(jprobe_init) module_exit(jprobe_exit) MODULE_LICENSE("GPL");这就是几年前我看到的一个镜像协议栈的原理。虽然Linux很难直接通过make config将整个网络协议栈编译成一个模块,但是我们自己可以手工构建一个网络协议栈模块,无非就是把net/ipv4目录编译成一个模块,然后使用jprobe钩住netif_receive_skb这个底层函数,将控制权导入到我们自己的协议栈模块中。说白了在冯.诺依曼这种串行处理的机器中,争夺的就是控制权,只要你占有了CPU,那控制权就属于你,一旦你有了控制权,你不光可以增删改查内存中的数据,还可以增删改查内存中的代码,因为数据和代码都在内存...
使用jprobe构建镜像协议栈的原理与感悟,布布扣,bubuko.com
原文地址:http://blog.csdn.net/dog250/article/details/29186145