はじめに
プロフェッショナルサービス部の窪川です。
普段インシデントレスポンスチームとして、各種ログや実行痕跡等々の調査、分析をさせていただいています。
開催から大分時間が経ってしまいましたが、先日のGlobal Cyber Skills Benchmark 2025へOneNTTの一員として参加してきましたので拙筆ながら一部問題の解説をさせていただければと思います。
私が回答した問題はForensics分野の以下5問となります。
- Smoke & Mirrors (Forensics, Very Easy)
- Phantom Check (Forensics, Very Easy)
- Ghost Thread (Forensics, Easy)
- The Nexus Breach (Forensics, Medium)
- Driver's Shadow (Forensics, Hard)
今回はHardのDriver’s Shadowについて解説させていただきます。
Driver’s Shadow
概要
A critical Linux server began behaving erratically under suspected Volnaya interference, so a full memory snapshot was captured for analysis. Stealthy components embedded in the dump are altering system behavior and hiding their tracks; your task is to sift through the snapshot to uncover these concealed elements and extract any hidden payloads.
本問題ではメモリイメージが渡されるので、それを解析して悪性検体に関する情報を解明していきます。また、Forensics分野は他の分野と違い、複数の小問に対する解答をそれぞれFlagとして投入する問題形式になっています。問題文は以下のとおりです。
- What is the name of the backdoor udev Rule (ex:10-name.rules)
- What is the hostname the rootkit connects to
- What is the XOR key used (ex: 0011aabb)
- What SYSCALLS are hooked with ftrace, sorted via SYSCALL number (ex: read:write:open)
- What is the name of the kernel module
- What string must be contained in a file in order to be hidden
- What owner UID and GID membership will make the file hidden (UID:GID)
- There is one bash process that is hidden in USERSPACE, what is its PID
- What is the resolved IP of the attacker
- What is the address of __x64_sys_kill, __x64_sys_getdents64 (ex: kill:getdents64)
バックドアの特定から始まり、悪性検体の情報を深掘りしていく構成となっています。
各設問に回答する形で解説を進めていきます。
解説
事前準備
メモリイメージの解析を行うため、volatility3を使っていきます。
volatilityを使う際、メモリイメージの取得元のOS情報が必要になるため、fileコマンドを実行してOSの種別を確認します。
$ file mem.elf
mem.elf: ELF 64-bit LSB core file, x86-64, version 1 (SYSV)
実行結果から、linuxのメモリイメージであることが分かりました。
続いて、volatilityではlinuxのメモリイメージを解析する際にシンボルテーブルが必要となるため、手動で作成します。今回、以下の記事を参考に実施しました。
シンボルテーブルを作成するには対応するOSのデバッグ情報付きカーネルが必要となります。
どのカーネルを取得すべきかはvolatilityのbannerプラグインを実行することで確認可能です。
$ vol -f mem.elf banner
Volatility 3 Framework 2.26.2
Progress: 100.00 PDB scanning finished
Offset Banner
0x10399cd0 Linux version 6.1.0-34-amd64 (debian-kernel@lists.debian.org) (gcc-12 (Debian 12.2.0-14+deb12u1) 12.2.0, GNU ld (GNU Binutils for Debian) 2.40) #1 SMP PREEMPT_DYNAMIC Debian 6.1.135-1 (2025-04-25)
0x162001a0 Linux version 6.1.0-34-amd64 (debian-kernel@lists.debian.org) (gcc-12 (Debian 12.2.0-14+deb12u1) 12.2.0, GNU ld (GNU Binutils for Debian) 2.40) # SMP PREEMPT_DYNAMIC Debian 6.1.135-1 (2025-04-25)
0x16360420 Linux version 6.1.0-34-amd64 (debian-kernel@lists.debian.org) (gcc-12 (Debian 12.2.0-14+deb12u1) 12.2.0, GNU ld (GNU Binutils for Debian) 2.40) #1 SMP PREEMPT_DYNAMIC Debian 6.1.135-1 (2025-04-25)
0x18181ac0 Linux version 6.1.0-34-amd64 (debian-kernel@lists.debian.org) (gcc-12 (Debian 12.2.0-14+deb12u1) 12.2.0, GNU ld (GNU Binutils for Debian) 2.40) #1 SMP PREEMPT_DYNAMIC Debian 6.1.135-1 (2025-04-25)5)
bannerの実行結果から、DebianのLinux version 6.1.0-34-amd64のカーネルを取得すれば良いことが分かりました。Debianの公式ページ(現在入手不可)からdebファイルをダウンロードし、dwarf2jsonを使うことでシンボルファイルを作成できます。その後、シンボルファイルをvolatilityのsymbolsディレクトリ以下に配置すればvolatilityによる解析の準備が完了となります。
What is the name of the backdoor udev Rule (ex:10-name.rules)
始めにバックドアの特定を行います。
今回udev ruleを用いたバックドアであることが示唆されているのでudev ruleの保存先である以下のディレクトリ内のルールを見ていきます。
/etc/udev/rules.d/
/usr/lib/udev/rules.d/
/run/udev/rules.d/
メモリイメージに残されているudev ruleを確認するためにtimelinerプラグインを実行していきます。
$ vol -f mem.elf timeliner.Timeliner > timeliner
続いて、実行結果からudevに関連した部分を見ていきます。
$ cat timeliner | grep "/udev/rules.d/"
Files Cached Inode for /usr/lib/udev/rules.d/60-libopenni2-0.rules N/A 2017-11-06 08:04:34.000000 UTC 2025-05-12 18:46:28.996000 UTC 2025-05-10 19:36:06.676383 UTC
Files Cached Inode for /usr/lib/udev/rules.d/60-libopenni2-0.rules N/A 2017-11-06 08:04:34.000000 UTC 2025-05-12 18:46:28.996000 UTC N/A
Files Cached Inode for /usr/lib/udev/rules.d/90-console-setup.rules N/A 2018-10-29 21:12:09.000000 UTC 2025-05-12 18:46:29.060000 UTC 2025-05-10 19:10:44.764328 UTC
Files Cached Inode for /usr/lib/udev/rules.d/90-console-setup.rules N/A 2018-10-29 21:12:09.000000 UTC 2025-05-12 18:46:29.060000 UTC N/A
Files Cached Inode for /usr/lib/udev/rules.d/40-usb-media-players.rules N/A 2019-01-20 14:39:01.000000 UTC 2025-05-12 18:46:28.984000 UTC 2025-05-10 19:39:52.468391 UTC
# (省略)
Files Cached Inode for /etc/udev/rules.d/60-vboxadd.rules N/A N/A 2025-05-12 19:19:55.911388 UTC N/A
Files Cached Inode for /etc/udev/rules.d/99-volnaya.rules N/A N/A 2025-05-12 19:19:55.919384 UTC N/A
全体を見ていくと、殆どのルールが近い時間帯にアクセスされていることが分かりますが、一部異なる時間帯にアクセスされているルール(60-vboxadd.rules, 99-volnaya.rules)を確認できます。
悪性を判定するため、これらのファイルをInodePagesプラグインでダンプし、中身を確認していくと99-volnaya.rules
がバックドアであることが分かりました。
$ mkdir file_dump
$ vol -f mem.elf -o file_dump linux.pagecache.InodePages --find /etc/udev/rules.d/99-volnaya.rules --dump
$ mv file_dump/inode_0x9a0974b66798.dmp file_dump/99-volnaya.rules
$ cat file_dump/99-volnaya.rules
ACTION=="add", ENV{MAJOR}=="1", ENV{MINOR}=="8", RUN+="/bin/volnaya_usr run"
What is the hostname the rootkit connects to
ルートキットの接続先を確認するため、始めにルートキットのダンプを行います。
ルールの時と同様に/bin/volnaya_usrのダンプを試みると失敗するため、timelinerの実行結果からvolnaya_usrを再度確認してみると/usr/bin/volnaya_usrに該当のファイルが存在していたことが分かります。
$ cat timeliner | grep volnaya_usr
Files Cached Inode for /usr/bin/volnaya_usr N/A 2025-05-12 19:17:34.102003 UTC 2025-05-12 19:20:29.156000 UTC 2025-05-12 19:20:12.731680 UTC
Files Cached Inode for /usr/bin/volnaya_usr N/A 2025-05-12 19:17:34.102003 UTC 2025-05-12 19:20:29.156000 UTC N/A
Files Cached Inode for /usr/bin/volnaya_usr N/A N/A 2025-05-12 19:20:29.156000 UTC N/A
/usr/bin/volnaya_usrに対して再度InodePagesプラグインを実行すると、ルートキットが取得できるため、IDAを用いてルートキットの解析を行います。

IDAで解析を開始すると初めにmain関数が表示されます。
mainの6行目では接続先であるhostnameの復号を行っている様子が確認できます。
volnaya_usrのパーミッションを変更した後、復号直後にブレークポイントを設定して実行することでhostnameの内容を確認できます。

以上の流れでcallback.cnc2811.volnaya.htb
が接続先であることが分かります。
What is the XOR key used (ex: 0011aabb)
前設問にてxor_keyを用いてhostnameの復号を行っていました。
中を見てみると本データは881ba50d42a430791ca2d9ce0630f5c9
であることが分かります。

What SYSCALLS are hooked with ftrace, sorted via SYSCALL number (ex: read:write:open)
ftraceを用いたフックが行われている場合、volatilityのlinux.tracing.ftrace.CheckFtraceプラグインを実行することで対象の関数を確認することができます。
$ vol -f mem.elf linux.tracing.ftrace.CheckFtrace > ftrace
$ cat ftrace
Volatility 3 Framework 2.26.0
ftrace_ops address Callback Callback address Hooked symbols Module Module address
0xffffc09803a0 - 0xffffc097e200 filldir64 volnaya_xb127 0xffffc097e000
0xffffc09802c8 - 0xffffc097e200 filldir volnaya_xb127 0xffffc097e000
0xffffc09801f0 - 0xffffc097e200 fillonedir volnaya_xb127 0xffffc097e000
0xffffc0980118 - 0xffffc097e200 __x64_sys_kill volnaya_xb127 0xffffc097e000
0xffffc0980040 - 0xffffc097e200 __x64_sys_getdents64 volnaya_xb127 0xffffc097e000
以上の結果からftraceによってフックされているシステムコールはkill, getdents64の2つであることが分かるため、答えはkill:getdents64
となります。
What is the name of the kernel module
カーネルモジュールの名前は前述のCheckFtraceによってModule欄に表示されているvolnaya_xb127
になります。
What string must be contained in a file in order to be hidden
詳細な動作を確認するため、次にmain関数内で実行されていたinstall_moduleを見ていきます。
中を見ていくと途中でsyscallが実行されていることが分かります。

https://chromium.googlesource.com/chromiumos/docs/+/master/constants/syscalls.md 等によって公開されているシステムコールテーブルを確認すると、175はinit_moduleを指す番号であることが分かります。
moduleの詳細を確認するため、以下のスクリプトを使ってsに格納されているポインタからmodule部分を419520 bytes抽出します。

output = "/home/ubuntu/work/htb/drivers_shadow/results/file_dump/inited_module"
ea = 0x00007FFFF7F42010;
size = 0x666c0;
data = get_bytes(ea, size, 0);
with open(output, "wb") as f:
f.write(data)
抽出したデータをIDAで読み込むと関数名が既に定義されているのでhooks_*という関数名とファイルを隠しているという設問の内容からhookによってファイルの表示の挙動を変えていると推測します。
実際にhook_filldirを確認することでファイル表示の条件判定にファイル名とMAGIC_WORDが使用されていること、MAGIC_WORDにvolnaya
が設定されていることを確認できます。


What owner UID and GID membership will make the file hidden (UID:GID)
ファイル名と同様にUID, GIDを使ったファイルの非表示もhookによって行われていると推測し、hook_sys_getdents64を見ていくとUSER_HIDE, GROUP_HIDEという定数を発見できます。

処理を辿ってみるとvfs_fstatatによって取得したuid, gidを定数と比較していることからこれがファイル表示に関連していると判断、1821:1992
がファイルを隠すために使用されたuidとgidになります。
There is one bash process that is hidden in USERSPACE, what is its PID
不審なbashプロセスを探すため、プロセス関連の情報を収集するためにPsAuxプラグインを実行します。
$ vol -f mem.elf linux.psaux.PsAux
実行結果を確認していくと複数のbashが実行されている中で1つだけ親プロセスの異なるものが確認できます。(PID: 2957)

追加の情報を収集するためSockstatプラグインを実行してみると以下の結果が得られました。
$ vol -f mem.elf linux.sockstat.Sockstat

以上の結果から、PIDが2957
のbashは外部との通信を行っている不審なプロセスと判断できます。
What is the resolved IP of the attacker
Sockstatの実行結果から攻撃者のIPアドレスは16.171.55[.]6(defanged)
であることが分かります。
What is the address of __x64_sys_kill, __x64_sys_getdents64 (ex: kill:getdents64)
syscallのアドレスはCheck_syscallプラグインを実行することで取得できます。
$ vol -f mem.elf linux.check_syscall.Check_syscall
実行結果には以下の内容が含まれています。
0xffff82000360 64bit 62 0xffffb88b6bf0 __x64_sys_kill
0xffff82000360 64bit 78 0xffffb8b7c530 __x64_sys_getdents
以上から__x64_sys_kill, __x64_sys_getdentsのアドレスは0xffffb88b6bf0:0xffffb8b7c770
であることが分かります。
おわりに
今回はHTB Business CTF 2025のForensics問題のWriteupを書かせていただきました。
他分野と比べると回答者が多く、難易度としては比較的低めに設定されていた印象です。
個人的には回答までの時間やWriteup公開の遅さなど幾つか反省点もありましたが、チームへの貢献はある程度できていたのでその点については良かった点だと感じています。
加えて、チームの順位もここ数年で1番を記録していたのでその点についても良かった点と認識しています。
ただし今回の結果に満足することなく、来年はTop10に入れるように日々の業務や研鑽に努めてまいります。