docker 学习笔记及其在 ctf 中的应用

docker的基本使用

阮一峰老师的文章:Docker 入门教程,学习过程中大部分参考了这篇文章。

安装docker

可以参考官网链接

我的安装过程如下:

  • 卸载旧版本:sudo apt-get remove docker docker-engine docker.io containerd runc

  • 根据本地ubuntu版本选择下载要安装docker版本的.deb包:https://download.docker.com/linux/ubuntu/dists/,转到对应版本pool/stable/目录

  • 安装下载的deb包:sudo dpkg -i /path/to/package.deb

  • 或者使用apt安装

    1
    2
    sudo apt-get update
    sudo apt-get install docker-ce docker-ce-cli containerd.io

    2022/11/27更新:在ubuntu20.04下,用上面的命令安装不成功,解决方案参考Installing Docker in Ubuntu, from repo. Can’t find a repo,命令如下:

    1
    2
    3
    4
    5
    6
    7
    sudo apt update
    sudo apt install docker-ce docker-ce-cli containerd.io
    sudo apt-get install ca-certificates curl gnupg lsb-release
    curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
    echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
    sudo apt-get update
    sudo apt-get install docker-ce docker-ce-cli containerd.io
  • 测试安装是否成功:sudo docker run hello-world

正常情况下运行docker是需要sudo权限的,为了防止每次都需要加一个sudo前缀,可以新建一个docker组,并将当前用户添加到这个组里。具体操作如下几条命令:

1
2
sudo groupadd docker
sudo usermod -aG docker $USER

重启后,直接运行docker run hello-world,发现普通用户也能运行啦!

常用命令

使用docker时,常用命令都列在这儿了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
docker image ls   #列出本机所有的image文件
docker image rm [imageName] #删除image文件
docker image pull library/hello-world #从远程仓库将library组下的hello-world这个image文件抓取到本地 (Docker官方提供的image文件都在library中)

docker container run hello-world #从image文件中,选取名为hello-world的image,生成一个运行的容器实例

docker container run -it ubuntu   #启动ubuntu容器时,起一个交互式shell
docker container run -it ubuntu bash #达到跟上一条命令同样的效果
docker container run -it ubuntu ls #启动一个ubuntu容器,并执行ls命令,执行完指定命令后,容器自动退出
docker container kill [containerID] #对于不会自动终止的容器,需要使用kill手动终止

docker container ls #列出本机正在运行的容器
docker container ls --all #列出本机所有容器,包括终止运行的容器
docker container rm [containerID] #对于终止运行的容器,防止其占用磁盘空间,可以使用该命令手动删除

docker container run --rm -it ubuntu /bin/bash #使用--rm参数,在容器终止运行后,会自动删除容器文件

docker container start [containerID] # 上面的docker container run命令每运行一次就会新建一个容器,因此可以通过start来启动已生成的容器
docker container stop [containerID]
docker container logs [containerID]
docker container exec -it [containerID] /bin/bash
docker container cp [containerID]:[/path/to/file] .

echo “123” > test.txt
cat > test1.txt <<EOF

1
2
3
4
EOF

实践:制作一个容器并发布

步骤:

  1. 编写 Dockerfile 文件

  2. 创建 image 文件

    1
    2
    3
    4
    docker image build -t koa-demo .
    # 或者
    docker image build -t koa-demo:0.0.1 .
    # 使用-t参数指定生成的image文件名,冒号后面指定标签(默认标签是latest)。最后的.指定文件所在路径,.表示当前路径
  3. 生成容器

    1
    2
    docker container run --rm -p 8000:3000 -it koa-demo:0.0.1         
    # 容器的 3000 端口映射到本机的 8000 端口
  4. 发布 image 文件

实践:使用docker-compose启动容器

Docker:Docker Compose 详解

1
2
sudo curl -L https://github.com/docker/compose/releases/download/1.21.2/docker-compose-$(uname -s)-$(uname -m) -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose

docker-compose.yml文件解析

1
2
3
4
5
6
version: '2' # 表示该 Docker-Compose 文件使用的是 Version 2 file
services:
docker-demo: # 指定服务名称
build: . # 指定 Dockerfile 所在路径
ports: # 指定端口映射,暴露端口信息 - "宿主机端口:容器暴露端口"
- "9000:8761"

使用docker-compose.yml启动容器

1
2
3
docker-compose up
docker-compose up -d # 后台启动并运行容器
# 在 docker-compose.yml 所在路径下执行该命令 Compose 就会自动构建镜像并使用镜像启动容器

实践:docker容器迁移

docker save/load :用来保存/加载image镜像包

docker export/import :用来保存/加载container容器包

1
2
3
4
5
6
7
8
9
10
docker image ls

docker save [ImageId] > xxx.tar
docker load < xxx.tar
docker tag [ImageId] xxx:1.1.x #有时候需要给镜像重命名并打tag
docker run -d --rm -h [HostName] --name [ContainerName] -p [HostPort:ContainerPort] xxx:1.1.x

docker container ls -all
docker export [ContainerId] > xxx.tar
docker import xxx.tar [ContainerName]:[Tag] #设置导入后的镜像名称和tag

ctf中的应用

起docker

通常ctf比赛中提供Dockerfile给我们,我们需要先build出image,然后再运行container

1
docker build -t nullptr . && docker run -p 1024:1024 --rm -it nullptr

方式2、带命令行

1
docker build -t nullptr . && docker run -p 1024:1024 --rm -it nullptr bash

方式3、新开一个端口,以特权模式运行,并且带命令行

1
docker build -t nullptr . && docker run -p 1024:1024 -p 1234:1234 --privileged --rm -it nullptr bash

解决网络的问题

ubuntu 19.10的source.list(注意是http,不是https):

1
2
3
4
5
6
7
8
9
10
11
12
13
# 默认注释了源码镜像以提高 apt update 速度,如有需要可自行取消注释
deb [trusted=yes] http://mirrors.tuna.tsinghua.edu.cn/ubuntu/ eoan main restricted universe multiverse
# deb-src https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ eoan main restricted universe multiverse
deb [trusted=yes] http://mirrors.tuna.tsinghua.edu.cn/ubuntu/ eoan-updates main restricted universe multiverse
# deb-src https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ eoan-updates main restricted universe multiverse
deb [trusted=yes] http://mirrors.tuna.tsinghua.edu.cn/ubuntu/ eoan-backports main restricted universe multiverse
# deb-src https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ eoan-backports main restricted universe multiverse
deb [trusted=yes] http://mirrors.tuna.tsinghua.edu.cn/ubuntu/ eoan-security main restricted universe multiverse
# deb-src https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ eoan-security main restricted universe multiverse

# 预发布软件源,不建议启用
# deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ eoan-proposed main restricted universe multiverse
# deb-src https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ eoan-proposed main restricted universe multiverse

以ALLES!CTF 2020中的nullptr这个题为例,其Dockerfile做了如下修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# docker build -t nullptr . && docker run -p 1024:1024 --rm -it nullptr

FROM ubuntu:19.10
#bling-我新增了下面这行,指定源,这样下载docker或者apt更新的时候会更快
ADD sources.list /etc/apt/

RUN apt-get update && apt-get install -y gdb
RUN useradd -d /home/ctf/ -m -p ctf -s /bin/bash ctf
RUN echo "ctf:ctf" | chpasswd

WORKDIR /home/ctf

COPY nullptr .
COPY flag .
COPY ynetd .

RUN chmod +x ./ynetd ./nullptr
#bling-我注释了下面这行,使docker起来后有root权限
#USER ctf

CMD ./ynetd ./nullptr

Dockerfile中更换国内源

gdbserver调试

有两种方法:

  1. 通过Dockerfile生成image,然后启动container时直接拉起目标进程(默认)

    docker container run --rm -p 8000:8888 -p 1234:1234 -d <img-name>:latest

    • 进入docker内部,并起一个shell:docker exec -it <containerID> /bin/bash

    • (本地)让目标进程停下来:通过python脚本连接docker服务,并在脚本中通过raw_input()停下来,给gdbserver一些时间attach

    • (docker内)查看目标进程的pid,启动gdbserver,attach到目标进程:

      1
      gdbserver :1234 --attach <pid>
    • (本地)启动gdb,连接远程目标:

      1
      2
      file <xxx>
      target remote :1234
  2. 启动container时命令行指定”/bin/bash”,进入docker后再手动起目标进程

    docker container run --rm -p 8000:8888 -p 1234:1234 -it <img-name>:latest /bin/bash

    • (docker内部)运行目标进程:./xxx &

    • (本地)让目标进程停下来:通过python脚本连接docker服务,并在脚本中通过raw_input()停下来,给gdbserver一些时间attach

    • (docker内部)查看目标进程的pid,启动gdbserver,attach到目标进程:

      1
      gdbserver :1234 --attach <pid>
    • (本地)启动gdb,连接远程目标:

      1
      2
      file <xxx>
      target remote :1234

拉取docker中文件

查看docker的 container id:

1
docker container list

将docker内文件拉取至本地:

1
docker cp <containerId>:/file/path/within/container /host/path/target

在poc脚本中指定如下libc和ld:

1
2
3
4
myelf  = ELF("./note")
libc   = ELF("./libc.so.6")
ld     = ELF("./ld-2.29.so")
io     = process(argv=[ld.path,myelf.path],env={"LD_PRELOAD" : libc.path})

ctf pwn题部署工具

socat

socat可以为每一个连接者提供一个独立的二进制程序执行环境

新版瑞士军刀:socat

socat基本用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
socat - -              # 把标准输入和标准输出对接,输入什么显示什么

## 网络测试
socat - TCP-LISTEN:8080 # 终端1 上启动 server 监听 TCP
socat - TCP:localhost:8080 # 终端2 上启动 client 链接 TCP

socat - TCP-LISTEN:8080,fork,reuseaddr # 终端1 上启动 server
socat - TCP:localhost:8080 # 终端2 上启动 client

socat - UDP-LISTEN:8080 # 终端1 上启动 server 监听 UDP
socat - UDP:localhost:8080 # 终端2 上启动 client 链接 UDP

## 端口转发
socat TCP-LISTEN:8080,fork,reuseaddr TCP:192.168.1.3:80 # 将8080端口所有流量转发给远程机器的 80 端口

## 远程登录
socat TCP-LISTEN:8080,fork,reuseaddr EXEC:/usr/bin/bash # 服务端提供 shell
socat - TCP:localhost:8080 # 客户端登录

## 网页服务
socat TCP-LISTEN:8080,fork,reuseaddr SYSTEM:"bash web.sh"

## 文件传输
socat -u TCP-LISTEN:8080 open:record.log,create # 服务端接收文件
socat -u open:record.log TCP:localhost:8080 # 客户端发送文件

## 透明代理
socat TCP-LISTEN:<本地端口>,reuseaddr,fork SOCKS:<代理服务器IP>:<远程地址>:<远程端口>,socksport=<代理服务器端口>
socat TCP-LISTEN:<本地端口>,reuseaddr,fork PROXY:<代理服务器IP>:<远程地址>:<远程端口>,proxyport=<代理服务器端口>

xinetd

在实体机上部署部分题目时(如一人起一个qemu),需要使用xinetd服务

基本使用方法:启动一个二进制

那么,对于一个新手来说,应该怎样入门xinetd的使用呢?这里我记录了几个重要的步骤:

  1. 首先,应当写好我们要运行的二进制程序。根据外来的连接请求,我们需要为它们分别起一个新的程序,用于交互。这里以一个简单的打印程序为例。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // gcc aaa.c -o aaa
    #include<stdio.h>
    int main(int argc, char *argv[]){
    printf("hello 1, %s\n",argv[0]);
    fflush(stdout);
    getchar();
    printf("hello 2, %s\n",argv[1]);
    fflush(stdout);
    getchar();
    printf("hello 3, %s\n",argv[2]);
    fflush(stdout);
    getchar();
    return 0;
    }
  2. 安装xinetd:sudo apt install xinetd

  3. xinetd的配置文件位于/etc/xinetd.d/目录下,我们需要在该目录下新建一个文件,名字随意

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    # /etc/xinetd.d/test
    service ctf
    {
    disable = no
    type = UNLISTED
    protocol = tcp
    socket_type = stream
    port = 9999
    wait = no

    server = /home/bling/aaa
    server_args = bbb ccc

    user = root
    }
  4. 配置文件确定无误后,可以重启一下xinetd服务,让配置生效:/etc/init.d/xinetd restart

  5. 配置文件中指定了监听端口为9999,所以我们尝试一下连接该端口,得到了跟预期一样的输出。可以多开几个窗口连接试试,看看netstat -pantu的结果。

    1
    2
    3
    4
    5
    6
    $ nc 127.0.0.1 9999
    hello 1, aaa

    hello 2, bbb

    hello 3, ccc

    以上就是对xnetd的简单使用过程,有了基础框架的了解,后续基于此再添加功能也会清晰很多。

使用xinetd启动多个qemu

对于需要启动qemu的情况,由于qemu启动参数较多,可以使用如下方法:

  1. 新建一个bash脚本文件,将工作目录切换到qemu运行所需文件的目录,然后执行qemu命令(可设置timeout 120,将每个qemu的运行时间限制在120s内,防止长时间过多占用计算机资源)

    1
    2
    3
    4
    # start.sh
    cd /home/bling/optee_v7/out/bin;

    timeout 120 /home/bling/optee_v7/qemu-system-arm -nographic -smp 2 -machine virt,secure=on -cpu cortex-a15 -d unimp -semihosting-config enable=on,target=native -m 1057 -bios bl1.bin -object rng-random,filename=/dev/urandom,id=rng0 -device virtio-rng-pci,rng=rng0,max-bytes=1024,period=1000 -netdev user,id=vmnic -device virtio-net-device,netdev=vmnic
  2. 将xinetd的配置文件的server和server_args改为如下设置,这样xinetd服务restart后,每当有新连接到9999端口时,就会使用sh程序执行start.sh,即启动一个qemu

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    service ctf
    {
    disable = yes
    type = UNLISTED
    protocol = tcp
    socket_type = stream
    port = 9999
    wait = no

    server = /bin/sh
    server_args = /home/bling/start.sh

    user = root
    }

    ps. ctf中对于需要启动qemu的场景,通常需要选手先过一个pow,目的是平衡服务端的性能。那么需要在上文xinetd的配置文件中调用/usr/bin/python3 /xx/xx/xx/pow.py,通过pow.py脚本再去启动qemu。一个实际的例子可以参考我出的optee的题(后续上传了再贴连接)。

ctf pwn常用的ctf_xinetd框架

在Docker中需要部署带chroot的xinetd服务时,考虑直接用ctf_xinetd模版

github ctf_xinetd

Pwn部署框架,出题(ubuntu)

现在大部分题目,都是利用xinetd+docker-compose来快速布置docker题目环境

  • docker-compose用于生成docker image并启动docker容器
  • xinetd在容器中,当容器开始运行后,它根据配置拉起对应的程序,并充当socat的功能

使用方法:

  • 将ctf_xinetd目录下载到本地,并更改
  • 增加docker-compose.yml,并配置
  • 通过 docker-compose up -d 就能启动容器+启动容器内的二进制程序

POW

出题时用到的

待补充…

解题时用到的

自研多进程暴破版

使用multiprocessing中的pool,参考廖雪峰老师的博客

一个ctf题的例子:python3 this.py tkhYS 26

以后碰到需要多进程跑的题目,改改参数处理,is_valid,calc_start函数就行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# this.py
from multiprocessing import Pool
import os
import sys
import time
import hashlib

prefix = sys.argv[1]
difficulty = int(sys.argv[2])
zeros = '0' * difficulty

def is_valid(digest):
if sys.version_info.major == 2:
digest = [ord(i) for i in digest]
bits = ''.join(bin(i)[2:].zfill(8) for i in digest)
return bits[:difficulty] == zeros

def calc_start(count,end):
time1 = time.time()
while True:
if count >= end:
print("exceed,count is %d" % count)
break
s = prefix + str(count)
if is_valid(hashlib.sha256(s.encode()).digest()):
time2 = time.time()
time3 = time2 - time1
print(count)
print(time3)
break
count +=1

pool = Pool()

# pool.apply_async(calc_start, [0,100000,])
pool.apply_async(calc_start, [0,2500000,])
pool.apply_async(calc_start,[2500000,5000000])
pool.apply_async(calc_start,[5000000,7500000])
pool.apply_async(calc_start,[7500000,10000000])
pool.apply_async(calc_start,[10000000,12500000])
pool.apply_async(calc_start,[12500000,15000000])
pool.apply_async(calc_start,[15000000,17500000])
pool.apply_async(calc_start,[17500000,20000000])
pool.apply_async(calc_start,[20000000,22500000])
pool.apply_async(calc_start,[22500000,25000000])
pool.apply_async(calc_start,[25000000,27500000])
pool.apply_async(calc_start,[27500000,30000000])
pool.apply_async(calc_start,[30000000,32500000])
pool.apply_async(calc_start,[32500000,35000000])
pool.apply_async(calc_start,[35000000,37500000])
pool.apply_async(calc_start,[37500000,40000000])
pool.apply_async(calc_start,[40000000,42500000])

print("---start----")
pool.close()
pool.join()
print("---end----")

比赛时长亭给的版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# python3 proof_of_work.py xxxx 26
import string
import sys
from hashlib import sha256
from itertools import chain, product
from multiprocessing import Pool

salt = None
hard_bit = 0

def challenge(c):
s = sha256(salt + ''.join(c).encode()).hexdigest()
h = int(s, 16)
if h >> (256 - hard_bit) == 0:
return ''.join(c)
return None

def solve():
all_case = chain.from_iterable(map(lambda x: product(string.ascii_letters, repeat=x), range(10)))
with Pool() as p:
m = p.imap(challenge, all_case, 1000)
return next(filter(lambda x: x != None, m))

if __name__ == "__main__":
if len(sys.argv) != 3:
print(f"{sys.argv[0]} salt hard_bit")
exit(0)
salt = sys.argv[1].encode()
hard_bit = int(sys.argv[2])
print(f"result: {solve()}")