关键词:容器取证、Docker 工件地图、overlay2、取证时机、混合证据、抢占式固定、daemon.json
难度:入门
前置知识:Linux 目录结构、磁盘镜像与哈希校验、Docker 基本概念(镜像/容器/卷)、JSON Lines 日志格式
一、概述
服务器取证的对象是一台还在跑的机器,证据与业务共存在同一套系统里。
容器把这件事又推了一层:业务代码、运行时状态、配置与日志被切成若干个独立生命周期——有的随进程消失、有的随容器删除、有的写进宿主磁盘长期存在。
分析者若按 Linux 服务器的老习惯去找 /tmp、auth.log、.bash_history,会漏掉容器里发生的一切。
涉 webshell、数据窃取、挖矿类案件里,容器已是高频起点:微服务架构的业务代码由 CI 打成镜像推入私库,入侵者拿到 Docker 或 K8s API 权限后不必碰宿主机上任何业务目录,直接起一个自己的容器就够了。
痕迹形态随之从"某个文件被改了"变成"某个容器被创建过、某个镜像被拉过、某个卷里多了一个文件"。
容器取证真正的难点不在找不到东西,而在它的证据是混合体:
| 证据类型 |
载体 |
生存期 |
| 容器内进程、内存、打开的文件 |
宿主机内存 |
容器停止即消失 |
| 容器内文件的改动(未挂卷部分) |
overlay2/<id>/diff/(upperdir) |
docker rm 即消失 |
| 容器元数据、启动参数、挂载表 |
containers/<id>/config.v2.json |
容器被删即消失 |
| 容器标准输出 / 错误日志 |
containers/<id>/<name>-json.log |
容器被删即消失 |
| 写入 named volume / bind mount 的数据 |
volumes/<name>/_data、宿主机业务目录 |
长期留存 |
| 镜像及其全部层 |
image/、overlay2/ |
直到 image rm 或 prune |
| 守护进程配置与事件 |
daemon.json、dockerd 日志 |
长期留存(events 缓冲除外) |
结论先行:容器取证的成败往往不在分析方法,而在动手时机。
容器一旦被 stop,进程内存与已建立连接就没了;一旦被 rm,容器内的一切(改过的文件、装过的软件、跑过的挖矿进程)连同可写层目录一起彻底消失,只剩镜像和卷。
在取证流程中的位置:本篇是容器场景的入口与导航,属于采集环节的前置判定层。
它不解析任何结构,只回答三个问题:数据在哪里、有多久的存活期、该先固定什么。
层结构解析见 镜像层与 overlay2 解析,持久数据见 Volume 与绑定挂载取证,编排层见 K8s 取证工件地图。
Linux 通用痕迹与宿主机层面本篇不重复,分别见 Linux 取证工件地图 与 Linux 服务器取证工件地图。
涉及的证据形态(具体到路径,不是"日志文件"这种大类):
| 路径 |
内容 |
判读要点 |
/var/lib/docker/image/<driver>/ |
镜像与层索引 |
imagedb/content 的 JSON 是层链的事实来源 |
/var/lib/docker/overlay2/<id>/ |
各容器的可写层与合并视图 |
diff/ 才是改动落点;merged/ 是运行时视图,不可直接当证据 |
/var/lib/docker/containers/<id>/ |
容器元数据 |
config.v2.json 含启动参数、挂载表、镜像摘要 |
/var/lib/docker/containers/<id>/<name>-json.log |
容器标准输出 / 错误 |
随容器删除而消失,是最易失的一类 |
/var/lib/docker/volumes/<name>/_data/ |
named volume 实际数据 |
持久化边界,攻击者主动选择的落点 |
/var/lib/docker/image/<driver>/repositories.json |
私库地址记录 |
出现陌生 registry 是重要线索 |
/etc/docker/daemon.json |
守护进程配置 |
存储驱动、镜像加速、是否开启远程 API |
/etc/docker/certs.d/ |
私库证书 |
私有仓库的认证痕迹 |
| dockerd 日志 |
事件与 API 调用记录 |
含 events 缓冲,容量有限、会被覆盖 |
能回答什么:这台主机上曾经运行过哪些容器与镜像、容器启动参数与挂载关系、哪些数据被写到了持久位置、守护进程如何配置。
不能回答什么:容器内具体发生过什么(除非容器还在运行或可写层未清理);某次网络连接的归属进程(需内存或流量证据)。
"检材里没有容器"不等于"没有容器活动过"——正确表述是"该容器已不存在于 /var/lib/docker/containers/",见 4.5 容器视图与文件存在性。
内容边界:所有命令面向已获得合法授权的目标主机或已制作的只读镜像,目标是发现与固定证据,不提供任何入侵手段、不涉及逃逸技术的实现。检测到容器逃逸或特权容器痕迹时,写的是"如何识别"与"如何排除误报"(见 容器逃逸与运行时痕迹)。
读者前提:需要 Linux 文件系统与进程模型的基础、Docker 的基本概念(镜像 / 容器 / 卷 / 挂载),以及 取证工件地图 提供的宿主侧全景。
如果主机还是在线的,请先读 2.4 证据保全的时机问题 与 3.1 第 0 步 再动手——这一步顺序错了,后面所有分析都是在分析一个已经变过的现场。
二、核心原理
2.1 容器是"易失证据 + 宿主持久化"的混合体
这是理解容器取证一切取舍的前提。
容器把传统"一个文件系统里混着数据和程序"的结构拆成了两层:
宿主文件系统(持久) 容器视图(易失)
───────────────────── ─────────────────────
image/ 镜像与层 ┌──────────────────┐
overlay2/diff/ 容器改动的实际落点 │ 容器内根文件系统 │ ← 只写 upperdir
containers/ 元数据 + 日志 │ 进程 / 内存 │ ← 停止即失
volumes/ 持久数据 │ 挂载点(指向宿主)│ ← 真正的持久边界
↑ └──────────────────┘
容器删除 = 左侧对应项一起消失
三条必须记住的推论:
- 容器内的改动默认不落盘。 容器删除后,攻击者上传到
/tmp、改过的容器内 /etc/passwd、装的工具全部消失,宿主机上什么痕迹都不留。攻击者恰恰知道这一点,所以恶意行为会主动写到 volume、bind mount 目录,或推到镜像里。
- "没找到证据"经常等于"容器已被删除"。 检材里看不到容器,正确表述是"该容器已不存在于
/var/lib/docker/containers/",而不是"未发现容器内活动"。
- 写 volume 是攻击者主动选择的持久化,因此卷里的东西比容器内的更值得查——这是第 03 篇称 volume 为"金矿"的原因。
2.2 关键路径速查表
数据根默认 /var/lib/docker,由 daemon.json 的 data-root 决定(必须先确认)。
| 路径 |
内容 |
取证价值 |
/var/lib/docker/containers/<id>/config.v2.json |
docker inspect 的完整来源(启动参数、挂载、Env、State) |
★★★★★ |
/var/lib/docker/containers/<id>/<name>-json.log |
容器 stdout/stderr,一行一个 JSON |
★★★★★ |
/var/lib/docker/containers/<id>/hostconfig.json |
宿主侧配置(端口绑定、restart 策略、cgroup) |
★★★★ |
/var/lib/docker/overlay2/<cache-id>/diff/ |
容器内所有改动的实际落地点(upperdir) |
★★★★★ |
/var/lib/docker/overlay2/<cache-id>/link |
lowerdir 层链 |
★★★ |
/var/lib/docker/image/overlay2/imagedb/content/sha256/<h> |
镜像配置 JSON(Env/Labels/Cmd/历史) |
★★★★★ |
/var/lib/docker/volumes/<name>/_data |
named volume 的真实数据 |
★★★★★ |
/etc/docker/daemon.json |
守护进程全局配置(存储驱动、log driver、私库) |
★★★★★ |
/etc/docker/certs.d/<registry>/ca.crt |
仓库 CA,指向哪里说明拉过哪个源 |
★★★★ |
~/.docker/config.json |
登录凭证(auths + base64 用户名口令,可逆) |
★★★★★ |
/var/log/docker.log 或 journalctl -u docker |
守护进程日志:镜像拉取、容器启停、错误 |
★★★★ |
/var/lib/containerd/ |
containerd 数据根(K8s 环境) |
★★★★ |
/var/lib/kubelet/、/var/log/pods/、/etc/kubernetes/ |
K8s 侧关键路径 |
★★★★★ |
⚠️ 三条最容易踩空的路:① daemon.json 改了 data-root,默认路径下什么都没有;② 日志驱动不是 json-file 时(journald/fluentd),containers/<id>/*-json.log 不存在;③ K8s 节点上没有 /var/lib/docker,容器由 containerd 托管,路径是 /var/lib/containerd/。K8s 环境详见第 04 篇。
2.3 docker CLI 是取证用的"活体视图"
目标机在线时,docker CLI 提供的信息比离线读文件方便,且大部分只读。
优先用 CLI 拿快照,再落磁盘深挖。
| 命令 |
取证作用 |
注意 |
docker info |
确认存储驱动、日志驱动、cgroup 版本 |
Storage Driver 决定后面读哪个目录 |
docker ps -a --no-trunc |
全部容器含已退出的;--no-trunc 给完整 ID |
不加则只见 12 位短 ID,与目录名对不上 |
docker inspect <id> |
容器全量配置,本文核心工件 |
存在即说明容器未被 rm |
docker logs --timestamps --tail all <id> |
容器全部输出 |
受 log driver 轮转配置限制 |
docker diff <id> |
容器内相对镜像的所有变更 |
只能对未删除的容器用 |
docker top <id> / docker stats --no-stream <id> |
进程列表、瞬时资源 |
抓挖矿(CPU 常 100%)的好办法 |
docker events --since 24h |
容器启停/镜像拉取事件 |
dockerd 重启即清空,条目数也有限 |
docker image ls --no-trunc / image inspect |
镜像清单与历史 |
离线等价见第 02 篇 |
docker volume ls -f dangling=true / volume inspect |
卷清单与挂载关系 |
ls 不显示未挂载的卷,要加 -f dangling=true |
docker export -o out.tar <id> |
导出容器文件系统 |
大容器耗时长且丢元数据,优先离线 |
docker cp <id>:/path ./out |
从容器内拷文件 |
会产生审计日志;容器已停时仍可用 |
⚠️ 会改变状态的命令,绝不能顺手执行:docker exec(在容器内跑命令,会写 bash 历史、可能触发已植入的 payload)、docker commit(把当前状态固化成新镜像,污染现场)。docker restart/start 同理——它们会重写 State.StartedAt 并丢内存。
2.4 证据保全的时机问题(本文最重要)
时间轴 ──────────────────────────────────────────────────────────────▶
容器运行中 docker stop docker rm
│ │ │
├ 内存(进程/连接/打开文件) │ │
├ upperdir diff/(容器内改动)│ ★ diff/ 仍在磁盘上 ★ │
├ config.v2.json + 日志 │ ★ 仍然保留 ★ │
│ │ │ config + 日志 + diff 都消失
├── 此刻采集:信息最全 ──┘
此时只剩:可写层、元数据、日志、
已写入 volume / bind mount 的数据、镜像
此时只剩:镜像、volume
| 操作后 |
仍在的证据 |
已消失的证据 |
docker stop |
容器可写层 overlay2/<id>/diff/ 完整保留、inspect 的全部 Config/HostConfig/Mounts、完整 stdout/stderr 日志、已写入卷与 bind mount 的数据、镜像层 |
只丢内存态:正在跑的外连、未落盘的临时文件、进程内的解密态;★ 可离线 mount 该层取证 |
docker rm |
卷数据、bind mount 目录、镜像层、dockerd 日志 |
上述全部 + 容器元数据、日志与可写层目录 |
docker volume rm |
bind mount、镜像 |
卷内数据 |
docker image rm / system prune |
bind mount |
镜像及其所有层 |
dockerd 重启 |
磁盘上的一切 |
docker events 缓冲、内存、未落盘数据 |
⚠️ 必须区分 stop 与 rm,这是本章最容易被写错的一处。 容器停止只是进程退出,可写层 diff/ 仍在 overlay2 目录里——文件系统的取证工作并没有因此中止。真正不可逆的是 docker rm:它删除容器元数据、日志文件与可写层目录。运维在"先停服务"的直觉下做 stop 是可以理解的,但紧接着的 docker rm -f 才是把证据彻底抹掉的那一步。报告里遇到"容器已停止"的现场,第一步不是放弃,而是去 overlay2/ 里把可写层拷出来(见第 02 篇)。
三条由此推出的操作纪律:
- 顺序不可颠倒:先内存 → 后容器状态 → 后磁盘。
内存里只有活进程才有的东西(正在跑的外连、解密中的密钥);容器状态里有"谁在何时用什么参数起了什么容器";磁盘里是落地的数据。反过来做,第二步信息会丢。
docker inspect 与 docker logs 必须在 docker rm 之前跑完并落盘。
stop 之后二者仍可用(Config 是静态文件、日志是文件),rm 之后彻底没了。
同时**stop 之后要立刻备份 overlay2/<id>/diff/**——可写层还在,但下一次 docker rm 就会连带删掉。
- 不要为了"干净"而清理。
docker system prune、volume prune、docker rm、builder prune 都是不可逆的证据销毁操作。
发现可疑容器时正确动作是先固定,再由委托方决定。
2.5 存储驱动的版本差异
读 overlay2 目录前必须确认存储驱动,位置与含义随版本变化:
| 存储驱动 |
大致版本 |
目录形态 |
取证注意 |
aufs |
Docker ≤ 17.05(AUFS 已停止维护) |
/var/lib/docker/aufs/ |
mnt 前缀的挂载式结构,非标准 diff/ |
devicemapper |
Docker ≤ 17.05 常见 |
/var/lib/docker/devicemapper/ |
数据在块设备映射出的稀疏文件里,无法直接当目录读 |
overlay2 |
Docker 17.06 之后默认 |
overlay2/<id>/{diff,merged,work,link} |
常规取证首选,详见第 02 篇 |
btrfs / zfs |
特殊存储后端 |
子卷形态 |
需专用工具导出子卷 |
判断方法:docker info 的 Storage Driver 一行;离线看 daemon.json 有无 storage-driver 字段;再退一步看 /var/lib/docker/ 下实际存在哪个目录——目录里有什么就说明当时用的是哪个(迁移期间可能并存)。
三、操作步骤
3.1 第 0 步(在线时):按"存活时间"排序的抢占式固定
这一步的顺序本身就是取证方法。
先抢最易失的。
---- 0.1 时间基准(先记,它几秒后就变)----
date -u '+%Y-%m-%dT%H:%M:%SZ' | tee /evidence/collection-utc.txt
timedatectl 2>/dev/null | tee -a /evidence/collection-utc.txt # 时区,影响时间线对齐
---- 0.2 内存(最易失,优先级最高)----
sudo ./avml /evidence/DigiForensics-mem.raw # 静态可执行,LiME 需编译加载模块
sha256sum /evidence/DigiForensics-mem.raw | tee /evidence/mem.sha256
---- 0.3 宿主机活体状态(挖矿/外连在这里露头)----
ps auxwwf | tee /evidence/ps-live.txt
ss -tulpn | tee /evidence/netstat-live.txt
ss -tapn state established