オープンソースAIセキュリティagentで24件のAndroid脆弱性を発見した私たちの経験

GitHub Security Lab は YAML で定義した taskflow で AI に Android アプリをレビューさせ、OsmAnd のサイレント位置追跡や Wikipedia アプリのアカウント乗っ取りチェーンなど 24 件の脆弱性を報告したが、各発見は人による再確認に依存している。

日本語
コピー
GitHub 风格的封面,深色底上是几何化的 Invertocat 猫标志,右侧有一列代码符号

オープンソースの AI セキュリティ agent で Android の脆弱性を 24 件見つけるまで

セキュリティ分野で AI が広がるなか、私たちのチームは GitHub Security Lab Taskflow Agent を作った。セキュリティ研究者が、自分にとって使い勝手のよい AI プロンプトとワークフローを自動化し、パッケージ化して共有できるようにするためのものだ。この記事では、Android アプリの脆弱性を見つけるための監査タスクフローを私がどう書いたかを紹介する。

新しいモデルはコードを理解する力がどんどん上がっているが、タスクフローに独自のプロンプトを書くことで、研究者はモデルを誘導できる。調査を小さなタスクに分割し、LLM が複雑な脆弱性にもっと速くたどり着けるようにしたり、そうしなければ完全に見落としていた脆弱性を見つけさせたりできる。

これらのタスクフローを使って、私はこれまでに Android アプリで 20 件以上の脆弱性を報告してきた。新しい脆弱性の公開時期は告知ページで確認できる。具体例を先に見たい人はこのまま読み進めてほしい。これらのタスクフローが見つけた高危険度の脆弱性の実例をいくつか挙げる。

自分のプロジェクトでこれらのタスクフローを動かす方法

すぐに試したい人向け。タスクフローはオープンソースで、動かすのは簡単だ。ただし注意点がある。GitHub Copilot のライセンスが必要で、プロンプトはプレミアムモデルのリクエストを消費する。タスクフローを実行すると大量のツール呼び出しが発生し、トークンをあっという間に使い切ることがある。

  1. seclab-taskflows リポジトリに移動し、codespace を起動する。
  2. codespace の初期化が終わるまで数分待つ。
  3. ターミナルで ./scripts/audit/run_mobile.sh myorg/myrepo を実行する

中規模のリポジトリなら、完了まで 1〜2 時間かかることがある。終わると SQLite ビューアが自動で開き、結果が表示される。「audit_results」テーブルを開き、「has_vulnerability」列にチェックマークが付いた行を探してほしい。

同僚の Peter と Mo が、自分たちの監査タスクフローについてブログ記事を書いている。あのタスクフローはそれ自体よくできているが、Android アプリには特有の脆弱性カテゴリがあり、そこに注目させたかったので誘導が必要だった。

まず、gather_mobile_entry_point_info.yaml というタスクフローを追加した。エントリポイントとは、攻撃者が制御するデータがコード内を流れうる箇所のことだ。このタスクフローはエントリポイントを受け取り、モバイルのエントリポイントとそれ以外に分類する。これで AI は、モバイルアプリ、Web サーバー、デスクトップアプリなど複数の種類のアプリを含むリポジトリを扱いつつ、正しい攻撃面を理解できる。

次に classify_application_local.yaml を変更した。ここにはよくある脆弱性カテゴリを並べてあり、LLM に各エントリポイントとコンポーネントを組み合わせて一つずつ検討するよう求めている。モバイルアプリの脆弱性は比較的知られておらず、LLM は非決定的なので、特定の重要な脆弱性カテゴリを確実にチェックさせなければならない。たとえば、前のタスクフローが intent ベースのエントリポイントを特定したなら、intent ベースのよくある脆弱性の一覧を用意して一つずつ確認させる。confused deputy や安全でないブロードキャストなどだ。これにより LLM はコンポーネント間の関連を捉え、常に脅威モデル全体を押さえられる。

2 種類のプロンプトを組み合わせて何度も実行すると、それぞれの利点を活かせる。厳密なプロンプトと繰り返し実行で明らかな脆弱性の見落としを防ぎ、広いプロンプトで AI の創造性を引き出す。

タスクフローが見つけた脆弱性の実例 2 件

この節では、タスクフローが見つけ、すでに公開された脆弱性を 2 件紹介する。これまでに発見・報告した脆弱性は合計 24 件だ。

OsmAnd によるユーザーの追跡

OsmAnd は Open-Street-Map を主要なデータソースとする人気のサードパーティ製ナビアプリだ。App Store と Play Store の両方で配信されており、ここでは Android 版を見る。ダウンロード数は 1000 万を超える。この節では、見つかった 3 件の脆弱性のうち最も興味深いものを紹介する。悪意あるアプリがこれを利用して端末の位置を追跡できる。 OsmAnd は MapActivity という activity をエクスポートしている。Android の activity とは、アプリ内の独立した単一の画面で、ユーザーが操作できる UI を提供するものだ。MapActivity はアプリ内で設定ファイルや deeplink を開く処理を担っており、エクスポートされている。エクスポートされた activity とは、自分のアプリ以外のコンポーネントから起動できる activity のことだ。 android.xml ファイルのスクリーンショット しかし設定ファイルを開く際、アプリは intent extras(settings_version、silent_import、replace、export_type_list_key)を受け取れるようになっている。Intent は Android におけるメッセージオブジェクトで、別のアプリコンポーネントに何らかの操作を要求するために使う。intent extras は intent に付随するキーと値のペアで、要求と一緒に情報を渡すためのものだ。MapActivity はこれらの extras がとある AIDL サービスから来るものと想定している。本来はプロセス内のチャネルを通じて渡されるべきもので、intent extras 経由ではない。どのアプリでも、エクスポートされた activity の任意の intent に任意の extras を入れられるからだ。外部の呼び出し元が設定できる extras を制限する仕組みを Android は提供していない。 MapActivity はエクスポートされているので、どのアプリでもこの activity に intent を送り、任意の extras を付けられる。そのなかには、ユーザーに気づかれずに設定をアプリへインポートさせる intent extras も含まれる。この Android アプリは handleOsmAndSettingsImport 関数を使って以下の設定をインポートする。

  • SilentImport:通知なしでのインポートを許可する
  • Replace:設定を追加するだけでなく置き換えることを許可する
  • SettingsTypes:ユーザーの確認なしでのインポートを許可する
private void handleOsmAndSettingsImport(Uri intentUri, String fileName, Bundle extras) { 
 fileName = fileName.replace(ZIP_EXT, ""); 
 if (extras != null && CollectionUtils.containsAny(extras.keySet(), 
 SETTINGS_VERSION_KEY, SETTINGS_LATEST_CHANGES_KEY)) { 
 int version = extras.getInt(SETTINGS_VERSION_KEY, -1); 
 String latestChanges = extras.getString(SETTINGS_LATEST_CHANGES_KEY); 
 boolean replace = extras.getBoolean(REPLACE_KEY); // ← attacker-controlled 
 boolean silentImport = extras.getBoolean(SILENT_IMPORT_KEY); // ← attacker-controlled 
 ArrayList<String> exportTypeKeys = 
 extras.getStringArrayList(EXPORT_TYPE_LIST_KEY); // ← attacker-controlled 
 List<ExportType> exportTypes = null; 
 if (exportTypeKeys != null) { 
 exportTypes = ExportType.valuesOf(exportTypeKeys); 
 } 
 handleOsmAndSettingsImport(intentUri, fileName, exportTypes, 
 replace, silentImport, latestChanges, version); 
 } else { 
 handleOsmAndSettingsImport(intentUri, fileName, 
 null, false, false, null, -1); // safe defaults 
 } 
} 

任意の設定をインポートできるようになったことで、地図のタイルを差し替えるといった重要な変更が可能になる。OsmAnd がタイルごとに生成する URL は次の形式だ:

return MessageFormat.format(urlTemplate, zoom + "", x + "", y + "");

OsmAnd はデフォルトでローカルのタイルを使うが、次の URL でデフォルトのタイルファイルを上書きできる:

こうすると、タイルごとの正確な x, y 座標がすべて漏れる。この URL はレスポンスに対応するタイルの画像を含むことを要求するので、攻撃者サーバーのバックエンドで OpenStreetMaps の該当タイルを返す。攻撃者は、ユーザーが OsmAnd アプリで読み込んだすべてのタイルの x, y 座標のリストを手に入れる。ユーザーはアプリの設定が改ざんされたことに気づかない。どのアプリでも、権限が一切なくても、これを使って OsmAnd の設定を上書きし、プライベートな位置データを攻撃者サーバーに送信できる。

# [TILE #1] 14:23:07 z=15 x=9649 y=12320 
# ├── center: 40.70979, -73.98743 
# └── 🗺️ https://www.openstreetmap.org/#map=15/40.70979/-73.98743

同じ脆弱性を利用して、ユーザーが OsmAnd でナビゲーションするたびにその起点と終点を取得し、これも攻撃者サーバーに送信できる。ユーザーは何も異常に気づかない。

[ROUTE #1] 07:02:47 vehicle=car waypoints=2 
 ├── path: /osrm/car/-122.084,37.4219983;-122.32450103759766,37.99944305419922 
 ├── 📍 ORIGIN: 37.421998, -122.084000 
 │ https://www.openstreetmap.org/#map=15/37.42200/-122.08400 
 ├── 🏁 DESTINATION: 37.999443, -122.324501 
 │ https://www.openstreetmap.org/#map=15/37.99944/-122.32450

deeplink による Wikipedia アカウントの乗っ取り

次は Wikipedia Android アプリだ。ユーザーはスマートフォンでこれを使って Wikipedia を閲覧する。アプリ内で Wikipedia のページを開くため、Wikipedia Android アプリは wikipedia:// deeplink のフックを登録してアプリを起動する。たとえば deeplink は wikipedia://wikipedia.org/wiki/PoC のようになる。しかしホスト名解決のロジック上の欠陥により、Wikipedia 以外の URL を読み込めてしまう。

 private fun handleIntent(intent: Intent) { 
 if (Intent.ACTION_VIEW == intent.action && intent.data != null) { 
 // TODO: handle special cases of non-article content, e.g. shared reading lists. 
 intent.data?.let { 
 if (it.authority.orEmpty().endsWith(WikiSite.BASE_DOMAIN)) { 
 // Pass it right along to PageActivity 
 val uri = Uri.parse(it.toString().replace("wikipedia://", WikiSite.DEFAULT_SCHEME + "://")) 
 startActivity(Intent(this, PageActivity::class.java) 
 .setAction(Intent.ACTION_VIEW) 
 .setData(uri)) 
 } 
 } 
 } 
 } 

このプリミティブがあれば、wikipedia:// deeplink でユーザーを任意のサイトに誘導し、ユーザーには Wikipedia のページだと思わせたまま、実際には攻撃者が制御するページを見せられる。さらに攻撃者はアプリの WebView で任意の JavaScript を実行できる。これは危険なプリミティブで、通常は安全と見なされる環境への入口を攻撃者に与えることになる。この脆弱性パターンは同じアプリ内で一度ならず二度現れる:

// SharedPreferenceCookieManager.kt:101 
if (domain.endsWith(domainSpec)) { 
 buildCookieList(cookieList, cookiesForDomainSpec, null) 
} 

2 つ目のコードは、あるページが wikipedia.org からの cookie を伴うべきかどうかを検査する。この 2 つの問題を組み合わせると、Wikipedia ページ上のすべての cookie を漏洩できる。しかもこれらの cookie はすべて長期間有効だ。

この 2 つの脆弱性を連鎖させると、強力なアカウント乗っ取りが成立する。

  1. 被害者がブラウザで deeplink を含む悪意あるページにアクセスしてクリックする。
  2. Wikipedia Android アプリが自動的に起動し、wikipedia.org で終わる攻撃者制御のページ(例:evil-wikipedia.org)を読み込む。被害者はこれを Wikipedia のページだと思い込み、アプリはユーザーの cookie を自動送信する。攻撃者は被害者のユーザー名、長期トークン、そして Wikimedia の各プロジェクト(すべての Wikipedia、Commons、Wikidata、Meta など)で共通のセッショントークンを手に入れる。

これらの例は、LLM が漠然とした脆弱性カテゴリだけでなく、深刻な影響を持つロジックの脆弱性も発見できることを示している。

LLM は脆弱性の発見が得意だが、深刻度の評価は苦手

LLM は脆弱性の発見が得意で、影響の小さい低リスクのバグさえ見つけられる。ただ、AI が返してくる問題の多くは、現実のシナリオではまず起こりえない非常に特殊な状態を必要とする。さらに、低リスクの脆弱性を報告しないよう明確に指示しても、おかまいなしに報告してくることも多い。そのため、個々の発見はモバイルアプリに詳しいセキュリティ研究者がレビューすべきだ。

もう一つ問題なのは、脆弱性の深刻度がしばしば誤って見積もられることだ。実際の影響は緩和要因によって変わり、深刻度が下がることがよくある。

Android アプリのパストラバーサルを例に取ると、ファイルパスが外部ストレージ内に制限されているなら、この種の問題の相対的な深刻度は低い。こうした緩和要因を LLM が見抜くのは、「PoC を作成せよ」と明確に指示しない限り難しい。脆弱性の発見に複数回の実行が必要なだけでなく、PoC の作成にも複数回の実行が必要で、LLM にその脆弱性を実際に悪用させなければならない。モデルの可用性と速度によっては、影響がそれほど大きくないかもしれない脆弱性に余分な時間を費やすことになる。

それでも LLM は間違える。たとえば、アプリが内部ストレージと外部ストレージの両方のデータを使う場合、内部ストレージのデータのほうが優先されることが多い。LLM は、外部ストレージからのデータ——つまりパストラバーサルで書き込める部分——がアプリの実際のデータを変えると勝手に思い込むかもしれない。しかし内部ストレージがこの外部データを上書きするなら、脆弱性は存在しない。こうした複雑な挙動が誤検知を生むが、LLM のコンテキストが大きくなり推論能力が上がるにつれて誤検知は減っていく。とはいえ現時点で、これを解決する唯一の方法は、LLM にデバッガを与えて概念実証コードと元のコードを実行させるか、研究者がプロンプトで LLM を誘導してこの種の問題を専門に調べさせることだ。

LLM は API の挙動を深く理解している

ある言語を専門とするセキュリティ研究者なら、よくあるコードパターンに通じている。どの関数が安全で、どれが危険か。たとえば Go では、path.Clean の使用は filepath.Clean よりはるかに危険で、人気製品の Windows 版に影響する多くの脆弱性の原因になりがちだ。驚いたことに、LLM は言語のソースコードが手元になくても、各言語のセキュリティに関わる一般的な API の挙動をよく理解している。脆弱性レポートを受けて LLM に生成させた概念実証コードのほとんどは、こちら側での修正がほとんど不要だった。これは、LLM が過去のセキュリティエクスプロイトと API の挙動について深い知識を持っていることを示している。

結果についての補足

この記事を書くにあたり、私たちはモバイルアプリで 24 件の Android 脆弱性を発見した。多くはパストラバーサルのような単純なものだった。深刻なものも少数あり、その一部はこの記事で紹介している。Android アプリのセキュリティはすでにかなり堅牢なので、見つかる脆弱性の種類はセキュリティ研究者が予想する通りの場所に収まっていた。たとえば WebView におけるアプリ間のスクリプト実行や、露出した JavaScript bridge などだ。

12 個の CWE について GHSL と平均 CVSS を示したグラフ。

AI によるセキュリティ研究は、今オープンソースプロジェクトを守る最良の手段のひとつだと考えている。その守備範囲は Web アプリ、モバイルアプリ、デスクトップアプリに及ぶ。

おわりに

セキュリティはすべてのオープンソースメンテナにとって最優先事項であるべきだし、今後数年で AI が開発とセキュリティの両面でメンテナにとって欠かせないツールになることも分かっている。[seclab-taskflow-agent](https://github.com/GitHubSecurityLab/seclab-taskflow-agent) を使えば数分でセキュリティに取りかかれる。AI で脆弱性を見つけるための面白く独特なプロンプトやツール、仕組みを見つけた人からのコントリビュートも歓迎する。

出典: GitHub Blog← ホームへ戻る