问题场景:
云计算IaaS平台上,经常使用libvirt+qemu-kvm做基础平台。libvirt会在/etc/libvirt/qemu/目录下,保存很多份qemu的配置文件,如ubuntu.xml。
作者发现其中的配置文件会在特定的场景下被修改,却不知道哪个进程是凶手。为了找到凶手,作者写下了这个debug工具。
代码分析:
代码路径:https://github.com/pacepi/whotouchmyfile
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/kprobes.h>
#include <linux/fs.h>
#include <linux/slab.h>
#include <asm/uaccess.h>
#define FILE_NAME 64//被监控的文件名最大字符数,按需求,自己改
static struct kprobe kp = {
.symbol_name = "vfs_write",//使用kprobe,监控vfs_write,如果修改文件,几乎逃不开这个入口。作者想到的其他情况就是使用了mmap映射文件,然后msync回写。
};
static char file_name[FILE_NAME] = {0};
static struct ctl_table_header *cth = NULL;
static struct ctl_path path = {
.procname = "kernel",//监控点放到了/proc/sys/kernel目录下
};
static struct ctl_table table[] = {
{
.procname = "who_touch_my_file",//命名是作者的一时想法。用法就是echo "file_name" > /proc/sys/kernel/who_touch_my_file
.data = file_name,
.maxlen = FILE_NAME,
.mode = 0644,
.proc_handler = proc_dostring,
},
{
}
};
static int handler_pre(struct kprobe *p, struct pt_regs *regs)
{//这里是重头戏,主要实现在这里。如果调用到了这里,说明就有进程调用了write,pwrite,writev,总之,陷入到了vfs_write这里。至于原因,留个悬念吧。后面开一篇来分析kprobe的实现
struct file *file = (struct file *)regs->di;//因为x86的参数传递规则是di,si,dx,cx,r8,r9,所以di就是vfs_write的第一个参数。arm默认是r0,r1,r2,
r3,相应的取r0
char *buf = NULL;
size_t size = 0;
#if 0
printk(KERN_INFO "pre_handler: p->addr = 0x%p, ip = %lx," " flags = 0x%lx\n", p->addr, regs->ip, regs->flags);
if (file->f_path.dentry && file->f_path.dentry->d_name.name)
printk(KERN_INFO "name = %s" , file->f_dentry->d_name.name);
#endif
//简单的字符串比较。这里需要注意一下,作者实验过的3.10和4.0.4,这里的数据结构发生了变化。对于不同版本,需要自己修改适配一下。
if (unlikely(strlen(file_name) && file && file->f_path.dentry && file->f_path.dentry->d_name.name && (strncmp(file->f_path.dentry->d_name.name, file_name, FILE_NAME) == 0)))
printk(KERN_INFO "bingo : %s\n" , file->f_path.dentry->d_name.name);
else
return 0;
size = regs->dx + 1;//vfs_write的第三个参数在dx中
printk(KERN_INFO "process = %s , pid = %ld, file = %s, size = %ld\n" , current->comm, current->pid, file->f_path.dentry->d_name.name, size);//进程从用户态call下来,到这里都是同步的,所以current就是调用vfs_write的caller,拿到进程名称和pid就很容易了
/*
* should be careful ! if size is too big, maybe over flow.//文件不大的时候,可以尝试打印一下,不过注意,内核栈和thread结构体一共8k。不要overflow。
buf = (char*)kmalloc(size, 0);
if (buf == NULL)
return 0;
memset(buf, 0x00, size);
if(copy_from_user(buf, regs->si, size))
goto out;
printk(KERN_INFO "%s\n" , buf);
*/
out :
if (buf)
kfree(buf);
return 0;
}
static void handler_post(struct kprobe *p, struct pt_regs *regs,
unsigned long flags)
{
//printk(KERN_INFO "post_handler: p->addr = 0x%p, flags = 0x%lx\n", p->addr, regs->flags);
}
static int handler_fault(struct kprobe *p, struct pt_regs *regs, int trapnr)
{
printk(KERN_INFO "fault_handler: p->addr = 0x%p, trap #%dn",
p->addr, trapnr);
return 0;
}
static int __init kprobe_init(void)
{//这里是八股文,init+exit
int ret;
kp.pre_handler = handler_pre;
kp.post_handler = handler_post;
kp.fault_handler = handler_fault;
ret = register_kprobe(&kp);//其实这里才是入口,ko向kernel注册kprobe
if (ret < 0) {
printk(KERN_INFO "register_kprobe failed, returned %d\n", ret);
goto out;
}
printk(KERN_INFO "register kprobe at %p\n", kp.addr);
cth = register_sysctl_paths(&path, &table);///proc/sys/kernel/who_touch_my_file在这里注册的
if (cth == NULL) {
printk(KERN_INFO "register_sysctl_paths failed\n");
ret = -EFAULT;
goto error;
}
return 0;
error:
unregister_kprobe(&kp);
out:
return ret;
}
static void __exit kprobe_exit(void)
{
if (cth)
unregister_sysctl_table(cth);
unregister_kprobe(&kp);
printk(KERN_INFO "kprobe at %p unregistered\n", kp.addr);
}
module_init(kprobe_init);
module_exit(kprobe_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("PiZhenwei p_ace@126.com");
其他的话
这里是否可以用systemtap?答案是可以的。本质来说,systemtap也是用kprobe实现的(不过它需要debug symbol,也就是vmlinux,不过也可以捕获更精确的代码,原因在后面的kprobe实现一起分析)。
所谓内核热补丁,也可以用kprobe实现。在不重启内核的情况下,动态加载ko,修改内核行为。