上一篇介绍了基于tracepoint的静态跟踪技术的实现,本文介绍的是基于kprobe的动态跟踪技术的实现。同样,动态跟踪也是故障排除的有力工具。kprobe是内核提供的一种动态跟踪技术机制,它允许动态安装内核模块来安装系统钩子,非常强大。我们先来看内核中的一个例子。#include#include#include#defineMAX_SYMBOL_LEN64//内核函数名hanckstaticcharsymbol[MAX_SYMBOL_LEN]="_do_fork";module_param_string(symbol,symbol,sizeof(符号),0644);staticstructkprobekp={.symbol_name=symbol,};//执行系统函数前执行的hookstaticint__kprobeshandler_pre(structkprobe*p,structpt_regs*regs){//...}//Execution系统单条指令后执行的hookfunction(不是在系统函数执行完之后)staticvoid__kprobeshandler_post(structkprobe*p,structpt_regs*regs,unsignedlongflags){//...}//hook执行错误或单次执行错误时要执行的函数(structkprobe*p,structpt_regs*regs,inttrapnr){//...}staticint__initkprobe_init(void){intret;//设置hookkp.pre_handler=handler_pre;kp.post_handler=handler_post;kp.fault_handler=handler_fault;//安装Hookregister_kprobe(&kp);return0;}staticvoid__exitkprobe_exit(void){unregister_kprobe(&kp);pr_info("kprobeat%punregistered\n",kp.addr);}//安装到内核后的初始化和注销函数module_init(kprobe_init)module_exit(kprobe_exit)MODULE_LICENSE("GPL");设置kprobe后,通过register_kprobe注册到内核intregister_kprobe(structkprobe*p){intret;structkprobe*old_p;structmodule*probed_mod;kprobe_opcode_t*addr;//通过系统函数名找到对应的地址,内核维护这个数据addr=kprobe_addr(p);//记录这个地址p->addr=addr;p->flags&=KPROBE_FLAG_DISABLED;p->nmissed=0;INIT_LIST_HEAD(&p->list);//之前是否有hook,如果有则插入已有的list,否则插入新的recordold_p=get_kprobe(p->addr);if(old_p){/*Sincethismayunoptimizeold_p,lockingtext_mutex.*/ret=register_aggr_kprobe(old_p,p);gotoout;}//将被黑系统函数的指令保存到probe结构中,因为下面要覆盖这段内存/*prepare_kprobe=>unsignedlongaddr=(unsignedlong)p->addr;unsignedlong*kprobe_addr=(unsignedlong*)(addr&~0xFULL);memcpy(&p->opcode,kprobe_addr,sizeof(kprobe_opcode_t));memcpy(p->ainsn.insn,kprobe_addr,sizeof(kprobe_opcode_t));*/ret=prepare_kprobe(p);INIT_HLIST_NODE(&p->hlist);//插入ker维护的哈希表nelhlist_add_head_rcu(&p->hlist,&kprobe_table[hash_ptr(p->addr,KPROBE_HASH_BITS)]);//hack系统函数所在内存的内容arm_kprobe(p);}注册一个probe,先找到对应的通过被黑函数名获取地址,然后保存该地址对应的内存信息,然后将probe插入hash表,最后调用arm_kprobe函数hacksystem函数所在内存的内容。看看arm_kprobe。voidarch_arm_kprobe(structkprobe*p){//#defineINT3_INSN_OPCODE0xCCu8int3=INT3_INSN_OPCODE;//将int3的内存复制到addrtext_poke(p->addr,&int3,1);text_poke_sync();perf_event_text_poke(p->addr,&p->opcode,1,&int3,1);}0xCC是intel架构下int3对应的指令。所以这里就是把hacked函数对应的指令前面部分改成int3。完成破解。system函数执行时,会执行int3触发trap,同时执行相应的处理函数do_int3(这个比较复杂,我也没有深入分析过,大概就是这个过程)。staticbooldo_int3(structpt_regs*regs){kprobe_int3_handler(regs);}intkprobe_int3_handler(structpt_regs*regs){kprobe_opcode_t*addr;structkprobe*p;structkprobe_ctlblk*kcb;addr=(kprobe_opcode_t*)(regs->ip-sizeof_tprobe)kcb=get_kprobe_ctl);//通过地址p=get_kprobe(addr);set_current_kprobe(p,regs,kcb);kcb->kprobe_status=KPROBE_HIT_ACTIVE;//执行pre_handlerhookif(!p->pre_handler||!p->pre_handler(p,regs))setup_singlestep(p,regs,kcb,0);}完成。在pre_handlerhook之后,单步标志将由setup_singlestep设置。staticvoidsetup_singlestep(structkprobe*p,structpt_regs*regs,structkprobe_ctlblk*kcb,intreeenter){//修改寄存器的值//设置eflags寄存器的tf位,允许单步调试regs->flags|=X86_EFLAGS_TF;regs->flags&=~X86_EFLAGS_IF;//设置下一条指令为系统函数的指令if(p->opcode==INT3_INSN_OPCODE)regs->ip=(unsignedlong)p->addr;elseregs->ip=(unsignedlong)p->ainsn.insn;}setup_singlestep首先设置允许单步调试,也就是说执行完下一条指令后,会触发trap执行一个处理函数。并将下一条指令设置为被黑函数对应的指令,在注册探针时保存。触发单步调试的trap后,最终会执行到kprobe_debug_handlerintkprobe_debug_handler(structpt_regs*regs){structkprobe*cur=kprobe_running();structkprobe_ctlblk*kcb=get_kprobe_ctlblk();//将命令恢复为系统函数命令resume_execution(cur,regs,kcb);regs->flags|=kcb->kprobe_saved_flags;//执行posthookpost_handler(cur,regs,0);}}在单步调试的trap处理函数中,会执行posthook,恢复真正的系统函数执行。这样就完成了这个过程。我们可以看到kprobe可以在系统函数执行之前执行我们的hook。此外,内核还提供了另一种机制kretprobe,用于在系统函数执行后返回前安装hook。让我们通过一个例子来了解一下kretprobe。structmy_data{ktime_tentry_stamp;};//记录函数执行开始时间}//记录函数执行结束时间=ktime_to_ns(k??time_sub(now,data->entry_stamp));return0;}staticstructkretprobemy_kretprobe={//函数返回前执行.handler=ret_handler,//函数启动前执行.entry_handler=entry_handler,.data_size=sizeof(structmy_data),/*Probeupto20instancesconcurrently.*/.maxactive=20,};staticcharfunc_name[NAME_MAX]="_do_fork";module_param_string(func,func_name,NAME_MAX,S_IRUGO);my_kretprobe.kp.symbol_name=func_name;//register_kretprobe(&my)_kretprobe可以看出系统函数的耗时可以通过kretprobe来计算。Kretprobe是基于kprobe实现的。主要逻辑是通过kprobe注册一个pre_handler,hack函数在pre_handler中的栈。因为函数执行时,返回地址是存放在栈中的。把这块内存改成内核代码,等到函数执行完,弹出返回地址的时候,就会执行内核hack的代码来执行我们的hook。执行完后会跳回到真正的返回地址继续执行。总结:内核通过劫持的方式实现了kprobe。基于kprobe的动态跟踪技术非常复杂和强大。我们可以使用这种机制来动态修改逻辑和收集信息。但是实现起来过于复杂,涉及到对CPU架构和内存模型的理解。本文也大致分析了这个过程。感兴趣的同学可以自行查看源码。