FFRIエンジニアブログ

株式会社FFRIセキュリティのエンジニアが執筆する技術者向けブログです

Black Hat EU 2020 参加報告 1

はじめに

こんにちは。基礎技術研究室リサーチエンジニアの中川です。12 月 7 日から 10 日に開催された Black Hat EU 2020 へ参加、登壇してきましたので報告します。

今年の Black Hat EU

新型コロナウイルスの影響があり、今年はバーチャルでの開催となりました。本来ロンドンでの開催でしたので、「初めてのロンドン旅行」という楽しみが失われてしまったのは残念でした。一方、長時間飛行機に乗る必要がなく、気軽に参加できるという点は良かったです。

発表は事前に各自でビデオを撮影し、その内容を投稿、発表の時間になったらそのビデオを運営が流すという形で行われました。事前に発表のビデオを撮影するよう求められたのは、時差の関係上リアルタイムの参加が難しい方のことを考えての対応だと思われます。

質疑応答は (一応、任意ではありましたが) できるだけリアルタイムに参加することを求められました。質疑応答は、Zoom と Swapcard という Virtual Event 用のプラットフォームを併用して行われ、各ツールはそれぞれ以下のように利用されました。

  • Swapcard: Q&A ボックスで質問の受け取り、チャットボックスでフィードバックの受け取り
  • Zoom: Q&A ボックス・チャットボックスで受け取った内容について口頭で回答

発表内容の背景と概要

今回、私と押場で "Jack-in-the-Cache: A New Code injection Technique through Modifying X86-to-ARM Translation Cache" というタイトルで発表してきました。

※ARM (もしくは ARM64) の表記について、昨今一般的になりつつある Arm ではなく、Microsoft に従い ARM とすべて大文字にしています。また、命令セット・コアのどちらを指す場合についても、簡単に ARM (もしくは ARM64) と表記しています。

最近の Windows 10 は動作可能な CPU として x86/x64 だけでなく、ARM64 も含んでいます。(Windows は初期段階で複数のアーキテクチャをサポートするようにはしていて、今ではサポートされていないですが、過去には MIPS や IA-64 や PowerPC も入っていました。) ARM64 上で動作する Windows は「ARM 版 Windows 10」と呼ばれます。

2017 年頃から HP Envy x2 や ASUS Nova Go など、この ARM 版 Windows 10 を搭載した端末が発売されるようになりました。最近ですと、Microsoft Surface Pro X が発売されましたが、この端末には Microsoft SQ1 という ARM64 CPU が搭載されています。

CPU の変更に際して問題になることとして、プログラムの互換性の問題があります。x86 から x64 の移行期では CPU のモードの切り替えで x86 のコードを x64 上で実行できたため問題になりませんでした。しかし、x86/x64 から ARM64 の場合だと当然ながらそのようなことはできません。

ARM 版 Windows ではこの問題を x86 エミュレーション機能を提供することで解決しています。要は、実行時に x86 のコードを ARM64 のコードに変換し、その変換結果を実行する機能です。

しかしながら、アプリケーションの起動のたびに x86 から ARM64 へバイナリ変換することは、パフォーマンスの大幅な低下に繋がります。せっかくアプリケーションが動作しても、アプリケーションが頻繁にフリーズするのでは、ユーザーが使ってくれないという事態になりかねません。

そこで、変換結果のキャッシュ機能が ARM 版 Windows には備わっています。最初にアプリケーションを起動した際は実行時にバイナリを変換するため、遅くなります。しかし、2 回目同じアプリケーションを起動した際には、キャッシュされた変換結果が使われるため、高速に実行できる仕組みとなっています。

このファイルは X86-To-ARM64 (XTA) キャッシュファイルと呼ばれています。

今回、我々は XTA キャッシュファイルの改ざんによるコードインジェクション、及びそのユースケースについて発表しました。この XTA キャッシュファイルの改ざんによるコードインジェクションを我々は XTA キャッシュハイジャッキングと呼んでいます。

XTA キャッシュファイルの改ざん自体は管理者権限が必要となるため、攻撃のハードルは高いのですが、それでも攻撃者としてこの手法を用いるメリットがあります。その点をまとめて発表しています。

本研究のきっかけ

研究の詳細については上記 Briefings のリンク先にあるスライドをご参照ください。このブログではどういったきっかけでこの研究がスタートしたのか、アブストラクトをサブミットするまで経緯の部分を紹介します。

発端は Cylance Research Team による ARM 版 Windows の x86 エミュレーションの解析ブログです。ARM 版 Windows の x86 エミュレーションに使われているキャッシュファイル、x86 エミュレーション実行の流れについての解析結果が記載されている記事です。

私は彼らのブログ記事を読み、素朴に「キャッシュファイル自体を改ざんしたら何が起きるのか。例えば正規ファイル実行時の振る舞いを変更させるとかできないか」と感じました。そこで、「just idea だけど」と Slack に以下のように書き込みを行ったところ、

図 1 当時の Slack の投稿

上司の押場が反応してくれて、「ちょっとやってみるか」と思ったのがきっかけです。

図 2 当時の Slack の投稿

幸い、基礎技術研究室では (繁忙期でない限りは) 1 週間のうち 1 日を好きな研究をする時間として使って良いという制度がありました。こちらを活用し、ひとまず手を動かしてみることにしました。

端末入手と実験

実際に ARM 版 Windows 10 が動作する端末を入手し、まず、キャッシュファイルの格納ディレクトリ (%SytemRoot%\XtaCache) の権限から確認しました。

デフォルトでは特定のサービス (XtaCache.exe) にのみ full permission が付与されており、中にあるファイルの読み書きは制限されています。しかしながら、管理者権限があれば、ディレクトリの所有者を別のユーザーに変更可能ということがわかりました。

ディレクトリのキャッシュファイルが読み書きできるようになった後、以下のように実験しました。

(1) 以下の test1.c をターゲットアーキテクチャ x86 でコンパイル (以下、プログラム 1 と表記) し、実行後、キャッシュファイルを取得

// test1.c
#include <stdio.h>

int main(int argc, char* argv[]) {
    if (argc != 2) {
        puts("1"); // argc が 2 でないとき 1 と表示
    } else {
        puts("2"); // argc == 2 のときに 2 と表示
    }
}

(2) 以下の test2.c をターゲットアーキテクチャ x86 でコンパイル (以下、プログラム 2 と表記) し、実行後、キャッシュファイルを取得

// test2.c test1.c と違い、表示される文字列の出力条件が逆
#include <stdio.h>

int main(int argc, char* argv[]) {
    if (argc == 2) {
        puts("1"); // argc == 2 のときに 1 と表示
    } else {
        puts("2"); // それ以外では 2 と表示
    }
}

(3) プログラム 2 のキャッシュファイルをプログラム 1 のキャッシュファイルにリネームし、プログラム 1 を再度実行

つまり、プログラム 1 の条件 argc != 2 を反転させたプログラム 2 を作成し、そのキャッシュファイルをプログラム 1 のキャッシュファイル名として保存し、プログラム 1 を実行したわけです。

すると、プログラム 1 は正常に実行でき、プログラム 1 を実行しているにも関わらず、argc == 2 のときに 1 と標準出力に表示されたのです。つまり、プログラム 2 のキャッシュファイルの内容が実行されたわけですね。

最初、キャッシュファイルの完全性を確認する機構が何かしら備わっていることを想定していたのですが、実際に調べたところそのような機構はなく、改ざんして別のコードを実行できることがわかりました。

この結果を基礎技術研究室のミーティングで話したところ「この研究に集中して取り組み、まとめた上でカンファレンス目指してみないか」と言われ、ここからフルタイムで研究することになります。

キャッシュファイルのフォーマットの解析

さて、簡単な条件分岐の反転ぐらいはできることがわかったわけですが、任意のコードをキャッシュファイルにインジェクトして実行できるのかはこの時点では不明でした。そもそも、キャッシュファイルのどこに ARM64 の翻訳結果が入っていて、x86 のコードとどう対応するのかすらこの時点ではわかっていませんでした。

ここから任意のコードを実行できるようにするには、キャッシュファイルのフォーマットの深い理解が必要でした。

最初にあげた Cylance Research Team のブログにも簡単には載っていたのですが、詳細な解析はなされておらず、解析のためのツールも提供されていませんでした。

そこで、キャッシュファイルのパースを行っていると推察される

  • キャッシュファイルを作成するコンパイラ (xtac.exe)
  • x86 エミュレーター DLL (xtajit.dll)

の 2 つを解析し、キャッシュファイルのフォーマットの解析に取り掛かりました。

x86 は ISA が非常に複雑で、デコード 1 つをとっても非常に複雑な処理になります。それを ARM64 に変換する処理が xtac.exextajit.dll には入っているわけですから、双方ともバイナリサイズは大きく、定義されている関数の数も膨大です。

さらに、xtac.exextajit.dll のデバッグシンボルを Microsoft が公開していないこともあり、非常に地道な作業となりました。根気強く解析することでキャッシュファイルの構造・各ヘッダーのメンバーの意味を一通り特定できました。

詳細は発表スライドの Appendix に記載があります。

さらに、キャッシュファイルのフォーマットの解析結果をもとに、radare2 向けに XTA キャッシュファイルのパーサープラグインを作成しました。このプラグインが入った radare2 はこちらから入手できます。

PoC コード実装

ここから押場と作業を分担し、押場には脅威分析・ユースケースの考察をお願いし、私は XTA キャッシュハイジャッキングの PoC コードの実装に取り掛かりました。

PoC コードの実装について説明する前に、ARM64 への翻訳結果としてどういったコードが含まれるのかを説明します。

ARM64 への翻訳結果として含まれるコードでは、x86 と ARM64 の各レジスタがそれぞれ以下のように対応しています。

図 3 ARM64 のレジスタと x86 のレジスタの対応関係

この対応表を用いることで、翻訳された ARM64 コードを理解できます。1 つ例を見てみましょう。

図 4 x86 のコードと翻訳された ARM64 のコード

上の図には、x86 コードと翻訳された後の ARM64 コードが示されています。図 3 によれば w29ebp に、w28esp に対応しています。それを踏まえると、ARM64 の最初の命令は、「esp の値を 4 引き、ebp レジスタの内容をスタックトップに格納する」という意味になります。これは x86 の push ebp そのものです。

※ARM64 アセンブリに馴染みのない方向けに少し補足をしておきます。str はレジスタの内容をメモリにストアするための命令で、第 1 オペランドの内容を第 2 オペランドで指定されたアドレスに格納します。第 2 オペランドの [x29, -4]! ですが、これは pre-indexed addressing mode と呼ばれるもので、str 命令実行後に x29 -= 4 を実行することを意味します。

その他の命令についても同じように理解できます。

Windows API などの関数の呼び出しについても x86 を忠実に再現する形になっていて、引数・リターンアドレスをスタックに push した上で行われます。以下の図 5 は発表スライドの Appendix からの引用で、MessageBoxA を呼び出す処理の変換結果となりますが、

  • 関数の引数を x28 で示すスタックに push
  • w6 に格納されているリターンアドレスの値をスタックに push

を関数呼び出し前に行っていることがわかります。

図 5 MessageBoxA 関数を呼び出す処理の変換結果

このように、キャッシュファイルに含まれるコードでは AArch64 EABI に従わない形で関数を呼び出します。

PoC となるシェルコード作成の際も、AArch64 EABI に従わない形での関数を呼び出す必要がありました。

ここは実は一苦労ありまして、native の dll を LoadLibrary し、AArch64 EABI に従う形で Windows API 呼び出せないか合わせて検討しています。しかし、一部の API については正常に呼び出せない、処理中にクラッシュするなどの不具合が発生するため困難ということがわかっています。

そのため、この AArch64 EABI に従わない形の関数呼び出しがシェルコード作成においては必須となります。

シェルコード内で Windows API を呼び出す際の注意点については発表資料の Appendix にも説明がありますので、詳しくはそちらをご参照ください。また、キャッシュファイルへのコードインジェクションを行うためのツールと合わせて以下に公開していますので、よろしければそちらもご参照ください。

https://github.com/FFRI/XtaTools

敗北、そして採択

PoC コード、各種ツール、研究内容をまとめたスライドをすべて作成・準備した段階で、ちょうど CFP がオープンしていた Black Hat USA に投稿しました。

投稿後、本来 accept/reject の通知が来るはずの日程を過ぎても連絡がなく、運営に連絡したところ...

「補欠枠に入っているから、他の発表がキャンセルになった場合繰り上がる。そうなったらすぐ連絡する。」

と連絡をもらい、結局繰り上がることなく reject となりました。

これは相当悔しかったのですが、「補欠に入るぐらいだから、他のところに出したら通るのでは」と思い、めげずにそのまま Black Hat EU の方に投稿するとめでたく accept となりました。朝、メールクライアントを起動し、Subject が "Congratulations! Your Submission for Black Hat Europe 2020 was Accepted" というメールを受け取ったときのことは今でも覚えています。

おわりに

簡単ながら、Black Hat EU 2020 で発表した内容と、発表に至るまでの経緯について紹介しました。

セキュリティ未経験で入社した私がここまで早くここに至れるとは正直思っていませんでした。私だけの力ではなく、周囲の手厚いサポートがあってのことでした。特に基礎技術研究室の他メンバーに解析や脅威分析において数多くのアドバイスを頂いたからこそ、成し遂げられたことでした。

次回は、Black Hat EU で聴講して気になった注目発表を取り上げ紹介します。

エンジニア募集

FFRIセキュリティではサイバーセキュリティに関する研究開発を希望するエンジニアを募集しています。採用に関してはこちらをご覧ください。