aboutsummaryrefslogtreecommitdiffstats

项目背景

随着 k8s/云原生等技术日渐普及,docker 容器在生产中的应用愈加广泛。由于 docker 并不是一个完整的操作系统,使用的内核依然是宿主机内核,则在 docker 实际使用过程中,可能会遭受攻击或产生泄露,从而威胁其他 docker 或宿主机。因而我们需要对 docker 内部的进程行为、文件修改等进行监视,在出现问题后便于回溯。

设计思路

整体设计

项目整体采用 MVC 的设计方式,设计思路如下图所示:

Linux kernel
Linux kernel
listener
(godo)
listener...
connector
connector
audit log
audit log
infos
infos
filter
filter
Viewer
Viewer
mongoDB
mongoDB

项目主要分为信息采集、信息过滤、数据库、信息展示四个模块,其工作方式如下:

  • 信息采集模块,在各宿主机上安装、并以 root 权限启动,负责监听由 Linux 内核发出的 netlink connector 消息、audit 审计消息,将其整理为有关进程的、有关文件的数据,送入数据库中。
  • 在使用时,用信息过滤模块连接数据库,该模块将从数据库中取出所有的消息并过滤无关内容,得到以 docker 守护进程为根的进程树;并在此树的基础上,对数据库中关于文件的记录进行过滤与整理。完成后,将过滤得到的数据送入数据库。
  • 信息展示模块,简要地展示过滤得到的、有关 docker 的数据。

信息采集

mongodb
mongodb
1. listen to the audit,
pass msg down
1. listen to the audi...
2. Organize recvd msg into events by transection number
2. Organize recvd msg into e...
3. Listen to the kernel connector, gets fork/exit events, pass them down
3. Listen to the kernel connector, g...
4. Deal with events recvd, push pid/file info into db
4. Deal with events...
Linux kernel
Linux kernel
audit log
audit log
audit
audit
process
process
netlink
connector
netlink...
Events
Events
Events
Events
process info
process info
/proc fs
/proc fs
pid info
pid info
file change
info
file change...
Text is not SVG - cannot display

信息采集模块对应本项目下的 listener 目录,主要负责收集 Linux-kernel 发出的 audit 系统审计消息、netlink connector 进程消息。分为四个协程,各自功能如下:

  • 1 号协程,接收来自内核的 audit 审计消息,传递给 2 号协程
  • 2 号,拿到 1 号发来的消息。对于 audit 审计消息而言,一个事件会被拆分为多条消息发送,但使用相同的时间戳、事务号。因而 2 号将收到的消息使用正则表达式进行简单解析,并用哈希表按照事务号存储,直到收到 eoe(本事件到此结束),将 hash 表中整理得到的 Event 事件发送给 4 号协程
  • 3 号,接收来自内核的 connector 消息,获取其中的进程事件(fork/exit)及进程号(ppid/parentTgid/pid/tgid),并通过/proc 文件系统查询 pid 对应的命令行参数 args、当前运行目录 cwd、根文件系统 rootfs、docker id(从 cgroup 查看),整理为 Event 事件,发送给 4 号协程
  • 4 号,接收 Event 事件,判断其类型,分别处理。代码中事件类型主要有进程复制、进程退出、进程执行文件(execve)、文件打开、文件关闭、写文件、切换根系统(pivot_root)等几种。

信息过滤

进程过滤与优化

首先,由于 listener 模块在插入时采用了多线程,可能出现同一个进程的两条消息被并行处理、数据库中出现两条记录,因而第一步,是将相同 pid 的多条记录合并为一条

现在开始考虑清洗数据的问题。Docker 是一个 C/S 架构的服务,因而我们真正关心的 docker 有关进程一定是 docker 守护进程的后代(虽然可能作为孤儿进程被 systemd 收养)。过滤进程数据,只需要构建以守护进程为根的进程树。在信息收集过程中,我们对 docker 守护进程(/usr/bin/dockerd)进行了特殊记录,标记了该 pid 的star=true。在过滤过程中,主要工作即围绕该 pid 展开。

  • 我们记录的条目以 pid 区分,而这里的 pid 实质上指的是 task id、可能是线程,tgid 才是 task group id 应当理解为进程。因而,为了构建进程树,最简单的办法是将各个 pid 按照 tgid 区分,成为一个新的结构;这些结构代表着进程、是进程树的节点,因而称为 tgidNode。在此过程中,我们也可以整理得到每个 tgid 的所有子代 tgid 编号。
  • 整理出来若干 tgidNode,从标记了 star 的 tgidNode 开始,采用广度优先遍历,得到整个进程树上的所有 tgidNode

接着,进行数据优化

  • 同一个 docker id 使用相同的 rootfs。在记录中,同一个 docker id 只有一个进程进行过 pivot_root,因而需要加以处理。
  • 同一个进程(tgid)的不同线程(pid)可能 ppid/parentTgid 不一样。原因为,在进程(pid==tgid)创建的时候,父进程一定还在;但过一会创建线程的时候,原父进程可能已死、该进程已经被 systemd 收容,所以记录的 ppid/parentTgid 不对。为解决该问题,需要检查每个 pid,如果存在该问题则进行修正,防止在按 pid 溯源时出错。
  • 部分 pid 可能并未收到对应的退出消息。为了部分地解决该问题,我们将进程退出时间(也就是 pid==tgid 的 pid 的退出时间)记录为没有 exit timestamp 的 pid 的退出时间。这样的补全是为了接下来在处理文件时使用。

文件过滤

众所周知,Linux 环境下,进程操作文件使用的是系统调用+文件描述符。

在记录的时候,由于 Linux 下进程是通过 open 系统调用,传入文件名和权限,得到文件描述符,使用、关闭时都是操作文件描述符而非文件名,所以记录时应当把已经关闭的和尚未关闭的区分开来。写文件时,在已经打开但尚未关闭的文件里按照 pid+fd 查找,记录写入时间;关闭时,将记录从 fd 表删除,加上关闭时间后存储到关闭的文件里。

但在整理时,二者都有写入记录,应该等同视之。将两张表的所有记录提取出来进行筛选,只保留 pid 在进程树上的那些文件记录;而后,对于尚未关闭的文件,查询 pid 退出时间,如有记录,则认为该文件在 pid 退出时才关闭。

最后,将处理得到的 tgidNode 构成的进程树、筛选之后的文件,全部记录到数据库里。

信息展示

现在已经获取了进程树和文件修改的详细记录,展示即可。本项目目前是在过滤完成之后,直接由过滤模块将进程树、进程详细信息、文件修改记录全部打印到标准输出。

编译与运行

本项目的编译运行较为简单。

在将本项目克隆下来后:

git submodule --init
cd listener
go build -o godo
cd ../filter
go build -o filter

编译完成后,将 godo 放置在宿主机上运行,godo 必须以 root 权限运行。有若干命令行参数,可以通过sudo ./godo -h查看。注意:

  • 指定参数使用等号,如-diag参数表示将内核原始 audit 消息输出到指定文件,使用时即sudo ./godo -diag=1.txt
  • 默认的数据库是本机的 mongodb,端口 27017;如要连接别的数据库,需要使用-mongo参数指定其链接,格式为ip:port。本处并未设置 mongodb 的用户名、密码,而是放开了权限直接登录。使用的数据库名为"test"。
  • backlog 大小默认为 1GB,最好只大不小。以字节为单位。
  • filter 放置在数据库所在的机器上,连接数据库。使用的数据库为 test,写入的数据库为 cooked。

而filter程序则直接放置在数据库所在机器上,在需要回溯的时候,直接运行filter程序(数据库没有账号密码控制),会输出进程树、每个进程的参数,及最终受改变的文件列表。