社区所有版块导航
Python
python开源   Django   Python   DjangoApp   pycharm  
DATA
docker   Elasticsearch  
aigc
aigc   chatgpt  
WEB开发
linux   MongoDB   Redis   DATABASE   NGINX   其他Web框架   web工具   zookeeper   tornado   NoSql   Bootstrap   js   peewee   Git   bottle   IE   MQ   Jquery  
机器学习
机器学习算法  
Python88.com
反馈   公告   社区推广  
产品
短视频  
印度
印度  
Py学习  »  docker

请问:可以用容器镜像推出Dockerfile吗?

Linux就该这么学 • 4 月前 • 127 次点击  

原文链接:https://www.cnblogs.com/Stephen/p/17561440.html

随着Docker Hub和TreeScale等公共Docker Registry变得越来越流行,管理员和开发人员下载不明来源的Docker镜像变得越来越常见。在大多数情况下,便利性胜过可预知的风险。在通常情况下,当一个Docker镜像被发布后,它会直接出现在列表中、git仓库中或通过相关链接提供。有时候镜像并没有提供Dockerfile。即使提供了Dockerfile,我们也很难保证预构建的镜像就是由给出的Dockerfile构建的,这些镜像对于我们而言, 就是一个黑盒子,我们甚至无法保证其使用的安全性。

也许您并不关心安全漏洞, 您可能只是想更新平时用的比较多的镜像, 例如nginx,使其能够运行在最新版本的Ubuntu上。又或者,你会想要发布一个更优化的镜像,因为另一个发行版的编译器更适合在编译时生成二进制文件。

不管什么原因,我们都需要将镜像恢复成Dockerfile的选项。Docker镜像并不是一个黑盒子。重建Dockerfile所需的大部分信息都可以被检索到。通过观察Docker镜像内部并检查其内部结构,我们将能够从一个任意的预编译容器中重建一个Dockerfile。

在本文中,我们将展示如何使用两个工具从镜像中重建Dockerfile: 前面提及的Dedockify是一个的Python脚本,Dive是一个Docker镜像浏览工具。使用的基本流程如下。

 

图片

使用 Dive

 

图片

Dive demo

为了快速了解镜像是如何组成的,我们将使用Dive学习一些高级的、可能对我们来说不熟悉的Docker概念。Dive工具可以检查Docker镜像的每一层(Layer)。

让我们创建一个简单的Dockerfile,用于测试。

把这个代码段直接贴到装有Docker的linux主机命令行中:

mkdir$HOME/test1
cd$HOME/test1
cat > Dockerfile <EOF ; touch testfile1 testfile2 testfile3
FROM scratch
COPY testfile1 /
COPY testfile2 /
COPY testfile3 /
EOF

输入以上内容并回车,我们就创建了一个新的Dockerfile,并在同一目录下填充了3个零字节的测试文件。

ls
Dockerfile  testfile1  testfile2  testfile3

现在,让我们使用这个Dockerfile构建一个镜像,并标记为example1。

docker build . -t example1

构建example1镜像时会产生以下输出:

Sending build context to Docker daemon  3.584kB
Step 1/4 : FROM scratch
--->
Step 2/4 : COPY testfile1 /
---> a9cc49948e40
Step 3/4 : COPY testfile2 /
---> 84acff3a5554
Step 4/4 : COPY testfile3 /
---> 374e0127c1bc
Successfully built 374e0127c1bc
Successfully tagged example1:latest

现在我们刚构建的example1镜像就已经完成了:

docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
example1            latest              374e0127c1bc        31 seconds ago      0B

由于没有可执行文件,所以该镜像将无法运行。我们仅将其作为一个简化的示例,说明如何在Docker镜像中查看存储层(Layer)。

我们可以从镜像的大小看出,这里没有源镜像。我们使用scratch来代替源镜像,它让Docker使用一个零字节的空白镜像作为源镜像。然后,我们通过复制三个额外的零字节测试文件来修改空白镜像,并将修改标记为example1。

现在,让我们用Dive来查看这个新镜像。

docker run --rm -it \
    -v /var/run/docker.sock:/var/run/docker.sock \
    wagoodman/dive:latest example1

执行上述命令将自动从Docker Hub拉取wagoodman/dive镜像,并产生Dive的输出。

Unabletofindimage'wagoodman/dive:latest'locally
latest:Pullingfromwagoodman/dive
89d9c30c1d48:Pullcomplete
5ac8ae86f99b:Pullcomplete
f10575f61141:Pullcomplete
Digest:sha256:2d3be9e9362ecdcb04bf3afdd402a785b877e3bcca3d2fc6e10a83d99ce0955f
Status:Downloadednewerimageforwagoodman/dive:latest
Image Source:docker://example-image
Fetchingimage...(thiscantakeawhileforlargeimages)
Analyzingimage...
Buildingcache...

 

图片

在列表中上下选择镜像的三个图层,在右侧显示的目录树中找到三个文件。

 

图片

我们可以看到右侧的内容随着选择每一层而变化。当每个文件被复制到一个空白的Docker从头镜像时,它被存储为一个新的层。

 

图片

如果您注意到的话, 我们还可以看到生成每个层所使用的命令。我们还可以看到源文件和更新文件的哈希值。

如果我们注意到Command:部分,我们应该看到以下内容:

#(nop) COPY file:e3c862873fa89cbf2870e2afb7f411d5367d37a4aea01f2620f7314d3370edcc in /
#(nop) COPY file:2a949ad55eee33f6191c82c4554fe83e069d84e9d9d8802f5584c34e79e5622c in /
#(nop) COPY file:aa717ff85b39d3ed034eed42bc1186230cfca081010d9dde956468decdf8bf20 in /

每个命令都提供了Dockerfile中用于生成镜像的原始命令。但是,原始文件名丢失了。看来恢复该信息的唯一方法是观察目标文件系统的变化,或者根据其他细节进行推断。稍后再详述。

Docker History

除了像dive这样的第三方工具之外,我们还可以随手使用的工具是docker history。如果我们在example1镜像上使用docker history命令,就可以查看我们在Dockerfile中创建该镜像时使用的条目。

docker history example1

运行玩应该得到以下结果:

IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
374e0127c1bc        25 minutes ago      /bin/sh -c #(nop) COPY file:aa717ff85b39d3ed…   0B
84acff3a5554        25 minutes ago      /bin/sh -c #(nop) COPY file:2a949ad55eee33f6…   0B
a9cc49948e40        25 minutes ago      /bin/sh -c #(nop) COPY file:e3c862873fa89cbf…   0B

CREATED BY列中的所有内容都被截断了。这些是通过Bourne shell传递的Dockerfile指令。这些信息对于重新创建我们的Dockerfile可能很有用,虽然在这里被截断了,但是我们也可以通过使用no-trunc选项来查看完整的信息:

$ docker history example1 --no-trunc
IMAGE                                                                     CREATED             CREATED BY                                                                                           SIZE                COMMENT
sha256:374e0127c1bc51bca9330c01a9956be163850162f3c9f3be0340bb142bc57d81   29 minutes ago      /bin/sh -c #(nop) COPY file:aa717ff85b39d3ed034eed42bc1186230cfca081010d9dde956468decdf8bf20 in /    0B
sha256:84acff3a5554aea9a3a98549286347dd466d46db6aa7c2e13bb77f0012490cef   29 minutes ago      /bin/sh -c #(nop) COPY file:2a949ad55eee33f6191c82c4554fe83e069d84e9d9d8802f5584c34e79e5622c in /    0B
sha256:a9cc49948e40d15166b06dab42ea0e388f9905dfdddee7092f9f291d481467fc   29 minutes ago      /bin/sh -c #(nop) COPY file:e3c862873fa89cbf2870e2afb7f411d5367d37a4aea01f2620f7314d3370edcc in /    0B

虽然这有一些有用的信息,但从命令行还原它可能还有些挑战。我们也可以使用docker inspect。然而,在本文中,我们将专注于使用Python的Docker Engine API。

使用 Python Docker Engine API

Docker发布了一个针对Docker Engine API的Python库,允许在Python中管理Docker。在下面的示例中,我们可以通过运行下面的Python 3代码来恢复与docker history类似的信息:




    
#!/usr/bin/python3

import docker

cli = docker.APIClient(base_url='unix://var/run/docker.sock')
print (cli.history('example1'))

输出结果如下:

[{'Comment': '', 'Created': 1583008507, 'CreatedBy': '/bin/sh -c #(nop) COPY file:aa717ff85b39d3ed034eed42bc1186230cfca081010d9dde956468decdf8bf20 in / ', 'Id': 'sha256:374e0127c1bc51bca9330c01a9956be163850162f3c9f3be0340bb142bc57d81', 'Size': 0, 'Tags': ['example:latest']}, {'Comment': '', 'Created': 1583008507, 'CreatedBy': '/bin/sh -c #(nop) COPY file:2a949ad55eee33f6191c82c4554fe83e069d84e9d9d8802f5584c34e79e5622c in / ', 'Id': 'sha256:84acff3a5554aea9a3a98549286347dd466d46db6aa7c2e13bb77f0012490cef', 'Size': 0, 'Tags': None}, {'Comment': '', 'Created': 1583008507, 'CreatedBy': '/bin/sh -c #(nop) COPY file:e3c862873fa89cbf2870e2afb7f411d5367d37a4aea01f2620f7314d3370edcc in / ', 'Id': 'sha256:a9cc49948e40d15166b06dab42ea0e388f9905dfdddee7092f9f291d481467fc', 'Size': 0, 'Tags': None}]

根据输出结果,我们可以发现, 如果重建Dockerfile的内容, 只需要解析所有相关数据并将其顺序反转一下。但是正如我们之前看到的,我们也注意到在COPY指令中有一些被哈希(Hash)过的内容。如前所述,这里的被哈希过的内容代表从层外使用的文件名。这些信息无法直接恢复。然而,正如我们在 Dive 中看到的,当我们搜索对该镜像层所做的更改时,我们可以推断出这些名称。有时,在原始复制指令将目标文件名作为目标的情况下,也可以推断出这些文件名。在其他情况下,文件名可能并不重要,允许我们使用任意文件名。而在其他情况下,虽然更难评估,但我们可以推断出在系统中其他地方被反向引用的文件名,例如在脚本或配置文件等支持依赖中。但无论如何,搜索层之间的所有变化是最可靠的。

 

图片

Dedockify

让我们再深入几步。为了更好地逆向该镜像转换为Dockerfile,我们需要解析所有内容并将其重新格式化为可读的形式。为了简化我们的实验, 以下代码已经可以从GitHub上的Dedockify仓库获取。感谢 LanikSJ 所有基础工作和编码。

from sys import argv
import docker

classImageNotFound(Exception):
    pass

classMainObj:
    def__init__(self):
        super(MainObj, self).__init__()
        self.commands = []
        self.cli = docker.APIClient(base_url='unix://var/run/docker.sock')
        self._get_image(argv[-1])
        self.hist = self.cli.history(self.img['RepoTags'][0])
        self._parse_history()
        self.commands.reverse()
        self._print_commands()

    def_print_commands(self):
        for i in self.commands:
            print(i)

    def_get_image(self, img_hash):
        images = self.cli.images()
        for i in images:
            if img_hash in i['Id']:
                self.img = i
                return
        raise ImageNotFound("Image {} not found\n".format(img_hash))

    def_insert_step(self, step):
        if"#(nop)"in step:
            to_add = step.split("#(nop) ")[1]
        else:
            to_add = ("RUN {}".format(step))
        to_add = to_add.replace("&&""\\\n    &&")
        self.commands.append(to_add.strip(' '))

    def_parse_history(self, rec=False):
        first_tag = False
        actual_tag = False
        for i in self.hist:
            if i['Tags']:
                actual_tag = i['Tags'][0]
                if first_tag andnot rec:
                    break
                first_tag = True
            self._insert_step(i['CreatedBy'])
        ifnot rec:
            self.commands.append("FROM {}".format(actual_tag))

__main__ = MainObj()

生成初始 Dockerfile

如果您已经完成了这一步,那么在您实验的主机上应该有两个镜像:wagoodman/dive和我们自定义的example1镜像。

$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
example1            latest              374e0127c1bc        42 minutes ago      0B
wagoodman/dive      latest              4d9ce0be7689        2 weeks ago         83.6MB

在我们使用dedockify对example1镜像中运行此命令,最终将产生以下结果:

$ python3 dedockify.py 374e0127c1bc
FROM example1:latest
COPY file:e3c862873fa89cbf2870e2afb7f411d5367d37a4aea01f2620f7314d3370edcc in /
COPY file:2a949ad55eee33f6191c82c4554fe83e069d84e9d9d8802f5584c34e79e5622c in /
COPY file:aa717ff85b39d3ed034eed42bc1186230cfca081010d9dde956468decdf8bf20 in /

我们提取到的信息与之前使用 Dive 解析镜像时看到的几乎一致。注意FROM指令显示的是example1:late而不是scratch。在这种情况下,我们的代码对基础镜像做出了不正确的假设。

作为对比, 我们对wagoodman/dive镜像做同样的处理.

$ python3 dedockify.py 4d9ce0be7689
FROM wagoodman/dive:latest
ADD file:fe1f09249227e2da2089afb4d07e16cbf832eeb804120074acd2b8192876cd28 in /
CMD ["/bin/sh"]
ARG DOCKER_CLI_VERSION=
RUN |1 DOCKER_CLI_VERSION=19.03.1 /bin/sh -c wget -O- https://download.docker.com/linux/static/stable/x86_64/docker-${DOCKER_CLI_VERSION}.tgz |     tar -xzf - docker/docker --strip-component=1 \
    &&     mv docker /usr/local/bin
COPY file:8385774b036879eb290175cc42a388877142f8abf1342382c4d0496b6a659034 in /usr/local/bin/
ENTRYPOINT ["/usr/local/bin/dive"]

与example1相比,这个镜像显示了更多的错误。我们看到ADD指令就在FROM指令之前。我们的代码再次做出了错误的假设。我们不知道ADD指令添加了什么。然而,我们可以直观地做出假设,即我们不确定基础镜像是什么。ADD指令可能是用来提取本地的tar文件到根目录。也有可能是用这种方法加载另一个基础镜像。

Dedockify limitation testing

让我们通过创建一个示例Dockerfile来进行实验,在这个示例中我们明确定义了基础镜像。和我们之前做的一样,在一个空目录下,直接从命令行运行下面的代码。

mkdir $HOME/test2
cd $HOME/test2
cat > Dockerfile << EOF ; touch testfile1 testfile2 testfile3
FROM ubuntu:latest
RUN mkdir testdir1
COPY testfile1 /testdir1
RUN mkdir testdir2
COPY testfile2 /testdir2
RUN mkdir testdir3
COPY testfile3 /testdir3
EOF

然后build镜像,将我们的新镜像标记为example2。这将创建一个与之前类似的镜像,只不过不使用scratch,而是使用ubuntu:latest作为基础镜像。

$ docker build . -t example2
Sending build context to Docker daemon  3.584kB
Step 1/7 : FROM ubuntu:latest
 ---> 72300a873c2c
Step 2/7 : RUN mkdir testdir1
 ---> Using cache
 ---> 4110037ae26d
Step 3/7 : COPY testfile1 /testdir1
 ---> Using cache
 ---> e4adf6dc5677
Step 4/7 : RUN mkdir testdir2
 ---> Using cache
 ---> 22d301b39a57
Step 5/7 : COPY testfile2 /testdir2
 ---> Using cache
 ---> f60e5f378e13
Step 6/7 : RUN mkdir testdir3
 ---> Using cache
 ---> cec486378382
Step 7/7 : COPY testfile3 /testdir3
 ---> Using cache
 ---> 05651f084d67
Successfully built 05651f084d67
Successfully tagged example2:latest

由于我们现在有了一个稍微复杂一些的Dockerfile来重建,而且我们也有了生成这个镜像所使用的Dockerfile,因此我们可以做一个对比。

$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
example2            latest              05651f084d67        2 minutes ago       64.2MB
example1            latest              374e0127c1bc        1 hour ago          0B
ubuntu              latest              72300a873c2c        9 days ago          64.2MB
wagoodman/dive      latest              4d9ce0be7689        3 weeks ago         83.6MB

运行dedockify脚本

$ python3 dedockify.py 05651f084d67
FROM ubuntu:latest
RUN /bin/sh -c mkdir testdir1
COPY file:cc4f6e89a1bc3e3c361a1c6de5acc64d3bac297f0b99aa75af737981a19bc9d6 in /testdir1
RUN /bin/sh -c mkdir testdir2
COPY file:a04cdcdf5fd077a994fe5427a04f6b9a52288af02dad44bb1f8025ecf209b339 in /testdir2
RUN /bin/sh -c mkdir testdir3
COPY file:2ed8ccde7cd97bc95ca15f0ec24ec447484a8761fa901df6032742e8f1a2a191 in /testdir3

这与最初的Dockerfile非常吻合。这次没有ADD指令,而FROM指令也是正确的。只要我们的基础镜像是在原始Dockerfile中定义的,并且避免使用scratch或者避免使用ADD指令从tar文件创建基础镜像,我们应该能够比较准确地重建Dockerfile。然而,我们仍然不知道被复制的原始文件的名称。

任意 Dockerfile 重建

现在,让我们尝试使用我们已经讨论过的工具,以正确的方式逆向工程一个Docker容器。我们将使用的容器在上面的示例基础上进行了修改。我们之前的Dockerfile被修改为example3。通过添加一个小的二进制可执行文件,该镜像已具备运行的功能。在Dedockify的GitHub仓库中可以找到源代码。由于这个镜像非常小,我们不需要构建或拉取它。我们可以通过下面的代码段将整个容器复制粘贴到我们的Docker环境中,来展示我们的命令行技巧 :)

uudecode << EOF | zcat | docker load
begin-base64 600 -
H4sICMicXV4AA2V4YW1wbGUzLnRhcgDtXVtvG8cVVnp56UN/QJ/YDQokgETN
zJkrgTykjgsbDSzDURMkshDM5YzFhiJVkkpiCELzH/pP+tYfkf/UsxRNXdxI
spe7lqv5IJF7PTM7Z87MmY9nZxgL2qG1DkN2nkXmtTecQwYrMMfsBHgXgFuV
eXLCI5c26sxdNHQsie2Nm8GYZEYp+l7g6vdim4MWBrgyBjaY4MbIjZ66hezG
OJ7N/ZSyMp1M5tddd9P5qw/3noA11f+XD5998XjnybVpcMa0lNfoH67oHxiI
jV4nhXjP9c/7701WC1pAY/v/+2wyvimNG+zfgLpi/0KbYv+d4KQapmpQNa0G
1WZ15Kc4npMsr7JUjGGMyLUzPEXJc1RCWqFkTtoEL5TgEaLSKlmOiXHnUSlK
jYsQSFacop9jnTHuDNtinP52GRss/r6pL5iM5344xum3tJWHL6rBSfVoMpuP
/SHSXXTFZ5NDuuB8/28znJ5tfTqf+3jwxTwNx9Ug+9EMLxybHM9fP4jT6erg
7vzlanvnCMeX5Sz2dsYRV0cejr+vBuPj0WizenCYXm0+PvQvlhn7cjI6PsTZ
qzNfTabfDccvPhsuc/twPJ++PJoM66I9u2Jn/Ofj4Wgl6nMfcLS8/XSzmtBm
NRqOj3+sTm+h/8b2P/IvcdqvbeiX07je/uXr/h9oZor9dwF/dHQbF74R3sz/
F1RfuKbLi//fAWr993846C/+J0f/aCONRR9/g/4vbXNQShb7LygoKGgTTDkP
1tiEUvkgJajgnTZRA0pAplFqZ1m02hqXjc4+aaVTztx4pzywfvPxH7X1V/0/
oZgu7X8XOKn8NB4M5xjnx9N6ROIPk5ZnI6y7P67aq55+uvvok+3j2XR7NIl+
tD0Lw/Hgwv5q9/zEYuNslz6q/f85MJsd0CBVD4SHEDhGkM4LDIqHQN4JjU5s
5NqiJguRAr3nKpgICoRhyLNiCgHQOLxhfLdN7teVMd7e4uD2AY5Gkzpv14/2
VuPgegyvDA3aOATr5cJkJUs0tMsRacytE6MsGZnp/piCEMaiijHmJJOIihvN
bh5WX0zh/7kqkA5od3t2QI+yFenjw4/Gk6OPe7Wqnuw++/rpzuMnu7295xdU
9bzar29/X6rPyenpRZZFMMG2GGwxsStgoPhAQF8KrZ1grqZb0iR+R5Xie5zO
hpPxgpbpM+hrOnUwnM0nU1LY3sm1AnnfOXCgDDPfnDM834aX9XOclXZvK/aW
Jf3VzrO/fvb4WW97jjPS95RXp5vXyxd9Qd0MSCnsLeQ/2Hn6dS8PRzgAIbLL
TiBEJ9Ej8hRZEmCURW6CcTaD8+h18BhtdsrJIFDGmBigS6w3HPfqTNbCeO8W
2SQjVBK4lW9RDOIW8hUZv+PG8XdWDOI2xWA4mYGkKvYWxQC3kO+YdMYxod5Z
McDNxQB9Zpkg38iJNymG2uxvFi005+RJwRsVQAAIkmURmaF+gwUXvAZjApis
tWaSJyWFoAaKOS1DFJrKR0omQSSNVvO6ABaNz20e/mITc0MOe9c1vJsVHh7N
X3674CKrwXx6jKf7l6jQzar233Ld8lXzl0d1E724eFY3bsOcvx2mWd14Lttt
riUXQXJAZFJn5MLriMHJTA6yT44zHZjHQJ2qZM5watCpBeeevFdMdANJXUkC
7ZMxjCq7TI6cbW+zUkEpqj/RMrBeWInCUuFqj3QZTxays9RrBNT+XBJDh2Qw
wQkfqXcAqjZK5RA5SNIJzywKSTKTMGiYjkoknwJjzgVg1vBwLilYbbwhFzsz
LYTTwqNgRiMEZhMYK7zztZ9gmHUZap8/0WOROCXRenUhT1Q8IlhltBbOihhR
ZhOtoucgwVRolD3DkqprS6bHZzlbmxMNOBJGRYVxLsmDcSGZJBkTjkdlEtW3
IKj4nA0oUJHDYgP5MZQ3qjHKZCrDqJSylvwZhAtPF5PRMfJEt4I0kjvms/Qq
U4VVnp6O6jD3PJpaujKcLqJqaNF5TlrR5lxS5jQ2IfVRv22MAhbInjAaiNIp
zkSGCAacVmAj0IPWDVoKPFCjQMXi0VX7p7eh4N8phI70kElxeg7vhJIKZYpa
qOy9J9Vpck3Qkx2I2qOMIUrSQ46GKrzkqHRb8R+cF/63CzTWfyvxH8LI8vtP
JyjxH/cbje1/DfEfWvDX4j8YL/bfBZbxH02rQYnZ6DBmY51obP/txH8oUX7/
7QSv+LU2g0DezP/nVF+EBij+fxdY6b/FIJC6PN4s/kOqWv/F/gsKCgrag2Jg
EIxFl3RyIWQB2bjkDIiYFeTIUuLJy6SNNUG4wKPl3GeXEgSfWuL/BNTxv6X/
bx+N9d/O+18gVen/u0Dh/+43Gtt/K+9/gWDF/jvBkv9rWg0uvf/lBSZQEqIi
YUoB50E7iU4JFD4ESbcok5Kq6SWbIWjFpbFSCOmUYaxwiR1yiY3tvxX+T3JR
3v/sBK8Cy+4O/yfO+L/y/lcnWOn/rvF/hf8vKCgoaBXGQgwYvEKWwNmY0euo
As/cks/mFGMgBI+BfD7jaJtrHaMnhx1ZMAF9a/M/6dL/d4HG+m9r/qfS/3eC
wv/dbzS2/7bmfyr23wmW/F/TanCR/7PKBZAyeI7SuGDqd0Y9ahVYZj6nJFFq
pZS2UieBDAC05HRxNBKE06nwfx3yf43tv6X5n3iJ/+0Er96ovHP8X/H/O8FK
/3eN/yu//xUUFBS0iqbOekv8nzSs9P9doLH+23n/V4ky/1cnKPzf/UZj+2+H
/wNd7L8TLPm/NXB2K/5vDbGEhf/riv9rbP/t8H/Ayvt/naDE/xX9L/S/mrxv
/Wnc7P9f1P+C/5OyxP8WFBQUtIqmizW1Ff9X+v9u0Fj/Lc3/Z4r/3wkK/3e/
0dj+W4r/K/P/dIMl/7eGNRtX/N8aYgkL/9cV/9fY/lua/0+xYv9doMT/Ff0v
9L9atWL9abwV/1fi/wsKCgpaRdPJetqK/6vX/y39f/torP+W4v94+f2vExT+
736jsf23FP+niv13glf8X/M5+1b8X1lL5H3i/5rafzv8n9Bl/a9OcPfW/1jy
f8X/7wQr/a/WMF5/Gm/F/xX+v6CgoKBVJPK8tUkQIUjIXCXMBoJj0oPO3Btu
jJc86SyRWaFFDOhQ8QxKJ8uya2n9D6bK+h+doLH+25n/j/My/0cnKPzf/UZj
+2/O/wmQr63/a0r8TzdY8n9Nq8FF/o+xoB1a6zBk51lkXpMADhmswByzE+Bd
AG5V5skJj1zaSKm4aOhYEpf4P8EE22KwxcSugIHiAwF9KbR2grlLRGCdsKrf
XuUQrJe25hYlS5SRHDFpoxNDACOzDxhToCpmUcUYc5JJRMWNZjfzihdTuGsc
41719NPdR59sH8+m26NJ9KPtWRiOBxf2V7vnJxYbZ7v0Ue0vScq9qt7dnh3Q
o2xF+vjwo/Hk6ONezUc+2X329dOdx092e3vPq21/dLR9gKPR5Hm1X9/+itic
HXih9EB4CIFjBOm8wKB4CIxMnRkbubaokRoGgd5zFUwEBcIw5FkxRbpC4/AG
frRO/gpHunchT3WGfoEtPTk93azSJH5Hqv4ep7PhZLxgm/sM+vqsItx9Wnmd
Kr/MS9919S3Jbj+NB8M5xvnxtM61P0y6tst3uJ72+4bG/X9z/l+9Pv4DXd7/
6Qa1sbWdxhuP/znXqsT/doJVY9tiGrX9X6t/Y67w/9SPlPF/J/jnw8//8sFq
78Fvf7Xx640/0Hf9/xs68h9OG//+/c8//am38cHGv37+6VFdWTZ7P0ymo/TH
3727jBcUFBQUNMKhHw8zzub9W/B4b4sb/H8O0lzyBemIEuX9n06wd1I9WFId
FVPOgzU2oVQ+SAkqeKdN1IASaKiPUrsFr2aNy0Znn8hNTzlz453ywBZ1iIbf
z/BosutfzOrRPP7oD49GCIORryNM6iH95/WIcXGyaaTg+eCTUl1fJBsJW9+y
uCRsfXPskbD1LdhRl9na3v4lYU1530vC1sdLVPun+8VP/SVMyVZnw/lkOsRZ
W2nc9PsvB7ja/pNBlPa/C5ysWuia61420mv4Pej0tNhcQUFBwV3GfwHMszUX
AMIAAA==
====
EOF

直接从命令行运行后会加载一个新的镜像 example3:latest。

$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
example3 latest 059a3878de45 5 minutes ago 63B

现在,让我们尝试重建Dockerfile。

$ python3 dedockify.py 059a3878de45
FROM example3:latest
WORKDIR /testdir1
COPY file:322f9f92e3c94eaee1dc0d23758e17b798f39aea6baec8f9594b2e4ccd03e9d0 in testfile1
WORKDIR /testdir2
COPY file:322f9f92e3c94eaee1dc0d23758e17b798f39aea6baec8f9594b2e4ccd03e9d0 in testfile2
WORKDIR /testdir3
COPY file:322f9f92e3c94eaee1dc0d23758e17b798f39aea6baec8f9594b2e4ccd03e9d0 in testfile3
WORKDIR /app
COPY file:b33b40f2c07ced0b9ba6377b37f666041d542205e0964bc26dc0440432d6e861 in hello
ENTRYPOINT ["/app/hello"]

这为我们提供了一个基础Dockerfile。由于example3:latest是这个镜像的名称,我们可以从上下文假设它使用了`scratch`。现在,我们需要看看有哪些文件被复制到了/testdir1、/testdir2、/testdir3和/app。让我们在Dive中运行这个镜像,看看如何恢复丢失的数据。

docker run - rm -it \
 -v /var/run/docker.sock:/var/run/docker.sock \
 wagoodman/dive:latest example3:latest

 

图片

如果您向下选择到最后一层,您将能够看到所有丢失的数据填充到右侧的目录树中。每个目录都复制了名为testfile1、testfile2和testfile3的零字节文件。在最后一层中,一个名为hello的63字节文件被复制到了/app目录中。

让我们恢复这些文件!由于无法直接从镜像中复制文件,因此我们需要先创建一个容器。

$ docker run -td --name example3 example3:latest
6fdca182a128df7a76e618931c85a67e14a73adc69ad23782bc9a5dc29420a27

现在,让我们使用下面从Dive恢复的路径和文件名将我们需要的文件从容器复制到主机。

mkdir $HOME/test3
cd $HOME/test3
docker cp example3:/testdir1/testfile1 .
docker cp example3:/testdir2/testfile2 .
docker cp example3:/testdir3/testfile3 .
docker cp example3:/app/hello .

我们可能得先检查我们的容器是否仍在运行。

$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
6fdca182a128 example3:latest "/app/hello"2 minutes ago Up 2 minutes wizardly_lamport

如果容器由于某种原因没有运行,没关系。我们可以验证它的状态,看看它是否已经停止。

$ docker container ls -a

我们还可以查看运行日志。

$ docker logs 6fdca182a128
Hello, world!

它似乎在运行一个输出Hello, world!程序。实际上,在这种情况下,Hello, world!程序并不是被设计成始终运行的。在19.03.6版本的Docker中,可能存在一个错误,导致程序无法正常终止。目前这是可以接受的。容器可以活动或停止;应用程序不需要持久化来恢复我们需要的任何数据。处于任何状态的容器都只需要从我们正在提取数据的源镜像中生成。

通过运行恢复的可执行文件来验证其行为,我们应该看到以下内容:

$ ./hello
Hello, world!

使用我们之前生成的Dockerfile,我们可以更新它以包含所有新的细节。这包括将FROM指令更新为从头开始,以及我们在使用Dive探索时发现的所有文件名。

FROM scratch
WORKDIR /testdir1
COPY testfile1 .
WORKDIR /testdir2
COPY testfile2 .
WORKDIR /testdir3
COPY testfile3 .
WORKDIR /app
COPY hello .
ENTRYPOINT ["/app/hello"]

再次,将所有文件合并到一个共享文件夹中,我们就可以运行我们逆向工程的Dockerfile了。

让我们先构建一个镜像。

$ docker build . -t example3:recovered
Sending build context to Docker daemon 4.608kB
Step 1/10 : FROM scratch
 - ->
Step 2/10 : WORKDIR /testdir1
 - -> Running in 5e8e47505ca6
Removing intermediate container 5e8e47505ca6
 - -> d30a2f002626
Step 3/10 : COPY testfile1 .
 - -> 4ac46077a588
Step 4/10 : WORKDIR /testdir2
 - -> Running in 8c48189da985
Removing intermediate container 8c48189da985
 - -> 7c7d90bc2219
Step 5/10 : COPY testfile2 .
 - -> 5b40d33100e1
Step 6/10 : WORKDIR /testdir3
 - -> Running in 4ccd634a04db
Removing intermediate container 4ccd634a04db
 - -> f89fdda8f059
Step 7/10 : COPY testfile3 .
 - -> 9542f614200d
Step 8/10 : WORKDIR /app
 - -> Running in 7614b0fdba42
Removing intermediate container 7614b0fdba42
 - -> 6d686935a791
Step 9/10 : COPY hello .
 - -> cd4baca758dd
Step 10/10 : ENTRYPOINT ["/app/hello"]
 - -> Running in 28a1ca58b27f
Removing intermediate container 28a1ca58b27f
 - -> 35dfd9240a2e
Successfully built 35dfd9240a2e
Successfully tagged example3:recovered

然后我们来运行这个镜像:

$ docker run - name recovered -dt example3:recovered
0f696bf500267a996339b522cf584e010434103fe82497df2c1fa58a9c548f20
$ docker logs recovered
Hello, world!

为了进一步验证,让我们再次使用 Dive 检查镜像。

docker run - rm -it \
 -v /var/run/docker.sock:/var/run/docker.sock \
 wagoodman/dive:latest example3:recovered

 

图片

此镜像显示的文件与原镜像相同。将两个镜像并排比较,它们都完全匹配。两者显示的文件大小相同。两者的功能完全相同。

以下是用于生成example3镜像的原始Dockerfile。

FROM alpine:3.9.2
RUN apk add - no-cache nasm
WORKDIR /app
COPY hello.s /app/hello.s
RUN touch testfile && nasm -f bin -o hello hello.s && chmod +x hello
FROM scratch
WORKDIR /testdir1
COPY - from=0 /app/testfile testfile1
WORKDIR /testdir2
COPY - from=0 /app/testfile testfile2
WORKDIR /testdir3
COPY - from=0 /app/testfile testfile3
WORKDIR /app
COPY - from=0 /app/hello hello
ENTRYPOINT ["/app/hello"]

我们可以看到,虽然我们不能完美地重建它,但我们能够大致重建它。像这样使用多阶段(stage)构建的Dockerfile是无法重建的。这些信息根本就不存在。我们唯一的选择是重建我们实际拥有的镜像的Dockerfile。如果我们有早期构建阶段的镜像,我们可以为每个阶段重构一个Dockerfile,但在这种情况下,我们只有最终构建阶段的镜像。但不管怎样,我们还是成功地从Docker镜像中重现了一个有用的Dockerfile。

后记

通过使用与Dive类似的方法,我们应该能够更新Dedockify的源代码,使其能够自动分析每一层,以恢复所有有用的文件信息。此外,该程序还可以更新为能够自动从容器中恢复文件并将其存储到本地,同时还能自动对Dockerfile进行适当的更新。最后,还可以对程序进行更新,使其能够轻松推断出基础层是否使用了Scratch或其他基础镜像。通过对恢复的Dockerfile语法进行一些额外的修改,Dedockify有可能被更新为在大多数情况下完全自动地将Docker镜像逆向工程为一个功能性的Dockerfile。

END

想要学习Linux系统的读者可以点击"阅读原文"按钮来了解书籍《Linux就该这么学》,同时也非常适合专业的运维人员阅读,成为辅助您工作的高价值工具书!


Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/181530
 
127 次点击