先日参加した Hero CTF 2023 で出題された Forensic の問題である「Windows Stands for Loser」をテーマに、Volatility を使った Windows メモリダンプの解析手法について学んだことを書いていきます。
他の問題の Writeup は以下です。
参考:Hero CTF 2023 Writeup - かえるのひみつきち
Windows Stands for Loser(Forensic)
This time, no realistic context, we just need you to find the commands that were executed and the time. (we don’t talk about windows commands here :p) (time to find : UTC+2) Format: Hero{secret:dd/mm/YYYY-hh:mm:ss} Author: Malon
問題バイナリとして与えられた memdump.mem を strings コマンドで参照してみると、どうやら Windows マシンのメモリダンプのようなものであることが読み取れます。
ただし、WinDbg で参照できるクラッシュダンプ形式のファイルではありませんでした。(たぶん FTK Imager で取得したファイルかも?)
とりあえずどのような情報が含まれているのかを確認するため、Volatility3 のオプションをいくつか試しつつ、問題を解いていくことにします。
もくじ
Volatility3 のコマンドを実行する(Windows)
とりあえずいくつかの基本的なコマンドを実行してメモリの情報を参照してみます。
今回使用しているコマンドはすべて Windows のメモリダンプを解析するためのコマンドです。
Linux などのメモリ内から情報を収集する場合は、Linux Tutorial にあるような専用のコマンドを使用します。
メモリから OS 情報を取得する
以下のコマンドで OS の情報を調べることができます。
vol3 -f memdump.mem windows.info.Info
出力結果は以下のようになりました。
NtMajorVersion が 10 なので OS は Windows 10 だということがわかります。
また、Minor Version が 19041 なので Windows 10 の 20H1 に該当することがわかります。
メモリから端末の通信状況を調査する
以下のコマンドでメモリから端末の通信状況を調査できます。
vol3 -f memdump.mem windows.netstat.NetStat
vol3 -f memdump.mem windows.netscan.NetScan
出力は、Netstat コマンドを実行した場合と似たような形式で行われます。
この時 Offset(V)
列に記録されるアドレスは、そのプロセスの EPROCESS 構造体のアドレスに一致します。
特に気になる点としては 192.229.221.95 の 80 番ポートに UWP アプリのコンテナとして動作する WWAHost.exe が接続している点ですが、一旦保留します。
また、NetScan プラグインを使用した出力は以下のようになりました。
ほぼ同じような出力結果ですが、NetScan の方が Pool tag quick scanningという手法を使用しており、カーネルのメモリ空間から隠蔽された情報も含めて収集できるようになるようです。
メモリからプロセスの一覧とコマンドラインを取得する
以下のコマンドでメモリからプロセスの一覧を取得できます。
vol3 -f memdump.mem windows.pslist.PsList
vol3 -f memdump.mem windows.psscan.PsScan
出力結果は以下のようになりました。
何か既定のプログラム以外のプロセスや見慣れないプロセスがないか探してみると、以下のようなプロセスが見つかりました。
5768 1464 ubuntu2204.exe
8888 5128 bash
5464 8020 FTK Imager.exe
FTK Imager
は良いとして、WSL が稼働している環境だということがわかります。
また、PsScan プラグインでも同等の出力を得ることができます。
プロセスの情報をツリー形式で表示できる PsTree プラグインというのもあるようです。
vol3 -f memdump.mem windows.pstree.PsTree
以下のような情報を取得できました。
さらに、メモリからシステム内で実行されているプロセスのコマンドライン情報を収集することもできます。
コマンドラインは以下のコマンドで列挙できます。
vol3 -f memdump.mem windows.cmdline.CmdLine
出力結果もかなり読みやすいです。
特定のプロセスのメモリ領域全体をダンプする
また、以下のコマンドでプロセスのメモリ全体のダンプも取得できます。
※ メモリのサイズによっては出力には少し時間がかかります。
# vol3 -o <出力先のパス> -f memdump.mem windows.memmap --dump --pid <PID>
vol3 -o /tmp -f memdump.mem windows.memmap --dump --pid 1000
プロセスからイメージファイルを取得する
PsList プラグインでは、プロセスの列挙だけでなくプロセスのイメージファイルの取得が可能です。
# vol3 -o <出力先のディレクトリ> -f memdump.mem windows.pslist.PsList --pid <プロセスの PID> --dump
vol3 -o /tmp -f memdump.mem windows.pslist.PsList --pid 6724 --dump
上記のコマンドを実行すると、以下のように指定したプロセスをダンプできます。
※ 拡張子は .dmp として表示されていますが、実際には PE としてダンプできます。
メモリからファイルオブジェクトを収集する
以下のコマンドでメモリからファイルオブジェクトのアドレスとフルパスを取得できます。
出力結果は非常に大きくなります。
vol3 -f memdump.mem windows.filescan.FileScan
今回の問題メモリの場合、jane というユーザフォルダの配下にある WSL 内のファイルのパスを特定することができます。
また、ここで特定したアドレスを使用して、メモリ内からファイルを取得することもできます。
以下のコマンド例では、0xa38f15daa4d0 に存在する WSL 内の .bashrc ファイルを tmp ディレクトリに保存しています。
# vol3 -o <出力先のディレクトリ> -f memdump.mem windows.dumpfiles.DumpFiles --virtaddr <ファイルオブジェクトの仮想アドレス>
vol3 -o /tmp -f memdump.mem windows.dumpfiles.DumpFiles --virtaddr 0xa38f15daa4d0
取得に成功すると以下のように表示されます。
取得したファイルを参照してみると、実際にメモリダンプ内から .bashrc ファイルを取得できていることがわかります。
プロセスにロードされている DLL の一覧を取得する
以下のコマンドでメモリからプロセスにロードされている DLL の一覧を取得できます。
vol3 -f memdump.mem windows.dlllist.DllList
以下のようにプロセスごとにロードしている DLL の列挙が可能ですが、--pid
オプションで PID を指定することが可能です。
また、プロセスのイメージファイルをダンプしたのと同じように、--dump
オプションを使用することでプロセスにロードされているすべての DLL ファイルをダンプすることもできます。
vol3 -o /tmp -f memdump.mem windows.dlllist.DllList --pid 6724 --dump
こちらも DLL ファイルとしてエクスポートされていることがわかります。
オブジェクトハンドルを列挙する
以下のコマンドでオブジェクトハンドルを列挙することができます。
vol3 -f memdump.mem windows.handles.Handles
Volshell3 で対話的にメモリを解析する
Volshell3 は対話的にメモリダンプを解析するためのインターフェースとして動作し、Python のインタプリタと同じように操作することができます。
参考:Volshell - A CLI tool for working with memory — Volatility 3 2.4.2 documentation
Windows のメモリダンプを Volshell3 で解析する場合には以下のコマンドを実行します。
volshell3 -f memdump.mem -w
-w
オプションは既知のシンボルを利用することを指定する重要なオプションであり、このオプションを使用しないとほとんどの情報を参照することができなくなります。
Volshell3 が起動すると以下のような画面になります。
EPROCESS にアクセスする
例えば以下のコマンドを実行すると、メモリ内の EPROCESS 構造体の情報を列挙できます。
proc = ps()
for p in proc:
print(p)
出力結果は以下のようになります。
また、Volshell3 では dt コマンドを使用できます。
これは、WinDbg の dt コマンドとほぼ同等の働きをします。
そのため、例えば以下のようなコマンドを実行することで構造体のオフセットを表示したり、特定の EPROCESS 構造体の中身を列挙できます。
# EPROCESS 構造体の情報を列挙
dt('_EPROCESS')
# ps()[0] で取得した EPROCESS の情報を列挙
proc = ps()[0]
dt(proc)
問題ファイルを解析する
Volatility を使ってシステムの基本的な情報を把握できたので、CTF の問題を解き進めていきます。
今回の問題では、メモリ内から何らかのコマンドとそのコマンドが実行された時刻が Flag になるようです。
しかし、CmdLine プラグインで列挙した情報からは、Flag になりそうなものはありませんでした。
そこで、システム内で実行されている WSL のプロセスに着目します。
Writeup で引用されている以下のページを見ると、WSL の bash プロセスの場合も、通常の Linux と同じコマンドを使用してメモリ内の情報を探索できるようです。
参考:Memory forensics and the Windows Subsystem for Linux - ScienceDirect
また、Volatility の linux_bash は bash プロセスのヒープをスキャンすることで、コマンドの実行履歴を簡単に探索できるようです。
参考:Volatility Labs: MoVP II - 3.3 - Automated Linux/Android Bash History Scanning
参考:Linux Tutorial — Volatility 3 2.4.2 documentation
つまり、WSL で稼働している bash プロセスに対して linux_bash プラグインの探索をかけることができれば実行されたコマンドを特定できそうです。
WSL のプロセスを調査する
冒頭に Info プラグインで確認した通り、このシステムのビルドは 20H1 なので、稼働している WSL のバージョンは 1 であることがわかります。
インサイド Windows の上巻に記載のある通り、WSL1 では、Lxss.sys と Lxcore.sys という PICO プロバイダ(PsRegisterPicoProvider API を使用してカーネルインターフェースへのアクセスを得るカーネルドライバ) によって提供されたインターフェースを使用します。
Pico プロバイダの下で実行されるプロセスは Pico プロセスとして管理されます。
WSL の Pico プロバイダ下で提供されるプロセスのメモリには Linux の vDSO(Virtual Dynamic Shared Object) に近しい構造体が書き込まれます。
参考:VDSO(arm)の実装をちょっと調べてみました - Qiita
以下の画像のように WSL の /bin/bash は Pico プロバイダによって管理される Pico プロセスとして動作します。
引用元:インサイド Windows 第 7 版 (上)
Pico プロバイダは Pico プロセスやスレッドの作成および終了を行う関数を持つとともに、Pico スレッドの syscall や例外が発生したときのコールバックを受け取ります。
そのため、Pico プロバイダ下で実行される Pico プロセスはカプセル化され、以下のようにラップされます。
引用元:インサイド Windows 第 7 版 (上)
例えば、以下のいずれかのコマンドで bash プロセスの EPROCESS 構造体の情報を列挙できます。
# 8888 5128 bash 0xa38f11b8a080
proc = ps()
for p in proc:
if p.UniqueProcessId == 8888:
print(dt(p))
# 8888 5128 bash 0xa38f11b8a080f
dt("_EPROCESS",0xa38f11b8a080)
ただし、この bash プロセスは前述の通り Pico プロバイダにカプセル化されている Pico プロセスなので、PEB の情報を持ちません。
bash プロセスのメモリを手動で解析する
正直ここからは自力で進めるのが完全に無理になってしまったので、Writeup の解説をなぞって進めていきます。
参考:HeroCTFv5/README.md at main · HeroCTF/HeroCTFv5 · GitHub
まず、以下のコマンドで PID:8888 として稼働している bash プロセスのメモリ空間をダンプします。
vol3 -o /tmp -f memdump.mem windows.memmap --dump --pid 8888
出力したダンプファイルに file コマンドをかけてみると、glibc locale file LC_CTYPE
として認識されました。
前述の通り、Vollatility の linux_bash プラグインを使うことで、Linux のメモリダンプからコマンドの実行履歴を抽出できます。
ただし、今回抽出したのは bash プロセスのメモリ空間のみのため、単純にこのプラグインを使用することはできません。
ここで、linux_bash の挙動について以下のように記載されているページがあります。
- Scan the heap of all running /bin/bash instances, or all processes period if —scan-all is supplied. The ---scan-all allows you to ignore the process name, in case an attacker copied a /bin/bash shell to /tmp/a and then entered commands. Furthermore, since we’re only scanning the heap of the process, its much quicker than a whole process address space scan.
- Look for # characters in heap segments. With the address in process memory for each # character, do a second scan for pointers to that address elsewhere on the heap. The goal is to find the timestamp member of the histentry structure. We’re essentially linking up data with pointers to the data.
- With each potential timestamp, we subtract 8 bytes (since it exists at offset 8 of the structure). That should give us the base address of the histentry. Now we can associate any other members of histentry (in particular the line member) with the timestamp.
- Once the scan is finished, collect all histentry structures and place them in chronological order by timestamp. Then report the results.
参考:Volatility Labs: MoVP II - 3.3 - Automated Linux/Android Bash History Scanning
bash プロセスからコマンドの履歴を取得するには、まず bash のプロセス全体をスキャンしたあと、ヒープセグメント内で #
の文字を探しだし、そこから histentry 構造体のタイムスタンプを特定します。
histentry 構造体には、入力された行のコマンドライン文字列や実行時のタイムスタンプなどが含まれます。
参考:history(3): GNU History Library - Linux man page
続いて、これらのタイムスタンプのアドレスから 8 バイト引くと histentry 構造体のベースアドレスを得ることができます。
最後に、メモリ内のすべての histentry 構造体をタイムスタンプ順に列挙してコマンドの実行履歴をメモリから収集します。
あとは、出力した bash プロセスのメモリダンプから上記の手順を手動で実行していきます。
まずは、プロセス全体から #
の文字を探しだし、メモリ内の UNIX タイムスタンプを探索します。
import os
i = 0
memdump = "./pid.8888.dmp"
with open(memdump, "rb") as f:
# ファイルから 1 バイトずつ探索して # が存在するか調べる
while i < os.path.getsize(memdump):
diese = f.read(1)
if not diese:
break
# '#' の文字列が見つかるまで進める
if diese == b"\x23":
one = f.read(1)
# 探索対象は UNIX タイムスタンプなので、先頭は必ず 1 になる
if one == b"\x31": # "1"
# タイムスタンプが見つかったら 9 バイト分読み出してファイルに書き込みする
next_data = f.read(9)
with open("./8888_extracted_info.txt", "a") as f2:
f2.write(f"offset: {hex(i)} - #1")
for byte in next_data:
f2.write(f"{chr(byte)}")
f2.write(f"\n")
i+=9
i+=1
i += 1
上記のスクリプトを実行すると、ノイズは多くなりますが以下の 3 つのタイムスタンプとそのオフセット見つかりました。
offset: 0x30a100 - #1683741543
offset: 0x362d30 - #1683741570
offset: 0x376d10 - #1683741539
続いて、これらのタイムスタンプのオフセットから仮想アドレスを特定の上、そのアドレスを参照しているポインタを見つけ出して histentry 構造体のベースアドレスを特定します。
抽出したメモリダンプのオフセットとロードされている仮想アドレスの対応は、先ほどのvol3 -o /tmp -f memdump.mem windows.memmap --dump --pid 8888
の出力結果から参照できます。
例えば、以下のように 0x30a000 は 0x00007fffeca66000 と対応しているので、0x30a100 のアドレスは 0x00007fffeca66100 と対応していることがわかります。
同じように他の 2 つのオフセットの対応も特定すると、以下の対応になることを特定しました。
offset: 0x30a100 : 0x00007fffeca66100
offset: 0x362d30 : 0x00007fffecabed30
offset: 0x376d10 : 0x00007fffecad2d10
これらの仮想アドレスをリトルエンディアンに変換し、メモリ内で探索します。
探索には適当な Hex Editor が使用できますが、今回は HxD を使用しました。
これでタイムスタンプのポインタを持つアドレスが特定できました。
つまり、この直前分の 8 バイトに書き込まれているアドレスが histentry 構造体に含まれるコマンドラインの文字列を指すポインタであるとと特定できました。
Volshell で仮想アドレスから値を取得する
最後に、特定した histentry 構造体の仮想アドレスを用いて、メモリ内から生データを出力させることでコマンドラインの情報を取得しました。
まず、cc コマンドでコンテキストを bash プロセスに変換した後、db コマンドで仮想アドレスの情報を取得しています。
# cc(offset=None, pid=None, name=None) : Change current shell context.
cc(pid=8888)
# >>> db(0x00007fffecabc4c0) /!\ you can ask to display more bits
>>> db(0x00007fffecabc4c0,200)
0x7fffecabc4c0 65 63 68 6f 20 4b 48 42 73 5a 57 46 7a 5a 53 42 echo.KHBsZWFzZSB
0x7fffecabc4d0 6b 62 32 34 6e 64 43 42 6d 61 57 35 6b 49 47 31 kb24ndCBmaW5kIG1
0x7fffecabc4e0 6c 49 48 64 70 64 47 67 67 64 47 68 6c 49 43 4a lIHdpdGggdGhlICJ
0x7fffecabc4f0 7a 64 48 4a 70 62 6d 64 7a 49 69 42 6a 62 32 31 zdHJpbmdzIiBjb21
0x7fffecabc500 74 59 57 35 6b 4c 43 42 30 61 47 56 79 5a 53 42 tYW5kLCB0aGVyZSB
0x7fffecabc510 70 63 79 42 68 49 47 5a 31 62 6d 35 70 5a 58 49 pcyBhIGZ1bm5pZXI
0x7fffecabc520 67 62 57 56 30 61 47 39 6b 4b 53 35 55 61 47 55 gbWV0aG9kKS5UaGU
0x7fffecabc530 67 63 32 56 6a 63 6d 56 30 49 47 6c 7a 49 44 6f gc2VjcmV0IGlzIDo
0x7fffecabc540 67 64 7a 56 73 58 7a 42 75 4d 77 3d 3d 20 7c 20 gdzVsXzBuMw==.|.
0x7fffecabc550 62 61 73 65 36 34 20 2d 64 00 ab ec ff 7f 00 00 base64.-d.......
>>> db(0x00007fffecabed30)
0x7fffecabed30 23 31 36 38 33 37 34 31 35 37 30 00 00 00 00 00 #1683741570.....
これによって実行されたコマンドラインとタイムスタンプを特定でき、Flag を取得でいました。
※ cc コマンドが Volshell3 だとなぜかエラーになってしまうので、ここだけ Vollatility 2 を使用します。
参考:Command Reference · volatilityfoundation/volatility Wiki
参考:Volatility 3 CheatSheet - onfvpBlog [Ashley Pearson]
まとめ
今回のテーマにした問題は結構難易度が高く、Solver も 3 チームのみの問題でした。
Vollatility の活用方法の幅が広がり、勉強になる良い問題だったと思います。
フォレンジッカーになりたい。