zebian.log

技術系備忘録とか

コンテナランタイムを自作した

コンテナの仕組みを勉強したかったため、Goでコンテナランタイムを自作した。雑実装だし未実装の機能もたくさんあるが、ある程度形になってきたため現状をまとめる。

リポジトリ

github.com

プロジェクト名から和の雰囲気を感じるが、これはリポジトリ名をkombu(昆布)にしたかったため、せっかくなら今回は和風で固めようと思ったから。趣があっていいんじゃないでしょうか。

dashiが自作コンテナランタイムだが、nimonoとyaminabeは実験的な要素で、セキュキャン2023でコンテナを使ったマルウェアサンドボックスを実装した経験があり、今回はその再実装を自作コンテナランタイムでやりたいなと思ったので実装している。言語としてGoを選択した理由は、Goを書いたことがなかったということと、コンテナを扱うOSSがGoで実装されている事例が多く見受けられたため。実際OCI(後述)のリファレンス実装でGoが使われており、ライブラリとしてそのまま使うことができたのでよかった。

コンテナを動かすために必要な技術

container-security.dev www.publickey1.jp

コンテナの仕組みを学ぶうえでこれらのサイトがとても役に立った。

低レベルランタイムと高レベルランタイム

コンテナランタイムは大きく分けて2種類あり、それぞれ役割分担しながらコンテナを実現している。

  • 低レベルランタイム - 実際にLinuxの機能を呼び出してコンテナを起動する(例:runC、gVisor)
  • 高レベルランタイム - コンテナイメージの管理などをしている(例:containerd)
Open Container Initiative (OCI)

www.opencontainers.org コンテナの技術仕様を標準化する組織。ランタイムやコンテナイメージの仕様を標準化している。Runtime Specification、Image Specification、Distribution Specificationといった仕様がある。コンテナソフトウェアといえばDockerが有名だが、DockerはこのOCIに準拠するにあたってソースコードが公開されるようになった。

コンテナイメージ

コンテナ環境をパッケージ化したもので、設定ファイル、ファイルシステムバンドルなどが含まれている。

Namespace

Linuxカーネルの機能で、Namespaceの内と外でシステムリソースを分離する。ネットワーク、ファイルシステム、PIDなどそれぞれ分離することができる。ファイルシステム、PIDを分離することによって、Namespaceの内側から外側のプロセスに干渉することができなくなる。コンテナの肝。

chroot, pivot_root

どちらもルートディレクト/を別のディレクトリにすげ替える機能。各コンテナ向けに用意されたrootfsに切り替えるために用いる。chrootができる権限を持っていると簡単にコンテナエスケープできてしまうため、代わりにpivot_rootを利用したほうが安全。

cgroup

プロセスをグループ化し、そのグループに属するプロセスに対してリソースの制限を行う機能。CPU使用率、メモリ使用量など細かく制限することができる。

Capability

Linuxにおける権限の仕組みで、ファイルやプロセスに対して権限を設定する。例えば先述のchrootができる権限、ネットワーク通信ができる権限、プロセスをKillできる権限など、様々な種類がある。特権を持っていなくても、特定の権限を経由してコンテナエスケープすることができてしまうものもあるため、過剰な権限の付与は危険。

Seccomp

特定のシステムコールの実行を禁止する機能。Capabilityの制限を突破されてもSeccompによって防ぐことができる。

コンテナランタイムの実装

dashiはCLIツールとして実装したので、コマンドを入力して操作する。当初は低レベルランタイムという立ち位置で開発をしていたが、この段階ではまだ役割を分けるメリットが特にないため、1つのバイナリとして実装した。

[_____@archlinux kombu]$ ./build/dashi
Usage: dashi <flags> <subcommand> <subcommand args>

Subcommands:
        commands         list all command names
        create           create new container
        delete           delete container
        download         download docker image and convert to OCI runtime bundle
        init             container's init process
        preinit          pre initialize container
        start            start container
downloadコマンド

指定されたDockerイメージをダウンロードし、OCIランタイムバンドル(Filesystem Bundle)に変換するコマンド。ここでは実装をサボっていて、内部でskopeoとumociを呼び出しているだけなので、これらのツールを事前にインストールする必要がある。ダウンロードと変換が完了すると、./bundles以下にバンドルが配置される。

[_____@archlinux kombu]$ ls -l ./bundles
total 8
drwx------ 3 root  root  4096 Jun 27 09:58 busybox-latest
drwx------ 3 root root 4096 May 16 17:12 ubuntu-latest

[_____@archlinux kombu]$ sudo ls -l ./bundles/busybox-latest
total 80
-rw-r--r--  1 root root  2967 Jun 27 09:58 config.json
drwxr-xr-x 11 root root  4096 May 19  2023 rootfs
-rw-r--r--  1 root root 66026 Jun 27 09:58 sha256_50aa4698fa6262977cff89181b2664b99d8a56dbca847bf62f2ef04854597cf8.mtree
-rw-r--r--  1 root root   372 Jun 27 09:58 umoci.json

このconfig.jsonに記述された設定に基づき、コンテナの設定・起動を行う。config.jsonの仕様はここで読むことができる

start/preinit/initコマンド

createコマンドでバンドルを指定してコンテナを作成することに成功すると、./containers/<CONTAINER NAME>以下にバンドルの中身がコピーされる。その後startコマンドでコンテナを起動することができる。

[_____@archlinux kombu]$ sudo ./build/dashi start test
2024/06/27 10:22:08 DEBU Received request from child req=get_cid
2024/06/27 10:22:08 DEBU Received request from child req=get_init_opt
2024/06/27 10:22:08 DEBU Mounted source=proc dest=./containers/test/rootfs/proc
2024/06/27 10:22:08 DEBU Mounted source=tmpfs dest=./containers/test/rootfs/dev
2024/06/27 10:22:08 DEBU Mounted source=devpts dest=./containers/test/rootfs/dev/pts
2024/06/27 10:22:08 DEBU Mounted source=shm dest=./containers/test/rootfs/dev/shm
2024/06/27 10:22:08 DEBU Mounted source=mqueue dest=./containers/test/rootfs/dev/mqueue
2024/06/27 10:22:08 DEBU Mounted source=sysfs dest=./containers/test/rootfs/sys
2024/06/27 10:22:08 DEBU Mounted source=cgroup2 dest=./containers/test/rootfs/sys/fs/cgroup
2024/06/27 10:22:08 DEBU Received request from child req=send_mount_list
2024/06/27 10:22:08 DEBU Set UID uid=0
2024/06/27 10:22:08 DEBU Set GID gid=0
2024/06/27 10:22:08 DEBU Set root path=./containers/test/rootfs
2024/06/27 10:22:08 DEBU Set cwd cwd=/
2024/06/27 10:22:08 DEBU Set env env="PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
2024/06/27 10:22:08 DEBU Set env env="TERM=xterm"
2024/06/27 10:22:08 DEBU Set env env="HOME=/root"
2024/06/27 10:22:08 DEBU Set hostname hostname=umoci-default
2024/06/27 10:22:08 DEBU Set rlimits
2024/06/27 10:22:08 DEBU Set capabilities caps="{Effective:536871968 Permitted:536871968 Inheritable:536871968}"
2024/06/27 10:22:08 INFO Start container... args=[/bin/bash]

root@umoci-default:/# uname -a
Linux umoci-default 6.8.7-arch1-1 #1 SMP PREEMPT_DYNAMIC Wed, 17 Apr 2024 15:20:28 +0000 x86_64 x86_64 x86_64 GNU/Linux

root@umoci-default:/# ps aux
USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root           1  0.0  0.0 1226956 4888 ?        Sl   01:22   0:00 ./build/dashi init
root           6  0.0  0.0   4588  3712 ?        S    01:22   0:00 /bin/bash
root          10  0.0  0.0   7888  3968 ?        R+   01:22   0:00 ps aux

root@umoci-default:/# exit
exit
2024/06/27 10:29:57 DEBU Received request from child req=unmount
2024/06/27 10:29:57 DEBU Unmounted dest=./containers/test/rootfs/sys/fs/cgroup
2024/06/27 10:29:57 DEBU Unmounted dest=./containers/test/rootfs/dev/pts
2024/06/27 10:29:57 DEBU Unmounted dest=./containers/test/rootfs/dev/shm
2024/06/27 10:29:57 DEBU Unmounted dest=./containers/test/rootfs/dev/mqueue
2024/06/27 10:29:57 DEBU Unmounted dest=./containers/test/rootfs/proc
2024/06/27 10:29:57 DEBU Unmounted dest=./containers/test/rootfs/dev
2024/06/27 10:29:57 DEBU Unmounted dest=./containers/test/rootfs/sys
2024/06/27 10:29:57 DEBU Received request from child req=close_con

[_____@archlinux kombu]$

Ubuntuのイメージを使った例だと、デフォルトでconfig.jsonに記述されているbashが呼び出される。psコマンドで確認するとしっかりPID分離が行われており、コンテナの内側からホストのプロセスに触れなくなっていることがわかる。

startコマンド実行からの処理の流れを図にした。

startコマンドを呼び出したあと、preinitコマンドをバックグラウンドで呼び出し、ソケット通信を行いながらコンテナの初期化処理を進めている。preinitコマンドを呼び出す段階でNamespace分離を行っている。GoではCloneflagsを設定するだけで分離することができるため便利。

preinitコマンドを呼び出すコード(dashi/cmd/start.go

cmd := exec.Command(os.Args[0], "preinit")
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.SysProcAttr = c.SpecSysProcAttr()
cmd.ExtraFiles = []*os.File{cSock.F}
if err := cmd.start(); err != nil {
    log.Error("Error occured", "err", err)
    return subcommands.ExitFailure
}

c.SpecSysProcAttr()でconfig.jsonのNamespaceの設定(linux.namespaces)を読み取り、CloneFlagsにパースしている。

preinitコマンド(dashi/cmd/preinit.go

...

if err := c.Init(&opt); err != nil {
    log.Error("Failed to initialize container", "err", err)
    return subcommands.ExitFailure
}

log.Info("Starting container...", "args", opt.Args)
if err := syscall.Exec("/proc/self/exe", []string{"/proc/self/exe", "init"}, os.Environ()); err != nil {
    log.Error("Failed to exec init", "err", err)
    return subcommands.ExitFailure
}

// unreachable here
return subcommands.ExitSuccess

c.Init()で現在のプロセスに対してconfig.jsonの各種設定を反映する。設定完了後にinitコマンドを呼び出しているが、preinitコマンドの呼び出し時と違い、execシステムコールを行い、プロセスを上書きしている。また、chrootを実行したあとであるため、os.Args[0]の代わりに/proc/self/exeを利用して自分自身を呼び出している。

dashi/cmd/init.go

...
// call program
cmd := exec.Command(opt.Args[0], opt.Args[1:]...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr

// socket connection check
go func() {
    pingReqBytes, _ := internal.RequestToBytes("ping")
    for range ticker.C {
        cSock.Write(pingReqBytes)
    }
}()

if err := cmd.Run(); err != nil {
    log.Warn("Exit status was not 0", "err", err)
}

return subcommands.ExitSuccess

initコマンドでは逆にpreinitコマンドの呼び出し時と同じようにexec.Commandで対象のプログラムを呼び出している。対象のプログラムの実行中、バックグラウンドでソケット通信による10秒間隔の疎通確認を行っている。initコマンドが終了する前に疎通が途切れると、ゾンビプロセスになったと判断して対象のプログラムのプロセスをKillする(未実装)。

特権コンテナ/非特権コンテナ

sudoをつけて実行すると特権コンテナが起動し、そうでなければ非特権コンテナが起動する。非特権コンテナでは権限の関係でコンテナの初期化の設定方法が変わるため、config.jsonの設定を非特権用に書き換える処理が追加されている。この処理はruncのコードを参考にした(というかほぼパクリ)。

まだ未実装の機能
  • cgroup
  • Seccomp
    • デバッグに使用しているUbuntuランタイムバンドルのconfig.jsonにSeccompの項目がなかったため、未検証
  • pivot_root
  • ゾンビプロセスの自動Kill
    • PIDがNamespaceによって分離されているため、プロセスの特定ができない
    • どうしようか考え中
  • コンテナ内でのネットワーク通信
Capability、設定が剥がれてる疑惑
root@umoci-default:/# ps aux
USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root           1  0.4  0.0 1227208 4628 ?        Sl   01:30   0:00 /proc/self/exe init
root           9  0.0  0.0   4588  3840 ?        S    01:30   0:00 /bin/bash
root          12 50.0  0.0   7888  3968 ?        R+   01:30   0:00 ps aux
root@umoci-default:/# cat /proc/1/status | grep Cap
CapInh: 0000000020000420
CapPrm: 000001ffffffffff
CapEff: 000001ffffffffff
CapBnd: 000001ffffffffff
CapAmb: 0000000000000000
root@umoci-default:/# cat /proc/9/status | grep Cap
CapInh: 0000000020000420
CapPrm: 000001ffffffffff
CapEff: 000001ffffffffff
CapBnd: 000001ffffffffff
CapAmb: 0000000000000000

本来ならばCapPrmCapEffCapInhと同じ値になっている必要があるが000001ffffffffff、つまり全許可になってしまっている。こういう表記になっているが、非特権コンテナではホストユーザーと同じ権限で動いている(mknod /dev/nullができなかった)?要検証。

マルウェアサンドボックスの実装

現状自作コンテナランタイムをサンドボックス環境として利用するにはあまりにもセキュリティがお粗末過ぎると思っているが、せっかくなので紹介する。

このマルウェアサンドボックスは指定したプログラムを自作コンテナランタイムで動いているコンテナ内で実行し、実行中に呼ばれたシステムコールのログを収集する。ログを解析し、あらかじめ定義しておいた検知ルールに違反していることがわかったら、その違反しているルールを出力する。振る舞い検知だけでマルウェアかどうか判断することはできないため、あくまで「違反している」という体にしている。

システムコールロガーについては前回の記事で書いたので、それを参考にしてほしい。 zebian.hatenablog.com

[_____@archlinux kombu]$ python3 ./task.py task_run

...

./build/yaminabe -t ./target_programs/remove_root/target/debug/remove_root -d ./detection_rules
[2024-07-04T02:23:40Z INFO  yaminabe::sandbox] Created mount directory
[2024-07-04T02:23:42Z INFO  yaminabe::sandbox] Execute runner script...
[2024-07-04T02:23:42Z INFO  yaminabe::wrapper] Running command in the container: ["sh", "/mnt/runner.sh"]
2024/07/04 11:23:45 DEBU Mounted source=proc dest=./containers/22042023-b8d0-48e1-836e-381c56384f3d/rootfs/proc
2024/07/04 11:23:45 DEBU Mounted source=tmpfs dest=./containers/22042023-b8d0-48e1-836e-381c56384f3d/rootfs/dev
2024/07/04 11:23:45 DEBU Mounted source=devpts dest=./containers/22042023-b8d0-48e1-836e-381c56384f3d/rootfs/dev/pts
2024/07/04 11:23:45 DEBU Mounted source=shm dest=./containers/22042023-b8d0-48e1-836e-381c56384f3d/rootfs/dev/shm
2024/07/04 11:23:45 DEBU Mounted source=mqueue dest=./containers/22042023-b8d0-48e1-836e-381c56384f3d/rootfs/dev/mqueue
2024/07/04 11:23:45 DEBU Mounted source=sysfs dest=./containers/22042023-b8d0-48e1-836e-381c56384f3d/rootfs/sys
2024/07/04 11:23:45 DEBU Mounted source=cgroup2 dest=./containers/22042023-b8d0-48e1-836e-381c56384f3d/rootfs/sys/fs/cgroup
2024/07/04 11:23:45 DEBU Mounted source=mount-22042023-b8d0-48e1-836e-381c56384f3d dest=./containers/22042023-b8d0-48e1-836e-381c56384f3d/rootfs/mnt
2024/07/04 11:23:45 DEBU Set UID uid=0
2024/07/04 11:23:45 DEBU Set GID gid=0
2024/07/04 11:23:45 DEBU Set root path=./containers/22042023-b8d0-48e1-836e-381c56384f3d/rootfs
2024/07/04 11:23:45 DEBU Set cwd cwd=/
2024/07/04 11:23:45 DEBU Set env env="PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
2024/07/04 11:23:45 DEBU Set env env="TERM=xterm"
2024/07/04 11:23:45 DEBU Set env env="HOME=/root"
2024/07/04 11:23:45 DEBU Set hostname hostname=umoci-default
2024/07/04 11:23:45 DEBU Set rlimits
2024/07/04 11:23:45 DEBU Set capabilities caps="{Effective:536871968 Permitted:536871968 Inheritable:536871968}"
2024/07/04 11:23:45 INFO Start container... args="[sh /mnt/runner.sh]"
2024/07/04 02:23:45 Loaded BPF binary
2024/07/04 02:23:45 Loaded BPF object
2024/07/04 02:23:45 Attached hook function
removed successfully!
2024/07/04 02:23:46 Successed to export log
2024/07/04 11:23:46 DEBU Unmounted dest=./containers/22042023-b8d0-48e1-836e-381c56384f3d/rootfs/sys/fs/cgroup
2024/07/04 11:23:46 DEBU Unmounted dest=./containers/22042023-b8d0-48e1-836e-381c56384f3d/rootfs/dev/pts
2024/07/04 11:23:46 DEBU Unmounted dest=./containers/22042023-b8d0-48e1-836e-381c56384f3d/rootfs/dev/shm
2024/07/04 11:23:46 DEBU Unmounted dest=./containers/22042023-b8d0-48e1-836e-381c56384f3d/rootfs/dev/mqueue
2024/07/04 11:23:46 DEBU Unmounted dest=./containers/22042023-b8d0-48e1-836e-381c56384f3d/rootfs/proc
2024/07/04 11:23:46 DEBU Unmounted dest=./containers/22042023-b8d0-48e1-836e-381c56384f3d/rootfs/dev
2024/07/04 11:23:46 DEBU Unmounted dest=./containers/22042023-b8d0-48e1-836e-381c56384f3d/rootfs/sys
2024/07/04 11:23:46 DEBU Unmounted dest=./containers/22042023-b8d0-48e1-836e-381c56384f3d/rootfs/mnt
2024/07/04 11:23:46 INFO Exited container
[2024-07-04T02:23:46Z INFO  yaminabe] violation detected!
[2024-07-04T02:23:46Z INFO  yaminabe] violated detection rule: DetectionRule { meta: Meta { name: "Sample rule", desc: "This is the sample rule" }, syscall: Some(Syscall { blacklist_numbers: [], frequent: [SyscallFrequent { threshold_count: 10, number: 59 }, SyscallFrequent { threshold_count: 4, number: 263 }], consecutive: [SyscallConsecutive { threshold_count: 4, number: 1 }] }), timestamp: Some(Timestamp { check_timetravel: true }) }
[2024-07-04T02:23:46Z INFO  yaminabe] message: "Frequent syscall detected: 263"
[2024-07-04T02:23:46Z INFO  yaminabe::sandbox] Removed mount directory

この例では、検体プログラムであるremove_rootが検知ルール「Sample rule」に違反しており、理由が「システムコール263番(renameat)が4回以上呼ばれた」ということがわかる。

detection_rules/sample.toml

[meta]
name = "Sample rule"
desc = "This is the sample rule"

[syscall]
# 指定のシステムコールが実行されていたら検知
#blacklist_numbers = [0, 1, 2, 3] # read, write, open, close
blacklist_numbers = []


# 頻繁に実行されるシステムコールを監視
[[syscall.frequent]]
# 検体の実行中にexecveの実行が10回を超えたら検知
threshold_count = 10
number = 59 # execve

[[syscall.frequent]]
threshold_count = 4
number = 263 # renameat

# 連続で実行されるシステムコールを監視
[[syscall.consecutive]]
# 連続でwriteが4回実行されたら検知
threshold_count = 4
number = 1

[timestamp]
# タイムスタンプが過去や未来に飛んだら(改ざんされたら)検知
check_timetravel = true

他にもいくつか検知項目を用意している。現在の実装ではOR条件になっているが、AND条件にする機能もあったほうがいいかもしれない。

システムコールロガーの問題点

システムコールロガーをコンテナ内で動かすことにおいて色々問題が発生したのでまとめる。

  • 非特権コンテナでBPFを実行できない
2024/05/30 18:36:45 Loaded BPF binary
2024/05/30 18:36:45 Failed to load BPF object: field HookX64SysCall: program hook_x64_sys_call: load BTF: BTF not supported (requires >= v4.18)

非特権環境で実行しようとするとと、このようにBTF not supportedと出力されて実行ができない。 docs.redhat.com このドキュメントによると、そもそも非特権環境では権限不足で動かないっぽい?

  • NamespaceでPID分離しているにもかかわらずホストのログが出力される

先述の通り非特権コンテナでは動かせないため、仕方なく特権コンテナで動かすことにした。

uptime: 0d 2:40:20:733.963.236, syscall: Some(Write), pid: 528, ppid: 483, comm: "node"
uptime: 0d 2:40:20:734.122.348, syscall: Some(Signalfd), pid: 528, ppid: 483, comm: "node"
uptime: 0d 2:40:20:734.173.862, syscall: Some(Signalfd), pid: 528, ppid: 483, comm: "node"
uptime: 0d 2:40:20:734.174.852, syscall: Some(Signalfd), pid: 528, ppid: 483, comm: "node"
uptime: 0d 2:40:20:734.185.193, syscall: Some(Shutdown), pid: 483, ppid: 479, comm: "node"
uptime: 0d 2:40:20:734.303.354, syscall: Some(Signalfd), pid: 483, ppid: 479, comm: "node"
uptime: 0d 2:40:20:734.304.845, syscall: Some(Signalfd), pid: 483, ppid: 479, comm: "node"
uptime: 0d 2:40:20:735.832.16, syscall: Some(Write), pid: 483, ppid: 479, comm: "node"
uptime: 0d 2:40:20:735.921.956, syscall: Some(Signalfd), pid: 483, ppid: 479, comm: "node"
uptime: 0d 2:40:20:735.923.555, syscall: Some(Signalfd), pid: 483, ppid: 479, comm: "node"
uptime: 0d 2:40:20:735.996.357, syscall: Some(Sendmsg), pid: 4754, ppid: 4735, comm: "tokio-runtime-w"
uptime: 0d 2:40:20:736.17.280, syscall: Some(SchedSetaffinity), pid: 4754, ppid: 4735, comm: "tokio-runtime-w"
uptime: 0d 2:40:20:736.22.692, syscall: Some(Signalfd), pid: 4754, ppid: 4735, comm: "tokio-runtime-w"
uptime: 0d 2:40:20:736.34.108, syscall: Some(Recvfrom), pid: 4754, ppid: 4735, comm: "code-5437499feb"
uptime: 0d 2:40:20:736.56.631, syscall: Some(SchedSetaffinity), pid: 4754, ppid: 4735, comm: "code-5437499feb"
uptime: 0d 2:40:20:736.64.799, syscall: Some(RtSigprocmask), pid: 4734, ppid: 4730, comm: "sshd"
uptime: 0d 2:40:20:736.69.315, syscall: Some(Read), pid: 4734, ppid: 4730, comm: "sshd"
uptime: 0d 2:40:20:736.79.392, syscall: Some(MemfdCreate), pid: 4734, ppid: 4730, comm: "sshd"
uptime: 0d 2:40:20:736.84.872, syscall: Some(RtSigprocmask), pid: 4734, ppid: 4730, comm: "sshd"
uptime: 0d 2:40:20:736.85.999, syscall: Some(Unshare), pid: 4734, ppid: 4730, comm: "sshd"
uptime: 0d 2:40:20:736.87.25, syscall: Some(RtSigprocmask), pid: 4734, ppid: 4730, comm: "sshd"
uptime: 0d 2:40:20:736.88.445, syscall: Some(Write), pid: 4734, ppid: 4730, comm: "sshd"
uptime: 0d 2:40:20:736.234.531, syscall: Some(RtSigprocmask), pid: 4734, ppid: 4730, comm: "sshd"
uptime: 0d 2:40:20:736.236.305, syscall: Some(Unshare), pid: 4734, ppid: 4730, comm: "sshd"
uptime: 0d 2:40:20:736.747.483, syscall: Some(Signalfd), pid: 9010, ppid: 9009, comm: "dashi"
uptime: 0d 2:40:20:736.749.506, syscall: Some(Getitimer), pid: 9010, ppid: 9009, comm: "dashi"

...

uptime: 0d 2:40:21:484.391.457, syscall: Some(Mmap), pid: 9038, ppid: 9024, comm: "target"
uptime: 0d 2:40:21:484.399.799, syscall: Some(Pipe), pid: 9038, ppid: 9024, comm: "target"
uptime: 0d 2:40:21:484.405.923, syscall: Some(Mkdirat), pid: 9038, ppid: 9024, comm: "target"
uptime: 0d 2:40:21:484.409.664, syscall: Some(Fstat), pid: 9038, ppid: 9024, comm: "target"
uptime: 0d 2:40:21:484.410.670, syscall: Some(Mmap), pid: 9038, ppid: 9024, comm: "target"
uptime: 0d 2:40:21:484.413.295, syscall: Some(Close), pid: 9038, ppid: 9024, comm: "target"
uptime: 0d 2:40:21:484.415.789, syscall: Some(Mkdirat), pid: 9038, ppid: 9024, comm: "target"
uptime: 0d 2:40:21:484.418.95, syscall: Some(Read), pid: 9038, ppid: 9024, comm: "target"
uptime: 0d 2:40:21:484.423.978, syscall: Some(Fstat), pid: 9038, ppid: 9024, comm: "target"
uptime: 0d 2:40:21:484.424.722, syscall: Some(Mmap), pid: 9038, ppid: 9024, comm: "target"
uptime: 0d 2:40:21:484.426.179, syscall: Some(Mmap), pid: 9038, ppid: 9024, comm: "target"
uptime: 0d 2:40:21:484.430.605, syscall: Some(Mmap), pid: 9038, ppid: 9024, comm: "target"
uptime: 0d 2:40:21:484.432.719, syscall: Some(Mmap), pid: 9038, ppid: 9024, comm: "target"
...

収集されたログを見れば分かる通り、コンテナ内には存在しないプロセスのログが存在するし、pid、ppidもホストから見たものになっている。この問題は前にも遭遇したことがあるため予想はしていたが、BPFプログラムはカーネルレベルで動いているため、これはおそらく仕様。本当は良くないが、仕方ないのでプロセス名「target」でフィルターをかけた。BPF、結構便利だけど若干融通が効かない場面がある。

eBPFに入門して簡単なシステムコールロガーを実装した

セキュキャン2023でSysmonForLinuxを使った経験があり、プログラムの挙動ログを自作ロガーで取りたいなと思ったので、Go+ebpf-goで簡単なシステムコールロガーを実装した。eBPFもGoも初心者なのでコードが汚いのは御愛嬌。

コード全体

コードはここに書いた。記事作成時点のコードであって、最新版ではないので注意。

main.go

package main

import (
    "bytes"
    _ "embed"
    "encoding/binary"
    "encoding/json"
    "fmt"
    "os"
    "os/signal"
    "syscall"

    "github.com/cilium/ebpf"
    "github.com/cilium/ebpf/link"
    "github.com/cilium/ebpf/ringbuf"
    "github.com/cilium/ebpf/rlimit"
)

//go:embed bpf_hook_syscall.o
var bpfBin []byte

type BpfObject struct {
    Events         *ebpf.Map     `ebpf:"events"`
    HookX64SysCall *ebpf.Program `ebpf:"hook_x64_sys_call"`
}

type SyscallEvent struct {
    Timestamp uint64
    SyscallNr uint32
    Pid       uint32
}

func (o *BpfObject) Close() error {
    if err := o.Events.Close(); err != nil {
        return err
    }

    if err := o.HookX64SysCall.Close(); err != nil {
        return err
    }

    return nil
}

func main() {
    spec, err := ebpf.LoadCollectionSpecFromReader(bytes.NewReader(bpfBin))
    if err != nil {
        panic(err)
    }

    if err := rlimit.RemoveMemlock(); err != nil {
        panic(err)
    }

    var o BpfObject
    if err := spec.LoadAndAssign(&o, nil); err != nil {
        panic(err)
    }
    defer o.Close()

    link, err := link.AttachTracing(link.TracingOptions{
        Program: o.HookX64SysCall,
    })
    if err != nil {
        panic(err)
    }
    defer link.Close()

    rd, err := ringbuf.NewReader(o.Events)
    if err != nil {
        panic(err)
    }

    sigCh := make(chan os.Signal, 1)
    signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)

    var event SyscallEvent
    var events []SyscallEvent

l:
    for {
        select {
        case <-sigCh:
            // received signal
            break l
        default:
        }

        // read record
        record, err := rd.Read()
        if err != nil {
            if err == ringbuf.ErrClosed {
                panic(err)
            }
            continue
        }

        // parse record
        if err := binary.Read(bytes.NewBuffer(record.RawSample), binary.NativeEndian, &event); err != nil {
            fmt.Printf("Failed to parse syscall event: %s\n", err)
            continue
        }

        events = append(events, event)
    }

    // export json log
    file, err := os.Create("syscall_events.json")
    if err != nil {
        panic(err)
    }
    defer file.Close()

    encoder := json.NewEncoder(file)

    if err := encoder.Encode(events); err != nil {
        panic(err)
    }

    fmt.Printf("Exported syscall events log\n")
}

bpf_hook_syscall.c

// +build ignore

#define __TARGET_ARCH_x86

#include <linux/bpf.h>
#include <linux/version.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>

char _license[] SEC("license") = "Dual MIT/GPL";

struct syscall_event
{
    __u64 timestamp;
    __u32 syscall_nr;
    __u32 pid;
};

struct
{
    __uint(type, BPF_MAP_TYPE_RINGBUF);
    __uint(max_entries, 1 << 24);
} events SEC(".maps");

// linux/arch/x86/entry/syscall_64.c
// long x64_sys_call(const struct pt_regs *regs, unsigned int nr)
SEC("fentry/x64_sys_call")
int BPF_PROG(hook_x64_sys_call, const struct pt_regs *regs, unsigned int nr)
{
    struct syscall_event *event;
    event = bpf_ringbuf_reserve(&events, sizeof(struct syscall_event), 0);

    // failed to reserve space in ringbuf
    if (!event)
    {
        return 0;
    }

    event->timestamp = bpf_ktime_get_ns();
    event->syscall_nr = nr;
    event->pid = bpf_get_current_pid_tgid() >> 32;

    bpf_ringbuf_submit(event, 0);

    return 0;
}

ビルド

clang -O2 -g -c -target bpf bpf_hook_syscall.c
go build

システムコールをフックする方法

eBPFではLinuxカーネルの特定の関数の実行をフックすることができる。調べた限りtracepointkprobefentryの3種類のやり方がある。カーネルがこれらの動作をサポートしている必要があるが、自分の環境ではサポートされているにもかかわらずtracepointとkprobeが動かなかった(もしかしたら実装が悪かっただけかも)。fentryはLinux5.5以降で使える。

フック関数

// linux/arch/x86/entry/syscall_64.c
// long x64_sys_call(const struct pt_regs *regs, unsigned int nr)
SEC("fentry/x64_sys_call")
int BPF_PROG(hook_x64_sys_call, const struct pt_regs *regs, unsigned int nr)
{
    ...
    return 0;
}

SEC("fentry/<関数名>")のようにフックしたい関数を登録する。

Linuxシステムコール関数はinclude/linux/syscalls.hで見ることができるが、sys_*関数を登録して実行してみたところ、関数が見つからないと言われてしまった(理由は謎。関数の実装がCではなくアセンブリだったから?)。

仕方がないので呼び出し元であるarch/x86/entry/syscall_64.cx64_sys_call関数をフックすることにした。

フック関数はマクロを使ってint BPF_PROG(<フック関数名>, <フックしたい関数の引数>, ...)のように実装する。Cのマクロの混沌を実感した。

イベントを記録する

struct syscall_event
{
    __u64 timestamp;
    __u32 syscall_nr;
    __u32 pid;
};

イベントログ用の構造体。上からタイムスタンプ、システムコール番号(x64_sys_call関数の第2引数をそのまま持ってきた)、システムコールを実行したプロセスのpid。

これらのイベントログをeBPFプログラムを呼び出したGo側に送りたい。BPFにはMapsという機能があり、そこでデータを呼び出し元とやり取りすることができる。ハッシュテーブルやリングバッファなど、データ構造も選択可能。今回はリングバッファを利用した。定義は次の通り。

struct
{
    __uint(type, BPF_MAP_TYPE_RINGBUF);
    __uint(max_entries, 1 << 24);
} events SEC(".maps");

フック関数内では、次のようにイベントの記録を行う。

... {
    struct syscall_event *event;
    event = bpf_ringbuf_reserve(&events, sizeof(struct syscall_event), 0);

    // failed to reserve space in ringbuf
    if (!event)
    {
        return 0;
    }

    event->timestamp = bpf_ktime_get_ns();
    event->syscall_nr = nr;
    event->pid = bpf_get_current_pid_tgid() >> 32;

    bpf_ringbuf_submit(event, 0);

    return 0;
}

リングバッファ上で記録用の領域を確保し、それぞれ値を書き込む。bpf_get_current_pid_tgid関数は戻り値が64ビットの値で、上位32ビットがpidなので右シフトする。

フック関数の登録とイベントの取得

今度はGo側の実装。

type BpfObject struct {
    Events         *ebpf.Map     `ebpf:"events"`
    HookX64SysCall *ebpf.Program `ebpf:"hook_x64_sys_call"`
}

...

var o BpfObject
if err := spec.LoadAndAssign(&o, nil); err != nil {
    panic(err)
}
defer o.Close()

link, err := link.AttachTracing(link.TracingOptions{
    Program: o.HookX64SysCall,
})
if err != nil {
    panic(err)
}
defer link.Close()

eBPFプログラムをオブジェクトに定義し、フック関数が実行されるように登録する。

type SyscallEvent struct {
    Timestamp uint64
    SyscallNr uint32
    Pid       uint32
}

...

rd, err := ringbuf.NewReader(o.Events)
if err != nil {
    panic(err)
}

sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)

var event SyscallEvent
var events []SyscallEvent

l:
    for {
        select {
        case <-sigCh:
            // received signal
            break l
        default:
        }

        // read record
        record, err := rd.Read()
        if err != nil {
            if err == ringbuf.ErrClosed {
                panic(err)
            }
            continue
        }

        // parse record
        if err := binary.Read(bytes.NewBuffer(record.RawSample), binary.NativeEndian, &event); err != nil {
            fmt.Printf("Failed to parse syscall event: %s\n", err)
            continue
        }

        events = append(events, event)
    }

次にリングバッファから送られるイベントログを取得するために、eBPFプログラムで定義したイベントログ用構造体と同じものを定義する。やり方によっては構造体の定義は自動化できるらしい。

リングバッファとのコネクションを生成し、無限ループでデータが送られてくるのを待機する。signal.Notifyによって外からSIGINTを送ると無限ループを抜けるようにした。

送られてきたイベントログはこんな感じ。

// export json log
file, err := os.Create("syscall_events.json")
if err != nil {
    panic(err)
}
defer file.Close()

encoder := json.NewEncoder(file)

if err := encoder.Encode(events); err != nil {
    panic(err)
}

fmt.Printf("Exported syscall events log\n")

最後にイベントログをJsonにエクスポートして終了。

感想

eBPF、実は思っていたよりも難しくないかもしれない。あと、ちゃんとしたコードを書かないとバイトコードチェックで弾かれる。賢い。

参考

zenn.dev zenn.dev ebpf-go.dev eunomia.dev

UEFI Memory Mapで躓いたポイント

概要

UEFIからブートするタイプのOSでは、ブートローダーで取得したUEFI Memory Mapを受け取り、その情報をもとにメモリアロケーションの初期化を行う。私の自作OSもしかり。

Memory Mapの内容は以下の通り(自作OSのコード)。 github.com

pub const UEFI_PAGE_SIZE: usize = 0x1000;

#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum MemoryType {
    Reserved,
    LoaderCode,
    LoaderData,
    BootServicesCode,
    BootServicesData,
    RuntimeServicesCode,
    RuntimeServicesData,
    Conventional,
    Unusable,
    AcpiReclaim,
    AcpiNonVolatile,
    Mmio,
    MmioPortSpace,
    PalCode,
    PersistentMemory,
    Other(u32),
}

#[derive(Debug, Copy, Clone)]
pub struct MemoryDescriptor {
    pub ty: MemoryType,
    pub phys_start: u64,
    pub virt_start: u64,
    pub page_cnt: u64,
    pub attr: u64,
}

ここで言うpage_cntは1ページがUEFI_PAGE_SIZEバイト=つまり4,096バイトのページの個数である。そして以下がOSが受け取ったMemory Mapの出力。

MemoryDescriptor { ty: BootServicesCode, phys_start: 0, virt_start: 0, page_cnt: 1, attr: 15 }
MemoryDescriptor { ty: Conventional, phys_start: 4096, virt_start: 0, page_cnt: 159, attr: 15 }
MemoryDescriptor { ty: LoaderData, phys_start: 1048576, virt_start: 0, page_cnt: 909, attr: 15 }
MemoryDescriptor { ty: Conventional, phys_start: 4771840, virt_start: 0, page_cnt: 883, attr: 15 }
MemoryDescriptor { ty: AcpiNonVolatile, phys_start: 8388608, virt_start: 0, page_cnt: 8, attr: 15 }
MemoryDescriptor { ty: Conventional, phys_start: 8421376, virt_start: 0, page_cnt: 3, attr: 15 }
MemoryDescriptor { ty: AcpiNonVolatile, phys_start: 8433664, virt_start: 0, page_cnt: 1, attr: 15 }
MemoryDescriptor { ty: Conventional, phys_start: 8437760, virt_start: 0, page_cnt: 4, attr: 15 }
MemoryDescriptor { ty: AcpiNonVolatile, phys_start: 8454144, virt_start: 0, page_cnt: 240, attr: 15 }
MemoryDescriptor { ty: BootServicesData, phys_start: 9437184, virt_start: 0, page_cnt: 3328, attr: 15 }
MemoryDescriptor { ty: Conventional, phys_start: 23068672, virt_start: 0, page_cnt: 698677, attr: 15 }
MemoryDescriptor { ty: LoaderData, phys_start: 2884849664, virt_start: 0, page_cnt: 32768, attr: 15 }
MemoryDescriptor { ty: Conventional, phys_start: 3019067392, virt_start: 0, page_cnt: 32769, attr: 15 }
MemoryDescriptor { ty: BootServicesData, phys_start: 3153289216, virt_start: 0, page_cnt: 32, attr: 15 }
MemoryDescriptor { ty: Conventional, phys_start: 3153420288, virt_start: 0, page_cnt: 9909, attr: 15 }
MemoryDescriptor { ty: LoaderCode, phys_start: 3194007552, virt_start: 0, page_cnt: 56, attr: 15 }
MemoryDescriptor { ty: Conventional, phys_start: 3194236928, virt_start: 0, page_cnt: 53, attr: 15 }
MemoryDescriptor { ty: BootServicesData, phys_start: 3194454016, virt_start: 0, page_cnt: 215, attr: 15 }
MemoryDescriptor { ty: Conventional, phys_start: 3195334656, virt_start: 0, page_cnt: 3, attr: 15 }
MemoryDescriptor { ty: BootServicesData, phys_start: 3195346944, virt_start: 0, page_cnt: 6, attr: 15 }
MemoryDescriptor { ty: Conventional, phys_start: 3195371520, virt_start: 0, page_cnt: 1, attr: 15 }
MemoryDescriptor { ty: BootServicesData, phys_start: 3195375616, virt_start: 0, page_cnt: 1501, attr: 15 }
MemoryDescriptor { ty: BootServicesCode, phys_start: 3201523712, virt_start: 0, page_cnt: 168, attr: 15 }
MemoryDescriptor { ty: BootServicesData, phys_start: 3202211840, virt_start: 0, page_cnt: 17, attr: 15 }
MemoryDescriptor { ty: Conventional, phys_start: 3202281472, virt_start: 0, page_cnt: 6, attr: 15 }
MemoryDescriptor { ty: LoaderData, phys_start: 3202306048, virt_start: 0, page_cnt: 4, attr: 15 }
MemoryDescriptor { ty: BootServicesData, phys_start: 3202322432, virt_start: 0, page_cnt: 88, attr: 15 }
MemoryDescriptor { ty: BootServicesCode, phys_start: 3202682880, virt_start: 0, page_cnt: 25, attr: 15 }
MemoryDescriptor { ty: BootServicesData, phys_start: 3202785280, virt_start: 0, page_cnt: 5, attr: 15 }
MemoryDescriptor { ty: BootServicesCode, phys_start: 3202805760, virt_start: 0, page_cnt: 59, attr: 15 }
MemoryDescriptor { ty: BootServicesData, phys_start: 3203047424, virt_start: 0, page_cnt: 11, attr: 15 }
MemoryDescriptor { ty: BootServicesCode, phys_start: 3203092480, virt_start: 0, page_cnt: 39, attr: 15 }
MemoryDescriptor { ty: BootServicesData, phys_start: 3203252224, virt_start: 0, page_cnt: 1, attr: 15 }
MemoryDescriptor { ty: BootServicesCode, phys_start: 3203256320, virt_start: 0, page_cnt: 3, attr: 15 }
MemoryDescriptor { ty: BootServicesData, phys_start: 3203268608, virt_start: 0, page_cnt: 3, attr: 15 }
MemoryDescriptor { ty: BootServicesCode, phys_start: 3203280896, virt_start: 0, page_cnt: 7, attr: 15 }
MemoryDescriptor { ty: BootServicesData, phys_start: 3203309568, virt_start: 0, page_cnt: 4, attr: 15 }
MemoryDescriptor { ty: BootServicesCode, phys_start: 3203325952, virt_start: 0, page_cnt: 14, attr: 15 }
MemoryDescriptor { ty: BootServicesData, phys_start: 3203383296, virt_start: 0, page_cnt: 1, attr: 15 }
MemoryDescriptor { ty: BootServicesCode, phys_start: 3203387392, virt_start: 0, page_cnt: 11, attr: 15 }
MemoryDescriptor { ty: BootServicesData, phys_start: 3203432448, virt_start: 0, page_cnt: 2, attr: 15 }
MemoryDescriptor { ty: BootServicesCode, phys_start: 3203440640, virt_start: 0, page_cnt: 13, attr: 15 }
MemoryDescriptor { ty: BootServicesData, phys_start: 3203493888, virt_start: 0, page_cnt: 5, attr: 15 }
MemoryDescriptor { ty: BootServicesCode, phys_start: 3203514368, virt_start: 0, page_cnt: 13, attr: 15 }
MemoryDescriptor { ty: BootServicesData, phys_start: 3203567616, virt_start: 0, page_cnt: 3, attr: 15 }
MemoryDescriptor { ty: BootServicesCode, phys_start: 3203579904, virt_start: 0, page_cnt: 12, attr: 15 }
MemoryDescriptor { ty: BootServicesData, phys_start: 3203629056, virt_start: 0, page_cnt: 1, attr: 15 }
MemoryDescriptor { ty: BootServicesCode, phys_start: 3203633152, virt_start: 0, page_cnt: 3, attr: 15 }
MemoryDescriptor { ty: BootServicesData, phys_start: 3203645440, virt_start: 0, page_cnt: 2, attr: 15 }
MemoryDescriptor { ty: BootServicesCode, phys_start: 3203653632, virt_start: 0, page_cnt: 8, attr: 15 }
MemoryDescriptor { ty: BootServicesData, phys_start: 3203686400, virt_start: 0, page_cnt: 3, attr: 15 }
MemoryDescriptor { ty: BootServicesCode, phys_start: 3203698688, virt_start: 0, page_cnt: 6, attr: 15 }
MemoryDescriptor { ty: BootServicesData, phys_start: 3203723264, virt_start: 0, page_cnt: 2, attr: 15 }
MemoryDescriptor { ty: BootServicesCode, phys_start: 3203731456, virt_start: 0, page_cnt: 11, attr: 15 }
MemoryDescriptor { ty: BootServicesData, phys_start: 3203776512, virt_start: 0, page_cnt: 3, attr: 15 }
MemoryDescriptor { ty: BootServicesCode, phys_start: 3203788800, virt_start: 0, page_cnt: 15, attr: 15 }
MemoryDescriptor { ty: BootServicesData, phys_start: 3203850240, virt_start: 0, page_cnt: 2, attr: 15 }
MemoryDescriptor { ty: BootServicesCode, phys_start: 3203858432, virt_start: 0, page_cnt: 2, attr: 15 }
MemoryDescriptor { ty: BootServicesData, phys_start: 3203866624, virt_start: 0, page_cnt: 2, attr: 15 }
MemoryDescriptor { ty: BootServicesCode, phys_start: 3203874816, virt_start: 0, page_cnt: 20, attr: 15 }
MemoryDescriptor { ty: BootServicesData, phys_start: 3203956736, virt_start: 0, page_cnt: 1, attr: 15 }
MemoryDescriptor { ty: BootServicesCode, phys_start: 3203960832, virt_start: 0, page_cnt: 8, attr: 15 }
MemoryDescriptor { ty: BootServicesData, phys_start: 3203993600, virt_start: 0, page_cnt: 2, attr: 15 }
MemoryDescriptor { ty: BootServicesCode, phys_start: 3204001792, virt_start: 0, page_cnt: 1, attr: 15 }
MemoryDescriptor { ty: BootServicesData, phys_start: 3204005888, virt_start: 0, page_cnt: 1, attr: 15 }
MemoryDescriptor { ty: BootServicesCode, phys_start: 3204009984, virt_start: 0, page_cnt: 20, attr: 15 }
MemoryDescriptor { ty: BootServicesData, phys_start: 3204091904, virt_start: 0, page_cnt: 2, attr: 15 }
MemoryDescriptor { ty: BootServicesCode, phys_start: 3204100096, virt_start: 0, page_cnt: 8, attr: 15 }
MemoryDescriptor { ty: BootServicesData, phys_start: 3204132864, virt_start: 0, page_cnt: 8, attr: 15 }
MemoryDescriptor { ty: BootServicesCode, phys_start: 3204165632, virt_start: 0, page_cnt: 4, attr: 15 }
MemoryDescriptor { ty: BootServicesData, phys_start: 3204182016, virt_start: 0, page_cnt: 4, attr: 15 }
MemoryDescriptor { ty: BootServicesCode, phys_start: 3204198400, virt_start: 0, page_cnt: 16, attr: 15 }
MemoryDescriptor { ty: BootServicesData, phys_start: 3204263936, virt_start: 0, page_cnt: 2, attr: 15 }
MemoryDescriptor { ty: BootServicesCode, phys_start: 3204272128, virt_start: 0, page_cnt: 1, attr: 15 }
MemoryDescriptor { ty: BootServicesData, phys_start: 3204276224, virt_start: 0, page_cnt: 1, attr: 15 }
MemoryDescriptor { ty: BootServicesCode, phys_start: 3204280320, virt_start: 0, page_cnt: 3, attr: 15 }
MemoryDescriptor { ty: BootServicesData, phys_start: 3204292608, virt_start: 0, page_cnt: 3, attr: 15 }
MemoryDescriptor { ty: BootServicesCode, phys_start: 3204304896, virt_start: 0, page_cnt: 35, attr: 15 }
MemoryDescriptor { ty: BootServicesData, phys_start: 3204448256, virt_start: 0, page_cnt: 513, attr: 15 }
MemoryDescriptor { ty: BootServicesCode, phys_start: 3206549504, virt_start: 0, page_cnt: 12, attr: 15 }
MemoryDescriptor { ty: BootServicesData, phys_start: 3206598656, virt_start: 0, page_cnt: 8, attr: 15 }
MemoryDescriptor { ty: BootServicesCode, phys_start: 3206631424, virt_start: 0, page_cnt: 11, attr: 15 }
MemoryDescriptor { ty: BootServicesData, phys_start: 3206676480, virt_start: 0, page_cnt: 5, attr: 15 }
MemoryDescriptor { ty: BootServicesCode, phys_start: 3206696960, virt_start: 0, page_cnt: 24, attr: 15 }
MemoryDescriptor { ty: BootServicesData, phys_start: 3206795264, virt_start: 0, page_cnt: 2, attr: 15 }
MemoryDescriptor { ty: BootServicesCode, phys_start: 3206803456, virt_start: 0, page_cnt: 1, attr: 15 }
MemoryDescriptor { ty: BootServicesData, phys_start: 3206807552, virt_start: 0, page_cnt: 2, attr: 15 }
MemoryDescriptor { ty: BootServicesCode, phys_start: 3206815744, virt_start: 0, page_cnt: 9, attr: 15 }
MemoryDescriptor { ty: BootServicesData, phys_start: 3206852608, virt_start: 0, page_cnt: 2, attr: 15 }
MemoryDescriptor { ty: BootServicesCode, phys_start: 3206860800, virt_start: 0, page_cnt: 1, attr: 15 }
MemoryDescriptor { ty: BootServicesData, phys_start: 3206864896, virt_start: 0, page_cnt: 2, attr: 15 }
MemoryDescriptor { ty: BootServicesCode, phys_start: 3206873088, virt_start: 0, page_cnt: 1, attr: 15 }
MemoryDescriptor { ty: BootServicesData, phys_start: 3206877184, virt_start: 0, page_cnt: 1, attr: 15 }
MemoryDescriptor { ty: BootServicesCode, phys_start: 3206881280, virt_start: 0, page_cnt: 4, attr: 15 }
MemoryDescriptor { ty: BootServicesData, phys_start: 3206897664, virt_start: 0, page_cnt: 2, attr: 15 }
MemoryDescriptor { ty: BootServicesCode, phys_start: 3206905856, virt_start: 0, page_cnt: 23, attr: 15 }
MemoryDescriptor { ty: BootServicesData, phys_start: 3207000064, virt_start: 0, page_cnt: 1045, attr: 15 }
MemoryDescriptor { ty: BootServicesCode, phys_start: 3211280384, virt_start: 0, page_cnt: 8, attr: 15 }
MemoryDescriptor { ty: BootServicesData, phys_start: 3211313152, virt_start: 0, page_cnt: 3, attr: 15 }
MemoryDescriptor { ty: BootServicesCode, phys_start: 3211325440, virt_start: 0, page_cnt: 1, attr: 15 }
MemoryDescriptor { ty: BootServicesData, phys_start: 3211329536, virt_start: 0, page_cnt: 1, attr: 15 }
MemoryDescriptor { ty: BootServicesCode, phys_start: 3211333632, virt_start: 0, page_cnt: 10, attr: 15 }
MemoryDescriptor { ty: BootServicesData, phys_start: 3211374592, virt_start: 0, page_cnt: 2, attr: 15 }
MemoryDescriptor { ty: BootServicesCode, phys_start: 3211382784, virt_start: 0, page_cnt: 2, attr: 15 }
MemoryDescriptor { ty: BootServicesData, phys_start: 3211390976, virt_start: 0, page_cnt: 1, attr: 15 }
MemoryDescriptor { ty: BootServicesCode, phys_start: 3211395072, virt_start: 0, page_cnt: 3, attr: 15 }
MemoryDescriptor { ty: BootServicesData, phys_start: 3211407360, virt_start: 0, page_cnt: 586, attr: 15 }
MemoryDescriptor { ty: RuntimeServicesData, phys_start: 3213807616, virt_start: 0, page_cnt: 256, attr: 9223372036854775823 }
MemoryDescriptor { ty: RuntimeServicesCode, phys_start: 3214856192, virt_start: 0, page_cnt: 256, attr: 9223372036854775823 }
MemoryDescriptor { ty: Reserved, phys_start: 3215904768, virt_start: 0, page_cnt: 128, attr: 15 }
MemoryDescriptor { ty: AcpiReclaim, phys_start: 3216429056, virt_start: 0, page_cnt: 18, attr: 15 }
MemoryDescriptor { ty: AcpiNonVolatile, phys_start: 3216502784, virt_start: 0, page_cnt: 128, attr: 15 }
MemoryDescriptor { ty: BootServicesData, phys_start: 3217027072, virt_start: 0, page_cnt: 513, attr: 15 }
MemoryDescriptor { ty: Conventional, phys_start: 3219128320, virt_start: 0, page_cnt: 136, attr: 15 }
MemoryDescriptor { ty: BootServicesData, phys_start: 3219685376, virt_start: 0, page_cnt: 32, attr: 15 }
MemoryDescriptor { ty: BootServicesCode, phys_start: 3219816448, virt_start: 0, page_cnt: 35, attr: 15 }
MemoryDescriptor { ty: BootServicesData, phys_start: 3219959808, virt_start: 0, page_cnt: 17, attr: 15 }
MemoryDescriptor { ty: BootServicesCode, phys_start: 3220029440, virt_start: 0, page_cnt: 24, attr: 15 }
MemoryDescriptor { ty: RuntimeServicesData, phys_start: 3220127744, virt_start: 0, page_cnt: 132, attr: 9223372036854775823 }
MemoryDescriptor { ty: AcpiNonVolatile, phys_start: 3220668416, virt_start: 0, page_cnt: 136, attr: 15 }
MemoryDescriptor { ty: Conventional, phys_start: 4294967296, virt_start: 0, page_cnt: 262144, attr: 15 }
MemoryDescriptor { ty: Reserved, phys_start: 4278173696, virt_start: 0, page_cnt: 4, attr: 0 }

1~3行目に着目してもらいたい。

1: MemoryDescriptor { ty: BootServicesCode, phys_start: 0, virt_start: 0, page_cnt: 1, attr: 15 }
2: MemoryDescriptor { ty: Conventional, phys_start: 4096, virt_start: 0, page_cnt: 159, attr: 15 }
3: MemoryDescriptor { ty: LoaderData, phys_start: 1048576, virt_start: 0, page_cnt: 909, attr: 15 }

2番目の領域、Conventionalのpage_cntは159、つまり領域のサイズとしては159 * 4,096 = 651,264バイトになる。開始アドレスが4,096とすると終了アドレスは4,096 + 651,264 = 655,360になる。

ところが、その次の3番目の領域の開始アドレスを見ると1048,576とあり、2番目の終了アドレスから393,216バイト分差がある。ここが躓きポイントであり、私が1年以上気付かなかった自作OSのメモリ破壊バグの原因である(実はみかん本にはこれについての言及が存在する)。

Memory Mapに存在しない領域をどう扱うか

Memory Mapに存在しない領域、先の例では2番目の領域と3番目の領域の間の393,216バイトのことだが、このような領域は空き領域として自由に読み書きできるように扱ってはいけない。

メモリ破壊バグについて

私の自作OSでは、ヒープアロケータ初期化以前のメモリ管理として、4KiBごとの領域をビットマップで管理するビットマップアロケータを実装している。Memory Mapを見るとOSが自由に扱える領域はConventional、BootServicesCode / Data(中身のデータの退避などをしたあとで)であるが、当初のビットマップアロケータは「全てのビットマップを空き領域としてマップしてから、自由に扱えない領域を使用済みとしてマップする」という実装になっていた。つまり、Memory Mapに存在しない領域は空き領域という扱いになっており、そこに何らかのデータが書き込まれた瞬間、メモリ破壊によってOSがクラッシュした。スタック関連のデータが破壊されている感触があったが、詳細は不明。

謎のクラッシュは大抵はメモリ破壊バグ(戒め)

2023年を振り返る

年末なので、今年1年を振り返ってみようと思います。

1月

だそうです...

今もデュアルでBenQを使ってる。BenQはいいぞ。

2月

確かデータサイエンスの授業の期末課題で取り扱ったネタ。徹夜でやってたのを覚えている。内容はしょうもなかったけど一応高評価をもらえた。 マルチスレッド実行が上手く行かなくてシングルでやったので、データの加工にクソほど時間を要した。

この時期特有の電気使い過ぎ問題。

3月

DTM、楽しいけどやる時間がなさすぎて結局趣味にはならなかった。

この頃ずっと自作OSのUSBドライバの実装に取り組んでた。ちなみにノートPCのキーボードは今でも(仮想?)PS/2制御だということを大分後に知った。

久々にお絵かきをしたり。

人生で最も長時間プレイしたゲームがHearts of Iron IVで、そんな感じのゲームを自作してみたくなった。

こういうのがあるので自作OSのデバッグと同時並行してQEMUソースコードを読む会が行われている。

4月

ぼざろの影響で聴き始めた。個人的にはアフターダークが一番好き。

Cのプリプロセッサで遊んでた。

ELFデバッガーを自作してた。

5月

ELFデバッガーの自作でptraceやforkを学んでたりした。

2度目のセキュリティ・キャンプに参加するために応募課題を書いてた。この悩みは結局杞憂だったが、hsjoihs氏に名言を教えてもらった。

この「作業」というやつが運命の分かれ道だった。

具体的に何をやっていたかというと、応募したゼミがZ2の「Rust製Linux向けアンチウイルス実装ゼミ」で、当時は「アンチウイルスに興味はあるが仕組みが全くわからん」という状態で応募しようとしていたため、せっかくなら少しでも実際に手を動かして勉強してみようと思い、リアルタイムマルウェア検知機をChatGPTに質問しながら作り、応募課題にその進捗状況や感想を書く欄もないのに無理矢理ねじ込んだ。 内容はシンプルで、inotifyによるディレクトリ内の変更検知、変更があったファイルの表層解析、ELFバイナリの静的解析を行っている。静的解析に関してはこの短時間で難しいことはできないので、脆弱性のあるscanfコードをバイナリから検出するみたいなことをしていた。

ちなみにこの古いtogetterまとめを見て大いに勇気づけられたので、みんなも応募課題を書くときはぜひ見てほしい。

6月

初めてSECCON Beginners CTFに参加した。Beginnersであってもやっぱり難しかった。

無事に選考通過。よかった~

ついにUSBキーボードの入力にまでこぎつけることができた。ちなみに入力を続けると止まってしまう現象は未だに直ってない。

アセンブラを自作し始めた。結局全然やらずに放置してしまっているので、いつかは再開したい。

7月

やらかし。メインで使ってたLinuxを破壊した。その後も色々あってメインでLinuxを使うのはやめてWindowsに戻した。今の時代WSL2とか便利なものがあるしね。

ちょうど事前学習期間だった気がする。

8月

ついにセキュリティ・キャンプが始まった!

本番でもかなり波乱万丈なことをやっていたが、とても勉強になった。

去年お世話になった内田さんとhikaliumさんと現地で再開できた。

田舎の工場事務所で社内SEのバイトを始めた。通勤に車で1時間かかるのはアレだけど待遇が良いので結構気に入ってる。

9月

成人した。結局タイミングがなくてほぼ飲酒してない。

バイトの初任給でStarfieldを買った。PCのスペックが低くてしょっちゅうフリーズしたり、メインクエストが進行不可になったりしている。悲しい。

10月

Chromebookをポチった。お絵かきに丁度よくて、こういう持ち運べるのが1台あると結構便利。

課題やらバイトやらの開発で10~12月はかなり忙しかった。セキュリティコンテスト、ドローンの自動操縦システムを作るなどをしていた。

11月

研究開発の課題用でFPGAをやることになったので、Tang Primer 20Kを購入した。思いつきでnand2tetrisを実装している。

友達が通っている千葉工大の文化祭に行った。ちぇりーたくあん氏のNAND CPUも実物で見れて感動。色々おしゃべりできたのでとても楽しかった。

12月

ひたすらFPGAをやっていた。nand2tetris自体は完成していて、MMIOを実装して機械語からLEDやディスプレイを制御できるように増築工事中。

総括

今年は色々なことに挑戦するという目標でやっていたので、本音を言うともうちょっと密度が欲しかったところですが、おおむね達成できたのではと思います。特に去年もそうでしたが、今年の2度目のセキュリティ・キャンプに参加したのをきっかけに視野がかなり広まったので、とても良い機会だったなと思います。

授業などが忙しくてセキュリティの勉強が全然できていなかったので、来年はちゃんとやります。

Tello SDKに書かれていない謎仕様

DJI Telloを使ってVideoとStateを取得するプログラムを書いていたときの出来事。

Tello SDK User Guide

www.ryzerobotics.com

Receive Tello State

Tello IP: 192.168.10.1 ->> PC/Mac/Mobile UDP Server: 0.0.0.0 UDP PORT: 8890

Remark3: Set up a UDP server on PC, Mac or Mobile device and listen the message from IP 0.0.0.0 via UDP PORT 8890. Do Remark2 to start receiving state data if you haven’t.

事前に192.168.10.1:8889に任意の任意のデータを送信しないと、Stateの受信に失敗する。

Receive Tello Video Stream

Tello IP: 192.168.10.1 ->> PC/Mac/Mobile UDP Server: 0.0.0.0 UDP PORT:1111

Remark4: Set up a UDP server on PC, Mac or Mobile device and listen the message from IP 0.0.0.0 via UDP PORT 11111.

Remark5: Do Remark2 if you haven’t. Then send “streamon” command to Tello via UDP PORT 8889 to start the streaming.

事前に192.168.10.1:62512に任意のデータを送信しないと、Videoの受信に失敗する。

Wiresharkを覗いてみた

どうやらStateの送信元ポートが8889、Videoの送信元ポートが62512らしい。つまり送信元ポートを叩いていることになる。

謎仕様と書いたが結局のところ...

PythonOpenCVのVideoCaptureでVideoを受信する際にはポートを叩く必要はなかった。VideoとStateを取得するプログラムはRustで書いているので、言語の問題なのかもしれない。

【Linux】Fallout4にModを導入する

LinuxでFallout4にModを導入することができたので、備忘録として残しておこうと思います。

私のPC環境

LinuxのSteamクライアントでFallout4が正常に動いていれば、特に問題はないと思います。

参考にしたサイト

www.nexusmods.com wiki.fallout4.z49.org

英語版Fallout4を日本語化する

Wikiを参考にしながらFallout4Localizerを使って日本語版のバックアップ、英語版の日本語化を行います。私はバッチファイルの内容を確認しながら、手動でファイルのコピーを行いました。

01Fallout4BackupJPFiles.bat

  • Data/Video, Data/Fallout4 - Voices.ba2, Data/Fallout4 - Vocies_rep.ba2Fallout4Localizer内にコピー

02Fallout4InstallJPFiles.bat

英語音声・日本語字幕の場合

  • 以下共通へ

日本語音声・日本語字幕の場合

  • Fallout4Localizer/BackupEnglishVoice内にFallout4 - Voices.ba2をコピー
  • Fallout4Localizer内の*_ja.*に該当するすべてのファイルを*_en.*に書き換え
  • Fallout4Localizer内の*_en.*に該当するすべてのファイルをData内ににコピー
  • バックアップしたFallout4 - Voices.ba2, Fallout4 - Voices_rep.ba2Data内にコピー

以下共通

  • Fallout4Localizer内のInterface, Strings, VideoDataにコピー

iniの編集

Fallout4.iniは、私の環境では/home/[user_name]/.local/share/Steam/steamapps/compatdata/377160/pfx/drive_c/users/steamuser/Documents/My Games/Fallout4/Fallout4.iniに位置しています。Wikiにもあるように、Fallout4.iniは削除してFallout4Custom.iniに以下を追加します。

[Archive]
bInvalidateOlderFiles=1
SResourceArchiveList=Fallout4 - Voices.ba2, Fallout4 - Meshes.ba2, Fallout4 - MeshesExtra.ba2, Fallout4 - Misc.ba2, Fallout4 - Sounds.ba2, Fallout4 - Materials.ba2, Fallout4 - Voices_rep.ba2

Mod Organizer 2をインストールする

次に、Mod Organizer 2をインストールします。Lutrisでも入手することができますが、このリポジトリからのインストールが推奨されているようです。 github.com

READMEに書かれている通り、必要パッケージを事前にインストールし、install.shを実行します。ポップアップが出現するので、それに従ってインストールを進めます。インストールが完了すると、SteamからFallout4を起動しようとするとMod Organizer 2が起動するようになります。私の場合は、インストールする前にProton6.3に変更して一度起動する必要がありました(指示を促すメッセージが表示されます)。

F4SEをインストールする

f4se.silverlock.org

Modを導入する

Nexus ModsからマニュアルダウンロードでModをダウンロードし、Mod Organizer 2からインストールすることができます。

Linuxカーネル(x86_64)をビルドしてQEMUで実行する

カーネルを取得する

$ git clone https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git
$ cd linux
# リポジトリを最新のリリースにチェックアウトする
$ git fetch --tags
$ latestTag=$(git describe --tags `git rev-list --tags --max-count=1`)
$ git checkout $latestTag

カーネルをビルドする

# x86_64の設定ファイルをコピー
$ cp arch/x86/configs/x86_64_defconfig ./.config
# Kernel hackingでいろいろ変更できる
$ make menuconfig
# CPUのコア数で並列処理
$ make -j$(nproc)

ユーザーランドを作る

Linuxカーネルは単体だけでは実行することができないため、BusyBoxを使ってinitramfsを作ります。initramfsはcpioアーカイブで、Linuxカーネルは起動時にファイルシステムをマウントする必要があるのですが、起動時はまだ各デバイスを判別することができないため、一時的なファイルシステムとして使われます。

$ cd ../
$ git clone https://git.busybox.net/busybox.git
$ cd busybox
# Setting -> Build static binaryをチェックする
$ make menuconfig
$ make install
$ find . | cpio -o --format=newc > ../../linux/rootfs.img

QEMUで実行する

bzImageLinuxカーネルの実行ファイルで-kernelオプションによってメモリ上に直接展開されるため、ブートローダーを用意する必要はありません。-initrdオプションでは作成したinitramfsを渡し、-appendオプションのrdinitによって、最初に実行されるプログラムをシェルに設定しています。

$ cd ../../linux
$ qemu-system-x86_64 -kernel ./arch/x86_64/boot/bzImage -initrd ./rootfs.img -append "root=/dev/ram rdinit=/bin/sh"

参考サイト

www.rinsymbol.net nullpo-head.hateblo.jp zenn.dev