逆向工程: 将docker镜像”反编译”为Dockerfile

Python项目加密实战:用PyArmor和Docker保护你的代码(附完整Dockerfile配置) 本文详细介绍了如何结合PyArmor与Docker,为Python项目构建一套工业级的代码保护与交付体系。通过多阶段Dockerfile设计、自动化资源迁移脚本以及依赖分离策略,实现从构建时加密到运行时加密的安全流程,有效保护核心知识产权,防止代码被反编译或窃取。 阅读详情

逆向工程: 将docker镜像”反编译”为Dockerfile

通过研究Docker镜像的内部结构,对Docker镜像进行逆向工程。

图片

在本文中, 我们将通过理解Docker镜像如何存储数据, 以及如何使用工具查看镜像方方面面的信息来逆向工程一个Docker镜像; 以及如何使用Python的Docker API来构建一个类似Dedockify的工具来创建Dockerfile。

图片

简介

随着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的输出。

Unable to find image 'wagoodman/dive:latest' locally
latest: Pulling from wagoodman/dive
89d9c30c1d48: Pull complete
5ac8ae86f99b: Pull complete
f10575f61141: Pull complete
Digest: sha256:2d3be9e9362ecdcb04bf3afdd402a785b877e3bcca3d2fc6e10a83d99ce0955f
Status: Downloaded newer image for wagoodman/dive:latest
Image Source: docker://example-image
Fetching image... (this can take a while for large images)
Analyzing image...
Building cache...

图片

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

图片

我们可以看到右侧的内容随着选择每一层而变化。当每个文件被复制到一个空白的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

class ImageNotFound(Exception):
    pass

class MainObj:
    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 and not rec:
                    break
                first_tag = True
            self._insert_step(i['CreatedBy'])
        if not 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 -
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
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。


如何系统学习网络安全?

网络安全作为数字时代的核心技术基石,已成为保障各行业安全稳定运行的坚实屏障。筑牢网络安全的防线,掌握先进的防护策略和技能正上升为国家与企业发展的战略优先级。

构建稳固的网络安全能力是一个体系的工程,需要从夯实基础理论出发,持续深化到攻防对抗与应急响应的实战层面。

如果你是准备学习网络安全(黑客)或者正在学习,我可以把我自用的360独家内部资料分享给你,包含以下内容你应该能用得上:

①网络安全学习路线
②20份渗透测试电子书
③安全攻防357页笔记
④50份安全攻防面试指南
⑤安全红队渗透工具包
⑥网络安全必备书籍
⑦100个漏洞实战案例
⑧安全大厂内部视频资源
⑨历年CTF夺旗赛题解析

一、网络安全(黑客)学习路线

网络安全(黑客)学习路线,形成网络安全领域所有的知识点汇总,它的用处就在于,你可以按照上面的知识点去找对应的学习资源,保证自己学得较为全面。

在这里插入图片描述

二、网络安全教程视频

我们在看视频学习的时候,不能光动眼动脑不动手,比较科学的学习方法是在理解之后运用它们,这时候练手项目就很适合了。

三、网络安全CTF实战案例

光学理论是没用的,要学会跟着一起敲,要动手实操,才能将自己的所学运用到实际当中去,这里带来的是CTF&SRC资料&HW资料,毕竟实战是检验真理的唯一标准嘛~

img

四、网络安全面试题

最后,我们所有的作为都是为就业服务的,所以关键的临门一脚就是咱们的面试题内容,所以面试题板块是咱们不可或缺的部分,这里我给大家准备的就是我在面试期间准备的资料。

在这里插入图片描述

网安其实不难,难的是坚持和相信自己,我的经验是既然已经选定网安你就要相信它,相信它能成为你日后进阶的高效渠道,这样自己才会更有信念去学习,才能在碰到困难的时候坚持下去。

机会属于有准备的人,这是一个实力的时代。人和人之间的差距不在于智商,而在于如何利用业余时间,只要你想学习,什么时候开始都不晚,不要担心这担心那,你只需努力,剩下的交给时间!

这份完整版的网络安全学习资料已经上传优快云,朋友们如果需要可以微信扫描下方优快云官方认证二维码免费领取【保证100%免费】

img

Docker镜像源代码保护实战:FastAPI+Vue3项目安全构建指南 在微服务与容器化架构中,Docker镜像已成为应用交付的标准载体。其核心原理是通过分层构建与联合文件系统,实现应用及其依赖的标准化打包与分发。这一技术为DevOps和持续部署带来了巨大价值,但同时也引入了新的安全挑战——镜像中的源代码极易被提取,导致核心业务逻辑与算法泄露。尤其在涉及敏感数据处理、AI模型部署等应用场景时,源码保护从优化项升级为基础安全实践。本文聚焦于**Docker多阶段构建**与**代码混淆**等关键技术,以FastAPI后端与Vue3前端项目为例,系统阐述如何通过构建过程优化与运行时加 阅读详情

相关推荐

构建Android安全分析Docker工作站:一站式解决环境配置难题

在移动应用安全领域,环境配置是开发者与安全研究员面临的首要挑战。容器化技术通过封装应用及其所有依赖,提供了环境一致性与隔离性的核心解决方案。Docker作为主流容器平台,其价值在于能够快速构建、分发和复现复杂的开发与分析环境,极大提升了工程效率。这一技术特别适用于需要特定工具链和依赖版本的安全分析场景,例如Android应用逆向工程与漏洞挖掘。通过将Android SDK、Java运行时、Python分析库以及主流安全工具(如apktool、jadx、Frida)集成到一个**Docker镜像**中,可以创

weixin_33767813的博客 390

通过docker镜像逆向dockerfile

当想让一个容器做两件事情,或者使一个Docker镜像包含来自两个不同镜像的依赖库时,就需要知道每个镜像的Dockerfile。本文介绍了如何通过docker history命令来对Docker镜像进行反向工程,得到它们的Dockerfile,并组织到一个Dockerfile里然后build,从而实现想做的事情。 常言道,“不要重复发明轮子!” 在使用Docker时,构建自己的镜像之前,最好在Docker Hub寻找一些可以直接使用的镜像做练习。把软件架构分布到一系列容器中,每一个容器只做一件事情,这样的效果

西西工作室 1788

CTFShow萌新区全攻略:从密码破解到Web渗透的实战解析

本文为CTFShow萌新区新手提供从密码破解到Web渗透的完整实战攻略。文章深入解析了密码学、隐写术、杂项和Web安全四大核心方向,分享了实用的解题工具、思维方法和绕过技巧,旨在帮助初学者建立系统的安全竞赛解题体系,而不仅仅是提供一份题解(WP)。

weixin_29216957的博客 300

docker镜像反推dockerfile

https://github.com/cucker0/DockerImage2Df https://hub.docker.com/r/cucker/image2df docker—通过images的镜像反推出Dockerfile docker镜像逆向Dockerfile

sky527759的博客 2134

从零部署DeepSeek V4 Flash:NVIDIA DGX Spark实战指南

GPU加速计算已成为现代人工智能基础设施的核心,其原理是通过并行处理架构大幅提升深度学习模型的训练与推理效率。在AI工程实践中,NVIDIA GPU凭借CUDA生态和专用驱动栈,为大规模语言模型部署提供了硬件基础。本文聚焦于企业级AI服务器部署场景,针对NVIDIA DGX Spark多节点系统的硬件初始化、驱动兼容性配置、容器化部署等关键环节,提供从系统层到应用层的完整解决方案。通过vLLM推理框架的实战配置,演示如何将DeepSeek V4 Flash模型高效部署于多GPU环境,涵盖Tensor Par

weixin_32296621的博客 382

ifix5.8连接ab plc的点-通过rslinx

以ifix连接ab plc的点为例 1.先在rslinx上把plc的点导出为opc server 2.使用opc power tool 作为opc client 去连接 3.保存到项目的PDB文件夹下面,并且以节点名命名。 也可以不以节点名命名,系统自动会生成一个以节点名命名的这种文件,就显得很乱。 同时,项目的PDB目录下面必须还得拷过来一个de...

中国智造2025 4114

Docker快速部署DAILA:一键拥有跨平台反编译AI辅助环境

DAILA是一款跨反编译器的AI辅助插件,支持GPT-4、Claude及本地模型,能为反编译工作提供智能分析与辅助功能。通过Docker部署DAILA,你可以快速搭建起跨平台的反编译AI辅助环境,无需复杂配置,让AI助力你的逆向分析工作。 ## 为什么选择Docker部署DAILA? Docker部署方式为DAILA带来了诸多优势,让你的反编译AI辅助环境搭建变得简单高效: - **跨平台兼

gitblog_00438的博客 842

南邮 2024 软件安全 实验四 自动脱壳+手动脱壳

这次这个实验很简单,就是一个简单的脱壳,然后进行爆破就行了,解法非常的多这里不多讲,会简单讲一下特殊情况这个点开是这样子:然后放进peid里面查看,我这个是原版的PEid,所以显示出来是这样:会显示nothing found,但其实如果你使用吾爱破解的魔改过的peid,它就会显示具体的壳名:然后你点击右下角的那个箭头,里面会有很多的插件用来自动脱壳:你也可以使用这个通用脱壳器,但是脱壳后的程序无法运行,这个放到后面讲。

ARbing_an的博客 2563

终极RetDec Docker部署指南:3步快速启动专业反编译服务的容器化方案

RetDec是一款基于LLVM的可重定向机器码反编译器,通过Docker容器化部署可以让开发者和安全研究人员快速搭建专业的反编译环境。本文将详细介绍如何通过Docker一键部署RetDec反编译服务,即使是新手也能在5分钟内完成配置并开始使用。 ## 📦 为什么选择Docker部署RetDec? RetDec作为功能强大的反编译工具,其环境配置涉及LLVM、Capstone等多个依赖库,传统

gitblog_01200的博客 477

腾讯会议录屏损坏修复:使用 recover_mp4 工具的完整解决方案

腾讯会议录屏损坏修复指南 当腾讯会议录屏因电脑蓝屏等意外导致MP4文件损坏时,可使用recover_mp4工具结合FFMPEG进行修复。操作步骤包括:1)准备相同参数正常视频作为参考;2)分析正常视频生成结构文件;3)分离损坏视频的音视频流;4)使用FFMPEG重新封装。该方法能有效修复因意外中断导致的视频索引损坏问题。修复前需将工具和文件放在同一目录下,并确保参考视频与损坏录屏的参数完全一致。

m0_62498496的博客 2628

告别龟速与断连:Arduino IDE下ESP32及ESP32-CAM一站式环境部署指南

本文提供Arduino IDE下ESP32及ESP32-CAM一站式环境部署指南,解决下载速度慢、断连等常见问题。通过国内镜像源加速、详细步骤讲解和ESP32-CAM专属配置技巧,帮助开发者快速完成环境搭建,避免常见陷阱,提升开发效率。

weixin_30576827的博客 771

dex2jar与Docker:容器化部署与使用

你是否曾在不同操作系统间切换时遭遇dex2jar工具依赖冲突?是否因Java版本差异导致反编译结果不一致?本文将通过Docker容器化技术,为你构建一个标准化、可移植的dex2jar工作环境,实现"一次构建,到处运行"的逆向工程工作流。 读完本文你将掌握: - 基于Docker的dex2jar环境搭建指南 - 容器化反编译Android应用的完整流程 - 多版本工具并行使用的容器编排技巧 - 企...

gitblog_00574的博客 675

揭秘AI人工智能中Stable Diffusion的模型微调技巧

Stable Diffusion作为一种强大的文本到图像生成模型,在艺术创作、设计、娱乐等多个领域展现出了巨大的应用潜力。然而,其默认的模型输出可能无法满足特定用户或特定场景的个性化需求。本文章的目的在于深入揭秘Stable Diffusion的模型微调技巧,让读者能够根据自己的需求对模型进行定制化调整,生成符合特定风格、主题或要求的图像。范围涵盖了从基础概念到实际操作,从理论原理到项目实战的全方位内容,旨在为读者提供一个系统且深入的学习指南。

大模型应用工坊 950

三步搭建容器化RetDec反编译环境:告别依赖地狱,聚焦逆向分析

在软件逆向工程与安全分析领域,反编译技术是将机器码转换为高级语言伪代码的核心手段,其原理是通过分析二进制文件的指令、数据与控制流,重建程序逻辑结构。这项技术的价值在于能深入理解闭源软件内部机制、分析恶意代码行为或进行漏洞挖掘。传统反编译工具部署常面临依赖复杂、环境配置繁琐等挑战,尤其在跨平台场景下。容器化技术通过封装应用及其运行环境,提供了开箱即用、环境隔离的解决方案,能显著提升工具部署效率与可移植性。本文以RetDec这一支持多架构的开源反编译器为例,结合Docker容器化实践,详细阐述如何通过镜像拉取、

384

游戏开发:逻辑与渲染分离技术解析

游戏开发中逻辑与渲染分离的架构设计 本文系统阐述了游戏开发中逻辑与渲染分离的设计原则与实践方法。文章首先定义了核心概念:逻辑层负责游戏规则与状态,渲染层处理视觉表现,二者通过数据模型层进行通信。通过历史回顾,揭示了从早期混合架构到现代分层设计的演变过程。 在技术实现层面,文章详细分析了分离架构的优势:性能优化、流畅度控制、测试便利性、扩展灵活性以及团队协作效率。提供了Unity和Unreal两大引擎的具体实现方案,包括代码示例、分层模型和最佳实践。特别探讨了网络同步、客户端预测、插值处理等关键技术难点。 文

本博客聚焦游戏开发技巧、创业实战心得及大学热门课程干货。这里既有技术深度,也有思维广度,无论你是开发者、创业者还是高校学子,都能在这里找到适合自己的成长之路! 1063

Statistics and Machine Learning Toolbox

Statistics and Machine Learning Toolbox(带书签),MATLAB统计学习工具

hass-config 完整安装指南:为 Home Assistant 部署一套按钮卡片驱动的自定义 Lovelace 仪表盘

本篇指南以仓库 [INSTALL.md](https://link.gitcode.com/i/b0da36b712daec70de35ae5546e2a1f0) 为骨架,完整讲解如何从零把 hass-config 这套以 button-card、card-mod、layout-card 为核心的自定义 Lovelace 界面装进你自己的 Home Assistant。读完你不仅能按部就班完成部署

gitblog_00792的博客 376

C# WinForm 基于DirectShow.dll编写的程序,可边预览边保存视频,音频和图片

基于微软的DirectShow.dll的二次开发1.自动识别连接的摄像头跟麦克风且可以对其进行选择(默认选中第一个)2.可录制wmv和avi两种格式的视频并且都带有音频,avi可以录制的分辨率和帧数进行选择,wmv默认进行了淸晰度优化如不喜欢可去除代码中有相关提示3.可在预览点开之后进行拍照,也可边录制视频边拍照4.添加了视频和音频的压缩功能(酌情使用)这次二次开发发现C#对DirectShow的相关信息太少,这次做完也不敢藏私共享出来,还望大家指正

radare2 官方 Docker 镜像使用与本地构建指南

radare2 作为一款 UNIX-like 逆向工程框架与命令行工具集,其 `dist/docker` 目录内置了完整的容器化方案:既提供基于 Ubuntu 与 snap 构建的官方稳定版镜像(可直接 `docker run` 使用),也提供基于 Debian、从 git 源码现场编译的 `r2docker` 开发镜像,还额外附带一个用于在 32 位(i386)用户态下运行 `r2r` 回归测试

gitblog_00204的博客 155

LinePhone3.2.7(android端最新版本) sip客户端

SIP客户端For android:LinePhone3.2.7(最新版本),官网地址:http://www.linphone.org/

上一篇: 攻防实战 | 没一句废话的攻防实战
下一篇: 容器(Docker & Singularity)使用入门
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值