[linux][kprobe]谁动了我的文件---使用kprobe找到目标进程

时间:2022-05-07
本文章向大家介绍[linux][kprobe]谁动了我的文件---使用kprobe找到目标进程,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

问题场景:

云计算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%lxn", 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 : %sn" , 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 = %ldn" , 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 "%sn" , 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%lxn", 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 %dn", ret);
        goto out;
    }
    printk(KERN_INFO "register kprobe at %pn", kp.addr);

    cth = register_sysctl_paths(&path, &table);///proc/sys/kernel/who_touch_my_file在这里注册的
    if (cth == NULL) {
        printk(KERN_INFO "register_sysctl_paths failedn");
        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 unregisteredn", 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,修改内核行为。