Keep learning, keep living...

0%

使用eBPF和BCC调查创建文件的进程

在我们的一个CentOS 7.8系统上,/tmp目录下偶尔会有一些随机名称的文件产生。业务比较久远,已经不清楚这些文件的来源,需要确定是哪个进程在创建它们。

LinuxVFS: Virtual File System通用文件模型的方式在物理存储介质上的文件系统和用户接口之间建立一个虚拟文件系统的抽象层。其中最重要的两个结构是:

  • inode: 存放文件的一般信息,每个inode结构都有自己的编号,这个号码唯一标识了文件系统中的文件。
  • dentry: 存放文件名称信息,以及与对应文件进行链接的有关信息。

VFS创建文件的最主要的步骤是:

  1. 为要创建的文件创建一个dentry结构
  2. 为要创建的文件创建一个inode结构并分配inode编号
  3. inode编号和文件名映射关系保存在所分配的dentry结构中
  4. dentry写入到父目录的数据区

这个过程中会调用到vfs_create函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int vfs_create(struct inode *dir, struct dentry *dentry, umode_t mode,
bool want_excl)
{
int error = may_create(dir, dentry);
if (error)
return error;

if (!dir->i_op->create)
return -EACCES; /* shouldn't it be ENOSYS? */
mode &= S_IALLUGO;
mode |= S_IFREG;
error = security_inode_create(dir, dentry, mode);
if (error)
return error;
error = dir->i_op->create(dir, dentry, mode, want_excl);
if (!error)
fsnotify_create(dir, dentry);
return error;
}

其中,dentry参数是文件的目录项结构,dir参数是dentry结构对应的inode结构。vfs_create检查是否可以在dir指定的目录下创建该dentry结构指定的文件对象,最后调用真实文件系统的create函数指针创建文件。

我们可以跟踪vfs_create来找出究竟是哪个进程在创建特定文件。

要动态跟踪内核函数,第一时间想到可以使用systemtap来实现。但最近发现CentOS7.6(内核版本:3.10.0-940.el7.x86_64)添加了eBPF的支持,而BCC工具更是基于eBPF提供了更简单的跟踪内核的方法。BCC提供的kprobekretprobe可以在内核函数开始或者返回前,执行我们自定义的代码。我们选择使用BCC来实现vfs_create跟踪。

BCC的安装可以参考:
https://www.redhat.com/en/blog/introduction-ebpf-red-hat-enterprise-linux-7

因为系统上创建文件比较频繁,因而要调查的数据输出较多,我们可以跟据父目录的inode编号来对数据进行过滤, 跟踪程序的主体逻辑如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int kprobe__vfs_create(struct pt_regs *ctx, struct inode *dir, struct dentry *dentry)
{
struct qstr d_name = dentry->d_name;

if (dir->i_ino != 12345678)
return 0;

if (d_name.len == 0)
return 0;

bpf_trace_printk("FNAME: %s\\n", d_name.name);

return 0;
};

编写BCC跟踪工具file_create:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#!/usr/bin/python
from __future__ import print_function
from bcc import BPF
import argparse

# arguments
examples = """file_create:
./file_create # trace all vfs_create()
./file_create -i 67149893 # trace file create in directory with inode
"""
parser = argparse.ArgumentParser(
description="Trace file create",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=examples)
parser.add_argument("-i", "--inode",
help="trace this inode only")
parser.add_argument("--ebpf", action="store_true",
help=argparse.SUPPRESS)
args = parser.parse_args()


bpf_source = """
#include <uapi/linux/ptrace.h>
#include <linux/fs.h>
#include <linux/sched.h>

int kprobe__vfs_create(struct pt_regs *ctx, struct inode *dir, struct dentry *dentry)
{
struct qstr d_name = dentry->d_name;

FILTER

if (d_name.len == 0)
return 0;

bpf_trace_printk("FNAME: %s\\n", d_name.name);

return 0;
};

"""

if args.inode:
bpf_source = bpf_source.replace('FILTER',
'if (dir->i_ino != %s) { return 0; }' % args.inode)
else:
bpf_source = bpf_source.replace('FILTER', '')

if args.ebpf:
print(bpf_source)
if args.ebpf:
exit()

bpf = BPF(text = bpf_source)
print("Started...")
bpf.trace_print()

查看/tmp目录的inode编号:

1
2
[root@default ebpf]# ls -id /tmp/
67149893 /tmp/

启动跟踪工具:

1
python file_create -i 67149893

在另一个终端上, 在/tmp目录下创建一个测试文件:

1
[root@default ~]# touch /tmp/12345

可以看到跟踪工具输出了相应进程的cmdlinepid,以及创建的文件名称:

1
2
3
[root@default ebpf]# python file_create -i 67149893
Started...
touch-3366 [000] d... 3077.238910: : FNAME: 12345

这样,那些随机文件再次被创建时我们就获取到对应进程的信息了。