本文发表已超过一年。较旧的文章可能包含过时内容。请检查页面中的信息自发布以来是否已失效。

容器取证分析

在我之前的文章 Kubernetes 中的取证容器检查点 中,我介绍了 Kubernetes 中的检查点功能,以及如何进行设置和使用。该功能的名称是取证容器检查点,但我没有详细介绍如何对 Kubernetes 创建的检查点进行实际分析。在本文中,我将详细介绍如何分析检查点。

检查点仍是 Kubernetes 中的一个 Alpha 功能,本文旨在提供一个预览,展示该功能未来可能如何工作。

准备工作

关于如何配置 Kubernetes 和底层 CRI 实现以启用检查点支持的详细信息,请参阅我的文章 Kubernetes 中的取证容器检查点

作为一个例子,我准备了一个容器镜像 (quay.io/adrianreber/counter:blog),我将在本文中对其进行检查点并进行分析。这个容器允许我在容器中创建文件,并将信息存储在内存中,这些信息我稍后想在检查点中找到。

为了运行该容器,我需要一个 Pod,对于这个例子,我使用以下 Pod 清单

apiVersion: v1
kind: Pod
metadata:
  name: counters
spec:
  containers:
  - name: counter
    image: quay.io/adrianreber/counter:blog

这将导致一个名为 counter 的容器在一个名为 counters 的 Pod 中运行。

容器运行后,我将对该容器执行以下操作

$ kubectl get pod counters --template '{{.status.podIP}}'
10.88.0.25
$ curl 10.88.0.25:8088/create?test-file
$ curl 10.88.0.25:8088/secret?RANDOM_1432_KEY
$ curl 10.88.0.25:8088

第一次访问在容器中创建了一个名为 test-file 的文件,内容为 test-file;第二次访问将我的秘密信息 (RANDOM_1432_KEY) 存储到容器内存的某个地方。最后一次访问只是在内部日志文件中添加了额外一行。

在分析检查点之前的最后一步是告诉 Kubernetes 创建检查点。正如之前文章中描述的,这需要访问 *kubelet* 仅限 checkpoint 的 API 端点。

对于 namespace 为 *default* 中 Pod 名为 *counters* 下的容器名 *counter*,*kubelet* API 端点可通过以下地址访问:

# run this on the node where that Pod is executing
curl -X POST "https://localhost:10250/checkpoint/default/counters/counter"

为了完整性,需要以下 curl 命令行选项,以便让 curl 接受 *kubelet* 的自签名证书并授权使用 *kubelet* checkpoint API

--insecure --cert /var/run/kubernetes/client-admin.crt --key /var/run/kubernetes/client-admin.key

检查点创建完成后,检查点应位于 /var/lib/kubelet/checkpoints/checkpoint-<pod-name>_<namespace-name>-<container-name>-<timestamp>.tar

在本文接下来的步骤中,我在分析检查点归档文件时将使用名称 checkpoint.tar

使用 checkpointctl 分析检查点归档文件

为了获取关于创建了检查点的容器的一些初步信息,我使用工具 checkpointctl,如下所示:

$ checkpointctl show checkpoint.tar --print-stats
+-----------+----------------------------------+--------------+---------+---------------------+--------+------------+------------+-------------------+
| CONTAINER |              IMAGE               |      ID      | RUNTIME |       CREATED       | ENGINE |     IP     | CHKPT SIZE | ROOT FS DIFF SIZE |
+-----------+----------------------------------+--------------+---------+---------------------+--------+------------+------------+-------------------+
| counter   | quay.io/adrianreber/counter:blog | 059a219a22e5 | runc    | 2023-03-02T06:06:49 | CRI-O  | 10.88.0.23 | 8.6 MiB    | 3.0 KiB           |
+-----------+----------------------------------+--------------+---------+---------------------+--------+------------+------------+-------------------+
CRIU dump statistics
+---------------+-------------+--------------+---------------+---------------+---------------+
| FREEZING TIME | FROZEN TIME | MEMDUMP TIME | MEMWRITE TIME | PAGES SCANNED | PAGES WRITTEN |
+---------------+-------------+--------------+---------------+---------------+---------------+
| 100809 us     | 119627 us   | 11602 us     | 7379 us       |          7800 |          2198 |
+---------------+-------------+--------------+---------------+---------------+---------------+

这已经给了我关于该检查点归档文件中检查点的一些信息。我可以看到容器的名称,关于容器运行时和容器引擎的信息。它还列出了检查点的大小 (CHKPT SIZE)。这主要是检查点中包含的内存页大小,但也有关于容器中所有更改文件的总大小的信息 (ROOT FS DIFF SIZE)。

额外的参数 --print-stats 解码检查点归档文件中的信息,并在第二个表格 (CRIU dump statistics) 中显示它们。这些信息是在创建检查点时收集的,并概述了 CRIU 需要多少时间来创建容器中进程的检查点,以及在检查点创建期间分析和写入了多少内存页。

更深入的挖掘

借助 checkpointctl,我能够获取关于检查点归档文件的一些高级信息。为了进一步分析检查点归档文件,我必须解压它。检查点归档文件是一个 tar 归档文件,可以使用 tar xf checkpoint.tar 命令解压。

解压检查点归档文件将得到以下文件和目录

  • bind.mounts - 这个文件包含关于 bind mount 的信息,在恢复时需要用于将所有外部文件和目录挂载到正确位置
  • checkpoint/ - 这个目录包含由 CRIU 创建的实际检查点
  • config.dumpspec.dump - 这些文件包含关于容器的元数据,在恢复时需要
  • dump.log - 这个文件包含在创建检查点时由 CRIU 创建的调试输出
  • stats-dump - 这个文件包含 checkpointctl 用于显示 dump 统计数据 (--print-stats) 的数据
  • rootfs-diff.tar - 这个文件包含容器文件系统上所有更改的文件

文件系统变更 - rootfs-diff.tar

进一步分析容器检查点的第一步是查看容器中已更改的文件。这可以通过查看文件 rootfs-diff.tar 来完成

$ tar xvf rootfs-diff.tar
home/counter/logfile
home/counter/test-file

现在可以研究容器中更改的文件了

$ cat home/counter/logfile
10.88.0.1 - - [02/Mar/2023 06:07:29] "GET /create?test-file HTTP/1.1" 200 -
10.88.0.1 - - [02/Mar/2023 06:07:40] "GET /secret?RANDOM_1432_KEY HTTP/1.1" 200 -
10.88.0.1 - - [02/Mar/2023 06:07:43] "GET / HTTP/1.1" 200 -
$ cat home/counter/test-file
test-file 

与此容器所基于的容器镜像 (quay.io/adrianreber/counter:blog) 相比,我可以看到文件 logfile 包含对容器提供的服务的所有访问信息,并且文件 test-file 正如预期被创建。

借助 rootfs-diff.tar,可以检查与容器基础镜像相比所有已创建或已更改的文件。

分析检查点进程 - checkpoint/

目录 checkpoint/ 包含 CRIU 在创建容器中进程检查点时创建的数据。目录 checkpoint/ 中的内容由不同的 镜像文件 组成,可以借助作为 CRIU 一部分分发的工具 CRIT 进行分析。

首先让我们概述一下容器内部的进程

$ crit show checkpoint/pstree.img | jq .entries[].pid
1
7
8

这个输出意味着我在容器的 PID 命名空间中有三个进程,它们的 PID 分别是:1, 7, 8

这只是从容器 PID 命名空间内部的视图。在恢复期间,这些 PID 将被精确地重新创建。从容器 PID 命名空间的外部看,恢复后 PID 会改变。

下一步是获取关于这三个进程的一些额外信息

$ crit show checkpoint/core-1.img | jq .entries[0].tc.comm
"bash"
$ crit show checkpoint/core-7.img | jq .entries[0].tc.comm
"counter.py"
$ crit show checkpoint/core-8.img | jq .entries[0].tc.comm
"tee"

这意味着我容器中的三个进程是 bash, counter.py (一个 Python 解释器) 和 tee。关于这些进程的父子关系的详细信息,在 checkpoint/pstree.img 中有更多数据可供分析。

让我们将目前收集的信息与仍在运行的容器进行比较

$ crictl inspect --output go-template --template "{{(index .info.pid)}}" 059a219a22e56
722520
$ ps auxf | grep -A 2 722520
fedora    722520  \_ bash -c /home/counter/counter.py 2>&1 | tee /home/counter/logfile
fedora    722541      \_ /usr/bin/python3 /home/counter/counter.py
fedora    722542      \_ /usr/bin/coreutils --coreutils-prog-shebang=tee /usr/bin/tee /home/counter/logfile
$ cat /proc/722520/comm
bash
$ cat /proc/722541/comm
counter.py
$ cat /proc/722542/comm
tee

在这个输出中,我首先检索容器中第一个进程的 PID,然后我在容器运行的系统上查找该 PID 和其子进程。我看到三个进程,第一个是 "bash",它在容器 PID 命名空间内的 PID 是 1。然后我查看 /proc/<PID>/comm,可以找到与检查点镜像中完全相同的值。

重要的是要记住,检查点将包含从容器 PID 命名空间内部的视图,因为这些信息对于恢复进程很重要。

crit 能告诉我们关于容器的最后一个例子是关于 UTS 命名空间的信息

$ crit show checkpoint/utsns-12.img
{
    "magic": "UTSNS",
    "entries": [
        {
            "nodename": "counters",
            "domainname": "(none)"
        }
    ]
}

这告诉我 UTS 命名空间内部的主机名是 counters

对于 CRIU 在创建检查点期间收集的每个资源,checkpoint/ 目录包含相应的镜像文件,可以借助 crit 进行分析。

查看内存页

除了可以借助 CRIT 解码的 CRIU 信息外,还有文件包含 CRIU 写入磁盘的原始内存页

$ ls  checkpoint/pages-*
checkpoint/pages-1.img  checkpoint/pages-2.img  checkpoint/pages-3.img

当我最初使用容器时,我在内存的某个地方存储了一个随机密钥 (RANDOM_1432_KEY)。让我们看看是否能找到它

$ grep -ao RANDOM_1432_KEY checkpoint/pages-*
checkpoint/pages-2.img:RANDOM_1432_KEY

果然,我的数据就在那里。这样我就可以轻松查看容器中所有进程的内存页内容,但同样重要的是要记住,任何能够访问检查点归档文件的人都可以访问存储在容器进程内存中的所有信息。

使用 gdb 进行进一步分析

查看检查点镜像的另一种可能性是使用 gdb。CRIU 仓库包含脚本 coredump,可以将检查点转换为 coredump 文件

$ /home/criu/coredump/coredump-python3
$ ls -al core*
core.1  core.7  core.8

运行 coredump-python3 脚本将把检查点镜像转换为容器中每个进程的一个 coredump 文件。使用 gdb 我也可以查看进程的详细信息

$ echo info registers | gdb --core checkpoint/core.1 -q

[New LWP 1]

Core was generated by `bash -c /home/counter/counter.py 2>&1 | tee /home/counter/logfile'.

#0  0x00007fefba110198 in ?? ()
(gdb)
rax            0x3d                61
rbx            0x8                 8
rcx            0x7fefba11019a      140667595587994
rdx            0x0                 0
rsi            0x7fffed9c1110      140737179816208
rdi            0xffffffff          4294967295
rbp            0x1                 0x1
rsp            0x7fffed9c10e8      0x7fffed9c10e8
r8             0x1                 1
r9             0x0                 0
r10            0x0                 0
r11            0x246               582
r12            0x0                 0
r13            0x7fffed9c1170      140737179816304
r14            0x0                 0
r15            0x0                 0
rip            0x7fefba110198      0x7fefba110198
eflags         0x246               [ PF ZF IF ]
cs             0x33                51
ss             0x2b                43
ds             0x0                 0
es             0x0                 0
fs             0x0                 0
gs             0x0                 0

在这个例子中,我可以看到在创建检查点时所有寄存器的值,我也可以看到容器 PID 1 进程的完整命令行:bash -c /home/counter/counter.py 2>&1 | tee /home/counter/logfile

总结

借助容器检查点功能,可以在不停止容器且容器不知情的情况下创建运行中容器的检查点。在 Kubernetes 中创建容器检查点的结果是一个检查点归档文件;使用不同的工具如 checkpointctl, tar, critgdb,可以分析检查点。即使使用像 grep 这样简单的工具,也可以在检查点归档文件中找到信息。

我在本文中展示的不同示例,说明如何分析检查点,只是一个起点。根据您的需求,可以更详细地查看某些内容,但本文应为您提供如何开始分析检查点的入门指导。

如何参与?

您可以通过多种方式联系 SIG Node