STORES Product Blog

こだわりを持ったお商売を支える「STORES」のテクノロジー部門のメンバーによるブログです。

スマホアプリの脆弱性診断って何するの?(iOS編)

*本記事は STORES Advent Calendar 2023 6日目の記事です

こんにちは。セキュリティ本部のsohです。

現在、弊社ではスマホアプリ診断内製化の準備を進めています。

同じようにスマホアプリの脆弱性診断を内製化したい、というニーズがある会社は多く存在しますが、実際のところ、スマホアプリを対象とした脆弱性診断士の確保は困難であり、外部ベンダーの方にすべてお願いせざるを得ないケースも多いかと思います。

また、その情報の少なさから、スマホアプリ診断を実施したいと考えている開発者や脆弱性診断士にとっても、「何をやればいいのか」「何から始めればいいのか」がわからないものである場合は多いかと思います。

そこで、この記事では「スマホアプリ診断って実際何をしているのか」と疑問を持つ方をターゲットとして、一般的なスマホアプリ診断の検証要件や検証方法について解説します。

注意

本記事では、基本的に執筆時点の最新版であるMASVS v2.0.0をベースとしています。 また、本記事ではiOSを対象としたアプリをベースに解説していますが、Androidの場合も基本的な診断観点は同様です。 なお、以下に記載することは絶対的な指標ではなく、あくまでも一般的な定義であることをご理解いただけますと幸いです。

要件とガイドライン

まず、診断基準の統一を目的として、検証要件やガイドラインを定義することから始めましょう。

Webアプリケーション診断の場合、OWASPが発行する「ASVS (Application Security Verification Standard)」が検証要件の1つとして存在しますが、 スマホアプリ診断の場合、「MASVS (Mobile Application Security Verification Standard)」をベースとして検証要件の定義を行います。

また、当該要件を満たすための診断ガイドラインとして「MASTG (Mobile Application Security Testing Guide)」が存在します。

基本的に、多くのスマホアプリ診断ではMASTGをベースとして、独自の診断観点(アプリ固有のロジック検証など)を追加した上で診断を実施します。

診断の流れ

診断の流れとしては、基本的に静的診断で賄える範囲の診断を行った後、全画面の表示(探索)を行います。その後、あらかじめ定義しておいた検証方法に基づいて動的な診断を行います。

また、Web観点での指摘も存在するため、Burp Suiteなどのローカルプロキシーツールを接続し、ログを収集した状態で作業を行います。

最初に全画面の表示を行う理由として、データ保存観点の診断項目の存在があげられます。

初期状態ではユーザーの個人情報などがストレージに保存されておらず、データ保存観点での診断が実施できないため、動的診断の前に全画面探索を行ったほうが効率が良い認識です。

診断観点

基本的には次項の検証方法に準拠した方法での診断を行います。

ただし、アプリケーションの特性によるロジック不備や、ユーザー体験を損ねかねないような動作に関しては、あらかじめ定義された観点から外れていた場合でも指摘を行うことがあります。個人的には脆弱性診断を脆弱性診断と割り切って実施するのではなく、より良いアプリケーションを作っていく品質管理的な観点も加えた診断を行うことが重要であると考えています。

必要な知識

基本的なセキュリティの知見に加え、iOS/Androidのファイルシステムや、ビルド時の設定項目などを理解しておく必要があります。

また、特にブラックボックスでの診断の場合、静的解析および動的解析の知識が必須となる場合が多いです。

くわえて、iOSのJailbreak(脱獄)やAndroidのRoot化の方法を知っておくことも重要ですし、Tweakの開発知識も必要となる場合があります。ただし、Tweakについては動的解析で賄える部分も大きいため、必須ではありません。

検証方法について

MASVSは、検証範囲によって大きく7つに分割されています。

  • MASVS-STORAGE
  • MASVS-CRYPTO
  • MASVS-AUTH
  • MASVS-NETWORK
  • MASVS-PLATFORM
  • MASVS-CODE
  • MASVS-RESILIENCE

それぞれの項目で被る部分がありますが、抜粋して解説します。

MASVS-STORAGE

MASVS-STORAGEでは、主に内部ストレージに保存された情報を精査します。

主な診断方法

すべての遷移を確認した後、内部ストレージを確認し機微情報が保存されていないかを精査します。

また、機微情報がキーボードキャッシュやバックアップとして保存されうる実装(設定)となっていないかの精査も実施します。

主な改善策

基本的には機微情報を内部ストレージに保存しないような実装に変更することが推奨されます。

また、暗号化して保存する場合、Keychainなどのセキュアなハードウェアモジュールを利用した情報保存が推奨されます。

詳細説明

例えば以下のような場合に指摘となります。

  • 機微情報が平文または安全でない方法で保存されている
  • 機微情報がログとして出力されている
  • 機微情報がキーボードキャッシュとして保存されうる
  • 機微情報がバックアップとして保存されうる

機微情報を平文で保存または出力することは、さまざまな形での漏えいリスクを伴います。 そのため、改善策としてはKeychainなど、セキュアなハードウェアに基づいた方法で保存することとなります。

また、見落とされがちなのがキーボードキャッシュです。

iOSの場合、テキストフィールドのisSecureTextEntryTrueになっていない場合やオートコレクションが有効な場合などに、ユーザー入力がキーボードキャッシュとして端末内に保存されます。

この挙動により、第三者に入力内容が漏えいしてしまうことが考えられるため、当該設定を見直し、機微情報を入力する可能性のあるフォームではキーボードキャッシュが保存されないように設定する必要があります。

また、メールアドレス入力欄やユーザー名入力欄でオートコレクションが有効になっている場合、必要に応じてユーザビリティの観点でも指摘を行っています。

MASVS-CRYPTO

MASVS-CRYPTOでは、暗号化方式について精査します。

主な診断方法

前項同様、すべての遷移を確認した後、内部ストレージを確認し機微情報が保存されていないかを精査します。

また、ソースコードの確認やリバースエンジニアリングなどにより、安全でない暗号化が行われていないかを精査します。

主な改善策

機微情報などは安全な暗号化を行うことが推奨されます。

詳細説明

例えば以下のような場合に指摘となります。

  • データが安全でない暗号方式で保存されている
  • 安全でない乱数生成を行っている

「データが安全でない暗号方式で保存されている」に関しては、例えば何らかのデータを暗号化してサーバー側に送信したい場合、その暗号化方式は堅牢であることが期待されます。

例えば、AESで暗号化する場合を例にすると、ECBモードは同じ平文ブロックを同一の暗号文ブロックとして出力します。そのため、暗号としては脆弱であるとみなされます。

なお、初期化ベクトルやSaltがユニークでない場合も、暗号としては脆弱であり、指摘となります。

また、「安全でない乱数生成を行っている」に関しては乱数として十分な予測不可能性を持たない関数(例えば rand())を使用している場合に指摘となります。

MASVS-AUTH

MASVS-AUTHでは、ローカルおよびリモートエンドポイントでの認証認可について精査します。

主な診断方法

リモートエンドポイントに対しては、Web診断同様の観点で診断を実施します。

ローカル認証に関しては、PINや生体認証が適切に実装され、攻撃者が不正に認証を突破するおそれがないかを静的解析・動的解析の両面で精査します。(動的解析の場合、Fridaなどのデバッガーや自作Tweakによる回避を試みます)

また、本来認証認可の制御が必要であると思われるのにも関わらず制御が行われていない場合も指摘となります。

主な改善策

詳細説明参照

詳細説明

例えば以下のような場合に指摘となります。

  • 多要素認証が正しく実装されていない
  • ローカル認証が正しく実施されていない
  • 適切な認証認可が実施されていない

「多要素認証が正しく実装されていない」に関しては、例えば多要素認証が実装されていない場合や短時間で何度も認証試行が可能な場合などに指摘としています。また、SMSでのOTPも一般的に脆弱とみなされるため、危険度を下げた上で指摘としています。

また、「ローカル認証が正しく実施されていない」に関しては、例えば生体認証を利用する場合に LAContextevaluatePolicy()を利用している場合やバイパスが容易である場合に指摘となります。

evaluatePolicy()に関しては生体認証を実施したあと、その結果をClosure内でBoolにて返却します。そのため、認証結果は True または Falseです。あくまでも二値的なものであるため、当該メソッドによる結果が本当に信頼できるのか(正しいのか)検証する術がありません。また、攻撃者は特にアプリのリバースエンジニアリング等を行う必要もなく、 True を返却するだけで良いので、バイパスが非常に容易です。

このことから、後続処理に利用する何らかの値(ユーザー固有)をKeychainに格納し、それを生体認証によって読み出すことがもっともセキュアかつ容易な方法であると考えられます。

また、Keychainにアイテムを保存する際、制約を課すために SecAccessControlCreateFlags を設定しますが、この値が不適切なものである場合も指摘となります。

現在定義されているSecAccessControlCreateFlagsは以下の5つです。

  • kSecAccessControlDevicePasscode

    パスコードを使用してアイテムにアクセスできるようにする

  • kSecAccessControlBiometryAny

    生体認証を利用するように制約を課す。アイテム登録後に保存された生体での認証も許容する

  • kSecAccessControlBiometryCurrentSet

    生体認証を利用するように制約を課す。アイテム登録時点で保存されている生体情報でのみ認証を許容する

  • kSecAccessControlUserPresence

    基本的に生体認証を利用し、生体認証が機能しなくなった場合にパスコードでの認証を許容する

  • kSecAccessControlWatch

    Apple Watchでの認証を許容する

このうち、安全とみなせるFlagは kSecAccessControlBiometryCurrentSet のみとなります。たとえば、パスコードはショルダーハッキングによる窃取が比較的容易であるため脆弱です。また、 kSecAccessControlBiometryAny に関しても、アンロック状態での置き忘れなどの際、攻撃者によって攻撃者の生体情報が保存された場合に、Keychainアイテムへのアクセスが可能になってしまうため脆弱であるとみなされます。

なお、リモートエンドポイントでの認証についてはWebアプリケーション診断(Web API診断)と被る部分かと思いますので、診断対象システム全体の特性を考慮した上で、統一的に診断基準の策定を行います。スマホ診断とWebアプリケーション診断で診断結果が異なる場合、混乱を招くおそれがあります。

MASVS-NETWORK

MASVS-NETWORKでは、通信の安全性について精査します。

主な診断方法

Info.plist などを確認して、App Transport Securityが有効であるかなどを精査します。

また、証明書のピンニングを行っているか、実際にローカルプロキシーを通したりリバースエンジニアリングを行って精査します。

主な改善策

特別な理由がない限り、App Transport Securityを有効にして、安全な通信の利用を強制することが推奨されます。

また、必要に応じて証明書のピンニングを行うことが推奨されます。

詳細説明

例えば以下のような場合に指摘となります。

  • 平文通信が可能となっている
  • 証明書のピンニングを行っていない

「平文通信が可能となっている」に関しては、App Transport Security(ATS)が無効になっている場合に指摘となります。ATSとは、Appleの推奨するベストプラクティスに従わない通信を許容しない設定(セキュリティ機能)です。具体的な説明(基準)は公式ドキュメントを参照していただきたいのですが、例えばTLS1.2未満での通信やHTTP通信が遮断されます。

また、ATSは選択的に無効化することが可能です。例えばサードパーティのAPIがHTTP通信を行っている場合などに開発者が明示的に設定することによって、ドメイン単位での無効化を行うことができます。この場合でも指摘となりますが開発者が選択的にATSを無効化しているという点においてはATS自体を無効化するより幾ばくか良い設定と言えるでしょう。なお、ATSはCFNetworkなど、低レベルなインタフェースに対して行う呼び出しには適用されないことには注意が必要です。

また、証明書のピンニング(ピン留め)を行っていない場合も、このカテゴリでの指摘となります。証明書のピンニングとは、OWASPによって解説されているとおり、ホストに対してあらかじめ証明書の公開鍵を紐付けるプロセスを指しています。ピンニングを行うことで、中間者攻撃などによって、接続したホストが想定された証明書と異なる証明書を利用している場合に通信を遮断することができます。同様にローカルプロキシーツールの利用を防ぐことも可能です。

ただし、証明書のピンニングを行うことで、たとえば証明書更新のタイミングで通信が行えない、などの不利益が発生するおそれがあります。これは運用面の改善を行うことで対処が可能ですが、社内リソースの問題で対応が難しい場合や通信書き換えによる不利益がサーバー側で対処出来ている場合にはリスクを許容することも考えられます。

MASVS-PLATFORM

MASVS-PLATFORMでは、アプリとプラットフォームとのやりとりの安全性について精査します。

主な診断方法

Info.plist を確認し、アプリ権限の確認を行ったり、カスタムURLスキームの利用が行われているかを精査します。

また、マルチタスク画面に機微情報が表示されうるかや、WebView周りの実装(設定)などについても、動的解析やリバースエンジニアリングなどで精査します。

主な改善策

詳細説明参照

詳細説明

例えば以下のような場合に指摘となります。

  • 不必要なアプリ権限が有効
  • ユニバーサルリンクを利用可能な場面でカスタムURLスキームを利用している
  • パスワードの自動入力を行うことができない
  • マルチタスク画面にて機微情報が表示されうる
  • WebViewに設定の不備が存在する
    • 不必要にJavaScriptが有効化されている
    • JavaScriptにネイティブメソッドが不必要にexposeされている

「ユニバーサルリンクを利用可能な場面でカスタムURLスキームを利用している」に関しては、カスタムURLスキームが利用されている場合に一律で指摘を行います。Appleのドキュメントで言及されているとおり、カスタムURLスキームの利用は現在推奨されていません。その理由として、複数のアプリが同様のカスタムURLスキームを利用できることが挙げられます。

たとえば、App Storeで同様のカスタムURLスキームが設定されたアプリAとアプリBが存在すると仮定します。両方をインストールした状態でカスタムURLスキームを開いた場合、実際どちらのアプリが開かれるかはわかりません。そのため、偽アプリでカスタムURLスキームを開いた場合、そのパラメーター等が漏えいしてしまうおそれがあります(ただし、私の観測範囲では先にインストールした方で開く場合が殆どです)

また、マルチタスク画面にて機微情報が表示されうる場合も本項にて指摘となります。たとえば、アカウント情報画面に電話番号が表示される場合、マルチタスク画面にその画面のスナップショットが保存されてしまうこととなります。そのため、マルチタスク画面では他の画像などを表示し、機微情報が表示されないようにする必要があります。

くわえて、WebViewにネイティブメソッドが不必要にexposeされている場合や、必要ないにもかかわらずJavaScriptが有効である場合にも指摘事項になり得ます。これはWebView内で表示されるページでの遷移によって、攻撃者によるネイティブメソッドの実行やXSSなどが起こり得るためです。ただし、機能上必要である場合やそこまで大きな問題になり得ない場合もあるため、リスクベースでの判断を行っています。

MASVS-CODE

MASVS-CODEでは、コード品質(セキュアコーディングなど)について精査します。

主な診断方法

objectionradare2などを使用し、各種セキュリティ機能が有効となっているかを精査します。

また、リバースエンジニアリングなどによって、サードパーティライブラリの脆弱性有無やアプリアップデート機能の実装有無を確認します。

主な改善策

ビルド時のセキュリティ機能の利用を強制することが推奨されます。

また、サードパーティライブラリの利用時には定期的な脆弱性の存在確認などを実施することが推奨されます。

くわえて、脆弱性対策などのためにアプリの強制アップデート機能の実装が望まれます。

詳細説明

例えば以下のような場合に指摘となります。

  • アプリのアップデートを強制する仕組みが存在しない
  • サードパーティのライブラリに脆弱性が存在する
  • 以下のようなセキュリティ機能が有効でない
    • ARC(Automatic Reference Counting)
    • PIE(Position Independent Executable)
    • Stack Canary

まず、「アプリのアップデートを強制する仕組みが存在しない」に関してですが、アプリに脆弱性が存在していた場合、アップデートを配信したとしてもアップデート実施タイミングはユーザーに依存します。そのため、強制的なアップデートを実施させるために、サーバー側でアプリバージョンを確認する仕組みが実装されている必要があります。

また、ARC、PIE、Stack Canaryなどのセキュリティ機能が有効化されていない場合も指摘となります。こちらに関しては、Xcode上で設定を確認するか、objectionやradare2などを利用して確認すると良いでしょう。

MASVS-RESILIENCE

MASVS-RESILIENCEでは、解析耐性について精査します。

主な診断方法

ソースコードの確認やリバースエンジニアリングなどにより、難読化や脱獄検知の実施有無などについて確認します。

また、デバッグ用の不要なコードやファイルが残存していないか精査します。

主な改善策

詳細説明参照

詳細説明

例えば以下のような場合に指摘となります。

  • アプリがエミュレーターで動作する
  • 難読化が実施されていない
  • デバッグシンボルが残存している
  • デバッグ用のコードやファイルが残存している
  • アプリが利用するファイルのうち、重要な処理に用いられるものに対して整合性検証が行われていない
  • 脱獄された端末での動作でないか検証を行っていない

まず、「アプリがエミュレーターで動作する」に関して解説します。

現状、Appleシリコン搭載のMacでは、iOSアプリを動作させることが可能です(他にもほぼiOS端末実機として動作するエミュレーターも存在しますが、詳細説明は割愛します)

ビジネス上の判断としてエミュレーターでの動作を許容している場合は問題ないのですが、自動化が行われるおそれや動的解析などといった観点を考慮した上で、最低でもロギングを行うことが推奨されています。

また、アプリに難読化が実施されていない場合も指摘になり得ます。たとえば、ゲームアプリや金融系アプリなど、解析が行われること自体が望ましくない場合、アプリを難読化し静的解析の難易度を高めることが望ましいとされます。

ただし、難読化によってフローが複雑化されることは、クラッシュログ解析やパフォーマンス面でのデメリットが大きく、導入に躊躇する部分です。そのため、アプリの特性によって導入要否を判断することが重要となります。

くわえて、デバッグ用のコードやファイルが残存している場合も指摘となります。デバッグ用のコードが残存している場合、後述の脱獄によって、たとえ呼び出しを行っていなくとも攻撃者が該当する関数を実行することが可能です。また、そもそも不必要なコードを残しておくこと自体望ましいこととは言えません(当然、その影響範囲によって指摘のレベルを変更します)

脱獄検知については次項にて解説します。

 脱獄とは

脱獄とは、Exploitを利用しiOS端末に課されたさまざまなソフトウェア的制限を解除することを指します。

脱獄を行うことで、具体的には以下のことが可能になります(ただし、この定義は絶対的なものではありません)

  • コード署名検証を無視したすべてのアプリの実行
  • Dynamic Libraryのインジェクション
  • 管理者権限の取得
  • すべてのパーティション(ディレクトリ)へのR/W権限

アプリ提供側にとってのリスク

脱獄は、アプリを提供する側にとって、以下のようなリスクが発生する脅威となり得ます。

  • メモリ書き換えによる値の変更(ポイントの書き換えなど)
  • アセット書き換えによるUI/動作の変更(画像の置き換えなど)
  • Method Swizzlingによるアプリ動作の変更(Paywallの削除、本来課金が必要な処理の不正実行、デバッグ用コードの実行、未リリース機能の実行など)
  • 通常利用では起こり得ないバグへの対応工数の増加

脱獄環境では、Dynamic Libraries(Tweak)をシステムデーモンやアプリなどのプロセスやフレームワークなどにインジェクトすることで、メソッドの書き換えを行う事が可能です。

そのため、ローカルで処理を実行している場合、攻撃者は考えうるほぼすべてのことが実行可能となります。

また、サーバーサイドで処理を実行している場合でも、iOS自体のFrameworkやアプリのMethodをHookすることで、課金検証の回避やライセンス認証の回避、本来実行できない処理の不正実行などが可能になるおそれがあります。

脱獄のタイプについて

脱獄は大きく分けて4つに分類されます。

  • Untethered Jailbreak

    脱獄状態が永続化する。(再起動後も脱獄状態となる)

  • Semi-Untethered Jailbreak

    デバイスを再起動するごとに、サイドロードされたアプリなどによる脱獄が必要となる。(PCは必要とならない)

  • Semi-Tethered Jailbreak

    デバイスを再起動するごとに、PCによる脱獄が必要となる

  • Tethered Jailbreak

    デバイスの起動自体にもPCが必要となる(一般的ではない)

また、システム領域への書き込み可否によって、さらに2種類に分類されます。

  • Rootful Jailbreak

    Rootful Jailbreakではシステム領域がR/WでRemountされるため、書き込みなどが可能です

  • Rootless Jailbreak

    Rootless Jailbreakでは、システム領域が Read-only でマウントされます。 そのため、/Library 配下への書き込みなどはできません。また、システム領域への書き込みを行わないため、一般的に脱獄検知のハードルは上がります

脱獄検知について

先述のとおり、脱獄端末上でのアプリ動作はさまざまなリスクが存在します。

また、解析による実害(直接的な被害)のみではなく通常起こり得ないバグによる対応コストの増加なども考えられます。

くわえて、ユーザーにとってもリスクが存在します。脱獄アプリ(Tweak)はroot権限を持つことができますし、iOSの場合、Tweakに対してroot権限の暗黙的な取得が許可されています。そのため、マルウェアなどが混入した場合、MFAなどを設定していたとしても、アカウント侵害のリスクなど攻撃範囲が広がることとなり得ます。

なお、当然ですがアプリの要求する最低のOSバージョン(iOSで言うDeployment Target)を脱獄が出来ないOSバージョンにすることが最も有効な対策です(実際のところ困難な場面が多いと思いますが)

注意点

当然、脱獄検知実装の注意点も存在します。

たとえば、アセンブラなど低レベルな言語を用いて脱獄検知などを開発している場合、新たなアーキテクチャを採用した端末上でアプリが動作しないという問題があげられます。また、脱獄検知の実装ミスなどによって、正常な端末を利用しているユーザーに対しても悪影響を及ぼしてしまうおそれも存在します。脱獄検知はあくまでも不正利用防止が目的であり、正常な端末を利用しているユーザーの体験を阻害するものであってはなりません。

また、あくまで脱獄対策など、大枠でのDRMは解析者とのいたちごっこです。完璧な対策は存在せず、解析を遅らせることが目的であることを理解することが重要です。

実装方法について

ここでは、脱獄検知の実装について、いくつかの例を紹介します。

なお、以下のコードをそのまま利用した場合でもバイパスは容易です。実際には複数の手法を組み合わせたり、複数回呼び出しを行ったり、難読化を行ったりします。

また、ここではObjective-Cでの実装方法の解説は行いません。理由としては、Swiftと比べてリバースエンジニアリングの難易度が比較的低いことが挙げられます。なお、2023年11月現在、Swiftで書かれたクラスのメンバー変数をHookすることはできません。

ファイルベースの検知

脱獄環境に特異なファイルが存在しないかや、本来権限を持たない箇所への書き込み可否などをチェックします。

  • ファイルの存在チェック
if FileManager.default.fileExists(atPath: "/Applications/Cydia.app") {
    // デバイスが脱獄済み
}
  • パーミッションのチェック
do {
    let pathToFileInRestrictedDirectory = "/private/jailbreak.txt"
    try "This is a test.".write(toFile: pathToFileInRestrictedDirectory, atomically: true, encoding: String.Encoding.utf8)
    try FileManager.default.removeItem(atPath: pathToFileInRestrictedDirectory)
    // デバイスが脱獄済み
} catch {
    // デバイスがストック状態
}

実際は何らかの方法で疑わしいファイルパスのリストを持っておき、それらすべての存在確認を行います。

ただし、脱獄手法によってはシステム領域への書き込みを行わないことには注意が必要です。

くわえて、一部のファイルはバックアップに残存する可能性があります。そのため脱獄端末のバックアップを非脱獄端末にリストアした場合に影響がないか確認してください。(本来やりたいこととしては「脱獄したデバイスでアプリが動作しているかの確認」であり、バックアップに残るファイルを使用した検知はその目的を満たさず、副作用も大きいためです)

また、Objective-C/Swift両方で、内部的にC言語の関数の呼び出しを行っている場合が多いため、C言語の関数にhookされた場合には検知機構が回避されてしまうおそれがあります。

プロトコルハンドラーのチェック

脱獄環境に特異なディープリンクが存在しないかをチェックします。

if let url = URL(string: "cydia://package/com.example.package"), UIApplication.shared.canOpenURL(url) {
    // デバイスが脱獄済み
}
インジェクションチェック

プロセスに脱獄環境特有のライブラリがインジェクトされていないかチェックします。

private func isSuspiciousLibraryInjected() -> Bool {
    let suspiciousLibraries = [
        "Substrate",
        "libinjector.dylib",
    ]
    let imageCount = _dyld_image_count()
    for idx in 0..<imageCount {
        guard let cString = _dyld_get_image_name(idx) else { continue }
        let libraryPath = String(cString: cString)
        for library in suspiciousLibraries where libraryPath.localizedCaseInsensitiveContains(library) {
            print(libraryPath)
            return true
        }
    }
    return false
}

リバースエンジニアリングでの検知回避

攻撃者はリバースエンジニアリングを含めた静的解析や動的解析によって検知回避の手法を探ります。

そのため、難読化を施すことで、解析耐性を上げ、クラックされるまでの時間を引き伸ばすことが対策となり得ます(あくまでも完璧な対策は存在せず、いたちごっこであることに注意してください)

また、前項の検知手法の紹介ではObjective-Cでの実装方法の解説は行いませんでした。理由としては、Swiftと比べてリバースエンジニアリングの難易度が比較的低いことが挙げられます。なお、2023年11月現在、Swiftで書かれたクラス/メソッドへTweakをインジェクトすることは比較的困難です。

例えば以下のようなコードがあったとします。

import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack {
            Text(isJailbroken() ? "Your device is jailbroken" : "Your device is NOT jailbroken")
        }
        .padding()
    }
    
    private func isJailbroken() -> Bool {
        return FileManager.default.fileExists(atPath: "/var/jb/Applications/Sileo.app")
    }
}

#Preview {
    ContentView()
}

上記のコードでは、端末の/var/jb/Applications/Sileo.appが存在する場合、脱獄環境での動作と見なし、「Your device is jailbroken」と表示します。

そこで、攻撃者は分岐部分にパッチを当てるなどの方法で、ロジックの書き換えを行い、成功時のメッセージを表示しようとします(実際は検知した場合にはアプリを終了する場合が多いかと思いますが、あくまで例として記載しています)

この場合、検知回避の方法は大きく分けて以下の2つです。

バイナリへのパッチによる検知回避

上記のコードをリバースエンジニアリングツール「Ghidra」で読み込むと、以下のような画面が表示されます。

Ghidraでのリバースエンジニアリング

左側のウィンドウは逆アセンブル結果、右側はデコンパイル結果(擬似コード)を示しています。

ここでは、if文によって分岐が行われていることがわかります。

そのため、if ((uVar3 & 1) == 0) の分岐を書き換えることなどで、ロジックの改変が可能です。

ここに対応する部分が、左側のアセンブラでは以下の箇所となります。

10000404c 37 00 00 94     bl         _$s3jbd11ContentViewV12isJailbroken33_57CA94FE   int _$s3jbd11ContentViewV12isJai
100004050 00 01 00 36     tbz        w0,#0x0,LAB_100004070

なお、uVar3 には 0x10000404cで呼び出されたisJailbroken()の戻り値が格納されます。isJailbroken()をディスアセンブルした結果は以下のとおりです。

isJailbroken()の逆アセンブル結果およびデコンパイル結果

ここではアセンブラについて詳しく説明することはしませんが、TBZ(Test bit and Branch if Zero)を TBNZ(Test bit and Branch if Nonzero)にして比較を逆(この場合、非脱獄端末で脱獄検知が行われる状態になるので、恒久的なバイパスとしては適切ではありません)にしたり、JMPで目的のアドレスまで飛ばすことで、検知の回避が可能です。または、isJailbroken()の戻り値が常に0になるように、x0レジスタに0を格納してもよいでしょう。

また、脱獄済みの端末の場合、署名検証は無視されるため、このままアプリを配布されることでユーザーが海賊版のアプリを実際に動作させることができます。

Tweakによる検知回避

脱獄環境ではプロセスへのDynamic Libraryのインジェクションが可能です。また、インジェクトを行うことで、関数内部でトランポリンコードを呼び出し、関数を攻撃者の任意のコードに置き換えることができます。

また、この方法であればアプリ自体の改変や再配布は必要ないため、Tweakを利用するユーザーにとっても使い勝手が良いものとなっています。

たとえば、さきほどのコードの場合、FileManager.default.fileExists の引数(atPath)の値が /var/jb/Applications/Sileo.app の場合にFalseを返すようにするか、isJailbroken()が常にFalseを返すように、ロジックを書き換えることで検知の回避ができます。

また、さきほどのGhidraでの疑似コードを見てみると、FileManager.default.fileExistsでは内部的にObjective-Cが呼び出されている事がわかります。

objc_msgSend()が呼び出されている

そのため、以下のコードでObjective-Cの- (BOOL)fileExistsAtPath:(NSString *)path をHookすることで、検知を回避することが可能です。

#import <Foundation/Foundation.h>

%hook NSFileManager
- (BOOL)fileExistsAtPath:(NSString *)path {
  if ([path isEqualToString:@"/var/jb/Applications/Sileo.app"]) {
    return NO;
  }
  return %orig;
}
%end

また、先述の通り、Swiftで書かれたクラスのメンバー変数はHookできません。ただ、今回の場合メンバー変数を書き換える必要はないため、以下のコードで関数自体のHookも可能です。

#import <Foundation/Foundation.h>
#import <substrate.h>

int is_jailbroken_hook()
{
   return 0;
}

__attribute__((constructor))
int main(void)
{
   MSHookFunction(MSFindSymbol(NULL,"_$s3jbd11ContentViewV12isJailbroken33_57CA94FEEDBD5B920EEB5397744C45C2LLSbyFTf4d_n"), (void*)is_jailbroken_hook, NULL);
   return 0;
}

または、Swift/Objective-Cで内部的に呼び出されているC言語の関数やシステムコールにHookすることでもバイパスは可能です。

おわりに

今回は主にiOSに絞ってスマートフォンアプリケーション診断について解説しました。

ちなみに筆者は個人開発時代に難読化を行っていたにも関わらずライセンス認証部分がクラックされた経験があります。そのため、難読化や脱獄検知を過信せず、クラックされたらどうするか、クラックされないためにはどうするかを常に検討していくことも非常に重要だと感じています。(なお、このときはDMCA Takedown Requestやライセンス認証周りのアップデートによる対応を行いました)

Webアプリ診断に共通する観点も多く存在しますし、また、スマートフォンアプリ開発経験があれば取っつきやすい印象を持った方も多いかと思います。

この記事を読んで、スマホアプリ診断に興味を持っていただければ嬉しいです。

また、訂正・質問等ございましたら、Twitterにてご連絡いただけますと幸いです。

(セキュリティ本部でのお仕事に興味がある方も、お気軽にご連絡ください!)

おまけ : STORES Advent Calendar 2023の宣伝

本記事は STORES Advent Calendar 2023 6日目の記事でした!

他の記事もぜひご覧くださいー!