概述
文件防火墙可以用来过滤文件操作,包括不限于open read write close等操作,相对于lsm syshook inline hook劫持形成的防火墙稳定性和能力都更为强大,虽然目前驱动好像卸载存在一定问题,但对于文件的操作具有强大的过滤能力。
核心原理
既然要过滤文件行为,肯定少不了函数劫持。redirfs 采用对于 vfs 和真实文件系统之间嵌入驱动redirfs的方式。简单讲struct inode 、struct dentry、struct file都属于vfs层面的概念,都具有各中文件操作集operation 。真实的文件系统会拥有自己的—xx_inode xx_dentry 等定义。vfs会调用真实文件系统中的文件操作集,而系统调用又会调用vfs中的文件操作集。所以最后形成了sys —>vfs --> 文件系统 的层级关系。这时,redirfs构造了自己的rfs_inode操作集,并形成操作内嵌能力。内核的文件操作过程就变成了
sys–>vfs–>redfs–>文件系统的文件操作过程。当然这个过程中,redirfs会对原始的文件操作集进行保存和调用,保证原始逻辑维持稳定。
核心替换位于
rfs_inode_set_default_fop
#define PROTOTYPE_FOP(op, new_op) \
RFS_ADD_OP_MGT(ri_new->f_op_new, inode->i_fop, op, new_op);
FUNCTION_FOP_open // a watermark for rfs_cast_to_rfile
FUNCTION_FOP_release
#undef PROTOTYPE_FOP
inode->i_fop = &ri_new->f_op_new;
这里的宏FUNCTION_FOP_open FUNCTION_FOP_release 实现了
ri_new->f_op_new.open = rfs_open
Ri_new->f_op_new.release = rfs_release。
而inode->i_fop = &ri_new->f_op_new。可以看到vfs的核心文件操作集被修改了,当然 file和dentry也做了类似的操作。
过滤驱动链
为了实现多驱动同时过滤文件系统的能力,redirfs 定义了 struct rfs_flt
struct rfs_flt {
struct list_head list;
struct rfs_op_info cbs[RFS_INODE_MAX][RFS_OP_MAX];
struct module *owner;
struct kobject kobj;
char *name;
int priority;
int paths_nr;
spinlock_t lock;
atomic_t active;
atomic_t count;
struct redirfs_filter_operations *ops;
};
过滤驱动
添加驱动链
Set_operations 将会注册多个文件操作到redifs驱动,op_id确定文件类型和操作类型, pre_cb,post_cb确定回调函数地址,最后关联到了rfs_root_list链表中
Redirfs_op_info 会对特定的文件的特定操作通过数组来设置hook函数
rfs_flt通过二维矩阵设置hook函数
#define RFS_OP_IDC(itype, op_id) (itype<<16 | op_id)
#define RFS_IDC_TO_ITYPE(idc) ((enum rfs_inode_type) (((idc) >> 16) & 0xFFFF))
#define RFS_IDC_TO_OP_ID(idc) ((enum rfs_op_id) ((idc) & 0xFFFF))
it = RFS_IDC_TO_ITYPE(rargs->type.id); //RFS_INODE_MAX
op_id = RFS_IDC_TO_OP_ID(rargs->type.id); //RFS_OP_MAX
enum redirfs_op_idc {
REDIRFS_NONE_DOP_D_REVALIDATE = RFS_OP_IDC(RFS_INODE_DNONE, RFS_OP_d_revalidate),
......
}
这里有个巨大的枚举类型 redirfs_op_idc 用于定义各种文件的属性和操作行为。
遍历驱动链
遍历驱动为反向过程。
enum rfs_inode_type{
/* dentry */
RFS_INODE_DNONE, /* negative dentry */
RFS_INODE_DSOCK, /* dentry for sock */
RFS_INODE_DLINK, /* dentry for link */
RFS_INODE_DREG, /* dentry for dreg */
RFS_INODE_DBULK,
RFS_INODE_DDIR,
RFS_INODE_DCHAR,
RFS_INODE_DFIFO,
/* inode */
RFS_INODE_SOCK,
RFS_INODE_LINK,
RFS_INODE_REG,
RFS_INODE_BULK,
RFS_INODE_DIR,
RFS_INODE_CHAR,
RFS_INODE_FIFO,
/* the last enum value, also stands for *any* */
RFS_INODE_MAX
};
enum rfs_op_id {
// dentry
RFS_OP_d_start, /* start of the range */
RFS_OP_d_revalidate,
RFS_OP_d_weak_revalidate,
RFS_OP_d_hash,
RFS_OP_d_compare,
RFS_OP_d_delete,
RFS_OP_d_init,
RFS_OP_d_release,
RFS_OP_d_prune,
RFS_OP_d_iput,
RFS_OP_d_dname,
RFS_OP_d_automount,
……...
}
从技术角度看,没有什么太新奇的东西,不过作者的代码功底确实很强,可以学习一下。
虚拟文件系统形成的关键
根据5.8的内核代码,
open在经过path_openat ----> do_open ---->vfs_open —>do_dentry_open这一系列转换的过程最终实现了虚拟文件系统到真实文件操作及真实文件系统的转换。
核心即在do_dentry_open中。他会经过三个操作
error = security_file_open(f);
f->f_op = fops_get(inode->i_fop);
open = f->f_op->open;
error = open(inode, f);
security_file_open即为lsm插桩检查点,为安全审计最常用的地点。这里不是重点。
接下来fops_get获取inode->i_fop就是核心了,inode->i_fop为文件系统安装过程需要赋值的关键函数。不同的文件系统一般会实现自己的inode->i_fop函数。及个人人为inode就代表具体的文件系统,之后open=f->f_op->open。可以明显看到赋值到了struct file结构体的过程,那么就可以理解为,具体的文件系统赋值给虚拟文件系统的过程,实现统一调用的设计思想。也就是说struct file就是虚拟文件系统。一本我们只会调用struct file里的操作,因为这里封装过了具体文件系统的所有操作,不同文件系统也通过这里进行过了切换,不需要我们做额外的工作。 简单讲struct file 就是虚拟文件系统的核心, struct inode就是实现具体文件系统的核心。
文件缓存
所有文件形成radix_tree进行缓存,这里分成两部分, dentry缓存和inode缓存。
通过rfs_object中转找到对应的rfs_inode
rfs_dentry也是同样的原理。二者都会找到自己的文件缓存数据结构redirfs_data
上级目录会遍历到avflt_root_data, 文件本身会找到avflt_inode_data。通过比对inode_cache_ver和root_cache_ver实现缓存认证。
最终通过i_write成员更新cache_ver。