アプリシールディングは、破られないように設計されているわけではありません。適切なスキル、ツール、時間を備えた熟練の攻撃者でなければバイパスできないほど、バイパスを困難にするよう設計されています。
その前提が変わり始めています。AIモデルは、経験の浅い攻撃者でも保護されたコードを理解し、バイパスを試みるのに必要なツールを操作できるよう手助けできます。
今週は、AIがアプリシールディングのバイパスにかかるコストをどこまで引き下げられるのかを見ていきます。
ほかにも、汚染されたRustのcrate、GitLabのクリティカルな欠陥に対する素早い悪用、実際に悪用されているMLflowの脆弱性、Copilot Personalの欠陥、Siemens PLCを探るAI生成スクリプト、バイナリ解析のためのangrなどを取り上げます。
この号の内容
🌡️ 脅威レベル:厳重警戒
⚡ ニュース:押さえておきたいセキュリティ関連の5つのニュース
🔎 ディープダイブ:縮小するアプリシールディングのバイパスコスト
🎣 これはフィッシング?
🔬 技術研究:本番環境で稼働する5つの銀行アプリのシールディングをバイパスする
🛠️ 今週のツール:angr
👤 今週の人物:Connor Tumbleson
📅 今週のイベント:Defcamp 2026
📚 今週の本:MacOS and iOS Internals
😅 今週のミーム
❓ 最後にひとつ質問
では、始めましょう。
今週の話題に入る前に、現在の脅威レベルを確認しておきます。
🌡️脅威レベル
⚡ ニュース
Rustのビルドを汚染するには86分で十分だった
Rustチームによると、arrayref、internment、append-only-vecの悪意あるバージョンがcrates.ioに登場したことから、メンテナーのコンピューターまたは認証情報が侵害された可能性が高いとのことです。汚染されたarrayrefのリリースはproc-macro1を取り込み、そのビルドスクリプトがコンパイル中にマルウェアをダウンロードしていました。Rustは86〜107分後にこれらのリリースを削除しました。RustSecはarrayref 0.3.10のダウンロードを2,285件記録しています。キャッシュとロックファイルを調べ、ビルドランナーを隔離し、影響を受けたビルド中に露出した認証情報をローテーションしてください。
GitLabへの悪用は公開から数日以内に始まった
クリティカルなGraphQLのコードインジェクションの欠陥であるCVE-2026-19478により、認証されていない攻撃者が、脆弱なセルフマネージドのGitLabインスタンス上の公開プロジェクトやユーザーデータを変更・削除できます。GitLabはパッチ適用済みのバージョンを8月17日にリリースしました。その後、watchTowrは自社のハニーポットに対する悪用を報告し、数分でこの問題を再現できたと述べています。GitLab.comとDedicatedはすでにパッチ適用済みでした。セルフマネージドの環境はただちにアップグレードし、そのうえでログから@gl_introducedリクエストを探し、影響を受けた公開リポジトリを確認してください。
クラウドメタデータの手前にあるAIプラットフォームが悪用されている
CISAのKEVカタログに、MLflowにおける認証不要のクリティカルなSSRFの欠陥CVE-2026-64849が追加されました。WebhookのリダイレクトやDNSリバインディングを悪用することで、攻撃者はサーバーに内部またはクラウドのメタデータエンドポイントへリクエストを送らせ、そのレスポンスを返させることができ、クラウドの認証情報が露出する可能性があります。3.15.0より前のMLflowが影響を受け、悪用も確認されています。3.15.0以降にアップグレードし、公開状態を解消し、Webhookのアクティビティを調べ、MLflowのホストから到達可能なシークレットをローテーションしてください。
Copilotのリンク1つで、連携アプリが情報窃取の経路になり得た
VaronisはCopilot Personalの3つの欠陥を公表しました。その中にはCVE-2026-24301も含まれ、細工されたリンク1つでプロンプトを自動実行させ、すでに認可済みのアプリに問い合わせ、その結果をCopilotのURL取得機能を通じて外部に持ち出せる可能性がありました。別の要約処理の経路では、永続メモリを汚染できる可能性もありました。この報告は一般消費者向けサービスを対象としたもので、Microsoft 365 Copilotで同じ挙動が起きることは確認されていません。Microsoftは8月18日にこれらの問題を修正しており、悪用は観測されていません。コネクターを監査し、OAuthのスコープを最小限に抑え、AIアシスタントのリンクを慎重に確認してください。
AI生成のエクスプロイトスクリプトが、物理プロセスを支える機械を探っている
米国の政府機関は、実際に進行中の脅威について警告しました。標的はSiemens S7 PLCと、より広範な産業環境です。攻撃者はインターネットスキャナーで露出したコントローラーを見つけ、snap7ライブラリを中心に構築されたAI生成のPythonスクリプトを展開して、PLCのメモリ、設定、ラダーロジックを読み書きしています。勧告はこの活動の帰属を特定しておらず、主に継続的な偵察と能力開発であると説明しています。コントローラーを棚卸しし、インターネットへの直接の露出をなくし、OTネットワークをセグメント化し、ファームウェアにパッチを適用し、異常なS7comm通信を監視してください。
🔎 ディープダイブ
縮小するアプリシールディングのバイパスコスト
OWASPは、アプリシールディングが破られないことは期待されていないと述べています。その目的は、バイパスを時間、リソース、専門知識の面で高くつくものにすることです。
では、そのコストを引き下げられるとしたら、どうなるのでしょうか。
NDSS 2026で発表されたリバースエンジニアリングの研究では、大規模言語モデル(LLM)を使った初心者は、コンパイル済みプログラムの動作を理解する能力が98%向上し、LLMを使わずに作業する専門家と同等の理解度に達しました。
EnIGMAなどの研究システムは、モデルにセキュリティツールや実行中のプログラムへの直接アクセスを与え、アクションを試し、その結果を観察し、次に何を試すかを決めさせることができます。
これは、AIモデルがシールディングのバイパスにかかるコストに直接影響を与えていることを示しています。
そこで問いはこうなります。
AIモデルは、アプリシールディングのバイパスにかかるコストをどこまで引き下げられるのか。
従来のアプリシールディングのバイパスプロセス
シールディングのバイパスは、大きく2つの段階に分けられます。
アプリケーションをリバースエンジニアリングし、その保護機能を特定して理解する
その理解を実際に動作するバイパスに落とし込み、保護機能が無効化されるまでアプローチを調整する
2018年の研究Honey, I Shrunk Your App Securityは、この両方の段階を実例で示しています。
まず研究者たちは、テスト対象のバージョンのPromon Shieldを手作業でリバースエンジニアリングしました。それがアプリケーションにどのように組み込まれるのか、保護機能がどのように動作するのか、そしてどうすれば無効化できるのかを調べたのです。
次に、その知見をNomorpに落とし込みました。影響を受けるアプリケーション全体でバイパスを自動化するツールです。
その後PromonはShieldを変更したため、Nomorpは恒久的なバイパスにはなりませんでした。
それでも、この事例は従来のプロセスをはっきりと示しています。まず特定の保護機能を理解し、次にその理解を使ってバイパスするという流れです。
従来、その最初の段階には経験豊富なリバースエンジニアが必要でした。コードを解釈し、それをアプリケーションの挙動と結びつけ、何が重要かを見極める必要があったからです。
いま縮小し始めているのが、この専門知識の壁です。
リバースエンジニアリングにおける専門知識の壁の縮小
同じNDSS 2026の研究Decompiling the Synergyは、2つの統制されたリバースエンジニアリング課題で、初心者24人と専門家24人を比較しました。
LLMの支援を受けると、次のような結果になりました。
初心者は、コンパイル済みプログラムの動作を理解する能力が約98%向上した
支援なしで作業する専門家と同等の成績を収めた
専門家の向上幅ははるかに小さく、初心者と比べて理解度の改善は限定的だった
これらの課題では、専門知識の差が大幅に縮まりました。
これが重要なのは、シールディングのバイパスが、保護されたアプリケーションを理解することから始まるからです。
この研究はシールディングされたモバイルアプリを対象にしたものではありません。しかし、リバースエンジニアリングの重要な一部であるコンパイル済みコードの理解に必要な専門知識が、大幅に少なくて済む可能性を示しています。
とはいえ、コードを理解しても保護機能が無効になるわけではありません。次の問いは、この優位性が、その理解を実際に動作するバイパスに変えるうえでも役立つのかどうかです。
保護機能の理解からバイパスの試行へ
攻撃者は保護機能を理解したあとも、それを回避する方法を見つけなければなりません。バイパスの候補を試し、アプリケーションの反応を確認し、保護機能が無効になるまで調整を続けます。
たとえば、アプリがroot化された端末を検知してアクセスをブロックするとします。攻撃者は、そのチェックが端末をroot化済みと報告しないようにすることを試みるかもしれません。それでもアプリがアクセスをブロックするなら、その理由を突き止め、バイパスを変更し、もう一度テストしなければなりません。
AIエージェントは、実在の銀行アプリで動作するシールディングのバイパスを作り出しました。
本番環境で稼働する5つの銀行アプリを対象とした評価では、AIエージェントを使ってシールディングの保護機能を特定し、そのバイパスを試みました。
あるアプリには、有効な保護機能が3つありました。起動時のrootとFridaのチェック、ネイティブコードを対象とする整合性チェック、そしてJavaのフック検知です。
エージェントはその3つすべてを改変しました。起動時のチェックがアプリを終了させないようにし、ネイティブコードを変更したあとで整合性の値を更新し、フック検知が計装を報告しなくなるよう変更したのです。
その後、アプリは1分以上にわたって認証済みセッションを維持し、複数の画面を移動し、バックグラウンドで繰り返し実行される整合性チェックも通過しました。アプリが動作し続ける間も、Fridaの痕跡は見える状態のままでした。
必要なツールが揃い、結果をはっきり観察できる場合、エージェントは高い成果を上げました。
エージェントは互いに連動する複数の保護機能を特定し、アプリのさまざまな部分にわたってコードを変更し、アプリケーションの実行中もその変更が有効であり続けるかをテストしました。また、保護されたコードを改変したあとに整合性の値を更新するなど、ある変更に合わせて別の変更を調整することもできました。
それでも、バイパスの過程でAIには人間の支援が必要でした。
エージェントに必要なツールが欠けているときは、人がそれを用意しなければなりませんでした。また、エージェントがタスクを誤解し、人間が軌道修正するまで間違った方向で試行を続けることもありました。
誤った道をたどると、コストはすぐに膨らみます。
別のElastic Security Labsの実験では、AI駆動のリバースエンジニアリングパイプラインが、難読化されたプログラムの最初のバージョンを12分未満、2.56ドルで解きました。
研究者たちがモデルの頼っていた近道を取り除くと、次の試行には約46分、8.83ドルかかり、それでも失敗しました。
これは、銀行アプリのバイパスで人間の導きが重要だった理由を示しています。修正がなければ、エージェントは結果の出ないアプローチに、さらに多くの時間とリソースを費やし続けかねません。
エージェントは、解析、改変、テストの多くをこなすことができました。信頼性が下がったのは、適切なツールがないときや、自らのアプローチが間違っていると気づく必要があるときでした。
では、バイパスのコストはどれだけ縮小したのか
AIはバイパスに必要な専門知識と手作業を減らしましたが、その削減効果はエージェントが正しい道を見つけられるかどうかにかかっています。
最も明確に測定された削減は、リバースエンジニアリングに表れています。NDSSの研究では、LLMの支援を受けた初心者の理解率が約98%向上し、支援なしで作業する専門家に並びました。
これは、バイパス全体のコストが98%下がったという意味ではありません。コストの大きな部分の一つ、つまりコンパイル済みコードを理解するのに必要な専門知識が、それらの課題で大きく下がったことを示しています。
銀行アプリの評価では、この削減がバイパスそのものにまで及び得ることが示されました。互いに連動する3つの保護機能を破るのに必要な解析、改変、テストの多くを、エージェントが担いました。人間は、欠けていたツールを用意し、うまくいかない試行を軌道修正し、最終結果を検証しました。
正しい道を見つけにくくなると、コストは再び上がります。
Elastic Security Labsの実験で見たように、モデルは難読化コードの課題の最初のバージョンを12分未満、2.56ドルで完了しました。研究者たちがモデルの使っていた近道を取り除くと、次の試行には約46分、8.83ドルかかり、それでも失敗しました。
つまり、失敗した試行は3.8倍の時間と3.4倍のコストをかけながら、何の結果も生みませんでした。これはシールディングのバイパスではなくリバースエンジニアリングの実験ですが、銀行アプリの評価で見られたのと同じ限界を測っています。エージェントは、人が介入するまで、間違った方向に時間とリソースを費やし続けかねないということです。
シールディングをセキュリティ制御として頼りにしているチームにとって、残存リスクは変わりました。
バイパスには、もはやすべての手順を専門家が手作業で行う必要はありません。残るコストは、ツールへのアクセス、エージェントの方向修正、結果の検証にますます集中しています。エージェントが進化するにつれ、こうした制約も弱まっていく可能性が高いでしょう。
🎣 これはフィッシング?
難易度:中
定例のAWSテストのために、デジタル署名された接続プロファイルが届きました。Connectをクリックしますか?
🔬 技術研究
本番環境で稼働する5つの銀行アプリのシールディングをバイパスする
モバイルアプリのシールディングとは、アプリケーション自体に組み込まれてコンパイルされる保護機能です。RASP、改ざん防止、アンチフッキング、root検知、ジェイルブレイク検知、実行時保護などの名前で販売されていますが、約束はどれも同じです。アプリがroot化された端末上、エミュレーター内、Fridaの監視下、デバッガーが接続された状態、あるいは再パッケージ化された状態にあると気づいたら、それを察知して対応する、というものです。
この約束には2つの側面があり、それぞれが独立して失敗します。
検知とは、何かがおかしいと判断することです。強制とは、それに対して何らかの措置を取ることです。
製品がrootを完璧に検知していても、アプリケーションがその答えを無視したり、すべての答えを一つの弱い分岐に集約したり、攻撃者が制御できる入力を信頼したりすれば、何も守れません。
当社は、4種類の商用シールディング製品を使用している、AndroidとiOSの本番環境の銀行アプリ5つを評価しました。検知は一貫して優れていました。破綻していたのは、一貫して強制の部分でした。
最初の症状:何も教えないように作られたクラッシュ
スキャンは最初のアプリをテスト端末にインストールして起動しました。5秒後、プロセスは消えていました。ダイアログもエラーもなく、ログにも何も残っていません。
クラッシュレポート:
signal 11 (SIGSEGV), fault addr 0x0
x0=0xdef040f0 x1=0x738416ae x4=0x2319d258 x5=0xbd7d47fb
x6..x30=0 sp=0 pc=0
tid: Thread-7x
シグナル11はSIGSEGV、つまりセグメンテーション違反です。プロセスが、カーネルの許可しない方法でメモリにアクセスしたということです。ここまではありふれたことです。ありふれていないのはレジスタダンプです。
ARM64では、pcはプログラムカウンターで、実行中の命令のアドレスを指します。spはスタックポインターで、コールスタックの基点となります。x0からx30は汎用レジスタで、引数、戻り値、ローカル変数を保持します。本物のクラッシュではこれらの値が残ります。だからこそクラッシュレポートは役に立つのです。プログラムカウンターは障害を起こした命令を示し、スタックポインターを使えばデバッガーで呼び出し元をさかのぼることができます。
ところがここでは、そのすべてがゼロです。調べるべき障害命令も、巻き戻すべきスタックもありません。唯一のバックトレースフレームは #00 pc 0x0 <unknown> となっています。
何者かが、プロセスを終了させる前に意図的にレジスタファイルを消去したのです。レポートに記載されたスレッドThread-7xは汎用のワーカーで、この判定とは何の関係もありません。また、0xdef040f0はこのプラットフォームではあり得そうなポインターではなく、保護コードが残した目印のように振る舞っています。
これは堅牢化されたアプリによく見られるパターンです。アプリは判定を告げる代わりに、終了する際にフォレンジック上のエビデンスを破壊します。そのため解析者は、どのチェックが発動したのかも、それがどこにあるのかも知ることができません。

汎用レジスタはすべてゼロで、バックトレースは不明なフレームが一つだけ。
これらの製品が実際に監視しているもの
最初の仕事は棚卸しです。これは何をチェックしているのか。
静的解析では、約30個のネイティブ検知関数が見つかりました。いずれもロード時に単一のJavaクラスに登録されています。
この細部は見た目以上に重要です。AndroidアプリはJNI(Java Native Interface)を通じてネイティブのCやC++のコードにアクセスします。ネイティブライブラリが関数をJavaに公開する方法は2つあります。一つは、Java_com_example_Foo_barのようなマングルされた名前でシンボルをエクスポートする方法で、ライブラリに対してnmを実行すれば誰でも見ることができます。もう一つは、JNI_OnLoadの中でRegisterNativesを呼び出し、実行時にメソッドを動的にバインドする方法です。
このライブラリは2つ目の方法を使っているため、検知関数の名前はどれもエクスポートされたシンボルとして現れません。見つけるには、登録テーブルを読むか、実行時にプロセスを監視する必要があります。
JNI_OnLoad @ 0x424264
checkHooks @ 0x424e50 scans the process memory map
checkForFridaAgent @ 0x424c38
isSuExists @ 0x424940
isFoundMagisk @ 0x425a90
isFoundDangerousProps @ 0x424810
isPermissiveSelinux @ 0x4248e8
getNativeSignature @ 0x42ca00 hashes the signing certificate
4つのカテゴリがあり、それぞれが異なる問いに答えます。
root検知は、端末が昇格した権限を与えている形跡を探します。ディスク上のsuバイナリ、Magiskの痕跡、書き込み可能なシステムパス、permissiveモードのSELinux、rootマネージャーのパッケージなどです。rootが重要なのは、それによって攻撃者がアプリのプライベートストレージを読み取り、プロセスにアタッチし、その実行時の挙動を改変できるからです。
フック検知は、実行時に関数の挙動を書き換えるフレームワークを探します。Frida、Xposed、LSPosed、Zygisk、Substrate、SandHook、Taichi、VirtualXposedなどです。フックはディスク上のAPKに手を触れずに関数の戻り値を変えられます。クライアント側のセキュリティチェックが破られるのは、まさにこの方法によってです。
checkHooksは/proc/self/mapsを読み取ります。LinuxとAndroidでは、このファイルに現在のプロセスにマップされたすべてのメモリ領域が、その権限と背後にあるファイルとともに列挙されます。そこに外部のライブラリや匿名の実行可能領域が現れれば、アプリは自身が計装されていると推測できます。
署名検証はgetNativeSignatureで、実行時にアプリの署名証明書のハッシュを計算します。AndroidではすべてのAPKに署名が必要です。アプリを改変して再パッケージ化する攻撃者は、通常は別の鍵で再署名しなければなりません。そのため、実行時の証明書を期待されるハッシュと比較すれば、単純な再パッケージ化は検知できます。
危険なプロパティの検知は、Androidのシステムプロパティを読み取ります。これはOSがビルドや端末の構成を公開するために使うキーと値のペアで、ro.build.tags、ro.debuggable、ro.secure、ro.hardware、ro.product.modelなどがあります。エミュレーターのイメージやエンジニアリングビルドは、そこに見分けのつく値を残します。
この有償製品の下には、無関係なベンダーによるさらに3つの制御があります。無料のオープンソースライブラリによる2つ目のrootチェック、アプリ自身のコンパイル済みDartコード内にある3つ目のチェック、そして4つ目のベンダーによるテレメトリです。マニフェストには <queries> エントリまで宣言されており、パッケージマネージャーがsu、Magisk、KingRootのパッケージに関する問い合わせに応答するようになっています。
一つのアプリに4つの独立した制御があり、同じ端末について4つの独立した見解を持っています。このことは最後に重要になります。
これは表面的な実装ではありません。弱点は検知の欠如ではありません。アプリが何を信頼し、その答えをどう扱うかにあります。
チェックを読むだけではうまくいかない理由
解析者とこのロジックの間には3つの層があり、3つともその役割を果たしています。
Java層はおとりです。アプリには、メソッド本体があるべき場所に4,129個の暗号化されたブロブが含まれており、起動時に初めて復号されて実行されます。メインアクティビティを含む重要なクラスは、ファイル内に読める形では存在しません。逆コンパイラーが示すのは構造といくつかの呼び出し箇所であり、挙動ではありません。
ネイティブライブラリはパックされています。保存時は暗号化され、エクスポートされたシンボルは一つだけで、実際の中身は起動後のメモリ上にしか存在しません。あるライブラリは、ランダム化されたファイル名とランダム化されたエクスポート名を持っています。
libGHDSDFIUPOIFDLS8DSFN23LK.so
export: _3Wbwdz5QepMbJNn8CiW3HwFivKZsZoNvu
そこでスキャンは、実行中のプロセスから復号済みのライブラリをダンプし、改めて調べました。それでもチェックは見つかりませんでした。
チェックは実行時に生成されます。復号されたコードは69回のシステムコールを発行します。ARM64でのシステムコールはsvc命令で、コール番号はレジスタx8に入るため、通常は逆アセンブラーでどのカーネル関数が求められているかを読み取れます。このうち64か所では番号が定数です。残りの5か所では、アプリが実行中に番号を計算するため、その5つは静的解析からは見えません。
その5つのうちの一つは、PROT_READ | PROT_WRITE | PROT_EXECで1ページを要求するmmapに解決されます。アプリはこれを約85ミリ秒ごとに行います。ページにコードを書き込み、実行し、破棄するのです。
書き込み可能かつ実行可能なメモリは、設計上めったに使われません。最新のシステムはこの2つの権限を分離しています。書き込んでから実行できるメモリこそが、コードインジェクションを可能にするからです。それでもパッカーやプロテクターはこれを使います。まさにこの目的、つまり安定したアドレスに安定したコードとして決して存在しないチェックを生成するためです。
文字列は保存されず、組み立てられます。完全に復号されたライブラリでqemu、goldfish、emulatorを検索しても、一件もヒットしません。アプリはこれらの文字列をスタック上で1文字ずつ組み立てるため、grepで探せる連続したテキストとしては決して存在しないのです。
メソッド名はUnicodeの紛らわしい文字です。別の製品で保護された2つ目のアプリは、Javaで同じ手口を使っています。逆コンパイラーはm13674と表示しますが、実行時の本当の名前はUnicodeの修飾文字ˎです。m13676と表示されるものの本当の名前はΙで、大文字のiとまったく同じように表示されるギリシャ文字の大文字イオタです。
逆コンパイラー上の名前を対象に書いたフックは、ただ発動しないだけです。エラーも失敗もなく、その沈黙を実際よりも強力な保護だと取り違えやすいのです。メソッドを名前ではなく型シグネチャで解決すればこの問題は避けられます。本記事の後半のフックが初回で成功しているのはそのためです。
アプリを外側から計測する
コードが読めないように作られているなら、読むのをやめて、その動きを観察しましょう。
スキャンはカーネルレベルのトレースをアタッチしました。これはプロセスの外側から観察するため、プロセス内に検知される痕跡を何も残しません。そのうえで、起動中にアプリが開いたすべてのファイルを記録しました。
opens ~150 property files, three times each
reads /proc/self/maps three times
reads /proc/self/status, /proc/self/comm, /sys/fs/selinux/context
opens the virtual device files: 0 times
opens any su or root path: 0 times
/procに馴染みのない読者のために説明すると、これはカーネルが必要に応じて生成する仮想ファイルシステムで、/proc/selfは呼び出し元プロセスが自分自身を見るためのビューです。mapsはマップされたメモリ領域を列挙します。statusはプロセスのメタデータを持ち、その中には現在このプロセスをトレースしているもののプロセスIDであるTracerPidが含まれます。commはプロセス名です。/sys/fs/selinux/contextは、プロセスが動作しているSELinuxのセキュリティコンテキストを報告します。
この結果で、すべての見方が変わりました。この種のチェックは、ディスク上のsuやエミュレーターのデバイスノードを探すものだと、ほとんど誰もが考えています。ところがこのチェックは、そのどちらも一度も開きません。起動時の判定は、プロパティの値、プロセス自身のメモリマップ、そしてトレース状態に基づいています。この3つのうち2つは、特権ユーザーが書き換えられるものです。
静的解析が生み出したのは、重要かもしれないチェックの一覧でした。トレースが示したのは、実際に実行されるチェックでした。
プロパティのゲートは、ファイルへの書き込み一つで崩れる
Androidは、/dev/__properties__/配下のファイルとして公開される共有メモリ領域を通じてシステムプロパティを公開しています。ライブラリは__system_property_getを通じてそれを読み取ります。値はメモリ上に保持されており、root化された端末では、アプリの起動前にその場で書き換えることができます。ゲートは、そこから読み取った値の整合性や真正性を何もチェックしません。
書き込み前:
signal 11 (SIGSEGV), fault addr 0x0
x0=0xdef040f0, pc=0, sp=0
crash report: GENERATED
書き込み後:
no crash report
process alive
UI drawn
native libraries loaded

強制終了は起きなくなります。APKへのパッチも、フックも、アプリへの改変も一切ありません。
だからといって、プロパティのチェックが無意味になるわけではありません。それが決定的な根拠にはならなくなるということです。root権限を持つ攻撃者が値を書き換えられるなら、アプリには独立した複数のシグナルによる裏付けか、一つのシグナルが反転しても崩れない強制の仕組みが必要です。
インデックスファイルが重要な理由
これは見た目以上に繊細な作業であり、その失敗の仕方は知っておく価値があります。
このディレクトリには2つの異なる構造があります。各プロパティを格納するprop_infoレコードと、プロパティの名前をそのレコードに解決するための、別個にシリアライズされたインデックスです。名前による検索はすべてこのインデックスを経由します。ファイル全体にわたって無差別に検索・置換を行うと、インデックスが破損します。
しかもその損傷は自らを隠します。プロパティの列挙はレコードを直接たどるため正常に動き続け、約460件のエントリを返しますが、個々のプロパティを名前で問い合わせると何も返りません。起動時にプロパティを読み取るライブラリはすべて壊れ、その壊れ方は直前に行った操作とは無関係に見えます。
そこでスキャンは、書き込む前に形状から本物のレコードを特定し、書き込み後は自らの作業を信用するのではなく検証します。
properties : 465 (was 465)
sdk : '31'
giveaways : 0
プロパティの数は変わらず、既知のルックアップは引き続き解決され、手がかりとなる値は消えている。そこまで確認して初めてアプリを起動します。環境を壊すことで成立するバイパスは、バイパスではありません。
Frida検知の正体はタイミング検知
Fridaは、この種の作業における標準的な動的計装ツールキットです。実行中のプロセスにエージェントを注入し、Javaメソッド、ネイティブ関数、システムコールをフックできます。
これらのアプリに対しては、Fridaは即座に強制終了させられます。一方、おそらくより侵襲的なデバッガーは、好きなだけ動かし続けられます。
debugger attaches 1.9s after launch -> ran 120 seconds, no complaint
Frida attaches 1.0s after launch -> killed
Frida launches together with the app -> killed
アプリは、起動の早い段階で/proc/self/statusを一度だけ読み取ります。このファイルの1行がTracerPidで、プロセスを何もトレースしていなければ0、そうでなければトレーサーのプロセスIDが入ります。デバッガーはptraceを使うため、この値を設定します。Fridaの注入もまた、スレッドを乗っ取ってエージェントをロードさせるために、短時間ptraceを使います。
チェックは、起動から約1秒後に一度だけ実行されます。当社はこれを地道な方法で確認しました。デバッガーをアタッチして処理を再開し、100秒間何もしないのです。判定は下されず、アプリは動き続けました。監視されていること自体はトリガーではないのです。
Fridaの問題は、その侵入の仕方にあります。注入は、起動時のこのウィンドウに2つの痕跡を残します。数百ミリ秒の間設定されるTracerPidと、エージェントが匿名ファイルからロードされた/proc/self/maps内の外部の実行可能マッピングです。判定は、エージェントスクリプトが最初の1行を出力する前に下されます。
{"c":7,"v":1,"p":{"id":107,"name":"Hooking Detected"}}
これを回避しようとFridaのスレッド名やポートを変更する人もいます。しかし何も変わりません。検知されていたのは、そもそも名前ではないからです。
つまり、アプリが問うているのは「これはFridaか」ではありません。むしろ「このウィンドウの間に、何かが自分をトレースしたり改変したりしていたか」に近いのです。これは異なる結果をもたらす別の制御です。チェックが既知の時点で一度だけ実行されるなら、タイミングがアタックサーフェスになります。チェックより前に入り込むことも、後から入り込むことも、プロセスを一時停止状態で起動し、アプリが最初の命令を実行する前に配置を済ませることもできます。スキャンは最後の方法を選びます。通常のアタッチでは決して見えないチェックに到達できるのはそのためです。
2つ目のアプリも、別の経路で同じ結論にたどり着きます。最初の命令からトレースすると、判定全体は、アプリが最後に行う4つの処理に集約されます。
openat(".../split_config.arm64_v8a.apk")
readlinkat("/proc/self/fd/60", ".../split_config.arm64_v8a.apk")
openat("/proc/self/status")
--- SIGBUS {si_code=BUS_ADRALN, si_addr=0x8f19c0bf} ---
+++ killed by SIGBUS +++
BUS_ADRALNを伴うSIGBUSはアライメント違反です。CPUがそのアクセス幅では受け付けないアドレスに対して、ロードやストアを行ったことを意味します。ARM64ではよくある偶発的なクラッシュではなく、障害アドレスは毎回まったく同じです。アプリは自身のトレース状態を読み取り、そのうえで意図的にアライメントのずれたアクセスを実行して、自らを終了させているのです。
一つの整数が8か所を決める
2つ目のAndroidアプリは、このセットの中で何よりも中心的な論点をよく示しています。
その保護は本格的です。ネイティブライブラリ内で暗号化された検知用の文字列、Java層で暗号化されたクラス名、Unicodeの紛らわしい文字によるメソッド名、そしてlibcを経由せずインラインのsvc命令として発行されるシステムコール。そのため、標準のsyscall()ラッパーをフックしても何も捕捉できません。
そのすべてが一つのJavaメソッドに集約されます。このメソッドにランダムな整数を渡すと、クリーンな端末では同じ整数がそのまま返ってきます。
アプリ内のすべての強制箇所が、この一つの値をチェックしています。
j/ma.java:480 if (rd.m13674(ctx, nextInt) != nextInt)
j/ma.java:551 if (rd.m13674(ctx, nextInt2) != nextInt2)
bk/a.java:844 m9379 = rd.m13674(ctx, nextInt) == nextInt ? -91 : 808;
bk/a.java:911 if (rd.m13674(ctx, nextInt2) == nextInt2)
ca/ma.java:2298 if (rd.m13674(ctx, r0) == r0)
ca/b.java:1331 if (rd.m13674(ctx, r0) == r0)
ca/a.java:816 if (rd.m13674(ctx, r0) == r0)
y/b.java:192,228 rd.m13674(ctx, SecureRandom.nextInt())

ランダムな整数は飾りではありません。これはノンスであり、最も手抜きな攻撃を防ぐために存在します。メソッドがクリーンな端末で単にtrueや0を返すのであれば、攻撃者はそれをフックして、その定数を常に返させればよいだけです。呼び出しのたびに新しいランダム値を渡し、それが返ってくることを要求すれば、期待される答えが毎回変わるため、定数を返すだけでは失敗します。
しかし、ここでは役に立ちません。そもそもメソッドをフックできる攻撃者なら、渡された引数をそのまま返せばよく、比較はすべての箇所で通過してしまいます。
3つのフックです。名前がUnicodeであるため、名前ではなく型シグネチャで解決しています。
{"type":"hooked","method":"m13674","sig":"int(android.content.Context,int)"}
{"type":"hooked","method":"m13676","sig":"boolean(java.lang.Exception)"}
{"type":"hooked","method":"m13677","sig":"int(android.content.Context,int,int)"}
{"type":"alive","pid":6285,"ping":1} ... {"type":"alive","pid":6285,"ping":18}

2分間の実行で、プロセスは安定したまま、クラッシュもなく、改ざん検知の経路は一度も発動しません。
失敗の原因は暗号ではなくアーキテクチャにあります。ネイティブ層は正しい判定を計算していました。ところがアプリケーションは、その判定を、プログラムの中で最も保護の弱い部分に置かれた、変更可能な一つの整数として信頼していたのです。
ピンニングのバイパスを本当に実証する方法
証明書ピンニングとは、アプリがOSのトラストストアだけに頼らないことを意味します。サーバーの証明書、あるいはその中の公開鍵が、アプリに同梱された値と一致するかどうかもチェックします。適切に実装されていれば、攻撃者が端末に独自のルートCAをインストールしても持ちこたえます。
しかし、ピンニングはアプリケーションのコードであるため、比較処理をフックしたりパッチを当てたりすれば破れます。
ほとんどのレポートは、これを弱い根拠で実証しています。フックがロードされた、プロキシに通信が表示された、アプリがクラッシュしなかった、といったものです。これらはバイパスと矛盾しませんが、ほかのいくつかの説明とも矛盾しません。
そこでスキャンは、アプリ自身の証明書チェッカーに対して統制された実験を行いました。チェッカーはアプリ自身のコンストラクターで生成し、アプリ自身のピンリストを読み込ませています。証明書は2回とも同じで、オブジェクトも同じ、ピンも同じです。唯一の変数は、フックが有効かどうかだけでした。
一方の実行では拒否され、もう一方では受け入れられ、それ以外はすべて固定されていました。目指すべき基準はこれです。「ツールがうまくいったと言った」ではなく、「保護された判定が、統制された条件下で変化した」ことです。テストはアプリが接続するのを待つのではなく、ピンニングのコードを直接動かすため、プロキシも通信のキャプチャも必要ありませんでした。
iOSでは、誰もレシートを確認しない
このiOSアプリは、ジェイルブレイクのゲート、2つの異なる言語で書かれた2つの独立したピンニング実装、35 MBの保護フレームワーク、そして複数のサードパーティSDKを備えています。
それなのに、自身のコードが改変されていないかを一度もチェックしません。
iOSにおける自己検証とは、SecStaticCodeCheckValidityのようなAPIやcsopsシステムコールを通じて自身のコード署名についてシステムに問い合わせるか、自身の__TEXTページのハッシュを計算して期待値と比較することを意味します。スキャンは、保護に関連する7つのバイナリすべてについて、この処理のあらゆる標準的な形を探しました。
main binary 36.8 MB self-check: NONE
Flutter framework 19.6 MB self-check: NONE
protection framework 35.0 MB self-check: NONE
attribution SDK 0.6 MB self-check: NONE
jailbreak detection 0.07 MB self-check: NONE
fingerprinting SDK 0.9 MB self-check: NONE
monitoring agent 5.9 MB self-check: NONE
難読化されたバイナリでは、シンボルがないことはほとんど何の証明にもなりません。シンボルは除去でき、呼び出しは生のシステムコール番号で行えるからです。そのため、スキャンはそこで止まりませんでした。保護フレームワークを逆アセンブルし、そのフレームワークがシステムコールを直接発行する1,761か所すべてを確認しました。コード署名を問い合わせている箇所は一つもありませんでした。
このフレームワークは、動作中のデバッガーや動作中の注入は見つけられます。しかし、自分自身のバイトが変わったことには気づけません。
そこで、バイトを変更しました。3つのパッチで、それぞれ数命令です。
Jailbreak check
sub sp, sp, #0x40 -> mov w0, #0 ; ret
Flutter TLS pinning
ldrb w0, [sp, #8] -> mov w0, #1
bic w8, w0, w0, asr#31 -> mov w8, #1
Payment SDK pinning, four places
mov w1, #2 -> mov w1, #1
ARM64の呼び出し規約では、w0はx0の下位32ビットで、x0は関数が戻り値を返すレジスタです。mov w0, #0に続くretは、ゼロを返す完結した関数であり、Objective-CではNOを意味します。つまり、ジェイルブレイクチェックのプロローグを置き換える8バイトによって、関数全体が「ジェイルブレイクされていない」を返すものになり、その後の処理は一切実行されません。
ピンニングのパッチも、一段下のレベルで同じように機能します。信頼の判定を報告するコールバックに、成功を報告させるのです。
興味深いのは3つ目のパッチです。アプリに不正な証明書を受け入れさせるのではなく、アプリが証明書を拒否する4つの箇所を変更しました。これらの箇所はチャレンジハンドラーに2、つまり接続のキャンセルを渡していました。それが今では1、つまりシステムの通常の評価へのフォールバックを渡します。正当な証明書を本当に受け入れる唯一の箇所には手を付けていません。
アプリは受け入れろと指示されることなく拒否をやめるため、その挙動は明らかに壊れたものではなく、もっともらしく見えます。
3つのパッチはいずれも、再パッケージ化と再署名を経ても残り、バンドル内の何からも反応はありませんでした。自身のレシートを一度も確認しないアプリであれば、まさに予想どおりの結果です。
シールディングは認可ではない
セットの中のあるアプリには、報告に値するシールディングの欠落はありませんでしたが、それとは関係なくクリティカルな脆弱性がありました。
アクティビティ(Activity)はAndroidアプリのエントリーポイントで、おおむね1つの画面に相当します。デフォルトではアプリ内に閉じています。exported=”true”を指定すると、ほかのアプリから起動できるようになります。インテントフィルターにBROWSABLEカテゴリを追加すると、アプリのカスタムスキームを使ったリンクをたどることで、Webブラウザーから起動できるようになります。
このアクティビティはエクスポートされ、ブラウザーから起動可能で、トランザクションリポジトリに書き込む前に認証、セッション、呼び出し元のチェックを一切行っていませんでした。
アプリのデータをすべて消去し、誰もログインしていない状態で、JavaScriptの1行だけで十分でした。
<script>
window.location.href = "app://quickpay?payee=BrowserAttacker&amount=200.00";
</script>
ブラウザーがその画面を起動しました。その後、トランザクション一覧には、ベースラインでは5件だったエントリが6件あり、攻撃者の受取人と金額が記録されていました。
これを捕捉できるシールディング製品はなかったでしょう。端末に不審な点が何もなかったからです。シールディングは、侵害された端末を攻撃するコストを引き上げます。クリーンな端末での認可の欠如に対しては何もできず、トランザクション自体に対するサーバー側のチェックの代わりにもなりません。
すべてのアプリに共通するパターン
検知技術は、どのアプリでも決して単純ではありませんでした。パックされたネイティブコード、1秒間に何度も新しいメモリに生成されるコード、libcより下のレイヤーで発行されるシステムコール、テキストとして決して存在しない文字列、JNIの動的登録、メモリマップのスキャン、署名のハッシュ計算、ラテン文字のふりをしたギリシャ文字のメソッド名、そして何も教えないように仕組まれたクラッシュ。
失敗はすべて、その一層内側にありました。
特権を持つ攻撃者が書き換えられるプロパティ値を信頼していた
すべてを決める、起動時の単一のウィンドウ
変更可能な一つの整数を読み取る8つの強制箇所
改変に気づく自己検証のないまま、パッチを当てられたiOSのコード
rootを正しく検知しながら、何も強制しなかったアプリ
そして最も鋭い発見は、バイパスですらありませんでした。これらのアプリの一つを、パッチもフックもプロパティの編集もない、ごく普通のroot化された端末にインストールしたところ、アプリはまっすぐログイン画面に進み、そのまま留まりました。シールディングはrootに気づき、正しく報告しました。それでもアプリは動き続けました。誰もその報告を聞いていなかったからです。
検知機能が答えるのは「何が見えるか」です。制御が答えるのは「何をするのか、そして攻撃者はその判定を変えられるのか」です。多くの導入事例は、前者には強く、後者には弱いのです。
自社アプリで実施すべき3つのテスト
検知ではなく、対応を確認する。アプリがrootを検知できるかどうかを問うのではありません。root化された端末に載せて観察してください。アプリは終了するのか、機密性の高いフローを制限するのか、認証をブロックするのか、それとも記録するだけで動き続けるのか。動き続けるなら、手に入れたのは制御ではなくテレメトリです。
強制箇所の数を数える。判定がどのように挙動に反映されるかをトレースしてください。すべてを決める一つのメソッドはあるか。答え全体を担う一つのブール値や整数はあるか。フックでそれを変えられるか。強制は機密性の高い操作の直前で行われるのか、それとも起動時に一度だけか。
特にiOSでは、自身のコードの整合性を検証する。アプリは自身のコード署名を問い合わせたり、自身のテキストページのハッシュを計算したりしているか。再パッケージ化と再署名を検知できるか。保護フレームワーク自体を検証しているか。フェイルクローズになっているか。それがなければ、ほかのすべての保護は、反転されるのを待つ一つの命令にすぎず、バンドル内の何もそれに気づきません。
評価の実施方法
モバイルシールディングは判断を誤りやすい分野です。ツールがロードされたことは実証になりません。クラッシュが消えたことも、常に実証になるとは限りません。プロキシに通信が表示されたことは、ピンニングのバイパスの実証にはなりません。手法は結果と同じくらい重要です。
バイパスの前にベースラインを取る。すべてのアプリは、まず何もアタッチしていない、root化されていないクリーンな端末で起動しました。動作するか。終了するか。終了するなら、最初に発動するのは何か。その答えが最も外側のシールドを示し、以降のあらゆる主張の基準になります。これがなければ、保護機能とバグを区別できません。
両方の層をマッピングする。パッケージの調査、逆コンパイル、逆アセンブルによって、前述の検知機能の一覧とオフセット、8つの強制箇所とその背後にあるメソッド、iOSのパッチポイント、証明書の検証ロジックを導き出しました。
端末の状態を問いに合わせる。ベースラインにはクリーンな端末、rootとプロパティの作業にはroot化された端末、ピンニングには通信傍受用の端末、通常のアタッチで見える前に発動するチェックには一時停止状態での起動、iOSの改ざんテストにはパッチを当てて再署名したビルドを使いました。一つの端末状態ですべての問いに答えることはできません。
一度に変える変数は一つだけ。プロパティの書き込み前後の同じアプリ。ピンニングのフック適用前後の同じ証明書。早いアタッチと遅いアタッチでの同じプロセス。パッチ適用前後の同じiOSバイナリ。
観察可能なセキュリティ上の効果を求める。検出結果として数えたのは、セキュリティに関わる結果が変化した場合だけです。終了していたアプリが動き続けた、UIが保護された状態に到達した、証明書の判定が反転した、リポジトリが認証されていない入力を受け入れた、あるいはパッチを当てたビルドが検知されずに動作した、といった場合です。経過時間は決して成功の基準になりません。「30秒間生き延びた」は観察ではなく、願望にすぎないからです。

評価が自らに課したすべての基準と、それに対して実際に得られた結果。
🛠️ 今週のツール
プログラムが取り得る挙動を調べるには、入力を一つずつテストする必要がある。
必ずしもそうではありません。
angrは、シンボリック実行を使って多数の取り得る入力を一度に表現し、どの条件がプログラム内の異なる経路につながるかを判定します。
angrは、コンパイル済みのソフトウェアを調べるためのオープンソースのバイナリ解析フレームワークで、ソースコードが入手できない場合でも使えます。
イメージとしては、次のようなものです。
Pythonで書かれた、逆アセンブラー+実行シミュレーター+制約ソルバー
何ができるのか
angrは、セキュリティ研究者にバイナリ解析のためのツール群を提供します。次のようなことができます。
関数を復元し、それらの接続関係をマッピングする
コードを逆アセンブル・逆コンパイルする
プログラムの実行をシミュレートする
メモリとレジスタを通じてデータを追跡する
コードのどの部分が結果に影響するかを解析する
特定の地点に到達するために必要な条件を判定する
それらの条件を満たす入力を生成する
シンボリック実行の仕組み
通常のプログラム実行では、固定された値を使います。入力が10なら、プログラムはその値によって生じる分岐をたどります。
シンボリック実行は、固定された入力を未知の値に置き換えます。プログラムが分岐に到達すると、angrは取り得る各結果の条件を記録します。
たとえば、ある分岐では入力が8文字より長いことが必要で、別の分岐では入力が特定の値で始まることが必要かもしれません。angrはこうした条件を集め、ソルバーを使ってそれらを満たす入力を見つけます。
疑わしいコードから再現可能なテストへ
ある研究者が、クローズドソースのファイルパーサーを調べているとします。
疑わしい関数を見つけたものの、そこに到達するのは、ファイル内のいくつかのフィールドが特定の値を持つ場合だけです。
angrを使えば、研究者は次のことができます。
バイナリをロードする。
調べたい関数の場所を特定する。
関連するファイルの内容を未知の値として表現する。
その関数に至る条件を探索する。
それらの条件を満たすファイルを生成する。
生成されたファイルは、統制された環境でテストして挙動を確認できます。
解析のカスタマイズ
angrは、固定されたスキャナーではなく、Pythonライブラリの集合として構築されています。
研究者は、解析をどこから始めるか、どのデータを未知として扱うか、プログラムのどの部分を調べたいかを決められます。
複雑な関数を単純化したり、ライブラリやOSとのやり取りをモデル化したりすることもできます。そのためangrは、さまざまなバイナリや研究課題に合わせて柔軟に使えます。
苦手とする場面
プログラムは、分岐のたびに取り得る経路を増やしていくことがあります。大規模なバイナリでは、その数が急速に増え、解析が遅くなることがあります。
ソフトウェアは、angrが自動的には再現できないファイル、ネットワーク、OSのサービス、ハードウェアに依存している場合もあります。研究者は多くの場合、範囲を絞り込み、欠けている部分をモデル化する必要があります。
代わりにならないもの
angrは自動の脆弱性スキャナーではありません。
逆アセンブラー、デバッガー、ファザー、そして結果を解釈できる研究者と組み合わせたときに、最も力を発揮します。それらのツールがプログラムを明らかにし、テストするのを助ける一方で、angrはプログラムが取り得る挙動をプログラマブルに調べる手段を提供します。
angrは、研究者がバイナリを調べる段階から、それが実際に何をできるのかをテストする段階へと進むのを助けます。
👏 今週の人物
Connor Tumbleson
今週は、Connor Tumblesonを紹介します。Apktoolを長年メンテナンスしてきた人物です。
Connorは2011年からApktoolをメンテナンスし、Androidとそのビルドツールの進化に合わせて互換性を保ってきました。リリースを管理し、互換性の問題を修正し、2026年にリリースされた大規模な刷新版であるApktool 3へとプロジェクトを導く役割も果たしました。
Apktoolを使うと、研究者はAndroidアプリケーションを読みやすいプロジェクト構造にデコードし、そのリソースやSmaliコードを調べ、変更を加えたあとに再ビルドできます。Androidのリバースエンジニアリングとモバイルセキュリティテストにおいて、欠かせないツールになっています。
Androidセキュリティで最も広く使われているオープンソースツールの一つを15年にわたってメンテナンスし、刷新してきた功績により、Connor Tumblesonを今週の人物に選びました。
📅 今週のイベント
DefCamp 2026
技術的なセキュリティ研究と実践的なチャレンジの組み合わせが好みなら、DefCamp 2026は注目する価値があります。
16回目を迎えるDefCampには、数千人のセキュリティ専門家、研究者、学生がブカレストに集まります。カンファレンスでは90人以上の講演者による技術講演、パネルディスカッション、ライブデモが行われるほか、Hacking Villageでのコンペティションも開催されます。
プログラムは、攻撃と防御のセキュリティ、AIの脅威、ソフトウェアサプライチェーン、クラウドセキュリティ、ランサムウェア、脅威インテリジェンス、暗号を扱います。メインカンファレンスに先立ち、11月16日〜18日には別途、実践的なワークショップが開催されます。
📍 ルーマニア、ブカレスト、国民の館(Palace of the Parliament)
📅 2026年11月19日〜20日
📚 今週の本
MacOS and iOS Internals
iPhoneやMacのインターフェースの下では、実際に何が起きているのでしょうか。
全3巻からなるMacOS and iOS Internalsシリーズで、Jonathan LevinはAppleのOSを内側から検証しています。本書は、プロセス、Mach-O、XPC、launchdといったユーザーモードのコンポーネントから、XNUカーネル、ファイルシステム、そしてコード署名やサンドボックスといったセキュリティの仕組みへと進んでいきます。
アプリケーション層より下のmacOSとiOSを理解したいセキュリティ研究者、リバースエンジニア、開発者にとって、徹底した技術リファレンスです。
😅 今週のミーム
AIエージェントがアプリシールディングをバイパスするのに専門家の助けをあまり必要としなくなるなら、攻撃者にとってバイパスを高くつくものにし続けるために、シールディングは何を変えなければならないのでしょうか。
The Breach Brief、Ostorlabチーム











