APP下载

Docker组件间标准输入输出复制的DoS攻击分析

2020-12-18周天昱申文博杨男子李金库秦承刚喻望

网络与信息安全学报 2020年6期
关键词:字段调用实例

周天昱,申文博,杨男子,李金库,秦承刚,喻望

Docker组件间标准输入输出复制的DoS攻击分析

周天昱1,2,申文博2,杨男子3,李金库3,秦承刚4,喻望4

(1. 浙江大学NGICS大平台,浙江 杭州 310027;2. 浙江大学网络空间安全学院,浙江 杭州 310027;3. 西安电子科技大学网络与信息安全学院,陕西 西安 710071;4. 蚂蚁科技集团股份有限公司,浙江 杭州 310000)

近年来,Docker技术因其部署灵活、可扩展性强,获得了大规模应用。Docker采用模块化设计,在降低开发和维护的复杂性的同时引入了针对组件间通信的拒绝服务(DoS)攻击。在Docker容器内频繁进行stdout输出会引起Docker组件消耗大量CPU,造成DoS攻击。经过分析,可发现容器实例中的stdout输出会触发Docker各个组件的goroutine,进行频繁输出复制。为系统化地找出可被DoS攻击的goroutine创建的路径,提出使用静态分析的方法来分析Docker各组件,设计并实现了Docker组件静态分析框架,最后在Docker上进行了测试,成功分析得到了34条此类路径,其中22条路径经验证,可成功被动态触发。

容器;Docker组件;DoS攻击;静态分析

1 引言

近年来,以Docker为代表的容器技术因其部署灵活,可扩展性强而获得了广泛的应用。在具体实现上,Docker采用模块化设计,根据功能分为不同的模块(又称组件),包含Docker命令行、Docker守护进程、容器守护进程以及runc等组件。这些组件实现了功能上的紧密聚合,各个组件间采用文件或者socket通信,实现了容器的快速创建和灵活管理。Docker的模块化设计使各个模块可以独立开发和设计,降低了开发和维护的复杂性。此外,模块化设计提升了Docker的可扩展性,如gVisor使用runsc(sand-boxed container)替换runc,提升容器安全性的同时,与上层Docker的接口实现了无缝结合。

但是,Docker的模块化设计引入了利用组件间通信进行拒绝服务(DoS)攻击的可攻击面。本文发现使用printf在容器实例内持续输出数据到stdout,可以引发Docker组件的高CPU消耗,造成对宿主机CPU资源的DoS攻击。其原因是Docker各个组件间通过自动触发的goroutine不断对stdio进行复制,消耗大量CPU,导致DoS攻击。然而此类DoS攻击引起的CPU消耗属于不同的Docker组件进程,而这些组件进程,被多个容器实例所共享,不属于单个容器实例,因此无法利用cgroups进行限制。

为了找出所有可被DoS攻击的路径,本文提出使用静态分析来系统化地找出所有具有stdio复制的goroutine,用LLVM编译器分析Docker各组件,以负责容器实例的stdio复制的函数为特征,找到各组件内所有能创建goroutine并调用特征函数的路径,然后跨组件将这些路径连接起来,最终找到所有能触发这些路径的Docker命令(用户可通过在终端输入Docker命令和docker-cli交互并触发这些路径)。

要实现Docker组件静态分析框架需要解决两个技术挑战。

第一,在分析Docker组件LLVM IR中的特征函数调用路径时,由于组件使用Go语言编写并大量使用了接口(interface),而在IR中一般以间接调用的方式来实现接口成员函数的调用,因此无法通过分析调用语句获得接口成员函数的目标函数,从而导致大量路径分析在接口成员函数处断裂,严重影响分析效果。为了解决该问题,本文采用基于数据流分析和函数类型匹配的方法,找到了接口成员函数的具体实现,并建立了接口成员函数调用表辅助路径分析。

第二,LLVM编译器只能分析单个组件内的函数调用路径,不同组件之间的路径无法通过组件LLVM IR的分析得到,但用户只能通过Docker命令和docker-cli以及后续的各个组件进行交互,因此最终路径应为从Docker命令开始到组件内创建goroutine结束的跨组件路径,然而LLVM只能分析单个组件内的调用路径,不同组件之间的路径无法通过组件IR分析得到。为了解决该问题,本文提出了跨组件路径连接技术,通过分析Docker组件间通信的协议(如HTTP、gRPC和ttrpc)和相关函数,利用特征字段和函数的匹配成功连接了不同组件的路径。

本文的贡献如下。

1) 本文针对Docker组件间stdio复制设计了一种新的DoS攻击方法,并对该攻击进行了分析,分析了该攻击成功的原因,发现负责stdio复制的goroutine会被该攻击不断触发从而消耗大量CPU。

2) 本文提出并实现了Docker组件静态分析框架,系统化地找出所有可能被DoS攻击的goroutine创建路径。该分析框架包括接口成员函数解析,使用Pass分析各组件内调用路径和跨组件连接路径等新技术。

3) 本文在Docker上测试了静态分析框架,成功找出了各组件内创建goroutine的路径,并通过路径连接分析出可被Docker命令触发的路径总计34条,其中22条通过动态验证确定可被触发。

2 相关工作

Dockers使用cgroups(control groups)进行硬件资源的限制[1]。cgroups是Linux内核提供的用于监控和限制进程资源的工具[2]。cgroups使用控制器(controller)来控制具体的计算资源,包括用于限制CPU使用量的CPU控制器,限制内存使用的memory控制器,限制块设备访问速率的blkio控制器等。用户通过访问cgroups提供的虚拟文件系统cgroupfs,以文件操作的方式来访问和创建cgroup。每个cgroup都有对应的资源控制器设置,将进程添加进具体的cgroup即可限制该进程以及它的后续进程的资源访问。

然而,Docker虽然可以限制容器实例对计算资源的访问,恶意攻击者仍然可以在容器实例内部对宿主操作系统上的计算资源进行DoS攻击。Gao等[3]在2019年提出可以在容器实例内部触发宿主操作系统中进程或线程对CPU、I/O的消耗,从而达到DoS的攻击效果。这些被触发的进程或线程并非由容器实例直接创建,不受容器实例所在cgroup的限制,且它们所在的cgroup通常不设置资源使用上限,因此可以消耗大量计算资源。本文将这种间接对计算资源进行DoS攻击的方法分为4类:①触发内核线程的工作流,使其消耗大量资源;②使内核线程创建子进程,该子进程与内核线程同属一个cgroup,由子线程消耗大量资源;③触发当前操作系统中的服务进程消耗资源;④利用中断上下文来消耗资源。文献[3]对每一种类型的攻击都列举了攻击实例,但并未阐述是如何找到这些实例的,同时缺乏系统性查找全部DoS攻击的方案。

Docker的安全问题一直是学术界和工业界关注的热点。Bui[4]分析了Docker容器的隔离性,以及对Linux安全特性的利用。Gupta[5]比较了Docker依赖的Linux Container技术和虚拟机技术在安全性上的差别。Bacis等[6]对Dockerfile进行扩展,使用户在能够生成镜像时指定SELinux[7]规则来加强容器安全性。Combe等[8]总结了不正确的Docker配置可能导致的安全漏洞。Chelladhurai等[9]探讨了Docker中DoS的攻击和防护措施。Shu等[10]探究了Docker Hub存在的安全性漏洞。Yasrab[11]总结了目前Docker容器存在的安全风险,并提出安全性更强的Docker容器部署方案。Jian等[12]总结了Docker容器逃逸攻击的方法和特点,并提出了一种基于namespaces[13]状态检查的防御方法。王鹃等[14]利用可信计算的相关技术设计了Docker安全防护模块,保护容器和镜像不被篡改并监控容器内部进程。Lin等[15]筛选了223个能够在Linux操作系统上进行攻击的有效攻击方法,并从中挑选出88个典型的攻击样例在Docker容器当中进行测验,最终发现有50个攻击方法可以进行有效的攻击,包括信息窃取、远程控制、DoS和提权攻击。

现有工作基本集中在Docker容器本身的安全性研究上,并未对Docker各组件通信过程中存在的漏洞进行分析和防护。

3 Docker组件介绍

Docker运行容器实例需要依赖多个组件的协同工作,各个组件通过HTTP、gRPC和ttrpc技术进行通信,发送请求与回复,如图1所示。

图1 Docker各组件关系

Figure 1 Relationships of Docker components

1) docker-cli是Docker为用户提供的可以和dockerd进行交互的工具。用户可以在终端输入Docker命令,每次执行命令都会创建一个docker-cli与之对应,docker-cli会根据用户输入的命令内容,将参数打包成REST格式的HTTP请求,发送到dockerd。

2) dockerd是Docker的管理进程,负责接收所有符合Docker Engine API的请求(如docker-cli发送的HTTP请求),并将容器相关的操作打包为gRPC请求发送给containerd。宿主操作系统中同一时间只能运行一个dockerd进程,它在初始化Docker服务的时候被创建,并一直运行在后台。dockerd进程与docker-cli进程是一对多的关系,同一个dockerd进程同时为所有的docker-cli进程提供服务。

3) containerd负责容器实例的创建、监控和结束,容器的网络和命名空间,以及容器镜像的传输和存储。containerd对外开放gRPC API,默认通过监听套接字来和dockerd进行交互,在接收到dockerd的gRPC请求后,containerd分析请求内容,根据内容的不同(如镜像处理或者容器实例创建)将其分配到对应的服务中。containerd进程是在初始化containerd服务的时候被创建并一直在后台运行,该进程与dockerd进程是一对一的关系。containerd与containerd-shim是一对多的关系,负责所有containerd-shim进程的创建和管理,并使用ttrpc协议进行通信。

4) containerd-shim负责使用runc创建并启动容器实例,并在启动完成runc退出后接管容器实例,同时负责容器实例的标准输入输出以及打开文件,从而在containerd或dockerd意外退出的时候保证容器实例的正常运行。containerd-shim是容器实例进程的父进程,每个容器实例都与一个containerd-shim进程一一对应。

4 DoS攻击分析

本文设计了一种针对Docker组件的DoS攻击方法。受到Gao等[3]利用宿主操作系统的journald进程进行DoS攻击的启发,本文尝试了多种在容器实例内可能会触发宿主操作系统进行日志记录的操作,发现当使用printf在容器实例内持续输出数据到stdout时,会引发Docker各组件消耗大量CPU,从而造成DoS攻击。本节首先介绍DoS攻击的设置,然后给出攻击结果,最后对攻击成功的原因进行详细分析。

4.1 攻击设置

本文在Docker上测试了该DoS攻击。测试使用的Docker版本为19.03.9,宿主机操作系统为Ubuntu 18.04.4 LTS,内核版本为 4.15.0,物理主机型号为Dell Opti-Plex-7060,CPU型号为 Intel i7-8700 (6核12线程),内存大小为16 GB。

图2 DoS攻击中各组件CPU使用率

Figure 2 The CPU utilization of components in the DoS attack

图3 Docker组件复制容器实例stdio原理分析(以stdout为例)

Figure 3 Docker components copy container instance’s stdio (stdout for example)

本文使用docker run命令创建并启动容器实例,命令的参数包括:①-it参数,用于开放容器实例的stdin并分配tty设备用于终端交互;②--cpuset-cpus="0"参数,用于限制容器实例运行在单个CPU核上,从而该容器实例中运行的所有进程占用CPU至多为单核100%(宿主机CPU包含12核,总CPU资源为1200%);③容器镜像名称,本文使用Docker Hub中的Ubuntu官方镜像来创建容器;④在容器中运行的命令,本文指定在容器中运行攻击程序(使用printf向stdout持续输出数据)。除了以上参数,本文还通过--log-driver参数来控制Docker容器日志记录其是否启用,并测试了在启用日志和禁用日志的配置下DoS攻击的效果。

4.2 攻击结果

测试结果显示该DoS攻击可以成功触发Docker组件消耗大量CPU。图2展示了在DoS攻击中,Docker各组件的CPU使用率,可以看出在启用日志和禁用日志两种配置下的Docker各个组件总的CPU使用率分别为607.1%和703.5%(宿主机总CPU资源为1200%),远高于100%的容器实例CPU限制。在启用日志记录的配置下,dockerd进程的CPU使用率最高(240.7%),这是因为dockerd不仅将容器的stdout传递给docker-cli,还将这些数据记录到日志文件中。禁用日志记录后,由于不再进行日志文件的读写,反而加快了Docker组件对容器stdout复制的速度,从而使各组件的CPU使用率总和升高。

4.3 原理分析

为了分析在DoS攻击测试中Docker组件大量消耗CPU的具体原因,本文使用pprof动态分析了Docker各组件内函数在被攻击时的CPU使用量,结果显示docker-cli、dockerd和containerd-shim内CPU占用率最高的函数分别为io.Copy、io.CopyBuffer和io.CopyBuffer,本文称其为特征函数,进一步分析之后发现这些特征函数都位于特殊的、可被反复触发的goroutine中。goroutine是一种轻量级的线程[16],与其他线程并发运行。一个Go应用程序通常同时运行多个执行不同任务的goroutine,这些goroutine可以被自动触发。

根据pprof中统计函数的CPU占用率和调用关系,发现在基于printf的DoS攻击中,Docker不同组件中的goroutine被攻击程序不断触发,其内部的特征函数(docker-cli中的io.Copy,dockerd和containerd-shim中的io.CopyBuffer)频繁地进行数据读写,导致消耗大量CPU,使DoS攻击成功。在此基础上,本文对该goroutine进行了分析,发现该goroutine中的特征函数被用于复制容器实例的stdout,一旦容器实例产生stdout输出,该goroutine中的特征函数就会被触发,对stdout中的输出进行复制,因此攻击者可通过在容器实例内部持续输出stdout触发各组件goroutine中的特征函数频繁地进行数据读写,达到对组件的DoS攻击。由此可见,利用组件间stdio复制进行DoS攻击需满足两个条件:①有goroutine被创建;②goroutine中使用特征函数(io.Copy或io.CopyBuffer)进行stdio复制。正是Docker组件进程中存在此类goroutine,攻击者才能对其进行DoS攻击。

通过分析Docker各组件,本文发现若要复制容器实例的stdout,就需要在容器实例初始化阶段创建新的goroutine,并在其内部调用特征函数进行容器实例stdout输出的复制。图3分别展示了Docker各组件在容器实例创建阶段和容器实例运行阶段对goroutine和stdout复制的操作。当用户在终端执行docker start命令时,Docker各个组件协同创建新的容器实例,同时创建goroutine并在其内部调用特征函数负责复制容器实例的stdout,本文将docker-cli、dockerd和containerd-shim创建的用于复制容器实例stdout的goroutine分别称为goroutine1、goroutine2和goroutine3(goroutine1内调用io.Copy,goroutine2和goroutine3内调用io.CopyBuffer),如图3(a)所示。

容器实例运行阶段,Docker各组件采用套接字、命名管道、匿名管道和ptmx/pts设备对容器实例的stdout进行中继,如图3(b)所示。containerd-shim根据容器实例初始化的配置参数(如-it或-d等)来决定和容器实例的通信方法:如果使用-it参数,则使用ptmx/pts设备进行交互,容器实例将stdout写入pts设备后,containerd-shim从ptmx设备中读出数据;若使用-d参数,containerd-shim与容器实例通过匿名管道进行通信。containerd-shim把容器实例的stdout通过命名管道传递给dockerd,dockerd将获得的数据通过套接字转发给docker-cli。

容器实例stdout未产生数据时,特征函数处于阻塞状态。在运行阶段,容器实例内部产生stdout后,如printf DoS攻击中的Hello输出,containerd-shim内goroutine3中的io.CopyBuffer函数被触发,先读出容器实例的stdout(通过ptmx设备或匿名管道),再写入和dockerd共享的命名管道,如图3(b)所示,进而触发dockerd中的goroutine2将数据写入和docker-cli通信的套接字中,最终触发docker-cli中负责复制stdout的goroutine1,把数据输出到用户终端上,用户即可观察到该输出,即字符串“Hello!”。综上可知,如果在容器实例创建阶段,Docker组件创建了负责复制stdio的goroutine,攻击者可以在容器运行阶段,通过不断输出到stdout,触发各组件goroutine中的特征函数不断地进行数据读写,从而大幅提高各组件的CPU使用率,造成对于宿主机CPU资源的DoS攻击。

目前常用的资源限制技术cgroups无法通过限制CPU使用量抵御针对Docker组件间stdio的DoS攻击。cgroups限制CPU使用量往往只针对容器实例进程,对其他Docker组件则不作限制,因此组件内goroutine可消耗的CPU资源并无上限。更大的问题是,所有的容器实例共享同一个dockerd进程,因此现有cgroups技术无法区分、限制单个容器实例所引发的dockerd的CPU消耗。所以现有技术无法通过限制CPU来防护针对Docker组件间stdio的DoS攻击。

5 分析框架设计与实现

从攻击原理分析中,可知在容器实例创建阶段,Docker各个组件创建的使用特征函数对stdout进行复制的goroutine会引发DoS攻击,而现有的cgroups技术并不能防护此类DoS攻击,因此本文提出一个Docker组件的静态分析框架,使用静态分析方法来找出所有能创建此类goroutine的路径,然后在不同组件间分析路径的连接,最终找到所有能触发此类路径的Docker命令。

本文提出的Docker组件调用路径静态分析框架如图4所示,使用Docker各个组件的源代码作为输入。分析框架首先使用gollvm将Docker各组件源代码编译为LLVM IR,然后对每个组件IR中的接口成员函数进行解析,为所有接口成员函数的间接调用语句建立调用表,进而利用特征函数(docker-cli对应io.Copy,dockerd和containerd-shim对应io.CopyBuffer)逐个分析各个组件,得到每个组件内部创建goroutine并调用特征函数的路径,最后使用Docker组件间通信协议对不同组件内的路径进行连接,得到从Docker命令输入该组件创建goroutine的完整路径。

图4 Docker组件函数调用路径静态分析框架

Figure 4 Static analysis framework for function call paths in Docker components

虽然基本思路简单,但实现该分析框架需要解决两个技术挑战。

第一,在分析Docker调用路径时,需要构建完整的程序控制流图(CFG,control flow graph),然而由于Docker组件使用Go语言编写,其内大量使用了接口,对接口成员函数的调用最终通过间接调用来实现,因此无法由调用语句分析得到目标函数,导致无法形成完整的调用路径,无法构建完整的程序控制流图。为了解决该问题,本文设计了接口成员函数解析方法,利用数据流分析和函数类型匹配找到了接口成员函数的具体实现,进而建立接口成员函数调用表,使在进行组件分析时可以得到完整路径。

第二,由于用户只能通过Docker命令和docker-cli以及后续的各个组件进行交互,因此最终路径应为从Docker命令开始到组件内创建goroutine结束的跨组件路径,然而LLVM只能分析单个组件内的调用路径,不同组件之间的路径无法通过组件IR分析得到。为了解决该问题,本文提出了跨组件路径连接方法,根据组件两两交互时使用的通信函数和对应的特征字段,通过特征字段和函数的匹配完成不同组件的路径连接。

5.1 接口成员函数解析

在对Docker各个组件进行分析时,需要通过分析函数的调用关系来构建完整的程序控制流图,后续可以在控制流图进行路径遍历,进而找到完整的调用路径。因此完整的程序控制流图对后续分析至关重要。在分析Docker组件时,由于Docker组件使用Go语言编写,大量使用接口,而接口成员函数的调用往往通过间接调用实现。无法通过解析调用语句得到目标函数,这导致大量的路径分析在接口成员函数处断裂,程序控制流图不完整,严重影响分析效果。

图5 Go语言interface在LLVM IR中的实现

Figure 5 Implementation of Go interface in LLVM IR

为了解决这个问题,本文提出结合数据流分析和函数类型来对接口成员函数的目标函数进行解析。图5以一个简单的Go语言程序为例,介绍一个接口成员函数调用的全过程。图5(a)中定义了接口I(第3~5行),该接口内有两个成员函数Get和Set;同时程序中的结构体A实现了接口I,包含Get函数和Set函数(图5(a)第8~18行)。main函数(第24行)中创建了结构体A的实例a,并将其作为参数传入函数f(第20行)。函数f中调用了接口I的实例i的成员函数Set,即a.Set。图5(b)展示了源代码编译出的LLVM IR,本文将其对接口成员函数调用实现分为3步:首先,创建全局变量@pimt...main.A存放所有结构体A实现的接口I的成员函数,其中结构体A实现的Get函数(@main.A.Get)为@pimt...main.A中的第二个成员变量,对应偏移量为1,Set函数(@main.A.Set)对应的偏移量为2;其次,在接口实例被创建的函数中,如本例子中@main.main函数,创建%I.0类型的变量%i.addr,并将全局变量@pimt...main.A存入%i.addr中偏移量为0的成员变量中;最后,在接口成员函数的调用时(图5(b)第18~25行),先从%I.0类型的变量%i.addr中取出其偏移量为0的成员变量,再从该成员变量中取它的偏移量为2的成员变量,最终得到的变量%.field.ld.2中存放的是结构体A实现的Set函数(@main.A.Set)的地址。

基于以上分析,本文提出使用数据流分析和函数类型匹配的方法寻找接口成员函数的目标函数。首先,找到所有存放接口成员函数的全局变量(如图5(b)中的@pimt...main.A),由于这类全局变量具有统一的命名规则(变量名以imt或pimt开头),因此可以通过匹配pimt和imt关键字来找到它们。在找到所有这类全局变量后,根据全局变量的初始化语句确定每个接口成员函数对应的偏移量,如@main.A.Get和@main.A.Set分别对应偏移量1和2。然后,利用LLVM编译器提供的“定义−调用”链来遍历这类全局变量被store指令使用的位置(如图5(b)第12行),通过分析store指令中存放对象(图中为%field.15)的来源可知,该全局变量被存放到了某类型(图中为%I.0)变量中的特定偏移量(图中为0)处,从而明确了类型%I.0与全局变量@pimt...main.A的关系。最后,对所有间接调用语句(图5(b)第25行)进行分析,查找被调用变量(图中为%.field.ld.2)的来源,如果是接口成员函数的调用,则会有类似图5(b)中第20~23行的语句,从类型%I.0的变量中连续取了两次成员变量,第一次使用偏移量0取出了全局变量@pimt...main. A,第二次使用偏移量2取出接口成员函数@main.A.Set,如果有多个结构体类型实现了接口I,则认为此处调用点可调用到所有Set函数的实现。

5.2 组件内函数调用路径分析

在对接口成员函数目标函数解析后,本文在各个组件的控制流图上进行分析,找出各个组件内的包含stdio复制的goroutine创建路径。本文设计并实现了组件内函数调用路径分析Pass[17]。该Pass以Docker组件的LLVM IR和组件内负责容器实例stdio复制的特征函数(docker-cli中使用io.Copy,dockerd和containerd-shim中使用io.CopyBuffer)为输入,输出该组件内会创建goroutine并调用特征函数的路径。

最终的Pass流程如图6所示,具体步骤如下。①在接收到特征函数名称和Docker组件LLVM IR后,首先根据特征函数名称,如“io.Copy”或“io.CopyBuffer”,遍历组件IR,找到和该名称对应的函数(LLVM::Function),将其记录下来作为待搜索函数。②判断待搜索函数是否为接口成员函数,由于5.1节中解析了组件IR中所有的接口成员函数,因此可以由检查待搜索函数是否位于接口成员函数调用表中来判断,若在表中,说明待搜索函数为接口成员函数,直接从表中取出所有调用该待搜索函数的函数;若不在表中,则说明待搜索函数不是接口成员函数,此时使用LLVM“定义−调用”链找到所有待搜索函数的调用处,由调用点可推断出调用函数。③若第②步没有得到任何调用函数,说明目前找到的调用路径已经完整,即可跳到第⑤步进行路径输出;否则,对所有的调用函数进行分析,判断待搜索函数是否在新创建的goroutine中被调用。goroutine的创建在LLVM IR中是以@__go_go函数调用实现的,因此要判断待搜索函数是否在新创建的goroutine中被调用,只要分析在调用函数内,待搜索函数的地址是否被存入一个变量并作为@__go_go函数的参数被调用即可。④若待搜索函数处于调用函数新创建的goroutine中,则将该路径标记为创建goroutine的路径,否则不做任何操作。接着对所有的调用函数,重复步骤②至步骤④,直到无法找到任何调用函数为止。⑤对路径进行判断,如果路径被标记为创建goroutine的路径,说明该路径中创建了新的goroutine并调用了特征函数,符合分析的目标,将其输出即可。按照以上步骤最终可以得到该组件内所有创建goroutine并调用特征函数的路径。

图6 Docker组件内函数调用路径分析Pass流程

Figure 6 Flow chart of Pass analysis for function call path in Docker components

5.3 跨组件路径连接

5.2节实现的LLVM Pass找到的是Docker单个组件内的路径,本节通过对Docker各组件的通信方法进行分析,整理出了每两个组件之间通信函数和交互特征,使用字段匹配、函数匹配等方式成功连接各组件相关的函数调用路径。

docker-cli与dockerd的交互函数如图7所示。docker-cli通过向dockerd发送HTTP请求来完成和dockerd的交互,而dockerd会根据接收到的HTTP请求字段来调用对应的处理函数。图7(a)展示了docker-cli中发送启动容器实例请求的函数ContainerStart,该函数将特定的字段“/containers//start”作为HTTP请求中path字段的值,并调用post函数将该HTTP请求发送至dockerd。图7(b)展示了dockerd中调用initeRoutes初始化HTTP请求Router的过程,该Router根据HTTP请求的path字段来指定对应的处理函数,图中的postContainerStart函数是处理容器实例启动请求的函数,函数中对应的path字段的值为“/containers/{name:.*}/start”。该字段与docker-cli中发送的HTTP请求的path字段值相对应。由此可得,dockerd中postContainerStart可以被docker-cli中的ContainerStart函数触发,从而完成跨组件调用。因此,本文通过匹配HTTP请求中的path字段值,来完成docker-cli与dockerd的路径连接。

dockerd与containerd、containerd与containerd- shim的路径连接与上文介绍的方法类似。dockerd通过gRPC和containerd进行通信,dockerd向containerd发送gRPC请求,如图3(a)所示,通过请求中的method字段指定相应的gRPC响应,如dockerd中tasksClient.Start函数将“/containerd. services.tasks.v1.Tasks/Start”作为gRPC请求中的method字段,而containerd收到该请求后,调用_Tasks_Start_Handler函数对其进行响应,并且_Tasks_Start_Handler函数同样包含上述字段,从而可以通过匹配该字段来完成dockerd和containerd的路径连接。

图7 docker-cli 与 dockerd 的交互函数

Figure 7 Interact functions between docker-cli and dockerd

containerd通过ttrpc和containerd-shim进行通信,如图3(a)所示。containerd通过请求中的service字段和method字段来指定ttrpc响应函数,如containerd调用shimClient.Start函数将“containerd.runtime.linux.shim.v1.Shim”作为service字段、“Start”作为method字段组成ttrpc请求包发送给containerd-shim。containerd-shim在初始化阶段调用RegisterShimService函数注册所有ttrpc服务,同时按照service字段和method字段来指定响应函数,在这个例子中,shimClient.Start的响应函数为local.Start函数。因此在连接containerd和containerd-shim的路径时,可以通过service字段和method字段来匹配对应的请求和响应函数来完成跨组件路径连接。

通过匹配HTTP、gRPC和ttrpc请求包中的特定字段,可以建立组件间的函数调用关系,从而完成docker-cli与dockerd、dockerd与containerd、containerd与containerd-shim中的调用路径连接。通过路径连接可以得到goroutine1、goroutine2、goroutine3对应的完整的创建路径,以及触发这些goroutine创建的Docker命令。

6 实验

本节使用Docker测试实现的静态分析框架原型。在测试中,成功将Docker各组件编译为LLVM IR,并且使用静态分析框架原型成功地分析出了各组件内goroutine的创建路径,包含docker-cli的goroutine1,dockerd的goroutine2和containerd-shim的goroutine3,然后通过跨组件路径连接得到了这些goroutine的创建对应在docker-cli中的命令,最后验证了这些路径能否成功被触发并创建相应的goroutine进行容器实例的stdio复制。

6.1 实验设置

本文分析的Docker版本为Docker CE 19.03.9,对应的Docker各组件版本如表1第2列所示。使用的LLVM编译器版本为11.0.0git,gollvm版本为 go1.14.2。验证路径时使用的物理设备和系统环境与第4.1节中介绍的相同。

表1 Docker各组件对应的LLVM IR

在源代码基础上,本文需要将各组件编译为LLVM IR。首先,设置gollvm为默认Go编译器,使用go build命令编译组件源码,命令中需要使用-x-work参数记录编译过程中的所有编译指令;然后,将编译指令中的生成对象替换为LLVM IR文件,再次执行,得到该组件内所有package对应的LLVM IR;最后,使用llvm-link将所有包的LLVM IR链接起来,得到该组件对应的LLVM IR。本文使用代码行数统计工具cloc来统计各个组件的源代码行数。实验中Docker各个组件的版本、源代码行数、对应的LLVM IR大小如表1所示。

6.2 实验结果

实验结果主要分为两部分:各个组件内路径统计和跨组件路径统计。Docker各个组件路径分析结果如表2所示。docker-cli、dockerd以及containerd-shim可创建包含stdio复制的goroutine路径分别有12条、71条和30条。另外,containerd经过分析不存在可被Docker命令触发的容器实例的stdio复制路径。

表2 Docker各组件路径分析结果

跨组件路径分析结果如表3所示,本文实现的静态分析框架原型分析出docker-cli中通过命令可以创建goroutine1、goroutine2和goroutine3的路径分别有12条、13条和9条,总计34条。本文同时对所有分析出来的跨组件路径进行了动态运行验证,能够成功被用户输入的docker-cli命令触发并创建对应goroutine的路径分别为8条、8条和6条,总计22条,如表3第3列所示。对比第3列和第2列,可以得出静态分析的准确率分别为66.7%、61.5%和66.7%。

表3 跨组件路径分析结果

在验证路径时,本文发现Docker存在Healthcheck机制,当创建容器镜像时使用的Dockerfile中包含HEALTHCHECK命令时,Docker会定时检查以此镜像启动的容器实例是否死锁,而检查过程中会创建复制容器实例的stdio的goroutine(goroutine2和goroutine3),但此处的goroutine会在检查结束之后立刻终止,无法被用来做DoS攻击。因此,所有和该机制有关的路径(如docker unpause等命令触发的路径)都不视为验证成功的路径,因为它们并没有主动创建goroutine进行stdio复制,即使触发了容器实例的Healthcheck机制,也无法被用来做DoS攻击。

在动态运行验证时可被成功触发的路径中,有部分路径可以由同一个Docker命令(命令参数也相同)触发,运行该命令可同时触发多个goroutine的创建。其中,docker start -it命令可以同时触发docker-cli中goroutine1、goroutine2、goroutine3对应的路径,因而在运行该命令后可同时创建这3种goroutine。图8展示了该命令可以触发的路径。图左侧为容器实例初始化阶段,docker-cli、dockerd和containerd-shim分别创建了goroutine1、goroutine2和goroutine3具体的函数调用链。图右侧展示了容器实例运行阶段,当有stdout产生时,各组件的goroutine会把数据复制最终写入用户终端,因而容器实例内部的攻击者可通过在容器实例内持续输出数据到stdout来进行对Docker组件的DoS攻击。可以同时触发3种goroutine创建的Docker命令和参数组合,共计6种。

值得注意的是,在执行一些Docker命令后(如docker run -d),仅有goroutine2和goroutine3被创建,当攻击者在容器实例内使用printf进行DoS攻击时,goroutine2和goroutine3仍可被成功触发并消耗大量CPU,但此时Docker组件的CPU消耗总和小于3种goroutine都被创建时进行DoS攻击后的CPU消耗。

7 结束语

本文提出了一种针对Docker组件的DoS攻击方法,通过在容器实例内部使用printf频繁输出数据到stdout令Docker组件消耗大量CPU造成DoS攻击。经过对该攻击原理的分析,本文发现Docker组件在容器实例初始化阶段会创建新的goroutine,并在其内部调用特征函数,如io.Copy或io.CopyBuffer,对容器实例的stdio进行复制,因此这些goroutine会被printf DoS攻击不断触发消耗大量CPU而使DoS攻击成功。当前cgroups技术并不能防御此类DoS攻击。

图8 Docker组件stdio复制触发路径(以docker start为例)

Figure 8 Docker components stdio copy trigger path(take docker start for example)

为了分析所有可能导致DoS攻击的路径,本文提出使用静态分析的方法来系统化地分析Docker组件,使用LLVM编译器分析Docker各组件创建goroutine并调用特征函数进行容器实例stdio复制的路径。本文设计并实现了接口成员函数解析、组件内函数调用路径分析和跨组件路径连接等技术,实现了静态分析框架原型系统,并且在最新Docker版本上进行了测试。测试结果显示静态分析框架原型系统成功地找出了各组件内创建goroutine的路径,并通过路径连接分析出可被 Docker命令触发的路径总计34条,经过动态验证,这些路径中总计22条可成功被Docker命令触发并创建对应goroutine。

[1] MERKELD. Docker: lightweight Linux containers for consistent development and deployment[J]. Linux Journal, 2014(239):2.

[2] SEYFRIED S. Resource management in Linux with control groups[C]//Proceedings of the Linux-Kongress. 2010.

[3] GAO X, GU Z S, LI Z F, et al. Houdini′s escape: breaking the resource rein of Linux control groups[C]//Proceedings of the 2019 ACM SIGSAC Conference on Computer and Communications Security. 2019: 1073-1086.

[4] BUI T. Analysis of docker security[J]. arXiv preprint arXiv: 1501.02967, 2015.

[5] GUPTA U. Comparison between security majors in virtual machine and Linux containers[J]. arXiv preprint arXiv:1507.07816, 2015.

[6] BACIS E, MUTTI S, CAPELLI S, et al. Docker policy modules: mandatory access control for docker containers[C]//2015 IEEE Conference on Communications and Network Security (CNS). 2015: 749-750.

[7] SMALLEY S, VANCE C, SALA-MON W. Implementing selinux as a linux security module[J]. NAI Labs Report, 2001, 1(43): 139.

[8] COMBE T, MARTIN A, PIETRO R. To docker or not to docker: a security perspective[J]. IEEE Cloud Computing, 2016, 3(5): 54-62.

[9] CHELLADHURAI J, CHELLIAH P R, KUMAR A. Securing Docker containers from denial of service (DoS) attacks[C]//2016 IEEE International Conference on Services Computing (SCC). 2016: 856-859.

[10] SHU R, GU X H, ENCK W L. A study of security vulnerabilities on docker hub[C]//Proceedings of the Seventh ACM on Conference on Data and Application Security and Privacy. 2017: 269-280.

[11] YASRAB R. Mitigating docker security issues[J]. arXiv preprint arXiv:1804.05039, 2018.

[12] JIAN Z Q, CHEN L. A defense method against docker escape attack[C]//Proceedings of the 2017 International Conference on Cryptography, Security and Privacy. 2017: 142-146.

[13] ROSENR. Namespaces and cgroups, the basis of Linux containers[C]//Proceedings of the NetDev. 2016.

[14] 王鹃, 胡威, 张雨菡, 等. 基于Docker的可信容器[J]. 武汉大学学报(理学版), 2017, 63(2): 102-108.

WANG J, HU W, ZHANG Y H, et al. Trusted container based on Docker[J]. Journal of Wuhan University (Natural Science Edition), 2017, 63(2): 102-108.

[15] LIN X, LEI L G, WANG Y W, et al. A measurement study on linux container security: attacks and countermeasures[C]//Proceedings of the 34th Annual Computer Security Applications Conference. 2018: 418-429.

[16] PRABHAKARR, KUMARR. Concurrent programming with go[R]. 2011.

[17] LOPES B C, AULERR.Getting started with LLVM core libraries[M]. Packt Publishing Ltd, 2014.

论文引用格式:周天昱, 申文博, 杨男子, 等. Docker组件间标准输入输出复制的DoS攻击分析[J]. 网络与信息安全学报, 2020, 6(6): 45-56.

ZHOU T Y, SHEN W B, YANG N Z, et al. The analysis of DoS attacks on Docker inter-component stdio copy[J]. Chinese Journal of Network and Information Security, 2020, 6(6): 45-56.

Analysis of DoS attacks on Docker inter-component stdio copy

ZHOU Tianyu1,2, SHEN Wenbo2, YANG Nanzi3,LI Jinku3,QIN Chenggang4,YU Wang4

1. Zhejiang University NGICS Platform, Hangzhou 310027, China 2. School of Cyber Science and Technology, Zhejiang University, Hangzhou 310027, China 3. School of Cyber Engineering, Xidian University, Xi'an 710071, China 4. Ant Financial Services Group, Hangzhou 310000, China

In recent years, Docker has been widely deployed due to its flexibility and high scalability. However, its modular design leads to the DoS attacks on inter-component communication. A new DoS attack that outputs to stdout, causing high CPU usages among different Docker components. Analysis shows that the stdout output triggers the goroutines of Docker components. To find all goroutines setup paths, using the static analysis method to analyze the Docker components systematically was proposed. A static analysis framework was designed and implemented, and evaluated on Docker source code. The results show that static analysis framework finds 34 paths successfully, while 22 of them are confirmed by runtime verification.

container, Docker components, DoS attack, static analysis

s: Leading Innovative and Entrepreneur Team Introduction Program of Zhejiang Province (2018R01005), The Key R&D Program of Shaanxi Province (2019ZDLGY12-06), Fundamental Research Funds for the Central Universities(Zhejiang University NGICS Platform

TP393

A

10.11959/j.issn.2096−109x.2020074

周天昱(1995− ),男,浙江杭州人,浙江大学硕士生,主要研究方向为容器安全。

申文博(1989− ),男,浙江大学研究员、博士生导师,主要研究方向为操作系统安全、容器安全、软件及系统攻防、硬件安全、程序分析。

杨男子(1996− ),男,陕西咸阳人,西安电子科技大学硕士生,主要研究方向为容器安全。

李金库(1976− ),男,河北保定人,西安电子科技大学教授、博士生导师,主要研究方向为系统与网络安全、移动安全、云计算及其安全。

秦承刚(1983− ),男,江苏徐州人,主要研究方向为操作系统、系统安全。

喻望(1986− ),男,重庆人,主要研究方向为Linux内核、安全容器技术。

2020−07−01;

2020−09−24

申文博,shenwenbo@zju.edu.cn

浙江省引进培育领军型创新创业团队(2018R01005);陕西省重点研发计划基金(2019ZDLGY12-06);中央高校基本科研业务费专项基金(浙江大学NGICS大平台)

猜你喜欢

字段调用实例
带钩或不带钩选择方框批量自动换
核电项目物项调用管理的应用研究
浅谈台湾原版中文图书的编目经验
系统虚拟化环境下客户机系统调用信息捕获与分析①
无正题名文献著录方法评述
无正题名文献著录方法评述
完形填空Ⅱ
完形填空Ⅰ
利用RFC技术实现SAP系统接口通信
C++语言中函数参数传递方式剖析