ドライバーへの没入:NeoQUEST-2019ジョブの例を使用したリバースの一般原則







すべてのプログラマと同様に、あなたはコードが大好きです。 あなたと彼は親友です。 しかし、遅かれ早かれ、あなたと一緒にコードがないような瞬間が来るでしょう。 はい、信じられませんが、あなたとあなたの間に大きなギャップがあります。あなたは外にいて、彼は奥深くにいます。 絶望から、あなたは、他の皆と同様に、反対側に行かなければなりません。 リバースエンジニアリングの側へ。



例としてNeoQUEST-2019のオンラインフェーズのタスクNo. 2を使用して、リバースドライバーWindowsの一般原則を分析します。 もちろん、例は非常に単純化されていますが、プロセスの本質はこれから変わりません-唯一の問題は、表示する必要があるコードの量です。 経験と運を備えて、始めましょう!

与えられた



伝説によると、トラフィックダンプと同じトラフィックを生成したバイナリファイルの2つのファイルが与えられました。 まず、Wiresharkを使用してダンプを確認します。









ダンプにはUDPパケットのストリームが含まれ、各パケットには6バイトのデータが含まれます。 このデータは、一見すると、ランダムなバイトセットです。トラフィックから何も取得することはできません。 したがって、私たちはすべてを解読する方法を教えてくれるバイナーに注目します。

IDAで開きます。









何らかのドライバーに直面しているようです。 WSKプレフィックスを持つ関数は、WindowsカーネルモードネットワークプログラミングインターフェイスであるWinsockカーネルを指します。 MSDNでは、WSKで使用される構造と機能の説明を見ることができます



便宜上、Windows Driver Kit 8(カーネルモード)-wdk8_km(またはそれ以降の)ライブラリをIDAにロードして、そこで定義されているタイプを使用できます。









注意、逆!



いつものように、エントリポイントから開始します。









順番に行きましょう。 まず、Wskが初期化され、ソケットが作成されてビンに入れられます。これらの関数については詳しく説明しません。有用な情報は含まれていません。



sub_140001608関数は、4つのグローバル変数を設定します。 InitVarsと呼びましょう。 それらの1つでは、アドレス0xFFFFF78000000320に値が書き込まれます。 このアドレスを少しググってみると、システムが起動した瞬間からシステムタイマーのティック数を記録していると仮定できます。 ここでは、変数にTickCountという名前を付けましょう。









EntryPointは、IRPパケット(I / O要求パケット)を処理するための機能をセットアップします。 それらの詳細については、MSDNをご覧ください。 すべてのタイプの要求に対して、スタック内の次のドライバーにパケットを渡すだけの関数が定義されています。









ただし、タイプIRP_MJ_READ(3)には、別の関数が定義されています。 IrpReadと呼びましょう。















その中に、CompletionRoutineがインストールされます。









CompletionRoutineは、未知の構造にIRPから受信したデータを取り込み、リストに追加します。 これまでのところ、パッケージの内容はわかりません-後でこの関数に戻ります。

EntryPointをさらに調べます。 IRPハンドラーを定義した後、sub_1400012F8関数が呼び出されます。 内部を見て、すぐにデバイス(IoCreateDevice)が作成されていることに注目してください。









関数AddDeviceを呼び出します。 タイプが正しい場合、デバイス名は「\\ Device \\ KeyboardClass0」であることがわかります。 したがって、ドライバーはキーボードと対話します。 キーボードのコンテキストでIRP_MJ_READについて調べてみると、KEYBOARD_INPUT_DATA構造がパケットで送信されていることがわかります。 CompletionRoutineに戻り、どのようなデータが渡されるのかを見てみましょう。









ここでのIDAは構造を適切に解析しませんが、オフセットと追加の呼び出しによって、ListEntry、KeyData(キーのスキャンコードはここに格納されます)、およびKeyFlagsで構成されることがわかります。

AddDeviceの後、関数sub_140001274がEntryPointで呼び出されます。 彼女は新しいストリームを作成します。









ThreadFuncで何が起こるか見てみましょう。









彼女はリストから値を取得して処理します。 すぐに関数sub_140001A18に注意してください。









WskSocketへのポインタと番号0x89E0FEA928230002とともに、処理されたデータをsub_140001A68関数の入力に渡します。 パラメーター番号をバイト単位で分析すると(0x89 = 137、0xE0 = 224、0xFE = 243、0xA9 = 169、0x2328 = 9000)、トラフィックダンプからまったく同じアドレスとポートを取得します:169.243.224.137:9000。 この関数が指定されたアドレスとポートにネットワークパケットを送信すると仮定するのは論理的です-詳細は考慮しません。

送信前にデータがどのように処理されるかを見てみましょう。



最初の2つの要素については、生成された値で同等の処理が実行されます。 ティック数は計算に使用されるため、擬似乱数の生成に直面していると想定できます。















番号を生成した後、以前にTickCountを呼び出した変数の値を上書きします。 数式の変数はInitVarsで設定されます。 この関数の呼び出しに戻ると、これらの変数の値がわかります。その結果、次の式が得られます。



(54773 + 7141 * prev_value)%259200



これは線形合同擬似乱数ジェネレータです。 TickCountを使用してInitVarsで初期化されます。 後続の各番号に対して、以前の番号が初期値として機能します(ジェネレーターは2バイト値を返し、後続の生成にも同じ値が使用されます)。









キーボードから送信される2つの値の乱数と同等になった後、メッセージの残りの2バイトを形成する関数が呼び出されます。 すでに暗号化されている2つのパラメーターと一定の値のxorを生成するだけです。 これは何らかの形でデータを復号化する可能性が低いため、メッセージの最後の2バイトには有用な情報が含まれていないため、それらを考慮することはできません。 しかし、暗号化されたデータをどうするか?

正確に暗号化されているものを詳しく見てみましょう。 KeyDataは、かなり広い範囲の値を取ることができるスキャンコードであり、推測は容易ではありません。 ただし、 KeyFlagsはビットフィールドです。









スキャンコードのを見ると、ほとんどの場合、フラグが0(キーが押されている)または1(キーが押されている)になっていることがわかります。 KEY_E0が公開されることはほとんどありませんが、遭遇する可能性はありますが、KEY_E1に会う可能性は非常に小さいです。 したがって、次のことを試みることができます。ダンプからデータを調べ、暗号化されたKeyFlagsの値を選択し、0と同等の値を作成し、連続する2つのPSCを生成します。 まず、KeyDataは単一バイトであり、生成されたMSSの正確性を上位バイトで確認できます。 次に、次の暗号化されたKeyFlagsは、正しいPSCで同等のものを実行すると、同じビット値を取ります。 これがそうでないことが判明した場合、元々見ていたKeyFlagsが1などであったことを受け入れます。

アルゴリズムを実装してみましょう。 これにはpythonを使用します。



アルゴリズムの実装
#  -   keymap = […] # ,   Wireshark traffic_dump = […] #  def bxnor(a, b): return ((~a & 0xffff) | b) & (a | (~b & 0xffff)) #   def brgen(a): return ((7141 * a + 54773) % 259200) & 0xffff def decode(): #     for i in range(0, len(traffic_dump) - 1): #   KeyFlags probe = traffic_dump[i][1] #   - scancode = traffic_dump[i+1][0] #    KeyFlags tester = traffic_dump[i+1][1] fail = True #     (  KEY_E1) for flag in range(4): rnd_flag = bxnor(flag, probe) rnd_sc = brgen(rnd_flag) next_flag = bxnor(tester, brgen(rnd_sc)) #   KeyFlags if next_flag in range(4): sc = bxnor(rnd_sc, scancode) if sc < len(keymap): sym = keymap[sc] if next_flag % 2 == 0: print(sym, end='') fail = False break #   -      KeyFlags   if fail: print('Something went wrong on {} pair'.format(i)) return print() if __name__ == "__main__": decode()
      
      







ダンプから受け取ったデータに対してスクリプトを実行します。









そして、復号化されたトラフィックでは、最も望ましいラインが見つかります!



NQ2019DABE17518674F97DBA393415E9727982FC52C202549E6C1740BC0933C694B3DE









すぐに残りのタスクの分析を含む記事があります、お見逃しなく!



PSそして、NeoQUEST-2019で少なくとも1つのタスクを完了したすべての人に賞品が与えられることを思い出させてください! メールで手紙を確認し、届いていない場合はsupport@neoquest.ruにメールしてください



All Articles