一次 Docker 镜像的逆向工程
创始人
2024-03-02 07:06:30
0

这要从一次咨询的失误说起:政府组织 A 让政府组织 B 开发一个 Web 应用程序。政府机构 B 把部分工作外包给某个人。后来,项目的托管和维护被外包给一家私人公司 C。C 公司发现,之前外包的人(已经离开很久了)构建了一个自定义的 Docker 镜像,并将其成为系统构建的依赖项,但这个人没有提交原始的 Dockerfile。C 公司有合同义务管理这个 Docker 镜像,可是他们他们没有源代码。C 公司偶尔叫我进去做各种工作,所以处理一些关于这个神秘 Docker 镜像的事情就成了我的工作。

幸运的是,Docker 镜像的格式比想象的透明多了。虽然还需要做一些侦查工作,但只要解剖一个镜像文件,就能发现很多东西。例如,这里有一个 Prettier 代码格式化 的镜像可供快速浏览。

首先,让 Docker 守护进程 daemon 拉取镜像,然后将镜像提取到文件中:

docker pull tmknom/prettier:2.0.5
docker save tmknom/prettier:2.0.5 > prettier.tar

是的,该文件只是一个典型 tarball 格式的归档文件:

$ tar xvf prettier.tar
6c37da2ee7de579a0bf5495df32ba3e7807b0a42e2a02779206d165f55f1ba70/
6c37da2ee7de579a0bf5495df32ba3e7807b0a42e2a02779206d165f55f1ba70/VERSION
6c37da2ee7de579a0bf5495df32ba3e7807b0a42e2a02779206d165f55f1ba70/json
6c37da2ee7de579a0bf5495df32ba3e7807b0a42e2a02779206d165f55f1ba70/layer.tar
88f38be28f05f38dba94ce0c1328ebe2b963b65848ab96594f8172a9c3b0f25b.json
a9cc4ace48cd792ef888ade20810f82f6c24aaf2436f30337a2a712cd054dc97/
a9cc4ace48cd792ef888ade20810f82f6c24aaf2436f30337a2a712cd054dc97/VERSION
a9cc4ace48cd792ef888ade20810f82f6c24aaf2436f30337a2a712cd054dc97/json
a9cc4ace48cd792ef888ade20810f82f6c24aaf2436f30337a2a712cd054dc97/layer.tar
d4f612de5397f1fc91272cfbad245b89eac8fa4ad9f0fc10a40ffbb54a356cb4/
d4f612de5397f1fc91272cfbad245b89eac8fa4ad9f0fc10a40ffbb54a356cb4/VERSION
d4f612de5397f1fc91272cfbad245b89eac8fa4ad9f0fc10a40ffbb54a356cb4/json
d4f612de5397f1fc91272cfbad245b89eac8fa4ad9f0fc10a40ffbb54a356cb4/layer.tar
manifest.json
repositories

如你所见,Docker 在命名时经常使用 哈希 hash 。我们看看 manifest.json。它是以难以阅读的压缩 JSON 写的,不过 JSON 瑞士军刀 jq 可以很好地打印 JSON:

$ jq . manifest.json
[
  {
    "Config": "88f38be28f05f38dba94ce0c1328ebe2b963b65848ab96594f8172a9c3b0f25b.json",
    "RepoTags": [
      "tmknom/prettier:2.0.5"
    ],
    "Layers": [
      "a9cc4ace48cd792ef888ade20810f82f6c24aaf2436f30337a2a712cd054dc97/layer.tar",
      "d4f612de5397f1fc91272cfbad245b89eac8fa4ad9f0fc10a40ffbb54a356cb4/layer.tar",
      "6c37da2ee7de579a0bf5495df32ba3e7807b0a42e2a02779206d165f55f1ba70/layer.tar"
    ]
  }
]

请注意,这三个 层 Layer 对应三个以哈希命名的目录。我们以后再看。现在,让我们看看 Config 键指向的 JSON 文件。它有点长,所以我只在这里转储第一部分:

$ jq . 88f38be28f05f38dba94ce0c1328ebe2b963b65848ab96594f8172a9c3b0f25b.json | head -n 20
{
  "architecture": "amd64",
  "config": {
    "Hostname": "",
    "Domainname": "",
    "User": "",
    "AttachStdin": false,
    "AttachStdout": false,
    "AttachStderr": false,
    "Tty": false,
    "OpenStdin": false,
    "StdinOnce": false,
    "Env": [
      "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
    ],
    "Cmd": [
      "--help"
    ],
    "ArgsEscaped": true,
    "Image": "sha256:93e72874b338c1e0734025e1d8ebe259d4f16265dc2840f88c4c754e1c01ba0a",

最重要的是 history 列表,它列出了镜像中的每一层。Docker 镜像由这些层堆叠而成。Dockerfile 中几乎每条命令都会变成一个层,描述该命令对镜像所做的更改。如果你执行 RUN script.sh 命令创建了 really_big_file,然后用 RUN rm really_big_file 命令删除文件,Docker 镜像实际生成两层:一个包含 really_big_file,一个包含 .wh.really_big_file 记录来删除它。整个镜像文件大小不变。这就是为什么你会经常看到像 RUN script.sh && rm really_big_file 这样的 Dockerfile 命令链接在一起——它保障所有更改都合并到一层中。

以下是该 Docker 镜像中记录的所有层。注意,大多数层不改变文件系统镜像,并且 empty_layer 标记为 true。以下只有三个层是非空的,与我们之前描述的相符。

$ jq .history 88f38be28f05f38dba94ce0c1328ebe2b963b65848ab96594f8172a9c3b0f25b.json
[
  {
    "created": "2020-04-24T01:05:03.608058404Z",
    "created_by": "/bin/sh -c #(nop) ADD file:b91adb67b670d3a6ff9463e48b7def903ed516be66fc4282d22c53e41512be49 in / "
  },
  {
    "created": "2020-04-24T01:05:03.92860976Z",
    "created_by": "/bin/sh -c #(nop)  CMD [\"/bin/sh\"]",
    "empty_layer": true
  },
  {
    "created": "2020-04-29T06:34:06.617130538Z",
    "created_by": "/bin/sh -c #(nop)  ARG BUILD_DATE",
    "empty_layer": true
  },
  {
    "created": "2020-04-29T06:34:07.020521808Z",
    "created_by": "/bin/sh -c #(nop)  ARG VCS_REF",
    "empty_layer": true
  },
  {
    "created": "2020-04-29T06:34:07.36915054Z",
    "created_by": "/bin/sh -c #(nop)  ARG VERSION",
    "empty_layer": true
  },
  {
    "created": "2020-04-29T06:34:07.708820086Z",
    "created_by": "/bin/sh -c #(nop)  ARG REPO_NAME",
    "empty_layer": true
  },
  {
    "created": "2020-04-29T06:34:08.06429638Z",
    "created_by": "/bin/sh -c #(nop)  LABEL org.label-schema.vendor=tmknom org.label-schema.name=tmknom/prettier org.label-schema.description=Prettier is an opinionated code formatter. org.label-schema.build-date=2020-04-29T06:34:01Z org
.label-schema.version=2.0.5 org.label-schema.vcs-ref=35d2587 org.label-schema.vcs-url=https://github.com/tmknom/prettier org.label-schema.usage=https://github.com/tmknom/prettier/blob/master/README.md#usage org.label-schema.docker.cmd=do
cker run --rm -v $PWD:/work tmknom/prettier --parser=markdown --write '**/*.md' org.label-schema.schema-version=1.0",
    "empty_layer": true
  },
  {
    "created": "2020-04-29T06:34:08.511269907Z",
    "created_by": "/bin/sh -c #(nop)  ARG NODEJS_VERSION=12.15.0-r1",
    "empty_layer": true
  },
  {
    "created": "2020-04-29T06:34:08.775876657Z",
    "created_by": "/bin/sh -c #(nop)  ARG PRETTIER_VERSION",
    "empty_layer": true
  },
  {
    "created": "2020-04-29T06:34:26.399622951Z",
    "created_by": "|6 BUILD_DATE=2020-04-29T06:34:01Z NODEJS_VERSION=12.15.0-r1 PRETTIER_VERSION=2.0.5 REPO_NAME=tmknom/prettier VCS_REF=35d2587 VERSION=2.0.5 /bin/sh -c set -x &&     apk add --no-cache nodejs=${NODEJS_VERSION} nodejs-np
m=${NODEJS_VERSION} &&     npm install -g prettier@${PRETTIER_VERSION} &&     npm cache clean --force &&     apk del nodejs-npm"
  },
  {
    "created": "2020-04-29T06:34:26.764034848Z",
    "created_by": "/bin/sh -c #(nop) WORKDIR /work"
  },
  {
    "created": "2020-04-29T06:34:27.092671047Z",
    "created_by": "/bin/sh -c #(nop)  ENTRYPOINT [\"/usr/bin/prettier\"]",
    "empty_layer": true
  },
  {
    "created": "2020-04-29T06:34:27.406606712Z",
    "created_by": "/bin/sh -c #(nop)  CMD [\"--help\"]",
    "empty_layer": true
  }
]

太棒了!所有的命令都在 created_by 字段中,我们几乎可以用这些命令重建 Dockerfile。但不是完全可以。最上面的 ADD 命令实际上没有给我们需要添加的文件。COPY 命令也没有全部信息。我们还失去了 FROM 语句,因为它们扩展成了从基础 Docker 镜像继承的所有层。

我们可以通过查看 时间戳 timestamp ,按 Dockerfile 对层进行分组。大多数层的时间戳相差不到一分钟,代表每一层构建所需的时间。但是前两层是 2020-04-24,其余的是 2020-04-29。这是因为前两层来自一个基础 Docker 镜像。理想情况下,我们可以找出一个 FROM 命令来获得这个镜像,这样我们就有了一个可维护的 Dockerfile。

manifest.json 展示第一个非空层是 a9cc4ace48cd792ef888ade20810f82f6c24aaf2436f30337a2a712cd054dc97/layer.tar。让我们看看它:

$ cd a9cc4ace48cd792ef888ade20810f82f6c24aaf2436f30337a2a712cd054dc97/
$ tar tf layer.tf | head
bin/
bin/arch
bin/ash
bin/base64
bin/bbconfig
bin/busybox
bin/cat
bin/chgrp
bin/chmod
bin/chown

看起来它可能是一个 操作系统 operating system 基础镜像,这也是你期望从典型 Dockerfile 中看到的。Tarball 中有 488 个条目,如果你浏览一下,就会发现一些有趣的条目:

...
dev/
etc/
etc/alpine-release
etc/apk/
etc/apk/arch
etc/apk/keys/
etc/apk/keys/alpine-devel@lists.alpinelinux.org-4a6a0840.rsa.pub
etc/apk/keys/alpine-devel@lists.alpinelinux.org-5243ef4b.rsa.pub
etc/apk/keys/alpine-devel@lists.alpinelinux.org-5261cecb.rsa.pub
etc/apk/protected_paths.d/
etc/apk/repositories
etc/apk/world
etc/conf.d/
...

果不其然,这是一个 Alpine 镜像,如果你注意到其他层使用 apk 命令安装软件包,你可能已经猜到了。让我们解压 tarball 看看:

$ mkdir files
$ cd files
$ tar xf ../layer.tar
$ ls
bin  dev  etc  home  lib  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var
$ cat etc/alpine-release
3.11.6

如果你拉取、解压 alpine:3.11.6,你会发现里面有一个非空层,layer.tar 与 Prettier 镜像基础层中的 layer.tar 是一样的。

出于兴趣,另外两个非空层是什么?第二层是包含 Prettier 安装包的主层。它有 528 个条目,包含 Prettier、一堆依赖项和证书更新:

...
usr/lib/libuv.so.1
usr/lib/libuv.so.1.0.0
usr/lib/node_modules/
usr/lib/node_modules/prettier/
usr/lib/node_modules/prettier/LICENSE
usr/lib/node_modules/prettier/README.md
usr/lib/node_modules/prettier/bin-prettier.js
usr/lib/node_modules/prettier/doc.js
usr/lib/node_modules/prettier/index.js
usr/lib/node_modules/prettier/package.json
usr/lib/node_modules/prettier/parser-angular.js
usr/lib/node_modules/prettier/parser-babel.js
usr/lib/node_modules/prettier/parser-flow.js
usr/lib/node_modules/prettier/parser-glimmer.js
usr/lib/node_modules/prettier/parser-graphql.js
usr/lib/node_modules/prettier/parser-html.js
usr/lib/node_modules/prettier/parser-markdown.js
usr/lib/node_modules/prettier/parser-postcss.js
usr/lib/node_modules/prettier/parser-typescript.js
usr/lib/node_modules/prettier/parser-yaml.js
usr/lib/node_modules/prettier/standalone.js
usr/lib/node_modules/prettier/third-party.js
usr/local/
usr/local/share/
usr/local/share/ca-certificates/
usr/sbin/
usr/sbin/update-ca-certificates
usr/share/
usr/share/ca-certificates/
usr/share/ca-certificates/mozilla/
usr/share/ca-certificates/mozilla/ACCVRAIZ1.crt
usr/share/ca-certificates/mozilla/AC_RAIZ_FNMT-RCM.crt
usr/share/ca-certificates/mozilla/Actalis_Authentication_Root_CA.crt
...

第三层由 WORKDIR /work 命令创建,它只包含一个条目:

$ tar tf 6c37da2ee7de579a0bf5495df32ba3e7807b0a42e2a02779206d165f55f1ba70/layer.tar
work/

原始 Dockerfile 在 Prettier 的 git 仓库中


via: https://theartofmachinery.com/2021/03/18/reverse_engineering_a_docker_image.html

作者:Simon Arneaud 选题:lujun9972 译者:DCOLIVERSUN 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

相关内容

Buildroot vs ...
在嵌入式与边缘计算开发中,操作系统的选择直接决定了产品的开发效率、...
2026-07-20 15:54:01
外媒初探Win11 WSL...
IT之家 7 月 2 日消息,科技媒体 Windows Lates...
2026-07-17 23:36:40
阿里云开源项目 Drago...
AIPress.com.cn报道 1月22日消息,由阿里云与蚂蚁集...
2026-01-22 19:17:48
云轴科技申请用于AI应用的...
国家知识产权局信息显示,上海云轴科技股份有限公司申请一项名为“一种...
2026-01-03 19:16:53
树莓派发布基于Debian...
2025-10-02 18:20:12 作者:狼叫兽 10月2...
2025-10-04 13:15:45
微软官宣!将开源Windo...
快科技8月5日消息,微软近日宣布,计划开源Windows 11的用...
2025-08-05 18:42:51

热门资讯

“Linux 中国” 开源社区... 这是一个艰难的决定 —— 这不是调侃,而是当你真正做出改变了你十几年生活轨迹的决定时,你就会明白这个...
硬核观察 #1263 德国铁路... #1 德国铁路公司招聘 Windows for Workgroups 3.11 管理员该职位要求了解...
人工智能教程(七):Sciki... 在本系列的 上一篇文章 中,我们用 TensorFlow 构建了第一个神经网络,然后还通过 Kera...
关闭 WSL 中正在运行的 L... 以下是使用 WSL 关闭在 Windows 内运行的 Linux 系统的不同方法。你使用 WSL 在...
MX Linux 23.2 “... 最新的 MX Linux 23.2 小版本更新带来了基本的改进和新增功能。MX Linux 推出了 ...
硬核观察 #1262 网络上最... #1 网络上最古老的软件存档库之一即将关闭美国新墨西哥州立大学(NMSU)最近宣布,即将于 2024...
如何在 RHEL 上通过绑定配... 在 Linux 中,你可以根据自己的需要创建高级网络设置,如 网络绑定 Network Bondin...
Linux Matrix 消息... Matrix 用户可以升级 Fractal 6 了!Fractal 是目前 最好的 Matrix 客...
如何通过 VLC 使用字幕 使用 VLC 媒体播放器播放和管理字幕的新手指南。我是一个超级动漫迷,如果你想观看最新的剧集,那么你...
硬核观察 #1261 X 计划... #1 X 计划在奥斯汀建立内容审核总部据报道,该部门将招聘 100 名全职员工,将主要关注 CSE ...