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が比較的残るので、どのデータを送ろうとしているかを文字列から推測できる。ここで pasteboard や clipboard といった対象データのシンボルが、送信系シンボル(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
-
プライバシーポリシー — アプリ提供者が公開する個人情報の取り扱い方針。書いてある内容と実装が一致している保証は無いので、自分で反証できる手段を持っておくと強い。 ↩
-
CopyHistory.app — 筆者自作のmacOS用クリップボード履歴アプリ。 ↩
-
CopyClip.app — FIPLAB Ltd. が提供するmacOS用クリップボード履歴アプリ。Mac App Store配布版 v1.9.86 (
com.fiplab.clipboard) を監査対象とした。 ↩ -
com.apple.security.network.client — App Sandbox 環境で外向きのネットワーク通信を許可する Entitlement キー。これが無ければサンドボックス内のアプリは外に出られない。 ↩
-
Entitlement — アプリに与えられた権限の宣言。コード署名に組み込まれており、OSが実行時にチェックする。Sandboxやネットワーク権限、Keychainアクセス等を制御する。 ↩
-
StoreKit — Apple の課金フレームワーク。In-App Purchase の処理に Apple のサーバーへの通信が必要なため、課金機能を持つだけのアプリでも
network.clientが必要になる。 ↩ -
SFSafariViewController — アプリ内にSafariのWebViewを埋め込むためのフレームワーク。ヘルプページや利用規約を表示するだけでネット通信が発生する。 ↩
-
App Sandbox — macOS / iOS のセキュリティ機構。アプリが触れるファイル、IPC、ネットワーク等をホワイトリスト方式で制限する。Mac App Store配布アプリでは必須。 ↩
-
Swift シンボル — Swift コンパイラが生成する関数名や型名。Objective-C と違って多くは mangled name で残るが、
stringsでもクラス名やメソッド名の手がかりが拾えることが多い。 ↩ -
didRegisterForRemoteNotificationsWithDeviceToken — リモート通知のデバイストークン受領時に呼ばれる UIApplication のデリゲートメソッド。 ↩
-
aps-environment — リモート通知を有効にするための Entitlement。
developmentかproductionが入る。これが無いとどれだけコードに書いてあっても通知は実際には届かない。 ↩ -
mitmproxy — オープンソースの HTTPS インターセプトプロキシ。CLI (mitmproxy)、Web UI (mitmweb)、Python API を持つ。
brew install mitmproxyで導入できる。 ↩ -
MITM(Man-In-The-Middle) — 通信の中継地点に割り込み、復号して中身を読み、必要に応じて改変もできる手法。自分の端末上で自分の通信に対して行う限りは合法な調査手段。 ↩
-
URLSession — Foundation の HTTP クライアント API。デフォルトでシステムのプロキシ設定を尊重する
URLSessionConfiguration.defaultを使うため、システム web proxy の設定だけで多くのアプリの通信が捕まる。 ↩ -
Google Apps Script — Googleが提供するスクリプティング環境。Webhookとして公開すると
script.google.comへの POST が 302 でscript.googleusercontent.comにリダイレクトされる構造になっている。 ↩ -
QUIC — Google が開発した UDP ベースの伝送プロトコル。HTTP/3 のトランスポート層として採用された。TCP より低遅延だが、HTTP プロキシでは捕まえられない。 ↩
-
HTTP/3 — QUIC 上で動く HTTP の最新版。Chrome/Safari/iOS は対応済み。アプリの URLSession でも
URLSessionConfigurationで有効化されていれば使われる。 ↩ -
CFNetwork — macOS / iOS の低レベルネットワーキングフレームワーク。URLSession の裏で動いている。システムプロキシ設定との連動はこの層で処理される。 ↩
-
Certificate Pinning — アプリが特定の証明書(または公開鍵)のみを信頼するように実装する手法。攻撃者の MITM を防ぐのが目的だが、正当な調査でも MITM ができなくなる副作用がある。 ↩
-
CoreData — Apple のオブジェクト永続化フレームワーク。デフォルトでは SQLite を裏に持つ。ローカル保存専用で、ネットワーク同期は CloudKit など別レイヤーが担当する。 ↩
-
StatHat — シンプルな統計サービス。
ezPostStat:withCount:forUser:系の API は「統計名(文字列) + 数値 + ユーザ識別子」しか積めない設計で、任意のペイロードを乗せることはできない。 ↩ -
org.nspasteboard.ConcealedType — NSPasteboard の慣習的な型。パスワードマネージャ等が「履歴に残してほしくないコピー」をマークするのに使う。クリップボード履歴アプリ側がこれを尊重するかどうかで、プライバシー配慮の度合いが分かる。 ↩
-
IOPlatformUUID — IOKit から取れる Mac ハードウェア固有のUUID。
ioreg -d2 -c IOPlatformExpertDeviceで確認できる。テレメトリに付与すると「匿名」ではなく「擬似匿名」になる。 ↩