
1.3 测试、分析环境搭建
1.3.1 虚拟化、容器、Docker
1)虚拟化与容器
虚拟化技术已经成为一种被大家广泛认可的服务器资源共享方式,它可以在按需构建操作系统实例的过程中,为系统管理员提供极大的灵活性。起初,大家普遍认为基于Hypervisor的方式可以在最大限度上提供灵活性。所有虚拟机实例都能够运行任何其所支持的操作系统,而不受其他实例的影响。然而,越来越多的用户发现Hypervisor提供这样一种广泛支持的特性其实是在给自己制造麻烦。从Hypervisor环境的角度来说,每个虚拟机实例都需要运行客户端操作系统的完整副本及其包含的大量应用程序;从实际运行的角度来说,由此产生的沉重负载将会影响虚拟机工作效率及性能表现。
因此,又出现了容器(Container)技术。容器与虚拟机的区别如图1-9、表1-6所示。虚拟机包括应用程序、必需的库或二进制文件及完整的客户操作系统,需要更多的资源。容器包括应用程序及其所有的依赖项。但是,容器之间共享操作系统内核,在宿主操作系统上的用户空间中作为独立进程运行,因此容器所需的资源要少得多(例如,它们不需要一个完整的操作系统),所以容器易于部署且可快速启动,具有更高的密度,在同一硬件单元上可以运行更多服务,从而降低成本。容器的主要目标是使环境(依赖项)在不同的部署中保持不变。也就是说,可以在计算机上调试容器,然后将其部署到保证具有相同环境的另一台计算机上。

图1-9 容器与虚拟机的区别
表1-6 容器与虚拟机的区别

2)Docker
Docker是一个开源的应用容器引擎,可以轻松地为任何应用创建一个轻量级的、可移植的、自给自足的容器。开发者在本地主机上或服务器上编译测试的容器可以批量地在生产环境中部署,包括VMs(虚拟机)、裸金属、OpenStack集群和其他的基础应用平台。可以简单地理解为,Docker类似于集装箱,各式各样的货物,经过集装箱的标准化进行托管,而集装箱和集装箱之间没有影响。也就是说,Docker平台就是一个软件集装箱化平台,这就意味着我们自己可以构建应用程序,将其依赖关系一起打包到一个容器中,然后这容器就很容易运送到其他的机器上并运行,而且非常易于装载、复制、移除,因此非常适合软件弹性架构。
3)Docker镜像
操作系统分为内核和用户空间。对于Linux而言,内核启动后,会挂载根(Root)文件系统为其提供用户空间支持。而Docker镜像(Image)就相当于一个根文件系统。Docker镜像是一个特殊的文件系统,除了提供容器运行时所需的程序、库、资源、配置等文件,还包含一些为运行时准备的配置参数(如匿名卷、环境变量、用户等)。
镜像和容器的关系,就像是面向对象程序设计中的类和实例一样,镜像是静态的定义,容器是镜像运行时的实体。容器可以被创建、启动、停止、删除、暂停等。
容器的实质是进程,但与直接在宿主执行的进程不同,容器进程运行于自己的独立的命名空间。
4)镜像仓库
镜像构建完成后,可以很容易地在当前宿主上运行,但是,如果需要在其他服务器上使用这个镜像,就需要一个集中存储、分发镜像的服务,Docker Registry就是这样的服务。一个Docker Registry中可以包含多个仓库(Repository);每个仓库可以包含多个标签(Tag);每个标签对应一个镜像。所以说,镜像仓库是Docker用来集中存放镜像文件的地方,类似于代码仓库。
通常,一个仓库会包含同一个软件不同版本的镜像,而标签就常用于对应该软件的各个版本。可以通过<仓库名>:<标签>的格式来指定具体是这个软件哪个版本的镜像。如果不给出标签,将以latest作为默认标签。
5)Docker安装
本书的大多数测试、分析是在Ubuntu20.04上进行的,所以此处只给出Ubuntu20.04上Docker的安装与使用方法。
可以使用如下命令安装Docker:

安装完成后可以输入docker version命令,检验是否安装成功,如图1-10所示。

图1-10 docker version命令输出
6)Docker镜像常用命令
docker image pull是下载镜像的命令。镜像从远程镜像仓库服务的仓库中下载。默认情况下,镜像会从Docker Hub的仓库中拉取。例如,docker image pull alpine:latest命令的含义是从Docker Hub的alpine仓库中拉取标签为latest的镜像。
docker image rm是删除镜像的命令。例如,docker image rm alpine:latest命令的含义是删除alpine:latest镜像。
docker image ls命令列出了本地Docker主机上存储的镜像。可以通过--digests参数来查看镜像的SHA256签名。本书在测试时,会生成大量的镜像,部分镜像如图1-11所示。
7)Docker创建镜像
当我们从Docker镜像仓库中下载的镜像不能满足我们的需求时,可以通过使用Dockerfile命令来创建一个新的镜像。Dockerfile是一个用来构建镜像的文本文件,其文本内容包含一条条构建镜像所需的命令和说明。以如下Dockerfile为例定制一个nginx镜像(构建好的镜像内会有一个/usr/share/nginx/html/index.html文件)。
FROM nginx
RUN echo '这是一个本地构建的nginx镜像' > /usr/share/nginx/html/index.html

图1-11 使用docker image ls命令列出的本书测试分析中得到的镜像(部分)
其中用到了两个关键字FROM、RUN。
● FROM:定制的镜像都是基于FROM指定的镜像,这里的nginx就是定制需要的基础镜像。后续的操作都是基于nginx的。
● RUN:用于执行后面跟着的命令行命令,有以下两种格式。
shell格式:

exec格式:

除了FROM、RUN,还有很多关键字,Dockerfile常用的关键字及其作用、格式描述如表1-7所示。
表1-7 Dockerfile常用的关键字及其作用、格式描述

续表

Dockerfile编写完成后,可以使用docker build命令创建一个新的镜像,格式如下:

-t是给镜像指定一个tag,dir是Dockerfile所在的目录。
8)Docker容器常用命令
启动容器docker run[OPTIONS]IMAGE[COMMAND][ARG...],常用选项说明如下:
● -d,--detach=false,指定容器运行于前台还是后台,默认为false。
● -i,--interactive=false,打开STDIN,用于控制台交互。
● -t,--tty=false,为容器重新分配一个伪输入终端,通常与-i同时使用。
● -w,--workdir="",指定容器的工作目录。
● -e,--env=[],指定环境变量,容器中可以使用该环境变量。
● -P,--publish-all=false,随机端口映射,容器内部端口随机映射到主机的端口。
● -p,--publish=[],指定端口映射,格式为主机端口:容器端口。
● -h,--hostname="",指定容器的主机名。
● --rm=false,指定容器停止后自动删除容器(不支持以docker run -d启动的容器)。
● -v,--volume=[],给容器挂载存储卷,挂载到容器的某个目录。
例如:
(1)运行一个在后台执行的容器,同时还能用控制台管理。

(2)运行一个带有命令在后台不断执行的容器,不直接展示容器内部信息。

(3)运行一个在后台不断执行的容器,同时带有命令,程序被中止后还能重启继续运行,还能用控制台管理。

(4)为容器指定一个名字。

(5)容器暴露80端口,并指定宿主机80端口与其通信(前面的是宿主机端口,后面的是容器需暴露的端口)。

(6)指定容器内目录与宿主机目录共享(前面的是宿主机文件夹,后面的是容器需共享的文件夹)。

此外,还有查看所有容器命令docker ps -a,以及导出容器命令docker export。例如:

导入容器,可以使用docker import命令从容器快照文件中导入镜像。
1.3.2 HAS2020资格赛的文件结构
可从GitHub上下载HAS2020资格赛的题目。如图1-12所示,每个文件夹的名称与表1-1中的“内部名称”一一对应,基本上每个文件夹对应一个挑战赛的题目。
打开每个挑战题的文件夹,一般有challenge、solver两个文件夹,个别的挑战题会多一个generator文件夹。jackson挑战题对应文件夹下的文件如图1-13所示。

图1-12 HAS2020资格赛的文件结构

图1-13 jackson挑战题对应文件夹下的文件
challenge文件夹下有一个Dockerfile文件,其作用是生成挑战题镜像,solver文件夹下也有一个Dockerfile文件,其作用是生成解题镜像,这是主办方给出的挑战题的解答,用于检验挑战题是否正确,注意,对于主办方给出的解答,参赛者看不到,这只是给主办方使用的。generator文件夹下一般也有一个Dockerfile文件,其作用是生成挑战题所需要的文件,如一个充满随机数的文件等。
在每个挑战题的目录下都有一个Makefile文件,因此打开Ubuntu终端,执行make build命令,生成本题所需要的challenge、solver、generator镜像。也可以分别执行:

在Ubuntu终端中,执行make test命令,首先运行challenge容器,然后运行solver容器,正常情况下最后输出一个flag,表示题目设计、编码正确,解题也正确。
为了更直观地理解HAS挑战赛的题目结构、原理,接下来本书会以HAS2020资格赛中用于熟悉比赛环境的题目basic-file为例进行介绍。
1.3.3 basic-file示例
1)文件结构
basic-file挑战题的目的是使参赛者熟悉HAS挑战赛的一种题目模式:主办方会给参赛者一些文件,其中隐藏有flag信息,参赛者找到flag,并提交给主办方,从而得到对应的分数。basic-file挑战题的文件结构如图1-14所示,可见其只有generator、solver两个文件夹。

图1-14 basic-file挑战题的文件结构
2)Makefile文件
打开Makefile文件,其内容如下:

通过前文的介绍,我们对生成generator、solver镜像的语句是熟悉的。当调用make test命令时,首先运行challenge容器,然后运行solver容器,因为此处没有challenge容器,所以就会首先运行generator容器,然后运行solver容器。
观察使用make test命令时调用的第一条docker run命令:
● 运行generator容器,其中指定了容器名称是basic-file:generator。
● 设置一个环境变量FLAG,即需要参赛者得到的flag。
● 将本地的data目录加载到容器的out目录。
观察使用make test命令时调用的第二条docker run命令:
● 运行solver容器,其中指定了容器名称是basic-file:solver。
● 设置一个环境变量DIR,其值为/mnt。
● 将本地的data目录加载到容器的mnt目录。
3)generator文件夹
generator文件夹下有一个Dockerfile文件,内容如下:

其中用到了generator-base文件夹下的upload,此处不详细解释upload文件的作用,读者可以理解该文件就是用于宿主机与容器交互的。
从这个Dockerfile中可以发现:
● 该容器基于Ubuntu18.04。
● 编译时会将make_challenge.sh文件复制到容器中。
● 容器启动后会执行make_challenge.sh文件。
打开make_challenge.sh文件,内容如下:

该文件的作用就是先将环境变量FLAG保存到flag.txt文件中,然后压缩为flag.tar.gz,最后使用upload传递给宿主机。
4)solver文件夹
solver文件夹下有一个Dockerfile文件,内容如下:

从这个Dockerfile中可以发现:
● 该容器基于Ubuntu18.04。
● 编译时会将solve.sh文件复制到容器中。
● 容器启动后会执行solve.sh文件。
打开solve.sh文件,内容如下:

该文件的作用就是解压缩flag.tar.gz文件,该文件与宿主机和两个容器之间通过data文件夹实现共享,读取其中的flag信息。
5)测试
在basic-file目录下,执行make build命令,输出如图1-15所示。使用docker image ls命令查看新得到的镜像,结果如图1-16所示。使用make test命令测试新得到的镜像,结果如图1-17所示。可以发现分别运行了basic-file:generator、basic-file:solver两个容器,最后得到flag信息。
为了更加了解其中的过程,可以不使用make test命令做测试,而是分别执行两次docker run命令。在终端中执行如下语句:

可以发现,在宿主机的根目录下新建了一个data文件夹,其中正是文件flag.tar.gz,解压缩后是文件flag.txt,其内容就是上面传输的环境变量FLAG。对于参赛者而言,需要做的就是获取其中的flag的值,并提交给主办方。solver是主办方做的自动验证题目是否可以正常解答的容器,可以在终端中执行如下语句,终端会输出flag信息。

在真实的HAS挑战赛中,参赛者是不知道solver文件夹的,只是会得到challenge或者generator提供的信息,有时是一个文件,有时是一个链接地址,参赛者要做的就是依据这些信息得到flag的值,并提交给主办方。在这里flag的值是在运行generator容器时指定的,实际上在比赛中,每个参赛者都会有一个ticket,提交ticket给主办方,主办方在运行generator容器时,会随机生成一个flag的值,保证每个参赛者的flag的值都互不相同。

图1-15 在basic-file目录下使用make build命令得到的输出

图1-16 使用docker image ls命令查看新得到的镜像

图1-17 使用make test命令测试新得到的镜像
本节通过basic-file挑战题介绍了HAS挑战赛的题目结构、原理,其模式是通过文件的形式给出挑战信息,实际上还有其他形式,如链接地址,在本书后面分析具体题目时,将会详细介绍。