この記事は、2023年4月22日に弊社が開催したRicerca CTF 2023の公式Writeupです。今回はMiscおよびForensicsカテゴリの問題の解法を紹介します。
配布ファイルやスクリプトは以下のGitHubリポジトリを参照してください。
https://github.com/RICSecLab/ricerca-ctf-2023-public
また、他のジャンルのWriteupは以下の投稿から読むことができます。
問題・Writeup著者:arata-nvm
以下のプログラムがサーバーで実行されています。
import subprocess
def base64_decode(s: str) -> bytes:
proc = subprocess.run(['base64', '-d'], input=s.encode(), capture_output=True)
if proc.returncode != 0:
return ''
return proc.stdout
if __name__ == '__main__':
password = input('password: ')
if password.startswith('b3BlbiBzZXNhbWUh'):
exit(':(')
if base64_decode(password) == b'open sesame!':
print(open('/flag.txt', 'r').read())
else:
print('Wrong')
このプログラムは入力された文字列をbase64でデコードし、その結果がopen sesame!
と等しいときにフラグを表示します。ただし、b3BlbiBzZXNhbWUh
(open sesame!
をbase64でエンコードしたもの)が入力された場合は即座にプログラムを停止します。
まず、base64のデコード処理にbase64コマンドが使用されていることに注目します。Pythonの標準ライブラリにはbase64モジュールがあるため、わざわざ外部コマンドを呼び出しているのは不自然です。
そこでbase64コマンドの実装を調査すると、パディング文字=
の後の文字列もデコードされることがわかります。この仕様を利用すればb3BlbiBzZXNhbWUh
のフィルターに引っかからず、かつopen sesame!
としてデコードされるような文字列を作れそうです。
そのような文字列の1つの例としては、o
とpen sesame!
を分割してbase64でエンコードし、再び結合したものが考えられます。実際、以下の実行結果からその文字列がopen sesame!
とデコードされることがわかります。
$ echo -n 'o' | base64
bw==
$ echo -n 'pen sesame!' | base64
cGVuIHNlc2FtZSE=
$ echo 'bw==cGVuIHNlc2FtZSE=' | base64 -d
open sesame!
したがって、bw==cGVuIHNlc2FtZSE=
を入力するとフラグが得られます。
$ nc gatekeeper.2023.ricercactf.com 10005
password: bw==cGVuIHNlc2FtZSE=
RicSec{...}
問題・Writeup著者:pinksawtooth
この問題はWindowsメモリフォレンジックの問題です。マルウェアが実行されたと思われるマシンのメモリダンプが与えられます。マルウェアを抽出して、どのような挙動をするかを解析するまでの一連の能力が問われます。
memory.rawをvolatility3で解析します。 まずはwinfows.infoでメモリの情報を確認します。
次に、pstreeで動作しているプロセスを確認します。 explorer.exeの下にcmd.exeから実行されたpowershell.exeが存在します。
cmdlineコマンドでそれぞれのコマンドライン引数を確認します。 powershell.exeの引数にbase64された長い文字列が存在するため、不審であると判断できます。
オプションから以下の通り、execute policyをバイパスしてbase64でエンコードしたコマンドを実行していることがわかります。
pOwERsHEll -eP bYpASs -e
メインの処理と予想される引数を、base64でデコードしましょう。デコード結果は以下になります。
PowerShellの文字列を実行するIEXを用いて、難読化しているようです。ここから難読化処理を解除します。
今回はCTFの問題で危険な処理は入っていないと予想できるので、IEXを削除した部分をPowerShell上で実行し、本来実行される文字列のみを取得しましょう。なお、実際の検体を解析する際は、かならずVMやWindows Sandboxなどの隔離された環境で実行しましょう。
今回は先頭の& ( $PsHOme[4]+$psHoME[34]+'X')がIEX部分になるので、除去して実行した結果が以下になります。
再度IEXによる難読化が行われているようなので、同様にIEXを削除し、文字列を取得します。 先頭の . ( $vErbOsePrEFeReNcE.TosTrIng()[1,3]+'X'-jOiN'')がIEX部分になります。
実行した結果は以下となります。
先頭のif ($env:COMPUTERNAME -eq "RICSEC") {
によって、コンピュータ名がRICSECの場合のみ実行されるようになっているため、この部分を削除します。
さらに、今回は末尾にIEXがあるようなのでifの閉じ括弧とあわせて| &( $SheLLID[1]+$SheLliD[13]+'x')};
も同時に削除します。
これらを取り除いたコードを実行した結果は以下になります。
さらに末尾の|.( $enV:cOMsPEC[4,15,25]-JOiN'')で実行しているようです。
こちらも除去し、実行します。
読みやすいコードが出現しました。
先頭のif((Get-Date).Month -eq 4 -and (Get-Date).Day -eq 1) {
によって、日時が4/1の場合のみ実行されるようなので、最後尾の};
も含め除去します。
残りのスクリプトは文字列操作が読み取れるので、;
で区切り適宜実行しながら復元すると以下になります。
flagと思われるHKCU:\Software\Microsoft\CTF
のfiend
からデータを読み込み、文字列f1bb3r
とXORを行った後に、AESで暗号化して元のレジストリにデータを書き戻していることがわかります。
ただし、AESの暗号鍵となるSHA256ハッシュに渡している文字列がf1bb3r
を各文字に分解した後の10進数スペース区切りの文字列102 49 98 98 51 114
である点に注意が必要です(powershellの難読化スクリプトを流用する場合は意識する必要はありません)。
IVは0..15
なので\x00..\x0f
までのバイト列であることもわかります。
つまりこの逆操作を行えばよいだけとなります。
書き戻されているはずの、暗号化されたデータをprintkeyで確認しましょう。
\x39\xda\x2a\x85\xc9\x5b\x42\x17\x84\x11\xd8\x23\x3b\x0b\xf2\x0e\x26\x8c\x95\x89\xff\xe6\xf1\x7e\x4b\xf8\x43\x42\xd0\x24\x37\x70とわかりました。復号スクリプトを書きましょう。
import hashlib
from Crypto.Cipher import AES
c = b"\x39\xda\x2a\x85\xc9\x5b\x42\x17\x84\x11\xd8\x23\x3b\x0b\xf2\x0e\x26\x8c\x95\x89\xff\xe6\xf1\x7e\x4b\xf8\x43\x42\xd0\x24\x37\x70"
key = hashlib.sha256(b"102 49 98 98 51 114").digest()
iv = b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f"
aes = AES.new(key, AES.MODE_CBC, iv)
c = aes.decrypt(c)
k = "f1bb3r"
flag = ""
for i in range(len(c)):
flag += chr(c[i] ^ ord(k[i % 6]))
if "}" in flag:
break
print(flag)
実行します。
$ python solver.py
RicSec{6r347_90w3r!}
flagが得られました。