- Texts.comチームは、自社と似たスタンドアロンのデスクトップアプリであるmacOS向けMeta Messengerを解析しようとしたが、証明書ピンニングのためプロキシベースのMITM解析が妨げられた
- Metaの証明書ピンニングは、アプリが許可した証明書のみを信頼するようにし、ユーザーが作成した認証局でリクエストを傍受して復号する方式を遮断する
- Fridaベースの動的計測はMessengerでクラッシュや配布の複雑さが大きく、チームはより小さく再現可能なバイナリパッチを選んだ
- Hopperでの解析の結果、
IsUsingSandbox()がtrueを返すように4バイトを書き換えると、custom sandbox使用時にSSL検証を無効化するコードパスに入れることが分かった
- パッチ済みの実行ファイルで元のバイナリを置き換え、署名を処理すると、プロキシツールでヘッダー、レスポンス本文、リクエスト情報を確認できるようになった
Texts.comがMessengerを解析した理由
- Texts.comでMetaプラットフォームプロジェクトを担当するBatuhan İçözは、macOS向けMessengerアプリが自社モデルに近いスタンドアロンのデスクトップアプリであり、解析する価値があると判断した
- ネットワークリクエストの傍受は参入障壁が低く、アプリの動作を理解する第一歩として活用しやすい
- しかしMetaはアプリに証明書ピンニングを適用してセキュリティモデルを強化し、ユーザーが自分自身を対象に行うMITM解析まで防いでいた
証明書ピンニングが防ぐもの
- プロキシクライアントでリクエストを傍受するには、ユーザーが作成した認証局を設定して信頼する必要がある
- その認証局が発行した証明書を通じて、リクエスト情報を傍受し復号できる
- サービスが証明書ピンニングを実装すると、アプリは特定の認証局が発行した証明書しか受け入れない
- この場合、ユーザー作成の証明書は無効になるため、リクエストを傍受できない
パッチ前の状態と目標
- 証明書ピンニングを無効化しない限り、すべてのリクエストが“Internal Error”を返す
- プロキシソフトウェアには“SSL Handshake Failed”と表示され、リクエストのライフサイクルが最後まで進行しない
- この状態ではリクエスト内容を推測しにくい
- 目標は、ネットワークデバッグツールでリクエスト、レスポンス、ヘッダーを直接読めるようにすることだった
失敗した回避策と最終的な選択
- 過去に動作した方法の1つは、バイナリ内のURL文字列をTLSを実装していない自前ホスティングのエンドポイントに置き換える方式だった
- このエンドポイントがクライアントとサーバーの間でリクエストとレスポンスを転送する
- Messengerのような大規模アプリより、小規模アプリに向いている
- Fridaのような動的計測ライブラリも候補だったが、Messengerでは安定性が低かった
- フックを仕掛けるとクラッシュが頻発した
- オーバーヘッドのせいで問題箇所を見つけにくかった
- 実行に必要な環境やツール構成があり、チームメンバーに配布するには複雑だった
- 長年保守してきたFridaスクリプトも試した
- このスクリプトは一般的な証明書ピンニングライブラリと回避手法に使われており、ほとんどのアプリで動作する
- Metaのアプリ群はその「ほとんど」には含まれなかった
- 最終的に、チームメンバーへ簡単に共有できるバイナリパッチで証明書ピンニングを完全に無効化する方式を選んだ
Hopperで見つけたパッチ箇所
- Messengerをダウンロードしてアプリケーションフォルダへ移動した後、
/Applications/Messenger.app/Content/MacOS/Messengerのコンパイル済みARMバイナリをHopperに読み込んだ
- Hopperはコンパイル済みバイナリの逆アセンブル、逆コンパイル、再コンパイル、デバッグ、可視化ができる
- バイナリと参照を読み込んだ後、
certificate、ssl、pinningのような語を検索した
"SSL pinning verification failed for host:"という文字列が解析の出発点になった
- コンパイル済みバイナリは過度に変更するとクラッシュする恐れがあるため、可能な限り小さな変更だけを適用する戦略を取った
- 理想的な変更は、ブール値の反転、条件文の反転、数個の命令の修正のように影響範囲が狭いパッチである
IsUsingSandbox()を常にtrueにする
- 制御フローグラフで実行フローを可視化し、接続された参照をたどって上流へ進んだ
"Using custom sandbox -> turn off SSL verification"という文字列を見つけた
- このフラグを決める関数の参照をファイル内で検索し、プロシージャ先頭でその参照を確認した
IsUsingSandbox()関数で戻り値が代入される地点を追跡した
w0レジスタはw19から移されて返される
w19は元々load byte命令で代入されていた
w19をロードせず常にtrueに設定すれば、IsUsingSandbox()はtrueを返す
- 先に見つけた文字列どおりなら、custom sandbox使用時にはSSL検証が無効になるため、この変更で証明書ピンニングが無効化される
Original
ARM: ldrb w19, [sp, #0x40 + var_20]
HEX: F3 83 40 39
Rewritten
ARM: mov w19, #1
HEX: 33 00 80 52
- この置き換えはhexadecimal modeでアプリケーションのバイトコードを直接修正して行った
実行結果と再署名
- Hopperの“Produce New Executable”オプションで新しい実行ファイルを書き出した
- 実行ファイルの署名を削除した後、元のMessengerバイナリを新しいバイナリに置き換えた
- Messengerを再起動すると、プロキシツールでヘッダー、レスポンス本文、その他のリクエスト情報が見えるようになった
- バイナリ全体のサイズ97,477,728バイトのうち、4バイトの修正だけでリクエストの傍受が可能になった
- iOSで似たアプローチを見たい場合は、Hassan Mostafaによる2020年のInstagram証明書ピンニング回避の記事を参照できる
- その記事は、脱獄したiPhoneで条件分岐命令を反転させてInstagramの証明書ピンニングを解除した事例である
- コンパイル済みバイナリはBatuhanに渡された
- Batuhanは署名証明書を入手してインストールした後、アプリケーションに署名した
- その後、自分のシステム上でそのバイナリを使い、自身のリクエストを確認できた
codesign --force --deep -s CERTNAME_OR_ID /Applications/Messenger.app
1件のコメント
Hacker News のコメント
似たような道をたどって、デコンパイル/修正/再コンパイルまでやろうとした瞬間に諦めた
ここまで来ると執念だし、実際に何時間かかったのか気になる。自分は打ち切り基準を決めていて、その通りにした
最初は2時間ほどいろいろなコマンドを変えて試してから諦めた。その後、リバースエンジニアの「Hassan Mostafa」(cyclon3)が以前に同じ方法で成功した記事、つまり iOS の Instagram に Hopper Disassembler を適用した記事を見て、その夜に再挑戦したが失敗した。同じコマンドも探して変えてみた
そこでやめることにし、数週間後、少ししこりが残った状態で思いつきで再び試したところ、サンドボックス関数を見つけてから30分ほどで終わった
eBPFを使えば、TLS 暗号化の前にデータを読めそう: Debugging with eBPF Part 3: Tracing SSL/TLS connections https://blog.px.dev/ebpf-openssl-tracing/
実際のアプリのトラフィックを傍受するプロキシへルーティングすると、目的によっては時間を大きく短縮できる。たとえば認証/セッション設定後に初めて発生するリクエストのパラメータを1つ自動的に変えたい場合、初期手順をすべて実行するクライアントを新たに書いたり、eBPF フィルタで修正ロジックを書いたりするより、アプリにそのまま進ませてプロキシで1か所だけ変えるほうがはるかに速い
rustlsを静的リンクした Rust プログラムには通用しないとても賢いやり方。ただ、サンドボックスモードでも証明書ピンニングを強制することはできた気がする
大学時代に Snapchat を中間者攻撃で覗こうとしたが、そこも証明書ピンニングを使っていて、結局突破できなかった記憶がある
エントリポイントすら見つけられなかった。比較的小さなソーシャルメディアアプリにしては、2015年の時点ですでにセキュリティがとんでもなく強かった
サンドボックスモードで証明書ピンニングを使っていたとしても、ピン留め証明書の検査を取り除く別の方法があった可能性が高い
今はもう違うけど
この記事を見ると +Orc の時代を思い出す。望まない分岐を見つけて NOP 化する方法のような、当時はよくある知識だったものがかなり失われた気がする
今は学ぶべき技術がずっと多いから、それも無理はない
[1]: https://en.m.wikipedia.org/wiki/Old_Red_Cracker
それでも NOP パッチをする人はまだ多いと思う。ただ複雑さが増しただけ。DRM を破ったり、任意のモバイルアプリを hex エディタなどで調べたりする人たちは今もいる
最近のプログラムはより複雑で始めるのは難しくなったが、一方で必要な知識にはよりアクセスしやすくなっている
Meta アプリのトラフィックを傍受したいなら、わざわざそんなことをする必要はない
https://www.facebook.com/whitehat/bugbounty-education/261571...
こうした修正を難しくするのに、ランタイムのバイナリチェックサムは役に立ったのだろうか
モバイルアプリでは標準的な慣行ではないのか? iOS や Android SDK はこうした機能を提供しているのか? 公式リリースプロセスと結びついて、それぞれの脱獄されていないプラットフォーム上で強制されるような形だと思う
初歩的な質問ではあるが、最終的な解法がバイナリの数バイトを修正することだったので、防げそうに見えた
脱獄されていないプラットフォームでは、通常は開発者証明書でこれを行う
Meta、少なくとも Messenger のリバースエンジニアリング対策はかなり緩そうに見える
高度な難読化まで行かなくても、プロダクションビルドから
IsUsingSandbox()を丸ごと削除するのは簡単だったはず証明書ピンニングは攻撃者が改ざんしにくくするためのもので、ユーザーがやりにくくするためのものではなかった
初めてアプリをクラックしたときは当然失敗すると思っていたが、実際にはこういうふうに修正しやすい JNE/JEZ ポイントを見つけるのは思ったより簡単だった
間違った場所を選んでも、元のファイルに戻して別の場所を試せばいい
こういうのは AI が簡単に自動化できそう。候補地点をいくつか選んで JEZ/JNZ を反転させ、アプリを実行して小言画面が出るかどうかを見るだけだから
失敗条件がきちんと定義されているなら、結局は候補を絞ればいい
AI が Denuvo のようなものをゼロショットで破れるなら、それはまた別の話だけど
これほど大きな企業のアプリケーションが、なぜ完全には難読化されておらず、改変されたバイナリの実行を防ぐ保護機構も十分に入れていないのか気になる
十分に熟練している、または動機の強い個人/集団/政府なら、結局は突破する。クライアントバイナリを配布するというのは、そもそもそういう性質のもの
防ごうとして時間とお金を莫大に使うこともできる。以前Pinterestが独自言語と仮想マシンを配布しようとしたことがあったが、私は反対した。あるいは、クライアントコードは基本的にすでに侵害されているものとして受け入れ、ロジックをサーバー側に置いて先に進めばよい
証明書ピンニングはほぼ無料に近く、「この鍵以上でないと搭乗不可」のような仕組み。安全というわけではないが、雑多な試みはふるい落とせる
もちろんリバースエンジニアリングにも影響はあるが、それはおまけに近い
結局コードはユーザーの端末上で実行され、ユーザーはコードが行うことを観察できるため、いつでも逆難読化が可能。一人が解いて結果を共有すれば、複製も非常に簡単になる。難読化が役に立たないという意味ではないが、あまり多くの時間を費やす対象ではない
攻撃者が端末に物理的にアクセスできるなら、その時点で防ぐ方法はない。できることは、手順をより面倒にして、うんざりして諦めることを期待するくらい
難読化はこの実験の結果にほとんど影響しなかったはずで、アプローチが動的インストルメンテーションを少し多めに使う方向に変わった可能性はある。私が見た中で最も効果的な難読化はVM難読化だったが、性能への影響がかなり大きい。難読化は通常のデバッグもより難しくする
改変されたバイナリの防止はシステムレベルで行われ、アプリケーションレベルでも実装可能で、一般的でもある。しかしその機能自体も迂回できるし、セキュリティチェックが終わったあとにFridaのような動的インストルメンテーションライブラリで改変することもできる
Metaにとって、リバースエンジニアたちといたちごっこをするのが最善だとは思えない
記事で使われたプロキシツールが何なのか気になる。実行中はすべてのアプリケーショントラフィックをそこへルーティングするのか?
くだらない質問だったら申し訳ない
macOSではすべてのアプリケーショントラフィックをそこへルーティングし、端末に自己署名証明書をインストールしてからプロキシに接続すれば、iOS端末もプロキシできる