Unit 42

LabyREnthセキュリティ コンテスト: Windows種目1~6の解答

Clock Icon 5 min read

概要

今回このシリーズでは、LabyREnth、Unit 42セキュリティ コンテストの課題の解答を明らかにします。週に1種目ずつ解答を公開しますが、今回は、Windows種目の課題1~6です。課題7~9については、次週公開します。

Windows 課題1: こいつらは悪い虫だ

課題作成者: Richard Wartell @wartortell

PEファイルが1つ与えられているためDetectItEasyで開いてみると、このファイルがUPXで圧縮されたものであることが分かります。

そこで、upx –dで解凍してみますが、そう簡単にはいきません。

どうやらこの課題を解く以前に、このファイルを自力で解凍する必要がありそうです。UPXは、はじめにpushad命令を使ってレジスタをスタックに積んでおくことで、解凍後にレジスタを検索して元々の入口点にジャンプできるようになります。スタックに積まれているレジスタの位置にハードウェア読み出しブレークポイントを設定するようIDAのデバッガーに指示をしておけばOEP(元々の入口点)に近づけます。

ブレークポイントに達したら、このブレークポイントを取り除き、元々の入口点に到達するテール ジャンプまで実行します。

popa命令の後にテールジャンプが現れる

解凍された状態のコードにジャンプした後、新たに発見したOEPを補助情報としてScyllaを使えばプロセスのダンプを取ることができます。

解凍済みの実行形式ファイルをBinary Ninjaで開いてみると、good boyメッセージを表示する経路と、bad boyメッセージを表示する経路があることが分かります。この分岐の直前で呼ばれる関数があります。この関数はキーをチェックしてどちらの経路を取るか決定します。

Good Boy経路とBad Boy経路を示すmain

check_keyにリネームされた関数を調べてみると、この関数がスタックまで複数バイト移動して入力が16バイト長かどうかチェックしていることが分かります。

その後プログラムは、一連のデバッギング対策チェックを始めます。これらのチェックがトリガーとなった場合、これらのチェックは関数に0 (FALSE)を返させます。各チェックに先立ち、私たちの文字列に対して文字列エンコーディング操作も行われます。

最初のデバッギング対策チェックは、プロセスがデバッグされている最中か否かを調べるCheckRemoteDebuggerPresentの呼び出しです。

2番目のデバッギング対策チェックは、OLLYDBGという名前のウィンドウがないかチェックするFindWindowWの呼び出しです。ここでOLLYDBGとはアナリストがよく使うデバッガーのことです。

3番目のデバッギング対策チェックは、プロセスがデバッグされている最中か否かを調べるIsDebuggerPresentの呼び出しです。

最後となる4番目のデバッギング対策チェックは、タイミング チェックとしてアセンブリ命令のrdtscを2回使うことです。その目的は、プロセスの実行に時間がかかっていることからプロセスがデバッグされている可能性が大か否かをチェックすることにあります。

これらのデバッギング対策チェックにすべて合格すると、最後の文字列操作に辿り着きます。この文字列操作は、複数バイトからなる初期バッファにおけるオフセットに対して、すべての操作の結果をチェックします。それらが等しくない場合、この関数は0 (FALSE)を返します。しかしそれらが等しければ結果が可算され、これが最後の操作におけるXOR演算キーとして使われます。

初期バッファをコピーし、操作をPythonで書き直すと解答の手がかりを得ることができます。

スクリプトを実行して解答の手がかりを得ます。

python solve.py

PAN{C0nf1agul4ti0ns_0n_4_J08_W3LL_D0N3!}

Windows 課題2: ヤギは何と言っているのかな

課題作成者: Richard Wartell @wartortell

これをdnSpyで開いて逆コンパイルし、デバッグすることができます。key_click関数が興味深いものに見えますが、それはキーが何らかの順序で押されたかどうか、状態を追跡しているからです。

キーには番号が振られています。白いキーに対して開始値を0として左から右に番号が振られ、黒いキーに対しても同様に番号が振られています。キーが正しい順序で押されるとdo_a_thing()が呼び出されます。

この関数により、面白いDavid Bowieの動画が再生されますが、同時に背景で解答の手がかりがアスキー アートでスクロールします。

PAN{B4BY_Y3LL5_5O_LOUD!}

Windows 課題3: 自分のゼニガメは幸せにしておかなくっちゃ

課題作成者: Tyler Halfpop @0xtyh

ポケモンのゼニガメの課題に関して、実行形式ファイルが与えられています。このバイナリを実行すると、ゼニガメのなかなかのアスキー アートとパスワード チェックが現れます。パスワードを間違えると、悲しいことにゼニガメはあっけなく死んでしまい、それでプログラムは終わってしまいます。

正しくないパスワードで死んだゼニガメ

このバイナリをBinary Ninjaで開き、main関数を調べてみましょう。最初の分岐命令を調べると、パスワード チェックをしている401070の直前に関数呼び出しがあります。文字列“incorrect”との単なる文字列比較だということが分かります。

パスワード チェック関数

パスワードを正しく入力すると、うれしいことにゼニガメを死なせずに済んだことが分かり、さらに出力を得ます。デバッギング対策チェックと仮想マシン対策チェックを通過する必要があります。通過すると今度は答がanswer.jpgファイルに書かれていると言われます。

正しいパスワードの出力

プログラム実行後に書かれたanswer.jpgファイルが存在していますが、壊れています。そのため、どのようにしたらプログラムがディスクにこのファイルを正しく書き込むようにできるか検討する必要があります。main関数の最後の方を見ると、マルチバイトのXORキーを使っているループがあります。

answer.jpgファイルを書いているXORループ

各ステップを通過すれば、正しいイメージを出力する正しいキーが得られる、と仮定することができます。各段階においてチェックがあり、さらにその後にコードを難読化するための偽物のrand() == rand()チェックがありますが、これにはなんともおかしなメッセージが伴っています。有難いことに、私たちが行き詰まったり、正しい経路について確信が持てない場合でも、各段階で役に立つヒントもあります。

偽物のrandチェックを伴うSleep/GetTickCountチェック

最初のチェックは、一般的なデバッガー ウィンドウ クラスが見つかるか否か判断することです。

2番目のチェックは、オフセットfs: [30h+2]におけるProcess Environment Blockを調べてプロセスがデバッグされている最中か否かを判断することです。

3番目のチェックは、Windows APIのGetTickCount()を使って、システムが新たに起動されたものでないことを確認することです。

4番目のチェックは、GetTickCount()とともにSleepを使い、スリープ呼び出しを迂回しようとするものです。

5番目のチェックは、プロセスがデバッグされている最中か否かを調べるため、Windows APIのIsDebuggerPresentを使うだけです。6番目のチェックは、プロセスがデバッグされている最中か否かを調べるため、同様にWindows APIのCheckRemoteDebuggerPresentを呼び出します。

7番目の段階では、CPUが2個よりも多いか否かをチェックしています。

8番目の段階では、RAMが1024 GBよりも多いか否かをチェックしています。

最後のチェックは、CPUハイパーバイザ ビットが設定されているか否かを調べるものです。

バイナリ内のプログラムを一歩ずつ進めて行くと、正しい経路が取られていることが確かめられます。そうして正しいイメージが得られます。

正しい経路を辿った跡の図

各段階で解答の正しい手がかりを得、XOR演算処理されたイメージのバッファをつかまえてPythonで復号化することもできるでしょう。

正しいイメージ

最終的に、バイナリをデコードして解答の手がかりを得ることができます(済みません(笑))。

PAN{Th3_$quirtL3_$qu@d_w@z_bLuffiNg}

Windows 課題4: 99本のビールが壁に、99本のビールだよ。1本取ってみんなに回せば、壁にはビールが98本だ。さあて、ビールのピッチャー回さにゃならん、順番に

課題作成者: Jacob Soo @_jsoo_

この課題に関して、参加者の皆さんには有効なシリアル番号を要求してくるx64バイナリが与えられました。

シリアル番号が間違っていると下記のイメージを目にするはずです。

これは昔ながらのcrackmeプログラムのようです。ユーザー入力をチェックしている関数を見つけてみましょう。見つけるためには、怪しげな文字列が使えそうか常に目を光らせていなければなりません。

Hopperを使うと、次のイメージが示すとおり下記の複数の文字列が見つかります。

そこで“strings”の1つを選んで、これがどこで参照されていたのかを調べましょう。

さあ、GetDlgItemTextまたはGetDlgItemTextWというAPIコールが1つでも見つけることができるか調べてみましょう。さて、どうやら140001464に1つあるようです。

デバッガーをステップ実行して処理の流れに任せてみると、1400014b3で文字列長チェックに遭遇します。これで入力文字列は長さが32文字でなければならないことが分かりました。

再び一歩ずつ進めてみると、140001500において、入力文字が1、2または3でなければならないことを確かめるチェックに遭遇します。この課題に関するシリアル番号が1、2、3でしか構成されていないことがこれで確認されました。

仮に140001750においてアプリケーションを分析したならば、[0, 13, 7]という初期の容量を持つ配列が存在していることが分かるでしょう。しかし“ピッチャー”の最大容量は[19, 13, 7]であり、期待する最終状態は配列が[10, 10, 0]になることです。要するに、大きさが7、13、19である3つの“ピッチャー”があり、大きさが7と13の“ピッチャー”はどちらも満杯ですが、大きさが19の容器は空である、ということです。必要なのは2つの容器(大きさが13と19)において10になることです。

疑似コードでこれを書き直してみましょう。3つの“ピッチャー”の状態が配列Mで表され、3つのピッチャーがa, b, cであると仮定しましょう。

最初のうちは、ここで何が起きているのか、実際のところ非常に分かりにくく見えるかもしれません。しかしよく見てみましょう。

これはピッチャーの限界値です。

さて、シリアル番号において、仮に31 (10数値の31ではなく数字"3"と数字"1"の並びと見る)から始めた場合、要するにこれはピッチャーC(3番目)の中身をピッチャーA(1番目)が満杯になるよう移し替えることを意味しています。
そのように、これの目的は“ビール”を移し回してピッチャーA == 10かつピッチャーB == 10にすることです。

これが有名な“液体を注ぐパズル”であることに気付かれたことでしょう。このパズルのことを私たちの大部分とは言わないまでも、一部の人たちは学校に通っている頃聞いたことがあるでしょう。

こうした発見に基づいてツールを書くことができます。しかしペンと紙を使って行うこともできるでしょう。下記のようなものになるはずです。

0-13-7
7-13-0
19-1-0
12-1-7
12-8-0
5-8-7
5-13-2
18-0-2
18-2-0
11-2-7
11-9-0
4-9-7
4-13-3
17-0-3
17-3-0
10-3-7
10-10-0

これをシリアル コードに書き直してみると、こうなります。31211332133221321332133221321332

これをシリアル番号として使うと、問題が解けてこのようなメッセージが返ってきます。

PAN{C0ngr47ulaT1onsbuddyy0Uv3solvedthere4l_prObL3M}

Windows 課題5: お気に入りの10進数コードを選びなさい

課題作成者: Jacob Soo @_jsoo_

RGB.exeを実行すると、すぐに3つのスライダーが現れますが、おそらくRBGカラーに対応しているものと思われます。それらの値を設定すると、色を確認することができます。このことから、解答の手がかりを手に入れるのに、3つの正しい値を考える必要があることが伺えます。

正しくない値

Exeinfoを使ってPEファイルをざっと調べてみると、これが.NETプログラムであり、de4dotを使えば解凍できることが分かります。

バイナリ タイプを調べる

実行形式ファイルに対してde4dotを実行すると新たに“RGB-cleaned.exe”というファイルが作成されます。そこで、dnSpyを使えばこのファイルを逆コンパイルし、元となっているソース コードを調べることができます。

バイナリの難読化を解除する

ソース コードを調べてみると、解く必要のある課題に到達します。

アルゴリズム

要するに、表示したいMessageBoxを得るのに3条件が合致しなければなりません。この時点で、私はコードをちょっと変更して答えを常に表示するようにできるか、つつき回ることから始めました。しかし呼び出されている関数の中に入り込みはじめると、行われていることがもう少しあり、実際の数字が必要だということが分かります。

数字の配列に対するXOR演算処理の数字

そのため、かわりに私は数学的な側面に取り組むことにしました。

合致する必要のある3条件は、1つの方程式の結果が別の方程式の結果と等しく、かつ特定の値のうちの1つが60よりも大きくなっている必要があるということになります。数字の可能な組み合せすべてにわたって繰り返すことで、総当たり攻撃を仕掛けることにします。というのも、各スライダーは1~255の範囲であり、そのうち1つは60~255の範囲になると分かっているからです。このことから255*255*(255-60)、つまりおおよそ1,250万の可能性が考えられます。これならば少しも長くかからないはずです。

ロジックを数分間考えた後、私は値を見つけるため下記のスクリプトを使います。

数秒以内で答えを得ます。この答えがRGB.exe内で有効となり解答の手がかりを得ることができます。

83 168 203

PAN{l4byr1n7h_s4yz_x0r1s_4m4z1ng}

Windows 課題6: shellC0DEの中にあるキーを見つけて王女様を救い出せ!

課題作成者: Josh Grunzweig @jgrunzweig

このWindows実行形式ファイルを開くと、何らかのシェルコードを扱っていることがすぐに分かります。このことが明らかでなかったとしても、与えられた手がかりが、対処中のものに関連するヒントをいくつか与えてくれるでしょう。

Discover the key in the sh>E11C0DE to rescue the Princess! (sh>E11C0DEの中にあるキーを見つけて王女様を救い出せ!)
@jgrunzweig

この問題を解こうとする人たちの便宜をはかるため、単にraw形式のシェルコード バイトを提供するのではなく、事前にシェルコードをコンパイルして実用的な実行形式ファイルにしておきました。開いてみるとimportが1つもなく関数が7つしかないことが直ちに分かります。

図1 シェルコード内の関数およびimportテーブル

このシェルコードをデバッグしなくても、与えられた7つの関数をざっと眺めれば何が突出しているか分かります。案の定、RC4であることがほぼ確実な関数を0x40106Cにおいて簡単に特定することができます。256回繰り返している2つのループが大きなヒントになります。この関数について検討してみると、これが実際にRC4であると確認することができます。

図2 RC4関数

fs:0x30をロードすることで始まる非常に小さな関数も特定されますが、このfs:0x30にはリバース エンジニアリングをする人ならば真っ先に関心を寄せるはずです。ご存じない方のために補足しますと、fs:0x30はProcess Environment Block (PEB)を指し示しており、PEBには豊富な情報が含まれています。したがって問題のこの関数はPEBのLoaderDataオフセット(プロセスにロード済みのモジュールに関する情報を保持している)にとりわけ注目します。次に私たちは3番目のロード済みモジュール(kernel32.dll)を得て、このDLLのベース アドレス(オフセット0x10)を捕まえます。この関数は、kernel32.dllのベース アドレスを実質上とらえている状態にあります。ここでkernel32.dllは、他の関数をロードするのに使われるのはほぼ間違いありません。

図3 kernel32のベース アドレスを取得する関数

データをハッシュ化しているように見受けられる別の関数の特定作業を続けます。これはROR13呼び出しで明らかだからです。

図4 ハッシュ化を行っている可能性のある関数

ここでデバッガー内のシェルコードの中をくまなく一歩ずつ動き始めましょう。kernel32のベース アドレスを取得した私たちの関数に対する複数の呼び出しをざっと調べます。私たちの関数の後には引数としてこのベース アドレスとDWORDをとる別の関数が続きます。この関数を詳しく調べると、この関数がkernel32のエクスポートされた関数すべてを渡り歩いて名前をハッシュ化し、与えられたDWORDと比較していることが分かります。これはシェルコードで使われる単純なトリックです。これを使うと、シェルコードが静的な状況で観察されている場合、どんな関数をマルウェアがロードしているところなのか、という点を攻撃者は分かりにくくすることができます。これに取り組むのに方法がいくつかあります。コードをデバッグし、遭遇するたびにリネームすることができます。あるいは、単純にGoogleでハッシュを検索することもできます。ROR13の手法はありふれたものなので、こうしたハッシュについて文書化してあるオンライン サイトが数多く存在します。例えばこのようなものがあります。

こうした小さな障害物を越えると、コードが何をしようとしているのか見えはじめ、コードが何を探しているのか理解することができます。コードを詳しく見てみると、54バイトから成るバッファを構築し、RC4を使って生成されたキーによりこのバッファをデコードしようとしていることが分かります。この状況ではキーが‘PAN{‘で始まっていて、メッセージ ボックス ダイアログのウィンドウの中にキーが表示されます。

キーはたくさんの変数を使って生成されています。これらの変数はスクリプトが動いているマシンから取り出したものです。初めの4バイトは変化しない値の‘b00!’です。これに続いて、コードは以下のデータを探します。

  • 現在の月+0x2D
  • 現在の日+0x5E
  • 現在の時間+0x42
  • オペレーティング システムのメジャー バージョン+0x3C
  • オペレーティング システムのマイナー バージョン+0x3F
  • isDebuggedフラグ(PEBから取得)+0x69
  • 言語バージョン+0x5E

これらの値を一緒にすると、長さが11バイトのキーになります。この情報だけでは総当たり攻撃を行うのは非常に難しいでしょう。しかし、キーの各バイトがどのように生成されているのか分かっているため、総当たり攻撃用にキー空間を限定することができます。そしてうまく行けばマルウェアが探しているものを突き止めることができます。

1年に月は12しかないことが分かっているので、生成された第1バイトは、1から12までの範囲にあると仮定することができます。同様に、1か月は最大で31日なので第2バイトは1から31までの範囲になります。こうしたパターンをRC4キーの残りのバイトについて続けて行きます。大多数の人にとって、オペレーティング システムのバージョンと言語バージョンについてキー空間を限定するのが一番難しく思われそうです。有難いことにオペレーティング システム(OS)の正式バージョンは総計しても極わずかしかありません。OSのメジャー バージョンは5、6、または10のいずれかの値になります。OSのマイナー バージョンは0、1、2、または3のいずれかの値になります。

言語バージョンに関しては、この実行フローのはじめの方にチェックがあります。そこではGetUserDefaultUILanguageの結果が第1言語識別子であり0x0すなわちLANG_NEUTRALであることが確認されています。こうしたことが分かったので、可能性が値0x0、0x04、0x08、0x0c、0x10または0x14に絞り込まれます。

これまでの情報をすべて使うと、下記のような総当たり攻撃スクリプトを作ることができます。

スクリプトをおよそ3分間実行すると、次のような結果が示されます。

PAN{th0se_puPP3ts_creeped_m3_out_and_I_h4d_NIGHTMARES}

Enlarged Image