※本記事は2016年10月6日に米国で掲載されたブログ記事の抄訳を基にしています。

今回このブログシリーズでは、LabyREnth、Unit 42セキュリティ コンテストの課題の解答を明らかにします。これまで7週間にわたって課題の各種目の解答を公開してきましたが、最終種目のランダム種目について解答をご提供するときがやって参りました。

ランダム課題1: OMG Java

課題作成者: Jacob Soo @_jsoo_

Javaファイルが与えられていますが、これをByteCodeViewerで逆コンパイルすることができます。

環境変数Adminが必要なことと、この変数の中にif条件(if –isDrunk)が存在することが分かります。この変数を使ってJavaファイルを実行すると次のような出力が得られます。

この出力をbase32デコードすればフラグが得られます。

ウォーミングアップとしてこのJavaアプリをお楽しみください。残りの課題はこれよりもずっと面白いものになりますよ。フラグはPAN{D0_Y0u_Ev3n_Base32}です。

ランダム課題2: 自分のことを整然と表現できますか。

課題作成者: Richard Wartell @wartortell

この課題では2つのファイルserver.pyおよびomglob_what_is_dis_crap.txtが与えられています。ざっとserver.pyを見てみると、これは、どこかで動き、コネクションを受け付け、正規表現を確認し、与えられた情報がその正規表現と一致しないときにはキーを返すサーバーのようです。

それではserver.pyがテストの対象としている正規表現とはどんなものでしょうか。おや、server.pyがomglob_what_is_dis_crap.txtから文字列を読み込んでいることが分かります。それでは見ていきましょう。

この正規表現から3種類の異なる条件が見て取れます。

  1. .*[^0mglo8sc1enC3].*
    この条件は“0mglo8sc1enC3”以外の文字を含む任意の文字列と正規表現がマッチすることを表しています。
  2. .{,190}|.{192,}
    この条件は長さが191文字でない任意の文字列と正規表現がマッチすることを表しています。
  3. .{97}[cgClm]
    条件の残りの部分はこれに似たものです。それぞれ、キー文字列における位置、およびその位置にある文字を伝えてくれます。実際のところ、各条件は、ある位置においてどんな文字が有効でないかを伝えるものです。

したがって、長さが191文字の文字列を作り、第3のタイプの条件を使って各位置にはどんな文字が来るかを伝える必要があります。ここでPythonスクリプトを作成することができます。このPythonスクリプトは、正規表現を構文解析して適切な条件をすべて洗い出し、これらの条件にマッチしない文字列を作り出します。それでは正規表現を構文解析する下記の正規表現を見て楽しんでください。

これを実行すると下記の出力が得られます。

いよいよこれで、この正規表現がサーバーに対してうまく働いてキーを取得してくれるかどうかこの正規表現を試すことができます。

これにより、次のレベルに辿り着くためのキーが返ってきます。

PAN{th4t5_4_pr311y_dum8_w4y_10_us3_r3g3x}

ランダム課題3: あの人たちがどうやって入り込んだのか、あの人たちが何を探していたのか、どちらもよく分からないけれど、クッキーのくずを少し残していったことは確かだ。

課題作成者: Anthony Kasza @anthonykasza

Wiresharkによるネットワーク トレース ファイルを開きIPv4のConversations(やり取り)を観察してみましたが、pcapの中には通信しているシステムが2つしか見つかりませんでした。ConversationsメニューのTCPタブを観察することで、1個の送信元ポートからほとんど全ての宛先ポートに対して張られている多数のコネクションがこのpcapファイルの中に含まれていることが分かるでしょう。これはポート スキャンであることを示しています。

この仮説は、TCPコネクションの中身を調べることで確認できますが、確認してみると中身は何もありません。各SYNパケット(7個を除く)に対してはRSTのレスポンスが返ってきます。残りの7個のコネクションはSYN+ACKというレスポンスを受け取り、コネクションの開始側によってRSTで直ちに閉じられています。このpcapの中にTCP SYNのポート スキャンが含まれているのは間違いありません。

この時点から、この課題の次のステップ用にpcapの中を探し回る必要が出てきました。トレース ファイル内のSYNパケット間での違いは下記のものしかありません。

  • 宛先ポート番号
  • シーケンス番号
  • TCPチェックサム

キャプチャ ファイルの最初のSYNパケットを観察することで、シーケンス番号内にzipファイル ヘッダー(0x504b0304)を見つけ出すことができるでしょう。また、私はこの課題に関するヒントをツイートしましたが、その際DoSクッキーに言及しました。DoSクッキーはSYNフラッド攻撃を防ぐのに使われる技術です。

下の図はトレース ファイルの最初のパケットのシーケンス番号におけるzipヘッダーを示しています。

dpktを利用しているPythonスクリプトを使えばSYNパケットのシーケンス番号からこのzipを抽出して再構築することが容易にできるでしょう。

抽出したzipファイルの中身を取り出してみると、番号を付けられたファイルが853個あることがすぐに分かります。これらのファイルを開いて中身を見れば、base64エンコーディングであることを認識できたはずです。ファイルはそれぞれbase64エンコードされたファイルの「チャンク」です。これらのファイルに対してgrepで‘==’を検索すれば、最後の「チャンク」である339.binを見つけることができるでしょう。次に、339.binの中身の冒頭“SBAW”をgrepで検索すると、339.binと一部が重複しているファイル531.binを特定することができるでしょう。この手動による作業を継続すれば、狂気じみたコピー アンド ペーストの技を使って作られた元のbase64 blobを再構築することができるでしょう。あるいはこのようなスクリプトを使うこともできたでしょう。

このbase64 blobをデコードすると、4個のイメージを含んでいる別のzipファイルが結果として得られます。これらのイメージの1つにフラグPAN{YouDiD4iT.GREATjob}が含まれています。

ランダム課題4: それで、あなたはこの課題に取り組むまでPHPのことが嫌いだと思っていたんだって

課題作成者: Josh Grunzweig @jgrunzweig

ランダム種目の4番目の課題に関してPHPスクリプトが与えられています。他ならぬこのスクリプトは約1500行にも及ぶもので、テキスト ベースの迷路のようなゲームを提示しています。このゲームでは正しい経路を選択して正解を得なければなりません。

根底をなすコードを眺めてみると、難読化されたPHPコードからなる巨大なblobがあり、その後にはすぐ上のデータを生成するHTMLが続きます。コードは主にジャンク コードとおぼしきものから構成されており、それに加えて、その中に点在する独特のコードからなる行が若干あり、これらの行が実際に作業を行います。

このファイルをコピーし、ジャンク コードを取り除いてみれば次のような結果が得られます。

上記コードの難読化を解除すると、何が実行されているかをさらに理解することができます。コメントが追加されており、コードは、それぞれが何を実行しているかを示すためより明確に形式化されています。

この情報に基づき、以下のPythonコードを使用して、提供されたPHPをデコードできます。

これによって、以下のデコードされたPHPコードが手元に残ります。

この時点で、スクリプト内にさまざまなオプションを入力したときに何が起こっているかをトレースできます。どのデータが以前に入力されたかを追跡するため、データは‘.backup.bin’ファイルに書き込まれます。コード全体に目を通し、どのようなチェックが含まれているか把握した後、以下の順序で提供する必要があることを判別します。

  1. Right
  2. 246 Degrees
  3. East
  4. Back
  5. West
  6. 94 Degrees
  7. 94 Degrees
  8. Up
  9. Down
  10. 246 Degrees
  11. East
  12. South
  13. Stroll
  14. North
  15. Left
  16. Up
  17. Up
  18. Skip
  19. Right
  20. South
  21. 246 Degrees
  22. Back
  23. 94 Degrees
  24. Run
  25. Left
  26. South
  27. Run
  28. East
  29. 246 Degrees
  30. Back
  31. Up
  32. Jump

これをプログラムに入力すると、以下が得られます。

これにより、キーはPAN{Life is a maze of complications.Also, puppets are sometimes involved.Deal with it.}であることが判明します。

ランダム5課題: 最新バージョンのAPT Maker Proを解読するには、おそらくへび使いにならなければなりません。Pythonの10行で実現できる最悪なものは何でしょうか。

課題作成者: Gabriel Kirkpatrick @gabe_k

TLOPは、LabyREnth CTFランダム コースの最終課題です。それをダウンロードすると、TLOP.pywというファイルが提供されます。このファイルを実行すると、APT Maker Pro – UNREGISTERED TRIAL VERSIONというプログラムが開かれます。APT Maker Proをアクティブ化する必要があることを通知する“Generate APT (APTの生成)”とラベルされたボタンと、プロダクト キーの入力を求めるダイアログを表示する“Activate APT Maker Pro! (APT Maker Proのアクティブ化)”とラベルされたボタンがあります。ここでの課題は、プログラムをアクティブ化するためのプロダクト キーを見つけ、APTの生成を可能にすることです。

まず、反転してみましょう。TLOP.pywは、pyファイルではなくpywファイルです。ただし、これらは、Windows pywでは、ファイルがコマンド ライン ウィンドウを表示しない点を除き大半は同じです。デバッグ情報をSTDOUTに出力したい場合は、ファイル名をpyに変更することができます。ファイルを開くと、10行のpythonが表示されます(わかりますか? なぜ10行のPython? TLOP…ばかげています)。インポート後のコードの大半は、単にTKinterウィンドウを設定しています。これは実際にはスプラッシュ画面です。最後の行が興味深いところです。

exec marshal.loads(zlib.decompress(<longgggggggg gibberish string>))

それは、marshal.loadsの結果を渡しているexec文で、一部のzlib形式のデータをロードしています。Marshalは、組み込みのPythonタイプとシリアライズおよびデシリアライズするためのPythonモジュールです。Pythonシェル内でexecを削除して、同じ行を実行すると、結果がコード オブジェクトであることを判別できます。Python内のexec文は、Pythonコードの文字列と、コンパイルされたPythonバイトコードを含むコード オブジェクトの2つのタイプの入力を受け入れます。コード オブジェクトは、コンパイルされたPythonファイルである.pycファイル内で頻繁に使用されています。これらのコンパイルされたPythonファイルは、Pythonモジュールがインポートされたときに生成されます。pycファイルには、Python バージョンを指定する32ビットのマジック番号、32ビットのコンパイルのタイムスタンプ、およびmarshal形式のコード オブジェクトが含まれます。すでにmarshal形式のコード オブジェクトを取得しているため、以下のコードを使用して、これを.pycファイルに変換できます。

uncompyle2を使用すると、作成したstage1.pycファイルを逆コンパイルできます。

逆コンパイルされた出力

逆コンパイルの結果には、UI用のコードの大半を含むAptMakerというクラスが含まれています。クラスの後に、RC4関数と、別のmarshalの結果に対する別のexecがあります。execによるオブジェクトを別のpycファイルに組み込み、それを分析できます。

uncompyle2を使用してstage2.pycを逆コンパイルしようとすると、以下のエラーが発生します。

それを他のPython逆コンパイラにスローすると同様のエラーが生成される可能性があります。そのため、別のアプローチを採る必要があります。逆コンパイルしないため、バイトコードの逆アセンブルを試すことができます。Pythonには、disと呼ばれるPythonバイトコードを逆アセンブルするための組み込みモジュールがあります。これを使用して、以下のコードを実行し、生成したpycファイルを逆アセンブルできます。

上記コードから以下の出力が生成されます。

コード出力は、比較的シンプルです。最初の命令、LOAD_CONST 0は、現在のコード オブジェクトのインデックス0にある定数をスタックにプッシュしています。その後、MAKE_FUNCTION 0が、スタックからコード オブジェクトを取得し、それを関数オブジェクトに変換します。3番目の命令、STORE_NAME 0は、スタック上のトップ アイテム(作成したばかりの関数)をポップし、それを名前とともにコード オブジェクトのインデックス0に保存します。この場合の名前は、“verify_license”です。この一連の命令が実際に行っているのは、verify_licenseという名前の関数の作成です。verify_license関数は、“is_licensed”関数内で以前に行った逆コンパイルで参照されていることがわかっています。

最後の2つの命令は、単に定数Noneをスタックにプッシュし、その後、それを返しているだけです。Pythonコード オブジェクトはすべて何かを返す必要があるため、これは明示的に値を返さないすべてのPythonコード オブジェクト内に存在します。

これで、このコードが実行していることは関数の作成のみであることがわかります。stage2.pycファイルをインポートすることで、実際に、Pythonシェルでそれを実行し、verify_license関数を使用することができます。以下のコードを実行すると、シェルから関数をさまざまに扱うことができます。

これから、verify_licenseがboolean値を返す関数であることがわかります。関数を逆アセンブルすると、以下の結果が得られます。

この命令を順番に調べてみましょう。

最初に、コード オブジェクトである定数0をスタックにプッシュしています。

次に、定数1のNone をスタックにプッシュしています。

DUP_TOPは、スタックのトップ アイテムを複製し、別のNoneをスタックにプッシュしています。

EXEC_STMTは、正規のPythonの“exec”キーワードと同等です。これは3つのパラメータを取ります。つまり、コード オブジェクトまたはPython文字列、およびグローバル変数とローカル変数を含む2つのオプション パラメータです。この場合、コードオブジェクトは関数の開始時にプッシュされたもので、グローバル変数とローカル変数は使用されていないため、それらはスタック上の2つのNoneです。

このコードは名前0の保存された値をプッシュし、それを返します。名前0は、ここでは“ “として表示されています。これは、有効なPython名ではなく、関数内の他の場所では参照されていないため、EXEC_STMTが実行するコードによって設定されると考えて間違いありません。

ここには多くのコードはないため、コードの中身はexecが実行されるコード オブジェクト内にあると想定できます。以下を実行すると、コード オブジェクトを逆アセンブルできます。

前回と異なり、ここではdisを実行しています。以下のような非常に長い出力が現れ始めます。

逆アセンブルの全景

実際に、完全に終了するまで逆アセンブルを実行したとしても、実際に役立つ情報は何も得られません。出力全体は、かなり大きい引数を伴う数千のEXTENDED_ARG命令で、それに、同様に大きな値に対するJUMP_FORWARDが続いています。

何が問題でしょうか? Python実行時には、Python命令の引数が、opargと呼ばれる符号付き32ビット整数で保存されます。引数をもつPython命令は3バイト長で、opcodeが1バイト、oparg値が2バイトです。これに伴う問題は、命令には32ビット全体ではなく、16ビット未満のopargしか設定できないことです。この制限を回避するために、PythonにはEXTENDED_ARGという命令があります。これは引数を16ビット左へシフトするため、1つの命令で上位16ビットを設定し、次の命令で下位16ビットを設定できるようになります。連続して複数のEXTENDED_ARG命令を含めると、Python実行時には、単に32ビット整数がシフトされ続け、ビットは底を突きます。ただし、disを使用してコードを逆アセンブルした場合は、opargはPython数値に保存されます。disは固定長32ビット整数ではなくPython数値を使用するため、数値は単一のEXTENDED_ARG命令ごとに増え続けます。

この問題と、disの型破りなバイトコードの処理方法に伴う他のいくつかの問題を理解した後、私は、多少は役立つ逆アセンブルを生成するために使用できるpyasmという独自のアセンブラ/逆アセンブラを記述しました。

stage2.pyc上でdispy.pyを実行すると、stage2.pyasmが得られます。stage2.pyasmの11行目からコード オブジェクトが見つかりましたが、実際の命令は100行目から始まっています。最初の命令の塊を見ると、パターンに気付き始めます。

一貫して、異なる引数値をもつ2、3のEXTENDED_ARG命令があり、それに続いて引数値0xFFFFを使用する133があることがわかります。1390行の命令の末尾近くまでスクロール ダウンすると、最後の2つの命令がこのパターンからわずかに逸れていることがわかります。

JUMP_FORWARDの実際の引数が 65533 << 16 | 52549で、結果として0xfffdcd45になると解決できます。opargは符号付きのため、実際には-144059です。それは、通常実行を開始する最初のバイトではなく、実際にはバイトコードの2番目のバイトにジャンプしています。これは命令が重複する形になります。2番目のバイトが最初の命令の引数となるため、コードがすべてのEXTENDED_ARG命令の引数の中に隠されます。

stage2.pycファイルを16進エディタで開くと、バイトコードの最初のバイトを削除できるため、2番目のバイトから逆アセンブルを開始できます。これを実行するために、stage2.pycの0x6Dにあるバイトを削除し、その後、バイトコード長0x69を含む32ビット整数を0x232BCから0x232BBに変更します。ここでファイルを逆アセンブルすると、コードの先頭で以下を確認できます。

今度は、通常のバイトコードのように見えてきました。最初に、“ライセンス キー”である名前0x9100をロードし、その後、401バイト前方にジャンプしています。前方にジャンプした後に401バイトを削除して、再度逆アセンブルすると、さらに多くのものが得られます。

いい感じです。より多くの情報を取得しつつあります。さらに数回これを実行すると、いくつかの興味深い要素がわかり始めます。

すべてのJUMP_FORWARD命令とNOP命令を取り除くと、それが実行している内容がより容易にわかるようになります。

“license_key”という名前の変数と、値0の定数を使用して、BINARY_SUBSCRを実行しています。これにより、インデックスにある値を取得でき、その後、それを空白の文字列である名前37122に保存しています。これは、Pythonの以下の行と同等です。

= license_key[0]

その後、同じことを実行しますが、変数“license_key”を使用するのではなく、定数37122 (実際にはPNG)を使用して、インデックス542にある値を取得し、それを、同じく空白の異なる名前37123に保存します。その後、コード オブジェクトである定数37131をロードし、execを実行します。定数37131内のコードを見ると、かなりシンプルです。

それは、設定したばかりの2つの空白の名前付き変数をロードし、それらを比較して、比較結果を3番目の空白の名前付き変数に保存しています。これは、ライセンス キーが実際にチェックされる場所です。プログラムを作成し、このコード オブジェクトにprint文を挿入するだけで、実際に、そのキーを見つけることができます。

ここで、makepyを使ってパッチが適用されたstage2.pyasmファイルを作成し、再度verify_licenseを実行すると、正しいキーが出力されることがわかります。

> from stage2solve import *
>>> verify_license(‘A’ * 100)
1
_
W
4
n
n
A
_
b
3
_
T
h
3
_
v
E
R
y
_
b
3
S
T
!
False
>>>

ライセンス キーは“1_W4nnA_b3_Th3_vERy_b3ST!”のため、先に進み、それをプログラムに組み込むと、緑色に変わり、アクティブ化されたことを確認できます。

やりました! これで、“Generate APT (APTの生成)”ボタンを押して、“EVIL_MALWARE_CYBER_PATHOGEN.pyc”と呼ばれるファイルを作成できます。それを実行すると、画面全体にわたりASCIIアート フラグがスクロールされます。

そして、flag! PAN{l1Ke_n0_oN3_ev3r_Wa5}!があります。

これで終了です。私たちがこれらの課題の作成を楽しんだように、皆さんがこれらの課題への参加を楽しんでいただけたら幸いです。他の脅威リサーチャーがこのコースの課題をどのように解決したかも確認してください。

ランダム1:

https://0xec.blogspot.de/2016/08/labyrenth-ctf-writeup-random-track.html
https://github.com/uafio/git/blob/master/scripts/labyREnth-2016/labyrenth-2016-random-1.txt

ランダム2:

https://0xec.blogspot.de/2016/08/labyrenth-ctf-writeup-random-track.html
https://github.com/uafio/git/blob/master/scripts/labyREnth-2016/labyrenth-2016-random-2.py

ランダム3:

https://0xec.blogspot.de/2016/08/labyrenth-ctf-writeup-random-track.html
https://github.com/uafio/git/blob/master/scripts/labyREnth-2016/labyrenth-2016-random-3.3.py

ランダム4:

https://0xec.blogspot.de/2016/08/labyrenth-ctf-writeup-random-track.html
https://github.com/uafio/git/blob/master/scripts/labyREnth-2016/labyrenth-2016-random-4.php

ランダム5:

https://0xec.blogspot.de/2016/08/labyrenth-ctf-writeup-random-track.html
https://github.com/uafio/git/blob/master/scripts/labyREnth-2016/labyrenth-2016-random-5.2.py