抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

0x01 前言

为了更深入和清晰的理解 Docker K8S 等组件漏洞的原理,通过我会选择 Debug 它们的源码进行断点调试追踪代码层面的调用逻辑。

刚开始学Go的时候一直很不理解Go的项目结构,跑或者去调试开源Go项目有隔阂感,低版本Docker因没有Go Module,采用较为原始的Go Vendor依赖管理方式,最近才开始学Go的我来说也是陌生,在加上Docker 20.10[1]项目重构之后分为多个组件(不直接用docker-ce源码是因为用新的可以一套代码新旧代码一起调),搭建起来也麻烦少许,Docker之间各个组件之间的调用方式又有点不同,导致搭建起来比较麻烦。后面学K8S这些发现调试环境搭建思路一样,一通百通,于是就总结一下。

本文环境

  • Goland IDE
  • Ubuntu 18.04

相信大多数人和我一样主力机用Win或者Mac,但Docker底层依赖Linux命名空间,所以在runC层面的漏洞需要Linux环境,这里通过本机Goland远程连至Ubuntu进行。直接在Ubuntu装Goland也可以。不是Ubuntu也不要紧,只是下文中安装的依赖包名可能有点不同。

0x02 Docker架构组成

调试之前,需要了解下 Docker 的组成,即组件的作用,这样有助于复现时理解漏洞所处在Docker组件

image-20220707232706988

Docker 架构可分为 4 个仓库

Docker 于 2013年开源一种可移植、灵活且易于部署的容器化项目 libcontainer,并于 2015年把代码捐赠给OCI(Open Container Initiative)以助力容器生态标准化。Contianerd 最初也是 Docker 的核心库,于 2017 年捐赠给 CNCF(Cloud Native Computing Foundation)。[2]

他们最初都为 Docker 的一部分,为更好构建云原生生态最终演变为几个不同的组件。

0x03 Goland Docker调试环境搭建

因为 docker engine 需要 root 权限,请以 root 用户身份执行以下命令

Golang 安装

许多漏洞环境需要的 go 版本较高,源安装较低,先手动安装 go

1
wget https://go.dev/dl/go1.17.9.linux-amd64.tar.gz

解压

1
tar xvf go1.17.9.linux-amd64.tar.gz -C /usr/local/

让 go 可执行文件能在 $PATH 索引

1
ln -s /usr/local/go/bin/go /usr/bin/go

命令行运行 go 能正常回显即可

Goland Docker 源码下载

如0x02节介绍,要完整调试 Docker 源码,我们需要把四个仓库都拷贝下来,又因Docker低版本使用Go Vendor进行依赖管理,因此需要创建Go 项目源码结构

1
2
3
4
5
6
7
8
mkdir -p /opt/docker-moby/src/github.com/docker
mkdir -p /opt/docker-cli/src/github.com/docker
mkdir -p /opt/docker-containerd/src/github.com/containerd
mkdir -p /opt/docker-runc/src/github.com/opencontainers
git clone https://github.com/moby/moby.git /opt/docker-moby/src/github.com/docker/docker
git clone https://github.com/docker/cli.git /opt/docker-cli/src/github.com/docker/cli
git clone https://github.com/containerd/containerd.git /opt/docker-containerd/src/github.com/containerd/containerd
git clone https://github.com/opencontainers/runc.git /opt/docker-runc/src/github.com/opencontainers/runc

完成后目录结构如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
root@cloud_shoot /opt# tree -L 4 docker-*
docker-cli
└── src
└── github.com
└── docker
└── cli
docker-containerd
└── src
└── github.com
└── containerd
└── containerd
docker-moby
└── src
└── github.com
└── docker
└── docker
docker-runc
└── src
└── github.com
└── opencontainers
└── runc

这里使用 Goland 进行远程开发,通过 ssh 连接,配置好后,(如果本机环境为Linux,则直接)打开两个IDE依次打开的项目文件夹为

  • /opt/docker-moby/
  • /opt/docker-cli/
  • /opt/docker-containerd/
  • /opt/docker-runc/

非远程调试可跳过下面这步

本地Goland Go SDK 设置

不管是不是远程,都需要设置本地Go SDK,否则运行的时候会挂在编译前的本地检查阶段

image-20220812094507912

一般本地安装好 Go 会自动识别,没有的话 Add SDK 指定本地安装目录或者 Download.. 直接下载即可,版本和与一开始的Golang相同,尽量不要比一开始安装的Golang版本低即可

远程调试环境搭建(本地可跳过此步骤)

顺便介绍下远程调试配置方法,新建一个空项目

image-20220709125756637

配置远程SFTP访问

image-20220709130305721

image-20220709130320442

image-20220709130325363

设置SSH连接信息

image-20220709130333922

设置远程项目部署目录,最后点击OK

image-20220709130349846

image-20220709130535854

最后从远程下载源码即可,新建项目时还有个 go.mod 是无用的,删除即可

项目设置

此部配置目的是为了让本地的Goland正确识别代码依赖

这里注意因为默认 Goland 开启 Go Module,要取消

image-20220709133623468

并添加项目 Project GOPATH

image-20220709133634556

Go Build设置 (本地可跳过此步骤)

接下来配置远程Go环境,新建一个 Go Build 项目,注意 Run on 处选择SSH

image-20220709131410962

因刚刚我们配置了SFTP,直接在 Existing 中选择即可

image-20220709131502912

点击 Next,这一步报错无所谓,因为我远程用的是 fish shell,用 bash 的同学应该问题不大

image-20220709131542038

继续点Next即可, 确保下面红线框住的3处配置正确即可

image-20220709133146982

点击 Finish 即可

调试 Go 包选择

配置Go 包为:github.com/docker/cli/cmd/docker,因为cli的入口为这个包,后面moby和containerd根据要调试的包选择即可。注意勾选下面的 Build on remote target

image-20220709133410841

PS: 这里 Run kind 一定要为 Package,因为Go包构建的基础单位就是包,而不是文件,这里如果 Build 文件也可以运行,因为当前入口 cmd 下,只有一个包,在 Build moby 时,如果 Build file 会报错(问了下ssst0n3师父)

最终点击运行即可,观察运行的路径为远程路径

image-20220709133759989

点击运行右边的Debug按钮也可以正常运行,moby 和 containerd 服务的debug环境重复此步骤即可,不过有点注意事项

  1. 要先启动 containerd 在启动 dockerd 服务,因为从架构图可知,dockerd 服务依赖于 containerd 服务,否则启动异常
  2. 启动 containerd 服务前,要保证本机装有 containerd-shim 和 runc,也是重复上面的步骤,go build 一下即可,从上面运行 cli 可知,运行默认指定了 -o 选项 /opt/docker-cli/executables-4kyIqDG6jx/___10go_build_github_com_docker_cli_cmd_docker_linux ,把cntainerd-shim和runc运行后的路径文件复制一份至 /usr/bin 之类的目录即可,containerd服务启动前默认从环境变量的可执行文件目录搜索,当然也可以通过指定参数指定路径
  3. 见【一些其他问题】章节中的第3点

containerd-shim 和 runc debug 配置

至此,表面上远程Debug就这么轻松的完成了,凡事就怕有但是…

细心的同学看了0x02的架构图会发现,Containerd调Containerd-shim 是通过命令行调用的,Dockerd和Containerd好说,是HTTP和gRPC服务,直接Debug启动等待请求即可触发Debug,但命令行调用,不能事前Debug启动要咋整?一开始我用了最原始的print大法

后面看了下 ssst0n3 调 kubelet的文章[3] 和搜狗团队对runc源码分析文章[4],发现 Containerd-shim 和 runC 两个 cli 可以通过安装和启动 dlv ,通过 Goland 连接进行远程debug。原理是先写一个我们自定义的脚本替换 /usr/bin/containerd-shimrunc,里面包含了 dlv 启用相应命令 debug 服务监听,当 containerd 调用时,调用的是我们脚本,运行命令并阻塞在我们下的断点处,这时我们通过Goland remote debug连接即可

这里顺便给出具体的方法

设置源并安装 dlv,任意目录运行

1
2
go env -w GOPROXY=https://mirrors.aliyun.com/goproxy/,direct
go install github.com/go-delve/delve/cmd/dlv@latest

默认的 GOPATH 在 家目录/go 我这里安装到了 /root/go/bin/dlv

docker-runc/src/github.com/opencontainers/runc 目录执行

1
2
# 添加调试信息,避免对一些函数进行优化内联造成调试困难
make EXTRA_FLAGS='-gcflags="all=-N -l"'

把编译出来的 runc 移动到默认环境变量找到的地方,方便其他组件调用,并写一个脚本通过 dlv 去调用

1
2
3
4
5
6
7
8
mv runc /usr/bin/runc.debug

cat <<EOF > /usr/bin/runc
#!/bin/bash
/root/go/bin/dlv --listen=:2345 --headless=true --api-version=2 --accept-multiclient exec /usr/bin/runc.debug -- $*
EOF

chmod +x /usr/bin/runc

Goland端配置

image-20220709230931326

containerd-shim 也是类似

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
cd /root/docker-containerd-go/src/github.com/containerd/containerd/cmd/containerd-shim
go build -gcflags=all="-N -l"

mv containerd-shim /usr/bin/containerd-shim.debug

cat <<EOF > /usr/bin/containerd-shim
#!/bin/bash
/root/go/bin/dlv --listen=:2346 --headless=true --api-version=2 --accept-multiclient exec /usr/bin/containerd-shim.debug -- $*
EOF

chmod +x /usr/bin/containerd-shim

cd /root/docker-containerd-go/src/github.com/containerd/containerd/cmd/containerd-shim-runc-v2
go build -gcflags=all="-N -l"

mv containerd-shim-runc-v2 /usr/bin/containerd-shim-runc-v2.debug

cat <<EOF > /usr/bin/containerd-shim-runc-v2
#!/bin/bash
/root/go/bin/dlv --listen=:2347 --headless=true --api-version=2 --accept-multiclient exec /usr/bin/containerd-shim-runc-v2.debug -- $*
EOF

chmod +x /usr/bin/containerd-shim-runc-v2

此时我们通过 cli 发送一个请求,即可打通 dockerd containerd containerd-shim 和 runc 了

image-20220709234327163

一些其他问题

1、启动Dockerd问题

1
failed to start daemon: error while opening volume store metadata database: time out

docker 没退出干净,看残留的docker进程,然后kill掉就行

1
ps -ef | grep docker

2、可能在运行的Linux上原本通过metarget之类的安装了Docker或者K8S,但它们有的时候卸载不会把containerd和runc给卸载掉,因此搭建前最后确保这些卸载干净

1
apt remove containerd.io

3、在搭建 dockerd 和 containerd 服务时候可能报错如下

1
2
3
4
5
6
# pkg-config --cflags  -- devmapper
Package devmapper was not found in the pkg-config search path.
Perhaps you should add the directory containing `devmapper.pc'
to the PKG_CONFIG_PATH environment variable
No package 'devmapper' found
pkg-config: exit status 1

这是因为相关库没安装导致的,根据错误提示安装相应的依赖即可

1
apt install libdevmapper-dev

除了这个包我还安装了这两个包

1
2
apt install pkg-config
apt install libseccomp-dev

缺的包跟操作系统有关,根据报错提示来就好啦~

引用

[1] https://github.com/docker/docker-ce

[2] https://www.docker.com/resources/what-container/

[3] https://ssst0n3.github.io/post/%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8/%E5%AE%89%E5%85%A8%E7%A0%94%E7%A9%B6/%E5%AE%B9%E5%99%A8%E5%AE%89%E5%85%A8/%E5%AE%B9%E5%99%A8%E9%9B%86%E7%BE%A4%E5%AE%89%E5%85%A8/k8s/%E6%BA%90%E7%A0%81%E5%AE%A1%E8%AE%A1/%E5%A6%82%E4%BD%95%E5%BC%80%E5%8F%91%E5%B9%B6%E7%BC%96%E8%AF%91%E4%BB%A3%E7%A0%81/kubelet-%E8%BF%9C%E7%A8%8B%E8%B0%83%E8%AF%95.html

[4] https://mp.weixin.qq.com/s/mSlc2RMRDe6liXb-ejtRvA

评论