6/20

2026

macOSアプリが裏で情報を送ってないかを自分で確かめる方法 — 静的解析と動的キャプチャの2段構え

#macOS#セキュリティ#プライバシー#リバースエンジニアリング#mitmproxy#ネットワーク監査macOSアプリが裏で情報を送ってないかを自分で確かめる方法 — 静的解析と動的キャプチャの2段構え

macOSアプリが裏で情報を送ってないかを自分で確かめる方法

App Store や配布ページで落とした macOS アプリが、想定外の宛先に・想定外のデータ(クリップボード本体、個人情報、デバイスID 等)を裏で送ってないか。プライバシーポリシー1を信じる以外に、自分の手元で配布バイナリだけから反証する方法はある。

この記事では、ソースコードが手元になくても再現できる汎用手順を、静的解析 → 動的キャプチャの2段構えで書く。最後に、実際に自分で監査した2本(CopyHistory.app2 = 完全シロ、CopyClip.app3 = テレメトリあり・でもクリップボードは未送信)を対比に使う。

検査の問いは3つだけ

どのアプリでも、聞くことは同じ3つに分解できる。

問い何を確かめるか
どこへどのホスト・どのドメインに繋ぎ得るか/繋いだか
何をそのリクエストの本文に何が乗っているか
いつアイドル時に送るのか、特定の操作のときだけか

そして判断の核は、この3つを混同しないことだ。

送れる能力がある ≠ 送るコードがある ≠ 実際に送っている

com.apple.security.network.client4 の Entitlement5 があっても、StoreKit6(課金)や SFSafariViewController7(ヘルプを開く)のためなだけ、というのは普通に起きる。「ネット通信できるってことは情報抜いてるに違いない」と直感で黒く塗らない。逆も同じで、「サンドボックス8済みだから安心」も安心しすぎない。

層ごとに反証していく。黒い宛先 + 送信コード + 対象データの3本が1本に繋がったときだけ、初めて「クロ」と言える。

静的解析: 配布バイナリだけで4層を潰す

アプリを起動せず、/Applications/<App>.app の中身だけで進める。実行バイナリは Contents/MacOS/<Exec> にある。

層1: 能力(Entitlement と署名)

codesign -d --entitlements :- Contents/MacOS/<Exec>
codesign -dvvv Contents/MacOS/<Exec> 2>&1 | grep -E "Authority|TeamID|Identifier"
  • com.apple.security.app-sandbox があればサンドボックス済み。OS がファイル/IPC アクセスを制限してくれる安心材料
  • com.apple.security.network.client = 外向き通信が可能。「能力」であって「実行」ではない
  • Authority誰の署名かが分かる。Mac App Store 配布なら Apple Mac OS Application Signing、自作なら自分の Apple Development 証明書

ここで network.client が無ければ、そもそも外には出せない。終わり。

層2: 依存(送信SDKが入ってないか)

otool -L Contents/MacOS/<Exec>          # リンク先 framework 一覧
ls -la Contents/Frameworks 2>/dev/null  # 埋め込みサードパーティ
  • otool -L が全部 /System/Library/... で埋まっているなら、外部 SDK は無し
  • Contents/Frameworks が空なら、同梱の第三者ライブラリも無し
  • Firebase / Sentry / Crashlytics / Amplitude / Mixpanel / Adjust など、解析・送信系の SDK が見えたらそこからさらに調べる必要がある

層3: 宛先(URL と解析サービス名)

strings -n 6 Contents/MacOS/<Exec> | \
  grep -iE "https?://|api\.|analytics|telemetry|firebase|sentry|crashlytics|amplitude|mixpanel|posthog|googleapis" | \
  sort -u
ls Contents/Resources/*.plist && cat Contents/Resources/*.plist

⚠️ plist を必ず見る。 決定的な宛先がバイナリではなく plist に直書きされていることがある。strings だけで判断すると見落とす。Contents/Resources 配下の plist は全部目を通す。

層4: 送信コード + 積むデータ

strings -n 5 Contents/MacOS/<Exec> | \
  grep -iE "URLSession|dataTask|uploadTask|httpBody|application/json|multipart"

strings -n 4 Contents/MacOS/<Exec> | \
  grep -iE "feedback|email|version|operatingSystemVersion|deviceToken|device|payload"

Swift はシンボル9が比較的残るので、どのデータを送ろうとしているかを文字列から推測できる。ここで pasteboardclipboard といった対象データのシンボルが、送信系シンボル(URLSession.dataTask 等)と一緒に出てくれば、要警戒。

📝 雛形が残ってるだけ、もよくある。例えば didRegisterForRemoteNotificationsWithDeviceToken10 が見えても、Entitlement に aps-environment11 が無ければ実際には呼ばれない。能力と実行を必ず突き合わせる

静的解析で言えるのは「送り得るか」まで

ここまでで「送り得るか/その能力はあるか/宛先候補はどこか」までは固められる。だが、実際に何バイトがどのホストに飛んだかは、走らせて覗くしかない。最後の1%を埋めるのが動的編。

動的キャプチャ: 実トラフィックを2層で見る

動的検査では、宛先中身を別ツールで同時に張る。

見るものツール
宛先実際にどのホストへ・いつ繋いだかlsof / nettop
中身その通信の本文に何が乗ってるかローカル MITM プロキシ(mitmproxy12 等)

宛先だけ見ても、本文は TLS で暗号化されていて読めない。本文を読むには自分で復号する MITM13 が要る。

この2層は必ず両方張る。 理由は後述(落とし穴②)。

宛先と「いつ繋ぐか」を掴む

pgrep -fl <Exec>                                   # 起動中か / pid 取得
lsof -nP -iTCP -iUDP 2>/dev/null | grep -i <Exec>  # アクティブな接続のスナップショット
nettop -p <pid> -l 0                               # pid のトラフィックを継続監視(0 = 無限)
  • アイドル時に socket が無ければ → 常時垂れ流しはしてないという強い証拠
  • nettop を張ったまま対象の操作(「フィードバックを送信」ボタン等)を起こす → 「その操作のときだけ・どのホストに繋ぐか」が見える

ここまでで「いつ・どこへ」は掴める。ただし HTTPS の中身は読めない。

本文を平文で読む(HTTPS 復号)

ローカルに MITM プロキシを立てて、自分で復号する。

# 0) 準備(mitmproxy = CLI / 無料)
brew install mitmproxy
mitmweb --listen-port 8080   # 一度起動して ~/.mitmproxy/ に CA 証明書を生成 → Ctrl-C

# 1) 証明書を System キーチェーンに直 install(mitm.it 経由より確実)
sudo security add-trusted-cert -d -r trustRoot \
  -k /Library/Keychains/System.keychain ~/.mitmproxy/mitmproxy-ca-cert.pem

# 2) キャプチャ開始(mitmweb は起動しっぱなし。UI は http://127.0.0.1:8081)
mitmweb --listen-port 8080
networksetup -setwebproxy "Wi-Fi" 127.0.0.1 8080
networksetup -setsecurewebproxy "Wi-Fi" 127.0.0.1 8080

# 3) 動作確認(重要): 適当な https をブラウザで開き mitmweb に「復号済み」で出るか
#    CONNECT どまりで Body が読めない / TLS handshake failed → 証明書信頼が効いていない

# 4) 対象アプリで送信を「1回だけ」実行
#    → mitmweb のフィルタ ~u <host正規表現> で該当ホストに絞り、Request Body を平文確認
  • URLSession14(Foundation)はシステムプロキシ設定を自動で尊重するので、アプリ側に手を入れる必要はない
  • App Sandbox 下のアプリでも URLSession 経由ならシステムプロキシを通る
  • mitmweb のフィルタ ~u <host正規表現> で対象ホストだけに絞れる

後始末(必ず戻す)

networksetup -setwebproxystate "Wi-Fi" off
networksetup -setsecurewebproxystate "Wi-Fi" off
sudo security delete-certificate -c mitmproxy /Library/Keychains/System.keychain

MITM ルート証明書を残すな。 検証後すぐ削除する。残したままだと、自分の全 HTTPS 通信が傍受可能な穴になる(誰かに mitmproxy をローカル起動されたら全部読まれる)。入れ方と消し方は必ずセットで持つ。

動的検査の3つの落とし穴

ここが動的検査の山場であり、宛先や対象アプリに依存しない本質。

落とし穴①: 「最後に見えるリクエスト」≠「ペイロードを運ぶリクエスト」

リダイレクトや複数ホップで、本文が別のリクエストに乗っていることがある。 mitmweb で最新の POST を開いて Body が空、で「シロだ」と早合点しない。通信チェーン全体を見渡して、本文が積まれた POST を探す

実例: Google Apps Script15 へのフィードバック送信は、script.google.com への POST → 302 リダイレクトで script.googleusercontent.com へ飛ぶ。本文(フィードバックテキスト + アプリ版 + OS版)は最初の POST 側にある。後ろの GET だけ見て「Body 空」と読むと、何が送られたか分からないまま結論を出してしまう。

落とし穴②: HTTP プロキシは TCP しか見えない(QUIC/HTTP3 はすり抜ける)

これが動的検査で一番見落とされる点

mitmproxy が見るのは TCP 443(普通の HTTPS)まで。QUIC16 / HTTP317 は UDP 443 で動くため、HTTP プロキシは素通りする。proxy 画面が無音でも、実は UDP で送ってる、というのが普通に起きる。

対策はシンプル: proxy(中身)と nettop(全宛先)を同時に張り、差分を取る。

  • proxy にだけ出ている → TCP の本文がそこに乗っている
  • nettop にだけ出ている → proxy が捕まえられない通信(QUIC 含む)。「見えてないだけで送ってる」合図
  • 両方に同時に出てれば、その宛先と中身は対応している

救い: macOS でシステム web proxy を設定すると、CFNetwork18 は基本 TCP CONNECT に倒れて QUIC が無効化されることが多い(確信度: 中〜高)。それでも、保険として nettop は必ず併走させる。

落とし穴③: 証明書信頼を1つ間違えると、何も復号できない

System キーチェーンに信頼設定が乗っていないと、mitmproxy は CONNECT どまりで Body が読めないか、TLS handshake が失敗する。どちらも「中身が読めない」状態なので、シロかクロかの判断材料そのものが取れない。

入れ方の検証は手順 5-b の 3) でやる: 任意の https をブラウザで開いて、mitmweb 上で「復号済み」として Body が読めることを必ず確認してから、対象アプリの送信を実行する。動作確認をスキップして送信ボタンを押すと、「送ったのに proxy に出ない=シロ」と結論しがちだが、実は単に復号できていないだけ、というケースが起きる。

なお、独自に証明書ピンニング19を行うアプリは MITM が弾かれて接続失敗する。その場合は接続失敗として見える=それも情報だ(少なくとも素直なプロキシ傍受は不可、と分かる)。

実例: シロとグレーの対比

ここまでの手順を実際に2本のアプリに当てて差を見る。どちらも自分で監査した結果。

A. CopyHistory.app(自作)= 完全シロ

クリップボード履歴アプリ。静的解析の結果:

  • Entitlement: app-sandbox + network.client + files.user-selected.read-only
  • リンク先: 全部 /System/Library/...(Apple 標準のみ)。Contents/Frameworks は空
  • 文字列に出てくる外部宛先は script.google.com(フィードバック送信先)と Apple のヘルプ URL だけ
  • 解析 SDK の文字列は無し

動的:

  • 起動して放置 → socket 無し。アイドル時の送信は皆無
  • フィードバック送信を1回だけ実行 → script.google.com への POST 1本のみ。本文はそのときユーザが書いたフィードバック本文 + アプリ版 + OS版だけ
  • クリップボード本体は CoreData20 でローカル保存。送信系統に pasteboard 由来データが繋がっていない

ユーザ操作(フィードバック送信)がトリガーのときだけ・限定された本文だけ・1宛先。クリップボード本体や個人情報の裏送信は無い。完全シロ。

B. CopyClip.app(FIPLAB, MAS版 v1.9.86)= テレメトリあり・でもクリップボードは未送信

App Store で配布されているクリップボード履歴アプリ。

  • Entitlement: app-sandbox 済み・network.client あり
  • 同梱 Dragon.framework(同じ TeamID の自前共有コア)に実在の宛先が3つ:
宛先用途何を送っているか
api.stathat.com利用カウンタ起動回数、広告表示回数、CPUコア数 + デバイス UUID。StatHat21の API は「統計名+数値+ユーザID」しか積めない設計
api.mailchimp.comメール購読ユーザがメール入力したときだけ(opt-in)
api.fiplabcdn.com/happiness/評価/フィードバック促しWebView + JS ブリッジ
  • クリップボード本体はローカル保存copyclip.sqlite / CoreData)。動的キャプチャでも pasteboard 由来データが送信パケットに乗っているのは確認できなかった
  • org.nspasteboard.ConcealedType22 を尊重している(パスワードマネージャ等の秘匿コピーを履歴に取り込まない)。プライバシー配慮の積極材料

グレーな点:

  • デバイス UUID(IOPlatformUUID23)が付くテレメトリは完全匿名ではない
  • 広告関連の文字列(gad = Google Ads SDK 由来)の実宛先は静的では断定できない → ここは動的で必ず確認する必要がある

結論: 「能力あり・宛先あり・送信コードあり、でも対象データ(クリップボード本体)は送信コードに接続されていない」という静的解析の典型例。能力と実行を別個に潰さないと、「ネットに出てるからクロ」と早合点してしまう。CopyHistory との対比で、検査が層の問題であることがよく分かる。

教訓

層ごとに反証する姿勢を残す。これが宛先非依存・アプリ非依存の核。

やってはいけない読み正しい読み
network.client がある → クロStoreKit やヘルプを開くだけかもしれない
Framework に外部 SDK がある → クロ広告/課金/クラッシュレポート用かもしれない
宛先に怪しいドメインがある → クロ送信コードに繋がってない雛形かもしれない
サンドボックス済み → シロサンドボックス内で堂々と送ることもできる
proxy に何も出ない → シロUDP 443(QUIC)で送ってるかもしれない
Body が空の POST が見えた → シロリダイレクトで本文が別リクエストにあるかも

直感でクロと決めつけない/シロと安心しない。 静的で「何を送り得るか」を固め、動的で「実際に何バイト出たか」を1回の操作分だけ確認する。1回送れば十分で、送信前後の nettop の宛先差分を取れば、目的の通信以外の隠れ通信が無いかも同時に検証できる。

Footnotes

  1. プライバシーポリシー — アプリ提供者が公開する個人情報の取り扱い方針。書いてある内容と実装が一致している保証は無いので、自分で反証できる手段を持っておくと強い。

  2. CopyHistory.app — 筆者自作のmacOS用クリップボード履歴アプリ。

  3. CopyClip.app — FIPLAB Ltd. が提供するmacOS用クリップボード履歴アプリ。Mac App Store配布版 v1.9.86 (com.fiplab.clipboard) を監査対象とした。

  4. com.apple.security.network.client — App Sandbox 環境で外向きのネットワーク通信を許可する Entitlement キー。これが無ければサンドボックス内のアプリは外に出られない。

  5. Entitlement — アプリに与えられた権限の宣言。コード署名に組み込まれており、OSが実行時にチェックする。Sandboxやネットワーク権限、Keychainアクセス等を制御する。

  6. StoreKit — Apple の課金フレームワーク。In-App Purchase の処理に Apple のサーバーへの通信が必要なため、課金機能を持つだけのアプリでも network.client が必要になる。

  7. SFSafariViewController — アプリ内にSafariのWebViewを埋め込むためのフレームワーク。ヘルプページや利用規約を表示するだけでネット通信が発生する。

  8. App Sandbox — macOS / iOS のセキュリティ機構。アプリが触れるファイル、IPC、ネットワーク等をホワイトリスト方式で制限する。Mac App Store配布アプリでは必須。

  9. Swift シンボル — Swift コンパイラが生成する関数名や型名。Objective-C と違って多くは mangled name で残るが、strings でもクラス名やメソッド名の手がかりが拾えることが多い。

  10. didRegisterForRemoteNotificationsWithDeviceToken — リモート通知のデバイストークン受領時に呼ばれる UIApplication のデリゲートメソッド。

  11. aps-environment — リモート通知を有効にするための Entitlement。developmentproduction が入る。これが無いとどれだけコードに書いてあっても通知は実際には届かない。

  12. mitmproxy — オープンソースの HTTPS インターセプトプロキシ。CLI (mitmproxy)、Web UI (mitmweb)、Python API を持つ。brew install mitmproxy で導入できる。

  13. MITM(Man-In-The-Middle) — 通信の中継地点に割り込み、復号して中身を読み、必要に応じて改変もできる手法。自分の端末上で自分の通信に対して行う限りは合法な調査手段。

  14. URLSession — Foundation の HTTP クライアント API。デフォルトでシステムのプロキシ設定を尊重する URLSessionConfiguration.default を使うため、システム web proxy の設定だけで多くのアプリの通信が捕まる。

  15. Google Apps Script — Googleが提供するスクリプティング環境。Webhookとして公開すると script.google.com への POST が 302 で script.googleusercontent.com にリダイレクトされる構造になっている。

  16. QUIC — Google が開発した UDP ベースの伝送プロトコル。HTTP/3 のトランスポート層として採用された。TCP より低遅延だが、HTTP プロキシでは捕まえられない。

  17. HTTP/3 — QUIC 上で動く HTTP の最新版。Chrome/Safari/iOS は対応済み。アプリの URLSession でも URLSessionConfiguration で有効化されていれば使われる。

  18. CFNetwork — macOS / iOS の低レベルネットワーキングフレームワーク。URLSession の裏で動いている。システムプロキシ設定との連動はこの層で処理される。

  19. Certificate Pinning — アプリが特定の証明書(または公開鍵)のみを信頼するように実装する手法。攻撃者の MITM を防ぐのが目的だが、正当な調査でも MITM ができなくなる副作用がある。

  20. CoreData — Apple のオブジェクト永続化フレームワーク。デフォルトでは SQLite を裏に持つ。ローカル保存専用で、ネットワーク同期は CloudKit など別レイヤーが担当する。

  21. StatHat — シンプルな統計サービス。ezPostStat:withCount:forUser: 系の API は「統計名(文字列) + 数値 + ユーザ識別子」しか積めない設計で、任意のペイロードを乗せることはできない。

  22. org.nspasteboard.ConcealedType — NSPasteboard の慣習的な型。パスワードマネージャ等が「履歴に残してほしくないコピー」をマークするのに使う。クリップボード履歴アプリ側がこれを尊重するかどうかで、プライバシー配慮の度合いが分かる。

  23. IOPlatformUUID — IOKit から取れる Mac ハードウェア固有のUUID。ioreg -d2 -c IOPlatformExpertDevice で確認できる。テレメトリに付与すると「匿名」ではなく「擬似匿名」になる。