脅威リサーチ

ネットワークプロトコルのファジングに関する取り組み:Microsoft IMAPクライアントプロトコルの分析

投稿者 Wayne Chin Yick Low | 2022年10月7日

ネットワークにおけるプロトコルとは、コンピュータから送信されたRAWデータを解釈するための標準的な形式やプロセスを定義する一連のルールです。ネットワークプロトコルは、コンピュータの共通言語と言えます。ネットワーク内のコンピュータではさまざまなソフトウェアやハードウェアが使用されていますが、それぞれの間で交信ができるのはプロトコルがあるからです。

インターネット上にはさまざまなネットワークプロトコルがあり、それぞれが持つ役割もさまざまで、中には複雑で高度なものもあります。本質的に複雑であるがゆえ、ネットワークアプリケーションのセキュリティにおける脆弱性は必然と言えます。ネットワークアプリケーションのセキュリティホールは、他の攻撃ベクトルに比べ、セキュリティに与える影響が大きい傾向があります。攻撃者はこの脆弱性を利用して、該当のコンピュータにおいてユーザーからの干渉を受けることなくリモートコードの実行ステータスを取得できてしまうからです。こうした攻撃はすでに実際に起こっています。悪名高いWannaCryランサムウェアもその一例です。EternalBlueと呼ばれるSimple Message Block(SMB)プロトコルを利用して、ビットコイン暗号通貨においてデータを暗号化したり、身代金支払いを要求したりしてMicrosoft Windowsコンピュータを攻撃します。現在までに、このマルウェアに感染したことのあるコンピュータは、150ヵ国で20万台以上に上ると推定されています。

ネットワークアプリケーションの強化は、WannaCryなどの攻撃ベクトルを最小限に抑えるためのミッションクリティカルなタスクとなります。脅威リサーチャーは、ネットワークプロトコルをファジングするツールなどを活用して、最も利用されているネットワークアプリケーションを適切に数多く保護できるよう取り組んでいます。この脆弱性を見つける手法では、テスト対象のアプリケーションに不正なパケットを送信して、ネットワークプロトコル実装の脆弱性を突き止めていきます。特に一般的に利用されているアプリケーションでこうした脆弱性を発見して報告することが、サイバーリスクの低減につながります。

フォーティネットのリサーチャーも、他の脅威研究コミュニティに加わって、この目標の達成をサポートしています。今回のブログでは、Microsoft Internet Message Access Protocol(IMAP)クライアントプロトコルの監査およびファジングプロセスについて解説します。新たな脆弱性は発見されませんでしたが、このステップバイステップガイドは、ファジング手法の戦略を脅威検知ツールや分析ツールに加えたり改善したりしようとするユーザーにとって有益となることでしょう。

IMAPクライアントプロトコルの目的

ネットワークアプリケーションでは、クライアントやサーバーアーキテクチャを使用してデータのやり取りが行われます。しかし、クライアントとサーバー間のデータ解釈は、同じネットワークプロトコル仕様を共有していても異なります。データ解釈は、個々の仕様に対応する各コンポーネントに実装されたパーサーによって実行されるのが一般的です。そのため、リサーチャーはクライアントとサーバーの両方を検査して、パーサーが正しく実装されていることを確認する必要があります。

こうしたことから、リサーチャーはセキュリティ実装を監査する際、クライアントよりもサーバーに注視する傾向があります。しかし、クライアントもセキュリティが手薄になると、簡単に悪用されてしまう可能性があります。とは言え、IMAPクライアントのセキュリティ問題に関してベンダーから公式に報告されている様子もあまりありません。そこで、IMAPクライアント実装を調査し、同時にオープンソースのファザー「What The Fuzz(WTF)」を実体験してみることにしました。WTFは、カスタマイズ可能で、クロスプラットフォームのスナップショットをベースとした分散型のコードカバレッジ付きファザーです。Microsoft Windowsで実行されているユーザー / カーネルモードターゲットを攻撃することができます。

今回の記事について:Microsoft IMAPクライアントにおいてセキュリティの問題は発見されませんでしたので、本投稿では脆弱性に関する公開情報はありません。しかし、今回遭遇した奇妙な体験や制約をはじめ、WTFファザーモジュールのデバッグに関するヒントやコツを紹介していきます。また、IMAP応答入力の逆プロセスについても紹介します。特に、当初脆弱と思われていたものが、コードを徹底的に調査したところ問題がなかったことが判明したことは、興味深い内容と言えるでしょう。

基本的なIMAPプロトコルを理解する

IMAPクライアントでは、さまざまなIMAPの操作に対するコマンドを幅広くサポートしています。クライアントコマンドは、操作を開始させて、サーバーからの応答を待ちます。各クライアントコマンドの先頭には、「tag」と呼ばれる識別子が付いています。この「タグ」は、クライアントから送信されるすべてのコマンドに対して一意である必要があります。一般的なクライアントコマンドは次のとおりです。

<tag> <command> <arg1 arg2 …>

クライアントにおいて重要なことは、仕様のとおり正確に構文に従わなければならないことです。スペースや引数が欠落したコマンドや、余分なスペースや引数があるコマンドが送信されると構文エラーになります。

IMAP接続は、クライアント / サーバーネットワーク接続の確立、サーバーからの最初のサーバーグリーティング、クライアント / サーバーインタラクションで構成されます。クライアント / サーバーインタラクションは、クライアントコマンド、サーバーデータ、サーバーの完了結果応答で構成されます。

クライアントやサーバーによって行われるインタラクションは、すべてキャリッジリターンや改行で終わる文字列の形式になります。

クライアントはまず最初に、特定のポート上のリモートサーバーに接続する必要があります。今回は、OpenSSLを使用して、端末からカスタムIMAPサーバーに接続します。

$ openssl s_client -connect 192.168.0.2:993 –crlf
... server verifications removed for brevity...
* OK IMAP4rev1 Service Ready

サーバーステータス応答には、「*」がプレフィックスとして付加されます(タグなし応答)。サーバーグリーティングやサーバーのステータスを示すもので、コマンドの完了(差し迫ったシステムシャットダウンアラートなど)を示すものではありません。

クライアントが次に送信する一般的なコマンドはCAPABILITYです。CAPABILITYコマンドを使用すると、クライアントは、サーバーがサポートする機能のリストを取得できるようになります。タグなし応答にリストされていない機能は、タグ付き応答で表示されているサーバーによって不正なコマンドとして扱われます。

tag1 CAPABILITY
* CAPABILITY IMAP4rev1 LITERAL+ SASL-IR CHILDREN UNSELECT MOVE IDLE NAMESPACE CONDSTORE COMPRESS=DEFLATE X-GM-EXT-1 METADATA ID APPENDLIMIT AUTH=PLAIN
tag1 OK CAPABILITY completed

ステータス応答はタグ付きの場合もあればタグなしの場合もあります。タグ付きのステータス応答では、クライアントコマンドの完了結果(OK、NO、BAD(不正))がステータスとして表示され、コマンドと一致するプレフィックスタグが付けられます。

メールボックスにアクセスする場合、クライアントは、予めIMAPサーバーにて認証を受ける必要があります。IMAPサーバー実装によっては、匿名で特定のメールボックスにアクセスできるものもあります。次の例にあるカスタムIMAPサーバーでは匿名でのアクセスが可能です。ただし、認証されたクライアント向けの機能リストは、認証されていないクライアント向けと異なるのがポイントです。ログインクライアントの方が、IMAPサーバーの機能においてロック解除されるものが多いのが一般的です。

tag2 LOGIN username password
tag2 OK LOGIN completed

クライアントのログイン後、メールボックスのフォルダが一覧表示

tag3:LIST
* LIST (HasNoChildren) "/" INBOX
tag3 OK LIST completed

これで、メールボックスのフォルダ階層を確認することができます。フォルダには名前属性があり、括弧内に示されます。HasNoChildrenやHasChildrenなど、一部の属性においては、フォルダ階層を移動するときに便利です。属性がHasNoChildrenとなっている場合、メールボックスには、現在認証されているクライアントがアクセスできる派生メールボックスがないことを示します。

メールボックスのフォルダ構造を確認した上で、クライアントはSELECTコマンドを使用してフォルダ上のセッションを開きます。こうして、メールボックス内のメッセージにアクセスできるようになります。

tag4 SELECT "INBOX"
* FLAGS (Seen)
* OK [PERMANENTFLAGS (\*)] Flags permitted
* 1 EXISTS
* OK [UIDNEXT 7] Predicted next UID
* OK [UIDVALIDITY 1] UIDs valid
tag4 OK [READ-WRITE] SELECT completed

選択したステータスが返されると、サーバーは、上記のタグなしデータをクライアントに必ず送信してから、クライアントにOKを返します。選択したステータスが正常に確立されると、選択したステータスの状態であることがクライアントに伝えられ、メールボックスからメッセージを検索したりダウンロードしたりすることができるようになります。

tag5 SEARCH ALL
* SEARCH 1
tag5 OK SEARCH completed

SEARCHコマンドでは、指定された検索条件に一致するメッセージをメールボックスで検索します。検索条件は、複数の検索キーで構成されます。これにより、特定のフィールド名のヘッダーを有するメッセージや、ヘッダーのテキストに特定の文字列を含むメッセージを検索するなど、検索条件をより包括的にすることができるようになります。上記は、最も単純な形式のSEARCHコマンド例となり、サーバーで使用可能なメッセージをすべて検索できるようになっています。タグなし応答では、カスタムIMAPサーバーで使用可能なメッセージが1つあることを示しています。

クライアントによっては、メールのメッセージがプレビュー表示される場合があります。これは、FETCHコマンドを実行すると、メッセージのヘッダーのみをダウンロードすることができます。UID FETCHコマンドを実行すると、メールメッセージ全体がダウンロードされ、クライアントアプリケーションにローカル保存することができます。

tag6 UID FETCH 6 (RFC822.HEADER BODY.PEEK[1])
* 1 FETCH (RFC822.HEADER {194}
From: contact@example.org
To: contact@example.org
Subject: A little message, just for you
Date: Wed, 22 Mar 2022 14:31:59 +0000
Message-ID: <0000000@localhost/>
Content-Type: text/plain


 BODY[1] {11}
Hi there :) UID 6)
tag6 OK UID FETCH completed

図1:IMAPクライアントサーバーの状態とフロー図。引用元:IETF Internet Message Access Protocol(IMAP)バージョン4rev2

IMAPクライアントのファザー開発への道のり

現在のファジングにおいて、主要なファザープログラムと併せてファジングの作業を行うにはハーネスが必要です。WTFのファザーの場合、ハーネスは必須ではありませんが、専用のファザーモジュールが必要になります。AFL/WinAFLの経験がある方にとっては、効率的なハーネスプログラムの作成に多くの時間を費やすことになるところですが、WTFファザーモジュールの開発やトラブルシューティングにほとんどの時間が費やされることになるでしょう。内部では、WTFのファザーがエミュレーターとなり、ファザーモジュールが実行するメモリダンプ内のコードをプログラムでエミュレートします。基本的に、ファザーモジュールのコアは、関数ブレークポイントとブレークポイントハンドラーで構成されます。このブレークポイントハンドラーは、ターゲットによって使用される入力データの傍受や変更をはじめ、I/O操作、レジストリ操作、スレッドスケジューリングなどの機能の複製など、さまざまな目的に使用されるロジックで構成されています。プロジェクトのリポジトリには、ファザー開発プロセスに関する包括的なガイドラインが用意されています。

まず始めに、ファザーモジュールで使用される仮想イメージのスナップショットをファズしたりダンプしたりするターゲットコンポーネントを指定する必要があります。プロジェクトリポジトリのドキュメンテーションによれば、このスナップショットイメージは、パーサールーチンで入力データが使用される際における対象のターゲットモジュールのエントリポイントから取得されるのが一般的です。Microsoft IMAPクライアントの場合、InternetMail.dllがIMAPとPOP3の両方のクライアントプロトコルを実装するターゲットコンポーネントとなります。このDLLモジュールは、サービスのWindowsホストプロセス(svchost.exeとも呼ばれます)によってホストされます。

Windows Mailは、このモジュールと相互作用するフロントエンドユーザーインタフェース(UI)となります。このUIにより、ユーザーはIMAPアカウントを設定したり、メールサーバーからメールメッセージをダウンロードしたりできるようになります。IMAPクライアントのファザーモジュールを書き込んでいるときに、障害が多数発生しました。幸いなことに、その一部がプロジェクトの課題追跡システムに記録されています。障害の大部分は、どの作業においてもターゲット固有のものになりますが、これらの問題や対策を記録しておくことは有益であると考えました。

IMAPクライアントのファザーモジュール開発準備

準備は成功の鍵です。特に新たなファザーを使用する場合はこれが当てはまります。WTFのファザー向けにファザーモジュールを書き込むのは大変な作業です。メモリダンプからコードをエミュレートすることになるからです。ソフトウェアエミュレーションの場合、エミュレートされたコードがネイティブデバイスの実行コードと同じように機能するかどうかはわかりません。そのため、エミュレーションしたものが意図したとおりに機能できるようになるまで、障害をいくつも解決していかなければなりません。したがって、始める前にファザーモジュールをトレースしたりデバッグしたりできるよう適切なツールを準備しておくことが重要です。

WTFのファザーは、カバレッジのトレースログとTenetのトレースファイルの2種類のトレースファイルをサポートしています。基本的に、カバレッジのトレースログには、エミュレーターによって実行される各命令のトレースが含まれています。これにより、ファザーモジュールの問題がほとんど分析できるようになります。Tenetのトレースファイルには、実行された各命令と、各命令によって処理されたメモリおよびスタックデータが含まれています。Tenetのプラグインでできることは、Tenetのトレースファイルを消費することだけです。Tenetは、優れたトレース / 記録 / 再生のIDA Proプラグインで、オフラインデバッグにも便利です。WTFのファザーによって生成されたTenetのトレースファイルは、IDA Proで再生することができます。これにより、ユーザーは実行コードを調べたり、メモリやスタックでのデータの読み書きを分析したりできるようになるので、ファザーモジュールのデバッグやトラブルシューティングが一段と楽になります。

ただし、記録されたトレースファイルが大きすぎる場合、プラグインでの処理に非常に時間がかかることに注意してください。たとえば、トレースファイルのサイズが数ギガバイトある場合、ホストメモリがすぐにいっぱいになってしまい、IDA Proでトレースの再生ができなくなる可能性があります。対策としては、「--trace-starting-address」コマンドラインパラメータをWTFのファザーに導入します。すると、ファザーが特定のアドレスに達したときだけ、トレースを開始するようになります。この新たに導入されたコマンドラインパラメータにより、トレースファイルのサイズが大幅に縮小されました。ただし、場合によっては、このフィルタリングメカニズムが結果的にあまり上手くいかない場合もあります。対象となる関数の開始アドレスが一意ではないため、取得するトレースが依然として大きくなってしまう場合があります。たとえば、関数が確定的でない場所で、さらにそのような場所が複数となってトリガーされると、予期せずトレース機能が起動することがあり、目的を達成できなくなります。.

図2:Tenet トレースの単純なフィルタリング

いくつかの実験後、WinDbg Previewにおいてタイムトラベルデバッグ(TTD)機能を確認することができました。これで、オフラインデバッグと同じ目的を達成することができます。WinDbg Previewでは実行中のプロセスがアタッチされ、TTD独自のDLLトレーサーがターゲットプロセスに挿入されます。挿入されたDLLトレーサーは、ターゲットプロセスのランタイム実行をキャプチャし、物理ディスクに格納されたトレースファイルに実行コードを保持する役割を担っています。このプロセスをシミュレーションするため、簡易的なIMAPサーバーを作成して、IMAP接続が確立されたときにJSON形式で定義されたIMAPパケットを読み取り、接続クライアントのWindows Mailにパケットを送信できるようにしました。同時に、WinDbg PreviewがサービスのWindowsホストプロセスにアタッチされ、コード実行を記録することができるようになります。ただし、このアプローチは手動となり、一度に1つの実行トレースしか生成できません。それでも、TTDはオフラインデバッグの機能を補完するのに有用な機能となります。

図3:別のアプローチで実行可能なターゲットのコード実行トレースを生成

他のユースケースでは、差分デバッグの技術が活用されています。TTDとTenetで生成されたトレースを比較して、数多くのファザーモジュールの問題を詳細にトラブルシューティングする技術です。それでもなお、ファザーモジュールの開発中に発生した複雑な問題をデバッグするとなると、トレースファイル生成においてはTenetが最初の選択肢になります。

本ブログの後半では、Tenetのトレースファイルではなくカバレッジのトレースログを使用して、もっと明らかな問題を直接特定するヒントとコツを紹介します。これにより、ファザーモジュールの開発にかかる時間を節約できるようになります。

IMAPクライアントのファザーモジュール開発

WTFのファザーモジュールは、WTFフレームワークの上層で動作します。各ファザーモジュールは、コールバック関数を実装する必要があります。この関数は、WTFフレームワークで登録され、後にWTFの実行ファイルによりトリガーされるものです。プロジェクトのリポジトリにはステップバイステップガイドラインもありますので、ここでは開発の詳細を説明しません。

さらに、IMAPには、メールボックスの作成、削除、名前の変更、新しいメッセージの確認、メッセージの完全削除、フラグの設定や削除、メッセージ属性やテキストの選択取得などの操作があります。しかし、IMAPプロトコルにおいて包括的なミューテーション戦略を実装させようとすると、時間がなくなる可能性があります。ですので、今回は、Windows MailとIMAPサーバーとのやり取りで使用されるIMAPコマンドのみに注目します。まず、WinDbg Previewデバッガをターゲットプロセスにアタッチして、実際にIMAPサーバー(Gmail)とやり取りをするWindows Mailの実行トレースを生成し、IMAPトランザクションで一般的なコマンドを収集しました。リスト1では、デバッガの出力を示しています。Windows MailクライアントがGmailサーバーに送信するIMAPコマンドで構成されています。

0:000> bp  InternetMail!ImapCommunicationManager::_IssueCommandRaw+0x2b ".printf \"_SendText: %ma\\n\", rdx;gc"

breakpoint 0 redefined

0:000> g

ModLoad: 00007ff9`8fcc0000 00007ff9`8fd9b000   c:\windows\system32\efswrt.dll

ModLoad: 00007ff9`a22a0000 00007ff9`a22e5000   C:\Windows\SYSTEM32\feclient.dll

ModLoad: 00000241`a5a30000 00000241`a5a34000   C:\Windows\System32\UserDataAccessRes.dll

ModLoad: 00007ff9`c9380000 00007ff9`c9401000   C:\Windows\System32\fwpuclnt.dll

_SendText: A2 NOOP

_SendText: A3 CAPABILITY

_SendText: A4 ID ("vendor" "Microsoft" "os" "Windows Mobile" "os-version" "10.0" "guid" "45363445433632373438364639394335353946323134423434313739434334344239413838323431")

_SendText: A5 LOGIN "censoredusername" "censoredpassword"

_SendText: A6 NAMESPACE

_SendText: A7 LIST "" %

_SendText: A8 LIST "[Gmail]/" *

_SendText: A9 SELECT "INBOX" (CONDSTORE)

_SendText: A10 SEARCH UNDELETED SINCE 28-Jun-2022

_SendText: A11 FETCH 89:90 (INTERNALDATE UID FLAGS X-GM-THRID RFC822.SIZE BODY.PEEK[HEADER.FIELDS (DATE FROM SUBJECT CONTENT-TYPE X-MS-TNEF-Correlator CONTENT-CLASS IMPORTANCE PRIORITY X-PRIORITY THREAD-TOPIC REPLY-TO)] BODYSTRUCTURE)

ModLoad: 00007ff9`cbef0000 00007ff9`cc09e000   C:\Windows\system32\windowscodecs.dll

_SendText: A12 UID FETCH 219 (RFC822.HEADER BODY.PEEK[1.2] BODY.PEEK[2])

_SendText: A13 UID FETCH 220 (RFC822.HEADER BODY.PEEK[2])

_SendText: A14 SELECT "INBOX" (CONDSTORE)

_SendText: A15 SEARCH UNDELETED SINCE 1-Apr-2022

_SendText: A16 FETCH 58:90 (INTERNALDATE UID FLAGS X-GM-THRID RFC822.SIZE BODY.PEEK[HEADER.FIELDS (DATE FROM SUBJECT CONTENT-TYPE X-MS-TNEF-Correlator CONTENT-CLASS IMPORTANCE PRIORITY X-PRIORITY THREAD-TOPIC REPLY-TO)] BODYSTRUCTURE)

_SendText: A17 SELECT "INBOX" (CONDSTORE)

リスト1:Windows Mailクライアントが送信したIMAPコマンドのデバッガ出力

ミューテーションアプローチでは、NAMESPACE、LIST、SELECT、SEARCH、FETCHのコマンドに対するIMAP応答に注目します。UID FETCHコマンドのファジングはスキップすることにしました。この応答ハンドラーには、ローカルファイルシステム内のメッセージデータベースの読み込みや書き込みが含まれるからです。残念ながら今回のケースでは、WTFがデフォルトでI/Oサブシステムのエミュレーションフレームワークを提供している場合でも、この操作を実装することができないのは明らかです。メッセージヘッダーパーサーなどの重要な解析操作のほとんどがFETCHコマンドで実行されるので、この方がうまくバランスが取れると判断しました。

IMAPパケットを構成する一連の構造化テキストメッセージの定義についてはこちらをご覧ください。そのため、IMAPパケットのミューテーション戦略も構造に対応している必要があります。構造に対応したミューテーションライブラリ「libprotobu-buutator」をヒントに、JSONファイル形式を使用して、ミューテーションされたIMAP応答をそれぞれ格納しました。このJSONファイルは、ファザーモジュールへの入力テストケースとして機能します。仕様によると、JSONオブジェクトのクリティカルコンポーネントはResponseParamsとなり、IMAPクライアントが解釈するコアデータで構成されます。とは言え、ミューテーターとしてはResponseParams、ResponseStatusResponseTypeからのミューテーションデータに注目します。

{

  "Packets": [

    {

      "Command": "NAMESPACE",

      "ResponseParams": [

        [

          "NIL NIL ((\"\" \".\"))"

        ]

      ],

      "ResponseStatus": "OK",

      "ResponseTag": "A6",

      "ResponseType": 42

    },

    {

      "Command": "LIST",

      "ResponseParams": [

        [

          "() \"/\" \"INBOX\""

        ]

      ],

      "ResponseStatus": "OK",

      "ResponseTag": "A7",

      "ResponseType": 42

    },

    {

      "Command": "LIST",

      "ResponseParams": [

        [

          "(\\All \\HasNoChildren) \"/\" \"INBOX/All Mail\""

        ]

      ],

      "ResponseStatus": "OK",

      "ResponseTag": "A8",

      "ResponseType": 42

    },

    {

      "Command": "SELECT",

      "ResponseParams": [

        [

          "* FLAGS (\\Seen)",

          "* OK [PERMANENTFLAGS (\\*)] Flags permitted",

          "* 1 EXISTS",

          "* 0 RECENT",

          "* OK [UIDNEXT 7] Predicted next UID",

          "* OK [UIDVALIDITY 1] UIDs valid"

        ]

      ],

      "ResponseStatus": "OK [READ-WRITE]",

      "ResponseTag": "A9",

      "ResponseType": 42

    },

    {

      "Command": "SEARCH",

      "ResponseParams": [

        [

          "SEARCH 1"

        ]

      ],

      "ResponseStatus": "OK",

      "ResponseTag": "A10",

      "ResponseType": 42

    },

    {

      "Command": "FETCH",

      "ResponseParams": [

        [

          "38 FETCH",

          "UID 6",

          "INTERNALDATE \"31-Mar-2022 24:59:59 +0800\"",

          "FLAGS (\\Seen ks6JUz2l )",

          "RFC822.SIZE 433313086",

          "X-GM-THRID ",

          "BODYSTRUCTURE (\"report\" \"alternative\" (\"attachment\" \"(\"filename\" \"winmail.dat\")\" \"charset\" \"(\"filename\" \"winmail.dat\")\" \"\" \"\" \"charset\" \"us-ascii\" \"charset\" \"utf-8\") i.DS. I3j7]iQxo:hxfSl \"BASE64\" 26 )",

          "BODY[HEADER.FIELDS (DATE FROM SUBJECT CONTENT-TYPE X-MS-TNEF-Correlator CONTENT-CLASS IMPORTANCE PRIORITY X-PRIORITY THREAD-TOPIC REPLY-TO)] {671}\r\nContent-Duration: 5725206\r\nContent-Type: \u0002\r\nDate: Wed, 22\n Feb 2022 10:08:--62094 \r\nFrom: sample <sample@sample.com>\r\nMIME-Version: 97.56\r\nMessage-Context: voide 󠀪󠁕\r\nReceived: from DB5EUR03FT058.eop-EUR03.prod.protection.outlook.com (2603:10a6:6:2d:cafe::b0)\r\nby DB6PR07CA0011.outlook.office365.com(2603:10a6:6:2d::21)\r\nwith Microsoft SMTP Server(version = TLS1_2,cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384)\r\nid 15.20.4352.8\r\nvia Frontend Transport; Wed, 14 Jul 2021 05:46:31 +0000\r\nReply-To: example/example\r\n<reply+ABFSGURWRY340282366920938463463374607431768211458OIKSMTFLHORW\r\nReturn-Path: sampleʱ\r\nSubject: Example message\r\nX-MS-TNEF-Correlator: \r\n"

        ]

      ],

      "ResponseStatus": "OK",

      "ResponseTag": "A11",

      "ResponseType": 42

    }

  ]

}

リスト2:IMAP応答入力テストケースの例

課題1:Extensible Storage Engineキャッシュのクリーンアップ

Windows Mailでは、統合型のストアデータベースを使用して、メールアドレスやメッセージなどのメールデータをローカルファイルシステムに保管します。このデータベースは、%LOCALAPPDATA%\Comms\UnistoreDB\store.volのパスにあります。ESENT(Extensible Storage Engine)は、独自のバイナリ形式を使用したデータベースを構築して、データを構造化します。このバイナリ形式は、ESEDatabaseViewなどのツールで確認することができます。ESENTを使用するメリットは、データへの高パフォーマンスアクセスを最大化するキャッシングメカニズムがあることです。そしてこのキャッシュメカニズムこそが、最初の障害をもたらすのです。

キャッシュバッファには、ESENTパラメータ「JET_paramVerPageSize」に基づいてサイズが内部的に割り当てられます。このパラメータは、システムサービスによりUserDataServiceが起動すると初期化されます。デフォルトのキャッシュサイズは0x2000で、ページサイズの粒度に合わせて調整する必要があります。しかし、これがWTFのファザーモジュールのコンテキストで問題となります。

問題は、キャッシュバッファがいっぱいになったとき、ESENTが、キャッシュバッファを消去するためにワークアイテムをキューに入れてしまうことです。ワークアイテムは、スレッドプールにプログラム送信されるサブルーチンです。ワークアイテムが非同期的に実行されるので、スケジューラーシステムがシステムリソースの可用性に基づいて警告を発してしまうのです。残念ながら、メカニズムが複雑なため、WTFのファザーでもエミュレートすることができません。結果的に、スレッドAPI(KERNELBASE!QueueUserWorkItemなど)に達すると、コンテキストスイッチ中にファザーモジュールが終了することになります。コンテキストスイッチ後もファズするのはCPU時間の無駄になります。図4のように、あらゆるWTFのファザーモジュールで同様のブレークポイントハンドラーを見つける必要があるのは、こうした背景があるからです。

図4:コンテキストスイッチ中にファザーモジュールを停止するブレークポイントハンドラー

コンテキストスイッチが突然発生した場合、作成者はその原因を把握して、必要なコードパスに到達できる対策を実装する必要があります。そのためには、WTFのファザーにより生成されたカバレッジのトレースログを分析し、0vercl0kのSymbolizerで後処理を行う必要があります。図5は、コンテキストスイッチ中に停止するカバレッジのトレースログの例です。

図5:Symbolizerを介して生成されたカバレッジのトレースログ例

カバレッジのトレースログを分析できる高度な手法はありません。バックトラッキングをひたすら実行して、モジュールや関数の遷移(Moda!funcnameX -> moddB!funcnameYなど)を特定し、コンテキストスイッチの原因を突き止めます。モジュールファイルをIDA Proにロードして、基礎となるコードを統計的に調査したり把握したりするのが一般的です。ただし、特にIDA Proで自動的に解決できない仮想関数呼び出しがコードに含まれている場合などは、静的コード分析を十分に実行できないことがあります。そのような場合、TTDを使用すると仮想関数呼び出しを解決したり、実行コードを調べたりすることができるようになります。

図6:カバレッジのトレースログでコンテキストスイッチの原因を確認

図6では、ESENT!CGPTaskManager::ErrTMPost+0xd4KERNELBASE!QueueUserWorkItemを呼び出し、実行可能スレッドは基本的にスレッドプールキューに配置され、ESENT!CGPTaskManager::ErrTMPostESENT!VER::VERSignalCleanupから派生していることが示されています。関数の詳細な分析の後、TTDを活用して、現在のバッファキャッシュサイズとJET_paramVerPageSizeで指定されているデフォルトのキャッシュサイズを比較するESENT!VER::VERSignalCleanupの意義を究明することにしました。現在のキャッシュバッファがいっぱいになったとき、この関数が、QueueUserWorkItemを呼び出して、キャッシュクリーンアップスレッド「ESENT!VER::VERIRCECleanProc」を実行します。その結果、コンテキストスイッチが発生します。そこで、クリーンアップ手順がトリガーされないようにする方法を見つけることが課題となります。

最初に考えたのは、最も簡単な対策として、デフォルトのキャッシュサイズを0x2000から最大サイズの0x10000に増やすことでした。MSDNのドキュメンテーションによると、データベースエンジンの構成設定は、APIのJetSetSystemParameterで調整することが技術的に可能です。しかし、外部プログラムを使用して分離されたシステムサービスのプロセススペースにある設定を変更してしまうと、今回の目的は達成することができません。

# RetAddr            : Call Site

00 00007ff9`d9773c38  : ESENT!SetConfiguration+0xce

01 00007ff9`d9773adf  : ESENT!ErrSetSystemParameter+0x10c

02 00007ff9`d9772fe1  : ESENT!JetSetSystemParameterEx+0x203

03 00007ff9`d9772e83  : ESENT!JetSetSystemParameterExA+0x65

04 00007ff9`cc099d77  : ESENT!JetSetSystemParameterA+0x53

05 00007ff9`cc099e12  : UserDataPlatformHelperUtil!SetJetSystemParameter+0x2f

06 00007ff9`e833627a  : UserDataPlatformHelperUtil!SetParametersInitOnce+0x32

07 00007ff9`e5d709b1  : ntdll!RtlRunOnceExecuteOnce+0x9a

08 00007ff9`cc09a199  : KERNELBASE!InitOnceExecuteOnce+0x21

09 00007ff9`8cb98280  : UserDataPlatformHelperUtil!SetCommsServiceJetGlobalSystemParameters+0x29  0a 00007ff9`8cb909d6  : unistore!DBManager::_InitializeJetInstance+0x134

0b 00007ff9`8cb908bd  : unistore!DBManager::InitializeDeviceVolumeImpl+0x32

0c 00007ff9`8cb786e2  : unistore!DBManager::InitializeDeviceVolume+0xd9

0d 00007ff9`8cc02743  : unistore!DBManager::GetInstance+0x8c

0e 00007ff9`8cbb49a0  : unistore!ObjectCollection::Init+0x7b

0f 00007ff9`8cbb8c5e  : unistore!CFactory::CreateObjectCollection+0x80

10 00007ff9`8cbb7a57  : unistore!CStoreManager::GetStoreCollection+0x9e

11 00007ff9`8cbb6faf  : unistore!CStoreManager::FinalConstruct+0xd7

12 00007ff9`8cbb7050  : unistore!ATL::CComObject<CStoreManager>::CreateInstance+0x5f

13 00007ff9`8cb86cca  : unistore!CStoreManager::CreateInstance+0x50

14 00007ff9`8cb86d6b  : unistore!_CreateInprocStoreManager+0x12e

15 00007ff9`8cb87373  : unistore!_CreateStoreManager+0x77

16 00007ff9`8cb8730f  : unistore!CreateStoreManagerWithToken+0x23

17 00007ff9`a9f1050e  : unistore!CreateStoreManager+0xaf

18 00007ff9`a9f117e0  : CEMAPI!GetStoreManagerCache+0x12

19 00007ff9`a9f0e407  : CEMAPI!MapiCtx::_GetSingletonInstance+0x6c

1a 00007ff9`a9f0e5fa  : CEMAPI!LogonMapi+0x97

1b 00007ff9`8c6fa92c  : CEMAPI!MAPILogonEx+0x5a

1c 00007ff9`8c6f5738  : MessagingDataModel2!CSmStore::FinalConstruct+0x64

1d 00007ff9`8c6f3794  : MessagingDataModel2!ATL::CComCreator<ATL::CComObject<CSmStore> >::CreateInstance+0xc0

1e 00007ff9`e6e7aa6d  : MessagingDataModel2!ATL::CComClassFactory::CreateInstance+0x64

1f 00007ff9`e6e8baef  : combase!CServerContextActivator::CreateInstance+0x1fd [onecore\com\combase\objact\actvator.cxx @ 874]

20 00007ff9`e6e79c4c  : combase!ActivationPropertiesIn::DelegateCreateInstance+0x8f [onecore\com\combase\actprops\actprops.cxx @ 1960]

21 00007ff9`e6ee5b28  : combase!CApartmentActivator::CreateInstance+0xcc [onecore\com\combase\objact\actvator.cxx @ 2178]

22 00007ff9`e6eecaac  : combase!CProcessActivator::CCICallback+0x68 [onecore\com\combase\objact\actvator.cxx @ 1617]

23 00007ff9`e6ee1bfe  : combase!CProcessActivator::AttemptActivation+0x4c [onecore\com\combase\objact\actvator.cxx @ 1504]

24 00007ff9`e6e7a844  : combase!CProcessActivator::ActivateByContext+0x9e [onecore\com\combase\objact\actvator.cxx @ 1348]

25 00007ff9`e6e8baef  : combase!CProcessActivator::CreateInstance+0x94 [onecore\com\combase\objact\actvator.cxx @ 1248]

26 00007ff9`e6e7b604  : combase!ActivationPropertiesIn::DelegateCreateInstance+0x8f [onecore\com\combase\actprops\actprops.cxx @ 1960]

27 00007ff9`e6e8baef  : combase!CClientContextActivator::CreateInstance+0x124 [onecore\com\combase\objact\actvator.cxx @ 558]

28 00007ff9`e6e8eb83  : combase!ActivationPropertiesIn::DelegateCreateInstance+0x8f [onecore\com\combase\actprops\actprops.cxx @ 1960]

29 00007ff9`e6e8e125  : combase!ICoCreateInstanceEx+0x8a3 [onecore\com\combase\objact\objact.cxx @ 1931]

2a 00007ff9`e6e8df5c  : combase!CComActivator::DoCreateInstance+0x175 [onecore\com\combase\objact\immact.hxx @ 388]

2b (Inline Function)  : combase!CoCreateInstanceEx+0xd1 [onecore\com\combase\objact\actapi.cxx @ 320]

2c 00007ff9`8c76c607  : combase!CoCreateInstance+0x10c [onecore\com\combase\objact\actapi.cxx @ 264]

2d 00007ff9`8c76c2e0  : MessagingDataModel2!MessagingNotificationFactory::CreateNotificationManager+0x53

2e 00007ff9`8c86be4a  : MessagingDataModel2!Messaging_StartNotification+0x30

2f 00007ff9`8c922a02  : userdataservice!DataModelService::OnStarted+0x9a

30 00007ff9`8c9223c9  : userdataservice!ServiceBase::_ServiceMainInner+0x126

31 00007ff6`8e23296e  : userdataservice!ServiceBase::ServiceMain+0x71

32 00007ff9`e71b0752  : svchost!ServiceStarter+0x5fe

33 00007ff9`e75054e0  : sechost!ScSvcctrlThreadW+0x32

34 00007ff9`e832485b  : KERNEL32!BaseThreadInitThunk+0x10

35 00000000`00000000  : ntdll!RtlUserThreadStart+0x2b

リスト3:システムホストのサービスセットとなるデータベースエンジンの設定を示すコールスタック

次に、リスト3のコールスタックを見て、データベースエンジンの設定が行われる前にUserDataServiceをハイジャックし、ESENT.dllの特定のオフセット値でハードコードされたデフォルトのキャッシュサイズを調整するかたちでこの問題に対処することを考えました。そして、実際に試してみることにしました。

サービスDLLのハイジャックは簡単です。ターゲットサービスのレジストリエントリの検索は、下記で定義されているとおりです。

HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\UserDataSvc\Parameters
ServiceDLL= %SystemRoot%\System32\userdataservice.dll

ServiceDLLエントリをカスタムのサービスDLLファイルに適応させると、次のようになります。

HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\UserDataSvc\Parameters
ServiceDLL=c:\userdatasvc\UserDataSvcProxy.dll

カスタムのサービスDLLは、ServiceMainSvchostPushServiceGlobalsの2つの必須モックアップ関数をエクスポートします。上記のレジストリエントリが調整されると、システムサービスによりカスタムのサービスDLLがロードされ、モックアップServiceMain関数が実行されます。モックアップServiceMain関数は、ESENT.dllの特定のオフセット値でjet_paramVerPageSizeをパッチ適用します。パッチ適用後、UserDataServiceによりエクスポートされた最初のServiceMain関数に実行が移り、通常どおりの初期ルーチンが続行されます。

カスタムのサービスDLLの完全なソースコードは、こちらをご覧ください。

# Child-SP          RetAddr               Call Site

00 000000a6`d807d3d8 00007ff9`c86209d6     unistore!DBManager::_InitializeJetInstance+0x5

01 000000a6`d807d3e0 00007ff9`c86208bd     unistore!DBManager::InitializeDeviceVolumeImpl+0x32

02 000000a6`d807d420 00007ff9`c86086e2     unistore!DBManager::InitializeDeviceVolume+0xd9

03 000000a6`d807d490 00007ff9`c8692743     unistore!DBManager::GetInstance+0x8c

04 000000a6`d807d4e0 00007ff9`c86449a0     unistore!ObjectCollection::Init+0x7b

05 000000a6`d807d710 00007ff9`c8648c5e     unistore!CFactory::CreateObjectCollection+0x80

06 000000a6`d807d770 00007ff9`c8647a57     unistore!CStoreManager::GetStoreCollection+0x9e

07 000000a6`d807d7e0 00007ff9`c8646faf     unistore!CStoreManager::FinalConstruct+0xd7

08 000000a6`d807d8e0 00007ff9`c8647050     unistore!ATL::CComObject<CStoreManager>::CreateInstance+0x5f

09 000000a6`d807d910 00007ff9`c8616cca     unistore!CStoreManager::CreateInstance+0x50

0a 000000a6`d807d950 00007ff9`c8616d6b     unistore!_CreateInprocStoreManager+0x12e

0b 000000a6`d807d990 00007ff9`c8617373     unistore!_CreateStoreManager+0x77

0c 000000a6`d807da20 00007ff9`c861730f     unistore!CreateStoreManagerWithToken+0x23

0d 000000a6`d807da70 00007ff9`c91a050e     unistore!CreateStoreManager+0xaf

0e 000000a6`d807dab0 00007ff9`c91a17e0     CEMAPI!GetStoreManagerCache+0x12

0f 000000a6`d807daf0 00007ff9`c919e407     CEMAPI!MapiCtx::_GetSingletonInstance+0x6c

10 000000a6`d807db30 00007ff9`c919e5fa     CEMAPI!LogonMapi+0x97

11 000000a6`d807db70 00007ff9`9b1aa92c     CEMAPI!MAPILogonEx+0x5a

12 000000a6`d807dbd0 00007ff9`9b1a5738     MessagingDataModel2!CSmStore::FinalConstruct+0x64

13 000000a6`d807dc20 00007ff9`9b1a3794     MessagingDataModel2!ATL::CComCreator<ATL::CComObject<CSmStore> >::CreateInstance+0xc0

14 000000a6`d807dc60 00007ff9`e4adaa6d     MessagingDataModel2!ATL::CComClassFactory::CreateInstance+0x64

15 000000a6`d807dc90 00007ff9`e4aebaef     combase!CServerContextActivator::CreateInstance+0x1fd [onecore\com\combase\objact\actvator.cxx @ 874]

16 000000a6`d807de10 00007ff9`e4ad9c4c     combase!ActivationPropertiesIn::DelegateCreateInstance+0x8f [onecore\com\combase\actprops\actprops.cxx @ 1960]

17 000000a6`d807dea0 00007ff9`e4b45b28     combase!CApartmentActivator::CreateInstance+0xcc [onecore\com\combase\objact\actvator.cxx @ 2178]

18 000000a6`d807df50 00007ff9`e4b4caac     combase!CProcessActivator::CCICallback+0x68 [onecore\com\combase\objact\actvator.cxx @ 1617]

19 000000a6`d807dfa0 00007ff9`e4b41bfe     combase!CProcessActivator::AttemptActivation+0x4c [onecore\com\combase\objact\actvator.cxx @ 1504]

1a 000000a6`d807dff0 00007ff9`e4ada844     combase!CProcessActivator::ActivateByContext+0x9e [onecore\com\combase\objact\actvator.cxx @ 1348]

1b 000000a6`d807e080 00007ff9`e4aebaef     combase!CProcessActivator::CreateInstance+0x94 [onecore\com\combase\objact\actvator.cxx @ 1248]

1c 000000a6`d807e0d0 00007ff9`e4adb604     combase!ActivationPropertiesIn::DelegateCreateInstance+0x8f [onecore\com\combase\actprops\actprops.cxx @ 1960]

1d 000000a6`d807e160 00007ff9`e4aebaef     combase!CClientContextActivator::CreateInstance+0x124 [onecore\com\combase\objact\actvator.cxx @ 558]

1e 000000a6`d807e200 00007ff9`e4aeeb83     combase!ActivationPropertiesIn::DelegateCreateInstance+0x8f [onecore\com\combase\actprops\actprops.cxx @ 1960]

1f 000000a6`d807e290 00007ff9`e4aee125     combase!ICoCreateInstanceEx+0x8a3 [onecore\com\combase\objact\objact.cxx @ 1931]

20 000000a6`d807ef30 00007ff9`e4aedf5c     combase!CComActivator::DoCreateInstance+0x175 [onecore\com\combase\objact\immact.hxx @ 388]

21 (Inline Function) --------`--------     combase!CoCreateInstanceEx+0xd1 [onecore\com\combase\objact\actapi.cxx @ 320]

22 000000a6`d807f070 00007ff9`9b21c607     combase!CoCreateInstance+0x10c [onecore\com\combase\objact\actapi.cxx @ 264]

23 000000a6`d807f110 00007ff9`9b21c2e0     MessagingDataModel2!MessagingNotificationFactory::CreateNotificationManager+0x53

24 000000a6`d807f190 00007ff9`a458be4a     MessagingDataModel2!Messaging_StartNotification+0x30

25 000000a6`d807f1d0 00007ff9`a4642a02     UserDataService!DataModelService::OnStarted+0x9a

26 000000a6`d807f2f0 00007ff9`a46423c9     UserDataService!ServiceBase::_ServiceMainInner+0x126

27 000000a6`d807f320 00007ff9`9b340ca7     UserDataService!ServiceBase::ServiceMain+0x71

28 000000a6`d807f350 00007ff9`9b333666     userdatasvcproxy!SvchostPushServiceGlobals+0xcaf2

29 000000a6`d807f358 000000a6`d807f3e8     userdatasvcproxy!ServiceMain

リスト4:カスタムのサービスDLLがUserDataServiceをハイジャックしていることを示すコールスタック

あらゆる設定を終えた後、カスタムサービスDLLをロードしてキャッシュサイズを0x10000に調整し、新しいスナップショットイメージに対してファザーモジュールを実行しました。しかし残念ながら、それでもクリーンアップ手順に到達してしまいました。そのため、別の対策を見つけなければならなくなりました。

ESENT!VER::VERSignalCleanupを調べましたが、関数が値を呼び出し元関数に返さないことはわかっています。この認識から、関数のルーチンはクリーンアップ手順が正常に実行されたかどうかは関係ないものだと考えました。さらに、ESENTでは、予期しない動作を引き起こす可能性のあるグローバルな状態やイベントが追跡されていないと思われます。これを考慮して、ブレークポイントをシンプルに設定して、ブレークポイントに到達したときにすぐに呼び出し元に戻るように関数をシミュレーションするかたちでクリーンアップ手順をスキップすることにしました(図7参照)。

図7:ESENT!VER::VERSignalCleanupをスキップして、コンテキストスイッチを回避

これで完成です。ファザーモジュールは、クリーンアップ手順の先で実行されるので、コンテキストスイッチに到達することもありません。ただし、これにより、スナップショットイメージ内のメモリ使用量が大幅に増加する可能性があるので注意してください。とは言え、スナップショットイメージはファジングループが終わると元の状態に戻るので、潜在的な問題を引き起こすものではありません。つまり、ダングリング状態のキャッシュバッファは無視できる程度ということです。

課題2:アンロードされたDLLをロードし、ページアウトされたメモリを実行する

ソフトウェアのエミュレーションに馴染みのある方ならば、エミュレーターをネイティブマシンのように動作させることは不可能であることはご存じでしょう。同じことがWTFのファザーにも当てはまります。このような制約が生じたときの対策を見つけておく必要があります。ただし、対策が容易かどうかは制約状況の複雑さにもより、スナップショットイメージを微調整するだけの簡単な対策で済む場合もあります。

次の問題は、WTFが、アンロードされたDLLファイルをファイルシステムからロードしようとすると、コンテキストスイッチが発生することです。ここでも、カバレッジのトレースログと一部の抜粋コードを分析して、問題の根本原因を特定することができました(図8参照)。カバレッジのトレースログより、CoCreateInstance APIMCCSEngineShared!Decode2047Header+0xfeから呼び出されていることがわかります。このCOM APIは、クラスID(今回はCLSID_CMultiLanguage)で指定されたCOMオブジェクトをロードする役割を担っています。このクラスIDは、C:\WINDOWS\SYSTEM32\mlang.dllに対応します。

図8:アンロードされたDLLファイルのロードを分析

この情報を使用して、COMオブジェクトDLLを手入力でターゲットプロセスに挿入し、イメージを新しいスナップショットとしてダンプしてテストしました。すると、MCCSEngineShared!Decode2047Headerを凌ぐ結果が得られました。しかし同時に、別の問題にも直面しました。

図9:メモリアクセスエラーでさらに新たなコンテキストスイッチが発生

カバレッジのトレースログ(図9)を見ると、ユーザーモード「exsmime!CMimeReader::FindBoundary」がカーネルモード「nt!MiUserFault」に変換される異常なコード実行が発生していることが判明しました。経験上、エミュレーターが予約メモリアドレスに到達したか、メモリアドレスがページファイルにスワップアウトされた可能性があります。これは、パフォーマンス上の理由でページファイル内の使用頻度の低いメモリを保持する一般的なWindowsのメモリ管理メカニズムです。これを検証するため、WinDbgデバッガでメモリダンプをロードして、exsmime!CMimeReader::FindBoundary+0x4fで指定されたコードを調べました(図10参照)。

図10:仮想関数呼び出し時のメモリアクセスエラー

仮想関数は仮想関数テーブルから呼び出されるものですが、仮想関数「exsmime!CHdrContentType::value」の行き先はTTDスナップショットによって決まります(図11参照)。

図11:TTDで仮想関数の行き先アドレスが決まる

こうしたメモリアクセスの問題を回避するために、lockmemのユーティリティを実行しました。これにより、指定したプロセスにおいて使用可能なメモリ領域がすべてメモリに保持されるようになるため、ページファイルへの書き込みができなくなり、アクセスがあった場合はページ違反が発生することになります。最良の結果を得るには、常に完全なメモリダンプを実行して、予期しないメモリアクセスの問題を回避することをお勧めします。このヒントは、カーネルモードのコンポーネントをファジングする場合に特に有益です。

課題3:レジストリフック

Windowsレジストリは、Windowsオペレーティングシステムおよびアプリケーションの低レベル設定を格納する階層データベースです。このデータベースには、ファイルシステム内のレジストリハイブ情報が保持されています。つまり、レジストリ操作には一定のI/O操作が含まれます。これらの操作はいずれもエミュレーターではサポートされていないため、これらの操作機能を複製しておく必要があります。

WTFでは、書き込み時にI/O操作を複製するために、レジストリフック(以降「reghook」)ではなく、fshootkのサブシステムが提供されます。一見したところ、APIが異なるため、reghookとしてfshookを再使用することができないように思われますが、一部の実装においてはfshookからreghookへ適合させることが可能です。たとえば、fshoakクラスとRegHandleTable_tクラスでは疑似ハンドルのアルゴリズムを再使用できます。fshookとreghookの重要な違いは、対象とするコンテンツ(I/O操作のファイルコンテンツやレジストリ操作のレジストリデータなど)のエミュレーション方法です。たとえば、reghookでは、レジストリ操作により新しいハンドルが開放される場合、RegOpenKey APIが呼び出されて特定のレジストリキーのハンドルが開放されます。そして、該当のフックハンドラーが、APIコールをネイティブマシンにリダイレクトします。つまり、ネイティブデバイスは、ネイティブAPIでレジストリキーの開放を試行し、レジストリキーが存在する場合はハンドルを返すことになります。開放されたハンドルは、ネイティブマシンでは有効ですが、メモリダンプのゲストマシンでは有効ではありません。したがって、疑似ハンドルを生成して、ネイティブハンドルと結びつける必要があります。現行のreghook実装については、こちらをご覧ください。

繰り返しになりますが、現行のreghook実装は完全なものではありません。他のターゲットについては包括的なテストが行われていません。しかし、既存のreghookを拡張して他のレジストリAPIをサポートすることは、さほど複雑ではないはずです。

RFC822.SIZEの奇妙な事例

ファザーモジュールの配備や配布後、ファザーが収集した情報から興味深いものを集めてみました。そこからさらに分析するために、まずコードトレースを生成して、LighthouseプラグインでIDA Proにロードしました。

そして、ミューテーション入力を操作するコード(特にファザーがターゲットにフィードするResponseParams)を見つけるために、InternetMail.dllのリバースエンジニアリングにまず注目しました。すると、FETCH応答「RFC822.SIZE」に奇妙なResponseParamsがあり、すぐさま注意を向けました。資料によると、RFC822.SIZEは、メッセージのサイズを表すFETCHコマンドの属性の1つでした。簡単に言えば、クライアントに到着したメールメッセージ全体のサイズ(メールヘッダー、コンテンツ、添付ファイルなど)をクライアントに通知するものでした。

興味深いのは、この値に対するコードのサニタイズが非常にシンプルであることです(リスト5の抜粋コード参照)。単に、メッセージのサイズが4ギガバイト(10進数では4294967295、32ビット16進数では0xFFFFFFFF)ではないことを確認するだけだからです。4ギガバイトの場合、エラーが発生します。

else if (!_strnicmp("RFC822.SIZE", strItem, 0xBui64))

{

  stdstring_startItemData = this->stdstring_startItemData;

  this->rfc822_size = 0;

  v11 = strtoul(stdstring_startItemData, 0i64, 10); // **(1)**

  this->rfc822_size = v11;

  if (v11 == 0xFFFFFFFF) // **(2)**

  {

         v7 = 37;

         hr = INTSAFE_E_ARITHMETIC_OVERFLOW;

  LABEL_17:

         Log_HREvent(hr, 0, "onecoreuap\\base\\mailcontactscalendarsync\\engines\\internetmail\\imap\\fetch.cpp", v7);

  }

}

リスト5:RFC822.SIZEを取得してデータ構造体に保存する

(1)では、strtoulにより有効な変換が実行されなかった場合、ゼロ値が返されます。一方で、(2)のコードのサニタイズは意味があるとは思えません。4294967294(32ビット16進数の0xFFFFFFFE)などの値がコード内の算術演算で使用されと、チェックをバイパスして、算術オーバーフローを招いてしまう可能性があるからです。コードを調べた結果、この値を操作する関数を1つ見つけただけでした。当然のことながら、ここで確認したコードのサニタイズも同じものでした。

__int64 __fastcall HeaderParser::_PostNewMessageCreation(HeaderParser* this, struct IMessage* lppMessage)

{

       rfc822_size = this->rfc822_size;

       if ((_DWORD)rfc822_size != 0xFFFFFFFF)

       {

              v1 = (void*)*((_QWORD*)this->pImapSyncContext + 27); // **(A)**

              if (rfc822_size <= 0xA000)

              {

                     if ((unsigned int)rfc822_size <= 0x5000ui64)

                     {

                            if ((unsigned int)rfc822_size <= 0x2800ui64)

                           {

                                  offset = 0x48i64;

                                  if ((unsigned int)rfc822_size > 0x1400ui64)

                                         offset = 0x50i64;

                           }

                           else

                           {

                                  offset = 0x58i64;

                           }

                     }

                     else

                     {

                           offset = 0x60i64;

                     }

              }

              else

              {

                     offset = 0x68i64;

              }

              _InterlockedExchangeAdd64((volatile signed __int64*)((char*)v1 + offset), 1ui64); // **(B)**

              _InterlockedExchangeAdd64((volatile signed __int64*)v1 + 8, (unsigned int)rfc822_size);

       }      // **(C)**

リスト6:HeaderParser::_PostNewMessageCreationがRFC822.SIZEを操作

(A)のv1ポインターはpImapSyncContextから取得されます。これは、不明なポインターが同期状態を保持するデータ構造に関連する可能性があることを示します。コードをさらに詳しく見ると、(B)と(C)に算術演算が2つあることが確認できます。(B)の場合、RFC822.SIZEの値に応じて増分操作が行われ、増分値の結果がv1ポインターに保存されます。一方、RFC822.SIZEの値は(C)に集約されます。これはもっと深く見てみる価値がありそうです。

そこで、複数のFETCH ResponseParamsと偽のRFC822.SIZEで構成されるIMAPパケットを用意し、TTDで実行コードをキャプチャしてみることにします。

{

    "Packets": [

        {

            "ResponseParams": [

                [

                    "NIL NIL ((\"\" \".\"))"

                ]

            ],

            "ResponseType": 42,

            "Command": "NAMESPACE",

            "ResponseStatus": "OK",

            "ResponseTag": "A6"

        },

        {

            "ResponseParams": [

                [

                    "() \"/\" \"INBOX\""

                ]

            ],

            "ResponseType": 42,

            "Command": "LIST",

            "ResponseStatus": "OK",

            "ResponseTag": "A7"

        },

        {

            "ResponseParams": [

                [

                    ""

                ]

            ],

            "ResponseType": 42,

            "Command": "LIST",

            "ResponseStatus": "OK",

            "ResponseTag": "A8"

        },

        {

            "ResponseParams": [

                [

                    "FLAGS (\\Seen)",

                    "OK [PERMANENTFLAGS (\\*)] Flags permitted",

                    "2 EXISTS",

                    "0 RECENT",

                    "OK [UIDNEXT 3] Predicted next UID",

                    "OK [UIDVALIDITY 1] UIDs valid"

                ]

            ],

            "ResponseType": 42,

            "Command": "SELECT",

            "ResponseStatus": "OK [READ-WRITE]",

            "ResponseTag": "A9"

        },

        {

            "ResponseParams": [

                [

                    "SEARCH 1 2"

                ]

            ],

            "ResponseType": 42,

            "Command": "SEARCH",

            "ResponseStatus": "OK",

            "ResponseTag": "A10"

        },

        {

            "ResponseParams": [

                [

                    "1 FETCH",

                    "UID 1",

                    "INTERNALDATE \"31-Mar-2022 24:59:59 +0800\"",

                    "FLAGS (\\Seen)",

                    "RFC822.SIZE 4294967294",

                    "X-GM-THRID NIL",

                    "BODYSTRUCTURE (\"TEXT\" \"PLAIN\" () NIL NIL NIL 11 1 NIL NIL NIL NIL)",

                    "BODY[HEADER.FIELDS (DATE FROM SUBJECT CONTENT-TYPE X-MS-TNEF-Correlator CONTENT-CLASS IMPORTANCE PRIORITY X-PRIORITY THREAD-TOPIC REPLY-TO)]{135}\r\nFrom: contact@example.org\r\nSubject: A little message, just for you\r\nDate: Wed, 22 Mar 2022 14:31:59 +0000\r\nContent-Type: text/plain\r\n\r\n"

                ],

                [

                    "2 FETCH",

                    "UID 2",

                    "INTERNALDATE \"31-Mar-2022 24:59:59 +0800\"",

                    "FLAGS (\\Seen)",

                    "RFC822.SIZE 4294967294",

                    "X-GM-THRID NIL",

                    "BODYSTRUCTURE (\"TEXT\" \"PLAIN\" () NIL NIL NIL 11 1 NIL NIL NIL NIL)",

                    "BODY[HEADER.FIELDS (DATE FROM SUBJECT CONTENT-TYPE X-MS-TNEF-Correlator CONTENT-CLASS IMPORTANCE PRIORITY X-PRIORITY THREAD-TOPIC REPLY-TO)]{135}\r\nFrom: contact@example.org\r\nSubject:  B little message, just for you\r\nDate: Wed, 22 Mar 2022 14:31:59 +0000\r\nContent-Type: text/plain\r\n\r\n"

                ]

            ],

            "ResponseType": 42,

            "Command": "FETCH",

            "ResponseStatus": "OK",

            "ResponseTag": "A11"

        }

    ]

}

リスト7:偽のRFC822.SIZEを使用する2つのFETCH応答パラメータを有するIMAPパケット

0:000> !tt 0
Setting position to the beginning of the trace
Setting position: 78:0
(1f58.9e0): Break instruction exception - code 80000003 (first/second chance not available)
Time Travel Position: 78:0
ntdll!NtClose+0x14:
00007fff`11a834b4 c3              ret

0:000> x internetmail!*PostNewMessageCreation*
00007fff`24253a64 InternetMail!HeaderParser::_PostNewMessageCreation (protected: long __cdecl HeaderParser::_PostNewMessageCreation
(struct IMessage *))
0:000> bp InternetMail!HeaderParser::_PostNewMessageCreation+0x84 "r r9; dc r10+30"
0:000> g
ModLoad: 00007fff`46c00000 00007fff`46c81000   C:\Windows\System32\fwpuclnt.dll
ModLoad: 00007fff`40e20000 00007fff`40e62000   C:\Windows\system32\mlang.dll
r9=00000000fffffffe
00000251`a1f590b8  00000000 00000000 00000000 00000000  ................
00000251`a1f590c8  00000000 00000000 00000000 00000000  ................
00000251`a1f590d8  00000000 00000000 00000000 00000000  ................
00000251`a1f590e8  00000000 00000000 00000001 00000000  ................
00000251`a1f590f8  00000000 00000000 00000001 00000000  ................
00000251`a1f59108  00000000 00000000 f672a68a c667e448  ..........r.H.g.
00000251`a1f59118  00000000 40200000 f673a6ba c667e449  ...... @..s.I.g.
00000251`a1f59128  00000000 00000000 00000000 00000000  ................
Time Travel Position: 106C6FF:0
InternetMail!HeaderParser::_PostNewMessageCreation+0x84:
00007fff`24253ae8 f04d0fc14a40    lock xadd qword ptr [r10+40h],r9 ds:00000251`a1f590c8=0000000000000000
0:012> g
r9=00000000fffffffe
00000251`a1f590b8  00000000 00000000 00000000 00000000  ................
00000251`a1f590c8  fffffffe 00000000 00000000 00000000  ................
00000251`a1f590d8  00000000 00000000 00000000 00000000  ................
00000251`a1f590e8  00000000 00000000 00000002 00000000  ................
00000251`a1f590f8  00000000 00000000 00000001 00000000  ................
00000251`a1f59108  00000000 00000000 f672a68a c667e448  ..........r.H.g.
00000251`a1f59118  00000000 40200000 f673a6ba c667e449  ...... @..s.I.g.
00000251`a1f59128  00000000 00000000 00000000 00000000  ................
Time Travel Position: 1346091:0
InternetMail!HeaderParser::_PostNewMessageCreation+0x84:
00007fff`24253ae8 f04d0fc14a40    lock xadd qword ptr [r10+40h],r9 ds:00000251`a1f590c8=00000000fffffffe
0:012> p
Time Travel Position: 1346091:1
InternetMail!HeaderParser::_PostNewMessageCreation+0x8a:
00007fff`24253aee 488364245000    and     qword ptr [rsp+50h],0 ss:000000d2`e71fe3b0=0000000000000000
0:012> dc r10+30
00000251`a1f590b8  00000000 00000000 00000000 00000000  ................
00000251`a1f590c8  fffffffc 00000001 00000000 00000000  ................
00000251`a1f590d8  00000000 00000000 00000000 00000000  ................
00000251`a1f590e8  00000000 00000000 00000002 00000000  ................
00000251`a1f590f8  00000000 00000000 00000001 00000000  ................
00000251`a1f59108  00000000 00000000 f672a68a c667e448  ..........r.H.g.
00000251`a1f59118  00000000 40200000 f673a6ba c667e449  ...... @..s.I.g.
00000251`a1f59128  00000000 00000000 00000000 00000000  ................

リスト8:デバッガ出力において、RFC822.SIZEの集約値がv1ポインターに表示

リスト8で強調表示されている箇所は、集約値がv1ポインターの隣接フィールドをオーバーフローしたことを示しています。ただし、上書きされたフィールドがセキュリティ上の問題を引き起こすかどうかは不明です。したがって、このRAWメモリのデータ構造フィールドを特定する必要があります。そこで、TTD.Utility.GetHeapAddressを使用して、開始のヒープアドレスと、ヒープアドレスが割り当てられ初期化される場所を明らかにします。

0:012> dx -g @$cursession.TTD.Utility.GetHeapAddress(0x00000251`a1f590b8)
===================================================================================================================================
=                             = Action   = Heap             = Address          = Size     = Flags  = (+) TimeStart = (+) TimeEnd  =
===================================================================================================================================
= [0x1151e] : [object Object] - Alloc    - 0x251a2120000    - 0x251a1f58f10    - 0x1b8    - 0x0    - D9B98:EC      - D9B9C:9C     =
= [0x29b79] : [object Object] - Alloc    - 0x251a2120000    - 0x251a1f58f10    - 0x1b8    - 0x0    - 216478:EC     - 21647C:9C    =
= [0x2adb1] : [object Object] - Alloc    - 0x251a2120000    - 0x251a1f58f10    - 0x1b8    - 0x0    - 220BC6:EC     - 220BCA:9C    =
= [0x2c419] : [object Object] - Alloc    - 0x251a2120000    - 0x251a1f58f10    - 0x1b8    - 0x0    - 22C5A7:EC     - 22C5AB:9C    =
= [0x2d88a] : [object Object] - Alloc    - 0x251a2120000    - 0x251a1f58f10    - 0x1b8    - 0x0    - 236CF9:EC     - 236CFD:9C    =
= [0x2ea3d] : [object Object] - Alloc    - 0x251a2120000    - 0x251a1f58f10    - 0x1b8    - 0x0    - 23FCCF:EC     - 23FCD3:9C    =
= [0x49a30] : [object Object] - Alloc    - 0x251a2120000    - 0x251a1f58f10    - 0x1b8    - 0x0    - 3985B0:EC     - 3985B4:9C    =
= [0x4aeec] : [object Object] - Alloc    - 0x251a2120000    - 0x251a1f58f10    - 0x1b8    - 0x0    - 3A33F8:EC     - 3A33FC:9C    =
= [0x4c02b] : [object Object] - Alloc    - 0x251a2120000    - 0x251a1f58f10    - 0x1b8    - 0x0    - 3AC168:EC     - 3AC16C:9C    =
= [0x76275] : [object Object] - Alloc    - 0x251a2120000    - 0x251a1f58f60    - 0x190    - 0x0    - 5C6A57:A9     - 5C6A5B:9C    =
= [0x7b09a] : [object Object] - Alloc    - 0x251a2120000    - 0x251a1f58f60    - 0x190    - 0x0    - 602818:81     - 60281C:9C    =
= [0x7b14d] : [object Object] - Alloc    - 0x251a2120000    - 0x251a1f58f60    - 0x190    - 0x0    - 602E2E:81     - 602E32:9C    =
= [0x7b1c3] : [object Object] - Alloc    - 0x251a2120000    - 0x251a1f58f60    - 0x1a0    - 0x0    - 6031FD:58     - 603201:9C    = 0:012> !tt 6031FD:58 
Setting position: 6031FD:58
ModLoad: 00007fff`40e20000 00007fff`40e62000   C:\Windows\system32\mlang.dll
(1e8c.360): Break instruction exception - code 80000003 (first/second chance not available)
Time Travel Position: 6031FD:58
ntdll!RtlAllocateHeap:
00007fff`515e89a0 48895c2408      mov     qword ptr [rsp+8],rbx ss:000000d2`e7dfee80=000000d200000001
0:005> kb
 # RetAddr               : Args to Child                                                           : Call Site
00 00007fff`5076c750     : 000000d2`00000001 00000000`00000000 000000d2`e7dff000 00000000`00000000 : ntdll!RtlAllocateHeap
01 00007fff`36bb1d97     : 00007fff`36c14678 000000d2`e7dff008 00000251`a1f8be30 00000251`a1f59dd8 : msvcrt!malloc+0x70
02 00007fff`36bb33da     : 00007fff`36c14678 00007fff`336b651c 00000000`00000000 00000251`a1f50001 : SYNCUTIL!operator new+0x23
03 00007fff`36bcb229     : 00000000`00000001 000000d2`e7dff088 00007876`9ee69ed8 00000000`00000001 : SYNCUTIL!operator new+0x12
04 00007fff`36bcb7c1     : 000000d2`00000001 00007fff`2420ecfc 00000251`a1f02f00 00000251`a1f02e10 : SYNCUTIL!SyncStatsHelpers::_LookupAccountSyncStats+0xdd
05 00007fff`2420976d     : 00000251`a1f02f00 00000000`00000000 00000000`00000000 00000251`a1f02f00 : SYNCUTIL!GetCurrentSyncStats+0x21
06 00007fff`243100d2     : 00000000`00000000 00000251`a1fa9950 000000d2`e7dff0a0 00000251`a1f59d50 : InternetMail!ImapIdleExecutor::Initialize+0x3d
07 00007fff`2430c5f1     : 00000000`00000000 00000251`a1f08980 00000000`00000000 00000251`a1f59d50 : SyncController!AccountSyncControllerAggregator::_Initialize+0x17e
08 00007fff`242eec39     : 00000251`a1f08988 00007fff`00000000 00000251`a1f08988 00000251`a1f08980 : SyncController!AccountSyncControllerAggregator::CreateInstance+0x24d
09 00007fff`242ea861     : 00000251`00000000 00007fff`2435dfd0 00000251`a2120000 00007fff`515e8d78 : SyncController!SyncBookkeeper::_AddSyncController+0x2dd
0a 00007fff`242c8fd4     : 000063c9`e8e3b474 00007fff`515e752d 00000251`a1f91b10 00007fff`51662d96 : SyncController!SyncBookkeeper::CreateSyncController+0xf1
0b 00007fff`242c7b4b     : 00000251`a1f6bc88 000000d2`e7dff390 00000251`a1f08900 000000d2`e7dff498 : SyncController!SyncActivityFactory::_CreateActivity+0x34
0c 00007fff`36c508ae     : 00007fff`36c76410 00000251`a1f73b90 00000000`00000000 00000000`00000000 : SyncController!ProviderActivityFactory<SyncActivityFactory,&SyncActivityFactoryCLSID>::CreateDefaultActivityForJob+0xdb
0d 00007fff`36c50c13     : 00000251`a1f08900 00007fff`36c76410 00000251`a1f75048 00007fff`5076c750 : aphostservice!StdJobProviderByClsId<_BootstrapSyncAccountsSNJobArgs,unsigned long>::CreateActivity+0x7e
0e 00007fff`36c2547e     : 00000000`00000000 000000d2`e7dff580 00007fff`36c76410 00000000`00000000 : aphostservice!StdJob<_BootstrapSyncAccountsSNJobArgs,unsigned long>::ExecuteActivityStep+0x53
0f 00007fff`36c2455d     : 00000251`a1f6dae8 00000251`a3b5e4b0 00000251`aa0f9d00 00000000`7ffe0386 : aphostservice!JobScheduler::_UpdateJobQueue+0xd7e
10 00007fff`5162952a     : 00000251`aa0f9dc8 00000000`00000000 00000251`aa0f9dc8 00000251`a1e02340 : aphostservice!JobScheduler::s_BackgroundThreadProc+0x2ed
11 00007fff`515d6eb6     : 00000000`00000000 00000000`00000000 00000251`a1e02340 00000251`a1ecfdf0 : ntdll!TppWorkpExecuteCallback+0x13a
12 00007fff`503254e0     : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : ntdll!TppWorkerThread+0x686
13 00007fff`515c485b     : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : KERNEL32!BaseThreadInitThunk+0x10
14 00000000`00000000     : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : ntdll!RtlUserThreadStart+0x2b

リスト9:v1ポインターに関するGetHeapAddress出力とヒープ割り当てのコールスタック

TTD.Utility.GetHeapAddressの出力より、v1ポインターの開始ヒープアドレスが0x251a1f58f60で、SYNCUTIL!SyncStatsHelpers::_LookupAccountSyncStatsから初期化されていることがわかります。また、この関数では、v1ポインターがSYNCUTIL!SyncStatsHelpers::_LoadSyncStatsに移され、v1ポインターが参照するデータ構造にさまざまな統計データがロードされるのがわかります。

__int64 __fastcall SyncStatsHelpers::_LoadSyncStats(

       GUID* rguid,

       AccSyncStates_t* v1,

       struct SyncStatsHelpers::MUTABLE_SYNC_STATS* a3)

{

       hKey = 0i64;

       v5 = (HANDLE*)tlx::replace<HKEY__*, long (*)(HKEY__*), &long RegCloseKey(HKEY__*), 0>(&hKey);

       RegKey = SyncCreateRegKey(rguid, v5);

       if (RegKey < 0)

       {

              v9 = 186i64;

       LABEL_3:

              Log_HREvent_5((unsigned int)RegKey, 1i64, v7, v9);

              if (hKey)

                     RegCloseKey(hKey);

              return (unsigned int)RegKey;

       }

       RegKey = SyncStatsHelpers::_LoadStatsCounter(

              (SyncStatesHelpers*)hKey,

              (HKEY)L"ReceivedMailCount",

              (const unsigned __int16*)&v22,

              v8);

       if (RegKey < 0)

       {

              v9 = 191i64;

              goto LABEL_3;

       }

       _InterlockedExchange64((volatile __int64*)&v1->AccReceivedMailCount, v22);

       RegKey = SyncStatsHelpers::_LoadStatsCounter(

              (SyncStatesHelpers*)hKey,

              (HKEY)L"DeletedMailCount",

              (const unsigned __int16*)&v22,

              v11);

       if (RegKey < 0)

       {

              v9 = 192i64;

              goto LABEL_3;

       }

       _InterlockedExchange64((volatile __int64*)&v1->AccDeletedMailCount, v22);

       RegKey = SyncStatsHelpers::_LoadStatsCounter(

              (SyncStatesHelpers*)hKey,

              (HKEY)L"UpdatedMailCount",

              (const unsigned __int16*)&v22,

              v12);

       if (RegKey < 0)

       {

              v9 = 193i64;

              goto LABEL_3;

       }

       _InterlockedExchange64((volatile __int64*)&v1->AccUpdatedMailCount, v22);

       RegKey = SyncStatsHelpers::_LoadStatsCounter(

              (SyncStatesHelpers*)hKey,

              (HKEY)L"SentMailCount",

              (const unsigned __int16*)&v22,

              v13);

       if (RegKey < 0)

       {

              v9 = 194i64;

              goto LABEL_3;

       }

       _InterlockedExchange64((volatile __int64*)&v1->AccSentMailCount, v22);

       RegKey = SyncStatsHelpers::_LoadStatsCounter(

              (SyncStatesHelpers*)hKey,

              (HKEY)L"MailReceivedCountUnder5k",

              (const unsigned __int16*)&v22,

              v14);

       if (RegKey < 0)

       {

              v9 = 196i64;

              goto LABEL_3;

       }

       _InterlockedExchange64((volatile __int64*)&v1->AccMailReceivedCountUnder5k, v22);

       RegKey = SyncStatsHelpers::_LoadStatsCounter(

              (SyncStatesHelpers*)hKey,

              (HKEY)L"MailReceivedCount5kTo10k",

              (const unsigned __int16*)&v22,

              v15);

       if (RegKey < 0)

       {

              v9 = 197i64;

              goto LABEL_3;

       }

       _InterlockedExchange64((volatile __int64*)&v1->AccMailReceivedCount5kTo10k, v22);

       RegKey = SyncStatsHelpers::_LoadStatsCounter(

              (SyncStatesHelpers*)hKey,

              (HKEY)L"MailReceivedCount10kTo20k",

              (const unsigned __int16*)&v22,

              v16);

       if (RegKey < 0)

       {

              v9 = 198i64;

              goto LABEL_3;

       }

       _InterlockedExchange64((volatile __int64*)&v1->AccMailReceivedCount10kTo20k, v22);

       RegKey = SyncStatsHelpers::_LoadStatsCounter(

              (SyncStatesHelpers*)hKey,

              (HKEY)L"MailReceivedCount20kTo40k",

              (const unsigned __int16*)&v22,

              v17);

       if (RegKey < 0)

       {

              v9 = 199i64;

              goto LABEL_3;

       }

       _InterlockedExchange64((volatile __int64*)&v1->AccMailReceivedCount20kTo40k, v22);

       RegKey = SyncStatsHelpers::_LoadStatsCounter(

              (SyncStatesHelpers*)hKey,

              (HKEY)L"MailReceivedCountOver40k",

              (const unsigned __int16*)&v22,

              v18);

       if (RegKey < 0)

       {

              v9 = 200i64;

              goto LABEL_3;

       }

       _InterlockedExchange64((volatile __int64*)&v1->AccMailReceivedCountOver40k, v22);

       RegKey = SyncStatsHelpers::_LoadStatsCounter(

              (SyncStatesHelpers*)hKey,

              (HKEY)L"ClientUnreadToReadCount",

              (const unsigned __int16*)&v22,

              v19);

       if (RegKey < 0)

       {

              v9 = 202i64;

              goto LABEL_3;

       }

       _InterlockedExchange64((volatile __int64*)&v1->AccClientUnreadToReadCount, v22);

       RegKey = SyncStatsHelpers::_LoadStatsCounter(

              (SyncStatesHelpers*)hKey,

              (HKEY)L"MailReceivedKB",

              (const unsigned __int16*)&v22,

              v20);

       if (RegKey < 0)

       {

              v9 = 205i64;

              goto LABEL_3;

       }

       _InterlockedExchange64((volatile __int64*)&v1->AccMailReceivedKB, v22 << 10);

       if (hKey)

              RegCloseKey(hKey);

       return 0i64;

}

リスト10:メールアカウントの統計データでv1ポインターを初期化

この時点で、v1ポインターのデータ構造フィールドを一部回復することができました。v1ポインターは同期統計のデータオブジェクトであり、現在のセッション統計、集計された統計、アカウント統計を保持し、データ構造を使用して表示することができるものであることを突き止めました(リスト11参照)。

struct CSyncStates

{

  void *lpVtbl;

  DWORD RefCount;

  DWORD UnkCount;

  SyncStates_t SyncStates;

};

struct SyncStates_t

{

  LPCRITICAL_SECTION CritSecObj;

  QWORD qwUnknown08;

  QWORD qwUnknown10;

  QWORD qwUnknown18;

  QWORD qwUnknown20;

  AccSyncStats_t AccSyncStats;

  AggrSyncStats_t AggrSyncStats;

  SessionSyncStats_t SessSyncStats;

};

struct AccSyncStats_t

{

  QWORD StartAccountSyncStats;

  QWORD qwUnknown30;

  QWORD AccReceivedMailCount;

  QWORD AccDeletedMailCount;

  QWORD AccUpdatedMailCount;

  QWORD AccSentMailCount;

  QWORD qwUnknown58;

  QWORD qwUnknown60;

  QWORD AccMailReceivedKB;

  QWORD AccMailReceivedCountUnder5k;

  QWORD AccMailReceivedCount5kTo10k;

  QWORD AccMailReceivedCount10kTo20k;

  QWORD AccMailReceivedCount20kTo40k;

  QWORD AccMailReceivedCountOver40k;

  QWORD AccClientUnreadToReadCount;

};

struct AggrSyncStats_t

{

  QWORD StartAggrSyncStats;

  QWORD qwUnknown08;

  QWORD AggrReceivedMailCount;

  QWORD AggrDeletedMailCount;

  QWORD AggrUpdatedMailCount;

  QWORD AggrSentMailCount;

  QWORD qwUnknown30;

  QWORD qwUnknown38;

  QWORD AggrMailReceivedKB;

  QWORD AggrMailReceivedCountUnder5k;

  QWORD AggrMailReceivedCount5kTo10k;

  QWORD AggrMailReceivedCount10kTo20k;

  QWORD AggrMailReceivedCount20kTo40k;

  QWORD AggrMailReceivedCountOver40k;

  QWORD AggrClientUnreadToReadCount;

};

struct SessionSyncStats_t

{

  QWORD StartSessionSyncStats;

  QWORD qwUnknown08;

  QWORD SessReceivedMailCount;

  QWORD SessDeletedMailCount;

  QWORD SessUpdatedMailCount;

  QWORD SessSentMailCount;

  QWORD qwUnknown30;

  QWORD qwUnknown38;

  QWORD SessMailReceivedKB;

  QWORD SessMailReceivedCountUnder5k;

  QWORD SessMailReceivedCount5kTo10k;

  QWORD SessMailReceivedCount10kTo20k;

  QWORD SessMailReceivedCount20kTo40k;

  QWORD SessMailReceivedCountOver40k;

  QWORD SessClientUnreadToReadCount;

};

リスト11:v1ポインタのデータ構造

リスト11で定義されているデータ構造をIDA Proに適用して、HeaderParser::_PostNewMessageCreationを再度逆コンパイルすると、コードの逆コンパイルが向上します(リスト12参照)。

__int64 __fastcall HeaderParser::_PostNewMessageCreation(HeaderParser* this, struct IMessage* lppMessage)

{

       rfc822_size = this->rfc822_size;

       if ((_DWORD)rfc822_size != 0xFFFFFFFF)

       {

              CurrentSyncStats = (SessionSyncStats_t*)*((_QWORD*)this->pImapSyncContext + 27);

              if (rfc822_size <= 0xA000)

              {

                     if ((unsigned int)rfc822_size <= 0x5000ui64)

                     {

                           if ((unsigned int)rfc822_size <= 0x2800ui64)

                           {

                                  offset = 0x48i64;

                                  if ((unsigned int)rfc822_size > 0x1400ui64)

                                         offset = 0x50i64;

                           }

                           else

                           {

                                  offset = 0x58i64;

                           }

                     }

                     else

                     {

                           offset = 0x60i64;

                     }

              }

              else

              {

                     offset = 0x68i64;

              }

              _InterlockedExchangeAdd64((volatile signed __int64*)((char*)CurrentSyncStats + offset), 1ui64);

              _InterlockedExchangeAdd64(

                     (volatile signed __int64*)&CurrentSyncStats->SessMailReceivedKB,

                     (unsigned int)rfc822_size);

       }

リスト12:HeaderParser::_PostNewMessageCreationの逆コンパイルコードが向上

このことから、このフィールドは64ビットのフィールドであることが判明しました。つまり、当初疑われていた算術オーバーフローは有害性のものではなかったのです。また、SessionSyncStats_t-> SessMailReceivedKBの操作コードも検証しましたが、この値は最終的にレジストリキー「MailReceivedKB」に書き込まれることが判明しました。

__int64 __fastcall AggregateAccountSyncStats(

       GUID* AccountGuid,

       __int64 a2,

       struct SyncStatsHelpers::AccountStatSingletons** a3)

{

       SyncStates = 0i64;

       hr = SyncStatsHelpers::_LookupAccountSyncStats(AccountGuid, (SyncStates_t*)&SyncStates, a3);

       if (hr >= 0)

       {

              EnterCriticalSection((LPCRITICAL_SECTION)SyncStates);

              SyncStatsHelpers::MUTABLE_SYNC_STATS::AggregateSyncStats(&SyncStates->AggrSyncStates, &SyncStates->SessSyncStates);

              SyncStatsHelpers::MUTABLE_SYNC_STATS::AggregateSyncStats(

                     (AggrSyncStates_t*)&SyncStates->AccSyncStates,

                     (SessionSyncStates_t*)&SyncStates->AggrSyncStates);

              hr = SyncStatsHelpers::_StoreSyncStats(AccountGuid, &SyncStates->AccSyncStates, v8);

              if (hr >= 0)

                     hr = 0;

              else

                     Log_HREvent_5((unsigned int)hr, 1i64, v10, 346i64);

              LeaveCriticalSection((LPCRITICAL_SECTION)SyncStates);

       }

       else

       {

              Log_HREvent_5((unsigned int)hr, 1i64, v5, 338i64);

       }

       return (unsigned int)hr;

}

__int64 __fastcall SyncStatsHelpers::_StoreSyncStats(

       GUID* rguid,

       AccSyncStates_t* SyncStates,

       const struct SyncStatsHelpers::MUTABLE_SYNC_STATS* a3)

{

       hKey = 0i64;

       v5 = (HANDLE*)tlx::replace<HKEY__*, long (*)(HKEY__*), &long RegCloseKey(HKEY__*), 0>(&hKey);

       RegKey = SyncCreateRegKey(rguid, v5);

       if (RegKey < 0)

       {

              v8 = 227i64;

       LABEL_3:

              Log_HREvent_5((unsigned int)RegKey, 1i64, v7, v8);

              if (hKey)

                     RegCloseKey(hKey);

              return (unsigned int)RegKey;

       }

       v10 = _InterlockedExchangeAdd64((volatile signed __int64*)&SyncStates->AccReceivedMailCount, 0i64);

       RegKey = SetRegQWORD(hKey, 0i64, L"ReceivedMailCount", v10);

       if (RegKey < 0)

       {

              v8 = 231i64;

              goto LABEL_3;

       }

       v11 = _InterlockedExchangeAdd64((volatile signed __int64*)&SyncStates->AccDeletedMailCount, 0i64);

       RegKey = SetRegQWORD(hKey, 0i64, L"DeletedMailCount", v11);

       if (RegKey < 0)

       {

              v8 = 232i64;

              goto LABEL_3;

       }

       v12 = _InterlockedExchangeAdd64((volatile signed __int64*)&SyncStates->AccUpdatedMailCount, 0i64);

       RegKey = SetRegQWORD(hKey, 0i64, L"UpdatedMailCount", v12);

       if (RegKey < 0)

       {

              v8 = 233i64;

              goto LABEL_3;

       }

       v13 = _InterlockedExchangeAdd64((volatile signed __int64*)&SyncStates->AccSentMailCount, 0i64);

       RegKey = SetRegQWORD(hKey, 0i64, L"SentMailCount", v13);

       if (RegKey < 0)

       {

              v8 = 234i64;

              goto LABEL_3;

       }

       v14 = _InterlockedExchangeAdd64((volatile signed __int64*)&SyncStates->AccMailReceivedCountUnder5k, 0i64);

       RegKey = SetRegQWORD(hKey, 0i64, L"MailReceivedCountUnder5k", v14);

       if (RegKey < 0)

       {

              v8 = 236i64;

              goto LABEL_3;

       }

       v15 = _InterlockedExchangeAdd64((volatile signed __int64*)&SyncStates->AccMailReceivedCount5kTo10k, 0i64);

       RegKey = SetRegQWORD(hKey, 0i64, L"MailReceivedCount5kTo10k", v15);

       if (RegKey < 0)

       {

              v8 = 237i64;

              goto LABEL_3;

       }

       v16 = _InterlockedExchangeAdd64((volatile signed __int64*)&SyncStates->AccMailReceivedCount10kTo20k, 0i64);

       RegKey = SetRegQWORD(hKey, 0i64, L"MailReceivedCount10kTo20k", v16);

       if (RegKey < 0)

       {

              v8 = 238i64;

              goto LABEL_3;

       }

       v17 = _InterlockedExchangeAdd64((volatile signed __int64*)&SyncStates->AccMailReceivedCount20kTo40k, 0i64);

       RegKey = SetRegQWORD(hKey, 0i64, L"MailReceivedCount20kTo40k", v17);

       if (RegKey < 0)

       {

              v8 = 239i64;

              goto LABEL_3;

       }

       v18 = _InterlockedExchangeAdd64((volatile signed __int64*)&SyncStates->AccMailReceivedCountOver40k, 0i64);

       RegKey = SetRegQWORD(hKey, 0i64, L"MailReceivedCountOver40k", v18);

       if (RegKey < 0)

       {

              v8 = 240i64;

              goto LABEL_3;

       }

       v19 = _InterlockedExchangeAdd64((volatile signed __int64*)&SyncStates->AccClientUnreadToReadCount, 0i64);

       RegKey = SetRegQWORD(hKey, 0i64, L"ClientUnreadToReadCount", v19);

       if (RegKey < 0)

       {

              v8 = 242i64;

              goto LABEL_3;

       }

       v20 = _InterlockedExchangeAdd64((volatile signed __int64*)&SyncStates->AccMailReceivedKB, 0i64);

       RegKey = SetRegQWORD(hKey, 0i64, L"MailReceivedKB", v20 >> 10);

       if (RegKey < 0)

       {

              v8 = 246i64;

              goto LABEL_3;

       }

       if (hKey)

              RegCloseKey(hKey);

       return 0i64;

}

リスト13:レジストリキーにプッシュされているセッション同期の統計データ

結論

本投稿の時点で、ファザーモジュールにより脆弱性は発見されませんでした。また、コード監査を手入力で行い、メモリ破損の潜在的な問題を探しましたが、結果は得られませんでした。しかし、Microsoft IMAPクライアントをファジングするプロセスにおいて多くのことが得られました。この調査プロジェクトの重要なポイントは、WTFのファザーについて理解が深まったことと、実際のターゲットを使用して実践的な経験が得られたことです。また、本投稿で取り上げられなかった課題もまだいくつかありますが、このツールは間違いなく、ブラックボックスファジングのセキュリティリサーチャーにとって必須ツールの1つと言えます。